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

138
store/membershipSlice.ts Normal file
View File

@@ -0,0 +1,138 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import Purchases from 'react-native-purchases';
import {
extractMembershipProductsFromOfferings,
getPlanMetaById,
hasActiveMembership,
pickActiveProductId,
resolvePlanDisplayName,
summarizeProducts,
type MembershipPlanSummary,
} from '@/services/membership';
import type { RootState } from './index';
import { logout, updateProfile } from './userSlice';
export interface MembershipState {
plans: MembershipPlanSummary[];
activePlanId: string | null;
activePlanName: string | null;
hasActiveMembership: boolean;
loading: boolean;
error: string | null;
lastUpdated: number | null;
activeProductIds: string[];
}
const initialState: MembershipState = {
plans: [],
activePlanId: null,
activePlanName: null,
hasActiveMembership: false,
loading: false,
error: null,
lastUpdated: null,
activeProductIds: [],
};
export const fetchMembershipData = createAsyncThunk<
{
plans: MembershipPlanSummary[];
activePlanId: string | null;
activePlanName: string | null;
hasActiveMembership: boolean;
activeProductIds: string[];
lastUpdated: number;
},
void,
{ rejectValue: string }
>('membership/fetchMembershipData', async (_, { rejectWithValue, dispatch }) => {
try {
const offerings = await Purchases.getOfferings();
const products = extractMembershipProductsFromOfferings(offerings);
const plans = summarizeProducts(products);
const customerInfo = await Purchases.getCustomerInfo();
const { activeProductId, activeProductIds } = pickActiveProductId(
customerInfo,
products,
);
const activePlanMeta = activeProductId
? getPlanMetaById(activeProductId)
: undefined;
const activeProduct = activeProductId
? products.find((product) => product.identifier === activeProductId) ?? null
: null;
const hasActive = hasActiveMembership(customerInfo);
const activePlanName = hasActive
? resolvePlanDisplayName(activeProduct, activePlanMeta)
: null;
dispatch(
updateProfile({
vipPlanName: activePlanName ?? undefined,
isVip: hasActive,
}),
);
return {
plans,
activePlanId: activeProductId,
activePlanName,
hasActiveMembership: hasActive,
activeProductIds,
lastUpdated: Date.now(),
};
} catch (error: any) {
const message =
error?.message ??
(typeof error === 'string' ? error : '获取会员信息失败');
return rejectWithValue(message);
}
});
const membershipSlice = createSlice({
name: 'membership',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchMembershipData.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchMembershipData.fulfilled, (state, action) => {
state.loading = false;
state.error = null;
state.plans = action.payload.plans;
state.activePlanId = action.payload.activePlanId;
state.activePlanName = action.payload.activePlanName;
state.hasActiveMembership = action.payload.hasActiveMembership;
state.activeProductIds = action.payload.activeProductIds;
state.lastUpdated = action.payload.lastUpdated;
})
.addCase(fetchMembershipData.rejected, (state, action) => {
state.loading = false;
state.error =
(action.payload as string) ?? '获取会员信息失败,请稍后重试';
})
.addCase(logout.fulfilled, () => ({
...initialState,
plans: [],
activeProductIds: [],
}));
},
});
export const selectMembershipState = (state: RootState): MembershipState =>
state.membership;
export const selectMembershipPlans = (state: RootState) =>
state.membership.plans;
export const selectActiveMembershipPlanName = (state: RootState) =>
state.membership.activePlanName;
export default membershipSlice.reducer;