feat(membership): 实现会员系统和购买流程
- 创建 MembershipModalContext 统一管理会员弹窗 - 优化 MembershipModal 产品套餐展示和购买流程 - 集成 RevenueCat SDK 并初始化内购功能 - 在个人中心添加会员 Banner,引导非会员用户订阅 - 修复日志工具的循环引用问题,确保错误信息正确记录 - 版本更新至 1.0.20 新增了完整的会员购买流程,包括套餐选择、购买确认、购买恢复等功能。会员 Banner 仅对非会员用户展示,已是会员的用户不会看到。同时优化了错误日志记录,避免循环引用导致的序列化失败。
This commit is contained in:
2
app.json
2
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",
|
||||
|
||||
@@ -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<number[]>([]);
|
||||
const clickTimeoutRef = useRef<number | null>(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() {
|
||||
</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 = () => (
|
||||
<View style={styles.sectionContainer}>
|
||||
@@ -398,6 +427,7 @@ export default function PersonalScreen() {
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<UserHeader />
|
||||
{userProfile.isVip ? null : <MembershipBanner />}
|
||||
<StatsSection />
|
||||
<View style={styles.fishRecordContainer}>
|
||||
{/* <Image
|
||||
@@ -474,6 +504,11 @@ const styles = StyleSheet.create({
|
||||
elevation: 2,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
membershipBannerImage: {
|
||||
width: '100%',
|
||||
height: 180,
|
||||
borderRadius: 16,
|
||||
},
|
||||
// 用户信息区域
|
||||
userInfoContainer: {
|
||||
flexDirection: 'row',
|
||||
@@ -609,4 +644,3 @@ const styles = StyleSheet.create({
|
||||
marginLeft: 4,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import { BackgroundTaskManager } from '@/services/backgroundTaskManager';
|
||||
import { fetchChallenges } from '@/store/challengesSlice';
|
||||
import AsyncStorage from '@/utils/kvStore';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MembershipModalProvider } from '@/contexts/MembershipModalContext';
|
||||
|
||||
|
||||
function Bootstrapper({ children }: { children: React.ReactNode }) {
|
||||
@@ -207,12 +208,14 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
||||
|
||||
return (
|
||||
<DialogProvider>
|
||||
<MembershipModalProvider>
|
||||
{children}
|
||||
<PrivacyConsentModal
|
||||
visible={showPrivacyModal}
|
||||
onAgree={handlePrivacyAgree}
|
||||
onDisagree={handlePrivacyDisagree}
|
||||
/>
|
||||
</MembershipModalProvider>
|
||||
</DialogProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<PurchasesStoreProduct | null>(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 ?? [];
|
||||
|
||||
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(sortedProducts);
|
||||
} else {
|
||||
console.warn('No active offerings found or no packages available.');
|
||||
captureMessageWithContext('没有找到可用的产品套餐', {
|
||||
hasCurrentOffering: offerings.current !== null,
|
||||
packagesLength: offerings.current?.availablePackages.length || 0
|
||||
});
|
||||
}
|
||||
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
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
console.log('visible', visible);
|
||||
capturePurchaseEvent('error', '获取产品套餐失败', errorData);
|
||||
|
||||
// 设置空状态,避免界面卡在加载状态
|
||||
setProducts([]);
|
||||
setSelectedProduct(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 添加购买状态监听器
|
||||
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 = () => (
|
||||
<View style={styles.benefitsContainer}>
|
||||
<Image source={require('@/assets/images/img_vip_unlock_title.png')} style={styles.benefitsTitleBg} />
|
||||
<View style={styles.benefitItem}>
|
||||
<MaterialIcons name="check-circle" size={20} color="#DF42D0" />
|
||||
<Text style={styles.benefitText}>获得无限制的回复次数</Text>
|
||||
<View style={styles.benefitsTitleContainer}>
|
||||
<Text style={styles.benefitsTitle}>解锁全部健康功能</Text>
|
||||
<Text style={styles.benefitsSubtitle}>开启您的健康蜕变之旅</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.benefitItem}>
|
||||
<MaterialIcons name="check-circle" size={20} color="#DF42D0" />
|
||||
<Text style={styles.benefitText}>获得无限制的个人专属话题库</Text>
|
||||
<Text style={styles.benefitText}>高级营养分析,精准卡路里计算</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.benefitItem}>
|
||||
<MaterialIcons name="check-circle" size={20} color="#DF42D0" />
|
||||
<Text style={styles.benefitText}>获得无广告优质体验</Text>
|
||||
<Text style={styles.benefitText}>定制化减脂计划,科学体重管理</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.benefitItem}>
|
||||
<MaterialIcons name="check-circle" size={20} color="#DF42D0" />
|
||||
<Text style={styles.benefitText}>获得未来免费升级,开启更多功能</Text>
|
||||
<Text style={styles.benefitText}>深度健康数据分析,洞察身体变化</Text>
|
||||
</View>
|
||||
|
||||
|
||||
{/* 皇冠图标 */}
|
||||
<View style={styles.crownContainer}>
|
||||
<Image
|
||||
{/* <Image
|
||||
source={require('@/assets/images/img_profile_vip_bg.png')}
|
||||
style={styles.crownIcon}
|
||||
/>
|
||||
/> */}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -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 (
|
||||
<TouchableOpacity
|
||||
@@ -738,11 +728,11 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
disabled={loading}
|
||||
activeOpacity={loading ? 1 : 0.8}
|
||||
accessible={true}
|
||||
accessibilityLabel={`${product.title} ${plan.price}`}
|
||||
accessibilityHint={loading ? "购买进行中,无法切换套餐" : `选择${product.title}套餐`}
|
||||
accessibilityLabel={`${displayTitle} ${priceLabel}`}
|
||||
accessibilityHint={loading ? '购买进行中,无法切换套餐' : `选择${displayTitle}套餐`}
|
||||
accessibilityState={{ disabled: loading, selected: isSelected }}
|
||||
>
|
||||
{product.identifier === 'com.ilookai.mind_gpt.Lifetime' && (
|
||||
{plan.recommended && (
|
||||
<View style={styles.recommendedBadge}>
|
||||
<Text style={styles.recommendedText}>推荐</Text>
|
||||
</View>
|
||||
@@ -755,7 +745,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
plan.type === 'quarterly' && styles.quarterlyPlanTitle,
|
||||
plan.type === 'weekly' && styles.weeklyPlanTitle,
|
||||
]}>
|
||||
{product.title}
|
||||
{displayTitle}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -766,7 +756,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
plan.type === 'quarterly' && styles.quarterlyPlanPrice,
|
||||
plan.type === 'weekly' && styles.weeklyPlanPrice,
|
||||
]}>
|
||||
{plan.price}
|
||||
{priceLabel || '--'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -807,6 +797,13 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
{renderMembershipBenefits()}
|
||||
|
||||
{/* 会员套餐选择 */}
|
||||
{products.length === 0 && (
|
||||
<View style={styles.configurationNotice}>
|
||||
<Text style={styles.configurationText}>
|
||||
暂未获取到会员商品,请在 RevenueCat 中配置 iOS 产品并同步到当前 Offering。
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.plansContainer}>
|
||||
{products.map(renderPlanCard)}
|
||||
</View>
|
||||
@@ -857,14 +854,20 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.purchaseButton,
|
||||
loading && styles.disabledButton
|
||||
(loading || !selectedProduct) && styles.disabledButton
|
||||
]}
|
||||
onPress={handlePurchase}
|
||||
disabled={loading}
|
||||
disabled={loading || !selectedProduct}
|
||||
accessible={true}
|
||||
accessibilityLabel={loading ? "正在处理购买" : "购买会员"}
|
||||
accessibilityHint={loading ? "购买正在进行中,请稍候" : "点击购买选中的会员套餐"}
|
||||
accessibilityState={{ disabled: loading }}
|
||||
accessibilityLabel={loading ? '正在处理购买' : '购买会员'}
|
||||
accessibilityHint={
|
||||
loading
|
||||
? '购买正在进行中,请稍候'
|
||||
: selectedProduct
|
||||
? `点击购买${selectedProduct.title || '已选'}会员套餐`
|
||||
: '请选择会员套餐后再进行购买'
|
||||
}
|
||||
accessibilityState={{ disabled: loading || !selectedProduct }}
|
||||
>
|
||||
{loading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
@@ -872,7 +875,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
<Text style={styles.purchaseButtonText}>正在处理购买...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={styles.purchaseButtonText}>购买</Text>
|
||||
<Text style={styles.purchaseButtonText}>开启健康蜕变</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -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,
|
||||
|
||||
88
contexts/MembershipModalContext.tsx
Normal file
88
contexts/MembershipModalContext.tsx
Normal 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;
|
||||
}
|
||||
@@ -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 = "<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; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -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 */,
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.19</string>
|
||||
<string>1.0.20</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
63
utils/logger-test.ts
Normal file
63
utils/logger-test.ts
Normal 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();
|
||||
}
|
||||
@@ -41,26 +41,66 @@ class Logger {
|
||||
}
|
||||
|
||||
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 = {
|
||||
id: Date.now().toString() + Math.random().toString(36).substr(2, 9),
|
||||
timestamp: Date.now(),
|
||||
level,
|
||||
message,
|
||||
data
|
||||
data: safeData
|
||||
};
|
||||
|
||||
// 同时在控制台输出
|
||||
// 同时在控制台输出 - 使用原生 console 方法避免循环调用
|
||||
try {
|
||||
const logMethod = level === 'ERROR' ? console.error :
|
||||
level === 'WARN' ? console.warn :
|
||||
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 {
|
||||
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<void> {
|
||||
// addLog 方法已经包含了安全的数据处理逻辑
|
||||
await this.addLog('ERROR', message, data);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user