Files
digital-pilates/services/membership.ts
richarjiang 7cd290d341 feat(membership): 重构会员系统架构并优化VIP卡片显示
- 创建独立的会员服务模块 services/membership.ts,统一管理会员计划元数据和工具函数
- 新增 membershipSlice Redux状态管理,集中处理会员数据和状态
- 重构个人中心VIP会员卡片,支持动态显示会员计划和有效期
- 优化会员购买弹窗,使用统一的会员计划配置
- 改进会员数据获取流程,确保状态同步和一致性
2025-10-29 16:08:58 +08:00

212 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 };
};