見出し画像

Flutterを活用してサクッと検証!〜アプリのサブスク購入のOSごとのデータの流れ〜

この記事はPioneer Advent Calendar 2022の8日目の記事です。

生活の中で必需品となりつつあるスマートフォンアプリは、1タップでサブスクリプション購入ができ、よりリッチなサービスが使えるものも多くなってきています。
サブスクリプション購入はGoogleやAppleで違う部分があるかもしれない??という疑問から、Flutterでアプリを作って更新時のデータの流れまでを検証してみました。

自己紹介

こんにちは! SaaS Technology Center モバイル開発部所属の方波見です。
2022年1月からパイオニアでAndroidエンジニアとして働いています。
担当アプリはPioneerSmartSync(パイオニアスマートシンク)です。

なぜ検証してみたのか?

きっかけはサーバー側のメンバーとアプリ内の購入について話をしていた際に、「自動更新のサブスクリプションって、有効期限間近とかをサーバー側でウォッチしていないといけないんだっけ?」という疑問からでした。

技術ブログを色々と調べてみたものの、購入時の実装やデータの流れを書いているものが多く、私自身もアプリ内での購入機能を実装したことがなかったため、OSごとの更新時のトリガーがどのようになっているかを完全に把握しているわけではありませんでした。

また、検証時のデータはどちらのOSも実機からの送信が必須であり、私がiOSをネイティブで実装できる自信がなかったことから、今回はFlutterのin_app_purchaseというライブラリを使ってサクッと検証してみることにしました。
選定理由としては、ライク数の多さと、Flutterの公式ライブラリであるという点が決め手で選定しました。

検証のために必要な機能

必要な機能としては以下の4つです。

  • サブスクリプションのプランを表示する

    • 一つのプランだけ検証したいのであれば、一覧は不要かもしれない

  • サブスクリプションの購入をする

  • 購入後にレシート情報をサーバーに送る(レシート検証*)

  • サブスクリプション購入の更新期日を迎える

レシート検証とは?

アプリ内での購入を行う際にはどちらのOSも図のように自社サーバーを介したレシート検証という機能が必要でした。

レシート検証の流れ

レシート検証は、ストア側の購入情報とアプリ側の購入情報を第三者的な立ち位置から検証を行う役目をしてくれます。

この機能があることによって、「アプリから偽装されたレシートが送られてきていないか」や、多重購入・購入処理漏れを防ぐことができます。

早速、アプリを作ってみる

実装前の準備

In_app_purchaseのライブラリをimportするのはとても簡単で、プロジェクトのディレクトリでコマンドを打つだけです。

$ flutter pub add in_app_purchase

あとは、in_app_purchaseのライブラリを使いたいファイルで以下を記述すれば、準備は完了です。

import 'package:in_app_purchase/in_app_purchase.dart';

サブスクリプションのプランを表示する

コード的にはmethodを一つ呼び出せばプランが取得できる!という手軽さなのですが、今回は新しくアプリを作って内部テストとして検証してみたため、個人的にはここでの事前準備が多く、つまずくことが一番多かったです。

具体的な事前準備はこちらです。

  • ストアのアプリ情報を入力・申請を済ませておく

    • プライバシーポリシーや提供国・価格設定などストア公開できる状態にしておくことが必要

    • iOSはスクリーンショットの提出も必須です

  • ストアにサブスクプラン審査用のアプリをアップロードしておく

    • Androidはパーミッションが付与された状態でのアップロードが必要

  • サブスクプランの作成・審査

    • ストアの申請とはまた別のためわかりにくかったです

  • テスト用アカウントの登録

    • Androidはテスト用のアカウントがプライマリアカウントとしてログインしていないと購入失敗になる

事前準備が完了したら、以下のmethodを呼び出して、プランの一覧を取得します。

...

final ProductDetailsResponse response = 
    await InAppPurchase.instance.queryProductDetails(${プランのプロダクトIDのSet<String>}); 

 if (response.notFoundIDs.isNotEmpty) { 
  // 取得できなかった時のハンドリング 
} 

List<ProductDetails> productDetails = response.productDetails; 

取得できたプラン一覧(productDetails)を表示するために、今回は必要な情報を最低限表示できそうなListTileを利用しました。

...

Card( 
  child: ListTile( 
    leading: const Icon(Icons.monetization_on_outlined),  
    textColor: AppColors.textColor, 
    title: Text(productDetails.title), // 定期購入プランのタイトル 
    subtitle: Text(productDetails.description), // Android: アプリ名, iOS: 定期購入プランのタイトル 
    trailing: ElevatedButton( 
      style: ElevatedButton.styleFrom( 
          backgroundColor: AppColors.background, 
          textStyle: const TextStyle(color: AppColors.textColor)), 
      onPressed: onPressed, // 金額のボタンを押した時の処理 
      child: Text(productDetails.price), // 定期購入プランの金額 
    ), 
  ), 
); 
無事にプランの表示をすることができました

サブスクリプションの購入をする

サブスクリプションの購入は、プランの一覧から選ばれたプランを指定し、buyNonConsumableを呼び出すだけで、OSごとの購入確認から、購入をリクエスト(レシート検証の図①)と購入結果(レシート検証の図②)できたかどうかまでをいい感じに吸収してくれています。

try { 
  PurchaseParam purchaseParam = PurchaseParam(productDetails: details); 
  await  InAppPurchase.instance.buyNonConsumable(purchaseParam: purchaseParam); 
} catch (err) { 
  // エラーの時の処理 
  … 
} 
Android
iOS

購入後にレシート情報をサーバーに送る(レシート検証)

In_app_purchaseのライブラリでは、purchaseStreamを事前にlistener登録しておくと購入状態に変化があったときに通知してくれます。

アプリ<->ストア間の購入処理が完了した際にも、purchaseStreamでレシート情報が通知(レシート検証の図③)されます。

通知されたレシート情報をBase64にエンコードし、サーバーに通知してあげれば完了です。

Stream purchaseUpdated = InAppPurchase.instance.purchaseStream; 
purchaseUpdated.listen((purchaseDetailsList) { 
    for (final PurchaseDetails details in purchaseDetailsList) { 
        switch (details.status) { 
            case PurchaseStatus.purchased: 
                // 購入成功 
                // 必要なレシート情報をBase64にエンコード 
                final data = base64.encoder.convert(details.verificationData.localVerificationData.codeUnits); 
                // サーバに送る処理 
                … 
            } 
        } 
    }, onDone: () { 
      // 完了の時の処理 
    }, onError: (error) { 
      // エラーの時の処理 
}); 

今回は、サーバー側の処理はサーバーのメンバーが実装してくれたので、割愛します。
レシート検証部分はFirebase Cloud Functionを使った実現も可能みたいなので、またの機会にチャレンジしてみたいと思います。

参考にさせていただいた記事

サブスクリプション購入の更新期日を迎える

ついに、調査したかった本題です。

検証用のアカウントで購入した際は、実際のプランよりかなり短縮されて更新期限がきます。
OSごとの差はこちらの図をご覧ください。

今回は期間を1週間に設定していたため、Androidは5分・iOSは3分待つ程度で結果が得られました。

アプリ側に通知がきたり、検知ができたりする訳ではなかったため、サーバー側のログを調査したところ、更新のトリガーはAndroid/iOS共にストア側からキックされる仕組みになっていました。

そのため、アプリ側はサーバに保管されている提供情報を参照し、サブスクリプションが有効かどうか判断する必要がありそうです。

最後に

結果的には、 Android/iOS共にストア側からキックされるため、OSごとの差分も発生せず、有効期限をサーバ側でウォッチしておく必要がないということが確認できました。

検証アプリをネイティブで作るべきか、マルチプラットフォームのフレームワークで作るべきかというのは別課題もあるかと思いますが、今回のケースは両OSで検証が必要だったので、Flutterを活用するメリットも存分に受けられたため、とても良かったです。

Pioneer Advent Calendar 2022 の9日目は、SaaS Technology Center 技術推進室 平井俊之さんの「エンタープライズ企業にAWS ControlTowerを導入してみた」です。
是非お楽しみに!

パイオニア株式会社では、変革に向けて一緒に働く仲間を募集中です!
老舗メーカーの変革に少しでも共感、チャレンジしてみたいと思われた方は、下記の採用ページをご覧ください。