feat(membership): 重构会员系统架构并优化VIP卡片显示

- 创建独立的会员服务模块 services/membership.ts,统一管理会员计划元数据和工具函数
- 新增 membershipSlice Redux状态管理,集中处理会员数据和状态
- 重构个人中心VIP会员卡片,支持动态显示会员计划和有效期
- 优化会员购买弹窗,使用统一的会员计划配置
- 改进会员数据获取流程,确保状态同步和一致性
This commit is contained in:
richarjiang
2025-10-29 16:08:58 +08:00
parent fcf1be211f
commit 7cd290d341
7 changed files with 569 additions and 114 deletions

211
services/membership.ts Normal file
View File

@@ -0,0 +1,211 @@
import type {
CustomerInfo,
PurchasesOfferings,
PurchasesStoreProduct,
} from 'react-native-purchases';
export type MembershipPlanType = 'weekly' | 'quarterly' | 'lifetime';
export interface MembershipPlanMeta {
id: string;
fallbackTitle: string;
subtitle: string;
type: MembershipPlanType;
recommended?: boolean;
tag?: string;
originalPrice?: string;
}
export const MEMBERSHIP_PLAN_META: MembershipPlanMeta[] = [
{
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',
},
];
const MEMBERSHIP_PLAN_META_MAP = new Map(
MEMBERSHIP_PLAN_META.map((plan) => [plan.id, plan]),
);
export interface MembershipPlanSummary {
id: string;
title: string;
description: string;
price: number;
priceString: string;
type: MembershipPlanType | 'unknown';
fallbackTitle?: string;
subtitle?: string;
originalPrice?: string;
tag?: string;
recommended?: boolean;
}
export const getPlanMetaById = (id: string): MembershipPlanMeta | undefined =>
MEMBERSHIP_PLAN_META_MAP.get(id);
export const resolvePlanDisplayName = (
product: PurchasesStoreProduct | null | undefined,
fallbackPlan?: MembershipPlanMeta,
defaultName = 'VIP 会员',
): string => {
const productTitle = product?.title?.trim();
if (productTitle) {
return productTitle;
}
if (fallbackPlan?.fallbackTitle) {
return fallbackPlan.fallbackTitle;
}
return defaultName;
};
export const extractMembershipProductsFromOfferings = (
offerings: PurchasesOfferings,
): PurchasesStoreProduct[] => {
const packages = offerings.current?.availablePackages ?? [];
const matchedProducts = packages
.map((pkg) => pkg.product)
.filter((product) => MEMBERSHIP_PLAN_META_MAP.has(product.identifier));
if (matchedProducts.length > 0) {
return MEMBERSHIP_PLAN_META
.map((plan) =>
matchedProducts.find((product) => product.identifier === plan.id),
)
.filter((product): product is PurchasesStoreProduct => Boolean(product));
}
return packages.map((pkg) => pkg.product);
};
export const summarizeProducts = (
products: PurchasesStoreProduct[],
): MembershipPlanSummary[] =>
products.map((product) => {
const meta = getPlanMetaById(product.identifier);
return {
id: product.identifier,
title: product.title,
description: product.description,
price: product.price,
priceString: product.priceString,
type: meta?.type ?? 'unknown',
fallbackTitle: meta?.fallbackTitle,
subtitle: meta?.subtitle,
originalPrice: meta?.originalPrice,
tag: meta?.tag,
recommended: meta?.recommended ?? false,
};
});
export 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 [];
};
export const collectActiveProductIdentifiers = (
customerInfo: CustomerInfo,
): string[] => {
const collected = new Set<string>();
Object.values(customerInfo.entitlements.active).forEach((entitlement) => {
if (entitlement?.productIdentifier) {
collected.add(entitlement.productIdentifier);
}
});
customerInfo.nonSubscriptionTransactions.forEach((transaction) => {
if (transaction?.productIdentifier) {
collected.add(transaction.productIdentifier);
}
});
getActiveSubscriptionIds(customerInfo).forEach((identifier) => {
if (identifier) {
collected.add(identifier);
}
});
return Array.from(collected);
};
export const hasActiveMembership = (customerInfo: CustomerInfo): boolean => {
if (Object.keys(customerInfo.entitlements.active).length > 0) {
return true;
}
if (customerInfo.nonSubscriptionTransactions.length > 0) {
return true;
}
return getActiveSubscriptionIds(customerInfo).length > 0;
};
export const pickActiveProductId = (
customerInfo: CustomerInfo,
availableProducts: PurchasesStoreProduct[],
): {
activeProductId: string | null;
activeProductIds: string[];
} => {
const activeProductIds = collectActiveProductIdentifiers(customerInfo);
if (activeProductIds.length === 0) {
return { activeProductId: null, activeProductIds };
}
const availableIds = new Set(
availableProducts.map((product) => product.identifier),
);
for (const plan of MEMBERSHIP_PLAN_META) {
if (
activeProductIds.includes(plan.id) &&
availableIds.has(plan.id)
) {
return { activeProductId: plan.id, activeProductIds };
}
}
for (const identifier of activeProductIds) {
if (availableIds.has(identifier)) {
return { activeProductId: identifier, activeProductIds };
}
}
return { activeProductId: null, activeProductIds };
};