- 创建独立的会员服务模块 services/membership.ts,统一管理会员计划元数据和工具函数 - 新增 membershipSlice Redux状态管理,集中处理会员数据和状态 - 重构个人中心VIP会员卡片,支持动态显示会员计划和有效期 - 优化会员购买弹窗,使用统一的会员计划配置 - 改进会员数据获取流程,确保状态同步和一致性
212 lines
5.5 KiB
TypeScript
212 lines
5.5 KiB
TypeScript
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 };
|
||
};
|