|
|
@@ -1,6 +1,6 @@
|
|
|
import 'react-native-gesture-handler';
|
|
|
|
|
|
-import React, { createContext, useContext, useRef, useState } from 'react';
|
|
|
+import React, { createContext, useContext, useRef, useState, useEffect } from 'react';
|
|
|
import { Platform, StyleSheet, View, Alert } from 'react-native';
|
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
|
@@ -18,8 +18,9 @@ import { ThemeProvider } from '@/theme';
|
|
|
import '@/translations';
|
|
|
|
|
|
import * as RNIap from 'react-native-iap';
|
|
|
+import type { Purchase, PurchaseError } from 'react-native-iap';
|
|
|
|
|
|
-const itemSubs = ['com.recipemuse.vip.monthly']; // 你的订阅ID
|
|
|
+const itemSubs = ['com.recipemuse.vip.monthly']; // 月度会员订阅ID
|
|
|
|
|
|
|
|
|
// 在 DEFAULT_URL 常量下方添加以下常量
|
|
|
@@ -71,13 +72,16 @@ const USER_AGENT = Platform.select({
|
|
|
function App() {
|
|
|
const webViewRef = useRef<WebView>(null);
|
|
|
const [showWebView, setShowWebView] = useState(false);
|
|
|
+ const [isIAPInitialized, setIsIAPInitialized] = useState(false);
|
|
|
+ const [availableProducts, setAvailableProducts] = useState<any[]>([]);
|
|
|
+ const [isSubscribed, setIsSubscribed] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
* 发送消息到 WebView
|
|
|
*/
|
|
|
- const sendMessageToWebView = (webViewRef: React.RefObject<WebView>, message: any) => {
|
|
|
+ const sendMessageToWebView = (webViewRef: React.RefObject<WebView | null>, message: any) => {
|
|
|
if (webViewRef.current) {
|
|
|
const messageString = typeof message === 'string' ? message : JSON.stringify(message);
|
|
|
// 使用更可靠的通信方式
|
|
|
@@ -90,7 +94,7 @@ function App() {
|
|
|
*/
|
|
|
const handleWebViewMessage = (
|
|
|
event: any,
|
|
|
- webViewRef: React.RefObject<WebView>,
|
|
|
+ webViewRef: React.RefObject<WebView | null>,
|
|
|
) => {
|
|
|
try {
|
|
|
const data = event.nativeEvent.data;
|
|
|
@@ -220,8 +224,304 @@ function App() {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ /**
|
|
|
+ * 初始化IAP
|
|
|
+ */
|
|
|
+ useEffect(() => {
|
|
|
+ initIAP();
|
|
|
+ return () => {
|
|
|
+ // 清理IAP连接
|
|
|
+ RNIap.endConnection();
|
|
|
+ };
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化应用内购买
|
|
|
+ */
|
|
|
+ const initIAP = async () => {
|
|
|
+ try {
|
|
|
+ await RNIap.initConnection();
|
|
|
+ console.log('IAP连接初始化成功');
|
|
|
+ setIsIAPInitialized(true);
|
|
|
+
|
|
|
+ // 获取可用的订阅产品
|
|
|
+ const products = await RNIap.fetchProducts({ skus: itemSubs, type: 'subs' });
|
|
|
+ console.log('可用订阅产品:', products);
|
|
|
+ setAvailableProducts(products || []);
|
|
|
+
|
|
|
+ // 检查现有订阅状态
|
|
|
+ await checkSubscriptionStatus();
|
|
|
+
|
|
|
+ // 监听购买更新
|
|
|
+ const purchaseUpdateSubscription = RNIap.purchaseUpdatedListener(
|
|
|
+ async (purchase: Purchase) => {
|
|
|
+ console.log('购买更新:', purchase);
|
|
|
+ console.log('购买状态:', purchase.purchaseState);
|
|
|
+ console.log('平台:', purchase.platform);
|
|
|
+
|
|
|
+ const receipt = purchase.purchaseToken || purchase.transactionId;
|
|
|
+
|
|
|
+ if (receipt) {
|
|
|
+ try {
|
|
|
+ // 验证收据(这里应该发送到你的后端服务器验证)
|
|
|
+ const isValid = await validateReceipt(receipt, purchase);
|
|
|
+
|
|
|
+ if (!isValid) {
|
|
|
+ Alert.alert('订阅失败', '收据验证失败,请联系客服');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 完成交易 - iOS和Android处理方式不同
|
|
|
+ if (Platform.OS === 'ios') {
|
|
|
+ // iOS: 完成交易,订阅类产品不消耗
|
|
|
+ await RNIap.finishTransaction({ purchase, isConsumable: false });
|
|
|
+ console.log('iOS交易已完成');
|
|
|
+ } else if (Platform.OS === 'android') {
|
|
|
+ // Android: 需要确认购买(acknowledge)
|
|
|
+ if (purchase.purchaseToken) {
|
|
|
+ await RNIap.acknowledgePurchaseAndroid(purchase.purchaseToken);
|
|
|
+ console.log('Android购买已确认');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新订阅状态
|
|
|
+ setIsSubscribed(true);
|
|
|
+
|
|
|
+ Alert.alert('订阅成功', '感谢您订阅会员服务!');
|
|
|
+
|
|
|
+ // 发送订阅成功消息到WebView
|
|
|
+ sendMessageToWebView(webViewRef, {
|
|
|
+ type: 'SUBSCRIPTION_SUCCESS',
|
|
|
+ status: 'success',
|
|
|
+ message: '订阅成功',
|
|
|
+ data: {
|
|
|
+ productId: purchase.productId,
|
|
|
+ transactionId: purchase.transactionId,
|
|
|
+ purchaseToken: purchase.purchaseToken,
|
|
|
+ platform: purchase.platform,
|
|
|
+ transactionDate: purchase.transactionDate,
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('验证收据失败:', error);
|
|
|
+ Alert.alert('订阅失败', `验证收据时出错: ${error.message || '未知错误'}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ // 监听购买错误
|
|
|
+ const purchaseErrorSubscription = RNIap.purchaseErrorListener(
|
|
|
+ (error: PurchaseError) => {
|
|
|
+ console.error('购买错误:', error);
|
|
|
+ if (error.code !== RNIap.ErrorCode.UserCancelled) {
|
|
|
+ Alert.alert('购买失败', error.message || '请稍后重试');
|
|
|
+ }
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ // 保存订阅以便清理
|
|
|
+ return () => {
|
|
|
+ purchaseUpdateSubscription.remove();
|
|
|
+ purchaseErrorSubscription.remove();
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ console.error('IAP初始化失败:', error);
|
|
|
+ Alert.alert('初始化失败', '无法连接到应用商店');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 验证收据(示例实现,实际应该发送到后端服务器)
|
|
|
+ */
|
|
|
+ const validateReceipt = async (
|
|
|
+ receipt: string,
|
|
|
+ purchase: Purchase,
|
|
|
+ ) => {
|
|
|
+ // TODO: 将收据发送到你的后端服务器进行验证
|
|
|
+ // 示例代码:
|
|
|
+ // const response = await fetch('https://your-api.com/validate-receipt', {
|
|
|
+ // method: 'POST',
|
|
|
+ // headers: { 'Content-Type': 'application/json' },
|
|
|
+ // body: JSON.stringify({ receipt, productId: purchase.productId }),
|
|
|
+ // });
|
|
|
+ // const result = await response.json();
|
|
|
+ // return result.valid;
|
|
|
+
|
|
|
+ console.log('验证收据:', receipt);
|
|
|
+ console.log('购买信息:', purchase);
|
|
|
+ return true;
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理应用内购买 - 购买月度会员订阅
|
|
|
+ */
|
|
|
const handleInAppPurchase = async () => {
|
|
|
+ if (!isIAPInitialized) {
|
|
|
+ Alert.alert('提示', '应用商店连接未就绪,请稍后重试');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否已订阅
|
|
|
+ if (isSubscribed) {
|
|
|
+ Alert.alert('提示', '您已经是会员了');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 构建购买请求参数
|
|
|
+ const purchaseRequest: any = {
|
|
|
+ type: 'subs',
|
|
|
+ request: {
|
|
|
+ ios: {
|
|
|
+ sku: itemSubs[0],
|
|
|
+ },
|
|
|
+ android: {
|
|
|
+ skus: [itemSubs[0]],
|
|
|
+ },
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ // Android需要提供subscriptionOffers(从产品信息中获取)
|
|
|
+ if (Platform.OS === 'android' && availableProducts.length > 0) {
|
|
|
+ const product = availableProducts.find(p => p.id === itemSubs[0]);
|
|
|
+ if (product?.subscriptionOfferDetailsAndroid?.[0]) {
|
|
|
+ const offer = product.subscriptionOfferDetailsAndroid[0];
|
|
|
+ purchaseRequest.request.android.subscriptionOffers = [
|
|
|
+ {
|
|
|
+ sku: itemSubs[0],
|
|
|
+ offerToken: offer.offerToken,
|
|
|
+ },
|
|
|
+ ];
|
|
|
+ console.log('Android订阅offer:', offer);
|
|
|
+ } else {
|
|
|
+ console.warn('未找到Android订阅offer,将尝试不带offer购买');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('发起购买请求:', purchaseRequest);
|
|
|
+
|
|
|
+ // 请求购买订阅
|
|
|
+ await RNIap.requestPurchase(purchaseRequest);
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('购买订阅失败:', error);
|
|
|
+
|
|
|
+ // 区分不同的错误类型
|
|
|
+ if (error.code === RNIap.ErrorCode.UserCancelled) {
|
|
|
+ console.log('用户取消购买');
|
|
|
+ } else if (error.code === RNIap.ErrorCode.AlreadyOwned) {
|
|
|
+ Alert.alert('提示', '您已经拥有此订阅');
|
|
|
+ setIsSubscribed(true);
|
|
|
+ } else if (error.code === RNIap.ErrorCode.ItemUnavailable) {
|
|
|
+ Alert.alert('购买失败', '此商品暂时不可用,请稍后重试');
|
|
|
+ } else {
|
|
|
+ Alert.alert('购买失败', error.message || '请稍后重试');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 恢复购买
|
|
|
+ */
|
|
|
+ const restorePurchases = async () => {
|
|
|
+ try {
|
|
|
+ // iOS调用系统恢复购买API
|
|
|
+ if (Platform.OS === 'ios') {
|
|
|
+ await RNIap.restorePurchases();
|
|
|
+ console.log('iOS恢复购买请求已发送');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取可用购买记录
|
|
|
+ const availablePurchases = await RNIap.getAvailablePurchases();
|
|
|
+ console.log('可恢复的购买:', availablePurchases);
|
|
|
+
|
|
|
+ if (availablePurchases.length === 0) {
|
|
|
+ Alert.alert('提示', '没有找到可恢复的购买记录');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let hasSubscription = false;
|
|
|
+
|
|
|
+ // 处理恢复的购买
|
|
|
+ for (const purchase of availablePurchases) {
|
|
|
+ if (itemSubs.includes(purchase.productId)) {
|
|
|
+ hasSubscription = true;
|
|
|
+
|
|
|
+ // 验证并激活订阅
|
|
|
+ const receipt = purchase.purchaseToken || purchase.transactionId || '';
|
|
|
+ const isValid = await validateReceipt(receipt, purchase);
|
|
|
+
|
|
|
+ if (isValid) {
|
|
|
+ // Android需要确认购买
|
|
|
+ if (Platform.OS === 'android' && purchase.purchaseToken) {
|
|
|
+ try {
|
|
|
+ await RNIap.acknowledgePurchaseAndroid(purchase.purchaseToken);
|
|
|
+ console.log('Android购买已确认');
|
|
|
+ } catch (error) {
|
|
|
+ console.log('购买可能已经被确认过:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ setIsSubscribed(true);
|
|
|
+ console.log('订阅已恢复:', purchase.productId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
+ if (hasSubscription) {
|
|
|
+ Alert.alert('恢复成功', '您的订阅已恢复');
|
|
|
+
|
|
|
+ // 发送恢复成功消息到WebView
|
|
|
+ sendMessageToWebView(webViewRef, {
|
|
|
+ type: 'SUBSCRIPTION_RESTORED',
|
|
|
+ status: 'success',
|
|
|
+ message: '订阅已恢复',
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ Alert.alert('提示', '没有找到会员订阅记录');
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('恢复购买失败:', error);
|
|
|
+ Alert.alert('恢复失败', error.message || '无法恢复购买记录');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查订阅状态
|
|
|
+ */
|
|
|
+ const checkSubscriptionStatus = async () => {
|
|
|
+ try {
|
|
|
+ const availablePurchases = await RNIap.getAvailablePurchases();
|
|
|
+ console.log('所有可用购买记录:', availablePurchases);
|
|
|
+
|
|
|
+ const activeSubscription = availablePurchases.find(
|
|
|
+ (purchase) => itemSubs.includes(purchase.productId),
|
|
|
+ );
|
|
|
+
|
|
|
+ if (activeSubscription) {
|
|
|
+ console.log('当前订阅状态:', activeSubscription);
|
|
|
+
|
|
|
+ // 检查订阅是否过期(Android)
|
|
|
+ if (Platform.OS === 'android') {
|
|
|
+ // Android的自动续订状态
|
|
|
+ const androidPurchase = activeSubscription as any;
|
|
|
+ const isActive = androidPurchase.autoRenewingAndroid !== false;
|
|
|
+ setIsSubscribed(isActive);
|
|
|
+ return isActive;
|
|
|
+ } else {
|
|
|
+ // iOS的订阅状态
|
|
|
+ setIsSubscribed(true);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ setIsSubscribed(false);
|
|
|
+ return false;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('检查订阅状态失败:', error);
|
|
|
+ setIsSubscribed(false);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
return (
|