- 将 MaterialIcons 替换为 Ionicons 以保持图标库一致性 - 重新设计会员购买界面,采用分段卡片布局和权益对比表格 - 添加 Liquid Glass 兼容的悬浮返回按钮 - 优化套餐卡片样式,使用渐变背景和标签展示 - 添加会员权益对比功能,清晰展示 VIP 与普通用户差异 - 更新任务文档,记录图标库使用规范和按钮组件 Liquid Glass 兼容性实现模式
1275 lines
40 KiB
TypeScript
1275 lines
40 KiB
TypeScript
/* eslint-disable react-hooks/exhaustive-deps */
|
||
import CustomCheckBox from '@/components/ui/CheckBox';
|
||
import { USER_AGREEMENT_URL } from '@/constants/Agree';
|
||
// import { useAuth } from '@/contexts/AuthContext';
|
||
// import { UserApi } from '@/services';
|
||
import { log, logger } from '@/utils/logger';
|
||
import {
|
||
captureMessage,
|
||
captureMessageWithContext,
|
||
capturePurchaseEvent,
|
||
captureUserAction
|
||
} from '@/utils/sentry.utils';
|
||
import { Toast as GlobalToast } from '@/utils/toast.utils';
|
||
import { Ionicons } from '@expo/vector-icons';
|
||
import { captureException } from '@sentry/react-native';
|
||
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
||
import { LinearGradient } from 'expo-linear-gradient';
|
||
import React, { useEffect, useRef, useState } from 'react';
|
||
import {
|
||
ActivityIndicator,
|
||
Alert,
|
||
Dimensions,
|
||
Linking,
|
||
Modal,
|
||
ScrollView,
|
||
StyleSheet,
|
||
Text,
|
||
TouchableOpacity,
|
||
View
|
||
} from 'react-native';
|
||
import Purchases, { CustomerInfo, PurchasesStoreProduct } from 'react-native-purchases';
|
||
|
||
const { height } = Dimensions.get('window');
|
||
|
||
interface MembershipModalProps {
|
||
visible: boolean;
|
||
onClose?: () => void;
|
||
onPurchaseSuccess?: () => void;
|
||
}
|
||
|
||
interface MembershipPlan {
|
||
id: string;
|
||
fallbackTitle: string;
|
||
subtitle: string;
|
||
type: 'weekly' | 'quarterly' | 'lifetime';
|
||
recommended?: boolean;
|
||
tag?: string;
|
||
originalPrice?: string;
|
||
}
|
||
|
||
const DEFAULT_PLANS: MembershipPlan[] = [
|
||
{
|
||
id: 'com.anonymous.digitalpilates.membership.lifetime',
|
||
fallbackTitle: '终身会员',
|
||
subtitle: '一次投入,终身健康陪伴',
|
||
type: 'lifetime',
|
||
recommended: true,
|
||
tag: '限时特价',
|
||
originalPrice: '¥898',
|
||
},
|
||
{
|
||
id: 'com.anonymous.digitalpilates.membership.quarter',
|
||
fallbackTitle: '季度会员',
|
||
subtitle: '3个月蜕变计划,见证身材变化',
|
||
type: 'quarterly',
|
||
originalPrice: '¥598',
|
||
},
|
||
{
|
||
id: 'com.anonymous.digitalpilates.membership.weekly',
|
||
fallbackTitle: '周会员',
|
||
subtitle: '7天体验,开启健康第一步',
|
||
type: 'weekly',
|
||
originalPrice: '¥128',
|
||
},
|
||
];
|
||
|
||
|
||
const BENEFIT_COMPARISON = [
|
||
{ title: 'AI拍照记录热量', vip: true, regular: false },
|
||
{ title: 'AI拍照识别包装', vip: true, regular: false },
|
||
{ title: '私人饮食建议', vip: true, regular: false },
|
||
{ title: '定制健身训练', vip: true, regular: false },
|
||
{ title: '每日健康提醒', vip: true, regular: true },
|
||
];
|
||
|
||
const PLAN_STYLE_CONFIG: Record<MembershipPlan['type'], { gradient: readonly [string, string]; accent: string }> = {
|
||
lifetime: {
|
||
gradient: ['#FFF1DD', '#FFE8FA'] as const,
|
||
accent: '#7B2CBF',
|
||
},
|
||
quarterly: {
|
||
gradient: ['#E5EAFE', '#F4E8FF'] as const,
|
||
accent: '#5B5BE6',
|
||
},
|
||
weekly: {
|
||
gradient: ['#FFF6E5', '#FFE7CC'] as const,
|
||
accent: '#FF9500',
|
||
},
|
||
};
|
||
|
||
|
||
export function MembershipModal({ visible, onClose, onPurchaseSuccess }: MembershipModalProps) {
|
||
const [selectedProduct, setSelectedProduct] = useState<PurchasesStoreProduct | null>(null);
|
||
const [loading, setLoading] = useState(false);
|
||
const [restoring, setRestoring] = useState(false);
|
||
// const { user } = useAuth()
|
||
// const { refreshUserInfo } = useAuth();
|
||
const [products, setProducts] = useState<PurchasesStoreProduct[]>([]);
|
||
|
||
// 协议同意状态 - 只需要一个状态
|
||
const [agreementAccepted, setAgreementAccepted] = useState(false);
|
||
|
||
// 保存监听器引用,用于移除监听器
|
||
const purchaseListenerRef = useRef<((customerInfo: CustomerInfo) => void) | null>(null);
|
||
|
||
// 根据选中的产品生成tips内容
|
||
const getTipsContent = (product: PurchasesStoreProduct | null): string => {
|
||
if (!product) return '';
|
||
|
||
const plan = DEFAULT_PLANS.find(item => item.id === product.identifier);
|
||
if (!plan) {
|
||
return '';
|
||
}
|
||
|
||
switch (plan.type) {
|
||
case 'lifetime':
|
||
return '终身陪伴,见证您的每一次健康蜕变';
|
||
case 'quarterly':
|
||
return '3个月科学计划,让健康成为生活习惯';
|
||
case 'weekly':
|
||
return '7天体验期,感受专业健康指导的力量';
|
||
default:
|
||
return '';
|
||
}
|
||
};
|
||
|
||
// useEffect(() => {
|
||
// if (user) {
|
||
// captureMessage('用户已登录,开始初始化 Purchases');
|
||
// initPurchases();
|
||
// }
|
||
// }, [user]); // 初始化只需要执行一次
|
||
|
||
// 单独的 useEffect 来处理购买监听器,依赖 visible 状态
|
||
useEffect(() => {
|
||
if (visible) {
|
||
setupPurchaseListener();
|
||
setAgreementAccepted(false);
|
||
initPurchases();
|
||
} else {
|
||
removePurchaseListener();
|
||
setProducts([]);
|
||
setSelectedProduct(null);
|
||
setAgreementAccepted(false);
|
||
setLoading(false);
|
||
setRestoring(false);
|
||
}
|
||
|
||
// 组件卸载时确保移除监听器
|
||
return () => {
|
||
removePurchaseListener();
|
||
log.info('MembershipModal 购买监听器已清理');
|
||
};
|
||
}, [visible]); // 依赖 visible 状态
|
||
|
||
|
||
|
||
const initPurchases = async () => {
|
||
capturePurchaseEvent('init', '开始获取会员产品套餐');
|
||
|
||
try {
|
||
// 添加延迟,确保 RevenueCat SDK 完全初始化
|
||
await new Promise(resolve => setTimeout(resolve, 500));
|
||
|
||
const offerings = await Purchases.getOfferings();
|
||
log.info('获取产品套餐', { offerings });
|
||
|
||
logger.info('获取产品套餐成功', {
|
||
currentOffering: offerings.current?.identifier || null,
|
||
availablePackagesCount: offerings.current?.availablePackages.length || 0,
|
||
allOfferingsCount: Object.keys(offerings.all).length,
|
||
});
|
||
|
||
const packages = offerings.current?.availablePackages ?? [];
|
||
|
||
if (packages.length === 0) {
|
||
log.warn('没有找到可用的产品套餐', {
|
||
hasCurrentOffering: offerings.current !== null,
|
||
packagesLength: offerings.current?.availablePackages.length || 0,
|
||
});
|
||
logger.info('没有找到可用的产品套餐', {
|
||
hasCurrentOffering: offerings.current !== null,
|
||
packagesLength: offerings.current?.availablePackages.length || 0,
|
||
});
|
||
setProducts([]);
|
||
setSelectedProduct(null);
|
||
return;
|
||
}
|
||
|
||
const matchedProducts = packages
|
||
.map(pkg => pkg.product)
|
||
.filter(product => DEFAULT_PLANS.some(plan => plan.id === product.identifier));
|
||
|
||
const orderedProducts = DEFAULT_PLANS
|
||
.map(plan => matchedProducts.find(product => product.identifier === plan.id))
|
||
.filter((product): product is PurchasesStoreProduct => Boolean(product));
|
||
|
||
const fallbackProducts = packages.map(pkg => pkg.product);
|
||
const productsToUse = orderedProducts.length > 0 ? orderedProducts : fallbackProducts;
|
||
|
||
log.info('productsToUse', productsToUse)
|
||
|
||
setProducts(productsToUse);
|
||
|
||
// 获取产品后,检查用户的购买记录并自动选中对应套餐
|
||
await checkAndSelectActivePlan(productsToUse);
|
||
|
||
setSelectedProduct(current => current ?? (productsToUse[0] ?? null));
|
||
} catch (e: any) {
|
||
// 安全地处理错误对象,避免循环引用
|
||
const errorData = {
|
||
message: e?.message || '未知错误',
|
||
code: e?.code || null,
|
||
name: e?.name || 'Error',
|
||
// 只包含基本的错误信息,避免可能的循环引用
|
||
};
|
||
|
||
log.error('获取产品套餐失败', { error: errorData });
|
||
captureException(e);
|
||
capturePurchaseEvent('error', '获取产品套餐失败', errorData);
|
||
|
||
// 设置空状态,避免界面卡在加载状态
|
||
setProducts([]);
|
||
setSelectedProduct(null);
|
||
}
|
||
};
|
||
|
||
// 添加购买状态监听器
|
||
const setupPurchaseListener = () => {
|
||
log.info('设置购买监听器', { visible });
|
||
|
||
// 如果已经有监听器,先移除
|
||
if (purchaseListenerRef.current) {
|
||
removePurchaseListener();
|
||
}
|
||
|
||
// 创建监听器函数
|
||
const listener = (customerInfo: CustomerInfo) => {
|
||
log.info('购买状态变化监听器触发', { customerInfo, visible });
|
||
|
||
// 检查是否有有效的购买记录
|
||
const hasActiveEntitlements = Object.keys(customerInfo.entitlements.active).length > 0;
|
||
const hasNonSubscriptionTransactions = customerInfo.nonSubscriptionTransactions.length > 0;
|
||
const hasActiveSubscriptions = Object.keys(customerInfo.activeSubscriptions).length > 0;
|
||
|
||
if (hasActiveEntitlements || hasNonSubscriptionTransactions || hasActiveSubscriptions) {
|
||
capturePurchaseEvent('success', '监听到购买状态变化', {
|
||
hasActiveEntitlements,
|
||
hasNonSubscriptionTransactions,
|
||
hasActiveSubscriptions,
|
||
activeEntitlementsCount: Object.keys(customerInfo.entitlements.active).length,
|
||
nonSubscriptionTransactionsCount: customerInfo.nonSubscriptionTransactions.length,
|
||
activeSubscriptionsCount: Object.keys(customerInfo.activeSubscriptions).length,
|
||
modalVisible: visible
|
||
});
|
||
|
||
log.info('检测到购买成功,准备刷新用户信息并关闭弹窗');
|
||
|
||
// 延迟一点时间,确保购买流程完全完成
|
||
setTimeout(async () => {
|
||
// 刷新用户信息
|
||
// await refreshUserInfo();
|
||
|
||
// 调用购买成功回调
|
||
onPurchaseSuccess?.();
|
||
|
||
// 关闭弹窗
|
||
onClose?.();
|
||
|
||
// 显示成功提示
|
||
GlobalToast.show({
|
||
message: '会员开通成功',
|
||
});
|
||
}, 1000);
|
||
}
|
||
};
|
||
|
||
// 保存监听器引用
|
||
purchaseListenerRef.current = listener;
|
||
|
||
// 添加监听器
|
||
Purchases.addCustomerInfoUpdateListener(listener);
|
||
|
||
log.info('购买监听器已添加');
|
||
};
|
||
|
||
// 移除购买状态监听器
|
||
const removePurchaseListener = () => {
|
||
if (purchaseListenerRef.current) {
|
||
log.info('移除购买监听器');
|
||
Purchases.removeCustomerInfoUpdateListener(purchaseListenerRef.current);
|
||
purchaseListenerRef.current = null;
|
||
log.info('购买监听器已移除');
|
||
}
|
||
};
|
||
|
||
|
||
// 检查用户的购买记录并自动选中对应套餐
|
||
const checkAndSelectActivePlan = async (availableProducts: PurchasesStoreProduct[]) => {
|
||
try {
|
||
captureUserAction('开始检查用户购买记录');
|
||
|
||
// 获取用户的购买信息
|
||
const customerInfo = await Purchases.getCustomerInfo();
|
||
|
||
log.info('获取用户购买信息', { customerInfo });
|
||
|
||
// 记录详细的购买状态日志
|
||
captureMessageWithContext('获取用户购买信息成功', {
|
||
activeEntitlementsCount: Object.keys(customerInfo.entitlements.active).length,
|
||
nonSubscriptionTransactionsCount: customerInfo.nonSubscriptionTransactions.length,
|
||
activeSubscriptionsCount: Object.keys(customerInfo.activeSubscriptions).length,
|
||
originalAppUserId: customerInfo.originalAppUserId,
|
||
firstSeen: customerInfo.firstSeen,
|
||
originalPurchaseDate: customerInfo.originalPurchaseDate,
|
||
latestExpirationDate: customerInfo.latestExpirationDate
|
||
});
|
||
|
||
// 查找激活的产品ID
|
||
let activePurchasedProductIds: string[] = [];
|
||
|
||
// 检查权益
|
||
Object.keys(customerInfo.entitlements.active).forEach(key => {
|
||
const entitlement = customerInfo.entitlements.active[key];
|
||
activePurchasedProductIds.push(entitlement.productIdentifier);
|
||
log.debug(`激活的权益: ${key}, 产品ID: ${entitlement.productIdentifier}`);
|
||
});
|
||
|
||
// 检查非订阅购买(如终身会员)
|
||
customerInfo.nonSubscriptionTransactions.forEach(transaction => {
|
||
activePurchasedProductIds.push(transaction.productIdentifier);
|
||
log.debug(`非订阅购买: ${transaction.productIdentifier}, 购买时间: ${transaction.purchaseDate}`);
|
||
});
|
||
|
||
// 检查订阅
|
||
Object.keys(customerInfo.activeSubscriptions).forEach(productId => {
|
||
activePurchasedProductIds.push(productId);
|
||
log.debug(`激活的订阅: ${productId}`);
|
||
});
|
||
|
||
// 去重
|
||
activePurchasedProductIds = [...new Set(activePurchasedProductIds)];
|
||
|
||
captureMessageWithContext('用户激活的产品列表', {
|
||
activePurchasedProductIds,
|
||
activePurchasedProductCount: activePurchasedProductIds.length
|
||
});
|
||
|
||
if (activePurchasedProductIds.length > 0) {
|
||
// 尝试在可用产品中找到匹配的产品
|
||
let selectedProduct: PurchasesStoreProduct | null = null;
|
||
|
||
// 优先级:终身会员 > 季度会员 > 周会员
|
||
const priorityOrder = DEFAULT_PLANS.map(plan => plan.id);
|
||
|
||
// 按照优先级查找
|
||
for (const priorityProductId of priorityOrder) {
|
||
if (activePurchasedProductIds.includes(priorityProductId)) {
|
||
selectedProduct = availableProducts.find(product => product.identifier === priorityProductId) || null;
|
||
if (selectedProduct) {
|
||
log.info(`找到优先级最高的激活产品: ${priorityProductId}`);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果按优先级没找到,尝试找到任何匹配的产品
|
||
if (!selectedProduct) {
|
||
for (const productId of activePurchasedProductIds) {
|
||
selectedProduct = availableProducts.find(product => product.identifier === productId) || null;
|
||
if (selectedProduct) {
|
||
log.info(`找到匹配的激活产品: ${productId}`);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (selectedProduct) {
|
||
setSelectedProduct(selectedProduct);
|
||
|
||
captureMessageWithContext('自动选中用户已购买的套餐', {
|
||
selectedProductId: selectedProduct.identifier,
|
||
selectedProductTitle: selectedProduct.title,
|
||
selectedProductPrice: selectedProduct.price,
|
||
allActivePurchasedProductIds: activePurchasedProductIds
|
||
});
|
||
|
||
} else {
|
||
captureMessageWithContext('未找到匹配的可用产品', {
|
||
activePurchasedProductIds,
|
||
availableProductIds: availableProducts.map(p => p.identifier)
|
||
});
|
||
log.warn('用户有激活的购买记录,但没有找到匹配的可用产品', {
|
||
activePurchasedProductIds,
|
||
availableProductIds: availableProducts.map(p => p.identifier)
|
||
});
|
||
}
|
||
} else {
|
||
captureMessageWithContext('用户没有激活的购买记录', {
|
||
hasEntitlements: Object.keys(customerInfo.entitlements.active).length > 0,
|
||
hasNonSubscriptions: customerInfo.nonSubscriptionTransactions.length > 0,
|
||
hasActiveSubscriptions: Object.keys(customerInfo.activeSubscriptions).length > 0
|
||
});
|
||
log.info('用户没有激活的购买记录,使用默认选择逻辑');
|
||
setSelectedProduct(availableProducts[0] ?? null);
|
||
}
|
||
|
||
} catch (error: any) {
|
||
// 安全地处理错误对象,避免循环引用
|
||
const errorData = {
|
||
message: error?.message || '未知错误',
|
||
code: error?.code || null,
|
||
name: error?.name || 'Error',
|
||
};
|
||
|
||
log.error('检查用户购买记录失败', { error: errorData });
|
||
captureException(error);
|
||
captureMessageWithContext('检查用户购买记录失败', errorData);
|
||
}
|
||
};
|
||
|
||
|
||
const handlePurchase = async () => {
|
||
// 验证是否已同意协议
|
||
if (!agreementAccepted) {
|
||
Alert.alert(
|
||
'请阅读并同意相关协议',
|
||
'购买前需要同意用户协议、会员协议和自动续费协议',
|
||
[
|
||
{
|
||
text: '确定',
|
||
style: 'default',
|
||
}
|
||
]
|
||
);
|
||
return;
|
||
}
|
||
|
||
// 验证是否选择了产品
|
||
if (!selectedProduct) {
|
||
Alert.alert(
|
||
'请选择会员套餐',
|
||
'',
|
||
[
|
||
{
|
||
text: '确定',
|
||
style: 'default',
|
||
}
|
||
]
|
||
);
|
||
return;
|
||
}
|
||
|
||
|
||
|
||
// 防止重复点击
|
||
if (loading) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 设置加载状态
|
||
setLoading(true);
|
||
|
||
// 记录购买开始事件
|
||
capturePurchaseEvent('init', `开始购买: ${selectedProduct.identifier}`, {
|
||
productIdentifier: selectedProduct.identifier,
|
||
productTitle: selectedProduct.title,
|
||
productPrice: selectedProduct.price
|
||
});
|
||
|
||
// 执行购买
|
||
const { customerInfo, productIdentifier } = await Purchases.purchaseStoreProduct(selectedProduct);
|
||
|
||
log.info('购买成功', { customerInfo, productIdentifier });
|
||
|
||
// 记录购买成功事件
|
||
capturePurchaseEvent('success', `购买成功: ${productIdentifier}`, {
|
||
productIdentifier,
|
||
hasActiveEntitlements: Object.keys(customerInfo.entitlements.active).length > 0,
|
||
activeEntitlementsCount: Object.keys(customerInfo.entitlements.active).length,
|
||
nonSubscriptionTransactionsCount: customerInfo.nonSubscriptionTransactions.length,
|
||
activeSubscriptionsCount: Object.keys(customerInfo.activeSubscriptions).length
|
||
});
|
||
|
||
// 购买成功后,监听器会自动处理后续逻辑(刷新用户信息、关闭弹窗等)
|
||
log.info('购买流程完成,等待监听器处理后续逻辑');
|
||
|
||
} catch (error: any) {
|
||
captureException(error);
|
||
|
||
// 记录购买失败事件
|
||
capturePurchaseEvent('error', `购买失败: ${error.message || '未知错误'}`, {
|
||
errorCode: error.code || null,
|
||
errorMessage: error.message || '未知错误',
|
||
productIdentifier: selectedProduct.identifier
|
||
});
|
||
|
||
// 处理不同类型的购买错误
|
||
if (error.code === 1 || error.code === 'USER_CANCELLED') {
|
||
// 用户取消购买
|
||
GlobalToast.show({
|
||
message: '购买已取消',
|
||
});
|
||
} else if (error.code === 'ITEM_ALREADY_OWNED' || error.code === 'PRODUCT_ALREADY_PURCHASED') {
|
||
// 商品已拥有
|
||
GlobalToast.show({
|
||
message: '您已拥有此商品',
|
||
});
|
||
} else if (error.code === 'NETWORK_ERROR') {
|
||
// 网络错误
|
||
GlobalToast.show({
|
||
message: '网络连接失败',
|
||
});
|
||
} else if (error.code === 'PAYMENT_PENDING') {
|
||
// 支付待处理
|
||
GlobalToast.show({
|
||
message: '支付正在处理中',
|
||
});
|
||
} else if (error.code === 'INVALID_CREDENTIALS') {
|
||
// 凭据无效
|
||
GlobalToast.show({
|
||
message: '账户验证失败',
|
||
});
|
||
} else {
|
||
// 其他错误
|
||
GlobalToast.show({
|
||
message: '购买失败',
|
||
});
|
||
}
|
||
} finally {
|
||
// 确保在所有情况下都重置加载状态
|
||
setLoading(false);
|
||
log.info('购买流程结束,加载状态已重置');
|
||
}
|
||
};
|
||
|
||
const handleRestore = async () => {
|
||
// 防止重复点击
|
||
if (restoring || loading) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setRestoring(true);
|
||
|
||
captureUserAction('开始恢复购买');
|
||
|
||
// 恢复购买
|
||
const customerInfo = await Purchases.restorePurchases();
|
||
|
||
log.info('恢复购买结果', { customerInfo });
|
||
captureMessageWithContext('恢复购买结果', {
|
||
activeEntitlementsCount: Object.keys(customerInfo.entitlements.active).length,
|
||
nonSubscriptionTransactionsCount: customerInfo.nonSubscriptionTransactions.length,
|
||
activeSubscriptionsCount: Object.keys(customerInfo.activeSubscriptions).length,
|
||
managementUrl: customerInfo.managementURL,
|
||
originalAppUserId: customerInfo.originalAppUserId
|
||
});
|
||
|
||
// 检查是否有有效的购买记录
|
||
const hasActiveEntitlements = Object.keys(customerInfo.entitlements.active).length > 0;
|
||
const hasNonSubscriptionTransactions = customerInfo.nonSubscriptionTransactions.length > 0;
|
||
const hasActiveSubscriptions = Object.keys(customerInfo.activeSubscriptions).length > 0;
|
||
|
||
if (hasActiveEntitlements || hasNonSubscriptionTransactions || hasActiveSubscriptions) {
|
||
// 检查具体的购买内容
|
||
let restoredProducts: string[] = [];
|
||
|
||
// 检查权益
|
||
Object.keys(customerInfo.entitlements.active).forEach(key => {
|
||
const entitlement = customerInfo.entitlements.active[key];
|
||
restoredProducts.push(entitlement.productIdentifier);
|
||
});
|
||
|
||
// 检查非订阅购买(如终身会员)
|
||
customerInfo.nonSubscriptionTransactions.forEach(transaction => {
|
||
restoredProducts.push(transaction.productIdentifier);
|
||
});
|
||
|
||
// 检查订阅
|
||
Object.keys(customerInfo.activeSubscriptions).forEach(productId => {
|
||
restoredProducts.push(productId);
|
||
});
|
||
|
||
log.info('恢复的产品', { restoredProducts });
|
||
capturePurchaseEvent('restore', '恢复购买成功', {
|
||
restoredProducts,
|
||
restoredProductsCount: restoredProducts.length
|
||
});
|
||
|
||
try {
|
||
// 调用后台服务接口进行票据匹配
|
||
captureUserAction('开始调用后台恢复购买接口');
|
||
|
||
// const restoreResponse = await UserApi.restorePurchase({
|
||
// customerInfo: {
|
||
// originalAppUserId: customerInfo.originalAppUserId,
|
||
// activeEntitlements: customerInfo.entitlements.active,
|
||
// nonSubscriptionTransactions: customerInfo.nonSubscriptionTransactions,
|
||
// activeSubscriptions: customerInfo.activeSubscriptions,
|
||
// restoredProducts
|
||
// }
|
||
// });
|
||
|
||
// log.debug('后台恢复购买响应', { restoreResponse });
|
||
|
||
// captureMessageWithContext('后台恢复购买成功', {
|
||
// responseData: restoreResponse,
|
||
// restoredProductsCount: restoredProducts.length
|
||
// });
|
||
|
||
// 刷新用户信息
|
||
// await refreshUserInfo();
|
||
|
||
// 调用购买成功回调
|
||
onPurchaseSuccess?.();
|
||
|
||
// 关闭弹窗
|
||
onClose?.();
|
||
|
||
GlobalToast.show({
|
||
message: '恢复购买成功',
|
||
});
|
||
|
||
} catch (apiError: any) {
|
||
// 安全地处理错误对象,避免循环引用
|
||
const errorData = {
|
||
message: apiError?.message || '未知错误',
|
||
code: apiError?.code || null,
|
||
name: apiError?.name || 'Error',
|
||
restoredProductsCount: restoredProducts.length
|
||
};
|
||
|
||
log.error('后台恢复购买接口调用失败', { error: errorData });
|
||
captureException(apiError);
|
||
captureMessageWithContext('后台恢复购买接口失败', errorData);
|
||
|
||
// 即使后台接口失败,也显示恢复成功(因为 RevenueCat 已经确认有购买记录)
|
||
// 但不关闭弹窗,让用户知道可能需要重试
|
||
GlobalToast.show({
|
||
message: '恢复购买部分失败',
|
||
});
|
||
}
|
||
|
||
} else {
|
||
capturePurchaseEvent('restore', '没有找到购买记录', {
|
||
hasActiveEntitlements,
|
||
hasNonSubscriptionTransactions,
|
||
hasActiveSubscriptions,
|
||
activeEntitlementsCount: Object.keys(customerInfo.entitlements.active).length,
|
||
nonSubscriptionTransactionsCount: customerInfo.nonSubscriptionTransactions.length,
|
||
activeSubscriptionsCount: Object.keys(customerInfo.activeSubscriptions).length
|
||
});
|
||
GlobalToast.show({
|
||
message: '没有找到购买记录',
|
||
});
|
||
}
|
||
} catch (error: any) {
|
||
// 安全地处理错误对象,避免循环引用
|
||
const errorData = {
|
||
message: error?.message || '未知错误',
|
||
code: error?.code || null,
|
||
name: error?.name || 'Error',
|
||
};
|
||
|
||
log.error('恢复购买失败', { error: errorData });
|
||
captureException(error);
|
||
|
||
// 记录恢复购买失败事件
|
||
capturePurchaseEvent('error', `恢复购买失败: ${errorData.message}`, errorData);
|
||
|
||
// 处理特定的恢复购买错误
|
||
if (error.code === 'RESTORE_CANCELLED' || error.code === 'USER_CANCELLED') {
|
||
GlobalToast.show({
|
||
message: '恢复购买已取消',
|
||
});
|
||
} else if (error.code === 'NETWORK_ERROR') {
|
||
GlobalToast.show({
|
||
message: '网络错误',
|
||
});
|
||
} else if (error.code === 'INVALID_CREDENTIALS') {
|
||
GlobalToast.show({
|
||
message: '账户验证失败',
|
||
});
|
||
} else {
|
||
GlobalToast.show({
|
||
message: '恢复购买失败',
|
||
});
|
||
}
|
||
} finally {
|
||
// 确保在所有情况下都重置恢复状态
|
||
setRestoring(false);
|
||
log.info('恢复购买流程结束,恢复状态已重置');
|
||
}
|
||
};
|
||
|
||
|
||
const renderPlanCard = (product: PurchasesStoreProduct) => {
|
||
const plan = DEFAULT_PLANS.find(p => p.id === product.identifier);
|
||
|
||
if (!plan) {
|
||
return null;
|
||
}
|
||
|
||
const isSelected = selectedProduct === product;
|
||
const displayTitle = product.title || plan.fallbackTitle;
|
||
const priceLabel = product.priceString || '';
|
||
const styleConfig = PLAN_STYLE_CONFIG[plan.type];
|
||
|
||
return (
|
||
<TouchableOpacity
|
||
key={product.identifier}
|
||
style={[
|
||
styles.planCardWrapper,
|
||
isSelected && styles.planCardWrapperSelected,
|
||
loading && styles.disabledPlanCard,
|
||
]}
|
||
onPress={() => !loading && product && setSelectedProduct(product)}
|
||
disabled={loading}
|
||
activeOpacity={loading ? 1 : 0.8}
|
||
accessible={true}
|
||
accessibilityLabel={`${displayTitle} ${priceLabel}`}
|
||
accessibilityHint={loading ? '购买进行中,无法切换套餐' : `选择${displayTitle}套餐`}
|
||
accessibilityState={{ disabled: loading, selected: isSelected }}
|
||
>
|
||
<LinearGradient
|
||
colors={styleConfig?.gradient ?? ['#FFFFFF', '#FFFFFF']}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 1, y: 1 }}
|
||
style={styles.planCardGradient}
|
||
>
|
||
{plan.tag && (
|
||
<View style={styles.planTag}>
|
||
<Text style={styles.planTagText}>{plan.tag}</Text>
|
||
</View>
|
||
)}
|
||
|
||
<Text style={styles.planCardTitle}>{displayTitle}</Text>
|
||
<Text style={[styles.planCardPrice, styleConfig && { color: styleConfig.accent }]}>
|
||
{priceLabel || '--'}
|
||
</Text>
|
||
{plan.originalPrice && (
|
||
<Text style={styles.planCardOriginalPrice}>{plan.originalPrice}</Text>
|
||
)}
|
||
<Text style={styles.planCardDescription}>{plan.subtitle}</Text>
|
||
</LinearGradient>
|
||
</TouchableOpacity>
|
||
);
|
||
};
|
||
|
||
|
||
return (
|
||
<Modal
|
||
visible={visible}
|
||
transparent={true}
|
||
animationType="fade"
|
||
presentationStyle="overFullScreen"
|
||
>
|
||
<View style={styles.overlay}>
|
||
{/* 半透明背景 */}
|
||
<TouchableOpacity
|
||
style={styles.backdrop}
|
||
activeOpacity={1}
|
||
onPress={onClose} // 阻止点击背景关闭
|
||
/>
|
||
|
||
<View style={styles.modalContainer}>
|
||
{/* 悬浮返回按钮 - 移到 ScrollView 外部以确保始终在最上层 */}
|
||
<TouchableOpacity
|
||
onPress={onClose}
|
||
activeOpacity={0.7}
|
||
accessible={true}
|
||
accessibilityLabel="返回"
|
||
accessibilityHint="关闭会员购买弹窗"
|
||
style={styles.floatingBackButtonContainer}
|
||
>
|
||
{isLiquidGlassAvailable() ? (
|
||
<GlassView
|
||
style={styles.floatingBackButton}
|
||
glassEffectStyle="clear"
|
||
tintColor="rgba(255, 255, 255, 0.3)"
|
||
isInteractive={true}
|
||
>
|
||
<Ionicons name="chevron-back" size={24} color="#333" />
|
||
</GlassView>
|
||
) : (
|
||
<View style={[styles.floatingBackButton, styles.fallbackBackButton]}>
|
||
<Ionicons name="chevron-back" size={24} color="#333" />
|
||
</View>
|
||
)}
|
||
</TouchableOpacity>
|
||
|
||
<ScrollView
|
||
style={styles.modalContent}
|
||
contentContainerStyle={styles.modalContentContainer}
|
||
showsVerticalScrollIndicator={false}
|
||
>
|
||
|
||
<View style={styles.sectionCard}>
|
||
<View style={styles.sectionTitleRow}>
|
||
<View style={styles.sectionTitleBadge}>
|
||
<Ionicons name="star" size={16} color="#7B2CBF" />
|
||
</View>
|
||
<Text style={styles.sectionTitle}>会员套餐</Text>
|
||
</View>
|
||
<Text style={styles.sectionSubtitle}>灵活选择,跟随节奏稳步提升</Text>
|
||
|
||
{products.length === 0 ? (
|
||
<View style={styles.configurationNotice}>
|
||
<Text style={styles.configurationText}>
|
||
暂未获取到会员商品,请在 RevenueCat 中配置 iOS 产品并同步到当前 Offering。
|
||
</Text>
|
||
</View>
|
||
) : (
|
||
<>
|
||
<View style={styles.plansContainer}>
|
||
{products.map(renderPlanCard)}
|
||
</View>
|
||
{selectedProduct && (
|
||
<View style={styles.tipsContainer}>
|
||
<Ionicons name="bulb" size={18} color="#F29F05" />
|
||
<Text style={styles.tipsText}>{getTipsContent(selectedProduct)}</Text>
|
||
</View>
|
||
)}
|
||
</>
|
||
)}
|
||
</View>
|
||
|
||
<View style={styles.sectionCard}>
|
||
<View style={styles.sectionTitleRow}>
|
||
<View style={styles.sectionTitleBadge}>
|
||
<Ionicons name="checkbox" size={16} color="#FF9F0A" />
|
||
</View>
|
||
<Text style={styles.sectionTitle}>权益对比</Text>
|
||
</View>
|
||
<Text style={styles.sectionSubtitle}>核心权益一目了然,选择更安心</Text>
|
||
|
||
<View style={styles.comparisonTable}>
|
||
<View style={[styles.tableRow, styles.tableHeader]}>
|
||
<Text style={[styles.tableHeaderText, styles.tableTitleCell]}>权益</Text>
|
||
<Text style={[styles.tableHeaderText, styles.tableVipCell]}>VIP</Text>
|
||
<Text style={[styles.tableHeaderText, styles.tableNormalCell]}>普通用户</Text>
|
||
</View>
|
||
{BENEFIT_COMPARISON.map((row, index) => (
|
||
<View
|
||
key={row.title}
|
||
style={[
|
||
styles.tableRow,
|
||
index % 2 === 1 && styles.tableRowAlt,
|
||
]}
|
||
>
|
||
<Text style={[styles.tableCellText, styles.tableTitleCell]}>{row.title}</Text>
|
||
<View style={styles.tableVipCell}>
|
||
{row.vip ? (
|
||
<Ionicons name="checkmark-circle" size={20} color="#FFB200" />
|
||
) : (
|
||
<Ionicons name="remove" size={20} color="#D1D4DA" />
|
||
)}
|
||
</View>
|
||
<View style={styles.tableNormalCell}>
|
||
{row.regular ? (
|
||
<Ionicons name="checkmark-circle" size={20} color="#8E8E93" />
|
||
) : (
|
||
<Ionicons name="remove" size={20} color="#D1D4DA" />
|
||
)}
|
||
</View>
|
||
</View>
|
||
))}
|
||
</View>
|
||
</View>
|
||
|
||
<View style={styles.bottomSection}>
|
||
<View style={styles.noticeBanner}>
|
||
<Text style={styles.noticeText}>667+人正在定制私人计划</Text>
|
||
</View>
|
||
|
||
<TouchableOpacity
|
||
style={[
|
||
styles.purchaseButton,
|
||
(loading || !selectedProduct || !agreementAccepted) && styles.disabledButton
|
||
]}
|
||
onPress={handlePurchase}
|
||
disabled={loading || !selectedProduct || !agreementAccepted}
|
||
accessible={true}
|
||
accessibilityLabel={loading ? '正在处理购买' : '购买会员'}
|
||
accessibilityHint={
|
||
loading
|
||
? '购买正在进行中,请稍候'
|
||
: selectedProduct
|
||
? `点击购买${selectedProduct.title || '已选'}会员套餐`
|
||
: '请选择会员套餐后再进行购买'
|
||
}
|
||
accessibilityState={{ disabled: loading || !selectedProduct || !agreementAccepted }}
|
||
>
|
||
{loading ? (
|
||
<View style={styles.loadingContainer}>
|
||
<ActivityIndicator size="small" color="white" style={styles.loadingSpinner} />
|
||
<Text style={styles.purchaseButtonText}>正在处理购买...</Text>
|
||
</View>
|
||
) : (
|
||
<Text style={styles.purchaseButtonText}>立即订阅</Text>
|
||
)}
|
||
</TouchableOpacity>
|
||
|
||
<View style={styles.agreementRow}>
|
||
<CustomCheckBox
|
||
checked={agreementAccepted}
|
||
onCheckedChange={setAgreementAccepted}
|
||
size={16}
|
||
checkedColor="#E91E63"
|
||
uncheckedColor="#999"
|
||
/>
|
||
<Text style={styles.agreementPrefix}>开通即视为同意</Text>
|
||
<TouchableOpacity
|
||
onPress={() => {
|
||
Linking.openURL(USER_AGREEMENT_URL);
|
||
captureMessage('click user agreement');
|
||
}}
|
||
>
|
||
<Text style={styles.agreementLink}>《用户协议》</Text>
|
||
</TouchableOpacity>
|
||
<Text style={styles.agreementSeparator}>|</Text>
|
||
<TouchableOpacity
|
||
onPress={() => {
|
||
captureMessage('click membership agreement');
|
||
}}
|
||
>
|
||
<Text style={styles.agreementLink}>《会员协议》</Text>
|
||
</TouchableOpacity>
|
||
<Text style={styles.agreementSeparator}>|</Text>
|
||
<TouchableOpacity
|
||
onPress={() => {
|
||
captureMessage('click auto renewal agreement');
|
||
}}
|
||
>
|
||
<Text style={styles.agreementLink}>《自动续费协议》</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
<TouchableOpacity
|
||
style={[styles.restoreButton, (restoring || loading) && styles.disabledRestoreButton]}
|
||
onPress={handleRestore}
|
||
disabled={restoring || loading}
|
||
>
|
||
{restoring ? (
|
||
<View style={styles.restoreButtonContent}>
|
||
<ActivityIndicator size="small" color="#666" style={styles.restoreButtonLoader} />
|
||
<Text style={styles.restoreButtonText}>恢复中...</Text>
|
||
</View>
|
||
) : (
|
||
<Text style={styles.restoreButtonText}>恢复购买</Text>
|
||
)}
|
||
</TouchableOpacity>
|
||
</View>
|
||
</ScrollView>
|
||
</View>
|
||
</View>
|
||
</Modal>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
overlay: {
|
||
flex: 1,
|
||
justifyContent: 'flex-end',
|
||
},
|
||
backdrop: {
|
||
...StyleSheet.absoluteFillObject,
|
||
backgroundColor: 'rgba(0, 0, 0, 0.35)',
|
||
},
|
||
modalContainer: {
|
||
height: height * 0.92,
|
||
backgroundColor: '#F5F6FA',
|
||
borderTopLeftRadius: 28,
|
||
borderTopRightRadius: 28,
|
||
overflow: 'hidden',
|
||
},
|
||
modalContent: {
|
||
flex: 1,
|
||
},
|
||
modalContentContainer: {
|
||
paddingHorizontal: 20,
|
||
paddingBottom: 32,
|
||
paddingTop: 16,
|
||
},
|
||
floatingBackButton: {
|
||
width: 40,
|
||
height: 40,
|
||
borderRadius: 20,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
overflow: 'hidden',
|
||
},
|
||
floatingBackButtonContainer: {
|
||
position: 'absolute',
|
||
top: 20,
|
||
left: 20,
|
||
zIndex: 10, // 确保按钮在最上层
|
||
},
|
||
fallbackBackButton: {
|
||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||
|
||
},
|
||
sectionCard: {
|
||
backgroundColor: '#FFFFFF',
|
||
borderRadius: 20,
|
||
paddingHorizontal: 20,
|
||
paddingVertical: 18,
|
||
marginBottom: 20,
|
||
shadowColor: '#1C1C1E',
|
||
shadowOpacity: 0.06,
|
||
shadowRadius: 12,
|
||
shadowOffset: { width: 0, height: 6 },
|
||
elevation: 2,
|
||
},
|
||
sectionTitleRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
},
|
||
sectionTitleBadge: {
|
||
width: 28,
|
||
height: 28,
|
||
borderRadius: 14,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
backgroundColor: '#F4EFFD',
|
||
marginRight: 8,
|
||
},
|
||
sectionTitle: {
|
||
fontSize: 18,
|
||
fontWeight: '700',
|
||
color: '#2B2B2E',
|
||
},
|
||
sectionSubtitle: {
|
||
fontSize: 13,
|
||
color: '#6B6B73',
|
||
marginTop: 6,
|
||
marginBottom: 16,
|
||
},
|
||
configurationNotice: {
|
||
borderRadius: 16,
|
||
padding: 16,
|
||
backgroundColor: '#FFF4E5',
|
||
},
|
||
configurationText: {
|
||
fontSize: 14,
|
||
color: '#B86A04',
|
||
textAlign: 'center',
|
||
lineHeight: 20,
|
||
},
|
||
plansContainer: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
},
|
||
planCardWrapper: {
|
||
flex: 1,
|
||
marginHorizontal: 4,
|
||
borderRadius: 18,
|
||
overflow: 'hidden',
|
||
borderWidth: 1,
|
||
borderColor: 'rgba(123,44,191,0.08)',
|
||
},
|
||
planCardWrapperSelected: {
|
||
borderColor: '#7B2CBF',
|
||
shadowColor: '#7B2CBF',
|
||
shadowOpacity: 0.18,
|
||
shadowRadius: 16,
|
||
shadowOffset: { width: 0, height: 8 },
|
||
elevation: 4,
|
||
},
|
||
planCardGradient: {
|
||
paddingHorizontal: 16,
|
||
paddingVertical: 18,
|
||
minHeight: 170,
|
||
},
|
||
planTag: {
|
||
alignSelf: 'flex-start',
|
||
backgroundColor: '#2F2F36',
|
||
borderRadius: 14,
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 4,
|
||
marginBottom: 12,
|
||
},
|
||
planTagText: {
|
||
color: '#FFFFFF',
|
||
fontSize: 11,
|
||
fontWeight: '600',
|
||
},
|
||
planCardTitle: {
|
||
fontSize: 18,
|
||
fontWeight: '700',
|
||
color: '#241F1F',
|
||
},
|
||
planCardPrice: {
|
||
fontSize: 28,
|
||
fontWeight: '700',
|
||
marginTop: 12,
|
||
},
|
||
planCardOriginalPrice: {
|
||
fontSize: 13,
|
||
color: '#8E8EA1',
|
||
textDecorationLine: 'line-through',
|
||
marginTop: 4,
|
||
},
|
||
planCardDescription: {
|
||
fontSize: 12,
|
||
color: '#6C6C77',
|
||
marginTop: 12,
|
||
lineHeight: 17,
|
||
},
|
||
tipsContainer: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
backgroundColor: '#FEF4E6',
|
||
borderRadius: 14,
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 10,
|
||
marginTop: 16,
|
||
},
|
||
tipsText: {
|
||
flex: 1,
|
||
fontSize: 12,
|
||
color: '#9B6200',
|
||
marginLeft: 6,
|
||
lineHeight: 16,
|
||
},
|
||
comparisonTable: {
|
||
borderRadius: 16,
|
||
overflow: 'hidden',
|
||
borderWidth: 1,
|
||
borderColor: '#ECECF3',
|
||
},
|
||
tableRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
paddingVertical: 14,
|
||
paddingHorizontal: 16,
|
||
backgroundColor: '#FFFFFF',
|
||
},
|
||
tableHeader: {
|
||
backgroundColor: '#F8F8FF',
|
||
},
|
||
tableHeaderText: {
|
||
fontSize: 12,
|
||
fontWeight: '600',
|
||
color: '#575764',
|
||
textTransform: 'uppercase',
|
||
letterSpacing: 0.4,
|
||
},
|
||
tableCellText: {
|
||
fontSize: 13,
|
||
color: '#3E3E44',
|
||
},
|
||
tableTitleCell: {
|
||
flex: 1.3,
|
||
},
|
||
tableVipCell: {
|
||
flex: 0.8,
|
||
alignItems: 'center',
|
||
},
|
||
tableNormalCell: {
|
||
flex: 0.8,
|
||
alignItems: 'center',
|
||
},
|
||
tableRowAlt: {
|
||
backgroundColor: '#FBFBFF',
|
||
},
|
||
bottomSection: {
|
||
backgroundColor: '#FFFFFF',
|
||
borderRadius: 20,
|
||
paddingHorizontal: 20,
|
||
paddingVertical: 20,
|
||
marginBottom: 10,
|
||
shadowColor: '#1C1C1E',
|
||
shadowOpacity: 0.04,
|
||
shadowRadius: 12,
|
||
shadowOffset: { width: 0, height: 6 },
|
||
elevation: 1,
|
||
},
|
||
noticeBanner: {
|
||
backgroundColor: '#FFE7E0',
|
||
borderRadius: 12,
|
||
paddingVertical: 10,
|
||
paddingHorizontal: 16,
|
||
marginBottom: 16,
|
||
},
|
||
noticeText: {
|
||
color: '#D35400',
|
||
fontSize: 13,
|
||
textAlign: 'center',
|
||
fontWeight: '600',
|
||
},
|
||
purchaseButton: {
|
||
backgroundColor: '#151515',
|
||
borderRadius: 28,
|
||
height: 52,
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
marginBottom: 16,
|
||
},
|
||
disabledButton: {
|
||
backgroundColor: '#C6C6C8',
|
||
},
|
||
purchaseButtonText: {
|
||
color: '#FFFFFF',
|
||
fontSize: 18,
|
||
fontWeight: '700',
|
||
},
|
||
loadingContainer: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
},
|
||
loadingSpinner: {
|
||
marginRight: 8,
|
||
},
|
||
agreementRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
flexWrap: 'wrap',
|
||
marginBottom: 16,
|
||
},
|
||
agreementPrefix: {
|
||
fontSize: 11,
|
||
color: '#666672',
|
||
marginHorizontal: 6,
|
||
},
|
||
agreementLink: {
|
||
fontSize: 11,
|
||
color: '#E91E63',
|
||
textDecorationLine: 'underline',
|
||
fontWeight: '500',
|
||
marginHorizontal: 4,
|
||
},
|
||
agreementSeparator: {
|
||
fontSize: 11,
|
||
color: '#A0A0B0',
|
||
marginHorizontal: 2,
|
||
},
|
||
restoreButton: {
|
||
alignSelf: 'center',
|
||
paddingVertical: 6,
|
||
},
|
||
restoreButtonText: {
|
||
color: '#6F6F7A',
|
||
fontSize: 14,
|
||
fontWeight: '500',
|
||
},
|
||
disabledRestoreButton: {
|
||
opacity: 0.5,
|
||
},
|
||
restoreButtonContent: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
restoreButtonLoader: {
|
||
marginRight: 8,
|
||
},
|
||
disabledPlanCard: {
|
||
opacity: 0.5,
|
||
},
|
||
});
|