feat(vip): 限制底部栏自定义功能为VIP专享
非VIP用户尝试配置底部栏时将显示会员购买弹窗, 只有VIP会员才能自由开启或关闭导航标签。 包含会员权益说明的国际化支持和存储结构重构。
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.1.3",
|
"version": "1.1.4",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"scheme": "digitalpilates",
|
"scheme": "digitalpilates",
|
||||||
"userInterfaceStyle": "light",
|
"userInterfaceStyle": "light",
|
||||||
|
|||||||
@@ -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 }],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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内容
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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: '无限次使用',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user