diff --git a/app.json b/app.json index c44bd1b..15ce99d 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Out Live", "slug": "digital-pilates", - "version": "1.0.19", + "version": "1.0.20", "orientation": "portrait", "scheme": "digitalpilates", "userInterfaceStyle": "light", diff --git a/app/(tabs)/personal.tsx b/app/(tabs)/personal.tsx index 963c122..9d8b762 100644 --- a/app/(tabs)/personal.tsx +++ b/app/(tabs)/personal.tsx @@ -2,6 +2,7 @@ import ActivityHeatMap from '@/components/ActivityHeatMap'; import { PRIVACY_POLICY_URL, USER_AGREEMENT_URL } from '@/constants/Agree'; import { ROUTES } from '@/constants/Routes'; import { getTabBarBottomPadding } from '@/constants/TabBar'; +import { useMembershipModal } from '@/contexts/MembershipModalContext'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useNotifications } from '@/hooks/useNotifications'; @@ -14,7 +15,7 @@ import { useFocusEffect } from '@react-navigation/native'; import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Alert, Linking, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -22,7 +23,8 @@ const DEFAULT_AVATAR_URL = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud. export default function PersonalScreen() { const dispatch = useAppDispatch(); - const { confirmLogout, confirmDeleteAccount, isLoggedIn, pushIfAuthedElseLogin } = useAuthGuard(); + const { confirmLogout, confirmDeleteAccount, isLoggedIn, pushIfAuthedElseLogin, ensureLoggedIn } = useAuthGuard(); + const { openMembershipModal } = useMembershipModal(); const insets = useSafeAreaInsets(); const isLgAvaliable = isLiquidGlassAvailable() @@ -40,6 +42,14 @@ export default function PersonalScreen() { const clickTimestamps = useRef([]); const clickTimeoutRef = useRef(null); + const handleMembershipPress = useCallback(async () => { + const ok = await ensureLoggedIn(); + if (!ok) { + return; + } + openMembershipModal(); + }, [ensureLoggedIn, openMembershipModal]); + // 计算底部间距 const bottomPadding = useMemo(() => { return getTabBarBottomPadding(60) + (insets?.bottom ?? 0); @@ -241,6 +251,25 @@ export default function PersonalScreen() { ); + const MembershipBanner = () => ( + + { + void handleMembershipPress(); + }} + > + + + + ); + // 数据统计部分 const StatsSection = () => ( @@ -398,6 +427,7 @@ export default function PersonalScreen() { showsVerticalScrollIndicator={false} > + {userProfile.isVip ? null : } {/* - {children} - + + {children} + + ); } diff --git a/components/model/MembershipModal.tsx b/components/model/MembershipModal.tsx index 69b5f7d..5b25365 100644 --- a/components/model/MembershipModal.tsx +++ b/components/model/MembershipModal.tsx @@ -3,6 +3,7 @@ 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, @@ -17,10 +18,8 @@ import { ActivityIndicator, Alert, Dimensions, - Image, Linking, Modal, - Platform, ScrollView, StyleSheet, Text, @@ -39,70 +38,34 @@ interface MembershipModalProps { interface MembershipPlan { id: string; - title: string; + fallbackTitle: 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', + id: 'com.anonymous.digitalpilates.membership.lifetime', + fallbackTitle: '终身会员', + subtitle: '一次投入,终身健康陪伴', type: 'lifetime', recommended: true, }, { - id: 'com.ilookai.mind_gpt.ThreeMonths', - title: '季度会员', - subtitle: '每天1元\n连续包季', - price: '¥98', + id: 'com.anonymous.digitalpilates.membership.quarter', + fallbackTitle: '季度会员', + subtitle: '3个月蜕变计划,见证身材变化', type: 'quarterly', }, { - id: 'weekly_membership', - title: '周会员', - subtitle: '新人专享\n连续包周', - price: '¥18', + id: 'com.anonymous.digitalpilates.membership.weekly', + fallbackTitle: '周会员', + subtitle: '7天体验,开启健康第一步', 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(null); const [loading, setLoading] = useState(false); @@ -121,13 +84,18 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members 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 '到期后自动续费,可以随时随地取消'; + 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 ''; } @@ -144,88 +112,99 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members useEffect(() => { if (visible) { setupPurchaseListener(); - // 重置协议状态 setAgreementAccepted(false); + initPurchases(); } else { - // 弹窗关闭时移除监听器 removePurchaseListener(); + setProducts([]); + setSelectedProduct(null); + setAgreementAccepted(false); + setLoading(false); + setRestoring(false); } // 组件卸载时确保移除监听器 return () => { removePurchaseListener(); - console.log('MembershipModal 购买监听器已清理'); + log.info('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 初始化成功'); + capturePurchaseEvent('init', '开始获取会员产品套餐'); try { - // if (user?.id) { - // await Purchases.logIn(user.id); - // captureMessageWithContext('用户登录 Purchases 成功', { - // userId: user.id - // }); - // } + // 添加延迟,确保 RevenueCat SDK 完全初始化 + await new Promise(resolve => setTimeout(resolve, 500)); const offerings = await Purchases.getOfferings(); - console.log('offerings', offerings); + log.info('获取产品套餐', { offerings }); - captureMessageWithContext('获取产品套餐成功', { + logger.info('获取产品套餐成功', { currentOffering: offerings.current?.identifier || null, availablePackagesCount: offerings.current?.availablePackages.length || 0, - allOfferingsCount: Object.keys(offerings.all).length + 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); + const packages = offerings.current?.availablePackages ?? []; - // 获取产品后,检查用户的购买记录并自动选中对应套餐 - await checkAndSelectActivePlan(sortedProducts); - } else { - console.warn('No active offerings found or no packages available.'); - captureMessageWithContext('没有找到可用的产品套餐', { + if (packages.length === 0) { + log.warn('没有找到可用的产品套餐', { hasCurrentOffering: offerings.current !== null, - packagesLength: offerings.current?.availablePackages.length || 0 + 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) { - console.log('error', e); - // Error fetching customer info + // 安全地处理错误对象,避免循环引用 + const errorData = { + message: e?.message || '未知错误', + code: e?.code || null, + name: e?.name || 'Error', + // 只包含基本的错误信息,避免可能的循环引用 + }; + + log.error('获取产品套餐失败', { error: errorData }); captureException(e); - // captureMessageWithContext('初始化 Purchases 失败', { - // error: e.message || '未知错误', - // userId: user?.id || null - // }); + capturePurchaseEvent('error', '获取产品套餐失败', errorData); + + // 设置空状态,避免界面卡在加载状态 + setProducts([]); + setSelectedProduct(null); } - } - - console.log('visible', visible); - + }; // 添加购买状态监听器 const setupPurchaseListener = () => { - console.log('设置购买监听器,当前 visible 状态:', visible); + log.info('设置购买监听器', { visible }); // 如果已经有监听器,先移除 if (purchaseListenerRef.current) { @@ -234,8 +213,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members // 创建监听器函数 const listener = (customerInfo: CustomerInfo) => { - console.log('addCustomerInfoUpdateListener:', customerInfo); - console.log('addCustomerInfoUpdateListener 触发时 visible 状态:', visible); + log.info('购买状态变化监听器触发', { customerInfo, visible }); // 检查是否有有效的购买记录 const hasActiveEntitlements = Object.keys(customerInfo.entitlements.active).length > 0; @@ -253,7 +231,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members modalVisible: visible }); - console.log('检测到购买成功,刷新用户信息并关闭弹窗'); + log.info('检测到购买成功,准备刷新用户信息并关闭弹窗'); // 延迟一点时间,确保购买流程完全完成 setTimeout(async () => { @@ -280,16 +258,16 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members // 添加监听器 Purchases.addCustomerInfoUpdateListener(listener); - console.log('购买监听器已添加'); + log.info('购买监听器已添加'); }; // 移除购买状态监听器 const removePurchaseListener = () => { if (purchaseListenerRef.current) { - console.log('移除购买监听器'); + log.info('移除购买监听器'); Purchases.removeCustomerInfoUpdateListener(purchaseListenerRef.current); purchaseListenerRef.current = null; - console.log('购买监听器已移除'); + log.info('购买监听器已移除'); } }; @@ -302,7 +280,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members // 获取用户的购买信息 const customerInfo = await Purchases.getCustomerInfo(); - console.log('用户购买信息:', customerInfo); + log.info('获取用户购买信息', { customerInfo }); // 记录详细的购买状态日志 captureMessageWithContext('获取用户购买信息成功', { @@ -322,19 +300,19 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members Object.keys(customerInfo.entitlements.active).forEach(key => { const entitlement = customerInfo.entitlements.active[key]; activePurchasedProductIds.push(entitlement.productIdentifier); - console.log(`激活的权益: ${key}, 产品ID: ${entitlement.productIdentifier}`); + log.debug(`激活的权益: ${key}, 产品ID: ${entitlement.productIdentifier}`); }); // 检查非订阅购买(如终身会员) customerInfo.nonSubscriptionTransactions.forEach(transaction => { activePurchasedProductIds.push(transaction.productIdentifier); - console.log(`非订阅购买: ${transaction.productIdentifier}, 购买时间: ${transaction.purchaseDate}`); + log.debug(`非订阅购买: ${transaction.productIdentifier}, 购买时间: ${transaction.purchaseDate}`); }); // 检查订阅 Object.keys(customerInfo.activeSubscriptions).forEach(productId => { activePurchasedProductIds.push(productId); - console.log(`激活的订阅: ${productId}`); + log.debug(`激活的订阅: ${productId}`); }); // 去重 @@ -350,18 +328,14 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members let selectedProduct: PurchasesStoreProduct | null = null; // 优先级:终身会员 > 季度会员 > 周会员 - const priorityOrder = [ - 'com.ilookai.mind_gpt.Lifetime', - 'com.ilookai.mind_gpt.ThreeMonths', - 'weekly_membership' - ]; + 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) { - console.log(`找到优先级最高的激活产品: ${priorityProductId}`); + log.info(`找到优先级最高的激活产品: ${priorityProductId}`); break; } } @@ -372,7 +346,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members for (const productId of activePurchasedProductIds) { selectedProduct = availableProducts.find(product => product.identifier === productId) || null; if (selectedProduct) { - console.log(`找到匹配的激活产品: ${productId}`); + log.info(`找到匹配的激活产品: ${productId}`); break; } } @@ -393,7 +367,10 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members activePurchasedProductIds, availableProductIds: availableProducts.map(p => p.identifier) }); - console.log('用户有激活的购买记录,但没有找到匹配的可用产品'); + log.warn('用户有激活的购买记录,但没有找到匹配的可用产品', { + activePurchasedProductIds, + availableProductIds: availableProducts.map(p => p.identifier) + }); } } else { captureMessageWithContext('用户没有激活的购买记录', { @@ -401,16 +378,21 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members hasNonSubscriptions: customerInfo.nonSubscriptionTransactions.length > 0, hasActiveSubscriptions: Object.keys(customerInfo.activeSubscriptions).length > 0 }); - console.log('用户没有激活的购买记录,使用默认选择逻辑'); + log.info('用户没有激活的购买记录,使用默认选择逻辑'); + setSelectedProduct(availableProducts[0] ?? null); } } catch (error: any) { - console.log('检查用户购买记录失败:', error); + // 安全地处理错误对象,避免循环引用 + const errorData = { + message: error?.message || '未知错误', + code: error?.code || null, + name: error?.name || 'Error', + }; + + log.error('检查用户购买记录失败', { error: errorData }); captureException(error); - captureMessageWithContext('检查用户购买记录失败', { - error: error.message || '未知错误', - errorCode: error.code || null - }); + captureMessageWithContext('检查用户购买记录失败', errorData); } }; @@ -467,8 +449,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members // 执行购买 const { customerInfo, productIdentifier } = await Purchases.purchaseStoreProduct(selectedProduct); - console.log('购买成功 - customerInfo:', customerInfo); - console.log('购买成功 - productIdentifier:', productIdentifier); + log.info('购买成功', { customerInfo, productIdentifier }); // 记录购买成功事件 capturePurchaseEvent('success', `购买成功: ${productIdentifier}`, { @@ -480,7 +461,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members }); // 购买成功后,监听器会自动处理后续逻辑(刷新用户信息、关闭弹窗等) - console.log('购买流程完成,等待监听器处理后续逻辑'); + log.info('购买流程完成,等待监听器处理后续逻辑'); } catch (error: any) { captureException(error); @@ -527,7 +508,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members } finally { // 确保在所有情况下都重置加载状态 setLoading(false); - console.log('购买流程结束,加载状态已重置'); + log.info('购买流程结束,加载状态已重置'); } }; @@ -545,7 +526,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members // 恢复购买 const customerInfo = await Purchases.restorePurchases(); - console.log('恢复购买结果:', customerInfo); + log.info('恢复购买结果', { customerInfo }); captureMessageWithContext('恢复购买结果', { activeEntitlementsCount: Object.keys(customerInfo.entitlements.active).length, nonSubscriptionTransactionsCount: customerInfo.nonSubscriptionTransactions.length, @@ -579,7 +560,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members restoredProducts.push(productId); }); - console.log('恢复的产品:', restoredProducts); + log.info('恢复的产品', { restoredProducts }); capturePurchaseEvent('restore', '恢复购买成功', { restoredProducts, restoredProductsCount: restoredProducts.length @@ -599,7 +580,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members // } // }); - // console.log('后台恢复购买响应:', restoreResponse); + // log.debug('后台恢复购买响应', { restoreResponse }); // captureMessageWithContext('后台恢复购买成功', { // responseData: restoreResponse, @@ -620,14 +601,17 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members }); } catch (apiError: any) { - console.log('后台恢复购买接口调用失败:', apiError); - - captureException(apiError); - captureMessageWithContext('后台恢复购买接口失败', { - error: apiError.message || '未知错误', - errorCode: apiError.code || null, + // 安全地处理错误对象,避免循环引用 + 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 已经确认有购买记录) // 但不关闭弹窗,让用户知道可能需要重试 @@ -650,14 +634,18 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members }); } } catch (error: any) { - console.log('恢复购买失败:', error); + // 安全地处理错误对象,避免循环引用 + const errorData = { + message: error?.message || '未知错误', + code: error?.code || null, + name: error?.name || 'Error', + }; + + log.error('恢复购买失败', { error: errorData }); captureException(error); // 记录恢复购买失败事件 - capturePurchaseEvent('error', `恢复购买失败: ${error.message || '未知错误'}`, { - errorCode: error.code || null, - errorMessage: error.message || '未知错误' - }); + capturePurchaseEvent('error', `恢复购买失败: ${errorData.message}`, errorData); // 处理特定的恢复购买错误 if (error.code === 'RESTORE_CANCELLED' || error.code === 'USER_CANCELLED') { @@ -680,39 +668,39 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members } finally { // 确保在所有情况下都重置恢复状态 setRestoring(false); - console.log('恢复购买流程结束,恢复状态已重置'); + log.info('恢复购买流程结束,恢复状态已重置'); } }; const renderMembershipBenefits = () => ( - - - - 获得无限制的回复次数 + + 解锁全部健康功能 + 开启您的健康蜕变之旅 - 获得无限制的个人专属话题库 + 高级营养分析,精准卡路里计算 - 获得无广告优质体验 + 定制化减脂计划,科学体重管理 - 获得未来免费升级,开启更多功能 + 深度健康数据分析,洞察身体变化 + {/* 皇冠图标 */} - + /> */} ); @@ -725,6 +713,8 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members } const isSelected = selectedProduct === product; + const displayTitle = product.title || plan.fallbackTitle; + const priceLabel = product.priceString || ''; return ( - {product.identifier === 'com.ilookai.mind_gpt.Lifetime' && ( + {plan.recommended && ( 推荐 @@ -755,7 +745,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members plan.type === 'quarterly' && styles.quarterlyPlanTitle, plan.type === 'weekly' && styles.weeklyPlanTitle, ]}> - {product.title} + {displayTitle} @@ -766,7 +756,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members plan.type === 'quarterly' && styles.quarterlyPlanPrice, plan.type === 'weekly' && styles.weeklyPlanPrice, ]}> - {plan.price} + {priceLabel || '--'} @@ -807,6 +797,13 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members {renderMembershipBenefits()} {/* 会员套餐选择 */} + {products.length === 0 && ( + + + 暂未获取到会员商品,请在 RevenueCat 中配置 iOS 产品并同步到当前 Offering。 + + + )} {products.map(renderPlanCard)} @@ -857,14 +854,20 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members {loading ? ( @@ -872,7 +875,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members 正在处理购买... ) : ( - 购买 + 开启健康蜕变 )} @@ -971,6 +974,21 @@ const styles = StyleSheet.create({ marginBottom: 30, padding: 20, }, + benefitsTitleContainer: { + alignItems: 'center', + marginBottom: 20, + }, + benefitsTitle: { + fontSize: 18, + fontWeight: 'bold', + color: '#333', + marginBottom: 5, + }, + benefitsSubtitle: { + fontSize: 14, + color: '#666', + textAlign: 'center', + }, benefitsTitleBg: { width: 180, height: 20, @@ -1173,4 +1191,4 @@ const styles = StyleSheet.create({ lineHeight: 18, textAlign: 'center', }, -}); \ No newline at end of file +}); diff --git a/contexts/MembershipModalContext.tsx b/contexts/MembershipModalContext.tsx new file mode 100644 index 0000000..5f4d43d --- /dev/null +++ b/contexts/MembershipModalContext.tsx @@ -0,0 +1,88 @@ +import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import Purchases from 'react-native-purchases'; + +import { MembershipModal } from '@/components/model/MembershipModal'; +import { logger } from '@/utils/logger'; + +type MembershipModalOptions = { + onPurchaseSuccess?: () => void; +}; + +interface MembershipModalContextValue { + openMembershipModal: (options?: MembershipModalOptions) => void; + closeMembershipModal: () => void; +} + +const MembershipModalContext = createContext(null); + +export function MembershipModalProvider({ children }: { children: React.ReactNode }) { + const [visible, setVisible] = useState(false); + const [pendingSuccessCallback, setPendingSuccessCallback] = useState<(() => void) | undefined>(); + const [isInitialized, setIsInitialized] = useState(false); + + useEffect(() => { + // 直接使用生产环境的 API Key,避免环境变量问题 + const iosApiKey = 'appl_lmVvuLWFlXlrEsnvxMzTnKapqcc'; + + const initializeRevenueCat = async () => { + try { + // 检查是否已经配置过,避免重复配置 + if (!isInitialized) { + await Purchases.configure({ apiKey: iosApiKey }); + setIsInitialized(true); + console.log('[MembershipModalProvider] RevenueCat SDK 初始化成功'); + } + } catch (error) { + console.error('[MembershipModalProvider] RevenueCat SDK 初始化失败:', error); + // 初始化失败时不阻止应用正常运行 + } + }; + + initializeRevenueCat(); + }, [isInitialized]); + + const openMembershipModal = useCallback((options?: MembershipModalOptions) => { + setPendingSuccessCallback(() => options?.onPurchaseSuccess); + setVisible(true); + }, []); + + const closeMembershipModal = useCallback(() => { + setVisible(false); + setPendingSuccessCallback(undefined); + }, []); + + const handlePurchaseSuccess = useCallback(() => { + pendingSuccessCallback?.(); + }, [pendingSuccessCallback]); + + const contextValue = useMemo( + () => ({ + openMembershipModal, + closeMembershipModal, + }), + [closeMembershipModal, openMembershipModal], + ); + + return ( + + {children} + + + ); +} + +export function useMembershipModal(): MembershipModalContextValue { + const context = useContext(MembershipModalContext); + + if (!context) { + logger.error('useMembershipModal must be used within a MembershipModalProvider'); + // 抛出错误而不是返回 undefined,确保类型安全 + throw new Error('useMembershipModal must be used within a MembershipModalProvider'); + } + + return context; +} diff --git a/ios/OutLive.xcodeproj/project.pbxproj b/ios/OutLive.xcodeproj/project.pbxproj index 9a52c3e..f6ac040 100644 --- a/ios/OutLive.xcodeproj/project.pbxproj +++ b/ios/OutLive.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 32476CAEFFCE691C1634B0A4 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA3641BAC6078512F41509D /* ExpoModulesProvider.swift */; }; 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; + 792C52592EA880A7002F3F09 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 792C52582EA880A7002F3F09 /* StoreKit.framework */; }; 79B2CB702E7B954600B51753 /* OutLive-Bridging-Header.h in Sources */ = {isa = PBXBuildFile; fileRef = F11748442D0722820044C1D9 /* OutLive-Bridging-Header.h */; }; 79B2CB732E7B954F00B51753 /* HealthKitManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 79B2CB712E7B954F00B51753 /* HealthKitManager.m */; }; 79B2CB742E7B954F00B51753 /* HealthKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79B2CB722E7B954F00B51753 /* HealthKitManager.swift */; }; @@ -26,6 +27,7 @@ 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = OutLive/Info.plist; sourceTree = ""; }; 1EA3641BAC6078512F41509D /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-OutLive/ExpoModulesProvider.swift"; sourceTree = ""; }; 6F6136AA7113B3D210693D88 /* libPods-OutLive.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-OutLive.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 792C52582EA880A7002F3F09 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; 79B2CB712E7B954F00B51753 /* HealthKitManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = HealthKitManager.m; path = OutLive/HealthKitManager.m; sourceTree = ""; }; 79B2CB722E7B954F00B51753 /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HealthKitManager.swift; path = OutLive/HealthKitManager.swift; sourceTree = ""; }; 9B6A6CEBED2FC0931F7B7236 /* Pods-OutLive.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OutLive.release.xcconfig"; path = "Target Support Files/Pods-OutLive/Pods-OutLive.release.xcconfig"; sourceTree = ""; }; @@ -43,6 +45,7 @@ buildActionMask = 2147483647; files = ( AE00ECEC9D078460F642F131 /* libPods-OutLive.a in Frameworks */, + 792C52592EA880A7002F3F09 /* StoreKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -66,6 +69,7 @@ 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { isa = PBXGroup; children = ( + 792C52582EA880A7002F3F09 /* StoreKit.framework */, ED297162215061F000B7C4FE /* JavaScriptCore.framework */, 6F6136AA7113B3D210693D88 /* libPods-OutLive.a */, ); diff --git a/ios/OutLive/Info.plist b/ios/OutLive/Info.plist index 83f3444..e8edd7f 100644 --- a/ios/OutLive/Info.plist +++ b/ios/OutLive/Info.plist @@ -25,7 +25,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0.19 + 1.0.20 CFBundleSignature ???? CFBundleURLTypes diff --git a/utils/logger-test.ts b/utils/logger-test.ts new file mode 100644 index 0000000..ca3a2f1 --- /dev/null +++ b/utils/logger-test.ts @@ -0,0 +1,63 @@ +// 简单的日志记录器测试文件 +// 用于验证修复后的日志记录器是否能正确处理循环引用 + +import { log } from './logger'; + +// 测试循环引用对象 +const createCircularObject = () => { + const obj: any = { name: 'test' }; + obj.self = obj; // 创建循环引用 + return obj; +}; + +// 测试 Error 对象 +const createErrorObject = () => { + const error = new Error('Test error message'); + error.name = 'TestError'; + return error; +}; + +// 测试 RevenueCat 类型的错误对象 +const createRevenueCatError = () => { + return { + code: 'PRODUCT_NOT_FOUND', + message: 'There is an issue with your configuration. Check the underlying error for more details. There are no products registered in the RevenueCat dashboard for your offerings.', + underlyingErrorMessage: 'No products found', + name: 'RevenueCatError' + }; +}; + +// 运行测试 +export const testLogger = async () => { + console.log('开始测试日志记录器...'); + + try { + // 测试1: 循环引用对象 + console.log('测试1: 循环引用对象'); + const circularObj = createCircularObject(); + await log.info('测试循环引用对象', { data: circularObj }); + + // 测试2: Error 对象 + console.log('测试2: Error 对象'); + const errorObj = createErrorObject(); + await log.error('测试 Error 对象', { error: errorObj }); + + // 测试3: RevenueCat 错误对象 + console.log('测试3: RevenueCat 错误对象'); + const revenueCatError = createRevenueCatError(); + await log.error('测试 RevenueCat 错误', { error: revenueCatError }); + + // 测试4: 直接传递 Error 对象 + console.log('测试4: 直接传递 Error 对象'); + await log.error('测试直接传递 Error', createErrorObject()); + + console.log('所有测试完成!'); + } catch (error) { + console.error('测试过程中出现错误:', error); + } +}; + +// 如果直接运行此文件,执行测试 +if (require.main === module) { + testLogger(); +} \ No newline at end of file diff --git a/utils/logger.ts b/utils/logger.ts index 10660ee..cac9dc2 100644 --- a/utils/logger.ts +++ b/utils/logger.ts @@ -41,26 +41,66 @@ class Logger { } private async addLog(level: LogEntry['level'], message: string, data?: any): Promise { + // 安全地处理数据,避免循环引用 + let safeData = data; + if (data && typeof data === 'object') { + try { + // 对于非 ERROR 级别的日志,也进行安全序列化 + if (data instanceof Error) { + safeData = { + name: data.name, + message: data.message, + stack: data.stack + }; + } else { + // 使用 JSON.stringify 的 replacer 函数处理循环引用 + safeData = JSON.parse(JSON.stringify(data, (key, value) => { + if (typeof value === 'object' && value !== null) { + if (value.constructor === Object || Array.isArray(value)) { + return value; + } + // 对于其他对象类型,转换为字符串表示 + return value.toString ? value.toString() : '[Object]'; + } + return value; + })); + } + } catch (serializeError) { + // 如果序列化失败,只保存基本信息 + safeData = { + error: 'Failed to serialize data', + type: typeof data, + toString: data.toString ? data.toString() : 'N/A' + }; + } + } + const logEntry: LogEntry = { id: Date.now().toString() + Math.random().toString(36).substr(2, 9), timestamp: Date.now(), level, message, - data + data: safeData }; - // 同时在控制台输出 - const logMethod = level === 'ERROR' ? console.error : - level === 'WARN' ? console.warn : - level === 'INFO' ? console.info : console.log; - - logMethod(`[${level}] ${message}`, data || ''); + // 同时在控制台输出 - 使用原生 console 方法避免循环调用 + try { + const logMethod = level === 'ERROR' ? console.error : + level === 'WARN' ? console.warn : + level === 'INFO' ? console.info : console.log; + + logMethod(`[${level}] ${message}`, safeData); + } catch (consoleError) { + // 如果控制台输出失败,使用最基本的 console.log + console.log(`[${level}] ${message}`, typeof safeData === 'string' ? safeData : 'Object data'); + } try { const logs = await this.getLogs(); logs.push(logEntry); await this.saveLogs(logs); } catch (error) { + // 使用原生 console.error 避免循环调用 console.error('Failed to add log:', error); } } @@ -78,6 +118,7 @@ class Logger { } async error(message: string, data?: any): Promise { + // addLog 方法已经包含了安全的数据处理逻辑 await this.addLog('ERROR', message, data); }