Flutterで、flutter_stripeを利用してサブスクリプションの決済画面を作成する

この記事は、[Flutter Advent Calendar 2022](https://qiita.com/advent-calendar/2022/flutter) 23日目の記事です。

Flutterを利用してDartでiOS / Androidに対応したアプリケーションを開発できます。

この記事では、FlutterでStripeの決済情報入力画面を表示するためのライブラリ「`flutter_stripe`」を利用して、サブスクリプションの申し込みを完了する方法を紹介します。

## 事前準備

Flutterを動かすために、DartとFlutter CLIをインストールしましょう。

### Dartのインストール

Homebrewを利用して、Dartをインストールします。

```bash
$ brew tap dart-lang/dart
$ brew install dart
```

今回は、以下のバージョンを利用しています。

```bash
dart --version
Dart SDK version: 2.18.5 (stable) (Tue Nov 22 15:47:29 2022 +0000) on "macos_arm64"
```

### Flutterのインストール

ドキュメントのガイドに従って、Flutterをインストールします。

https://docs.flutter.dev/get-started/install/macos

今回は、以下のバージョンを利用しています。

```bash
$ flutter --version
Flutter 3.3.9 • channel stable • https://github.com/flutter/flutter.git
Framework • revision b8f7f1f986 (2 weeks ago) • 2022-11-23 06:43:51 +0900
Engine • revision 8f2221fbef
Tools • Dart 2.18.5 • DevTools 2.15.0
```

## アプリのセットアップ

準備ができれば、アプリをセットアップします。

ここでは、`flutter create`を実行しましょう。

```bash
$ flutter create first_app
Creating project first_app...
Running "flutter pub get" in first_app...                        1,398ms
Wrote 127 files.

All done!
In order to run your application, type:

  $ cd first_app
  $ flutter run

Your application code is in first_app/lib/main.dart.
```

`flutter run`でアプリを起動できます。

![スクリーンショット 2022-12-14 19.40.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/022c49fe-8b79-28e7-392e-b0176b9c0d2f.png)

## サブスクリプションデータを作成するAPIを用意しよう

Express.jsやNestJSなどで、サブスクリプションを作成するAPIを事前に用意しましょう。

```js
app.post('/create_subscription', async (req, res) => {
    const { id: customerId } = await stripe.customers.create()
    const ephemeralKey = await stripe.ephemeralKeys.create({
      customer: customerId,
    }, {
      apiVersion: '2022-08-01'
    })
    const subscription = await stripe.subscriptions.create({
      customer: customerId,
      items: [{
        price: 'price_xxxx',
        quantity: 1,
      }],
      payment_behavior: 'default_incomplete',
      expand: ['latest_invoice.payment_intent'],
      payment_settings: {
          save_default_payment_method: 'on_subscription',
      },
    })
    return {
      clientSecret: subscription.latest_invoice.payment_intent.client_secret,
      ephemeralKey: ephemeralKey.secret,
      customerId
    }
})
```

https://stripe.com/docs/billing/subscriptions/build-subscriptions?ui=elements

## Stripeのカード登録フォームを追加する

サブスクリプションの契約に利用するための、カード情報登録フォームを追加しましょう。

### flutter_stripeパッケージを追加する

まずは`flutter_stripe`パッケージを追加します。

https://pub.dev/packages/flutter_stripe

```bash
$ flutter pub get flutter_stripe
Downloading flutter_stripe 7.0.0...
Downloading stripe_platform_interface 7.0.0...
Downloading stripe_ios 7.0.0...
Downloading stripe_android 7.0.1...
Downloading json_annotation 4.7.0...
Downloading freezed_annotation 2.2.0...
Changed 7 dependencies!
```

:::note warn
**pod installに失敗する場合**

`flutter run`を実行すると、次のようなエラーが出ることがあります。

```
Analyzing dependencies
[!] CocoaPods could not find compatible versions for pod "stripe_ios":
  In Podfile:
    stripe_ios (from `.symlinks/plugins/stripe_ios/ios`)

Specs satisfying the `stripe_ios (from `.symlinks/plugins/stripe_ios/ios`)` dependency were found, but they required a higher minimum deployment target.
```

`ios/Podfile`と`/ios/Runner.xcodeproj/project.pbxproj`のiOSバージョンを新しいバージョンに変更すると、解決することがあります。

```diff
+++ b/ios/Podfile
@@ -1,5 +1,5 @@
 # Uncomment this line to define a global platform for your project
-# platform :ios, '11.0'
+platform :ios, '13.0'
```

```diff
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -272,7 +272,7 @@
                                GCC_WARN_UNINITIALIZED_AUTOS = YES_AG
GRESSIVE;
                                GCC_WARN_UNUSED_FUNCTION = YES;
                                GCC_WARN_UNUSED_VARIABLE = YES;
-                               IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+                               IPHONEOS_DEPLOYMENT_TARGET = 13.0;
```

参考: https://stackoverflow.com/questions/72485674/error-with-flutter-stripe-3-0-2-for-ios-11394

M1/M2系のMacbookなどを利用されている場合は、こちらもお試しください。

https://github.com/flutter/flutter/wiki/Developing-with-Flutter-on-Apple-Silicon

:::

また、サーバー側でサブスクリプションを作成して取得するため、httpモジュールも追加します。

```bash
$ flutter pub add http
...
Downloading http 0.13.5...
Downloading http_parser 4.0.2...
Changed 3 dependencies!
```

### flutter_stripeをセットアップする

`lib/main.dart`を変更して、ライブラリをセットアップします。

```diff_dart
+import 'package:flutter_stripe/flutter_stripe.dart';
+import 'dart:developer' as developer;
import 'dart:convert';
+import 'package:http/http.dart' as http;

void main() {
  WidgetsFlutterBinding.ensureInitialized();
+  Stripe.publishableKey =
      'pk_test_から始まるStripeの公開可能キー';
  runApp(const MyApp());
}
```

### `lib/main.dart`に決済画面を表示するボタンを追加する

決済画面（`PaymentSheet`）を表示するためのボタンを追加しましょう。

```diff_dart
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
+            ElevatedButton(
+              onPressed: () async {
+                try {
+                  developer.log("Hello");
+                } catch (e) {
+                  developer.log("error", error: e);
+                }
+              },
+              child: const Text("Show card form"),
+            )
          ],
```

ボタンが追加されました。

![スクリーンショット 2022-12-15 15.34.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/7c38a9b6-df2e-d371-3e38-7c30b5a0f5fd.png)

### 決済フォームを表示する

作成した`ElevatedButton`の`onPressed`イベントに、フォームを表示するための処理を追加しましょう。

```diff_dart
            ElevatedButton(
              onPressed: () async {
                try {
+                  final response =
+                      await http.get(Uri.http("localhost:3100", "/stripe"));
+                  final jsonResponse = jsonDecode(response.body);
+
+                  await Stripe.instance.initPaymentSheet(
+                    paymentSheetParameters: SetupPaymentSheetParameters(
+                        customerEphemeralKeySecret:
+                            jsonResponse['ephemeralKey'],
+                        customerId: jsonResponse['customerId'],
+                        paymentIntentClientSecret:
+                            jsonResponse['clientSecret']),
+                  );
+                  await Stripe.instance.presentPaymentSheet();
                } catch (e) {
                  developer.log("error", error: e);
                }
              },
              child: const Text("Show card form"),
            )
```

再読み込みに成功すると、ボタンクリックで決済フォームが表示されます。

![スクリーンショット 2022-12-15 15.38.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/b138acbf-c31a-ff8f-beac-ce1ac8642962.png)

すでにカード情報が登録されている顧客のIDをサーバー側で指定した場合、登録済みのカードを選ぶこともできます。

![スクリーンショット 2022-12-15 15.38.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/7d360f38-a333-2c24-cdc9-9c30893e1e83.png)

また、iOSではカードのスキャン機能も利用可能です。

![スクリーンショット 2022-12-15 15.38.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/27643bcf-6858-30d3-4e35-ea28ff2137ac.png)

## Documents

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