💳

【FlutterFire × Stripe】flutter_stripe パッケージで楽々カード決済

2022/12/01に公開約11,200字

この記事は、Flutter 大学アドベントカレンダー 2022 1日目の記事です。

はじめに

はじめまして、ダイゴです。

12月、今年もアドベントカレンダーの季節が始まりましたね。

今回は、個人的に前から気になっていた flutter_stripe パッケージを触って、シンプルなカード決済機能(PaymentSheet) を実装してみました。

サンプルアプリも作成したので、ぜひこちらも合わせて参考にしていただけると幸いです。

https://github.com/DaigoWakabayashi/flutter_stripe_example

目次

  1. パッケージのインストール・両OSでのセットアップ
  2. Stripe アカウントの作成 & Publishable Key の発行
  3. サーバー側の実装(Cloud Functions)
  4. クライアント側の実装(Flutter)
[✓] Flutter (Channel stable, 3.3.5, on macOS 13.0)
    • Flutter version 3.3.5 on channel stable
    • Dart version 2.18.2

1. パッケージのインストール・両OSでのセットアップ

まずはパッケージのセットアップをしていきます。

pubspec.yaml

プロジェクトの pubspec.yaml に、 flutter_stripe を追加して pub get します。

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
+ flutter_stripe: ^7.0.0

iOSの設定

Xcode を開いて

  • Runner > Project Runner > Deployment Target > iOS Deployment Target
  • Runner > Targets Runner > Minimum Deployments > Minimum Deployments > iOS

を 12.0 以上に指定します。

Androidの設定

  1. Android SDK のバージョン指定

android/app/build.gradle を以下のように指定します。

android {
-   compileSdkVersion flutter.compileSdkVersion
+   compileSdkVersion 33
    ndkVersion flutter.ndkVersion

    ~~~~~~~~~~~ 中略 ~~~~~~~~~~~

    defaultConfig {
        applicationId "com.example.flutter_stripe_example"
-       minSdkVersion flutter.minSdkVersion
+       minSdkVersion 21
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

ちなみに、flutter.〇〇SdkVersion の値は flutter_sdk/packages/flutter_tools/gradle/flutter.gradle で確認可能なので、そちらの値が ↑ 以上になっていれば flutter.〇〇SdkVersion のままで問題ありません。

  1. Kotlin のバージョン指定

android/build.gradle にある kotlin_version を 1.5.0 に以上に指定します。

buildscript {
+   ext.kotlin_version = '1.6.21' // ← 1.5.0 以上に指定
    repositories {
        google()
        mavenCentral()
    }
  1. style.xml の変更

android/app/src/main/res/values/styles.xmlandroid/app/src/main/res/values-night/styles.xml を以下のように変更します。

 <?xml version="1.0" encoding="utf-8"?>
 <resources>
    ~~~~~~~~~~~ 中略 ~~~~~~~~~~~
-    <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
+    <style name="NormalTheme" parent="Theme.MaterialComponents">
         <item name="android:windowBackground">?android:colorBackground</item>
     </style>
 </resources>
  1. gradle のバージョン確認

android/build.gradle のビルドツールのバージョンが新しいものになっているか確認し、古ければ修正します。(最新はこちらから確認できますが、現時点では 7 以上になっていれば問題はなさそうです)

    dependencies {
+       classpath 'com.android.tools.build:gradle:7.1.2' // もし 4 系とかになっていたら修正
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}
  1. MainActivity.kt の変更

android/app/src/main/kotlin/<アプリidのpath>/MainActivity.kt で、FlutterActivity → FlutterFragmentActivity に変更します。

  package com.example.flutter_stripe_example
  
  import io.flutter.embedding.android.FlutterActivity
+ import io.flutter.embedding.android.FlutterFragmentActivity

- class MainActivity: FlutterActivity() {
+ class MainActivity: FlutterFragmentActivity() {
  }

2. Stripe アカウントの作成 & Publishable Key の発行

Stripe アカウントの作成

以下のページにアクセスし、アカウント作成 or ログインします。

https://dashboard.stripe.com/login?redirect=%2Fdashboard

ダッシュボードから新規の Stripe アカウント(≒ アプリのアカウント)を発行します。

すると、ダッシュボードから PublishableKey が確認できます。

3. サーバー側の実装(Cloud Functions)

flutter_stripe で想定されている決済手法には

の2つがあるのですが、公式が推奨している & サーバー側の実装が比較的簡単な PaymentSheet による決済を実装していきます。

どちらにせよ、サーバー側で PaymentIntent のエンドポイントを作成する必要があるので、今回は Cloud Functions を使った上記のエンドポイント作成例を紹介します。

Cloud Functions の環境構築

少し雑になってしまうのですが、Cloud Functions の実行環境については、公式のドキュメントを参考に構築してください。

https://firebase.google.com/docs/functions/get-started

サンプルプロジェクトでは ESlint と Prettier で静的解析およびフォーマットを行っていますが、必須ではないのと設定が複雑なので、参考資料を載せておきます。

https://zenn.dev/big_tanukiudon/articles/c1ab3dba7ba111#eslint-導入-&-実行

https://maku.blog/p/yfow6dk/

PaymentIntent.create のエンドポイント作成

では関数の作成に取り掛かります。
まずは node の stripe パッケージをインストールし、

npm install stripe --save

2 で作成した Secret Key を使って以下のようなエンドポイントを作成します。

import * as functions from "firebase-functions";

const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);

/// PaymentIntent の作成
export const createPaymentIntent = functions.https.onCall(async (_, __) => {
  try {
    // 新しい Customer を作成(既存の場合は id を渡せばOK)
    const customer = await stripe.customers.create();
    // Ephemeral Key (一時的なアクセス権を付与するキー)を作成
    // https://stripe.com/docs/payments/accept-a-payment?platform=ios&ui=payment-sheet#add-server-endpoint
    const ephemeralKey = await stripe.ephemeralKeys.create(
      { customer: customer.id },
      { apiVersion: `2020-08-27` }
    );
    // PaymentIntent の作成
    const paymentIntent = await stripe.paymentIntents.create({
      amount: 1000,
      currency: `jpy`,
      customer: customer.id,
      automatic_payment_methods: {
        enabled: true,
      },
    });
    // アプリ側で必要な値を返却
    return {
      paymentIntent: paymentIntent.client_secret,
      ephemeralKey: ephemeralKey.secret,
      customer: customer.id,
    };
  } catch (error) {
    console.error(`error: %j`, error);
    return {
      title: `エラーが発生しました`,
      message: error,
    };
  }
});

上記に登場する Stripe リソースは以下のような役割があります。

  • Customer
    • Stripe 上の顧客、お金を支払う人(ユーザー)
    • カード情報などの決済手段を持つことができる
  • EphemeralKey
    • Customer(機密情報)へのアクセスを一時的に許可する Key
  • PaymentIntent
    • Stripe 上の決済データ
    • 決済金額や通貨などを指定できる

今回の例では決済ごとに Customer オブジェクトを作成していますが、実際のサービスであればクライアント側から受け取った CustomerId などを使って PaymentIntent を作成することになるかと思います。

4. クライアント側の実装(Flutter)

最後に Flutter(クライアント)側の実装をしていきます。
main 関数内で Stripe パッケージの publishableKey 設定と、Cloud Functions を呼び出せるように Firebase の初期化を行います。(Firebase の初期化は FlutterFire CLI を使えば楽です)

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Firebase の初期化
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

  // ダッシュボードの公開可能キー
  Stripe.publishableKey = const String.fromEnvironment('STRIPE_PK_DEV');

  runApp(const App());
}

PaymentSheet の表示

準備が整ったので、PaymentSheet を表示します。

PaymentSheet に関わるメソッドは

  • initPaymentSheet(PaymentSheet の初期化)
  • presentPaymentSheet(PaymentSheet の表示)
  • confirmPaymentSheetPayment(決済内容の確定)

の3つです。
全体のコードは以下になります。

class PaymentSheetPage extends HookWidget {
  const PaymentSheetPage({super.key});

  
  Widget build(BuildContext context) {
    final payment = useState<PaymentIntent?>(null);
    return Scaffold(
      appBar: AppBar(title: Text(runtimeType.toString())),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('金額:${payment.value?.amount.toString()}'),
            Text('ステータス:${payment.value?.status.name.toString()}'),
            Text('日時:${payment.value?.created.toString()}'),
            ElevatedButton(
              onPressed: () async {
                try {
                  // 1. Cloud Functions 上で PaymentIntent を作成
                  final callable = FirebaseFunctions.instance
                      .httpsCallable('createPaymentIntent');
                  final result = await callable.call();
                  final data = result.data;

                  // 2. PaymentSheet を初期化
                  await Stripe.instance.initPaymentSheet(
                    paymentSheetParameters: SetupPaymentSheetParameters(
                      customFlow: true,
                      merchantDisplayName: 'Flutter Stripe Example',
                      paymentIntentClientSecret: data['paymentIntent'],
                      customerEphemeralKeySecret: data['ephemeralKey'],
                      customerId: data['customer'],
                    ),
                  );

                  // 3. PaymentSheet を表示
                  await Stripe.instance.presentPaymentSheet();

                  // 4. 決済を確定
                  await Stripe.instance.confirmPaymentSheetPayment();

                  // 5. 決済内容を取得
                  final paymentIntent = await Stripe.instance
                      .retrievePaymentIntent(data['paymentIntent']);
                  payment.value = paymentIntent;
                } on StripeException catch (e) {
                  final error = e.error;
                  switch (error.code) {
                    case FailureCode.Canceled:
                      log('キャンセルされました', error: e);
                      break;
                    case FailureCode.Failed:
                      log('エラーが発生しました', error: e);
                      break;
                  }
                } on FirebaseFunctionsException catch (e) {
                  log('エラーが発生しました', error: e);
                } catch (e) {
                  log('不明なエラーが発生しました', error: e);
                }
              },
              child: const Text('Show PaymentSheet'),
            ),
          ],
        ),
      ),
    );
  }
}

実際に動かしてみると、Stripe 上で決済が成功しているのを確認できました。

テスト用のカード番号は以下のドキュメントにまとまっているので、決済エラーやカードブランドごとに検証をする場合はこちらを参考にしてください。

https://stripe.com/docs/testing#cards

まとめ

今回は flutter_stripe パッケージを触ってみました。

本当は payment_intent_succeeded をリッスンする WebHook のエンドポイントを作って、決済リスト表示みたいなことも解説できればと思ったのですが、時間が足りずミニマムな例になってしまいました。また時間のあるときに触ってみようと思います。

セキュアなカード決済機能を手軽に実装できるのは有り難いですね。
コミッターへの寄付も出来るみたいなので、お世話になった際はぜひ。

https://opencollective.com/flutter_stripe

最後までご覧いただき、ありがとうございました。

https://github.com/DaigoWakabayashi/flutter_stripe_example

参考

https://stripe.com/docs

https://docs.page/flutter-stripe/flutter_stripe/sheet

GitHubで編集を提案

Discussion

ログインするとコメントできます