この記事では、App内課金について説明していきます。
下記記事の続きです。
Expo+ReactNativeの課金①〜App内課金とApp外課金の違いについて
App外課金の実装をしたい方は下記で記載しております。
Expo+ReactNativeの課金③〜stripe/stripe-react-nativeでApp外課金をする
また、私はExpoでアプリを開発していますが、こちらはExpoを使っていないユーザでもできるはずです。
私が開発しているアプリのLangJournalで実装したものを例に紹介していきます。
今回は、「プレミアム会員」機能の実装をします。支払いは「月額・年額」を選べます。
また、iOSとAndroidでできるようにします。つまり4種類のサブスクリプションを設定する必要があります。
支払いはRevenueCatを通して行います。設定方法はこのドキュメントを見ながら進めていきます。私は、結構詰まったので一つずつ解説していきます。上のドキュメントと、このブログの記事を両方見ながら進めていくといいかと思います。
ここからRevenueCatのアカウントを作成してください。
私はプロジェクト名を「LangJournal」にしました。次にiOSとAndroidでそれぞれ下記フォームに詳細情報を入力するのですが、これが結構大変です。
AppNameやBundleIDはわかるかと思うのですが、下記も取得する必要があります。
・Shared Secret(iOS)(ここを参考にしてください)
・Service Credentials (Android)(ここを参考にしてください)
最終的に、下記のようになればOKです。
まずはApple StoreとGoogle Playでサブスクリプションを作成する必要があります。
Apple Storeはこちらを参考にしてください。
最終的にAppStoreConnectの「サブスクリプション」
サブスクリプションの詳細
上記のようになればOKです。私は多言語化しているのでApp Storeのローカリゼーションで英語も設定していますが、日本語のみの場合、必要ありません。
Google Playはこちらを参考にしてください。
最終的に上記のようになればOKです。
続いて、RevenueCat側の設定をしていきます。詳細はこちら。
最終的に下記のようになればOKです。
Entitlements
Products
Offering
これでブラウザを使った設定は完了です。ここからコーディングをしていきます。
yarn add react-native-purchases
まずは初期化します。アプリ立ち上げ時に呼ぶと良いでしょう。私は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にしております。
大事な箇所だけソースを抜粋して、説明していきます。
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の設定が結構戸惑うかと思いますが、実装コードは結構少なくできます。こちらのブログが少しでも皆様の参考になれば幸いです。詰まった箇所ありましたら、コメントください。できる限り回答したいと思います。
LangJournalは、日記を書くことで英語やフランス語などの外国語を学べるアプリです。英語学習に興味がある方や、私が開発したこのアプリに関心を持っている方は、ぜひインストールしてお試しください。
LangJournalのサイトはこちら