feat(membership): 重构会员系统架构并优化VIP卡片显示
- 创建独立的会员服务模块 services/membership.ts,统一管理会员计划元数据和工具函数 - 新增 membershipSlice Redux状态管理,集中处理会员数据和状态 - 重构个人中心VIP会员卡片,支持动态显示会员计划和有效期 - 优化会员购买弹窗,使用统一的会员计划配置 - 改进会员数据获取流程,确保状态同步和一致性
This commit is contained in:
@@ -1,6 +1,16 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import CustomCheckBox from '@/components/ui/CheckBox';
|
||||
import { USER_AGREEMENT_URL } from '@/constants/Agree';
|
||||
import { useAppDispatch } from '@/hooks/redux';
|
||||
import {
|
||||
MEMBERSHIP_PLAN_META,
|
||||
extractMembershipProductsFromOfferings,
|
||||
getActiveSubscriptionIds,
|
||||
getPlanMetaById,
|
||||
resolvePlanDisplayName,
|
||||
type MembershipPlanType,
|
||||
} from '@/services/membership';
|
||||
import { fetchMembershipData } from '@/store/membershipSlice';
|
||||
import { log, logger } from '@/utils/logger';
|
||||
import {
|
||||
captureMessage,
|
||||
@@ -36,59 +46,7 @@ interface MembershipModalProps {
|
||||
onPurchaseSuccess?: () => void;
|
||||
}
|
||||
|
||||
interface MembershipPlan {
|
||||
id: string;
|
||||
fallbackTitle: string;
|
||||
subtitle: string;
|
||||
type: 'weekly' | 'quarterly' | 'lifetime';
|
||||
recommended?: boolean;
|
||||
tag?: string;
|
||||
originalPrice?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_PLANS: MembershipPlan[] = [
|
||||
{
|
||||
id: 'com.anonymous.digitalpilates.membership.lifetime',
|
||||
fallbackTitle: '终身会员',
|
||||
subtitle: '一次投入,终身健康陪伴',
|
||||
type: 'lifetime',
|
||||
recommended: true,
|
||||
tag: '限时特价',
|
||||
originalPrice: '¥898',
|
||||
},
|
||||
{
|
||||
id: 'com.anonymous.digitalpilates.membership.quarter',
|
||||
fallbackTitle: '季度会员',
|
||||
subtitle: '3个月蜕变计划,见证身材变化',
|
||||
type: 'quarterly',
|
||||
originalPrice: '¥598',
|
||||
},
|
||||
{
|
||||
id: 'com.anonymous.digitalpilates.membership.weekly',
|
||||
fallbackTitle: '周会员',
|
||||
subtitle: '7天体验,开启健康第一步',
|
||||
type: 'weekly',
|
||||
originalPrice: '¥128',
|
||||
},
|
||||
];
|
||||
|
||||
// RevenueCat 在 JS SDK 中以 string[] 返回 activeSubscriptions,但保持健壮性以防类型变化
|
||||
const getActiveSubscriptionIds = (customerInfo: CustomerInfo): string[] => {
|
||||
const activeSubscriptions = customerInfo.activeSubscriptions as unknown;
|
||||
|
||||
if (Array.isArray(activeSubscriptions)) {
|
||||
return activeSubscriptions;
|
||||
}
|
||||
|
||||
if (activeSubscriptions && typeof activeSubscriptions === 'object') {
|
||||
return Object.values(activeSubscriptions as Record<string, string>).filter(
|
||||
(value): value is string => typeof value === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
// 权限类型枚举
|
||||
type PermissionType = 'exclusive' | 'limited' | 'unlimited';
|
||||
|
||||
@@ -151,37 +109,9 @@ const BENEFIT_COMPARISON: BenefitItem[] = [
|
||||
vipText: '基础提醒'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'AI教练对话',
|
||||
description: '与AI健康教练进行个性化对话咨询',
|
||||
vip: {
|
||||
type: 'unlimited',
|
||||
text: '无限次对话',
|
||||
vipText: '深度分析'
|
||||
},
|
||||
regular: {
|
||||
type: 'limited',
|
||||
text: '有限次对话',
|
||||
vipText: '每日10次'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '体态评估',
|
||||
description: '通过照片分析体态问题并提供改善建议',
|
||||
vip: {
|
||||
type: 'exclusive',
|
||||
text: '完全支持',
|
||||
vipText: '专业评估'
|
||||
},
|
||||
regular: {
|
||||
type: 'exclusive',
|
||||
text: '不可使用',
|
||||
vipText: '不可使用'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const PLAN_STYLE_CONFIG: Record<MembershipPlan['type'], { gradient: readonly [string, string]; accent: string }> = {
|
||||
const PLAN_STYLE_CONFIG: Record<MembershipPlanType, { gradient: readonly [string, string]; accent: string }> = {
|
||||
lifetime: {
|
||||
gradient: ['#FFF1DD', '#FFE8FA'] as const,
|
||||
accent: '#7B2CBF',
|
||||
@@ -221,6 +151,7 @@ const getPermissionIcon = (type: PermissionType, isVip: boolean) => {
|
||||
};
|
||||
|
||||
export function MembershipModal({ visible, onClose, onPurchaseSuccess }: MembershipModalProps) {
|
||||
const dispatch = useAppDispatch();
|
||||
const [selectedProduct, setSelectedProduct] = useState<PurchasesStoreProduct | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [restoring, setRestoring] = useState(false);
|
||||
@@ -238,7 +169,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
const getTipsContent = (product: PurchasesStoreProduct | null): string => {
|
||||
if (!product) return '';
|
||||
|
||||
const plan = DEFAULT_PLANS.find(item => item.id === product.identifier);
|
||||
const plan = MEMBERSHIP_PLAN_META.find(item => item.id === product.identifier);
|
||||
if (!plan) {
|
||||
return '';
|
||||
}
|
||||
@@ -299,9 +230,9 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
allOfferingsCount: Object.keys(offerings.all).length,
|
||||
});
|
||||
|
||||
const packages = offerings.current?.availablePackages ?? [];
|
||||
const productsToUse = extractMembershipProductsFromOfferings(offerings);
|
||||
|
||||
if (packages.length === 0) {
|
||||
if (productsToUse.length === 0) {
|
||||
log.warn('没有找到可用的产品套餐', {
|
||||
hasCurrentOffering: offerings.current !== null,
|
||||
packagesLength: offerings.current?.availablePackages.length || 0,
|
||||
@@ -315,17 +246,6 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
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);
|
||||
@@ -394,6 +314,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
|
||||
// 延迟一点时间,确保购买流程完全完成
|
||||
setTimeout(async () => {
|
||||
void dispatch(fetchMembershipData());
|
||||
// 刷新用户信息
|
||||
// await refreshUserInfo();
|
||||
|
||||
@@ -489,7 +410,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
let selectedProduct: PurchasesStoreProduct | null = null;
|
||||
|
||||
// 优先级:终身会员 > 季度会员 > 周会员
|
||||
const priorityOrder = DEFAULT_PLANS.map(plan => plan.id);
|
||||
const priorityOrder = MEMBERSHIP_PLAN_META.map(plan => plan.id);
|
||||
|
||||
// 按照优先级查找
|
||||
for (const priorityProductId of priorityOrder) {
|
||||
@@ -747,6 +668,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
restoredProducts,
|
||||
restoredProductsCount: restoredProducts.length
|
||||
});
|
||||
void dispatch(fetchMembershipData());
|
||||
|
||||
try {
|
||||
// 调用后台服务接口进行票据匹配
|
||||
@@ -856,16 +778,11 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
|
||||
|
||||
const renderPlanCard = (product: PurchasesStoreProduct) => {
|
||||
const plan = DEFAULT_PLANS.find(p => p.id === product.identifier);
|
||||
|
||||
if (!plan) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const planMeta = getPlanMetaById(product.identifier);
|
||||
const isSelected = selectedProduct === product;
|
||||
const displayTitle = product.title || plan.fallbackTitle;
|
||||
const displayTitle = resolvePlanDisplayName(product, planMeta);
|
||||
const priceLabel = product.priceString || '';
|
||||
const styleConfig = PLAN_STYLE_CONFIG[plan.type];
|
||||
const styleConfig = planMeta ? PLAN_STYLE_CONFIG[planMeta.type] : undefined;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
@@ -890,9 +807,9 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
style={styles.planCardGradient}
|
||||
>
|
||||
<View style={styles.planCardTopSection}>
|
||||
{plan.tag && (
|
||||
{planMeta?.tag && (
|
||||
<View style={styles.planTag}>
|
||||
<Text style={styles.planTagText}>{plan.tag}</Text>
|
||||
<Text style={styles.planTagText}>{planMeta.tag}</Text>
|
||||
</View>
|
||||
)}
|
||||
<Text style={styles.planCardTitle}>{displayTitle}</Text>
|
||||
@@ -902,13 +819,13 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
<Text style={[styles.planCardPrice, styleConfig && { color: styleConfig.accent }]}>
|
||||
{priceLabel || '--'}
|
||||
</Text>
|
||||
{plan.originalPrice && (
|
||||
<Text style={styles.planCardOriginalPrice}>{plan.originalPrice}</Text>
|
||||
{planMeta?.originalPrice && (
|
||||
<Text style={styles.planCardOriginalPrice}>{planMeta.originalPrice}</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.planCardBottomSection}>
|
||||
<Text style={styles.planCardDescription}>{plan.subtitle}</Text>
|
||||
<Text style={styles.planCardDescription}>{planMeta?.subtitle ?? ''}</Text>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
|
||||
Reference in New Issue
Block a user