Files
digital-pilates/components/model/MembershipModal.tsx
richarjiang 78620f18ee feat: 更新依赖项并优化组件结构
- 在 package.json 和 package-lock.json 中新增 @sentry/react-native、react-native-device-info 和 react-native-purchases 依赖
- 更新统计页面,替换 CircularRing 组件为 FitnessRingsCard,增强健身数据展示
- 在布局文件中引入 ToastProvider,优化用户通知体验
- 新增 SuccessToast 组件,提供全局成功提示功能
- 更新健康数据获取逻辑,支持健身圆环数据的提取
- 优化多个组件的样式和交互,提升用户体验
2025-08-21 09:51:25 +08:00

1176 lines
36 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 { useAuth } from '@/contexts/AuthContext';
// import { UserApi } from '@/services';
import {
captureMessage,
captureMessageWithContext,
capturePurchaseEvent,
captureUserAction
} from '@/utils/sentry.utils';
import { Toast as GlobalToast } from '@/utils/toast.utils';
import { MaterialIcons } from '@expo/vector-icons';
import { captureException } from '@sentry/react-native';
import React, { useEffect, useRef, useState } from 'react';
import {
ActivityIndicator,
Alert,
Dimensions,
Image,
Linking,
Modal,
Platform,
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;
title: string;
subtitle: string;
price: string;
originalPrice?: string;
discount?: string;
type: 'weekly' | 'quarterly' | 'lifetime';
recommended?: boolean;
}
const DEFAULT_PLANS: MembershipPlan[] = [
{
id: 'com.ilookai.mind_gpt.Lifetime',
title: '终身会员',
subtitle: '每天0.01元\n永久有效',
price: '¥128',
type: 'lifetime',
recommended: true,
},
{
id: 'com.ilookai.mind_gpt.ThreeMonths',
title: '季度会员',
subtitle: '每天1元\n连续包季',
price: '¥98',
type: 'quarterly',
},
{
id: 'weekly_membership',
title: '周会员',
subtitle: '新人专享\n连续包周',
price: '¥18',
type: 'weekly',
},
];
// {
// identifier: 'com.ilookai.mind_gpt.Lifetime',
// description: '终身会员',
// title: '终身会员',
// price: 128,
// priceString: '¥128',
// pricePerWeek: 128,
// pricePerMonth: 128,
// pricePerYear: 128,
// }, {
// identifier: 'com.ilookai.mind_gpt.ThreeMonths',
// description: '季度会员',
// title: '季度会员',
// price: 98,
// priceString: '¥98',
// pricePerWeek: 98,
// pricePerMonth: 98,
// pricePerYear: 98,
// }, {
// identifier: 'weekly_membership',
// description: '周会员',
// title: '周会员',
// price: 18,
// priceString: '¥18',
// pricePerWeek: 18,
// pricePerMonth: 18,
// pricePerYear: 18,
// }
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 '';
// 这里您可以根据不同的产品返回不同的提示内容
switch (product.identifier) {
case 'com.ilookai.mind_gpt.Lifetime':
return '一次购买,永久享受所有功能';
case 'com.ilookai.mind_gpt.ThreeMonths':
case 'weekly_membership':
return '到期后自动续费,可以随时随地取消';
default:
return '';
}
};
// useEffect(() => {
// if (user) {
// captureMessage('用户已登录,开始初始化 Purchases');
// initPurchases();
// }
// }, [user]); // 初始化只需要执行一次
// 单独的 useEffect 来处理购买监听器,依赖 visible 状态
useEffect(() => {
if (visible) {
setupPurchaseListener();
// 重置协议状态
setAgreementAccepted(false);
} else {
// 弹窗关闭时移除监听器
removePurchaseListener();
}
// 组件卸载时确保移除监听器
return () => {
removePurchaseListener();
console.log('MembershipModal 购买监听器已清理');
};
}, [visible]); // 依赖 visible 状态
const initPurchases = async () => {
if (Platform.OS === 'ios') {
Purchases.configure({
apiKey: 'appl_UXFtPsBsFIsBOxoNGXoPwpXhGYk'
});
} else if (Platform.OS === 'android') {
// Purchases.configure({
// apiKey: 'goog_ZqYxWbQvRgLzBnVfYdXcWbVn'
// });
}
capturePurchaseEvent('init', 'Purchases 初始化成功');
try {
// if (user?.id) {
// await Purchases.logIn(user.id);
// captureMessageWithContext('用户登录 Purchases 成功', {
// userId: user.id
// });
// }
const offerings = await Purchases.getOfferings();
console.log('offerings', offerings);
captureMessageWithContext('获取产品套餐成功', {
currentOffering: offerings.current?.identifier || null,
availablePackagesCount: offerings.current?.availablePackages.length || 0,
allOfferingsCount: Object.keys(offerings.all).length
});
if (offerings.current !== null && offerings.current.availablePackages.length !== 0) {
// 当前活跃的套餐存在
const packages = offerings.current.availablePackages;
console.log('Available packages:', packages);
// packages 是一个数组,包含 PurchasePackage 对象,每个对象都有 product 信息
// 可以根据这些信息来渲染你的 UI例如价格、标题等
const sortedProducts = packages.sort((a, b) => b.product.price - a.product.price).map(pkg => pkg.product);
setProducts(sortedProducts);
// 获取产品后,检查用户的购买记录并自动选中对应套餐
await checkAndSelectActivePlan(sortedProducts);
} else {
console.warn('No active offerings found or no packages available.');
captureMessageWithContext('没有找到可用的产品套餐', {
hasCurrentOffering: offerings.current !== null,
packagesLength: offerings.current?.availablePackages.length || 0
});
}
} catch (e: any) {
console.log('error', e);
// Error fetching customer info
captureException(e);
// captureMessageWithContext('初始化 Purchases 失败', {
// error: e.message || '未知错误',
// userId: user?.id || null
// });
}
}
console.log('visible', visible);
// 添加购买状态监听器
const setupPurchaseListener = () => {
console.log('设置购买监听器,当前 visible 状态:', visible);
// 如果已经有监听器,先移除
if (purchaseListenerRef.current) {
removePurchaseListener();
}
// 创建监听器函数
const listener = (customerInfo: CustomerInfo) => {
console.log('addCustomerInfoUpdateListener:', customerInfo);
console.log('addCustomerInfoUpdateListener 触发时 visible 状态:', 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
});
console.log('检测到购买成功,刷新用户信息并关闭弹窗');
// 延迟一点时间,确保购买流程完全完成
setTimeout(async () => {
// 刷新用户信息
// await refreshUserInfo();
// 调用购买成功回调
onPurchaseSuccess?.();
// 关闭弹窗
onClose?.();
// 显示成功提示
GlobalToast.show({
message: '会员开通成功',
});
}, 1000);
}
};
// 保存监听器引用
purchaseListenerRef.current = listener;
// 添加监听器
Purchases.addCustomerInfoUpdateListener(listener);
console.log('购买监听器已添加');
};
// 移除购买状态监听器
const removePurchaseListener = () => {
if (purchaseListenerRef.current) {
console.log('移除购买监听器');
Purchases.removeCustomerInfoUpdateListener(purchaseListenerRef.current);
purchaseListenerRef.current = null;
console.log('购买监听器已移除');
}
};
// 检查用户的购买记录并自动选中对应套餐
const checkAndSelectActivePlan = async (availableProducts: PurchasesStoreProduct[]) => {
try {
captureUserAction('开始检查用户购买记录');
// 获取用户的购买信息
const customerInfo = await Purchases.getCustomerInfo();
console.log('用户购买信息:', 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);
console.log(`激活的权益: ${key}, 产品ID: ${entitlement.productIdentifier}`);
});
// 检查非订阅购买(如终身会员)
customerInfo.nonSubscriptionTransactions.forEach(transaction => {
activePurchasedProductIds.push(transaction.productIdentifier);
console.log(`非订阅购买: ${transaction.productIdentifier}, 购买时间: ${transaction.purchaseDate}`);
});
// 检查订阅
Object.keys(customerInfo.activeSubscriptions).forEach(productId => {
activePurchasedProductIds.push(productId);
console.log(`激活的订阅: ${productId}`);
});
// 去重
activePurchasedProductIds = [...new Set(activePurchasedProductIds)];
captureMessageWithContext('用户激活的产品列表', {
activePurchasedProductIds,
activePurchasedProductCount: activePurchasedProductIds.length
});
if (activePurchasedProductIds.length > 0) {
// 尝试在可用产品中找到匹配的产品
let selectedProduct: PurchasesStoreProduct | null = null;
// 优先级:终身会员 > 季度会员 > 周会员
const priorityOrder = [
'com.ilookai.mind_gpt.Lifetime',
'com.ilookai.mind_gpt.ThreeMonths',
'weekly_membership'
];
// 按照优先级查找
for (const priorityProductId of priorityOrder) {
if (activePurchasedProductIds.includes(priorityProductId)) {
selectedProduct = availableProducts.find(product => product.identifier === priorityProductId) || null;
if (selectedProduct) {
console.log(`找到优先级最高的激活产品: ${priorityProductId}`);
break;
}
}
}
// 如果按优先级没找到,尝试找到任何匹配的产品
if (!selectedProduct) {
for (const productId of activePurchasedProductIds) {
selectedProduct = availableProducts.find(product => product.identifier === productId) || null;
if (selectedProduct) {
console.log(`找到匹配的激活产品: ${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)
});
console.log('用户有激活的购买记录,但没有找到匹配的可用产品');
}
} else {
captureMessageWithContext('用户没有激活的购买记录', {
hasEntitlements: Object.keys(customerInfo.entitlements.active).length > 0,
hasNonSubscriptions: customerInfo.nonSubscriptionTransactions.length > 0,
hasActiveSubscriptions: Object.keys(customerInfo.activeSubscriptions).length > 0
});
console.log('用户没有激活的购买记录,使用默认选择逻辑');
}
} catch (error: any) {
console.log('检查用户购买记录失败:', error);
captureException(error);
captureMessageWithContext('检查用户购买记录失败', {
error: error.message || '未知错误',
errorCode: error.code || null
});
}
};
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);
console.log('购买成功 - customerInfo:', customerInfo);
console.log('购买成功 - productIdentifier:', 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
});
// 购买成功后,监听器会自动处理后续逻辑(刷新用户信息、关闭弹窗等)
console.log('购买流程完成,等待监听器处理后续逻辑');
} 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);
console.log('购买流程结束,加载状态已重置');
}
};
const handleRestore = async () => {
// 防止重复点击
if (restoring || loading) {
return;
}
try {
setRestoring(true);
captureUserAction('开始恢复购买');
// 恢复购买
const customerInfo = await Purchases.restorePurchases();
console.log('恢复购买结果:', 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);
});
console.log('恢复的产品:', 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
// }
// });
// console.log('后台恢复购买响应:', restoreResponse);
// captureMessageWithContext('后台恢复购买成功', {
// responseData: restoreResponse,
// restoredProductsCount: restoredProducts.length
// });
// 刷新用户信息
// await refreshUserInfo();
// 调用购买成功回调
onPurchaseSuccess?.();
// 关闭弹窗
onClose?.();
GlobalToast.show({
message: '恢复购买成功',
});
} catch (apiError: any) {
console.log('后台恢复购买接口调用失败:', apiError);
captureException(apiError);
captureMessageWithContext('后台恢复购买接口失败', {
error: apiError.message || '未知错误',
errorCode: apiError.code || null,
restoredProductsCount: restoredProducts.length
});
// 即使后台接口失败,也显示恢复成功(因为 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) {
console.log('恢复购买失败:', error);
captureException(error);
// 记录恢复购买失败事件
capturePurchaseEvent('error', `恢复购买失败: ${error.message || '未知错误'}`, {
errorCode: error.code || null,
errorMessage: error.message || '未知错误'
});
// 处理特定的恢复购买错误
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);
console.log('恢复购买流程结束,恢复状态已重置');
}
};
const renderMembershipBenefits = () => (
<View style={styles.benefitsContainer}>
<Image source={require('@/assets/images/img_vip_unlock_title.png')} style={styles.benefitsTitleBg} />
<View style={styles.benefitItem}>
<MaterialIcons name="check-circle" size={20} color="#DF42D0" />
<Text style={styles.benefitText}></Text>
</View>
<View style={styles.benefitItem}>
<MaterialIcons name="check-circle" size={20} color="#DF42D0" />
<Text style={styles.benefitText}></Text>
</View>
<View style={styles.benefitItem}>
<MaterialIcons name="check-circle" size={20} color="#DF42D0" />
<Text style={styles.benefitText}>广</Text>
</View>
<View style={styles.benefitItem}>
<MaterialIcons name="check-circle" size={20} color="#DF42D0" />
<Text style={styles.benefitText}></Text>
</View>
{/* 皇冠图标 */}
<View style={styles.crownContainer}>
<Image
source={require('@/assets/images/img_profile_vip_bg.png')}
style={styles.crownIcon}
/>
</View>
</View>
);
const renderPlanCard = (product: PurchasesStoreProduct) => {
const plan = DEFAULT_PLANS.find(p => p.id === product.identifier);
if (!plan) {
return null;
}
const isSelected = selectedProduct === product;
return (
<TouchableOpacity
key={product.identifier}
style={[
styles.planCard,
isSelected && styles.selectedPlan,
loading && styles.disabledPlanCard,
]}
onPress={() => !loading && product && setSelectedProduct(product)}
disabled={loading}
activeOpacity={loading ? 1 : 0.8}
accessible={true}
accessibilityLabel={`${product.title} ${plan.price}`}
accessibilityHint={loading ? "购买进行中,无法切换套餐" : `选择${product.title}套餐`}
accessibilityState={{ disabled: loading, selected: isSelected }}
>
{product.identifier === 'com.ilookai.mind_gpt.Lifetime' && (
<View style={styles.recommendedBadge}>
<Text style={styles.recommendedText}></Text>
</View>
)}
<View style={styles.planHeader}>
<Text style={[
styles.planTitle,
plan.type === 'lifetime' && styles.lifetimePlanTitle,
plan.type === 'quarterly' && styles.quarterlyPlanTitle,
plan.type === 'weekly' && styles.weeklyPlanTitle,
]}>
{product.title}
</Text>
</View>
<View style={styles.planPricing}>
<Text style={[
styles.planPrice,
plan.type === 'lifetime' && styles.lifetimePlanPrice,
plan.type === 'quarterly' && styles.quarterlyPlanPrice,
plan.type === 'weekly' && styles.weeklyPlanPrice,
]}>
{plan.price}
</Text>
</View>
<Text style={styles.planSubtitle}>{plan.subtitle}</Text>
</TouchableOpacity>
);
};
return (
<Modal
visible={visible}
transparent={true}
animationType="slide"
presentationStyle="overFullScreen"
>
<View style={styles.overlay}>
{/* 半透明背景 */}
<TouchableOpacity
style={styles.backdrop}
activeOpacity={1}
onPress={onClose} // 阻止点击背景关闭
/>
{/* 会员内容 */}
<View style={styles.modalContainer}>
<ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false}>
{/* 关闭按钮 */}
{/* <TouchableOpacity
style={styles.closeButton}
onPress={onClose}
>
<Text style={styles.closeButtonText}>×</Text>
</TouchableOpacity> */}
{/* 会员权益介绍 */}
{renderMembershipBenefits()}
{/* 会员套餐选择 */}
<View style={styles.plansContainer}>
{products.map(renderPlanCard)}
</View>
{/* 产品选择提示区域 */}
{selectedProduct && (
<View style={styles.tipsContainer}>
<Text style={styles.tipsText}>
{getTipsContent(selectedProduct)}
</Text>
</View>
)}
{/* 协议同意区域 */}
<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={() => {
// Linking.openURL(MEMBERSHIP_AGREEMENT_URL);
captureMessage('click membership agreement');
}}>
<Text style={styles.agreementLink}></Text>
</TouchableOpacity>
<Text style={styles.agreementSeparator}> | </Text>
<TouchableOpacity onPress={() => {
// Linking.openURL(AUTO_RENEWAL_AGREEMENT_URL);
captureMessage('click auto renewal agreement');
}}>
<Text style={styles.agreementLink}></Text>
</TouchableOpacity>
</View>
{/* 购买按钮 */}
<TouchableOpacity
style={[
styles.purchaseButton,
loading && styles.disabledButton
]}
onPress={handlePurchase}
disabled={loading}
accessible={true}
accessibilityLabel={loading ? "正在处理购买" : "购买会员"}
accessibilityHint={loading ? "购买正在进行中,请稍候" : "点击购买选中的会员套餐"}
accessibilityState={{ disabled: loading }}
>
{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>
{/* 恢复购买按钮 */}
<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>
</ScrollView>
</View>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
overlay: {
flex: 1,
justifyContent: 'flex-end',
},
backdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.3)',
},
modalContainer: {
height: height * 0.6,
backgroundColor: 'white',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
overflow: 'hidden',
},
modalContent: {
flex: 1,
paddingHorizontal: 20,
paddingTop: 20,
},
closeButton: {
position: 'absolute',
top: 0,
right: 0,
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: 'rgba(0, 0, 0, 0.1)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1,
},
closeButtonText: {
fontSize: 18,
color: '#666',
fontWeight: '300',
},
configurationNotice: {
backgroundColor: '#FFF9E6',
borderRadius: 12,
padding: 16,
marginTop: 30,
marginBottom: 20,
alignItems: 'center',
borderWidth: 1,
borderColor: '#FFE4B5',
},
configurationText: {
fontSize: 14,
color: '#B8860B',
textAlign: 'center',
marginTop: 8,
marginBottom: 12,
},
configurationButton: {
backgroundColor: '#FF9500',
borderRadius: 8,
paddingHorizontal: 16,
paddingVertical: 8,
},
configurationButtonText: {
color: 'white',
fontSize: 12,
fontWeight: '600',
},
benefitsContainer: {
position: 'relative',
backgroundColor: '#FFE7F9',
borderRadius: 16,
marginBottom: 30,
padding: 20,
},
benefitsTitleBg: {
width: 180,
height: 20,
marginBottom: 10,
},
benefitItem: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 10,
},
benefitText: {
fontSize: 14,
color: '#333',
marginLeft: 8,
fontWeight: '500',
},
crownContainer: {
position: 'absolute',
top: 10,
right: 10,
width: 100,
height: 100,
justifyContent: 'center',
alignItems: 'center',
},
crownIcon: {
width: 80,
height: 80,
resizeMode: 'contain',
},
plansContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 20,
},
planCard: {
flex: 1,
marginHorizontal: 4,
borderRadius: 12,
borderWidth: 2,
borderColor: '#E0E0E0',
paddingVertical: 20,
paddingHorizontal: 12,
alignItems: 'center',
position: 'relative',
backgroundColor: 'white',
},
selectedPlan: {
borderColor: '#DF42D0',
backgroundColor: '#FFF5FE',
},
recommendedBadge: {
position: 'absolute',
top: -10,
backgroundColor: '#7B2CBF',
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 4,
},
recommendedText: {
color: 'white',
fontSize: 10,
fontWeight: 'bold',
},
planHeader: {
alignItems: 'center',
marginBottom: 8,
},
planTitle: {
fontSize: 16,
fontWeight: '600',
color: '#333',
},
lifetimePlanTitle: {
color: '#7B2CBF',
},
quarterlyPlanTitle: {
color: '#DF42D0',
},
weeklyPlanTitle: {
color: '#FF9500',
},
planPricing: {
alignItems: 'center',
marginBottom: 8,
},
planPrice: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
},
lifetimePlanPrice: {
color: '#7B2CBF',
},
quarterlyPlanPrice: {
color: '#DF42D0',
},
weeklyPlanPrice: {
color: '#FF9500',
},
planSubtitle: {
fontSize: 12,
color: '#666',
textAlign: 'center',
lineHeight: 16,
},
purchaseButton: {
backgroundColor: '#DF42D0',
borderRadius: 25,
height: 50,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 15,
marginTop: 10,
shadowColor: '#DF42D0',
shadowOffset: {
width: 0,
height: 4,
},
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 6,
},
disabledButton: {
backgroundColor: '#ccc',
shadowOpacity: 0,
elevation: 0,
},
purchaseButtonText: {
color: 'white',
fontSize: 18,
fontWeight: '600',
},
restoreButton: {
backgroundColor: 'transparent',
borderRadius: 25,
height: 50,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 20,
},
restoreButtonText: {
color: '#666',
fontSize: 16,
fontWeight: '500',
},
disabledRestoreButton: {
opacity: 0.5,
},
restoreButtonContent: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
restoreButtonLoader: {
marginRight: 8,
},
loadingContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
loadingSpinner: {
marginRight: 8,
},
disabledPlanCard: {
opacity: 0.5,
},
agreementRow: {
width: '100%',
marginTop: 6,
borderRadius: 12,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
agreementPrefix: {
fontSize: 10,
color: '#333',
marginRight: 4,
},
agreementLink: {
fontSize: 10,
color: '#E91E63',
textDecorationLine: 'underline',
fontWeight: '500',
},
agreementSeparator: {
fontSize: 10,
color: '#666',
marginHorizontal: 2,
},
tipsContainer: {
borderRadius: 8,
},
tipsText: {
fontSize: 12,
color: '#666',
lineHeight: 18,
textAlign: 'center',
},
});