feat(vip): 限制底部栏自定义功能为VIP专享

非VIP用户尝试配置底部栏时将显示会员购买弹窗,
只有VIP会员才能自由开启或关闭导航标签。
包含会员权益说明的国际化支持和存储结构重构。
This commit is contained in:
richarjiang
2025-12-01 16:56:54 +08:00
parent a47f0fb72e
commit be0dd750eb
7 changed files with 106 additions and 23 deletions

View File

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

View File

@@ -1,5 +1,6 @@
import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
import { useVipService } from '@/hooks/useVipService';
import { import {
resetToDefault, resetToDefault,
selectTabBarConfigs, selectTabBarConfigs,
@@ -9,7 +10,7 @@ import {
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import React, { useCallback } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { import {
Alert, Alert,
ScrollView, ScrollView,
@@ -20,6 +21,7 @@ import {
View View
} from 'react-native'; } from 'react-native';
import { MembershipModal } from '@/components/model/MembershipModal';
import { HeaderBar } from '@/components/ui/HeaderBar'; import { HeaderBar } from '@/components/ui/HeaderBar';
import { IconSymbol } from '@/components/ui/IconSymbol'; import { IconSymbol } from '@/components/ui/IconSymbol';
import { palette } from '@/constants/Colors'; import { palette } from '@/constants/Colors';
@@ -31,15 +33,38 @@ export default function TabBarConfigScreen() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const safeAreaTop = useSafeAreaTop(60); const safeAreaTop = useSafeAreaTop(60);
const configs = useAppSelector(selectTabBarConfigs); const configs = useAppSelector(selectTabBarConfigs);
const { isVip } = useVipService();
const [showMembershipModal, setShowMembershipModal] = useState(false);
// 处理开关切换 // 处理开关切换
const handleToggle = useCallback( const handleToggle = useCallback(
(tabId: string) => { (tabId: string) => {
// 直接检查用户是否是 VIP底部栏配置不是权益类功能而是基础功能
if (isVip) {
// VIP 用户可以正常切换
dispatch(toggleTabEnabled(tabId)); dispatch(toggleTabEnabled(tabId));
} else {
// 非 VIP 用户显示购买弹窗
setShowMembershipModal(true);
}
}, },
[dispatch] [dispatch, isVip]
); );
// 页面加载时检查 VIP 状态
useEffect(() => {
if (!isVip) {
// 非 VIP 用户进入页面时立即显示购买弹窗
setShowMembershipModal(true);
}
}, [isVip]);
// 购买成功回调
const handlePurchaseSuccess = useCallback(() => {
// 购买成功后可以执行一些操作,比如刷新用户信息
console.log('会员购买成功');
}, []);
// 恢复默认设置 // 恢复默认设置
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
Alert.alert( Alert.alert(
@@ -80,6 +105,11 @@ export default function TabBarConfigScreen() {
{t('personal.tabBarConfig.cannotDisable')} {t('personal.tabBarConfig.cannotDisable')}
</Text> </Text>
)} )}
{item.canBeDisabled && !isVip && (
<Text style={styles.vipSubtitle}>
{t('personal.tabBarConfig.vipOnly')}
</Text>
)}
</View> </View>
</View> </View>
@@ -87,7 +117,7 @@ export default function TabBarConfigScreen() {
<Switch <Switch
value={item.enabled} value={item.enabled}
onValueChange={() => handleToggle(item.id)} onValueChange={() => handleToggle(item.id)}
disabled={!item.canBeDisabled} disabled={!item.canBeDisabled || !isVip}
trackColor={{ false: '#E5E5E5', true: '#9370DB' }} trackColor={{ false: '#E5E5E5', true: '#9370DB' }}
thumbColor="#FFFFFF" thumbColor="#FFFFFF"
style={styles.switch} style={styles.switch}
@@ -153,6 +183,13 @@ export default function TabBarConfigScreen() {
</View> </View>
</ScrollView> </ScrollView>
{/* 会员购买弹窗 */}
<MembershipModal
visible={showMembershipModal}
onClose={() => setShowMembershipModal(false)}
onPurchaseSuccess={handlePurchaseSuccess}
/>
</View> </View>
); );
} }
@@ -253,6 +290,10 @@ const styles = StyleSheet.create({
fontSize: 12, fontSize: 12,
color: '#9370DB', color: '#9370DB',
}, },
vipSubtitle: {
fontSize: 12,
color: '#FF6B6B',
},
switch: { switch: {
transform: [{ scaleX: 0.9 }, { scaleY: 0.9 }], transform: [{ scaleX: 0.9 }, { scaleY: 0.9 }],
}, },

View File

@@ -194,6 +194,20 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
vipText: t('membershipModal.benefits.permissions.notSupported') vipText: t('membershipModal.benefits.permissions.notSupported')
} }
}, },
{
title: t('membershipModal.benefits.items.tabBarCustomization.title'),
description: t('membershipModal.benefits.items.tabBarCustomization.description'),
vip: {
type: 'exclusive',
text: t('membershipModal.benefits.permissions.fullSupport'),
vipText: t('membershipModal.benefits.permissions.unlimited')
},
regular: {
type: 'exclusive',
text: t('membershipModal.benefits.permissions.notSupported'),
vipText: t('membershipModal.benefits.permissions.notSupported')
}
},
]; ];
// 根据选中的产品生成tips内容 // 根据选中的产品生成tips内容

View File

@@ -92,6 +92,7 @@ export const personal = {
description: 'Use toggles to show or hide tabs', description: 'Use toggles to show or hide tabs',
resetButton: 'Reset', resetButton: 'Reset',
cannotDisable: 'Cannot be disabled', cannotDisable: 'Cannot be disabled',
vipOnly: 'VIP members only',
resetConfirm: { resetConfirm: {
title: 'Reset to Default?', title: 'Reset to Default?',
message: 'This will reset all tab bar settings and visibility', message: 'This will reset all tab bar settings and visibility',

View File

@@ -92,6 +92,7 @@ export const personal = {
description: '使用开关控制标签的显示和隐藏', description: '使用开关控制标签的显示和隐藏',
resetButton: '恢复默认', resetButton: '恢复默认',
cannotDisable: '此标签不可关闭', cannotDisable: '此标签不可关闭',
vipOnly: '仅限VIP会员',
resetConfirm: { resetConfirm: {
title: '恢复默认设置?', title: '恢复默认设置?',
message: '将重置所有底部栏配置和显示状态', message: '将重置所有底部栏配置和显示状态',
@@ -299,6 +300,10 @@ export const membershipModal = {
title: '解锁无限自定义挑战', title: '解锁无限自定义挑战',
description: '突破限制,邀请挚友同行,让坚持不再孤单,共同见证蜕变', description: '突破限制,邀请挚友同行,让坚持不再孤单,共同见证蜕变',
}, },
tabBarCustomization: {
title: '底部栏自定义',
description: '个性化底部导航栏,隐藏不需要的功能标签',
},
}, },
permissions: { permissions: {
unlimited: '无限次使用', unlimited: '无限次使用',

View File

@@ -27,7 +27,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.1.3</string> <string>1.1.4</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>

View File

@@ -9,10 +9,17 @@ export interface TabConfig {
icon: string; // SF Symbol 图标名 icon: string; // SF Symbol 图标名
titleKey: string; // i18n 翻译 key titleKey: string; // i18n 翻译 key
enabled: boolean; // 是否启用 enabled: boolean; // 是否启用
canBeDisabled: boolean; // 是否可以被禁用 canBeDisabled: boolean; // 是否可以被禁用(系统级配置,不持久化)
order: number; // 显示顺序 order: number; // 显示顺序
} }
// 用户可持久化的配置(只包含用户可控制的属性)
interface UserTabConfig {
id: string;
enabled: boolean;
order: number;
}
// State 接口 // State 接口
interface TabBarConfigState { interface TabBarConfigState {
configs: TabConfig[]; configs: TabConfig[];
@@ -34,7 +41,7 @@ export const DEFAULT_TAB_CONFIGS: TabConfig[] = [
icon: 'pills.fill', icon: 'pills.fill',
titleKey: 'statistics.tabs.medications', titleKey: 'statistics.tabs.medications',
enabled: true, enabled: true,
canBeDisabled: false, canBeDisabled: true, // 用药管理可以被关闭
order: 2, order: 2,
}, },
{ {
@@ -42,7 +49,7 @@ export const DEFAULT_TAB_CONFIGS: TabConfig[] = [
icon: 'timer', icon: 'timer',
titleKey: 'statistics.tabs.fasting', titleKey: 'statistics.tabs.fasting',
enabled: true, enabled: true,
canBeDisabled: true, // 只有断食可以被关闭 canBeDisabled: true, // 断食可以被关闭
order: 3, order: 3,
}, },
{ {
@@ -50,7 +57,7 @@ export const DEFAULT_TAB_CONFIGS: TabConfig[] = [
icon: 'trophy.fill', icon: 'trophy.fill',
titleKey: 'statistics.tabs.challenges', titleKey: 'statistics.tabs.challenges',
enabled: true, enabled: true,
canBeDisabled: false, canBeDisabled: true, // 挑战可以被关闭
order: 4, order: 4,
}, },
{ {
@@ -120,10 +127,16 @@ const tabBarConfigSlice = createSlice({
}, },
}); });
// 持久化配置到 AsyncStorage // 持久化配置到 AsyncStorage(只保存用户可控制的属性)
const saveConfigsToStorage = async (configs: TabConfig[]) => { const saveConfigsToStorage = async (configs: TabConfig[]) => {
try { try {
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(configs)); // 只保存用户可控制的属性enabled 和 order
const userConfigs: UserTabConfig[] = configs.map(config => ({
id: config.id,
enabled: config.enabled,
order: config.order,
}));
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(userConfigs));
logger.info('底部栏配置已保存'); logger.info('底部栏配置已保存');
} catch (error) { } catch (error) {
logger.error('保存底部栏配置失败:', error); logger.error('保存底部栏配置失败:', error);
@@ -136,12 +149,12 @@ export const loadTabBarConfigs = () => async (dispatch: any) => {
const stored = await AsyncStorage.getItem(STORAGE_KEY); const stored = await AsyncStorage.getItem(STORAGE_KEY);
if (stored) { if (stored) {
const configs = JSON.parse(stored) as TabConfig[]; const userConfigs = JSON.parse(stored) as UserTabConfig[];
// 验证配置有效性 // 验证配置有效性
if (Array.isArray(configs) && configs.length > 0) { if (Array.isArray(userConfigs) && userConfigs.length > 0) {
// 合并默认配置,确保新增的 tab 也能显示 // 合并用户配置和默认配置
const mergedConfigs = mergeWithDefaults(configs); const mergedConfigs = mergeWithDefaults(userConfigs);
dispatch(setConfigs(mergedConfigs)); dispatch(setConfigs(mergedConfigs));
logger.info('底部栏配置已加载'); logger.info('底部栏配置已加载');
return; return;
@@ -159,15 +172,24 @@ export const loadTabBarConfigs = () => async (dispatch: any) => {
} }
}; };
// 合并存储的配置和默认配置 // 合并用户配置和默认配置
const mergeWithDefaults = (storedConfigs: TabConfig[]): TabConfig[] => { const mergeWithDefaults = (userConfigs: UserTabConfig[]): TabConfig[] => {
const merged = [...storedConfigs]; const merged: TabConfig[] = [];
// 检查是否有新增的默认 tab // 遍历默认配置,将用户的 enabled 和 order 合并进来
DEFAULT_TAB_CONFIGS.forEach(defaultConfig => { DEFAULT_TAB_CONFIGS.forEach(defaultConfig => {
const exists = merged.find(c => c.id === defaultConfig.id); const userConfig = userConfigs.find(c => c.id === defaultConfig.id);
if (!exists) {
// 新增的 tab添加到末尾 if (userConfig) {
// 合并系统配置icon, titleKey, canBeDisabled从默认配置读取
// 用户配置enabled, order从用户配置读取
merged.push({
...defaultConfig,
enabled: userConfig.enabled,
order: userConfig.order,
});
} else {
// 新增的 tab使用默认配置
merged.push({ merged.push({
...defaultConfig, ...defaultConfig,
order: merged.length + 1, order: merged.length + 1,