feat(membership): 重构会员系统架构并优化VIP卡片显示
- 创建独立的会员服务模块 services/membership.ts,统一管理会员计划元数据和工具函数 - 新增 membershipSlice Redux状态管理,集中处理会员数据和状态 - 重构个人中心VIP会员卡片,支持动态显示会员计划和有效期 - 优化会员购买弹窗,使用统一的会员计划配置 - 改进会员数据获取流程,确保状态同步和一致性
This commit is contained in:
211
services/membership.ts
Normal file
211
services/membership.ts
Normal 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 };
|
||||
};
|
||||
Reference in New Issue
Block a user