I want to add In-App purchases (subscriptions) to my react-native app. I use react-native-iap. I added subscriptions in https://appstoreconnect.apple.com/ Features -> Subscriptions, I have 1 group with 2 subscriptions.
I added In-App purchase capability in Xcode.
My subscriptions screen:
import { useEffect } from 'react';
import { ImageBackground, Platform, SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, View } from 'react-native';
import { PurchaseError, requestSubscription, useIAP, withIAPContext } from 'react-native-iap';
import { ParamListBase, useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import AppButton from 'components/AppButton';
import CustomIcon from 'components/CustomIcon/CustomIcon';
import InstitutionExpiredModal from 'components/InstitutionExpiredModal';
import Tabs, { Tab } from 'components/Tabs';
import { COLORS } from 'constants/colors';
const subscriptionSkus = Platform.select({
ios: ['sub...', 'sub...'],
android: [],
}) as string[];
function MembershipScreen() {
const navigation = useNavigation<NativeStackNavigationProp<ParamListBase>>();
const options = [
'Text',
'Text',
'Text',
'Text',
'Text',
'Text',
];
const { connected, subscriptions, getSubscriptions, currentPurchase, finishTransaction } = useIAP();
console.log(subscriptions);
const handleBuySubscription = async (productId: string) => {
try {
await requestSubscription({
sku: productId,
});
} catch (error) {
if (error instanceof PurchaseError) {
console.error(error.code, error.message);
} else {
console.error('handleBuySubscription', JSON.stringify(error));
}
}
};
useEffect(() => {
getSubscriptions({ skus: subscriptionSkus });
}, []);
const tabs: Tab[] = [
{
title: 'Monthly',
Content: () =>
subscriptions.length ? (
<View className="mt-2">
<Text className="mb-[42px] text-center font-blender-bold text-[26px] lowercase text-black-100">
${subscriptions[0].price}/ {subscriptions[0].subscriptionPeriodUnitIOS}
</Text>
<AppButton onPress={() => handleBuySubscription(subscriptions[0].productId)} text="Subscribe" />
</View>
) : null,
},
{
title: 'Annual',
Content: () =>
subscriptions.length ? (
<View className="mt-2">
<Text className="mb-2 text-center font-blender-bold text-[26px] lowercase text-black-100">
${subscriptions[1].price}/ {subscriptions[1].subscriptionPeriodUnitIOS}
</Text>
<AppButton onPress={() => {}} text="Subscribe" />
</View>
) : null,
},
];
return (
<SafeAreaView className="h-full bg-white">
<StatusBar barStyle="dark-content" />
<ImageBackground
source={require('../../../assets/images/mainPattern.png')}
resizeMode="contain"
className="absolute left-0 top-0 mt-24 h-full w-full flex-1 grow"
/>
<ScrollView className="px-6" contentContainerStyle={styles.scrollContent} alwaysBounceVertical={false}>
<View className="relative mb-9 mt-4 flex-row items-center justify-center">
<CustomIcon name="back-button" size={23} className="absolute left-0" onPress={navigation.goBack} />
<Text className="font-blender-heavy text-[24px] uppercase text-black-100">Membership</Text>
</View>
<Text className="mb-2 text-center font-blender-bold text-[20px] text-black-100">You will get</Text>
{options.map((option, index) => {
return (
<View key={index} className="mt-4 flex-row items-center">
<CustomIcon name="check-circle" color={COLORS.pink} size={18} />
<Text className="ml-6 font-blender-medium text-[16px] text-black-100">{option}</Text>
</View>
);
})}
<Text className="mb-5 mt-12 text-center font-blender-bold text-[20px] text-black-100">Choose your plan</Text>
<View style={[styles.card, Platform.OS === 'ios' ? styles.shadowIOS : styles.shadowAndroid]}>
<Tabs tabs={tabs} containerClassName="flex-none" />
</View>
<Text className="mb-2 mt-auto text-center font-blender-bold text-[14px] text-black-100 underline">
Terms of service
</Text>
</ScrollView>
<InstitutionExpiredModal canShow />
</SafeAreaView>
);
}
I successfully get an array of my subscriptions:
[
{
"countryCode": "USA",
"currency": "USD",
"description": "...",
"discounts": [],
"introductoryPrice": "",
"introductoryPriceAsAmountIOS": "",
"introductoryPriceNumberOfPeriodsIOS": "",
"introductoryPricePaymentModeIOS": "",
"introductoryPriceSubscriptionPeriodIOS": "",
"localizedPrice": "10.99 $",
"platform": "ios",
"price": "10.99",
"productId": "sub...",
"subscriptionPeriodNumberIOS": "1",
"subscriptionPeriodUnitIOS": "MONTH",
"title": "...",
"type": "subs"
},
{
"countryCode": "USA",
"currency": "USD",
"description": "...",
"discounts": [],
"introductoryPrice": "",
"introductoryPriceAsAmountIOS": "",
"introductoryPriceNumberOfPeriodsIOS": "",
"introductoryPricePaymentModeIOS": "",
"introductoryPriceSubscriptionPeriodIOS": "",
"localizedPrice": "28.99 $",
"platform": "ios",
"price": "28.99",
"productId": "sub...",
"subscriptionPeriodNumberIOS": "1",
"subscriptionPeriodUnitIOS": "YEAR",
"title": "...",
"type": "subs"
}
]
But when I try to make subscribe via this function:
const handleBuySubscription = async (productId: string) => {
try {
await requestSubscription({
sku: productId,
});
} catch (error) {
if (error instanceof PurchaseError) {
console.error(error.code, error.message);
} else {
console.error('handleBuySubscription', JSON.stringify(error));
}
}
};
I get next error:
{"code":"E_UNKNOWN","message":"An unknown error occurred","domain":"SKErrorDomain","userInfo":{"NSUnderlyingError":{"code":"500","message":"underlying error","domain":"ASDErrorDomain","userInfo":{"NSUnderlyingError":{"code":"100","message":"underlying error","domain":"AMSErrorDomain","userInfo":{"NSLocalizedFailureReason":"The authentication failed.","NSLocalizedDescription":"Authentication Failed","NSMultipleUnderlyingErrorsKey":[null,null]}....
How can I fix it? I try to do subscribe by sandbox test user
My package.json:
{
"name": "...",
"version": "0.0.1",
"private": true,
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
"test": "jest",
"start::clearCache": "yarn start --reset-cache"
},
"dependencies": {
"@hookform/resolvers": "^3.0.0",
"@invertase/react-native-apple-authentication": "^2.2.2",
"@react-native-async-storage/async-storage": "^1.17.12",
"@react-native-firebase/app": "^17.5.0",
"@react-native-firebase/messaging": "^17.5.0",
"@react-native-google-signin/google-signin": "^10.0.1",
"@react-native-picker/picker": "^2.4.9",
"@react-navigation/bottom-tabs": "^6.5.7",
"@react-navigation/native": "^6.1.6",
"@react-navigation/native-stack": "^6.9.12",
"@shopify/react-native-skia": "^0.1.185",
"@tanstack/react-query": "^4.28.0",
"axios": "^1.3.4",
"classnames": "^2.3.2",
"d3": "^7.8.4",
"date-fns": "^2.29.3",
"jotai": "^2.0.3",
"nativewind": "^2.0.11",
"react": "18.2.0",
"react-hook-form": "^7.43.9",
"react-native": "^0.71.5",
"react-native-biometrics": "^3.0.1",
"react-native-email-link": "^1.14.5",
"react-native-gesture-handler": "^2.9.0",
"react-native-haptic-feedback": "^2.0.2",
"react-native-iap": "^12.10.5",
"react-native-localize": "^2.2.6",
"react-native-plaid-link-sdk": "^10.1.0",
"react-native-reanimated": "^3.1.0",
"react-native-restart": "^0.0.27",
"react-native-safe-area-context": "^4.5.0",
"react-native-screens": "^3.20.0",
"react-native-splash-screen": "^3.3.0",
"react-native-uuid": "^2.0.1",
"react-native-vector-icons": "^9.2.0",
"react-native-webview": "^12.0.2",
"zod": "^3.21.4"
},
"devDependencies": {
"@babel/core": "^7.21.4",
"@babel/preset-env": "^7.21.4",
"@babel/runtime": "^7.21.0",
"@react-native-community/eslint-config": "^3.2.0",
"@total-typescript/ts-reset": "^0.4.2",
"@tsconfig/react-native": "^2.0.2",
"@types/d3": "^7.4.0",
"@types/jest": "^29.5.0",
"@types/react": "^18.0.24",
"@types/react-native-vector-icons": "^6.4.13",
"@types/react-test-renderer": "^18.0.0",
"babel-jest": "^29.5.0",
"babel-plugin-module-resolver": "^5.0.0",
"eslint": "^8.37.0",
"eslint-plugin-ft-flow": "^2.0.3",
"eslint-plugin-simple-import-sort": "^10.0.0",
"jest": "^29.5.0",
"metro-react-native-babel-preset": "0.73.8",
"prettier": "^2.8.7",
"prettier-plugin-tailwindcss": "^0.2.7",
"react-native-dotenv": "^3.4.8",
"react-test-renderer": "18.2.0",
"tailwindcss": "^3.3.1",
"typescript": "^5.0.3"
},
"jest": {
"preset": "react-native"
}
}