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": {
|
"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",
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
{children}
|
<MembershipModalProvider>
|
||||||
<PrivacyConsentModal
|
{children}
|
||||||
visible={showPrivacyModal}
|
<PrivacyConsentModal
|
||||||
onAgree={handlePrivacyAgree}
|
visible={showPrivacyModal}
|
||||||
onDisagree={handlePrivacyDisagree}
|
onAgree={handlePrivacyAgree}
|
||||||
/>
|
onDisagree={handlePrivacyDisagree}
|
||||||
|
/>
|
||||||
|
</MembershipModalProvider>
|
||||||
</DialogProvider>
|
</DialogProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
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);
|
|
||||||
|
|
||||||
// 获取产品后,检查用户的购买记录并自动选中对应套餐
|
if (packages.length === 0) {
|
||||||
await checkAndSelectActivePlan(sortedProducts);
|
log.warn('没有找到可用的产品套餐', {
|
||||||
} else {
|
|
||||||
console.warn('No active offerings found or no packages available.');
|
|
||||||
captureMessageWithContext('没有找到可用的产品套餐', {
|
|
||||||
hasCurrentOffering: offerings.current !== null,
|
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) {
|
} 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
|
// 设置空状态,避免界面卡在加载状态
|
||||||
// });
|
setProducts([]);
|
||||||
|
setSelectedProduct(null);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
console.log('visible', visible);
|
|
||||||
|
|
||||||
|
|
||||||
// 添加购买状态监听器
|
// 添加购买状态监听器
|
||||||
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,
|
||||||
|
|||||||
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 */; };
|
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 */,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
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> {
|
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 方法避免循环调用
|
||||||
const logMethod = level === 'ERROR' ? console.error :
|
try {
|
||||||
level === 'WARN' ? console.warn :
|
const logMethod = level === 'ERROR' ? console.error :
|
||||||
level === 'INFO' ? console.info : console.log;
|
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 {
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user