Expo+ReactNativeの課金②〜react-native-purchasesでApp内課金をする

この記事では、App内課金について説明していきます。

下記記事の続きです。

Expo+ReactNativeの課金①〜App内課金とApp外課金の違いについて

App外課金の実装をしたい方は下記で記載しております。

Expo+ReactNativeの課金③〜stripe/stripe-react-nativeでApp外課金をする

また、私はExpoでアプリを開発していますが、こちらはExpoを使っていないユーザでもできるはずです。

最終的なゴール

私が開発しているアプリのLangJournalで実装したものを例に紹介していきます。

今回は、「プレミアム会員」機能の実装をします。支払いは「月額・年額」を選べます。

また、iOSとAndroidでできるようにします。つまり4種類のサブスクリプションを設定する必要があります。

RevenueCat Accountの設定

支払いはRevenueCatを通して行います。設定方法はこのドキュメントを見ながら進めていきます。私は、結構詰まったので一つずつ解説していきます。上のドキュメントと、このブログの記事を両方見ながら進めていくといいかと思います。

1:Create a RevenueCat Account

ここからRevenueCatのアカウントを作成してください。

2:Project and App Configuration

私はプロジェクト名を「LangJournal」にしました。次にiOSとAndroidでそれぞれ下記フォームに詳細情報を入力するのですが、これが結構大変です。

AppNameやBundleIDはわかるかと思うのですが、下記も取得する必要があります。

・Shared Secret(iOS)(ここを参考にしてください)
・Service Credentials (Android)(ここを参考にしてください)

最終的に、下記のようになればOKです。

3:Product Configuration

まずはApple StoreとGoogle Playでサブスクリプションを作成する必要があります。

Apple Storeはこちらを参考にしてください。

最終的にAppStoreConnectの「サブスクリプション」

サブスクリプションの詳細

上記のようになればOKです。私は多言語化しているのでApp Storeのローカリゼーションで英語も設定していますが、日本語のみの場合、必要ありません。

Google Playはこちらを参考にしてください。

最終的に上記のようになればOKです。

続いて、RevenueCat側の設定をしていきます。詳細はこちら

最終的に下記のようになればOKです。

Entitlements

Products

Offering

これでブラウザを使った設定は完了です。ここからコーディングをしていきます。

react-native-purchasesのインストール

Terminal
yarn add react-native-purchases

初期化

まずは初期化します。アプリ立ち上げ時に呼ぶと良いでしょう。私はApp.tsxで呼んでいます。

App.tsx
import Purchases from 'react-native-purchases';

// 中略
const initPurchases = useCallback(() => {
    // react-native-purchasesはreact-native-webに対応していません。
    if (Platform.OS === 'web') return;

    // setLogLevelでVERBOSEに設定しておくと細かいログが出ます。
    // 開発中はこれにしておくことをお勧めします
    // 公式でもVERBOSEにするように勧められていました。
    // 実際に私もログでかなり助けられました。
    Purchases.setLogLevel(LOG_LEVEL.VERBOSE);
    if (Platform.OS === 'ios') {
      // process.env.IOS_PURCHASESとprocess.env.ANDROID_PURCHASESは、RevenueCatのAPI keyです。
      Purchases.configure({ apiKey: process.env.IOS_PURCHASES! });
    } else if (Platform.OS === 'android') {
      Purchases.configure({
        apiKey: process.env.ANDROID_PURCHASES!,
      });
    }
  }, []);

  useEffect(() => {
    initPurchases();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

サブスクリプションの取得と購入

プレミアム会員登録画面です。私は下記のようなUIにしております。

大事な箇所だけソースを抜粋して、説明していきます。

BecomePremiumScreen.tsx
import Purchases, {
  PurchasesOffering,
  PurchasesPackage,
} from 'react-native-purchases';

import Purchases, {
  PurchasesOffering,
  PurchasesPackage,
} from 'react-native-purchases';

// 中略

const [currentOffering, setCurrentOffering] =
  useState<PurchasesOffering | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [purchasesPackage, setPurchasesPackage] = useState<PurchasesPackage>();

// すでにプレミアム会員になっているかどうか判定しています
const checkPremium = (customerInfo: CustomerInfo) => {
  if (typeof customerInfo.entitlements.active.Premium !== 'undefined') {
    return true;
  }
};

const onPressItem = useCallback((v) => {
  setPurchasesPackage(v);
}, []);

useEffect(() => {
  const f = async () => {
    const offerings = await Purchases.getOfferings();

    if (
      offerings.current !== null &&
      offerings.current.availablePackages.length !== 0
    ) {
      // サブスクリプションの一覧を取得してstateに保存(私の場合、年払いと月払いの2つが入っています)
      setCurrentOffering(offerings.current);
    }
  };

  f();
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const onPressSubmit = useCallback(async () => {
  if (!purchasesPackage || isLoading) return;
  setIsLoading(true);
  try {
    // ここで購入しています
    const {customerInfo} = await Purchases.purchasePackage(purchasesPackage);
    if (checkPremium(customerInfo)) {
      setIsPremium(true);
      navigation.goBack();
    }
  } catch (e: any) {
    if (!e.userCancelled) {
      console.log(e);
      Toast.show(I18n.t('becomePremium.error'), {
        duration: Toast.durations.LONG,
        position: Toast.positions.CENTER,
      });
    }
  } finally {
    setIsLoading(false);
  }
}, [isLoading, navigation, purchasesPackage, setIsPremium]);

// Restoreの実装をしています
const onPressRestore = useCallback(async () => {
  if (isLoading) return;
  setIsLoading(true);
  try {
    const restore = await Purchases.restorePurchases();
    if (checkPremium(restore)) {
      setIsPremium(true);
      navigation.goBack();
    }
  } catch (e) {
    console.log(e);
    Toast.show(I18n.t('becomePremium.error'), {
      duration: Toast.durations.LONG,
      position: Toast.positions.CENTER,
    });
  } finally {
    setIsLoading(false);
  }
}, [isLoading, navigation, setIsPremium]);

// 中略

// ラジオボックス表示の箇所
currentOffering &&
  currentOffering.availablePackages.map((item) => (
    <View key={item.identifier}>
      <RadioBox
        checked={item.identifier === purchasesPackage?.identifier}
        textComponent={
          <View style={styles.priceContainer}>
            <Text>{I18n.t(`becomePremium.${item.packageType}`)}</Text>
            <Text>{item.product.priceString}</Text>
          </View>
        }
        onPress={() => {
          onPressItem(item);
        }}
      />
    </View>
  ));
     

まとめ

以上になります。RevenueCatの設定が結構戸惑うかと思いますが、実装コードは結構少なくできます。こちらのブログが少しでも皆様の参考になれば幸いです。詰まった箇所ありましたら、コメントください。できる限り回答したいと思います。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です