Add guest auth and save-account flow

Introduce anonymous sign-in with in-app saving, guest recovery warnings, paywall prompts, and admin guest labeling.
This commit is contained in:
jjbrunton committed 2026-01-17 17:55:06 +00:00
1 parent ce68d57a49
commit 349f460e15
14 files changed
+1236 -176

No files matched your search

@@ -15,6 +15,7 @@ describe('authStore', () => {
partner: null,
isLoading: true,
isAuthenticated: false,
isAnonymous: false,
} as any);
useMatchStore.setState({ matches: [{ id: 'm1' }], newMatchesCount: 1 } as any);
@@ -31,9 +32,35 @@ describe('authStore', () => {
const state = useAuthStore.getState();
expect(state.user).toBeNull();
expect(state.isAuthenticated).toBe(false);
expect(state.isAnonymous).toBe(false);
expect(state.isLoading).toBe(false);
});
it('sets isAnonymous from auth user', async () => {
(supabase.auth.getSession as jest.Mock).mockResolvedValueOnce({
data: { session: { user: { id: 'me' } } },
});
(supabase.auth.getUser as jest.Mock).mockResolvedValueOnce({
data: { user: { id: 'me', is_anonymous: true } },
error: null,
});
const profileQuery: any = {
select: jest.fn(() => profileQuery),
eq: jest.fn(() => profileQuery),
maybeSingle: jest.fn(async () => ({ data: { id: 'me', couple_id: null } })),
};
(supabase.from as jest.Mock).mockReturnValue(profileQuery);
await useAuthStore.getState().fetchUser();
const state = useAuthStore.getState();
expect(state.isAuthenticated).toBe(true);
expect(state.isAnonymous).toBe(true);
});
it('clears stores and signs out when session invalid', async () => {
(supabase.auth.getSession as jest.Mock).mockResolvedValueOnce({
data: { session: { user: { id: 'me' } } },
@@ -50,51 +77,9 @@ describe('authStore', () => {
expect(useAuthStore.getState().user).toBeNull();
expect(useAuthStore.getState().isAuthenticated).toBe(false);
expect(useAuthStore.getState().isAnonymous).toBe(false);
expect(useMatchStore.getState().matches).toEqual([]);
expect(usePacksStore.getState().enabledPackIds).toEqual([]);
expect(useMessageStore.getState().unreadCount).toBe(0);
expect(useMessageStore.getState().activeMatchId).toBeNull();
expect(useSubscriptionStore.getState().subscription.isProUser).toBe(false);
expect(supabase.auth.signOut).toHaveBeenCalled();
});
it('fetches couple and partner when couple_id exists', async () => {
useAuthStore.setState({ user: { id: 'me', couple_id: 'c1' } } as any);
const couplesQuery: any = {
select: jest.fn(() => couplesQuery),
eq: jest.fn(() => couplesQuery),
maybeSingle: jest.fn(async () => ({ data: { id: 'c1' } })),
};
const partnerQuery: any = {
select: jest.fn(() => partnerQuery),
eq: jest.fn(() => partnerQuery),
neq: jest.fn(() => partnerQuery),
maybeSingle: jest.fn(async () => ({ data: { id: 'partner' } })),
};
(supabase.from as jest.Mock).mockImplementation((table: string) => {
if (table === 'couples') return couplesQuery;
if (table === 'profiles') return partnerQuery;
return couplesQuery;
});
await useAuthStore.getState().fetchCouple();
expect(useAuthStore.getState().couple).toEqual({ id: 'c1' });
expect(useAuthStore.getState().partner).toEqual({ id: 'partner' });
});
it('signOut clears local state even if Supabase fails', async () => {
(supabase.auth.signOut as jest.Mock).mockRejectedValueOnce(new Error('network'));
await useAuthStore.getState().signOut();
expect(useAuthStore.getState().user).toBeNull();
expect(useAuthStore.getState().isAuthenticated).toBe(false);
expect(useAuthStore.getState().isAnonymous).toBe(false);
expect(useAuthStore.getState().isLoading).toBe(false);
});
});