feat(membership): 实现会员系统和购买流程
- 创建 MembershipModalContext 统一管理会员弹窗 - 优化 MembershipModal 产品套餐展示和购买流程 - 集成 RevenueCat SDK 并初始化内购功能 - 在个人中心添加会员 Banner,引导非会员用户订阅 - 修复日志工具的循环引用问题,确保错误信息正确记录 - 版本更新至 1.0.20 新增了完整的会员购买流程,包括套餐选择、购买确认、购买恢复等功能。会员 Banner 仅对非会员用户展示,已是会员的用户不会看到。同时优化了错误日志记录,避免循环引用导致的序列化失败。
This commit is contained in:
@@ -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>
|
||||
{children}
|
||||
<PrivacyConsentModal
|
||||
visible={showPrivacyModal}
|
||||
onAgree={handlePrivacyAgree}
|
||||
onDisagree={handlePrivacyDisagree}
|
||||
/>
|
||||
<MembershipModalProvider>
|
||||
{children}
|
||||
<PrivacyConsentModal
|
||||
visible={showPrivacyModal}
|
||||
onAgree={handlePrivacyAgree}
|
||||
onDisagree={handlePrivacyDisagree}
|
||||
/>
|
||||
</MembershipModalProvider>
|
||||
</DialogProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user