株式会社ウーオの井草です。 UUUOではFlutterでアプリを開発していますが、最近クレジット決済機能を導入しました。
クレジットカードで商品を購入する際は、以下のように決済を行います。
- 購入を確定する前にカード番号を入力するフォームを表示する
- カード情報を入力後、どのカードを選択しているか分かるように確認用の情報を表示する
- 購入時、オーソリゼーションを実行する
--- 購入後 ---
- 出荷可能/不可で決済確定/キャンセルを実行する
全体の流れ
今回は、flutter_stripe | Flutter Packageを使うことでクレジット決済機能を手早く開発・リリースすることが出来たので、その手順を紹介します。
機能実装にあたって、以下の順に開発を行いました。
- Stripeアカウントの作成
- 決済フォーム表示用の情報取得
- 決済フォーム表示
- お支払い方法の表示
- オーソリゼーション
- (出荷不可の場合)キャンセル実行
- (出荷可能の場合)キャプチャ実行
1. Stripeアカウントの作成
flutter_stripeを使ってクレジット決済機能を導入する場合、Stripe | オンライン決済・決済代行プラットフォームからStripeのアカウントを登録する必要があります。
アカウント作成後にダッシュボードから公開可能キーとシークレットキーが確認できるようになります。
それぞれのキーは以下で使用します。
公開可能キー: flutter_stripeのpublishableKey設定
シークレットキー: Stripeのapi_key設定
2. 決済フォーム表示用の情報取得
決済フォームを表示する際、事前に顧客の支払い資格情報の設定や承認キーを取得する必要があります。 今回はRubyで情報登録やキーの取得を行なっています。
参考: https://stripe.com/docs/api
SetupIntent
Stripe.api_key = 'sk_...' # Stripeダッシュボードに記載されているシークレットキー def create_setup_intent customer = if params[:customer_id].present? # 既存のCustomerを取得する Stripe::Customer.retrieve(params[:customer_id]) else Stripe::Customer.create({ email: 'test@gmail.com', preferred_locales: ['ja'] }) end # SetupIntent(顧客の支払い資格情報)の設定 setup_intent = Stripe::SetupIntent.create({ customer: customer.id, description: 'hogehoge', payment_method_types: ['card'] }) # EphemeralKey(Customerへのアクセスを一時的に許可するためのキー)の作成 key = Stripe::EphemeralKey.create( { customer: customer.id }, # 顧客IDを指定する { stripe_version: '2022-11-15' } ) render json: { id: intent.id, client_secret: intent.client_secret, ephemeral_key: key.secret, customer_id: customer.id } end
3. 決済フォーム表示
flutter_stripeを使って決済フォームを表示していきます。
パッケージのインストール
pubspec.yamlにflutter_stripeを記述してflutter pub getします。
dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.2 flutter_stripe: ^9.0.0
決済フォーム
決済フォーム(PaymentSheet)の表示をFlutterで実装していきます。 画面上でボタンを用意し、ボタンを押すと以下が実行されるように記載します。
// この前にサーバからSetupIntentの情報を取得 final data = ...; await Stripe.instance.initPaymentSheet( paymentSheetParameters: SetupPaymentSheetParameters( customFlow: false, merchantDisplayName: 'ウーオ', customerEphemeralKeySecret: data['ephemeral_key'], setupIntentClientSecret: data['client_secret'], customerId: data['customer_id'], // 決済フォーム入力完了直後に決済処理を実行しない場合に立てる allowsDelayedPaymentMethods: true, // ボタンの色変更 appearance: const PaymentSheetAppearance( colors: PaymentSheetAppearanceColors( primary: Color(0xFF07889B), ), ), // 請求先住所のデフォルトを日本に設定 billingDetails: const BillingDetails( address: Address( city: '', country: 'JP', line1: '', line2: '', postalCode: '', state: '', ), ), primaryButtonLabel: '追加する', style: ThemeMode.system, ), ); // PaymentSheet を表示 await Stripe.instance.presentPaymentSheet();
上記のコードで、ボタンを押した後に決済フォームが表示されるようになります。
一度カード情報を入力すると、登録済みのカードを選択できるようになります。
※SetupIntentを登録する際にカード情報が登録されている顧客IDを指定することで、最初の表示の時点で登録済みのカードを選択できるようにすることもできます。
4. お支払い方法の表示
決済確定前にどのカードが選択されているか、お支払い方法を表示できるようにします。
カード情報を入力or選択すると最初に取得したSetupIntentに支払い方法(PaymentMethod)が登録されているので、その情報を取得します。
setup_intent = Stripe::SetupIntent.retrieve(params[:setup_intent_id]) payment_method = Stripe::PaymentMethod.retrieve(setup_intent.payment_method) render json: { id: payment_method.id, brand: payment_method.card['brand'], last4: payment_method.card['last4'] }
テスト用の番号(https://stripe.com/docs/testing?locale=ja-JP)を使用して確認
5. オーソリゼーション
購入ボタンが押されたタイミングでオーソリゼーションを実行していきます。 オーソリには①自動オーソリ②手動オーソリの2種類ありますが、ウーオではセリ前商品※という在庫未確定の商品もあるため②手動オーソリを採用しています。
※セリ前商品 水揚げ情報を元にセリ価格を予想し事前に出品する商品。時化で入荷が無かった、相場が高くて買えなかった場合は購入がキャンセルになる
payment_intent = Stripe::PaymentIntent.create({ amount: 9990, currency: 'jpy', # 日本円 customer: params[:customer_id], capture_method: 'manual', # 自動オーソリの場合は'automatic', 手動オーソリの場合は'manual'を設定 receipt_email: 'test@gmail.com' }) # カード情報が入力された後、決済のオーソリを実行する response = Stripe::PaymentIntent.confirm( payment_intent.id, { payment_method: params[:payment_method_id], receipt_email: 'test@gmail.com' } ) if response.status == 'requires_capture' # 決済成功の場合は'succeeded', オーソリ成功の場合は'requires_capture'が返って来る render status: :ok, json: { message: 'succeeded', payment: response } else render status: :bad_request, json: { message: 'failed' } end
上記の処理を実行すると、Stripeのダッシュボードでオーソリが実行されたことを確認できます。
6. (出荷不可の場合)キャンセル実行
前述したセリ前商品のように、購入をキャンセルする場合はオーソリのキャンセルを実行します。
response = Stripe::PaymentIntent.cancel( payment_id, { cancellation_reason: 'abandoned' } # duplicate, fraudulent, requested_by_customer, abandonedの中から選択 ) if response.status == 'canceled' puts 'success' end
上記で、Stripeのダッシュボードでキャンセルが実行されたことを確認できます。
7. (出荷可能の場合)キャプチャ実行
出荷が可能な場合は、オーソリのキャプチャ(確定)処理を実行します。
response = Stripe::PaymentIntent.capture( payment_id, { amount_to_capture: 9990 } ) if response.status == 'succeeded' puts 'success' end
上記実行で、Stripeのダッシュボードで決済が確定したことを確認できます。
クレジットカード決済を実装してみて
キーの管理
決済にあたってStripeで発行している公開可能キー/シークレットキーを使用しますが、ソースに直接書かず環境変数などを使用して適切に管理してください。ウーオでは公開可能キーの管理にGitHub Actionsの環境変数を使用しています。
Android端末での決済フォーム表示
Android端末でクレジットカード決済機能を使用する場合、必要な設定がいくつかあります(https://pub.dev/packages/flutter_stripeのRequirementsを参照)。
こちらを設定しないと決済フォームを表示するタイミングでエラーになります。
推奨されているPayment Sheetでの決済フォーム表示だと、デザインは大きく変更できない
決済フォームはボタンの色や一部の文言の変更は可能ですが、大幅なデザイン変更には対応していません。今回は早さ優先でPayment Sheet を採用しましたが、Cardfield / Cardformを使うことで柔軟性の高いフォームを作ることも可能なようでした。
https://docs.page/flutter-stripe/flutter_stripe/card_field
金額が前後する商品の扱いに苦戦
ウーオではkg売り商品という、kg数によって金額が決まる商品を扱っています。正確な金額は出荷前に決まるため、クレジットカード決済でオーソリする金額に工夫が必要でした。手動オーソリの場合、購入時に指定した金額以下であればキャプチャ時に違う金額を指定しても決済確定が出来るため、予想される金額より多めの金額でオーソリを実行し、キャプチャ時に正確な金額を指定することで対応を行なっています。
別の金額でキャプチャするとStripeのダッシュボード上では「一部返金」のステータスになりますが、決済が確定していることを確認できます。
今後対応したいこと
海外通貨の対応
現在ウーオでは一部海外のお客様でもアプリから購入できるように開発を進めています。クレジットカード決済は日本円のみの対応のため、海外通貨でも決済ができることを目指します。
銀行振込
Stripeではクレジットカード決済の他に銀行振込にも対応しています。 https://stripe.com/docs/payments/bank-transfers?locale=ja-JP
銀行振込の要望が多ければこちらの開発も進めていきたいです。
最後に
今回はクレジットカード決済の導入手順を紹介しました。ウーオでは水産流通を革新するため、プロダクトを通じてあらゆるアプローチをしています。ウーオの事業やプロダクト開発にご興味がある方は、以下をぜひご覧ください 👇
各ポジションの募集要項はこちら
ソフトウェアエンジニア(Mobile Application/Flutter) / 株式会社ウーオ