Files
digital-pilates/components/model/MembershipModal.tsx
richarjiang 82edb2593c feat(membership): 重构会员购买界面并添加图标库使用规范
- 将 MaterialIcons 替换为 Ionicons 以保持图标库一致性
- 重新设计会员购买界面,采用分段卡片布局和权益对比表格
- 添加 Liquid Glass 兼容的悬浮返回按钮
- 优化套餐卡片样式,使用渐变背景和标签展示
- 添加会员权益对比功能,清晰展示 VIP 与普通用户差异
- 更新任务文档,记录图标库使用规范和按钮组件 Liquid Glass 兼容性实现模式
2025-10-27 08:19:15 +08:00

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