207 Tech Blog

テクノロジーで物流を変える 207 (ニーマルナナ) 株式会社のテックブログ

Expo InAppPurchases から RevenueCat に移行

こんにちは。207 でソフトウェアエンジニアをやっている原口 (id:nagamejun)です。 今回は Expo InAppPurchases から RevenueCat に移行した内容について話します。

1年前に Expo の InAppPurchases に移行しましたが今回 RevenueCat に移行しました。その理由と導入方法・注意点についてまとめました。

tech.207-inc.com

expo SDK 45 に update 出来ない

207では先日のエントリーの通り EXPO のサポート期間中にバージョンアップを進めています。

tech.207-inc.com

expo SDK 45 に update した際に Android にて課金しようとするとエラーが発生し、課金できないという不具合が発生しました。issueはこちら

github.com

他にも解決してない issue があり brent氏 が expo-in-app-purchases は積極的に取り組んでいないので、revenuecat 使うと良いと思う(意訳)とコメントしてました。

i'd recommend trying out revenuecat's integration for a more fully featured option than what is available as of june 2022 in expo-in-app-purchases. we aren't actively working on this module at the moment so you will likely have more success with revenuecat's library. see this post for more info: https://www.revenuecat.com/blog/using-revenuecat-with-expos-managed-workflow/

Ruby3 に update 出来ない

サーバーサイドで receipt 情報のチェックに candy_check という gem を使ってましたが、メンテされてなく Ruby3 に update 出来ない状況です。

こうした問題があったので RevenueCat の導入をしました。

RevenueCatとは

iOSAndroid アプリの課金処理をサポートするプラットフォームです。
App Store / Play Store での課金処理を実装する際に、バックエンドで receipt の検証などを代わりに実施してくれます。

 

導入

$10,000 MTR 以上ある場合は課金する必要があるのでポチります。

今回は既存のプロジェクトがあるので移行ガイドを元に導入します。

RevenueCat への移行は2つの方法があり、データベースに base64 のレシートや購入トークンを全て保存しているならサーバーサイドで移行できます。

今回はクライアントサイドで移行する手順を記載します。

RevenueCat のセットアップ

実装の前に RevenueCat の設定をします。事前に Entitlements / Offering / Producs の説明を本家のドキュメントで確認しておくと良さそうです。

Configuring in-app products – RevenueCat 

Products(製品)の作成

Identifier には App Store , Play Store で登録した製品 ID やアイテム ID を設定
Entitlements(資格)の作成

作成した Product を Entitlement に紐付けしている状態
Package(パッケージ)の作成

今回は Package の Identifier は Monthly を選択します

今回は Package の Identifier は Monthly を選択します
Offering(提供品)の作成

Attach して Products を紐付けると完了です

Attach して Products を紐付けると完了です

実装

react: 18.0.0

expo: 45.0.0

react-native-purchases: 5.0.2

react-native-purchases 5系で破壊的変更が入ってます。当時 document が更新されてなかったので Release 5.0.0 · RevenueCat/react-native-purchases · GitHub で確認

 

ライブラリを追加

yarn add react-native-purchases

Expo Bare Workflow の場合

iOS

cd ios && pod install
Android 定義されてない場合は追加
 
./android/app/src/main/AndroidManifest.xml
<uses-permission android:name="com.android.vending.BILLING" />

SDKの初期化

useEffect(() => {
  const revenueCatApiKey = Platform.select({
      ios: ENV.revenueCat.iosApiKey,
      default: ENV.revenueCat.androidApiKey,
    });
  Purchases.configure({ apiKey: revenueCatApiKey });
}, []); 

クライアントサイドで古いシステムから移行する場合は、購読者を正しく追跡するために、RevenueCat SDK に1度だけ同期するように指示する必要があります。Purchases.syncPurchases(); を使います。初期化処理に書いておき、sync が終わってる場合は Purchases.logIn で customerInfo を取得します。

const SYNCED_PURCHASES_KEY = 'synced_purchases';
const isSynced: string | null = await AsyncStorage.getItem(
  SYNCED_PURCHASES_KEY,
);
let latestExpirationDate: string | null = '';
if (!isSynced) {
  await Purchases.syncPurchases();
  await AsyncStorage.setItem(SYNCED_PURCHASES_KEY, '1');
  const customerInfo = await Purchases.getCustomerInfo();
  ...
} else {
  const { customerInfo } = await Purchases.logIn(subscriberId);
  ...
}

購入ボタン

購入処理は Purchases.purchaseProduct(productIdentifier: string) を実行します。 RevenueCat では明示的に指定されてない限り日付は UTC で表されます。ここでは latestExpirationDate を JST に変換しています。 

const purchaseItemAsync = async (): Promise<void> => {
  setLoading(true);
  const productIdentifier =Platform.OS === 'ios' ? 'iosIdentifier': 'androidIdentifier';
  try {
    const { customerInfo } = await Purchases.purchaseProduct(productIdentifier);
    if (Object.entries(customerInfo.entitlements.active).length > 0) {
      const subscriptionExpiresAt = customerInfo.latestExpirationDate ? utcToJst(customerInfo.latestExpirationDate) : null;
... } } catch (e) { ... }

復元ボタン

復元処理は Purchases.restorePurchases() を実行します。

const restorePurchases = async () => {
  try {
    setLoading(true);
    const customerInfo = await Purchases.restorePurchases();
    if (Object.entries(customerInfo.entitlements.active).length > 0) {
      const subscriptionExpiresAt = customerInfo.latestExpirationDate
        ? utcToJst(customerInfo.latestExpirationDate)
        : null;
      ...
      successBar.show({ title: '復元しました' });
    } else {
      errorBar.show({ title: '復元対象がありません' });
    }
  } catch (e) {
    errorBar.show({ title: '復元処理に失敗しました' });
    ...
  }
  setLoading(false);
};

顧客情報が更新されたときに呼び出されるリスナー関数を設定します。updateExpiresAt 関数でアプリ内の state が更新できます。

const customerInfoUpdateListener: CustomerInfoUpdateListener = useCallback(
(customerInfo) => {
  updateExpiresAt(customerInfo.latestExpirationDate);
},[]);

useEffect(() => {
  Purchases.addCustomerInfoUpdateListener(customerInfoUpdateListener);
  return () => {
    Purchases.removeCustomerInfoUpdateListener(customerInfoUpdateListener);
  };
});

以上で終わりです。

必要があればバックエンドの方でも RevenueCat の API を叩いて有料コンテンツの出し分けを制御します。

REST API の Document Get or Create Subscriber

 

動作確認

TestFlight、内部テストでの購入が正常に動作しているか RevenueCat のサンドボックスモードで確認できます。

sandbox

sandbox

おわりに

 こんな感じで簡単に置き換えができました。課金コンテンツですが課金状況の分析なども可能になります。個人開発であれば無料枠で十分だと思うので気になる方は利用してみて下さい。

 

いつもの

207株式会社では、レガシーな物流業界の変革に挑む配達員向け効率化アプリ「TODOCUサポーター」を開発しています。 フロントエンドの開発をしたいメンバーを大絶賛募集中です!

もし少しでもご興味がありましたら以下のnotionをご覧ください!

207-inc.super.site