【Next.js】任意のクレジットカードでStripe決済

f:id:nekorokkekun:20191018181525p:plain:w1000
以前まとめたこちらの記事の続きとなります。

nekorokkekun.hatenablog.com
nekorokkekun.hatenablog.com
nekorokkekun.hatenablog.com

以前の段階で、StripeとFirestoreとFirebase Authenticationのユーザーを同時に作成し、登録したクレジットカードで単発決済を行うことに成功しました。

こちらの記事では、複数のクレカをFirestoreとStripeへ登録した上で、任意のクレジットカードでStripe決済ができるようにしていきます。

全体像の把握

f:id:nekorokkekun:20191018152325p:plain

以前の実装も含んでいるため複雑に見えますが、今回やるべきことは以下の通りです。

リポジトリのクローン

まずは今回使用するリポジトリをクローンしていきましょう。

$ git clone git@github.com:mesh1nek0x0/stripe-nextjs-on-firebase.git

ブランチの移動

現在、リポジトリをクローンした時点でmasterブランチにいます。
以下のgitコマンドを入力し、register-cardというタグのブランチを取得してください。

$ git checkout -b hogehoge register-card

hogehogeは、register-cardというタグの付くブランチをコピーする新たなブランチ名です。このブランチの命名はなんでも構いません。

そして次のコマンドを入力します。

$ yarn install
$ ✨  Done in 20.32s.

と出れば成功です。

Firebaseとの接続

次にクローンしたリポジトリを、事前に作成しておいたFirebaseのプロジェクトと接続させましょう。

Firebaseプロジェクトの作成については、以下をご覧の上、参考にしてください。
nekorokkekun.hatenablog.com

.env(新規作成)

apiKey=xxxxxxxxxxxxxxxxxxxxxxxx
authDomain=xxxxxxxxxxxxxxxxxxxxxxxx
databaseURL=xxxxxxxxxxxxxxxxxxxxxxxx
projectId=xxxxxxxxxxxxxxxxxxxxxxxx
storageBucket=
messagingSenderId=xxxxxxxxxxxxxxxxxxxxxxxx
appId=xxxxxxxxxxxxxxxxxxxxxxxx
stripeKey=xxxxxxxxxxxxxxxxxxxxxxxx

.envのstripeKeyに関しては以下が参考になります。
nekorokkekun.hatenablog.com


.firebaseerc

{
  "projects": {
    "default": "xxxxxxxxxxxxxxx"
  }
}

商品一覧・詳細画面の作成

商品一覧表示のページと、商品1つ1つの詳細画面表示ページを作成します。
pagesディレクトリ以下に、下記のディレクトリとファイルを作成しましょう。

products
 ┣ [product].js
 ┗ index.js

index.jsが商品一覧ページ、[product].jsが商品詳細ページとなります。
Next.jsでは、ファイル名を[ ]で囲うことでGETパラメータによる動的なルーティングが可能となります。

商品一覧画面の作成

まずは商品一覧画面を作成します。

pages/products/index.js

import React from "react";
import Link from "next/link";
import { firestore } from "../../lib/firebase";

function Products(props) {
  return (
    <>
      <h1>pages/products</h1>
      <Link href="/">
        <a>Go back to TOP</a>
      </Link>
      <ul>
        {props.products.map(product => {
          return (
            <li key={product.id}>
              <Link href="/products/[product]" as={`/products/${product.id}`}>
                <a>{product.pageName}</a>
              </Link>
              <ul>
                <li>{product.artistName}</li>
                <li>{product.monthlyFee}</li>
              </ul>
            </li>
          );
        })}
      </ul>
    </>
  );
}

Products.getInitialProps = async () => {
  const result = await firestore
    .collection("fanPages")
    .get()
    .then(snapshot => {
      let data = [];
      snapshot.forEach(doc => {
        data.push(
          Object.assign(
            {
              id: doc.id
            },
            doc.data()
          )
        );
      });

      return data;
    });
  return { products: result };
};

export default Products;

商品詳細画面の作成

次に商品詳細画面を作成します。

import React from "react";
import { auth, firestore } from "../../lib/firebase";
import Link from "next/link";

class Product extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isLogin: false,
      user: {}
    };
  }

  componentDidMount() {
    auth.onAuthStateChanged(authUser => {
      if (authUser) {
        const state = Object.assign(this.state, {
          isLogin: true,
          user: authUser
        });
        this.setState(state);
      } else {
        this.setState({
          isLogin: false,
          user: {}
        });
      }
    });
  }

  render() {
    return (
      <>
        <h1>pages/products/[product]</h1>
        <Link href="/products">
          <a>Go Back to Products List</a>
        </Link>
        <h2>PRODUCT DETAIL</h2>
        <ul>
          <li>PRODUCT NAME: {this.props.product.pageName}</li>
          <li>MONTHLY FEE: {this.props.product.monthlyFee}</li>
        </ul>
        <button>
          {this.state.isLogin
            ? `BUY as ${this.state.user.displayName}`
            : "PLEASE LOGIN"}
        </button>
      </>
    );
  }

  static async getInitialProps({ query }) {
    const result = await firestore
      .collection("fanPages")
      .doc(query.product)
      .get()
      .then(snapshot => {
        return snapshot.data();
      });
    return { product: result };
  }
}

export default Product;

特筆すべきポイントのみ解説をします。

ログイン状態か確認
  componentDidMount() {
    auth.onAuthStateChanged(authUser => {
      if (authUser) {
        const state = Object.assign(this.state, {
          isLogin: true,
          user: authUser
        });
        this.setState(state);
      } else {
        this.setState({
          isLogin: false,
          user: {}
        });
      }
    });
  }

コンポーネントがマウントされた段階で、アクセスユーザーがログインしているかどうかを確認しています。

ログインにはFirebase Authenticationを使用しています。
これは以前実装した通りなので、以下の記事をご覧ください。
nekorokkekun.hatenablog.com

ログインユーザーと未ログインユーザーで表示の出し分け
        <button>
          {this.state.isLogin
            ? `BUY as ${this.state.user.displayName}`
            : "PLEASE LOGIN"}
        </button>

こちらでは三項演算子を用いて、

  • ログインユーザーには、値段の表示されたボタンの表示
  • 未ログインユーザーには、「PLEASE LOGIN」というボタンの表示

を行っています。

商品情報の取得
    const result = await firestore
      .collection("fanPages")
      .doc(query.product)
      .get()
      .then(snapshot => {
        return snapshot.data();
      });
    return { product: result };

こちらは一般的なFirestoreのデータ取得のためのコードですが、注目すべきは

.doc(query.product)

です。

query.productには何が入っているかというと、GETパラメータのproducts/以降の乱数文字です。
そして、この乱数文字が何かというと、FirestoreのfanPagesの次層のドキュメント内に自動生成されたIDです。

GETのクエリパラメータに渡ってきた乱数文字に対応するIDのデータを取得し、pages/products/[product].jsへ引き渡しているというわけです。

Firestoreにデータの登録

これで商品一覧・詳細画面がそれぞれ作成されました。
しかし、まだ表示すべきデータが入っていないはずです。

そこでFirestoreのコンソールにアクセスし、データを直接登録していきましょう。
f:id:nekorokkekun:20191018165946p:plain
値は何でも構いませんが、データ取得後にリストレンダリングをするため、

  • fanPages(コレクション)
  • pageName
  • monthlyFee
  • artistName

はそのままで作成してください。

この状態で、
http://localhost:3000/products
にアクセスして見ましょう。

そうすると以下のような画面が表示されるはずです。
f:id:nekorokkekun:20191018165958p:plain

Firestoreに複数のドキュメントを登録すると、商品一覧数がそれに対応して増えていく仕組みです。

Payableコンポーネントの作成

次に登録したクレジットカードからどのカードで払うかを選択するためのPayableコンポーネントを作成していきましょう。

図で表すと、このような流れになります。
f:id:nekorokkekun:20191018171405p:plain

上記の図を以下のような流れで実現します。

  • クレジットカードをブラウザ上で選択
  • 選択されたクレジットカードのsourceを取得
  • 支払い確認 > 支払い確定の流れをhenadleChangeメソッドで作成

Payable.jsの作成

まずはPayable.jsコンポーネントを作成しましょう。

import { useState, useEffect } from "react";

function Payable(props) {
  // use hooks
  const [source, setSource] = useState({ key: "card", last4: "0000" });
  const [sources, setSources] = useState([
    { key: "dummy", last4: "choose payment card" }
  ]);

  // useEffect like componetDidMount
  useEffect(() => {
    const result = [
      { key: "card1", last4: "4242" },
      { key: "card2", last4: "5252" }
    ];
    // 読み込み終わったら、選択肢に反映
    setSources(result);
    // 取得できた最初の支払い方法をデフォルトに
    setSource(result[0]);
  }, []);

  const handleCharge = event => {
    event.preventDefault();
    alert(`${source.last4}(${source.key})で${props.amount}円はらいます`);
  };

  const handleChange = event => {
    const currentSource = sources.find(
      source => source.key === event.target.value
    );
    setSource(currentSource);
  };
  return (
    <>
      <form onSubmit={handleCharge}>
        <select value={source.key} onChange={handleChange}>
          {sources.map(value => {
            return (
              <option key={value.key} value={value.key}>
                {value.last4}
              </option>
            );
          })}
        </select>
        <button type="submit">Buy Now</button>
      </form>
    </>
  );
}

export default Payable;

こちらも特筆すべきポイントを解説していきます。

ReactHookの使用
import { useState, useEffect } from "react";

関数コンポーネントは従来、stateやライフサイクルフックを使用することができませんでした。
しかし、ReactHookを使用することでstateやライフサイクルフックと同様の機能を関数コンポーネントに実装できるようになりました。

上記のimportは、ReactHookのuseState、useEffectを使用するためのものです。

ちなみに、

  • useStateは、Stateと同様の機能の使用
  • useEffectは、componentDidMountと同様の機能の使用

を可能にするためのReactHookです。

stateを設定
  const [source, setSource] = useState({ key: "card", last4: "0000" });
  const [sources, setSources] = useState([
    { key: "dummy", last4: "choose payment card" }
  ]);

こちらでstateの実装をしています。

ダミーのクレカ情報を設定
  useEffect(() => {
    const result = [
      { key: "card1", last4: "4242" },
      { key: "card2", last4: "5252" }
    ];
    // 読み込み終わったら、選択肢に反映
    setSources(result);
    // 取得できた最初の支払い方法をデフォルトに
    setSource(result[0]);
  }, []);

現時点では、useEffectを使ってStateへダミーのクレジットカード情報を設定しています。

選択したカードをStateに設定
  const handleChange = event => {
    const currentSource = sources.find(
      source => source.key === event.target.value
    );
    setSource(currentSource);
  };

クレジットカードには1つ1つsourceが存在します。
このsourceには、Stripeがクレジットカードを特定するための情報が含まれていますが、セレクトボックスで選択したクレジットカードが「支払いで使用するカード」として認識されるために、Stateへ設定される形となります。

Payable.jsの埋め込み

作成したPayable.jsを[product].jsへ埋め込みましょう。

import React from "react";
import Payable from "../../components/Payable";
import { auth, firestore } from "../../lib/firebase";
import Link from "next/link";

class Product extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isLogin: false,
      user: {}
    };
  }

  componentDidMount() {
    auth.onAuthStateChanged(authUser => {
      if (authUser) {
        const state = Object.assign(this.state, {
          isLogin: true,
          user: authUser
        });
        this.setState(state);
      } else {
        this.setState({
          isLogin: false,
          user: {}
        });
      }
    });
  }

  render() {
    return (
      <>
        <h1>pages/products/[product]</h1>
        <Link href="/products">
          <a>Go Back to Products List</a>
        </Link>
        <h2>PRODUCT DETAIL</h2>
        <ul>
          <li>PRODUCT NAME: {this.props.product.pageName}</li>
          <li>MONTHLY FEE: {this.props.product.monthlyFee}</li>
        </ul>
        {this.state.isLogin ? (
          <Payable amount={this.props.product.monthlyFee} />
        ) : (
          "PLEASE LOGIN"
        )}
      </>
    );
  }

  static async getInitialProps({ query }) {
    const result = await firestore
      .collection("fanPages")
      .doc(query.product)
      .get()
      .then(snapshot => {
        return snapshot.data();
      });
    return { product: result };
  }
}

export default Product;

以下のような画面が表示されていれば成功です。
f:id:nekorokkekun:20191018173306p:plain

Pay nowボタンを押すとアラートウィンドウが表示され、OKを押すと支払う流れになります。

まだStripeと接続していないため、あくまでブラウザ上での動きとなります。

また、クレジットカード自体もダミーで表示させているもののため、sourceを保有しているものではありません。

Payable.jsで登録したクレジットカードを表示できるようにする

以下のようにPayable.jsを編集します。

import { useState, useEffect } from "react";
import { firestore } from "../lib/firebase";

function Payable(props) {
  // use hooks
  const [source, setSource] = useState({ key: "card", last4: "0000" });
  const [sources, setSources] = useState([
    { key: "dummy", last4: "choose payment card" }
  ]);

  // useEffect like componetDidMount
  useEffect(() => {
    firestore
      .collection("stripe_customers")
      .doc(props.currentUid)
      .collection("sources")
      .onSnapshot(
        snapshot => {
          let newSources = [];
          snapshot.forEach(doc => {
            const id = doc.id;
            newSources.push({ key: id, last4: doc.data().last4 });
          });
          setSources(newSources);
          setSource(newSources[0]);
        },
        () => {
          const state = Object.assign(sources, {
            sources: []
          });
          setSources(state);
        }
      );
  }, [props.currentUid]);

  const handleCharge = event => {
    event.preventDefault();
    alert(`${source.last4}(${source.key})で${props.amount}円はらいます`);
  };

  const handleChange = event => {
    const currentSource = sources.find(
      source => source.key === event.target.value
    );
    setSource(currentSource);
  };
  return (
    <>
      <form onSubmit={handleCharge}>
        <select value={source.key} onChange={handleChange}>
          {sources.map(value => {
            return (
              <option key={value.key} value={value.key}>
                {value.last4}
              </option>
            );
          })}
        </select>
        <button type="submit">Buy Now</button>
      </form>
    </>
  );
}

export default Payable;

この状態では画面上にエラーが表示されます。

FirebaseError: Function CollectionReference.doc() requires its first argument to be of type non-empty string, but it was: undefined

これは、Firestoreの stripe_customers の次層に当たるドキュメント「uid」に何も入っていない、もしくは適切な値が入っていないことで表示されているのです。

Payable.js 15行目

.doc(props.currentUid)

現状では、currentUidには何も入っていません。

そのため、[product].jsの45行目を以下の様に編集しましょう。

<Payable amount={this.props.product.monthlyFee} currentUid={this.state.user.uid} />

すでにログインした状態かつクレジットカードの登録が済んでいれば、問題なくクレジットカード情報が取得され、先ほどのダミー同様に表示されるはずです。

もしクレジットカード登録が済んでいない場合は以下を参考にしてください。
nekorokkekun.hatenablog.com

動作確認

Buy nowボタンを押すと以下の様なアラートウィンドウが表示されるはずです。
f:id:nekorokkekun:20191018174310p:plain

この(wJQ8UakCNS3py0qU)という乱数文字は、Firestoreの stripe_customers > uid > sources の層で登録されているクレジットカード固有のIDになります。

Firebaseへ決済情報の登録

次にFirebaseへ決済情報の登録を行います。
f:id:nekorokkekun:20191018174906p:plain

  • Buy nowボタンを押す
  • sourceのidが送信され、決済情報をFirebaseに送信する
  • chargeドキュメントに決済情報が登録される

という流れで実装します。

Payable.jsの編集

再びPayable.jsを編集します。

import { useState, useEffect } from "react";
import { firestore } from "../lib/firebase";

function Payable(props) {
  // use hooks
  const [source, setSource] = useState({ key: "card", last4: "0000" });
  const [sources, setSources] = useState([
    { key: "dummy", last4: "choose payment card" }
  ]);

  // useEffect like componetDidMount
  useEffect(() => {
    firestore
      .collection("stripe_customers")
      .doc(props.currentUid)
      .collection("sources")
      .onSnapshot(
        snapshot => {
          let newSources = [];
          snapshot.forEach(doc => {
            newSources.push({ key: doc.data().id, last4: doc.data().last4 });
          });
          setSources(newSources);
          setSource(newSources[0]);
        },
        () => {
          const state = Object.assign(sources, {
            sources: []
          });
          setSources(state);
        }
      );
  }, [props.currentUid]);

  const handleCharge = event => {
    event.preventDefault();
    alert(`${source.last4}(${source.key})で${props.amount}円はらいます`);
    firestore
      .collection("stripe_customers")
      .doc(props.currentUid)
      .collection("charges")
      .add({
        source: source.key,
        amount: parseInt(props.amount)
      });
  };

  const handleChange = event => {
    const currentSource = sources.find(
      source => source.key === event.target.value
    );
    setSource(currentSource);
  };
  return (
    <>
      <form onSubmit={handleCharge}>
        <select value={source.key} onChange={handleChange}>
          {sources.map(value => {
            return (
              <option key={value.key} value={value.key}>
                {value.last4}
              </option>
            );
          })}
        </select>
        <button type="submit">Buy Now</button>
      </form>
    </>
  );
}

export default Payable;

変更点のみを解説します。

sourceのidを取得
          snapshot.forEach(doc => {
            newSources.push({ key: doc.data().id, last4: doc.data().last4 });
          });

先ほど説明した様に、クレジットカード登録時にStripeにyとって、sourceというカード識別・支払い情報識別のためのデータが作成されます。

適切に支払いを行うには、使用するカードを指定した際に、Stateへ「このカードで、このように支払いをする」という情報を取得しなければなりません。

key: doc.data().id,

と記述することで、sourceのidを取得できるため、使用するカードのsourceも取得できるというわけです。

支払い情報をFirestoreへ登録

  const handleCharge = event => {
    event.preventDefault();
    alert(`${source.last4}(${source.key})で${props.amount}円はらいます`);
    firestore
      .collection("stripe_customers")
      .doc(props.currentUid)
      .collection("charges")
      .add({
        source: source.key,
        amount: parseInt(props.amount)
      });
  };

handleChargeのメソッドは先ほどもありましたが、こちらではFirestoreに単発決済の情報を登録するためのメソッドを追記しました。

これによって、stripe_customers > charges 以降の層に単発決済の情報が登録されることとなります。

動作確認

Buy nowボタンを押し、アラートボタンもOKを押したらFirestoreに決済情報が登録されているはずです。動作確認をしてみましょう。
f:id:nekorokkekun:20191018175945p:plain

ただ、この状態では

  • Stripeに決済情報が渡っていない
  • 決済情報としてsourceとamountしか持っていない

ため、まだ単発決済はできていません。

ちなみに「決済情報としてsourceとamountしか持っていない」という問題を解消すると、以下の様なデータが登録される様になります。
f:id:nekorokkekun:20191018180110p:plain

Firebase FunctionsでStripeに単発決済処理を送信

f:id:nekorokkekun:20191018180202p:plain

  • Firebase FUnctionsでStripeに決済情報を渡す
  • 完全な決済情報をFIrestoreに登録する

という2点を実装していきます。

Firebase Functionsの登録

functions/index.jsにcreateStripeChargeというFunctionを記述します。

// 上記省略
exports.createStripeCharge = functions.firestore
  .document("stripe_customers/{userId}/charges/{id}")
  .onCreate(async (snap, context) => {
    const val = snap.data();
    try {
      // Look up the Stripe customer id written in createStripeCustomer
      const snapshot = await admin
        .firestore()
        .collection(`stripe_customers`)
        .doc(context.params.userId)
        .get();
      const snapval = snapshot.data();
      const customer = snapval.customer_id;
      // Create a charge using the pushId as the idempotency key
      // protecting against double charges
      const amount = val.amount;
      const idempotencyKey = context.params.id;
      const charge = { amount, currency, customer };
      if (val.source !== null) {
        charge.source = val.source;
      }
      const response = await stripe.charges.create(charge, {
        idempotency_key: idempotencyKey
      });
      // If the result is successful, write it back to the database
      return snap.ref.set(response, { merge: true });
    } catch (error) {
      // We want to capture errors and render them in a user-friendly way, while
      // still logging an exception with StackDriver
      console.log(error);
      await snap.ref.set({ error: userFacingMessage(error) }, { merge: true });
      console.error(error);
      console.log(`user: ${context.params.userId}`);
    }
  });

function userFacingMessage(error) {
  return error.type
    ? error.message
    : "An error occurred, developers have been alerted";
}

書き込めたらFirebase Functionsにデプロイ していきます。

$yarn install
$ yarn deploy

もし以下の様なエラーが表示された場合は、使用しているnodeのバージョンと、functions/package.jsonのenginesに指定されているnodeのバージョンが異なるということです。

error functions@: The engine "node" is incompatible with this module. Expected version "8". Got "10.16.3"
error Commands cannot run with an incompatible environment.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

解決方法は以下を参照してください。
nekorokkekun.hatenablog.com

動作確認

先ほど同様、ブラウザからBuy nowボタンを押してみましょう。

その後、Stripeのコンソールにアクセスし、サイドメニューから「支払い」を押すと、単発決済が反映されているはずです。
またFirestoreのchargeドキュメント内にも、完璧な単発決済情報が登録されているはずです。

これで任意のクレジットカードでStripe決済が完了しました!お疲れ様でした!

【Next.js】FirebaseでStripeの単発決済の実装

f:id:nekorokkekun:20191014121813p:plain:w1000
以前まとめたこちらの記事の続きとなります。

nekorokkekun.hatenablog.com
nekorokkekun.hatenablog.com

以前の段階で、StripeとFirestoreとFirebase Authenticationのユーザーを同時に作成することができました。

こちらの記事では、Next.js上にFirebaseでStripeの単発決済の実装を行っていきます。

図解

f:id:nekorokkekun:20191014120030p:plain

上記の仕組みを以下の流れで実装していきます。

  • submitNewChargeというイベントを持った決済ボタン作成
  • ボタンを押すとFirestore上に stripe_customers > user_id > charges に決済情報が記録される
  • Firebase FunctionsのcreateStripeChargeが起動し、Stripe上に決済情報を送信
  • Stripe上で決済が行われる

ユーザーが行うのはボタンのクリックだけとなります。

実装

決済ボタン作成

まずは「submitNewChargeというイベントを持った決済ボタン作成」を行います。

イベントは以下の通りです。

    submitNewCharge = async (evt) => {
        evt.preventDefault();
        await
        db.collection('stripe_customers')
        .doc(firebase.auth().currentUser.uid)
        .collection('charges')
        .add({
            amount: 2000
          })
    }

また、ボタン自体は以下の通りです。

<button onClick={this.submitNewCharge}>決済</button>

amountを2000としていますが、実際には動的に商品の値段などをはめ込むシチュエーションが多いと思いますので、その場合は、以下のように記述しましょう。

    submitNewCharge = async (evt) => {
        evt.preventDefault();
        await
        db.collection('stripe_customers')
        .doc(firebase.auth().currentUser.uid)
        .collection('charges')
        .add({
            amount: this.props.detail.monthlyFee
          })
    }

createStripeChargeの作成

次にFirebase Functionsの作成を行います。

functions/index.js

exports.createStripeCharge = functions.firestore.document('stripe_customers/{userId}/charges/{id}').onCreate(async (snap, context) => {
  const val = snap.data();
  try {
    // Look up the Stripe customer id written in createStripeCustomer
    const snapshot = await admin.firestore().collection(`stripe_customers`).doc(context.params.userId).get()
    const snapval = snapshot.data();
    const customer = snapval.customer_id
    // Create a charge using the pushId as the idempotency key
    // protecting against double charges
    const amount = val.amount;
    const idempotencyKey = context.params.id;
    const charge = {amount, currency, customer};
    if (val.source !== null) {
      charge.source = val.source;
    }
    const response = await stripe.charges.create(charge, {idempotency_key: idempotencyKey});
    // If the result is successful, write it back to the database
    return snap.ref.set(response, { merge: true });
  } catch(error) {
    // We want to capture errors and render them in a user-friendly way, while
    // still logging an exception with StackDriver
    console.log(error);
    await snap.ref.set({error: userFacingMessage(error)}, { merge: true });
    return reportError(error, {user: context.params.userId});
  }
});

長いですが、要は「Firestoreの stripe_customers/{userId}/charges にドキュメントが作成されたら、その中から必要なデータを取得して、Stripeで決済処理をする」というだけです。

忘れずFunctionsをデプロイ しておきましょう。

$ cd functions
$cd yarn deploy

確認

実装自体は以上で完了です。実際にページから動作の確認をしてみましょう。

ボタンを押すとFirestoreとStripeに反映されています。
Firestore
f:id:nekorokkekun:20191014121257p:plain
Stripe
f:id:nekorokkekun:20191014121302p:plain

ちなみに以前の記事でご紹介したクレジットカード登録処理していない状態で決済を行うと、Firestoreに以下のように反映されます。
f:id:nekorokkekun:20191014121454p:plain

クレジットカード登録時にsourceが生成され、sourceの紐付きがある状態でなければStripeでの決済はできないからです。

ちなみにこのようにFirestoreに反映された決済はStripeには表示されません。

以上で単発決済の実装は完了です。お疲れ様でした!

【Next.js】StripeとFirestoreとFirebase Authenticationのユーザー同時削除

f:id:nekorokkekun:20191013185135p:plain:w1000
以前まとめたこちらの記事の続きとなります。
nekorokkekun.hatenablog.com

以前の段階で、StripeとFirestoreとFirebase Authenticationのユーザーを同時に作成することができました。

こちらの記事では、タイトルの通り、

StripeとFirestoreとFirebase Authenticationのユーザーを同時に削除

します。

図解

f:id:nekorokkekun:20191013183836p:plain

流れとしては簡単で、

  • コンソールのFirebase Authenticatonからユーザー削除
  • functionsのcleanupUserが起動
  • Stripeに紐づく顧客アカウントを削除
  • Firestoreに紐づくstripe_custmersの該当ドキュメントを削除

という感じになります。

一応、各コンソールを確認しておくと以下のとおりです。
Firebase Authentication
f:id:nekorokkekun:20191013184446p:plain
Firestore
f:id:nekorokkekun:20191013184459p:plain
Stripe
f:id:nekorokkekun:20191013184512p:plain

このように並べてみると、

uid => Firebase AuthenticationとFirestore
customer_id => FirestoreとStripe

とそれぞれつながっていることが分かりますね。

実装

以下のコードを追記しましょう。

functions/index.js

exports.cleanupUser = functions.auth.user().onDelete(async (user) => {
  const snapshot = await admin.firestore().collection('stripe_customers').doc(user.uid).get();
  const customer = snapshot.data();
  await stripe.customers.del(customer.customer_id);
  return admin.firestore().collection('stripe_customers').doc(user.uid).delete();
});

後はfunctionsディレクトリ内で以下のコマンドを実行すればOKです。

$ yarn deploy

Firebase FunctionsにcleanupUserがデプロイされたことを確認し、Firebaseのコンソール > Authentication からユーザーを削除してみましょう。

すると、cleanupUserが起動してStripeとFirestoreの紐づいたユーザーが削除されたはずです。

【React】関数コンポーネントにcomponentDidMount的なものを使いたい

Next.js(React)の関数コンポーネントにライフサイクルフックであるcomponentDidMount的な役割を持つ何かを実装したいときには、

useEffect(() => { ... } , []);

が使えます。

以下は一例です。

import { db }                from '../lib/db';
import React, { useEffect }  from 'react'; // importしておく
import Link                  from 'next/link';

import Header          from '../components/shared/Header';
import Footer          from '../components/shared/Footer';
import SiteDesctiption from '../components/index/Site-description';
import Wallpaper       from '../components/index/Wallpaper';

import GridList        from '@material-ui/core/GridList';
import GridListTile    from '@material-ui/core/GridListTile';
import GridListTileBar from '@material-ui/core/GridListTileBar';

import { makeStyles }  from '@material-ui/core/styles';

const useStyles = makeStyles(theme => ({

  popularArtists: {
    display:         'flex',
    flexWrap:        'wrap',
    justifyContent:  'space-around',
    overflow:        'hidden',
    backgroundColor: theme.palette.background.paper,
  },

  gridList: {
    flexWrap:  'nowrap',
    transform: 'translateZ(0)',
  },

}));

const Index = ({fanPages}) => {
    const classes = useStyles();
 // こんな感じで関数コンポーネントがマウントした際に行わせたい処理を書いておく
    useEffect(() => {
        console.log("hello")
      }, []);

    return (
        <>
            <Header />
            <Wallpaper />
            <SiteDesctiption />

            <div>
                <h3>アーティスト一覧</h3>
                <div className={classes.popularArtists}>
                    <GridList cellHeight={160} className={classes.gridList} cols={3}>
                        {fanPages.map(fanPage =>
                            <GridListTile key={fanPage.id} cols={fanPage.cols || 1}>
                                <Link href="/p/[detailid]" as={`/p/${fanPage.id}`}>
                                    <img src="../static/popular-artist-img.jpg" />
                                </Link>
                                <GridListTileBar title={fanPage.artistName} />
                            </GridListTile>
                        )}
                    </GridList>
                </div>
            </div>

            <Footer />
        </>
    );
}

Index.getInitialProps =

    async() => {
        let result =
                await
                    db.collection('fanPages')
                    .get()
                    .then(snapshot => {
                        let data = []
                        snapshot.forEach((doc) => {
                            data.push(
                            Object.assign({id: doc.id}, doc.data()))})
                        return data
                    }).catch(error => {
                        return []
    })

    return {fanPages: result}

};

export default Index;

【React】react-stripe-elementでフォームだけが表示されない

Next.js(React)でreact-stripe-elementを実装した際に、フォームだけが表示されないという現象が起こりました。

これは、Headタグ内で読み込んでいるstripe-jsのバージョンが不適合であったということでした。

<script src="https://js.stripe.com/v3/"></script>

を実装しなければならないところ

<script src="https://js.stripe.com/v2/"></script>

を実装していました。

react-stripe-elementでフォームだけが表示されない場合には読み込んでいるstrpe-jsのバージョンを疑ってみましょう!

【Next.js】Invariant Violation: React.Children.only expected to〜が吐き出される

こんなエラーが出た場合、

Invariant Violation: React.Children.only expected to receive a single React element child.

意味としては、「Reactのコンポーネントは、1つしかエレメントを内包できません」という意味となります。

        return (
            <StripeProvider stripe={this.state.stripe}>
                    <Head>
                        <script src="https://js.stripe.com/v2/"></script>
                    </Head>
                    <Header />
                    <div>
                        <h2>{detail.pageName}</h2>
                        <h4>概要</h4>
                        <p>{detail.body}</p>
                        <h4>アーティスト名 </h4>
                        <p>{detail.artistName}</p>
                        <h4>月額</h4>
                        <p>{detail.monthlyFee}円</p>
                    </div>
                    <Elements>
                        <CheckoutForm />
                    </Elements>
                    <Footer />
                    <style jsx>{`
                        div {
                            margin: 0 auto;
                            width: 80%;
                            height: 80%;
                        }
                        .fan-img {
                            display: block;
                        }
                    `}</style>
            </StripeProvider>
          );

ここでいうReactのコンポーネントは、

<StripeProvider stripe={this.state.stripe}>...</StripeProvide}>

のことで、その中のエレメントを何かしらの要素で1まとめにしてあげれば解決します。

以下のような感じです。

        return (
            <StripeProvider stripe={this.state.stripe}>
                <> // 追加
                    <Head>
                        <script src="https://js.stripe.com/v2/"></script>
                    </Head>
                    <Header />
                    <div>
                        <h2>{detail.pageName}</h2>
                        <h4>概要</h4>
                        <p>{detail.body}</p>
                        <h4>アーティスト名 </h4>
                        <p>{detail.artistName}</p>
                        <h4>月額</h4>
                        <p>{detail.monthlyFee}円</p>
                    </div>
                    <Elements>
                        <CheckoutForm />
                    </Elements>
                    <Footer />
                    <style jsx>{`
                        div {
                            margin: 0 auto;
                            width: 80%;
                            height: 80%;
                        }
                        .fan-img {
                            display: block;
                        }
                    `}</style>
                </> // 追加
            </StripeProvider>
          );

【Next.js】StripeとFirebaseにユーザー&クレカ登録同期処理ハンズオン

f:id:nekorokkekun:20191010233213p:plain:w1000
Next.jsで決済処理を実装したいという場合、FirebaseとStripeへ同時にユーザー登録をしたいという場合があるはずです。(厳密には、Stripeは顧客登録)

今回はStripeとFirebase Authenticationへのユーザー登録を同期させ、その後、クレジットカードを登録するという仕組みが体験できるハンズオンを進めていきます。

前提として

以下の項目については詳しく説明しませんので、理解しているという前提で進めていきます。

  • Firebaseの設定
  • Firebase Authentication
  • HOC(高階コンポーネント)
  • Stripeへのアカウント登録

また、以下の記事を参考にしていただければ、上記の前提知識についても身に付けられるかと思います。

nekorokkekun.hatenablog.com
nekorokkekun.hatenablog.com

全体の仕組み

複雑ですが、以下の仕組みで今回の実装を進めていきます。
f:id:nekorokkekun:20191010212532p:plain

ベースプロジェクトの準備

まずはベースとなるNext.jsのプロジェクトをgit cloneしましょう。

$ git clone git@github.com:mesh1nek0x0/stripe-nextjs-on-firebase.git

次にプロジェクト内に移動し、今回使用するブランチへ移動します。

$ cd stripe-nextjs-on-firebase/
$ git checkout start

Firebaseの準備

次にFirebaseのプロジェクトを作成し、Firebase SDK snippetをプロジェクトの設定から取得しましょう。

(例)Firebase SDK snippet

<!-- The core Firebase JS SDK is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/7.1.0/firebase-app.js"></script>

<!-- TODO: Add SDKs for Firebase products that you want to use
     https://firebase.google.com/docs/web/setup#available-libraries -->
<script src="https://www.gstatic.com/firebasejs/7.1.0/firebase-analytics.js"></script>

<script>
  // Your web app's Firebase configuration
  var firebaseConfig = {
    apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    authDomain: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    databaseURL: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    projectId: "practice-pf-funclub",
    storageBucket: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    messagingSenderId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    appId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    measurementId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  };
  // Initialize Firebase
  firebase.initializeApp(firebaseConfig);
  firebase.analytics();
</script>


次に.firebasercの中身を自身のFirebaseプロジェクト名に書き換えましょう。

{
  "projects": {
    "default": "practice-pf-funclub"
  }
}

.envをルートディレクトリに作成し、Firebase SDK snippetから取得したデータを以下のように書き換えましょう。

.env

apiKey=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
authDomain=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
databaseURL=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
projectId=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
storageBucket=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
messagingSenderId=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
appId=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
// stripeのパブリッシュキー(pkから始まるもの)も同時に設定
stripeKey=xxxxxxxxxxxxxxxxxxxxxxxxxxxx

次にlib/firebase/index.jsの設定に追記し、firestoreを使用できるようにしておきます。
lib/firebase/index.js

import firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";

const config = {
  apiKey: process.env.apiKey,
  authDomain: process.env.authDomain,
  databaseURL: process.env.databaseURL,
  projectId: process.env.projectId,
  storageBucket: process.env.storageBucket,
  messagingSenderId: process.env.messagingSenderId
};
if (!firebase.apps.length) {
  firebase.initializeApp(config);
}
const auth = firebase.auth();
const firestore = firebase.firestore();
export { auth, firestore, firebase };

Firebaseの料金プランを変更

実はFirebaseの料金プランがFreeのままの場合、Stripeのような外部へ通信することができません。

そのため、Firebaseのコンソールから料金プランを変更しておきましょう。

Blaze(従量課金制)だと、一定以下の通信は無料となるので、試しに使ってみる分にはこちらがおすすめです。

Firebase AuthenticationでGoogle登録を有効にする

Firebase Authenticationを使用してSNSユーザー登録を実装していきます。

そのため、FirebaseコンソールからFirebase AuthenticationのSNSユーザー認証におけるGoogleを有効にしておきましょう。

詳しくは以下の記事に記載されています。
nekorokkekun.hatenablog.com

StripeとFirebase Authenticationのユーザー登録同期

以下の順に処理を行っていきます。

  • Next.js上でサインイン
  • Firebase Authenticationでユーザーが作成される
  • Firebase functionsのトリガーが起動
  • Cloud Firestoreのstripe_customersコレクション以下にユーザーIDが保存される
  • Stripeの顧客に登録される

上記の流れを図にすると以下のようになります。
f:id:nekorokkekun:20191010212700p:plain

工程は複雑ですが、実際はユーザー登録のみで一気に全て行えるように設定していきましょう。

現状の確認

まずは現在のNext.jsの画面を確認してみましょう。

$ yarn install
$ yarn dev

これで http://localhost:3000 にアクセスしてみましょう。

すると以下のような画面が表示されます。
f:id:nekorokkekun:20191010212745p:plain

実際に使用するのは、「Sign in using google」の部分となります。

Firebase functions内の設定

まずはFirebase functionsの設定をしていきましょう。

Stripeモジュールの追加

functionsディレクトリ内でStripeモジュールを追加します。

$ cd functions/
$ yarn add stripe 
Firebase functionsにStripeのSecret API Keyを設定

次にFirebase functionsの環境変数にStripeのSecret API Keyを設定します。

firebase functions:config:set stripe.token="sk_test_xxxxxxxxxxxxxxxxxxxxxx"
✔  Functions config updated.

Please deploy your functions for the change to take effect by running firebase deploy --only functions
Firebase functions本体の設定

次にFirebase functions本体を設定しましょう。
functions/index.js

const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
const stripe = require("stripe")(functions.config().stripe.token);
const currency = functions.config().stripe.currency || "JPY";

exports.createStripeCustomer = functions.auth.user().onCreate(async user => {
  const customer = await stripe.customers.create({ email: user.email });
  return admin
    .firestore()
    .collection("stripe_customers")
    .doc(user.uid)
    .set({ customer_id: customer.id });
});

こちらは細かく解説をしていきます。

const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
const stripe = require("stripe")(functions.config().stripe.token);
const currency = functions.config().stripe.currency || "JPY";

この辺りは必要なモジュールやライブラリなどを読んでいます。

functions.auth.user().onCreate

「Firebase Authenticationでのユーザーをトリガーに以下の処理を行う」という意味になります。

const customer = await stripe.customers.create({ email: user.email });

「Stripeの顧客作成を行う上でStripeのユーザーのemail情報にはFirebase Authenticationのユーザーのemailを設定する」という意味になります。

こちらで出てくるawaitの後の「stripe」は、先ほどaddしたstripeモジュールから呼び込んでいるものです。

  return admin
    .firestore()
    .collection("stripe_customers")
    .doc(user.uid)
    .set({ customer_id: customer.id });

「Firestoreのstripe_customersコレクション > user.uidドキュメント以下にcustomer_idフィールドを作成して値を格納する」という意味になります。

このFirebase functionsを設定することで、ユーザー登録をすることによって一気にStripeとFirebase Authenticationにユーザー登録をし、Firestoreにcustomer_idを格納するというところまでの処理が進みます。

Firebase functionsのデプロイ

functions/index.jsの設定をFirebase functionsにデプロイ するために、functionsディレクトリ内で以下のコマンドを入力しましょう。

$ yarn deploy

これで「✔ functions[createStripeCustomer(us-central1)]: Successful update operation. 」と表示されればデプロイ 完了です。

StripeとFirebase Authenticationの同期確認

Next.jsのルートディレクトリに戻り、yarn devで再びlocalhost3000へアクセスしましょう。

ページに「Sign In using google」が表示されているはずなので、処理を進めてみましょう。

サインインが成功した旨のメッセージがブラウザ上のモーダルウィンドウで確認できたら、Firebase AuthenticationとStripeのそれぞれのコンソール上で登録ができているかどうかの確認をしてみてください。(Stripeは顧客登録の反映に若干のタイムラグがあります)

StripeのtokenをFirestoreに格納する

Stripeは、顧客のクレジットカード情報を私たちの代わりに保管してくれます。
そして登録されたクレジットカード情報を操作するためにはtokenを発行しなければなりません。

tokenの説明についてわかりやすい解説があったため引用します。

Stripeによるクレジットカード決済では何よりも”トークン”を取得する事が重要となります。

サービス提供者側では直接クレジットカードの情報を取得・管理する必要はなく、Stripe側でそれらを全て管理してくれます。
その際、Stripeが取得したクレジットカード情報を操作する為に実用となるのがこの”トークン”となります。


単発の決済であれば決済する際にクレジットカード情報をStrip経由で取得し、その取得した”トークン”に対して課金処理を行う事で実現します。
一方で定期的な課金を行う場合、取得した”トークン”情報を”顧客(Customer)”に結びつけ、その”顧客(Customer)”に対して課金処理を行う事で実現する事ができます。

このように、Stripeでクレジットカード決済を実装する場合、何よりもこの”トークン”の取得と管理について理解する必要があります。
引用元:
https://erorr.org/104/#i

以下のような流れでこちらの処理を実現します。

  • Next.jsでクレジットカード情報を入力
  • Stripeからtokenを発行
  • Firestoreへtokenを格納

上記の流れを図にすると以下のようになります。
f:id:nekorokkekun:20191010221126p:plain

クレジットカード登録ページの作成

まずはクレジットカードを登録するフォームがあるページを作成しましょう。

最初にReact上でStripeを使用する際の依存パッケージのインストールを行います。

$ yarn add react-stripe-elements

components/CheckoutForm.js

import React, { Component } from "react";
import { CardElement, injectStripe } from "react-stripe-elements";

class CheckoutForm extends Component {
  constructor(props) {
    super(props);
    this.submit = this.submit.bind(this);
  }

  async submit(ev) {
    const { token, error } = await this.props.stripe.createToken();
    this._element.clear();
    console.log(token);
  }

  render() {
    return (
      <div className="checkout">
        <p>Would you like to complete the purchase?</p>
        <CardElement onReady={c => (this._element = c)} />
        <button onClick={this.submit}>Purchase</button>
      </div>
    );
  }
}

export default injectStripe(CheckoutForm);

こちらは、Stripeのクレジットカード入力フォームコンポーネントです。

exportの際に「injectStripe」というHOCの引数にCheckoutFormコンポーネントを入れています。

このinjectStripeは公式によると…

injectStripe高次コンポーネント(HOC)を使用して、エレメントツリーに支払いフォームコンポーネントを構築します。

injectStripe HOCは、Elementsグループを管理するthis.props.stripeプロパティを提供します。

挿入されたコンポーネント内で、次のいずれかを呼び出すことができます。

  • this.props.stripe.createPaymentMethod
  • this.props.stripe.createToken
  • this.props.stripe.createSource
  • this.props.stripe.handleCardPayment
  • this.props.stripe.handleCardSetup

参考:
GitHub - stripe/react-stripe-elements: React components for Stripe.js and Stripe Elements

フォームに入力したクレジットカードのデータをpropsに格納し、指定された5つの情報をpropsから取り出すことができるとのことです。

そして以下の通り、injectStripeによってcreateTokenの情報をpropsから取り出していますね。

  async submit(ev) {
    const { token, error } = await this.props.stripe.createToken();
    this._element.clear();
    console.log(token);
  }


pages/purchase.js

import React, { Component } from "react";
import Head from "next/head";
import { Elements, StripeProvider } from "react-stripe-elements";
import CheckoutForm from "../components/CheckoutForm";

export default class Purchase extends Component {
  constructor(props) {
    super(props);
    this.state = { stripe: null };
  }
  componentDidMount() {
    // Create Stripe instance in componentDidMount
    // (componentDidMount only fires in browser/DOM environment)
    this.setState({
      stripe: window.Stripe(process.env.stripeKey)
    });
  }

  render() {
    return (
      <StripeProvider stripe={this.state.stripe}>
        <div className="example">
          <Head>
            <script src="https://js.stripe.com/v3/" />
          </Head>
          <h1>React Stripe Elements Example</h1>
          <Elements>
            <CheckoutForm />
          </Elements>
        </div>
      </StripeProvider>
    );
  }
}
  componentDidMount() {
    // Create Stripe instance in componentDidMount
    // (componentDidMount only fires in browser/DOM environment)
    this.setState({
      stripe: window.Stripe(process.env.stripeKey)
    });
  }

stateのstripeにdotenvモジュールで環境変数のstripeKeyを呼び出し、設定しています。

現状の確認

ここまでできたら、
http://localhost:3000/purchase
にアクセスしてみましょう。

そうすると、クレジットカード番号・使用期限・CVCの入力フォームのあるページが表示されます。

tokenをユーザに紐付けて保存

現在、tokenはユーザに紐付いていません。

しかし、実際にはサインイン済みのユーザがクレジットカードの登録を行うため、サインインしたユーザとtokenが紐付いている状態が適切です。

そこでwithAuth(サインインユーザのみアクセスを認証するHOC)のstateに、サインインしたユーザの情報を保有させ、tokenを作成した際にユーザと紐付けを行いましょう。

ちなみにwithAuthというHOCについての解説は以下の記事に詳しく書かれています。
nekorokkekun.hatenablog.com


具体的には以下のようなコードになります。

components/helpers/withAuth.js

import React from "react";
import router from "next/router";
import { auth } from "../../lib/firebase";
const withAuth = Component => {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        status: "LOADING",
        user: {}
      };
    }
    componentDidMount() {
      auth.onAuthStateChanged(authUser => {
        if (authUser) {
          this.setState( {
            status: "SIGNED_IN",
            user: authUser
          });
        } else {
          router.push("/");
        }
      });
    }
    renderContent() {
      const { status, user } = this.state;
      if (status == "LOADING") {
        return <h1>Loading ......</h1>;
      } else if (status == "SIGNED_IN") {
        return <Component {...this.props} currentUser={user} />;
      }
    }
    render() {
      return <>{this.renderContent()}</>;
    }
  };
};
export default withAuth;

こちらも重要なポイントの解説を行います。

        if (authUser) {
          this.setState( {
            status: "SIGNED_IN",
            user: authUser
          });

こちらで、認証したユーザのみ、認証ユーザの情報をstateへセットしています。

return <Component {...this.props} currentUser={user} />;

stateにセットした認証ユーザの情報をpropsに渡し、別のコンポーネントで使用できるようにしましょう。


CheckoutForm.jsにwithAuth.jsを追記します。

import React, { Component } from "react";
import { CardElement, injectStripe } from "react-stripe-elements";
import withAuth from "../components/helpers/withAuth";
import { firestore } from "../lib/firebase";

class CheckoutForm extends Component {
  constructor(props) {
    super(props);
    this.submit = this.submit.bind(this);
  }

  async submit(ev) {
    const { token, error } = await this.props.stripe.createToken();
    firestore
      .collection("stripe_customers")
      .doc(this.props.currentUser.uid)
      .collection("tokens")
      .add({ token: token.id })
      .then(() => {
        this._element.clear();
      });
  }

  render() {
    return (
      <div className="checkout">
        <p>Would you like to complete the purchase?</p>
        <CardElement onReady={c => (this._element = c)} />
        <button onClick={this.submit}>Purchase</button>
      </div>
    );
  }
}

export default withAuth(injectStripe(CheckoutForm));

tokenがサインインユーザに紐付いているか確認

http://localhost:3000 へアクセスし、クレジットカード情報を入力してサブミットしてみましょう。

ダミーデータとして

カード番号: 4242424242424242
期限:242
CVC:なんでも
郵便番号:なんでも

でテストができます。

サブミット後、Firestoreのstripe_customers > ユーザーID > tokens の中にランダムな英数字が入っていれば成功です。

token作成をトリガーにしたStripe sourceの作成

Stripeでtokenを作成した際にsourceというものを同時に作成します。

このsourceについては以下のサイトで詳しく解説がなされているため、引用します。

sourceは買い手が決済するために使う「モノ」を抽象化した概念であると言えます。その具象としては、クレジットカードやACH(銀行振込のようなもの)があります。1回きりの決済としてsourceに対して直接課金することもできますし、買い手に対して複数回課金するために買い手にsourceを関連付けておくこともできます。

引用元:
Stripe Sources APIにおける決済の抽象化 - blog.kymmt.com

こちらでは以下の処理を実装していきます。

  • tokenをFIrestoreに格納
  • Firebase functionsがtoken作成をトリガーにしてStripeへsourceの作成依頼を送信
  • Stripeが依頼を受けてsourceを作成
  • 作成されたsourceがFirestoreに格納

上記を図にした場合、以下のようなものになります。
f:id:nekorokkekun:20191010232007p:plain

functionsに追記

上記の処理は全てFirebase functionsで行うため、実装自体もfucntions/index.js内に書き込みましょう。

functions/index.js

// すでに実装したものは省略

exports.addPaymentSource = functions.firestore
  .document("/stripe_customers/{userId}/tokens/{pushId}")
  .onCreate(async (snap, context) => {
    const source = snap.data();
    const token = source.token;
    if (source === null) {
      return null;
    }

    try {
      const snapshot = await admin
        .firestore()
        .collection("stripe_customers")
        .doc(context.params.userId)
        .get();
      const customer = snapshot.data().customer_id;
      const response = await stripe.customers.createSource(customer, {
        source: token
      });
      return admin
        .firestore()
        .collection("stripe_customers")
        .doc(context.params.userId)
        .collection("sources")
        .doc(response.fingerprint)
        .set(response, { merge: true });
    } catch (error) {
      await snap.ref.set({ error: userFacingMessage(error) }, { merge: true });
      console.error(error);
      console.log(`user: ${context.params.userId}`);
    }
  });

function userFacingMessage(error) {
  return error.type
    ? error.message
    : "An error occurred, developers have been alerted";
}

Firebase functionsにデプロイ

以下のコマンドでFirebase functionsにデプロイ しましょう。

$ cd functions
$ yarn deploy

以下のように表示されていればデプロイ 成功です。

✔  functions[createStripeCustomer(us-central1)]: Successful update operation. 
✔  functions[addPaymentSource(us-central1)]: Successful create operation. 

✔  Deploy complete!

現状の確認

一度、Firebase Authenticationのユーザを削除した後にもう一度ユーザ認証を行い、クレジットカード情報の登録をしてみましょう。

FIrestoreのstripe_customers > ユーザID > sourcesに英数字が登録されていれば成功です。

これでStripeとFirebaseにユーザー&クレカ登録同期処理ハンズオンは完了しました。お疲れ様でした!