- 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.
1510 lines
50 KiB
TypeScript
1510 lines
50 KiB
TypeScript
/* 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',
|
||
},
|
||
});
|