Open34

revenuecat周りメモ

ゆーとゆーと
Purchases.configure(configuration);
final appUserID = await Purchases.appUserID;

config処理すると、revenuecatの情報を取得できる。

ゆーとゆーと

顧客情報の更新時にListerされる関数。
→ subsciptionの自動更新時にも実行される。

Purchases.addCustomerInfoUpdateListener((customerInfo) async {
// ...
}
ゆーとゆーと

Sandboxアカウントでテストする場合、サブスクの自動更新の間隔を選択できる。デフォルトだと5分間隔で更新される設定。
12回更新したら自動的に終了。
https://developer.apple.com/jp/help/app-store-connect/test-in-app-purchases/manage-sandbox-apple-id-settings/

デフォルトの設定は、すべてのアカウントで 1 か月 = 5 分の換算となります。以下の表を参考に、更新の間隔を調整してください。サブスクリプションは、12 回自動更新されると 13 回目の更新時に自動的にキャンセルされます。なお、Sandbox 上における更新頻度は、設定したサブスクリプション期間によっても異なります。

ゆーとゆーと

entitlement情報を取得する際、存在しないentitlementを指定してもエラーではなくnull or false的な感じで帰ってくるだけ。
場合によってはエラーハンドリングがあった方がいいかも。

      CustomerInfo customerInfo = await Purchases.getCustomerInfo();
      EntitlementInfo? entitlement =
          customerInfo.entitlements.all[entitlementID];
ゆーとゆーと

RevenuecatのユーザーIDは、何も指定しないと匿名のIDを勝手に生成してくれる。
以下のようにPurchasesConfigurationに指定すると、ユーザーIDを指定できる。

    configuration = PurchasesConfiguration(appleApiKey)
         ..appUserID = <Revenuecat_user_ID>;


とりあえず匿名のIDで生成して、後でID変更することも可能。

Purchases.login(<Revenuecat_user_ID>);


一般的にはAuthなどでアカウント管理していて、AuthのユーザーIDとRevenueCatのユーザーIDとを紐付けたいことが多い?なので、IDは指定する方が融通は効きそう。
また、異なるプラットフォーム間でのサブスクリプションの共有する際は、同一のRevenueCatのIDが必要になるので、なおさらIDの指定が必要そう​。

詳しくはDocs参照


【注】
.logout()メソッドを呼び出さない:
ログアウトメソッドを呼び出すと、新しい匿名のアプリユーザーIDが生成されます。
カスタムアプリユーザーIDのみを使用する場合は、このメソッドを呼び出さないようにしてください。

ゆーとゆーと

iOS設定

※似たような設定項目がありすぎてややこしい。

Developer / サブスクリプションの管理 / 自動更新サブスクリプションの提供

サブスクリプション > サブスクリプショングループ

サブスクリプショングループ: 様々なレベルや期間のサブスクリプションプロダクトを 1 グループにまとめたもの。ユーザが同時に登録できるのは、1 グループにつき 1 つのサブスクリプションのみです。

  • 一度命名すると変更不可
  • app store connectだけで表示するグループ名
    • ここの名前がユーザーに表示されることはない。
    • RevenueCaでもここのグループ名を指定することもない。
  • 命名は分かりやすく***_subscription_groupとかにした方が判別しやすいかも。
ゆーとゆーと

スクリプション > サブスクリプショングループ > サブスクリプション

サブスクリプション: 所定の期間中、随時更新されるコンテンツを購入できる製品。自動更新サブスクリプションは、ユーザが取り消さない限り自動更新されます。サブスクリプションの期間と価格は、App Store Connect で設定してください。




サブスクリプション

参照名:

参照名はApp Store Connectおよび「売上とトレンド」のレポートで使用されます。App Storeに表示されることはありません。名前は最大64文字です。

  • ここのapp store connectのコンソール上でのみで表示される。
    • 後に変更可能。
    • アプリで命名規則を考えておかないと統一性無くしそう。
  • 以下の「ローカリゼーション」の表示名と統一しとくのが良さそう?

製品ID:

  • このサブスクリプションのID。RevenueCatと連携するためにも使用する。
    • 一度決めたら変更不可
    • IDの命名は、<app名>_****_subscriptionとかが良さそう?
    • レポートに使用される固有な英数字のIDです。ある製品に対して使用した製品IDは、その製品が削除されても再度使用することはできません。




App Storeのローカリゼーション

これらの名前は、ユーザがサブスクリプション内容を管理する際、デバイス上に表示されます。

上記のように記載あるが、SANDBOX環境でサブスク管理画面を見ても反映されてない。
本番環境になると反映される?この画面じゃない?
「設定→ユーザー名→サブスクリプション」の画面?


iphoneの設定→サブスク管理画面

サブスクリプショングループ表示名

  • サブスク製品は一つだけなら、サブスクリプションの参照名(商品名)と統一でいいかもしれない。ユーザーがサブスクの管理画面を見て一目でわかる命名が大事そう。

アプリ表示オプション

  • カスタムもできるが、基本はアプリ名そのままで良さそう。
ゆーとゆーと

スクリプション > サブスクリプショングループ > サブスクリプション > サブスク名(製品名)


App Storeのローカリゼーション

サブスクリプションのローカライズされた表示名および説明がApp Storeに表示されます。

App Storeのローカリゼーションという設定項目は二種類あるので注意。
「デバイスの設定画面の表示設定」と「AppStore&決済画面の表示設定」とがあってややこしい。


アプリ上での決済画面

  • 上記の一個前の記事で指定したローカリゼーションの表示名と一緒で良さそう。

    • 違う表示名にする意味ある?

      上記の一個前の記事で添付した画像
  • 説明文はStoreのどこに表示される?

    • 未検証。
  • アプリ上でOfferingsを取得すると設定値を取得できる。以下参照。

ゆーとゆーと

【設定項目として検討必要なものまとめ】

  • サブスクリプショングループ名
    • 変更不可。
    • ユーザーには表示されない。
  • サブスクリプション名(製品名)
    • ユーザーには表示されない。
  • サブスクリプションID(製品ID)
    • 変更不可。
    • RevenueCatで使用する。
  • ローカリゼーションの表示名
  • ローカリゼーションの説明文
ゆーとゆーと

docs/1.RevenueCat アカウントを作成する

RevenueCat に登録し、プロジェクト内でアプリを設定する場合は、会社のアカウントを使用することをお勧めします。チームの他のメンバーをコラボレーターとしてプロジェクトに招待できますが、課金を管理できるのはプロジェクト オーナーだけです。プロジェクトの共同編集者は請求の詳細を管理できません。

docs/2. プロジェクトとアプリの構成

ステージングと本番アプリおよびユーザー

RevenueCat 自体には、ステージング用と本番用の別個の環境はありません。むしろ、ユーザーの基礎となるトランザクションは、サンドボックスと本番によって区別されます。

どの RevenueCat アプリでも、ストアからサンドボックス購入と製品購入の両方を行うことができます。ステージング用と本番用に別々のアプリがある場合は、RevenueCat で複数のプロジェクトを作成して、セットアップをミラーリングできます。

さらに、ユーザーも環境によって分離されません。同じユーザーが、アクティブなサンドボックスの購入とアクティブな製品の購入を同時に行うことができます。

docs/SDKの初期化と構成

ユーザー ID による購入の構成
アプリにユーザー認証システムがある場合は、構成時にユーザー ID を提供するか、後日、 .logIn().詳細については、ユーザーの識別に関するガイドをご覧ください。

docs/購入を復元

RevenueCat を使用すると、ユーザーはアプリ内購入を復元し、同じストア アカウント(Apple、Google、または Amazon アカウント) から以前に購入したコンテンツを再び有効にすることができます。
すべてのアプリに、ユーザーが復元メソッドをトリガーする何らかの方法を用意することをお勧めします。
ユーザーが購入したものにアクセスできなくなった場合 (例: アプリのアンインストール/再インストール、アカウント情報の紛失など) に備えて、Apple は復元メカニズムを必要とすることに注意してください。
docs/購入の復元

ゆーとゆーと

動作検証メモ

  • RevenueCatコンソールから、Offeringsを複数作り、並び替えた時の動作確認
  • AppStoreConnectで値段設定を変更した時の動作確認
ゆーとゆーと

サブスクプランの変更時の差額返金対応

https://developer.apple.com/jp/app-store/subscriptions/#ranking

アップグレード:ユーザーが、現在のサブスクリプションよりもサービスレベルの高いサブスクリプションを購入することを指します。
この場合、ユーザーはただちにアップグレードされ、元々のサブスクリプションの利用日数に基づく金額が返金されます。
追加のコンテンツや機能をユーザーがただちに利用できるようにしたい場合は、より高いランクのサブスクリプションとして設定し、ユーザーがアップグレードとして購入できるようにしてください。

上記はSANDBOX+ReveneCat上で動作確認できる?

ゆーとゆーと

Google Play 側の設定

アカウントの権限

サブスクの設定しようとして、権限がないとそもそもサブスクの画面に移動できない。
支払いの権限みたいなものを付与してもらう必要あり。

※注意: 「ユーザーの権限」と「支払いの権限」は別権限なこと注意。サブスク設定には「支払い側の権限」が必要。

ゆーとゆーと

APIアクセスも見れない。。
ここは権限付与してもらったら見れるようになる?

ゆーとゆーと

googleだと_が使えなくて-しかダメ。
(appleを_にしてるなら命名どうするか、どっちも-にするか)

ゆーとゆーと

googleの方が設定多い。

  • IDの命名をできるだけ両OSで合わせたい。
  • 一度設定すると変更不可が多いので慎重に。
ゆーとゆーと

RevenueCatのProductの設定も、DisplayName以外は変更不可。

ゆーとゆーと

手順に沿って設定したが、permittionが有効にならない。
これが原因かと思われるが、アプリ起動後の初期化処理でエラーが出る。

PlatformException (PlatformException(23, There is an issue with your configuration. Check the underlying error for more details., {code: 23, message: There is an issue with your configuration. Check the underlying error for more details., readableErrorCode: ConfigurationError, readable_error_code: ConfigurationError, underlyingErrorMessage: There's a problem with your configuration. None of the products registered in the RevenueCat dashboard could be fetched from the Play Store.


Google Playのクローズドテストを申請して、承認されるとチェック入った。
→同時にアプリ起動時のOfferingsとかの情報を取得できるようになった。
アプリをテストリリースが完了しないと動作確認すらできない事注意。

ゆーとゆーと

iOSのSANDBOX環境では、定期更新が初回だとうまくいくが、12回更新した後にもう一度サブスク開始するとうまく更新されない。

iphoneのサブスク管理から完全に消してから再度サブスク開始すると定期更新される用に戻った。

ゆーとゆーと
  • PCから直接実機buildするとSAND BOX環境が適用
  • TestFlightからだと実際のAppleIDがSANDBOXとして使用される→実質テストできない。
  • Firebase Distributionで配信するとSANDBOXアカウントで購入できた
ゆーとゆーと

初回起動時、ユーザーがサブスクに登録した事ない場合の Purchases.getCustomerInfo() の戻り値。

戻り値(json化)

当たり前だが殆ど空。

{
   "entitlements":{
      "all":{
         
      },
      "active":{
         
      }
   },
   "allPurchaseDates":{
      
   },
   "activeSubscriptions":[
      
   ],
   "allPurchasedProductIdentifiers":[
      
   ],
   "nonSubscriptionTransactions":[
      
   ],
   "firstSeen":"2023-11-22T01":"37":29Z,
   "originalAppUserId":<userID>,
   "allExpirationDates":{
      
   },
   "requestDate":"2023-11-22T01":"37":29Z,
   "latestExpirationDate":null,
   "originalPurchaseDate":null,
   "originalApplicationVersion":null,
   "managementURL":null
}
ゆーとゆーと

サブスク登録した状態での Purchases.getCustomerInfo() の戻り値。

戻り値(json化)
{
   "entitlements":{
      "all":{
         "<Entittlement ID>":{
            "identifier":"<Entittlement ID>",
            "isActive":true,
            "willRenew":true,
            "latestPurchaseDate":"2023-11-22T01":"43":43Z,
            "originalPurchaseDate":"2023-11-22T01":"43":49Z,
            "productIdentifier":"<Product ID>",
            "isSandbox":true,
            "ownershipType":"PURCHASED",
            "store":"APP_STORE",
            "periodType":"NORMAL",
            "expirationDate":"2023-11-22T02":"43":43Z,
            "unsubscribeDetectedAt":null,
            "billingIssueDetectedAt":null
         }
         // 以前に購読したことがあるプランがあればここに追加で記載される。
      },
      "active":{
         "<Entittlement ID>":{
            "identifier":"<Entittlement ID>",
            "isActive":true,
            "willRenew":true,
            "latestPurchaseDate":"2023-11-22T01":"43":43Z,
            "originalPurchaseDate":"2023-11-22T01":"43":49Z,
            "productIdentifier":"<Product ID>",
            "isSandbox":true,
            "ownershipType":"PURCHASED",
            "store":"APP_STORE",
            "periodType":"NORMAL",
            "expirationDate":"2023-11-22T02":"43":43Z,
            "unsubscribeDetectedAt":null,
            "billingIssueDetectedAt":null
         }
      }
   },
   "allPurchaseDates":{
      "<Product ID>":"2023-11-22T01":"43":43Z
   },
   "activeSubscriptions":[
      "<Product ID>"
   ],
   "allPurchasedProductIdentifiers":[
      "<Product ID>",
   ],
   "nonSubscriptionTransactions":[
      
   ],
   "firstSeen":"2023-11-22T01":"37":29Z,
   "originalAppUserId":<User ID>,
   "allExpirationDates":{
      "<Product ID>":"2023-11-22T02":"43":43Z
   },
   "requestDate":"2023-11-22T01":"43":55Z,
   "latestExpirationDate":"2023-11-22T02":"43":43Z,
   "originalPurchaseDate":"2013-08-01T07":"00":00Z,
   "originalApplicationVersion":1.0,
   "managementURL":"https":
}
ゆーとゆーと

Purchases.getOfferings(); すると、RevenueCatのOffering情報が取得される。以下のような戻り値が返される。

戻り値(json化)
{
    "all": {
        "<Offering ID>": {
            "identifier": "<Offering ID>",
            "serverDescription": "<Offering Description>",
            "metadata": {},
            "availablePackages": [
                {
                    "identifier": "$rc_weekly",
                    "packageType": "WEEKLY",
                    "product": {
                        "identifier": "<Product ID>",
                        "description": "<Product Description>",
                        "title":<Description>,
                        "price": 100.0,
                        "priceString":¥100,
                        "currencyCode": "JPY",
                        "introPrice": null,
                        "discounts": [],
                        "productCategory": "SUBSCRIPTION",
                        "defaultOption": null,
                        "subscriptionOptions": null,
                        "presentedOfferingIdentifier": null,
                        "subscriptionPeriod":P1W
                    },
                    "offeringIdentifier": "<Offering ID>"
                },
                {
                    // 他のPackage情報も上記と同様に記載される。割愛。
                }
            ],
            "lifetime": null,
            "annual": {
                "identifier": "$rc_annual",
                "packageType": "ANNUAL",
                "product": {
                    "identifier": "<Product ID>",
                    "description": "<Product Description>",
                    "title":<Description>,
                    "price": 1200.0,
                    "priceString":¥1200,
                    "currencyCode": "JPY",
                    "introPrice": null,
                    "discounts": [],
                    "productCategory": "SUBSCRIPTION",
                    "defaultOption": null,
                    "subscriptionOptions": null,
                    "presentedOfferingIdentifier": null,
                    "subscriptionPeriod":P1Y
                },
                "offeringIdentifier": "<Offering ID>"
            },
            "sixMonth": null,
            "threeMonth": null,
            "twoMonth": null,
            "monthly": null,
            "weekly": {
                "identifier": "$rc_weekly",
                "packageType": "WEEKLY",
                "product": {
                    "identifier": "<Product ID>",
                    "description": "<Product Description>",
                    "title":<Description>,
                    "price": 100.0,
                    "priceString":¥100,
                    "currencyCode": "JPY",
                    "introPrice": null,
                    "discounts": [],
                    "productCategory": "SUBSCRIPTION",
                    "defaultOption": null,
                    "subscriptionOptions": null,
                    "presentedOfferingIdentifier": null,
                    "subscriptionPeriod":P1W
                },
                "offeringIdentifier": "<Offering ID>"
            }
        },
    },
    "current": {
        "identifier": "<Offering ID>",
        "serverDescription": "<Offering Description>",
        "metadata": {},
        "availablePackages": [
            // 上記のall配下と同じ情報のため割愛
        ],
    }
}

現在のOffring情報だけ欲しいなら、.currentを使用して取得する。

      final offerings = await Purchases.getOfferings();
      final currentOffering = offerings.current;
ゆーとゆーと

SANDBOXの設定で3分毎に定期購読設定してるのに1時間毎になる

ゆーとゆーと

androidの実機debugにてpurchasePackage実行時に、以下のエラー。
→ 手元で動かすのはNGで、クローズドテストで配信したアプリ上じゃないとエラーになる?

🤖‼️ BillingWrapper purchases failed to update: DebugMessage: Please ensure the app is signed correctly.. ErrorCode: DEVELOPER_ERROR.null
🤖‼️ PurchasesError(code=PurchaseInvalidError, underlyingErrorMessage=Error updating purchases. DebugMessage: Please ensure the app is signed correctly.. ErrorCode: DEVELOPER_ERROR., message='One or more of the arguments provided are invalid.')


クローズドテストで配信しているアプリを動かしてみるとエラーなく課金モーダルが表示された。
やっぱりクローズドテストで配信したアプリでしか課金機能を検証できない
→ 動作確認とかしたいだけでも、ある程度形になったアプリをStoreに上げる必要あり。
→ 動作確認用にアプリを最低限作るのはいいとして、申請するまでのStoreの各種設定(スクショとか諸々の設定)が面倒すぎる。。。

ゆーとゆーと

https://www.revenuecat.com/docs/subscriber-attributes
Subscriber Attributesの予約語のkey指定の記載は以下。

dart
+ Purchases.setAttributes({ "\$displayName" : "hoge", "\$email" : "hoge@hoge.com" });

// 以下のように指定すると、予約語が反映されない(同じattributeがコンソールに表示されてしまう)ので注意。
- Purchases.setAttributes({ "displayName" : "hoge", "email" : "hoge@hoge.com" });

ゆーとゆーと

purchases_flutterを導入して、実際には課金機能を実装せずにandroidの内部テストで配信すると「アプリ内課金あり」と表示されてしまう。
iosのTestFlightでは確認できなかったが、おそらく同じように課金対象となる可能性がある。

androidのApp Bundleの中身を見ると、Billingの記載が入っている。おそらくこれが原因かと思われる。

課金機能を実際には実装していない&Storeの課金設定をしていなくても、purchases_flutterを追加してるだけで勝手にBillingの権限が追加されてしまう。

https://qiita.com/aguroshou0413/items/dfb53ba145f3cbb3e8c3#googleplaybillinglibraryのエラー

上記の記事を参考に、flutterアプリのAndroidManifest.xmlを確認してみたが、Billingの記載はどこにも存在しない。。。検索をかけても存在しない。
他にも色々検索してみたが解決方法が見つからなかったので、アプリ上からpurchases_flutterを削除する対応とすると、App BundleにBillingの記載がなくなり、内部テスト上でも「アプリ内課金あり」の記載がなくなったことが確認できた。