feat(membership): 实现会员系统和购买流程

- 创建 MembershipModalContext 统一管理会员弹窗
- 优化 MembershipModal 产品套餐展示和购买流程
- 集成 RevenueCat SDK 并初始化内购功能
- 在个人中心添加会员 Banner,引导非会员用户订阅
- 修复日志工具的循环引用问题,确保错误信息正确记录
- 版本更新至 1.0.20

新增了完整的会员购买流程,包括套餐选择、购买确认、购买恢复等功能。会员 Banner 仅对非会员用户展示,已是会员的用户不会看到。同时优化了错误日志记录,避免循环引用导致的序列化失败。
This commit is contained in:
richarjiang
2025-10-24 09:16:04 +08:00
parent b75a8991ac
commit 2e11f694f8
9 changed files with 437 additions and 186 deletions

View File

@@ -2,7 +2,7 @@
"expo": { "expo": {
"name": "Out Live", "name": "Out Live",
"slug": "digital-pilates", "slug": "digital-pilates",
"version": "1.0.19", "version": "1.0.20",
"orientation": "portrait", "orientation": "portrait",
"scheme": "digitalpilates", "scheme": "digitalpilates",
"userInterfaceStyle": "light", "userInterfaceStyle": "light",

View File

@@ -2,6 +2,7 @@ import ActivityHeatMap from '@/components/ActivityHeatMap';
import { PRIVACY_POLICY_URL, USER_AGREEMENT_URL } from '@/constants/Agree'; import { PRIVACY_POLICY_URL, USER_AGREEMENT_URL } from '@/constants/Agree';
import { ROUTES } from '@/constants/Routes'; import { ROUTES } from '@/constants/Routes';
import { getTabBarBottomPadding } from '@/constants/TabBar'; import { getTabBarBottomPadding } from '@/constants/TabBar';
import { useMembershipModal } from '@/contexts/MembershipModalContext';
import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useAuthGuard } from '@/hooks/useAuthGuard';
import { useNotifications } from '@/hooks/useNotifications'; import { useNotifications } from '@/hooks/useNotifications';
@@ -14,7 +15,7 @@ import { useFocusEffect } from '@react-navigation/native';
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient'; 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 { Alert, Linking, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; 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() { export default function PersonalScreen() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { confirmLogout, confirmDeleteAccount, isLoggedIn, pushIfAuthedElseLogin } = useAuthGuard(); const { confirmLogout, confirmDeleteAccount, isLoggedIn, pushIfAuthedElseLogin, ensureLoggedIn } = useAuthGuard();
const { openMembershipModal } = useMembershipModal();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const isLgAvaliable = isLiquidGlassAvailable() const isLgAvaliable = isLiquidGlassAvailable()
@@ -40,6 +42,14 @@ export default function PersonalScreen() {
const clickTimestamps = useRef<number[]>([]); const clickTimestamps = useRef<number[]>([]);
const clickTimeoutRef = useRef<number | null>(null); const clickTimeoutRef = useRef<number | null>(null);
const handleMembershipPress = useCallback(async () => {
const ok = await ensureLoggedIn();
if (!ok) {
return;
}
openMembershipModal();
}, [ensureLoggedIn, openMembershipModal]);
// 计算底部间距 // 计算底部间距
const bottomPadding = useMemo(() => { const bottomPadding = useMemo(() => {
return getTabBarBottomPadding(60) + (insets?.bottom ?? 0); return getTabBarBottomPadding(60) + (insets?.bottom ?? 0);
@@ -241,6 +251,25 @@ export default function PersonalScreen() {
</View> </View>
); );
const MembershipBanner = () => (
<View style={styles.sectionContainer}>
<TouchableOpacity
activeOpacity={0.9}
onPress={() => {
void handleMembershipPress();
}}
>
<Image
source={{ uri: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/banner/vip2.png' }}
style={styles.membershipBannerImage}
contentFit="cover"
transition={200}
cachePolicy="memory-disk"
/>
</TouchableOpacity>
</View>
);
// 数据统计部分 // 数据统计部分
const StatsSection = () => ( const StatsSection = () => (
<View style={styles.sectionContainer}> <View style={styles.sectionContainer}>
@@ -398,6 +427,7 @@ export default function PersonalScreen() {
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
> >
<UserHeader /> <UserHeader />
{userProfile.isVip ? null : <MembershipBanner />}
<StatsSection /> <StatsSection />
<View style={styles.fishRecordContainer}> <View style={styles.fishRecordContainer}>
{/* <Image {/* <Image
@@ -474,6 +504,11 @@ const styles = StyleSheet.create({
elevation: 2, elevation: 2,
overflow: 'hidden', overflow: 'hidden',
}, },
membershipBannerImage: {
width: '100%',
height: 180,
borderRadius: 16,
},
// 用户信息区域 // 用户信息区域
userInfoContainer: { userInfoContainer: {
flexDirection: 'row', flexDirection: 'row',
@@ -609,4 +644,3 @@ const styles = StyleSheet.create({
marginLeft: 4, marginLeft: 4,
}, },
}); });

View File

@@ -32,6 +32,7 @@ import { BackgroundTaskManager } from '@/services/backgroundTaskManager';
import { fetchChallenges } from '@/store/challengesSlice'; import { fetchChallenges } from '@/store/challengesSlice';
import AsyncStorage from '@/utils/kvStore'; import AsyncStorage from '@/utils/kvStore';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { MembershipModalProvider } from '@/contexts/MembershipModalContext';
function Bootstrapper({ children }: { children: React.ReactNode }) { function Bootstrapper({ children }: { children: React.ReactNode }) {
@@ -207,12 +208,14 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
return ( return (
<DialogProvider> <DialogProvider>
<MembershipModalProvider>
{children} {children}
<PrivacyConsentModal <PrivacyConsentModal
visible={showPrivacyModal} visible={showPrivacyModal}
onAgree={handlePrivacyAgree} onAgree={handlePrivacyAgree}
onDisagree={handlePrivacyDisagree} onDisagree={handlePrivacyDisagree}
/> />
</MembershipModalProvider>
</DialogProvider> </DialogProvider>
); );
} }

View File

@@ -3,6 +3,7 @@ import CustomCheckBox from '@/components/ui/CheckBox';
import { USER_AGREEMENT_URL } from '@/constants/Agree'; import { USER_AGREEMENT_URL } from '@/constants/Agree';
// import { useAuth } from '@/contexts/AuthContext'; // import { useAuth } from '@/contexts/AuthContext';
// import { UserApi } from '@/services'; // import { UserApi } from '@/services';
import { log, logger } from '@/utils/logger';
import { import {
captureMessage, captureMessage,
captureMessageWithContext, captureMessageWithContext,
@@ -17,10 +18,8 @@ import {
ActivityIndicator, ActivityIndicator,
Alert, Alert,
Dimensions, Dimensions,
Image,
Linking, Linking,
Modal, Modal,
Platform,
ScrollView, ScrollView,
StyleSheet, StyleSheet,
Text, Text,
@@ -39,70 +38,34 @@ interface MembershipModalProps {
interface MembershipPlan { interface MembershipPlan {
id: string; id: string;
title: string; fallbackTitle: string;
subtitle: string; subtitle: string;
price: string;
originalPrice?: string;
discount?: string;
type: 'weekly' | 'quarterly' | 'lifetime'; type: 'weekly' | 'quarterly' | 'lifetime';
recommended?: boolean; recommended?: boolean;
} }
const DEFAULT_PLANS: MembershipPlan[] = [ const DEFAULT_PLANS: MembershipPlan[] = [
{ {
id: 'com.ilookai.mind_gpt.Lifetime', id: 'com.anonymous.digitalpilates.membership.lifetime',
title: '终身会员', fallbackTitle: '终身会员',
subtitle: '每天0.01元\n永久有效', subtitle: '一次投入,终身健康陪伴',
price: '¥128',
type: 'lifetime', type: 'lifetime',
recommended: true, recommended: true,
}, },
{ {
id: 'com.ilookai.mind_gpt.ThreeMonths', id: 'com.anonymous.digitalpilates.membership.quarter',
title: '季度会员', fallbackTitle: '季度会员',
subtitle: '每天1元\n连续包季', subtitle: '3个月蜕变计划见证身材变化',
price: '¥98',
type: 'quarterly', type: 'quarterly',
}, },
{ {
id: 'weekly_membership', id: 'com.anonymous.digitalpilates.membership.weekly',
title: '周会员', fallbackTitle: '周会员',
subtitle: '新人专享\n连续包周', subtitle: '7天体验开启健康第一步',
price: '¥18',
type: 'weekly', 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) { export function MembershipModal({ visible, onClose, onPurchaseSuccess }: MembershipModalProps) {
const [selectedProduct, setSelectedProduct] = useState<PurchasesStoreProduct | null>(null); const [selectedProduct, setSelectedProduct] = useState<PurchasesStoreProduct | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -121,13 +84,18 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
const getTipsContent = (product: PurchasesStoreProduct | null): string => { const getTipsContent = (product: PurchasesStoreProduct | null): string => {
if (!product) return ''; if (!product) return '';
// 这里您可以根据不同的产品返回不同的提示内容 const plan = DEFAULT_PLANS.find(item => item.id === product.identifier);
switch (product.identifier) { if (!plan) {
case 'com.ilookai.mind_gpt.Lifetime': return '';
return '一次购买,永久享受所有功能'; }
case 'com.ilookai.mind_gpt.ThreeMonths':
case 'weekly_membership': switch (plan.type) {
return '到期后自动续费,可以随时随地取消'; case 'lifetime':
return '终身陪伴,见证您的每一次健康蜕变';
case 'quarterly':
return '3个月科学计划让健康成为生活习惯';
case 'weekly':
return '7天体验期感受专业健康指导的力量';
default: default:
return ''; return '';
} }
@@ -144,88 +112,99 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
setupPurchaseListener(); setupPurchaseListener();
// 重置协议状态
setAgreementAccepted(false); setAgreementAccepted(false);
initPurchases();
} else { } else {
// 弹窗关闭时移除监听器
removePurchaseListener(); removePurchaseListener();
setProducts([]);
setSelectedProduct(null);
setAgreementAccepted(false);
setLoading(false);
setRestoring(false);
} }
// 组件卸载时确保移除监听器 // 组件卸载时确保移除监听器
return () => { return () => {
removePurchaseListener(); removePurchaseListener();
console.log('MembershipModal 购买监听器已清理'); log.info('MembershipModal 购买监听器已清理');
}; };
}, [visible]); // 依赖 visible 状态 }, [visible]); // 依赖 visible 状态
const initPurchases = async () => { const initPurchases = async () => {
if (Platform.OS === 'ios') { capturePurchaseEvent('init', '开始获取会员产品套餐');
Purchases.configure({
apiKey: 'appl_UXFtPsBsFIsBOxoNGXoPwpXhGYk'
});
} else if (Platform.OS === 'android') {
// Purchases.configure({
// apiKey: 'goog_ZqYxWbQvRgLzBnVfYdXcWbVn'
// });
}
capturePurchaseEvent('init', 'Purchases 初始化成功');
try { try {
// if (user?.id) { // 添加延迟,确保 RevenueCat SDK 完全初始化
// await Purchases.logIn(user.id); await new Promise(resolve => setTimeout(resolve, 500));
// captureMessageWithContext('用户登录 Purchases 成功', {
// userId: user.id
// });
// }
const offerings = await Purchases.getOfferings(); const offerings = await Purchases.getOfferings();
console.log('offerings', offerings); log.info('获取产品套餐', { offerings });
captureMessageWithContext('获取产品套餐成功', { logger.info('获取产品套餐成功', {
currentOffering: offerings.current?.identifier || null, currentOffering: offerings.current?.identifier || null,
availablePackagesCount: offerings.current?.availablePackages.length || 0, 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 ?? [];
// 当前活跃的套餐存在
const packages = offerings.current.availablePackages; if (packages.length === 0) {
console.log('Available packages:', packages); log.warn('没有找到可用的产品套餐', {
// packages 是一个数组,包含 PurchasePackage 对象,每个对象都有 product 信息 hasCurrentOffering: offerings.current !== null,
// 可以根据这些信息来渲染你的 UI例如价格、标题等 packagesLength: offerings.current?.availablePackages.length || 0,
const sortedProducts = packages.sort((a, b) => b.product.price - a.product.price).map(pkg => pkg.product); });
setProducts(sortedProducts); 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(sortedProducts); await checkAndSelectActivePlan(productsToUse);
} else {
console.warn('No active offerings found or no packages available.');
captureMessageWithContext('没有找到可用的产品套餐', {
hasCurrentOffering: offerings.current !== null,
packagesLength: offerings.current?.availablePackages.length || 0
});
}
setSelectedProduct(current => current ?? (productsToUse[0] ?? null));
} catch (e: any) { } 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); captureException(e);
// captureMessageWithContext('初始化 Purchases 失败', { capturePurchaseEvent('error', '获取产品套餐失败', errorData);
// error: e.message || '未知错误',
// userId: user?.id || null
// });
}
}
console.log('visible', visible);
// 设置空状态,避免界面卡在加载状态
setProducts([]);
setSelectedProduct(null);
}
};
// 添加购买状态监听器 // 添加购买状态监听器
const setupPurchaseListener = () => { const setupPurchaseListener = () => {
console.log('设置购买监听器,当前 visible 状态:', visible); log.info('设置购买监听器', { visible });
// 如果已经有监听器,先移除 // 如果已经有监听器,先移除
if (purchaseListenerRef.current) { if (purchaseListenerRef.current) {
@@ -234,8 +213,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
// 创建监听器函数 // 创建监听器函数
const listener = (customerInfo: CustomerInfo) => { const listener = (customerInfo: CustomerInfo) => {
console.log('addCustomerInfoUpdateListener:', customerInfo); log.info('购买状态变化监听器触发', { customerInfo, visible });
console.log('addCustomerInfoUpdateListener 触发时 visible 状态:', visible);
// 检查是否有有效的购买记录 // 检查是否有有效的购买记录
const hasActiveEntitlements = Object.keys(customerInfo.entitlements.active).length > 0; const hasActiveEntitlements = Object.keys(customerInfo.entitlements.active).length > 0;
@@ -253,7 +231,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
modalVisible: visible modalVisible: visible
}); });
console.log('检测到购买成功,刷新用户信息并关闭弹窗'); log.info('检测到购买成功,准备刷新用户信息并关闭弹窗');
// 延迟一点时间,确保购买流程完全完成 // 延迟一点时间,确保购买流程完全完成
setTimeout(async () => { setTimeout(async () => {
@@ -280,16 +258,16 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
// 添加监听器 // 添加监听器
Purchases.addCustomerInfoUpdateListener(listener); Purchases.addCustomerInfoUpdateListener(listener);
console.log('购买监听器已添加'); log.info('购买监听器已添加');
}; };
// 移除购买状态监听器 // 移除购买状态监听器
const removePurchaseListener = () => { const removePurchaseListener = () => {
if (purchaseListenerRef.current) { if (purchaseListenerRef.current) {
console.log('移除购买监听器'); log.info('移除购买监听器');
Purchases.removeCustomerInfoUpdateListener(purchaseListenerRef.current); Purchases.removeCustomerInfoUpdateListener(purchaseListenerRef.current);
purchaseListenerRef.current = null; purchaseListenerRef.current = null;
console.log('购买监听器已移除'); log.info('购买监听器已移除');
} }
}; };
@@ -302,7 +280,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
// 获取用户的购买信息 // 获取用户的购买信息
const customerInfo = await Purchases.getCustomerInfo(); const customerInfo = await Purchases.getCustomerInfo();
console.log('用户购买信息:', customerInfo); log.info('获取用户购买信息', { customerInfo });
// 记录详细的购买状态日志 // 记录详细的购买状态日志
captureMessageWithContext('获取用户购买信息成功', { captureMessageWithContext('获取用户购买信息成功', {
@@ -322,19 +300,19 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
Object.keys(customerInfo.entitlements.active).forEach(key => { Object.keys(customerInfo.entitlements.active).forEach(key => {
const entitlement = customerInfo.entitlements.active[key]; const entitlement = customerInfo.entitlements.active[key];
activePurchasedProductIds.push(entitlement.productIdentifier); activePurchasedProductIds.push(entitlement.productIdentifier);
console.log(`激活的权益: ${key}, 产品ID: ${entitlement.productIdentifier}`); log.debug(`激活的权益: ${key}, 产品ID: ${entitlement.productIdentifier}`);
}); });
// 检查非订阅购买(如终身会员) // 检查非订阅购买(如终身会员)
customerInfo.nonSubscriptionTransactions.forEach(transaction => { customerInfo.nonSubscriptionTransactions.forEach(transaction => {
activePurchasedProductIds.push(transaction.productIdentifier); activePurchasedProductIds.push(transaction.productIdentifier);
console.log(`非订阅购买: ${transaction.productIdentifier}, 购买时间: ${transaction.purchaseDate}`); log.debug(`非订阅购买: ${transaction.productIdentifier}, 购买时间: ${transaction.purchaseDate}`);
}); });
// 检查订阅 // 检查订阅
Object.keys(customerInfo.activeSubscriptions).forEach(productId => { Object.keys(customerInfo.activeSubscriptions).forEach(productId => {
activePurchasedProductIds.push(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; let selectedProduct: PurchasesStoreProduct | null = null;
// 优先级:终身会员 > 季度会员 > 周会员 // 优先级:终身会员 > 季度会员 > 周会员
const priorityOrder = [ const priorityOrder = DEFAULT_PLANS.map(plan => plan.id);
'com.ilookai.mind_gpt.Lifetime',
'com.ilookai.mind_gpt.ThreeMonths',
'weekly_membership'
];
// 按照优先级查找 // 按照优先级查找
for (const priorityProductId of priorityOrder) { for (const priorityProductId of priorityOrder) {
if (activePurchasedProductIds.includes(priorityProductId)) { if (activePurchasedProductIds.includes(priorityProductId)) {
selectedProduct = availableProducts.find(product => product.identifier === priorityProductId) || null; selectedProduct = availableProducts.find(product => product.identifier === priorityProductId) || null;
if (selectedProduct) { if (selectedProduct) {
console.log(`找到优先级最高的激活产品: ${priorityProductId}`); log.info(`找到优先级最高的激活产品: ${priorityProductId}`);
break; break;
} }
} }
@@ -372,7 +346,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
for (const productId of activePurchasedProductIds) { for (const productId of activePurchasedProductIds) {
selectedProduct = availableProducts.find(product => product.identifier === productId) || null; selectedProduct = availableProducts.find(product => product.identifier === productId) || null;
if (selectedProduct) { if (selectedProduct) {
console.log(`找到匹配的激活产品: ${productId}`); log.info(`找到匹配的激活产品: ${productId}`);
break; break;
} }
} }
@@ -393,7 +367,10 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
activePurchasedProductIds, activePurchasedProductIds,
availableProductIds: availableProducts.map(p => p.identifier) availableProductIds: availableProducts.map(p => p.identifier)
}); });
console.log('用户有激活的购买记录,但没有找到匹配的可用产品'); log.warn('用户有激活的购买记录,但没有找到匹配的可用产品', {
activePurchasedProductIds,
availableProductIds: availableProducts.map(p => p.identifier)
});
} }
} else { } else {
captureMessageWithContext('用户没有激活的购买记录', { captureMessageWithContext('用户没有激活的购买记录', {
@@ -401,16 +378,21 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
hasNonSubscriptions: customerInfo.nonSubscriptionTransactions.length > 0, hasNonSubscriptions: customerInfo.nonSubscriptionTransactions.length > 0,
hasActiveSubscriptions: Object.keys(customerInfo.activeSubscriptions).length > 0 hasActiveSubscriptions: Object.keys(customerInfo.activeSubscriptions).length > 0
}); });
console.log('用户没有激活的购买记录,使用默认选择逻辑'); log.info('用户没有激活的购买记录,使用默认选择逻辑');
setSelectedProduct(availableProducts[0] ?? null);
} }
} catch (error: any) { } 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); captureException(error);
captureMessageWithContext('检查用户购买记录失败', { captureMessageWithContext('检查用户购买记录失败', errorData);
error: error.message || '未知错误',
errorCode: error.code || null
});
} }
}; };
@@ -467,8 +449,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
// 执行购买 // 执行购买
const { customerInfo, productIdentifier } = await Purchases.purchaseStoreProduct(selectedProduct); const { customerInfo, productIdentifier } = await Purchases.purchaseStoreProduct(selectedProduct);
console.log('购买成功 - customerInfo:', customerInfo); log.info('购买成功', { customerInfo, productIdentifier });
console.log('购买成功 - productIdentifier:', productIdentifier);
// 记录购买成功事件 // 记录购买成功事件
capturePurchaseEvent('success', `购买成功: ${productIdentifier}`, { capturePurchaseEvent('success', `购买成功: ${productIdentifier}`, {
@@ -480,7 +461,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
}); });
// 购买成功后,监听器会自动处理后续逻辑(刷新用户信息、关闭弹窗等) // 购买成功后,监听器会自动处理后续逻辑(刷新用户信息、关闭弹窗等)
console.log('购买流程完成,等待监听器处理后续逻辑'); log.info('购买流程完成,等待监听器处理后续逻辑');
} catch (error: any) { } catch (error: any) {
captureException(error); captureException(error);
@@ -527,7 +508,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
} finally { } finally {
// 确保在所有情况下都重置加载状态 // 确保在所有情况下都重置加载状态
setLoading(false); setLoading(false);
console.log('购买流程结束,加载状态已重置'); log.info('购买流程结束,加载状态已重置');
} }
}; };
@@ -545,7 +526,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
// 恢复购买 // 恢复购买
const customerInfo = await Purchases.restorePurchases(); const customerInfo = await Purchases.restorePurchases();
console.log('恢复购买结果:', customerInfo); log.info('恢复购买结果', { customerInfo });
captureMessageWithContext('恢复购买结果', { captureMessageWithContext('恢复购买结果', {
activeEntitlementsCount: Object.keys(customerInfo.entitlements.active).length, activeEntitlementsCount: Object.keys(customerInfo.entitlements.active).length,
nonSubscriptionTransactionsCount: customerInfo.nonSubscriptionTransactions.length, nonSubscriptionTransactionsCount: customerInfo.nonSubscriptionTransactions.length,
@@ -579,7 +560,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
restoredProducts.push(productId); restoredProducts.push(productId);
}); });
console.log('恢复的产品:', restoredProducts); log.info('恢复的产品', { restoredProducts });
capturePurchaseEvent('restore', '恢复购买成功', { capturePurchaseEvent('restore', '恢复购买成功', {
restoredProducts, restoredProducts,
restoredProductsCount: restoredProducts.length restoredProductsCount: restoredProducts.length
@@ -599,7 +580,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
// } // }
// }); // });
// console.log('后台恢复购买响应:', restoreResponse); // log.debug('后台恢复购买响应', { restoreResponse });
// captureMessageWithContext('后台恢复购买成功', { // captureMessageWithContext('后台恢复购买成功', {
// responseData: restoreResponse, // responseData: restoreResponse,
@@ -620,14 +601,17 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
}); });
} catch (apiError: any) { } catch (apiError: any) {
console.log('后台恢复购买接口调用失败:', apiError); // 安全地处理错误对象,避免循环引用
const errorData = {
captureException(apiError); message: apiError?.message || '未知错误',
captureMessageWithContext('后台恢复购买接口失败', { code: apiError?.code || null,
error: apiError.message || '未知错误', name: apiError?.name || 'Error',
errorCode: apiError.code || null,
restoredProductsCount: restoredProducts.length restoredProductsCount: restoredProducts.length
}); };
log.error('后台恢复购买接口调用失败', { error: errorData });
captureException(apiError);
captureMessageWithContext('后台恢复购买接口失败', errorData);
// 即使后台接口失败,也显示恢复成功(因为 RevenueCat 已经确认有购买记录) // 即使后台接口失败,也显示恢复成功(因为 RevenueCat 已经确认有购买记录)
// 但不关闭弹窗,让用户知道可能需要重试 // 但不关闭弹窗,让用户知道可能需要重试
@@ -650,14 +634,18 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
}); });
} }
} catch (error: any) { } 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); captureException(error);
// 记录恢复购买失败事件 // 记录恢复购买失败事件
capturePurchaseEvent('error', `恢复购买失败: ${error.message || '未知错误'}`, { capturePurchaseEvent('error', `恢复购买失败: ${errorData.message}`, errorData);
errorCode: error.code || null,
errorMessage: error.message || '未知错误'
});
// 处理特定的恢复购买错误 // 处理特定的恢复购买错误
if (error.code === 'RESTORE_CANCELLED' || error.code === 'USER_CANCELLED') { if (error.code === 'RESTORE_CANCELLED' || error.code === 'USER_CANCELLED') {
@@ -680,39 +668,39 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
} finally { } finally {
// 确保在所有情况下都重置恢复状态 // 确保在所有情况下都重置恢复状态
setRestoring(false); setRestoring(false);
console.log('恢复购买流程结束,恢复状态已重置'); log.info('恢复购买流程结束,恢复状态已重置');
} }
}; };
const renderMembershipBenefits = () => ( const renderMembershipBenefits = () => (
<View style={styles.benefitsContainer}> <View style={styles.benefitsContainer}>
<Image source={require('@/assets/images/img_vip_unlock_title.png')} style={styles.benefitsTitleBg} /> <View style={styles.benefitsTitleContainer}>
<View style={styles.benefitItem}> <Text style={styles.benefitsTitle}></Text>
<MaterialIcons name="check-circle" size={20} color="#DF42D0" /> <Text style={styles.benefitsSubtitle}></Text>
<Text style={styles.benefitText}></Text>
</View> </View>
<View style={styles.benefitItem}> <View style={styles.benefitItem}>
<MaterialIcons name="check-circle" size={20} color="#DF42D0" /> <MaterialIcons name="check-circle" size={20} color="#DF42D0" />
<Text style={styles.benefitText}></Text> <Text style={styles.benefitText}></Text>
</View> </View>
<View style={styles.benefitItem}> <View style={styles.benefitItem}>
<MaterialIcons name="check-circle" size={20} color="#DF42D0" /> <MaterialIcons name="check-circle" size={20} color="#DF42D0" />
<Text style={styles.benefitText}>广</Text> <Text style={styles.benefitText}></Text>
</View> </View>
<View style={styles.benefitItem}> <View style={styles.benefitItem}>
<MaterialIcons name="check-circle" size={20} color="#DF42D0" /> <MaterialIcons name="check-circle" size={20} color="#DF42D0" />
<Text style={styles.benefitText}></Text> <Text style={styles.benefitText}></Text>
</View> </View>
{/* 皇冠图标 */} {/* 皇冠图标 */}
<View style={styles.crownContainer}> <View style={styles.crownContainer}>
<Image {/* <Image
source={require('@/assets/images/img_profile_vip_bg.png')} source={require('@/assets/images/img_profile_vip_bg.png')}
style={styles.crownIcon} style={styles.crownIcon}
/> /> */}
</View> </View>
</View> </View>
); );
@@ -725,6 +713,8 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
} }
const isSelected = selectedProduct === product; const isSelected = selectedProduct === product;
const displayTitle = product.title || plan.fallbackTitle;
const priceLabel = product.priceString || '';
return ( return (
<TouchableOpacity <TouchableOpacity
@@ -738,11 +728,11 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
disabled={loading} disabled={loading}
activeOpacity={loading ? 1 : 0.8} activeOpacity={loading ? 1 : 0.8}
accessible={true} accessible={true}
accessibilityLabel={`${product.title} ${plan.price}`} accessibilityLabel={`${displayTitle} ${priceLabel}`}
accessibilityHint={loading ? "购买进行中,无法切换套餐" : `选择${product.title}套餐`} accessibilityHint={loading ? '购买进行中,无法切换套餐' : `选择${displayTitle}套餐`}
accessibilityState={{ disabled: loading, selected: isSelected }} accessibilityState={{ disabled: loading, selected: isSelected }}
> >
{product.identifier === 'com.ilookai.mind_gpt.Lifetime' && ( {plan.recommended && (
<View style={styles.recommendedBadge}> <View style={styles.recommendedBadge}>
<Text style={styles.recommendedText}></Text> <Text style={styles.recommendedText}></Text>
</View> </View>
@@ -755,7 +745,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
plan.type === 'quarterly' && styles.quarterlyPlanTitle, plan.type === 'quarterly' && styles.quarterlyPlanTitle,
plan.type === 'weekly' && styles.weeklyPlanTitle, plan.type === 'weekly' && styles.weeklyPlanTitle,
]}> ]}>
{product.title} {displayTitle}
</Text> </Text>
</View> </View>
@@ -766,7 +756,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
plan.type === 'quarterly' && styles.quarterlyPlanPrice, plan.type === 'quarterly' && styles.quarterlyPlanPrice,
plan.type === 'weekly' && styles.weeklyPlanPrice, plan.type === 'weekly' && styles.weeklyPlanPrice,
]}> ]}>
{plan.price} {priceLabel || '--'}
</Text> </Text>
</View> </View>
@@ -807,6 +797,13 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
{renderMembershipBenefits()} {renderMembershipBenefits()}
{/* 会员套餐选择 */} {/* 会员套餐选择 */}
{products.length === 0 && (
<View style={styles.configurationNotice}>
<Text style={styles.configurationText}>
RevenueCat iOS Offering
</Text>
</View>
)}
<View style={styles.plansContainer}> <View style={styles.plansContainer}>
{products.map(renderPlanCard)} {products.map(renderPlanCard)}
</View> </View>
@@ -857,14 +854,20 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
<TouchableOpacity <TouchableOpacity
style={[ style={[
styles.purchaseButton, styles.purchaseButton,
loading && styles.disabledButton (loading || !selectedProduct) && styles.disabledButton
]} ]}
onPress={handlePurchase} onPress={handlePurchase}
disabled={loading} disabled={loading || !selectedProduct}
accessible={true} accessible={true}
accessibilityLabel={loading ? "正在处理购买" : "购买会员"} accessibilityLabel={loading ? '正在处理购买' : '购买会员'}
accessibilityHint={loading ? "购买正在进行中,请稍候" : "点击购买选中的会员套餐"} accessibilityHint={
accessibilityState={{ disabled: loading }} loading
? '购买正在进行中,请稍候'
: selectedProduct
? `点击购买${selectedProduct.title || '已选'}会员套餐`
: '请选择会员套餐后再进行购买'
}
accessibilityState={{ disabled: loading || !selectedProduct }}
> >
{loading ? ( {loading ? (
<View style={styles.loadingContainer}> <View style={styles.loadingContainer}>
@@ -872,7 +875,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
<Text style={styles.purchaseButtonText}>...</Text> <Text style={styles.purchaseButtonText}>...</Text>
</View> </View>
) : ( ) : (
<Text style={styles.purchaseButtonText}></Text> <Text style={styles.purchaseButtonText}></Text>
)} )}
</TouchableOpacity> </TouchableOpacity>
@@ -971,6 +974,21 @@ const styles = StyleSheet.create({
marginBottom: 30, marginBottom: 30,
padding: 20, padding: 20,
}, },
benefitsTitleContainer: {
alignItems: 'center',
marginBottom: 20,
},
benefitsTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#333',
marginBottom: 5,
},
benefitsSubtitle: {
fontSize: 14,
color: '#666',
textAlign: 'center',
},
benefitsTitleBg: { benefitsTitleBg: {
width: 180, width: 180,
height: 20, height: 20,

View File

@@ -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<MembershipModalContextValue | null>(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 (
<MembershipModalContext.Provider value={contextValue}>
{children}
<MembershipModal
visible={visible}
onClose={closeMembershipModal}
onPurchaseSuccess={handlePurchaseSuccess}
/>
</MembershipModalContext.Provider>
);
}
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;
}

View File

@@ -10,6 +10,7 @@
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
32476CAEFFCE691C1634B0A4 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA3641BAC6078512F41509D /* ExpoModulesProvider.swift */; }; 32476CAEFFCE691C1634B0A4 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA3641BAC6078512F41509D /* ExpoModulesProvider.swift */; };
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; 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 */; }; 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 */; }; 79B2CB732E7B954F00B51753 /* HealthKitManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 79B2CB712E7B954F00B51753 /* HealthKitManager.m */; };
79B2CB742E7B954F00B51753 /* HealthKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79B2CB722E7B954F00B51753 /* HealthKitManager.swift */; }; 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 = "<group>"; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = OutLive/Info.plist; sourceTree = "<group>"; };
1EA3641BAC6078512F41509D /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-OutLive/ExpoModulesProvider.swift"; sourceTree = "<group>"; }; 1EA3641BAC6078512F41509D /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-OutLive/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
6F6136AA7113B3D210693D88 /* libPods-OutLive.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-OutLive.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; 79B2CB712E7B954F00B51753 /* HealthKitManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = HealthKitManager.m; path = OutLive/HealthKitManager.m; sourceTree = "<group>"; };
79B2CB722E7B954F00B51753 /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HealthKitManager.swift; path = OutLive/HealthKitManager.swift; sourceTree = "<group>"; }; 79B2CB722E7B954F00B51753 /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HealthKitManager.swift; path = OutLive/HealthKitManager.swift; sourceTree = "<group>"; };
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 = "<group>"; }; 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 = "<group>"; };
@@ -43,6 +45,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
AE00ECEC9D078460F642F131 /* libPods-OutLive.a in Frameworks */, AE00ECEC9D078460F642F131 /* libPods-OutLive.a in Frameworks */,
792C52592EA880A7002F3F09 /* StoreKit.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -66,6 +69,7 @@
2D16E6871FA4F8E400B85C8A /* Frameworks */ = { 2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
792C52582EA880A7002F3F09 /* StoreKit.framework */,
ED297162215061F000B7C4FE /* JavaScriptCore.framework */, ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
6F6136AA7113B3D210693D88 /* libPods-OutLive.a */, 6F6136AA7113B3D210693D88 /* libPods-OutLive.a */,
); );

View File

@@ -25,7 +25,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0.19</string> <string>1.0.20</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>

63
utils/logger-test.ts Normal file
View File

@@ -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();
}

View File

@@ -41,26 +41,66 @@ class Logger {
} }
private async addLog(level: LogEntry['level'], message: string, data?: any): Promise<void> { private async addLog(level: LogEntry['level'], message: string, data?: any): Promise<void> {
// 安全地处理数据,避免循环引用
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 = { const logEntry: LogEntry = {
id: Date.now().toString() + Math.random().toString(36).substr(2, 9), id: Date.now().toString() + Math.random().toString(36).substr(2, 9),
timestamp: Date.now(), timestamp: Date.now(),
level, level,
message, message,
data data: safeData
}; };
// 同时在控制台输出 // 同时在控制台输出 - 使用原生 console 方法避免循环调用
try {
const logMethod = level === 'ERROR' ? console.error : const logMethod = level === 'ERROR' ? console.error :
level === 'WARN' ? console.warn : level === 'WARN' ? console.warn :
level === 'INFO' ? console.info : console.log; level === 'INFO' ? console.info : console.log;
logMethod(`[${level}] ${message}`, data || ''); logMethod(`[${level}] ${message}`, safeData);
} catch (consoleError) {
// 如果控制台输出失败,使用最基本的 console.log
console.log(`[${level}] ${message}`, typeof safeData === 'string' ? safeData : 'Object data');
}
try { try {
const logs = await this.getLogs(); const logs = await this.getLogs();
logs.push(logEntry); logs.push(logEntry);
await this.saveLogs(logs); await this.saveLogs(logs);
} catch (error) { } catch (error) {
// 使用原生 console.error 避免循环调用
console.error('Failed to add log:', error); console.error('Failed to add log:', error);
} }
} }
@@ -78,6 +118,7 @@ class Logger {
} }
async error(message: string, data?: any): Promise<void> { async error(message: string, data?: any): Promise<void> {
// addLog 方法已经包含了安全的数据处理逻辑
await this.addLog('ERROR', message, data); await this.addLog('ERROR', message, data);
} }