Ship Expo Router screens, seeded stores, and polished UX so auth, shop, checkout, prescriptions, loyalty, family, and account processes work end-to-end offline. Co-authored-by: Cursor <cursoragent@cursor.com>
110 lines
3.4 KiB
TypeScript
110 lines
3.4 KiB
TypeScript
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
import { create } from "zustand";
|
|
import { createJSONStorage, persist } from "zustand/middleware";
|
|
|
|
export type ProfileMode = "personal" | "corporate";
|
|
|
|
export type OrgMembership = {
|
|
orgId: string;
|
|
orgName: string;
|
|
planName: string;
|
|
remainingAllowance: number;
|
|
monthlyCap: number;
|
|
};
|
|
|
|
type ProfileState = {
|
|
mode: ProfileMode;
|
|
memberships: OrgMembership[];
|
|
activeOrgId: string | null;
|
|
setMode: (mode: ProfileMode) => void;
|
|
setActiveOrg: (orgId: string | null) => void;
|
|
joinOrg: (code: string) => { ok: boolean; message: string };
|
|
activeMembership: () => OrgMembership | null;
|
|
quoteSplit: (subtotal: number) => { covered: number; selfPay: number };
|
|
consumeAllowance: (amount: number) => void;
|
|
};
|
|
|
|
const DEMO_ORGS: Record<string, OrgMembership> = {
|
|
"GISHEN-BANK": {
|
|
orgId: "org_bank",
|
|
orgName: "Demo Bank Ethiopia",
|
|
planName: "Staff",
|
|
remainingAllowance: 2500,
|
|
monthlyCap: 5000,
|
|
},
|
|
"GISHEN-NGO": {
|
|
orgId: "org_ngo",
|
|
orgName: "Demo NGO",
|
|
planName: "Standard",
|
|
remainingAllowance: 1200,
|
|
monthlyCap: 3000,
|
|
},
|
|
};
|
|
|
|
const seededMembership = { ...DEMO_ORGS["GISHEN-BANK"] };
|
|
|
|
export const useProfileModeStore = create<ProfileState>()(
|
|
persist(
|
|
(set, get) => ({
|
|
mode: "personal",
|
|
memberships: [seededMembership],
|
|
activeOrgId: null,
|
|
setMode: (mode) => set({ mode }),
|
|
setActiveOrg: (orgId) =>
|
|
set({ activeOrgId: orgId, mode: orgId ? "corporate" : "personal" }),
|
|
joinOrg: (code) => {
|
|
const key = code.trim().toUpperCase();
|
|
const org = DEMO_ORGS[key];
|
|
if (!org) return { ok: false, message: "Invalid join code" };
|
|
const memberships = get().memberships;
|
|
if (memberships.some((m) => m.orgId === org.orgId)) {
|
|
set({ activeOrgId: org.orgId, mode: "corporate" });
|
|
return { ok: true, message: "Already a member — switched to org" };
|
|
}
|
|
set({
|
|
memberships: [...memberships, { ...org }],
|
|
activeOrgId: org.orgId,
|
|
mode: "corporate",
|
|
});
|
|
return { ok: true, message: `Joined ${org.orgName}` };
|
|
},
|
|
activeMembership: () => {
|
|
const { activeOrgId, memberships, mode } = get();
|
|
if (mode !== "corporate" || !activeOrgId) return null;
|
|
return memberships.find((m) => m.orgId === activeOrgId) ?? null;
|
|
},
|
|
quoteSplit: (subtotal) => {
|
|
const m = get().activeMembership();
|
|
if (!m) return { covered: 0, selfPay: subtotal };
|
|
const covered = Math.min(subtotal, m.remainingAllowance);
|
|
return { covered, selfPay: Math.max(0, subtotal - covered) };
|
|
},
|
|
consumeAllowance: (amount) => {
|
|
const { activeOrgId, memberships } = get();
|
|
if (!activeOrgId || amount <= 0) return;
|
|
set({
|
|
memberships: memberships.map((m) =>
|
|
m.orgId === activeOrgId
|
|
? {
|
|
...m,
|
|
remainingAllowance: Math.max(0, m.remainingAllowance - amount),
|
|
}
|
|
: m
|
|
),
|
|
});
|
|
},
|
|
}),
|
|
{
|
|
name: "gishen-profile-mode",
|
|
storage: createJSONStorage(() => AsyncStorage),
|
|
version: 2,
|
|
migrate: (persisted: unknown) => {
|
|
const state = (persisted ?? {}) as Partial<ProfileState>;
|
|
return !state.memberships?.length
|
|
? { ...state, memberships: [seededMembership] }
|
|
: state;
|
|
},
|
|
}
|
|
)
|
|
);
|