Files
digital-pilates/components/model/MembershipModal.tsx
richarjiang bca6670390 Add Chinese translations for medication management and personal settings
- Introduced new translation files for medication, personal, and weight management in Chinese.
- Updated the main index file to include the new translation modules.
- Enhanced the medication type definitions to include 'ointment'.
- Refactored workout type labels to utilize i18n for better localization support.
- Improved sleep quality descriptions and recommendations with i18n integration.
2025-11-28 17:29:51 +08:00

1510 lines
50 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* eslint-disable react-hooks/exhaustive-deps */
import CustomCheckBox from '@/components/ui/CheckBox';
import { USER_AGREEMENT_URL } from '@/constants/Agree';
import { useAppDispatch } from '@/hooks/redux';
import { useI18n } from '@/hooks/useI18n';
import {
MEMBERSHIP_PLAN_META,
extractMembershipProductsFromOfferings,
getActiveSubscriptionIds,
getPlanMetaById,
resolvePlanDisplayName,
type MembershipPlanType,
} from '@/services/membership';
import { fetchMembershipData } from '@/store/membershipSlice';
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;
}
// RevenueCat 在 JS SDK 中以 string[] 返回 activeSubscriptions但保持健壮性以防类型变化
// 权限类型枚举
type PermissionType = 'exclusive' | 'limited' | 'unlimited';
// 权限配置接口
interface PermissionConfig {
type: PermissionType;
text: string;
vipText?: string; // VIP用户的特殊文案可选
}
// 权益对比项接口
interface BenefitItem {
title: string;
description?: string; // 功能描述,可选
vip: PermissionConfig;
regular: PermissionConfig;
}
const PLAN_STYLE_CONFIG: Record<MembershipPlanType, { 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',
},
};
// 根据权限类型获取对应的图标
const getPermissionIcon = (type: PermissionType, isVip: boolean) => {
switch (type) {
case 'exclusive':
return isVip ? (
<Ionicons name="checkmark-circle" size={20} color="#FFB200" />
) : (
<Ionicons name="remove" size={20} color="#D1D4DA" />
);
case 'limited':
return isVip ? (
<Ionicons name="checkmark-circle" size={20} color="#FFB200" />
) : (
<Ionicons name="time-outline" size={20} color="#8E8E93" />
);
case 'unlimited':
return (
<Ionicons name="checkmark-circle" size={20} color={isVip ? "#FFB200" : "#8E8E93"} />
);
default:
return <Ionicons name="remove" size={20} color="#D1D4DA" />;
}
};
export function MembershipModal({ visible, onClose, onPurchaseSuccess }: MembershipModalProps) {
const { t } = useI18n();
const dispatch = useAppDispatch();
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);
// 权益对比配置 - Move inside component to use t function
const benefitComparison: BenefitItem[] = [
{
title: t('membershipModal.benefits.items.aiCalories.title'),
description: t('membershipModal.benefits.items.aiCalories.description'),
vip: {
type: 'unlimited',
text: t('membershipModal.benefits.permissions.unlimited'),
vipText: t('membershipModal.benefits.permissions.unlimited')
},
regular: {
type: 'limited',
text: t('membershipModal.benefits.permissions.limited'),
vipText: t('membershipModal.benefits.permissions.dailyLimit', { count: 3 })
}
},
{
title: t('membershipModal.benefits.items.aiNutrition.title'),
description: t('membershipModal.benefits.items.aiNutrition.description'),
vip: {
type: 'unlimited',
text: t('membershipModal.benefits.permissions.unlimited'),
vipText: t('membershipModal.benefits.permissions.unlimited')
},
regular: {
type: 'limited',
text: t('membershipModal.benefits.permissions.limited'),
vipText: t('membershipModal.benefits.permissions.dailyLimit', { count: 5 })
}
},
{
title: t('membershipModal.benefits.items.healthReminder.title'),
description: t('membershipModal.benefits.items.healthReminder.description'),
vip: {
type: 'unlimited',
text: t('membershipModal.benefits.permissions.fullSupport'),
vipText: t('membershipModal.benefits.permissions.smartReminder')
},
regular: {
type: 'unlimited',
text: t('membershipModal.benefits.permissions.basicSupport'),
vipText: t('membershipModal.benefits.permissions.basicSupport')
}
},
{
title: t('membershipModal.benefits.items.aiMedication.title'),
description: t('membershipModal.benefits.items.aiMedication.description'),
vip: {
type: 'exclusive',
text: t('membershipModal.benefits.permissions.fullAnalysis'),
vipText: t('membershipModal.benefits.permissions.fullAnalysis')
},
regular: {
type: 'exclusive',
text: t('membershipModal.benefits.permissions.notSupported'),
vipText: t('membershipModal.benefits.permissions.notSupported')
}
},
{
title: t('membershipModal.benefits.items.customChallenge.title'),
description: t('membershipModal.benefits.items.customChallenge.description'),
vip: {
type: 'exclusive',
text: t('membershipModal.benefits.permissions.createUnlimited'),
vipText: t('membershipModal.benefits.permissions.createUnlimited')
},
regular: {
type: 'exclusive',
text: t('membershipModal.benefits.permissions.notSupported'),
vipText: t('membershipModal.benefits.permissions.notSupported')
}
},
];
// 根据选中的产品生成tips内容
const getTipsContent = (product: PurchasesStoreProduct | null): string => {
if (!product) return '';
const plan = MEMBERSHIP_PLAN_META.find(item => item.id === product.identifier);
if (!plan) {
return '';
}
switch (plan.type) {
case 'lifetime':
return t('membershipModal.plans.lifetime.subtitle');
case 'quarterly':
return t('membershipModal.plans.quarterly.subtitle');
case 'weekly':
return t('membershipModal.plans.weekly.subtitle');
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 {
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 productsToUse = extractMembershipProductsFromOfferings(offerings);
if (productsToUse.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;
}
log.info('productsToUse', productsToUse)
setProducts(productsToUse);
// 获取产品后,检查用户的购买记录并自动选中对应套餐
await checkAndSelectActivePlan(productsToUse);
// 确保始终有一个选中的产品
setSelectedProduct(current => {
// 如果已经有选中的产品,保持不变
if (current) return current;
// 否则选择第一个可用产品
return 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 activeSubscriptionIds = getActiveSubscriptionIds(customerInfo);
// 检查是否有有效的购买记录
const hasActiveEntitlements = Object.keys(customerInfo.entitlements.active).length > 0;
const hasNonSubscriptionTransactions = customerInfo.nonSubscriptionTransactions.length > 0;
const hasActiveSubscriptions = activeSubscriptionIds.length > 0;
if (hasActiveEntitlements || hasNonSubscriptionTransactions || hasActiveSubscriptions) {
capturePurchaseEvent('success', '监听到购买状态变化', {
hasActiveEntitlements,
hasNonSubscriptionTransactions,
hasActiveSubscriptions,
activeEntitlementsCount: Object.keys(customerInfo.entitlements.active).length,
nonSubscriptionTransactionsCount: customerInfo.nonSubscriptionTransactions.length,
activeSubscriptionsCount: activeSubscriptionIds.length,
modalVisible: visible
});
log.info('检测到购买成功,准备刷新用户信息并关闭弹窗');
// 延迟一点时间,确保购买流程完全完成
setTimeout(async () => {
void dispatch(fetchMembershipData());
// 刷新用户信息
// await refreshUserInfo();
// 调用购买成功回调
onPurchaseSuccess?.();
// 关闭弹窗
onClose?.();
// 显示成功提示
GlobalToast.show({
message: t('membershipModal.success.purchase'),
});
}, 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 });
// 记录详细的购买状态日志
const activeSubscriptionIds = getActiveSubscriptionIds(customerInfo);
captureMessageWithContext('获取用户购买信息成功', {
activeEntitlementsCount: Object.keys(customerInfo.entitlements.active).length,
nonSubscriptionTransactionsCount: customerInfo.nonSubscriptionTransactions.length,
activeSubscriptionsCount: activeSubscriptionIds.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}`);
});
// 检查订阅
activeSubscriptionIds.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 = MEMBERSHIP_PLAN_META.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: activeSubscriptionIds.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 () => {
// 添加调试日志
log.info('handlePurchase 被调用', {
loading,
productsLength: products.length,
selectedProductId: selectedProduct?.identifier,
selectedProductTitle: selectedProduct?.title,
agreementAccepted
});
// 验证是否已同意协议
if (!agreementAccepted) {
Alert.alert(
t('membershipModal.agreements.alert.title'),
t('membershipModal.agreements.alert.message'),
[
{
text: t('membershipModal.agreements.alert.confirm'),
style: 'default',
}
]
);
return;
}
// 如果没有选中的产品但有可用产品,自动选择第一个
if (!selectedProduct && products.length > 0) {
log.info('自动选择第一个可用产品', {
firstProductId: products[0]?.identifier,
firstProductTitle: products[0]?.title
});
setSelectedProduct(products[0]);
return; // 返回让用户确认选择后再点击
}
// 验证是否选择了产品
if (!selectedProduct) {
Alert.alert(
t('membershipModal.errors.selectPlan'),
'',
[
{
text: t('membershipModal.agreements.alert.confirm'),
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 });
const activeSubscriptionIds = getActiveSubscriptionIds(customerInfo);
// 记录购买成功事件
capturePurchaseEvent('success', `购买成功: ${productIdentifier}`, {
productIdentifier,
hasActiveEntitlements: Object.keys(customerInfo.entitlements.active).length > 0,
activeEntitlementsCount: Object.keys(customerInfo.entitlements.active).length,
nonSubscriptionTransactionsCount: customerInfo.nonSubscriptionTransactions.length,
activeSubscriptionsCount: activeSubscriptionIds.length
});
// 购买成功后,监听器会自动处理后续逻辑(刷新用户信息、关闭弹窗等)
log.info('购买流程完成,等待监听器处理后续逻辑');
} catch (error: any) {
captureException(error);
// 记录购买失败事件
capturePurchaseEvent('error', `购买失败: ${error.message || '未知错误'}`, {
errorCode: error.code || null,
errorMessage: error.message || '未知错误',
productIdentifier: selectedProduct.identifier
});
// 处理不同类型的购买错误
if (error.userCancelled || error.code === Purchases.PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {
// 用户取消购买
GlobalToast.show({
message: t('membershipModal.errors.purchaseCancelled'),
});
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.PRODUCT_ALREADY_PURCHASED_ERROR) {
// 商品已拥有
GlobalToast.show({
message: t('membershipModal.errors.alreadyPurchased'),
});
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.NETWORK_ERROR) {
// 网络错误
GlobalToast.show({
message: t('membershipModal.errors.networkError'),
});
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR) {
// 支付待处理
GlobalToast.show({
message: t('membershipModal.errors.paymentPending'),
});
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.INVALID_CREDENTIALS_ERROR) {
// 凭据无效
GlobalToast.show({
message: t('membershipModal.errors.invalidCredentials'),
});
} else {
// 其他错误
GlobalToast.show({
message: t('membershipModal.errors.purchaseFailed'),
});
}
} finally {
// 确保在所有情况下都重置加载状态
setLoading(false);
log.info('购买流程结束,加载状态已重置');
}
};
const handleRestore = async () => {
// 防止重复点击
if (restoring || loading) {
return;
}
try {
setRestoring(true);
captureUserAction('开始恢复购买');
// 恢复购买
const customerInfo = await Purchases.restorePurchases();
log.info('恢复购买结果', { customerInfo });
const activeSubscriptionIds = getActiveSubscriptionIds(customerInfo);
captureMessageWithContext('恢复购买结果', {
activeEntitlementsCount: Object.keys(customerInfo.entitlements.active).length,
nonSubscriptionTransactionsCount: customerInfo.nonSubscriptionTransactions.length,
activeSubscriptionsCount: activeSubscriptionIds.length,
managementUrl: customerInfo.managementURL,
originalAppUserId: customerInfo.originalAppUserId
});
// 检查是否有有效的购买记录
const hasActiveEntitlements = Object.keys(customerInfo.entitlements.active).length > 0;
const hasNonSubscriptionTransactions = customerInfo.nonSubscriptionTransactions.length > 0;
const hasActiveSubscriptions = activeSubscriptionIds.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);
});
// 检查订阅
activeSubscriptionIds.forEach(productId => {
restoredProducts.push(productId);
});
log.info('恢复的产品', { restoredProducts });
capturePurchaseEvent('restore', '恢复购买成功', {
restoredProducts,
restoredProductsCount: restoredProducts.length
});
void dispatch(fetchMembershipData());
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: t('membershipModal.errors.restoreSuccess'),
});
} 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: t('membershipModal.errors.restorePartialFailed'),
});
}
} else {
capturePurchaseEvent('restore', '没有找到购买记录', {
hasActiveEntitlements,
hasNonSubscriptionTransactions,
hasActiveSubscriptions,
activeEntitlementsCount: Object.keys(customerInfo.entitlements.active).length,
nonSubscriptionTransactionsCount: customerInfo.nonSubscriptionTransactions.length,
activeSubscriptionsCount: activeSubscriptionIds.length
});
GlobalToast.show({
message: t('membershipModal.errors.noPurchasesFound'),
});
}
} 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.userCancelled || error.code === Purchases.PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {
GlobalToast.show({
message: t('membershipModal.errors.restoreCancelled'),
});
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.NETWORK_ERROR) {
GlobalToast.show({
message: t('membershipModal.errors.networkError'),
});
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.INVALID_CREDENTIALS_ERROR) {
GlobalToast.show({
message: t('membershipModal.errors.invalidCredentials'),
});
} else {
GlobalToast.show({
message: t('membershipModal.errors.restoreFailed'),
});
}
} finally {
// 确保在所有情况下都重置恢复状态
setRestoring(false);
log.info('恢复购买流程结束,恢复状态已重置');
}
};
const renderPlanCard = (product: PurchasesStoreProduct) => {
const planMeta = getPlanMetaById(product.identifier);
const isSelected = selectedProduct === product;
// 优先使用翻译的标题,如果找不到 meta 则回退到产品标题
let displayTitle = product.title;
let displaySubtitle = planMeta?.subtitle ?? '';
if (planMeta) {
displayTitle = t(`membershipModal.plans.${planMeta.type}.title`);
displaySubtitle = t(`membershipModal.plans.${planMeta.type}.subtitle`);
} else {
// 如果没有 meta尝试使用 resolvePlanDisplayName (虽然这里主要依赖 meta)
displayTitle = resolvePlanDisplayName(product, planMeta);
}
const priceLabel = product.priceString || '';
const styleConfig = planMeta ? PLAN_STYLE_CONFIG[planMeta.type] : undefined;
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 ? t('membershipModal.loading.purchase') : t('membershipModal.actions.selectPlan', { plan: 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}
>
<View style={styles.planCardTopSection}>
{planMeta?.tag && (
<View style={styles.planTag}>
<Text style={styles.planTagText}>{t('membershipModal.plans.tag')}</Text>
</View>
)}
<Text style={styles.planCardTitle}>{displayTitle}</Text>
</View>
<View style={styles.planCardMiddleSection}>
<Text style={[styles.planCardPrice, styleConfig && { color: styleConfig.accent }]}>
{priceLabel || '--'}
</Text>
{planMeta?.originalPrice && (
<Text style={styles.planCardOriginalPrice}>{planMeta.originalPrice}</Text>
)}
</View>
<View style={styles.planCardBottomSection}>
<Text style={styles.planCardDescription}>{displaySubtitle}</Text>
</View>
</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={t('membershipModal.actions.back')}
accessibilityHint={t('membershipModal.actions.close')}
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, {
marginTop: 56
}]}>
<View style={styles.sectionTitleRow}>
<View style={styles.sectionTitleBadge}>
<Ionicons name="star" size={16} color="#7B2CBF" />
</View>
<Text style={styles.sectionTitle}>{t('membershipModal.sectionTitle.plans')}</Text>
</View>
<Text style={styles.sectionSubtitle}>{t('membershipModal.sectionTitle.plansSubtitle')}</Text>
{products.length === 0 ? (
<View style={styles.configurationNotice}>
<Text style={styles.configurationText}>
{t('membershipModal.errors.noProducts')}
</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}>{t('membershipModal.benefits.title')}</Text>
</View>
<Text style={styles.sectionSubtitle}>{t('membershipModal.benefits.subtitle')}</Text>
<View style={styles.comparisonTable}>
<View style={[styles.tableRow, styles.tableHeader]}>
<Text style={[styles.tableHeaderText, styles.tableTitleCell]}>{t('membershipModal.benefits.table.benefit')}</Text>
<Text style={[styles.tableHeaderText, styles.tableVipCell]}>{t('membershipModal.benefits.table.vip')}</Text>
<Text style={[styles.tableHeaderText, styles.tableNormalCell]}>{t('membershipModal.benefits.table.regular')}</Text>
</View>
{benefitComparison.map((row, index) => (
<View
key={row.title}
style={[
styles.tableRow,
index % 2 === 1 && styles.tableRowAlt,
]}
>
<View style={styles.tableTitleCell}>
<Text style={styles.tableCellText}>{row.title}</Text>
{row.description && (
<Text style={styles.tableDescriptionText}>{row.description}</Text>
)}
</View>
{/* VIP 权限列 */}
<View style={styles.tableVipCell}>
<View style={styles.permissionContainer}>
{getPermissionIcon(row.vip.type, true)}
<Text style={styles.permissionText}>{row.vip.vipText || row.vip.text}</Text>
</View>
</View>
{/* 普通用户权限列 */}
<View style={styles.tableNormalCell}>
<View style={styles.permissionContainer}>
{getPermissionIcon(row.regular.type, false)}
<Text style={styles.permissionText}>{row.regular.vipText || row.regular.text}</Text>
</View>
</View>
</View>
))}
</View>
</View>
<View style={styles.bottomSection}>
<View style={styles.agreementContainer}>
<View style={styles.checkboxWrapper}>
<CustomCheckBox
checked={agreementAccepted}
onCheckedChange={setAgreementAccepted}
size={16}
checkedColor="#E91E63"
uncheckedColor="#999"
/>
</View>
<Text style={styles.agreementText}>
{t('membershipModal.agreements.prefix')}
<Text
style={styles.agreementLink}
onPress={() => {
Linking.openURL(USER_AGREEMENT_URL);
captureMessage('click user agreement');
}}
>
{t('membershipModal.agreements.userAgreement')}
</Text>
<Text style={styles.agreementSeparator}> | </Text>
<Text
style={styles.agreementLink}
onPress={() => {
captureMessage('click membership agreement');
}}
>
{t('membershipModal.agreements.membershipAgreement')}
</Text>
<Text style={styles.agreementSeparator}> | </Text>
<Text
style={styles.agreementLink}
onPress={() => {
captureMessage('click auto renewal agreement');
}}
>
{t('membershipModal.agreements.autoRenewalAgreement')}
</Text>
</Text>
</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}>{t('membershipModal.actions.restoring')}</Text>
</View>
) : (
<Text style={styles.restoreButtonText}>{t('membershipModal.actions.restore')}</Text>
)}
</TouchableOpacity>
</View>
</ScrollView>
{/* 悬浮购买按钮 */}
<View style={styles.floatingPurchaseContainer}>
{isLiquidGlassAvailable() ? (
<GlassView
style={[
styles.purchaseButton,
(loading || products.length === 0) && styles.disabledButton
]}
glassEffectStyle="clear"
tintColor="rgba(21, 21, 21, 0.8)"
isInteractive={true}
>
<TouchableOpacity
onPress={handlePurchase}
disabled={loading || products.length === 0}
accessible={true}
accessibilityLabel={loading ? t('membershipModal.actions.processing') : t('membershipModal.actions.subscribe')}
accessibilityHint={
loading
? t('membershipModal.loading.purchase')
: products.length === 0
? t('membershipModal.loading.products')
: !selectedProduct
? t('membershipModal.errors.selectPlan')
: t('membershipModal.actions.purchaseHint', { plan: selectedProduct.title || '已选' })
}
accessibilityState={{ disabled: loading || products.length === 0 }}
style={styles.purchaseButtonContent}
>
{loading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="small" color="white" style={styles.loadingSpinner} />
<Text style={styles.purchaseButtonText}>{t('membershipModal.actions.processing')}</Text>
</View>
) : (
<Text style={styles.purchaseButtonText}>{t('membershipModal.actions.subscribe')}</Text>
)}
</TouchableOpacity>
</GlassView>
) : (
<View
style={[
styles.purchaseButton,
styles.fallbackPurchaseButton,
(loading || products.length === 0) && styles.disabledButton
]}
>
<TouchableOpacity
onPress={handlePurchase}
disabled={loading || products.length === 0}
accessible={true}
accessibilityLabel={loading ? t('membershipModal.actions.processing') : t('membershipModal.actions.subscribe')}
accessibilityHint={
loading
? t('membershipModal.loading.purchase')
: products.length === 0
? t('membershipModal.loading.products')
: !selectedProduct
? t('membershipModal.errors.selectPlan')
: t('membershipModal.actions.purchaseHint', { plan: selectedProduct.title || '已选' })
}
accessibilityState={{ disabled: loading || products.length === 0 }}
style={styles.purchaseButtonContent}
>
{loading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="small" color="white" style={styles.loadingSpinner} />
<Text style={styles.purchaseButtonText}>{t('membershipModal.actions.processing')}</Text>
</View>
) : (
<Text style={styles.purchaseButtonText}>{t('membershipModal.actions.subscribe')}</Text>
)}
</TouchableOpacity>
</View>
)}
</View>
</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: 100, // 增加底部内边距,避免内容被悬浮按钮遮挡
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',
fontFamily: 'AliBold',
},
sectionSubtitle: {
fontSize: 13,
color: '#6B6B73',
marginTop: 6,
marginBottom: 16,
fontFamily: 'AliRegular',
},
configurationNotice: {
borderRadius: 16,
padding: 16,
backgroundColor: '#FFF4E5',
},
configurationText: {
fontSize: 14,
color: '#B86A04',
textAlign: 'center',
lineHeight: 20,
fontFamily: 'AliRegular',
},
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: {
flex: 1,
paddingHorizontal: 16,
paddingVertical: 18,
minHeight: 170,
justifyContent: 'space-between',
},
planTag: {
alignSelf: 'flex-start',
backgroundColor: '#2F2F36',
borderRadius: 14,
paddingHorizontal: 6,
paddingVertical: 6,
marginBottom: 12,
},
planTagText: {
color: '#FFFFFF',
fontSize: 10,
fontWeight: '600',
fontFamily: 'AliBold',
},
planCardTitle: {
fontSize: 16,
fontWeight: '700',
color: '#241F1F',
fontFamily: 'AliBold',
},
planCardPrice: {
fontSize: 14,
fontWeight: '700',
marginTop: 12,
fontFamily: 'AliBold',
},
planCardOriginalPrice: {
fontSize: 13,
color: '#8E8EA1',
textDecorationLine: 'line-through',
marginTop: 2,
fontFamily: 'AliRegular',
},
planCardDescription: {
fontSize: 12,
color: '#6C6C77',
lineHeight: 17,
fontFamily: 'AliRegular',
},
planCardTopSection: {
flex: 1,
justifyContent: 'flex-start',
},
planCardMiddleSection: {
flex: 1,
justifyContent: 'center',
alignItems: 'flex-start',
},
planCardBottomSection: {
flex: 1,
justifyContent: 'flex-end',
},
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,
fontFamily: 'AliRegular',
},
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,
fontFamily: 'AliBold',
},
tableCellText: {
fontSize: 13,
color: '#3E3E44',
fontFamily: 'AliRegular',
},
tableTitleCell: {
flex: 1.5,
justifyContent: 'center',
},
tableVipCell: {
flex: 0.8,
alignItems: 'center',
justifyContent: 'center',
},
tableNormalCell: {
flex: 0.8,
alignItems: 'center',
justifyContent: '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,
},
purchaseButton: {
borderRadius: 28,
height: 64,
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden',
},
purchaseButtonContent: {
flex: 1,
width: '100%',
justifyContent: 'center',
alignItems: 'center',
},
floatingPurchaseContainer: {
position: 'absolute',
bottom: 34, // 底部安全区域
left: 20,
right: 20,
},
fallbackPurchaseButton: {
backgroundColor: '#151515',
},
disabledButton: {
backgroundColor: '#C6C6C8',
},
purchaseButtonText: {
color: '#FFFFFF',
fontSize: 18,
fontWeight: '700',
fontFamily: 'AliBold',
},
loadingContainer: {
flexDirection: 'row',
alignItems: 'center',
},
loadingSpinner: {
marginRight: 8,
},
agreementContainer: {
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'center',
marginBottom: 20,
paddingHorizontal: 4,
},
checkboxWrapper: {
marginTop: 2, // Align with text line-height
marginRight: 8,
},
agreementText: {
flex: 1,
fontSize: 11,
lineHeight: 16,
color: '#666672',
fontFamily: 'AliRegular',
},
agreementLink: {
fontSize: 11,
color: '#E91E63',
fontWeight: '500',
textDecorationLine: 'underline',
fontFamily: 'AliBold',
},
agreementSeparator: {
fontSize: 11,
color: '#A0A0B0',
},
restoreButton: {
alignSelf: 'center',
paddingVertical: 6,
},
restoreButtonText: {
color: '#6F6F7A',
fontSize: 12,
fontWeight: '500',
fontFamily: 'AliBold',
},
disabledRestoreButton: {
opacity: 0.5,
},
restoreButtonContent: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
restoreButtonLoader: {
marginRight: 8,
},
disabledPlanCard: {
opacity: 0.5,
},
// 新增样式:权限相关
tableDescriptionText: {
fontSize: 11,
color: '#8E8E93',
marginTop: 2,
lineHeight: 14,
fontFamily: 'AliRegular',
},
permissionContainer: {
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
paddingVertical: 4,
},
permissionText: {
fontSize: 10,
color: '#6B6B73',
marginTop: 4,
textAlign: 'center',
lineHeight: 12,
fontFamily: 'AliRegular',
},
});