はじめに
はじめまして、ダイゴと申します。
Flutter × Firebase × StripeAPIで、CtoCプラットフォームアプリを作ってみました。
- ユーザーの登録
- クレジットカード情報の登録・削除
- 本人確認
- 決済
しかできない簡易的なアプリですが、
難しいイメージのある「決済機能」をできるだけミニマルに実装してあるので、
個人開発などでサクッと導入してみたい場合などの助けになれば幸いです。
本記事では StripeAPI を FlutterFire アプリに組み込む方法と、決済を成立させるまでの実装方法について解説していきます。
サンプルアプリ
ログイン | 商品一覧 | アカウント |
---|---|---|
Googleアカウントでサインイン。同時にFirestore、Stripeの必要アカウントも作成。 | 右下の追加ボタンから商品を出品。購入ボタンで決済。 | クレジットカードや、本人確認(被決済のために必要)ができる。 |
事前準備
- Flutterアプリを作成
- Firebaseにつなぐ
- Functionsを使うため、Firebaseのプランをアップグレード
実装の流れ
- Stripeのプロジェクトを作成
- npm パッケージをインストール
- Customer (支払う側)と ConnectAccount (受け取る側)の作成
- Customer のクレジットカードを登録
- ConnectAccount の本人確認( Identification )
- 決済
1. Stripeのプロジェクトを作成
まずは、Stripeプロジェクトを作成します。
以下のURLから自分のStripeアカウントを作成します。
メールアドレスの認証を終え、サインインができたらStripeのダッシュボード画面が表示されます。
**「ビジネスの詳細」**は、一通りの実装を終えて本番運用するタイミングで追加しても大丈夫なので、一旦テストモードのまま実装を進めます。
左上の**「新規ビジネス」**部分をタップし、
ここに**サービス名(アプリ名)**を追加しておきましょう。
これでStripeの新規プロジェクトが作成できました。
2. Stripe の npm パッケージをインストール
Functions で StripeAPI を使えるように設定していきます。
package.json
があるディレクトリで、以下のコマンドを実行すると stripe の npm モジュールがインストールされます。
npm install stripe --save
# or
yarn add stripe
先程作成した Stripe プロジェクトと紐付けるためには、プロジェクトのsecret_key
が必要です。ダッシュボードの**「開発者→APIキー」** から、secret_key
を表示しコピーしておきます。
Functions 側で、 stripe パッケージの初期化を行います。
第一引数に、先程コピーしたsecret_key
を渡すことで、作成した Stripe プロジェクトに紐付けられます。
尚、今回のサンプルアプリでは、TypeScript で Functions の関数を書いていくので、typescript: true
を指定しておきます。
import * as functions from 'firebase-functions';
// Stripe の SecretKey(sk_〇〇〇〇) を環境変数に保存
const stripe = require('stripe')(functions.config().stripe.secret_key, {
typescript: true,
});
これで、Functions から StripeAPI を呼び出す準備が整いました。
決済に必要になるStripeリソースはこちらです。
リソース | 概要 |
---|---|
Customer | 顧客。購入者でありお金を支払うユーザー。 |
Source | 支払い方法(クレジットカード等)。customerに紐付けることが出来る。 |
ConnectAccount | お金を受け取ることのできるアカウント。 |
Individual | ConnectAccountの本人確認情報。被決済に必要。 |
Charge | 決済データ。 |
これらが揃えば、StripeConnectAPIを使った、CtoCの決済が通るようになります。
3. Customer (支払う側)と ConnectAccount (受け取る側)の作成
ここからはサンプルアプリのコードをピックアップしながら解説していきます。
アプリユーザーの新規作成時に、Customer (お金を払う側のアカウント) と ConnectAccount (お金を受け取る側のアカウント) を Stripe 上でも作ります。
今回のサンプルアプリでは、GoogleSignIn を実行し、Firestore 上にデータが無い場合は生成する、という仕組みにしています。
Future<void> signIn() async {
// Google ログイン
final credential = await _signInWithGoogle();
// user ドキュメントがあるか確認
final userId = credential.user?.uid;
final doc =
await FirebaseFirestore.instance.collection('users').doc(userId).get();
// user ドキュメントがない場合は作成
if (!doc.exists) {
/// Stripe の customer(お金を払う側のアカウント)を作成
final customerId =
await stripeRepo.createCustomer(credential.user?.email);
/// Stripe の connectAccount (お金を受け取る側のアカウント)を作成(後述)
final accountId =
await stripeRepo.createConnectAccount(credential.user?.email);
// user ドキュメントを作成
await userRepo.createUser(
credential.user,
customerId,
accountId,
);
}
}
Customer (支払う側アカウント)の作成
create メソッドを呼び出すと、Stripe 上でCustomerオブジェクトが生成されます。
オブジェクト内のid(cus_〇〇〇〇)
を、サービス側のユーザーデータに保存することで、Stripe 上の Customer とサービス側のユーザーを紐付けることが出来ます。
// MARK: - stripeのcustomerを作ってcustomerIdを返す
export const createCustomer = functions.region("asia-northeast1").https.onCall(async (data, context) => {
const email = data.email;
const customer = await stripe.customers.create(
{ email: email },
{ idempotencyKey: data.idempotencyKey });
const customerId = customer.id;
return { customerId: customerId }
});
ConnectAccount (送金先アカウント)の作成
ConnectAccount には、
の 3 種類が存在します。
Standard → Express → Customの順で、カスタマイズ性が高くなり、よりユーザーに Stripe を意識させずに実装することが可能です。
今回は、一番カスタマイズ性の高いCustomアカウントで実装しました
// MARK: - ConnectAccountを作成し、accountIdを返す
export const createConnectAccount = functions.region("asia-northeast1").https.onCall(async (data, context) => {
return await stripe.accounts.create({
type: 'custom',
country: 'JP',
email: data.email,
business_type: 'individual',
capabilities: {
card_payments: { requested: true }, // カード決済
transfers: { requested: true }, // 送金
},
individual: {
email: data.email,
},
settings: {
payouts: {
schedule: {
interval: 'manual',
},
},
},
}, {
idempotencyKey: data.idempotencyKey,
});
});
business_type
は、individual
かcompany
から選ぶことができ(non_profit
、government_entity
は US のみ)、どちらを選ぶかによって認証フローが少し変わってきます。今回は、個人であることを想定してindividual
を設定しました。
capabilities
は、アカウントに持たせる機能を指定するパラメータです。今回は、card_payments
(カード決済)とtransfers
(送金)を指定しました。
4. Customer のクレジットカードを登録
次に、先程作成した Customer (お金を支払うアカウント)にカード情報をもたせます。
カード情報を安全な形で登録するために、カード情報を Token 化したものを Customer に登録します。
このトークン化の処理はクライアントアプリ側から StripeAPI を直接呼び出すことで、自社のサーバーにはカード情報を送信せず、よりセキュアにカードの登録を行うことができます。
/// 入力内容からカード情報をトークン化
final stripeAPI = StripeApi(dotenv.env['STRIPE_PK'] as String);
final result = await stripeAPI.createToken(
{
'card': {
'number': newCreditCard.cardNumber,
'exp_month': expMonth,
'exp_year': expYear,
'cvc': newCreditCard.cvc,
},
},
);
final cardToken = result['id'];
return cardToken;
そして、 Functions 側で token 化したカード情報を customer に紐付けます。
export const createCardInfo = functions
.region("asia-northeast1")
.https.onCall((data, context) => {
const customerId = data.customerId;
const cardToken = data.cardToken;
return stripe.customers.createSource(customerId, { source: cardToken });
});
以下の記事で、カード情報をセキュアに管理するための実装方法がより詳しく紹介されています。
5. ConnectAccount の本人確認( Identification )
ConnectAccount を決済可能な状態にするには、
- 本人確認
- 利用規約への同意
を行い、charges_enabled(決済を受けれるかどうかのフラグ)
を true にする必要があります。
本人確認
ConnectAccount 内の、individual
が、本人確認用のパラメータです。
individual
内のパラメータはほぼ全て埋めなければ、決済可能な状態にならないので、アプリ側では以下のようなクラスを作り、account.update
メソッドを呼び出してアカウント情報を更新しています。
/// 本人確認情報
class StripeIndividual {
String? firstNameKanji; // 名
String? firstNameKana; // メイ
String? lastNameKanji; // 姓
String? lastNameKana; // セイ
Dob? dob; // 生年月日
Gender? gender = Gender.none; // 性別
String? phoneNumber; // 電話番号
Address? addressKanji = Address(); // 住所
Address? addressKana = Address(); // カナ住所
StripeVerification? verification; // 本人確認書類(免許証・パスポート等)
また、Stripe は WebHook を設定することができ、イベントを検知して自動で関数を走らせることが可能です。
サンプルアプリでは、ConnectAccount の update をトリガーに、Firestore 内の認証ステータスを更新するメソッドを設定しています。
利用規約への同意
利用規約(tos_acceptance
)は、アプリ側のIPアドレスと合意した日時(UNIX)を、Functions 側に渡しています。
// 利用規約
final int date = DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000; // 合意した日時(UNIX)
final ip = await _getIP(); // IPアドレス
tosAcceptance = TosAcceptance();
tosAcceptance?.date = date;
tosAcceptance?.ip = ip;
前述の本人情報(individual
)と、利用規約(tos_acceptance
)をアプリ側から受け取り、accounts.update
メソッドでアカウント情報を更新します。
// MARK: - ConnectAccountを更新する
export const updateConnectAccount = functions.region("asia-northeast1").https.onCall(async (data, context) => {
const result = await stripe.accounts.update(
data.accountId,
{
individual: data.individual,
tos_acceptance: data.tos_acceptance,
},
{ idempotencyKey: data.idempotencyKey }
);
return result;
});
2つが正しく入力されていることが確認されたときに、charges_enabled
が true になり、決済を受けることができるようになります。
6. 決済
カード情報を登録した Customerと、本人確認を終えた ConnectAccountが用意できたら、いよいよ本題である決済(Charge)を作成していきます。
Stripe には、3 種類の決済種別があり、どれを使うかによって、サービスの資金フローが大きく変わります。
① Direct charge
「顧客と子アカウント間で直接決済を行い、決済額の一部をプラットフォームアカウントへ送金する」 という決済方式です。
この場合、子アカウントが決済に対しての責任を持つことになるため、決済手数料の負担や返金対応は子アカウント自身が行います。
② Destination charge
「プラットフォームに支払いを作成すると同時に、送金先アカウント(単一)へいくら配分するかを決める」 という決済方式です。
この場合、プラットフォーム側が決済に対しての責任を持つため、決済手数料の負担や返金対応はプラットフォームが行います。
③ Separate Charges and Transfers
その名の通り、「決済と送金を分けて行う」 決済方式です。
プラットフォームに対して決済が行われ、プラットフォームの任意のタイミングで、任意のアカウント(複数可)に送金する、という資金フローを実現できます。
この場合も Destination charge と同様に、プラットフォーム側が決済に対しての責任を持つため、決済手数料の負担や返金対応はプラットフォームが行います。
今回は、
- 支払い総額の 10%をプラットフォーム手数料として徴収
- 残った 90%を ConnectAccount へ送金
というフローにしたかったので、② Destination chargeの形式で決済を作成します。
// MARK: - 決済を作成する
export const createStripeCharge = functions.region("asia-northeast1").https.onCall(async (data, context) => {
const customer = data.customerId; // 支払側(customer)のID
const amount = data.amount; // 支払い総額
const feeAmount = Math.floor(amount * 1 / 10); // 10%の手数料を引いた値を小数点以下で切り捨てる
const targetAccountId = data.targetAccountId; // 送金先のアカウント
// 決済を作成
const charge = await stripe.charges.create({
customer: customer,
amount: amount,
currency: "jpy",
application_fee_amount: feeAmount, // プラットフォーム手数料
transfer_data: {
destination: targetAccountId,
},
}, {
idempotencyKey: data.idempotencyKey,
},);
console.log('charges %j', charge);
return {chargeId: charge.id};
});
第 2 引数として渡しているidempotencyKey
は、べき等性を担保するためのパラメータです。
べき等とは、ある操作を 1 回行っても複数回行っても結果が同じであることをいう概念です。つまりidempotencyKey
をつければ、その key がついたリクエストを複数行ったとしても1回だけの処理にしてくれるということです。
すべての stripe のメソッドに対して、追加の引数として渡すとべき等にしてるので、特に決済などの重複してはいけない処理には必須となるパラメータです。
Stripe のダッシュボードで支払いが確認できました。
さいごに
今回は、FlutterFire × StripeAPI を使ったプラットフォーム型のサンプルアプリの実装を紹介しました。
私は、昨年からプログラミングを始めた身なので、最初は「決済機能?絶対むずいやん」と思っていましたが、やってみると「案外不可能でもないな」と思えるようになりました。
実際のサービスとしてリリースするためには、
- 領収書の発行
- 銀行口座の登録
- 残高の表示、振り込み
など、まだまだ色々なステップが必要ですが、この記事が決済機能実装のきっかけになれば幸いです。
最後まで読んでいただき、ありがとうございました。
参考
お世話になっているコミュニティ