From 7cd290d341de491cb8c150e540470ffcd92f9727 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Wed, 29 Oct 2025 16:08:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(membership):=20=E9=87=8D=E6=9E=84=E4=BC=9A?= =?UTF-8?q?=E5=91=98=E7=B3=BB=E7=BB=9F=E6=9E=B6=E6=9E=84=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96VIP=E5=8D=A1=E7=89=87=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建独立的会员服务模块 services/membership.ts,统一管理会员计划元数据和工具函数 - 新增 membershipSlice Redux状态管理,集中处理会员数据和状态 - 重构个人中心VIP会员卡片,支持动态显示会员计划和有效期 - 优化会员购买弹窗,使用统一的会员计划配置 - 改进会员数据获取流程,确保状态同步和一致性 --- app/(tabs)/personal.tsx | 181 ++++++++++++++++++++++- components/model/MembershipModal.tsx | 135 ++++------------- contexts/MembershipModalContext.tsx | 12 +- services/membership.ts | 211 +++++++++++++++++++++++++++ store/index.ts | 2 + store/membershipSlice.ts | 138 ++++++++++++++++++ store/userSlice.ts | 4 +- 7 files changed, 569 insertions(+), 114 deletions(-) create mode 100644 services/membership.ts create mode 100644 store/membershipSlice.ts diff --git a/app/(tabs)/personal.tsx b/app/(tabs)/personal.tsx index f485680..40068b8 100644 --- a/app/(tabs)/personal.tsx +++ b/app/(tabs)/personal.tsx @@ -6,12 +6,14 @@ import { useMembershipModal } from '@/contexts/MembershipModalContext'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useNotifications } from '@/hooks/useNotifications'; +import { selectActiveMembershipPlanName } from '@/store/membershipSlice'; import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/store/userSlice'; import { getItem, setItem } from '@/utils/kvStore'; import { log } from '@/utils/logger'; import { getNotificationEnabled, setNotificationEnabled as saveNotificationEnabled } from '@/utils/userPreferences'; import { Ionicons } from '@expo/vector-icons'; import { useFocusEffect } from '@react-navigation/native'; +import dayjs from 'dayjs'; import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; @@ -57,6 +59,7 @@ export default function PersonalScreen() { // 直接使用 Redux 中的用户信息,避免重复状态管理 const userProfile = useAppSelector((state) => state.user.profile); + const activeMembershipPlanName = useAppSelector(selectActiveMembershipPlanName); // 页面聚焦时获取最新用户信息 @@ -270,6 +273,72 @@ export default function PersonalScreen() { ); + const VipMembershipCard = () => { + const fallbackProfile = userProfile as Record; + const fallbackExpire = ['membershipExpiration', 'vipExpiredAt', 'vipExpiresAt', 'vipExpireDate'] + .map((key) => fallbackProfile[key]) + .find((value): value is string => typeof value === 'string' && value.trim().length > 0); + + const rawExpireDate = userProfile.membershipExpiration + + let formattedExpire = '长期有效'; + if (typeof rawExpireDate === 'string' && rawExpireDate.trim().length > 0) { + const parsed = dayjs(rawExpireDate); + formattedExpire = parsed.isValid() ? parsed.format('YYYY年MM月DD日') : rawExpireDate; + } + + const planName = + activeMembershipPlanName?.trim() || + userProfile.vipPlanName?.trim() || + 'VIP 会员'; + + return ( + + + + + + + + + 尊享会员 + + {planName} + + + + + + + + + 会员有效期 + + + {formattedExpire} + + + { + void handleMembershipPress(); + }} + > + + 更改会员套餐 + + + + + ); + }; + // 数据统计部分 const StatsSection = () => ( @@ -427,7 +496,7 @@ export default function PersonalScreen() { showsVerticalScrollIndicator={false} > - {userProfile.isVip ? null : } + {userProfile.isVip ? : } {/* void; } -interface MembershipPlan { - id: string; - fallbackTitle: string; - subtitle: string; - type: 'weekly' | 'quarterly' | 'lifetime'; - recommended?: boolean; - tag?: string; - originalPrice?: string; -} - -const DEFAULT_PLANS: MembershipPlan[] = [ - { - 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', - }, -]; - // RevenueCat 在 JS SDK 中以 string[] 返回 activeSubscriptions,但保持健壮性以防类型变化 -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).filter( - (value): value is string => typeof value === 'string' - ); - } - - return []; -}; - // 权限类型枚举 type PermissionType = 'exclusive' | 'limited' | 'unlimited'; @@ -151,37 +109,9 @@ const BENEFIT_COMPARISON: BenefitItem[] = [ vipText: '基础提醒' } }, - { - title: 'AI教练对话', - description: '与AI健康教练进行个性化对话咨询', - vip: { - type: 'unlimited', - text: '无限次对话', - vipText: '深度分析' - }, - regular: { - type: 'limited', - text: '有限次对话', - vipText: '每日10次' - } - }, - { - title: '体态评估', - description: '通过照片分析体态问题并提供改善建议', - vip: { - type: 'exclusive', - text: '完全支持', - vipText: '专业评估' - }, - regular: { - type: 'exclusive', - text: '不可使用', - vipText: '不可使用' - } - } ]; -const PLAN_STYLE_CONFIG: Record = { +const PLAN_STYLE_CONFIG: Record = { lifetime: { gradient: ['#FFF1DD', '#FFE8FA'] as const, accent: '#7B2CBF', @@ -221,6 +151,7 @@ const getPermissionIcon = (type: PermissionType, isVip: boolean) => { }; export function MembershipModal({ visible, onClose, onPurchaseSuccess }: MembershipModalProps) { + const dispatch = useAppDispatch(); const [selectedProduct, setSelectedProduct] = useState(null); const [loading, setLoading] = useState(false); const [restoring, setRestoring] = useState(false); @@ -238,7 +169,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members const getTipsContent = (product: PurchasesStoreProduct | null): string => { if (!product) return ''; - const plan = DEFAULT_PLANS.find(item => item.id === product.identifier); + const plan = MEMBERSHIP_PLAN_META.find(item => item.id === product.identifier); if (!plan) { return ''; } @@ -299,9 +230,9 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members allOfferingsCount: Object.keys(offerings.all).length, }); - const packages = offerings.current?.availablePackages ?? []; + const productsToUse = extractMembershipProductsFromOfferings(offerings); - if (packages.length === 0) { + if (productsToUse.length === 0) { log.warn('没有找到可用的产品套餐', { hasCurrentOffering: offerings.current !== null, packagesLength: offerings.current?.availablePackages.length || 0, @@ -315,17 +246,6 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members return; } - const matchedProducts = packages - .map(pkg => pkg.product) - .filter(product => DEFAULT_PLANS.some(plan => plan.id === product.identifier)); - - const orderedProducts = DEFAULT_PLANS - .map(plan => matchedProducts.find(product => product.identifier === plan.id)) - .filter((product): product is PurchasesStoreProduct => Boolean(product)); - - const fallbackProducts = packages.map(pkg => pkg.product); - const productsToUse = orderedProducts.length > 0 ? orderedProducts : fallbackProducts; - log.info('productsToUse', productsToUse) setProducts(productsToUse); @@ -394,6 +314,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members // 延迟一点时间,确保购买流程完全完成 setTimeout(async () => { + void dispatch(fetchMembershipData()); // 刷新用户信息 // await refreshUserInfo(); @@ -489,7 +410,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members let selectedProduct: PurchasesStoreProduct | null = null; // 优先级:终身会员 > 季度会员 > 周会员 - const priorityOrder = DEFAULT_PLANS.map(plan => plan.id); + const priorityOrder = MEMBERSHIP_PLAN_META.map(plan => plan.id); // 按照优先级查找 for (const priorityProductId of priorityOrder) { @@ -747,6 +668,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members restoredProducts, restoredProductsCount: restoredProducts.length }); + void dispatch(fetchMembershipData()); try { // 调用后台服务接口进行票据匹配 @@ -856,16 +778,11 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members const renderPlanCard = (product: PurchasesStoreProduct) => { - const plan = DEFAULT_PLANS.find(p => p.id === product.identifier); - - if (!plan) { - return null; - } - + const planMeta = getPlanMetaById(product.identifier); const isSelected = selectedProduct === product; - const displayTitle = product.title || plan.fallbackTitle; + const displayTitle = resolvePlanDisplayName(product, planMeta); const priceLabel = product.priceString || ''; - const styleConfig = PLAN_STYLE_CONFIG[plan.type]; + const styleConfig = planMeta ? PLAN_STYLE_CONFIG[planMeta.type] : undefined; return ( - {plan.tag && ( + {planMeta?.tag && ( - {plan.tag} + {planMeta.tag} )} {displayTitle} @@ -902,13 +819,13 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members {priceLabel || '--'} - {plan.originalPrice && ( - {plan.originalPrice} + {planMeta?.originalPrice && ( + {planMeta.originalPrice} )} - {plan.subtitle} + {planMeta?.subtitle ?? ''} diff --git a/contexts/MembershipModalContext.tsx b/contexts/MembershipModalContext.tsx index 381b659..a25962f 100644 --- a/contexts/MembershipModalContext.tsx +++ b/contexts/MembershipModalContext.tsx @@ -2,7 +2,8 @@ import React, { createContext, useCallback, useContext, useEffect, useMemo, useS import Purchases from 'react-native-purchases'; import { MembershipModal } from '@/components/model/MembershipModal'; -import { useAppSelector } from '@/hooks/redux'; +import { useAppDispatch, useAppSelector } from '@/hooks/redux'; +import { fetchMembershipData } from '@/store/membershipSlice'; import { selectUserProfile } from '@/store/userSlice'; import { logger } from '@/utils/logger'; @@ -18,6 +19,7 @@ interface MembershipModalContextValue { const MembershipModalContext = createContext(null); export function MembershipModalProvider({ children }: { children: React.ReactNode }) { + const dispatch = useAppDispatch(); const [visible, setVisible] = useState(false); const [pendingSuccessCallback, setPendingSuccessCallback] = useState<(() => void) | undefined>(); const [isInitialized, setIsInitialized] = useState(false); @@ -46,6 +48,7 @@ export function MembershipModalProvider({ children }: { children: React.ReactNod await Purchases.configure(configOptions); setIsInitialized(true); logger.info('[MembershipModalProvider] RevenueCat SDK 初始化成功'); + dispatch(fetchMembershipData()); } } catch (error) { logger.error('[MembershipModalProvider] RevenueCat SDK 初始化失败:', error); @@ -54,7 +57,7 @@ export function MembershipModalProvider({ children }: { children: React.ReactNod }; initializeRevenueCat(); - }, [isInitialized, userProfile?.id]); + }, [dispatch, isInitialized, userProfile?.id]); // 监听用户登录状态变化,在用户登录后更新RevenueCat的用户标识 useEffect(() => { @@ -70,14 +73,17 @@ export function MembershipModalProvider({ children }: { children: React.ReactNod console.log('[MembershipModalProvider] 更新RevenueCat用户标识:', userProfile.id); await Purchases.logIn(userProfile.id); } + dispatch(fetchMembershipData()); } catch (error) { console.error('[MembershipModalProvider] 更新RevenueCat用户标识失败:', error); } + } else if (isInitialized && !userProfile?.id) { + dispatch(fetchMembershipData()); } }; updateRevenueCatUser(); - }, [userProfile?.id, isInitialized]); + }, [dispatch, userProfile?.id, isInitialized]); const openMembershipModal = useCallback((options?: MembershipModalOptions) => { setPendingSuccessCallback(() => options?.onPurchaseSuccess); diff --git a/services/membership.ts b/services/membership.ts new file mode 100644 index 0000000..5aa5af6 --- /dev/null +++ b/services/membership.ts @@ -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).filter( + (value): value is string => typeof value === 'string', + ); + } + + return []; +}; + +export const collectActiveProductIdentifiers = ( + customerInfo: CustomerInfo, +): string[] => { + const collected = new Set(); + + 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 }; +}; diff --git a/store/index.ts b/store/index.ts index df8d1a3..234fb7a 100644 --- a/store/index.ts +++ b/store/index.ts @@ -5,6 +5,7 @@ import circumferenceReducer from './circumferenceSlice'; import exerciseLibraryReducer from './exerciseLibrarySlice'; import foodLibraryReducer from './foodLibrarySlice'; import foodRecognitionReducer from './foodRecognitionSlice'; +import membershipReducer from './membershipSlice'; import goalsReducer from './goalsSlice'; import healthReducer from './healthSlice'; import fastingReducer, { @@ -108,6 +109,7 @@ export const store = configureStore({ exerciseLibrary: exerciseLibraryReducer, foodLibrary: foodLibraryReducer, foodRecognition: foodRecognitionReducer, + membership: membershipReducer, workout: workoutReducer, water: waterReducer, fasting: fastingReducer, diff --git a/store/membershipSlice.ts b/store/membershipSlice.ts new file mode 100644 index 0000000..af7a51d --- /dev/null +++ b/store/membershipSlice.ts @@ -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; diff --git a/store/userSlice.ts b/store/userSlice.ts index 416a8b3..1e73421 100644 --- a/store/userSlice.ts +++ b/store/userSlice.ts @@ -78,6 +78,8 @@ export type UserProfile = { isVip?: boolean; freeUsageCount?: number; maxUsageCount?: number; + membershipExpiration?: string | null; + vipPlanName?: string; chestCircumference?: number; // 胸围 waistCircumference?: number; // 腰围 upperHipCircumference?: number; // 上臀围 @@ -431,4 +433,4 @@ export const selectUserAge = createSelector( } ); -export default userSlice.reducer; \ No newline at end of file +export default userSlice.reducer;