iOS React Native 中 RevenueCat 无法触发支付
结论
问题更可能出在购买流程发生异常后,错误被代码静默吞掉,而不是按钮本身没有响应。
当前实现有以下问题:
Purchases.getOfferings()写在try...catch外。审核环境中的请求一旦失败,函数会直接中断,界面也不会显示任何提示。- 代码没有检查
offerings.current和availablePackages是否存在。 catch没有记录错误,因此无法判断问题来自商品配置、网络、StoreKit,还是 RevenueCat 配置。- 即使购买失败,代码仍会执行
this.setState({isPremium: true}),导致用户被错误标记为 Premium。 customerInfo, productIdentifier = ...使用的是 JavaScript 逗号表达式,并非解构赋值,无法正确取得购买结果。- 如果
AsyncStorage中缺少用户数据,或者数据结构不符合预期,代码也会在发起购买前报错。
建议先修正代码,加入用户可见的错误提示和诊断日志,再检查 App Store Connect、RevenueCat Offering,以及审核环境使用的账号和网络条件。
修正购买代码
下面的写法处理了用户数据、Offering、Package 和购买请求中可能出现的异常。具体返回字段仍应以当前使用的 react-native-purchases 版本类型定义为准。
async buyProduct() {
if (this.state.isLoading) {
return;
}
this.setState({ isLoading: true });
try {
const userJson = await AsyncStorage.getItem('user');
const user = userJson ? JSON.parse(userJson) : null;
const email = user?.user?.email;
if (email) {
await Purchases.setAttributes({ email });
}
const offerings = await Purchases.getOfferings();
const currentOffering = offerings.current;
if (!currentOffering) {
throw new Error('No current RevenueCat offering is available.');
}
const selectedPackage = currentOffering.availablePackages?.[0];
if (!selectedPackage) {
throw new Error('The current offering contains no available packages.');
}
const purchaseResult = await Purchases.purchasePackage(selectedPackage);
const customerInfo = purchaseResult.customerInfo;
const isPremium =
Object.keys(customerInfo.entitlements.active).length > 0;
this.setState({ isPremium });
if (!isPremium) {
Alert.alert(
'Purchase not activated',
'The purchase completed, but no active entitlement was found.'
);
}
} catch (error) {
console.error('RevenueCat purchase failed:', {
message: error?.message,
code: error?.code,
userCancelled: error?.userCancelled,
underlyingErrorMessage: error?.underlyingErrorMessage,
});
this.setState({ isPremium: false });
if (!error?.userCancelled) {
Alert.alert(
'Unable to complete purchase',
error?.message || 'Please try again later.'
);
}
} finally {
this.setState({ isLoading: false });
}
}
请求期间还应禁用按钮,并显示明确的加载状态,避免审核人员点击后只看到静止的页面:
<View style={{ width: '90%', marginTop: 20 }}>
<TouchableOpacity
style={[
styles.button,
{
alignSelf: 'center',
backgroundColor: '#eeeeee',
padding: 10,
width: '100%',
borderRadius: 20,
marginBottom: 10,
opacity: this.state.isLoading ? 0.6 : 1,
},
]}
disabled={this.state.isLoading}
onPress={this.buyProduct}
>
<Text
style={{
color: '#333',
textAlign: 'center',
fontWeight: 'bold',
}}
>
{this.state.isLoading
? 'Loading…'
: global.I18n.t('payNow')}
</Text>
</TouchableOpacity>
</View>
如果使用 class component,还要确认 buyProduct 已正确绑定。例如:
this.buyProduct = this.buyProduct.bind(this);
也可以直接将它定义为箭头函数:
buyProduct = async () => {
// ...
};
不要依赖第一个 Package
使用 availablePackages[0],会让购买逻辑依赖数组顺序。更稳妥的做法是根据 RevenueCat Package 类型或产品标识选择目标套餐:
const selectedPackage =
offerings.current?.monthly ||
offerings.current?.availablePackages?.find(
item => item.product.identifier === 'your_product_identifier'
);
这些字段是否存在,取决于 RevenueCat 后台的 Package 配置。选中套餐后仍需检查空值,不能直接将结果传给 purchasePackage()。
检查 App Store Connect 配置
本地或 TestFlight 可以购买,并不表示 App Review 使用的沙盒环境一定能加载同一商品。需要逐项确认:
- Bundle ID、RevenueCat iOS API Key 和 App Store Connect 中的应用对应同一个应用。
- RevenueCat Product 使用的 product identifier 与 App Store Connect 完全一致。
- Product 已加入正确的 Offering,而且该 Offering 已设为 Current。
- Offering 中确实有可购买的 Package。
- 订阅商品处于可提交或可审核的有效状态,并与当前 App 版本一起提交审核。实际可选状态取决于 App Store Connect 当前流程。
- 付费应用协议、税务信息和银行信息没有待处理问题。
- 商品价格、销售地区和本地化信息均已配置完整。
- 审核人员所在的 storefront 可以获取该商品。
- RevenueCat entitlement 已正确关联到对应 Product。
还要在提交页面确认,相关 In-App Purchase 已附加到当前版本。首次提交新订阅时尤其容易漏掉这一步。
排除账号和后端条件
审核团队使用的账号、网络和设备状态通常与开发者不同。如果购买页面依赖自己的登录系统,还需要测试以下情况:
- 新注册且从未购买过的账号;
- 用户资料不完整,或者
AsyncStorage中没有缓存数据; - IPv6 网络、较慢的网络或请求超时;
- 后端是否限制地区、IP、账号角色或测试账号;
- iPad 布局中是否存在透明视图、Loading 遮罩或 Modal 拦截触摸;
- 隐私同意、登录状态或远程配置是否阻止 Offering 加载。
重点检查 isLoading 对应的遮罩层。如果请求失败后没有可靠地将它恢复为 false,不可见或半透明的全屏视图可能会一直拦截后续点击。
增加诊断日志
初始化 RevenueCat 时可以启用详细日志,但不要记录密码、完整收据或其他敏感信息:
import Purchases, { LOG_LEVEL } from 'react-native-purchases';
Purchases.setLogLevel(LOG_LEVEL.DEBUG);
同时记录以下非敏感状态:
const offerings = await Purchases.getOfferings();
console.log('RevenueCat offerings diagnostics:', {
hasCurrentOffering: Boolean(offerings.current),
packageCount: offerings.current?.availablePackages?.length ?? 0,
packageIdentifiers:
offerings.current?.availablePackages?.map(
item => item.identifier
) ?? [],
});
开发者可能无法直接读取生产版本中的日志,因此可以把购买阶段和错误码发送到现有的崩溃报告或遥测平台,但必须遵守隐私政策。建议区分这些阶段:
purchase_button_pressed
customer_attributes_updated
offerings_loaded
package_selected
purchase_started
purchase_completed
purchase_failed
这些记录可以帮助确认审核人员的点击是否到达 JavaScript,以及购买流程停在哪一步。
改进订阅状态判断
如果应用只通过某个特定 entitlement 授予权限,最好直接检查该标识,不要把任意 active entitlement 都视为 Premium:
async checkSubscriptionStatus() {
try {
const customerInfo = await Purchases.getCustomerInfo();
const isPremium = Boolean(
customerInfo.entitlements.active['premium']
);
this.setState({ isPremium });
return isPremium;
} catch (error) {
console.error('Error fetching customer info:', error);
this.setState({ isPremium: false });
return false;
}
}
将 'premium' 替换为 RevenueCat 后台实际配置的 entitlement identifier。
重新提交审核时提供的信息
修复后,可以在 App Review Notes 中说明:
- 打开付费墙的具体路径;
- 可供审核使用的账号和登录步骤;
- 点击按钮后应出现系统购买弹窗;
- 用于测试的订阅商品名称;
- 购买是否依赖网络;
- 已修复购买异常不显示提示的问题;
- 必要时附上真机录屏。
如果仍然被拒,可以请审核团队提供点击时间、截图或录屏,并确认当时是否出现过加载状态或错误提示。完成上述修改后,即使 StoreKit 或 Offering 在审核环境中失败,页面也会显示错误。诊断记录则能帮助区分两种情况:按钮事件没有触发,或者购买请求已经触发但随后失败。
备注:内容仅供参考。
