@mr.dj2u/library-registry 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +3 -0
  2. package/assets/mds/env/convex.env.example +1 -0
  3. package/assets/mds/env/firebase.env.example +6 -0
  4. package/assets/mds/env/supabase.env.example +3 -0
  5. package/assets/mds/project/auth-base.md +17 -0
  6. package/assets/mds/project/auth-convex.md +33 -0
  7. package/assets/mds/project/auth-firebase.md +22 -0
  8. package/assets/mds/project/auth-supabase.md +46 -0
  9. package/assets/mds/src/app/(auth)/reset-password.tsx +5 -0
  10. package/assets/mds/src/app/(auth)/sign-in.tsx +5 -0
  11. package/assets/mds/src/app/(auth)/sign-up.tsx +5 -0
  12. package/assets/mds/src/app/legal/updates.tsx +1 -0
  13. package/assets/mds/src/app/onboarding/complete.tsx +2 -0
  14. package/assets/mds/src/app/onboarding/features.tsx +1 -0
  15. package/assets/mds/src/app/onboarding/legal.tsx +2 -0
  16. package/assets/mds/src/app/onboarding.tsx +1 -1
  17. package/assets/mds/src/app/settings.tsx +10 -1
  18. package/assets/mds/src/db/adapter.ts +195 -0
  19. package/assets/mds/src/db/firebase.ts +77 -0
  20. package/assets/mds/src/db/index.firebase.ts +19 -0
  21. package/assets/mds/src/db/index.supabase.ts +22 -0
  22. package/assets/mds/src/db/supabase.ts +337 -0
  23. package/assets/mds/src/features/auth/adapters/base-auth-adapter.tsx +82 -0
  24. package/assets/mds/src/features/auth/adapters/convex-auth-adapter.tsx +156 -0
  25. package/assets/mds/src/features/auth/adapters/firebase-auth-adapter.tsx +147 -0
  26. package/assets/mds/src/features/auth/adapters/supabase-auth-adapter.tsx +166 -0
  27. package/assets/mds/src/features/auth/auth-guard-logic.ts +19 -0
  28. package/assets/mds/src/features/auth/auth-guard.tsx +77 -0
  29. package/assets/mds/src/features/auth/auth-provider.tsx +60 -0
  30. package/assets/mds/src/features/auth/auth-screen.tsx +265 -0
  31. package/assets/mds/src/features/auth/auth-types.ts +45 -0
  32. package/assets/mds/src/features/exposition/data-screen.tsx +4 -2
  33. package/assets/mds/src/features/exposition/expo-sdk-56-screen.tsx +119 -76
  34. package/assets/mds/src/features/exposition/exposition-screen.tsx +10 -11
  35. package/assets/mds/src/features/exposition/stylist-screen.tsx +62 -11
  36. package/assets/mds/src/features/legal/legal-acceptance-adapter.ts +190 -0
  37. package/assets/mds/src/features/legal/legal-acceptance-config.ts +79 -0
  38. package/assets/mds/src/features/legal/legal-agreement-screen.tsx +9 -4
  39. package/assets/mds/src/features/legal/legal-document-modal.tsx +7 -3
  40. package/assets/mds/src/features/legal/legal-document-view.tsx +1 -0
  41. package/assets/mds/src/features/legal/legal-documents.ts +17 -3
  42. package/assets/mds/src/features/legal/legal-update-screen.tsx +297 -0
  43. package/assets/mds/src/features/legal/use-legal-acceptance.ts +1 -38
  44. package/assets/mds/src/features/onboarding/complete-screen.tsx +138 -0
  45. package/assets/mds/src/features/onboarding/features-screen.tsx +129 -0
  46. package/assets/mds/src/features/onboarding/legal-review-screen.tsx +242 -0
  47. package/assets/mds/src/features/onboarding/onboarding-config-with-legal.ts +104 -0
  48. package/assets/mds/src/features/onboarding/onboarding-config.ts +104 -0
  49. package/assets/mds/src/features/onboarding/onboarding-persistence-sync.tsx +1 -0
  50. package/assets/mds/src/features/onboarding/welcome-screen.tsx +130 -0
  51. package/assets/mds/src/features/onboarding-state/adapters/memory-onboarding-state-adapter.ts +15 -0
  52. package/assets/mds/src/features/onboarding-state/adapters/supabase-onboarding-state-adapter.ts +45 -0
  53. package/assets/mds/src/features/onboarding-state/adapters/zustand-onboarding-state-adapter.ts +16 -0
  54. package/assets/mds/src/features/onboarding-state/adapters/zustand-supabase-onboarding-state-adapter.ts +53 -0
  55. package/assets/mds/src/features/onboarding-state/onboarding-state-core.ts +108 -0
  56. package/assets/mds/src/features/onboarding-state/onboarding-state-memory.ts +106 -0
  57. package/assets/mds/src/features/onboarding-state/onboarding-state-supabase.ts +246 -0
  58. package/assets/mds/src/features/onboarding-state/onboarding-state-types.ts +64 -0
  59. package/assets/mds/src/features/onboarding-state/onboarding-state-zustand-supabase.ts +120 -0
  60. package/assets/mds/src/features/onboarding-state/onboarding-state-zustand.ts +71 -0
  61. package/assets/mds/src/features/onboarding-state/onboarding-state.ts +22 -0
  62. package/assets/mds/src/features/onboarding-state/onboarding-store.ts +39 -0
  63. package/assets/mds/src/features/settings/settings-screen-logic.ts +122 -0
  64. package/assets/mds/src/features/settings/settings-screen.tsx +274 -28
  65. package/assets/mds/src/services/convex.ts +40 -0
  66. package/assets/mds/src/services/firebase.ts +70 -0
  67. package/assets/mds/src/services/supabase.ts +50 -0
  68. package/assets/mds/src/theme/color-utils.ts +42 -0
  69. package/assets/mds/src/theme/provider.tsx +15 -5
  70. package/assets/mds/src/types/database.ts +13 -0
  71. package/assets/mds/supabase/migrations/0001_mds_auth_onboarding.sql +107 -0
  72. package/dist/catalog.d.ts.map +1 -1
  73. package/dist/catalog.js +444 -29
  74. package/dist/catalog.js.map +1 -1
  75. package/dist/types.d.ts +5 -1
  76. package/dist/types.d.ts.map +1 -1
  77. package/package.json +1 -1
  78. package/assets/mds/src/app/onboarding/account-setup.tsx +0 -1
  79. package/assets/mds/src/app/onboarding/agreement.tsx +0 -1
  80. package/assets/mds/src/app/onboarding/terms.tsx +0 -1
  81. package/assets/mds/src/features/onboarding/account-setup-screen.tsx +0 -54
  82. package/assets/mds/src/features/onboarding/agreement-screen.tsx +0 -6
  83. package/assets/mds/src/features/onboarding/components/legal-document-view.tsx +0 -103
  84. package/assets/mds/src/features/onboarding/legal-documents.ts +0 -80
  85. package/assets/mds/src/features/onboarding/onboarding-screen.tsx +0 -167
  86. package/assets/mds/src/features/onboarding/terms-screen.tsx +0 -6
@@ -0,0 +1,147 @@
1
+ import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react';
2
+ import {
3
+ createUserWithEmailAndPassword,
4
+ onAuthStateChanged,
5
+ sendPasswordResetEmail,
6
+ signInWithEmailAndPassword,
7
+ signOut as firebaseSignOut,
8
+ } from 'firebase/auth';
9
+
10
+ import { firebaseAuth, getFirebaseAuth } from '../../services/firebase';
11
+
12
+ import type { AuthActionInput, AuthActionResult, AuthAdapter, AuthSession, AuthUser } from './auth-types';
13
+ import type { User } from 'firebase/auth';
14
+
15
+ function mapFirebaseUser(user: User): AuthUser {
16
+ return {
17
+ id: user.uid,
18
+ email: user.email,
19
+ name: user.displayName,
20
+ provider: 'firebase',
21
+ metadata: {
22
+ emailVerified: user.emailVerified,
23
+ phoneNumber: user.phoneNumber,
24
+ photoURL: user.photoURL,
25
+ },
26
+ };
27
+ }
28
+
29
+ function mapFirebaseSession(user: User | null): AuthSession | null {
30
+ if (!user) return null;
31
+ return {
32
+ user: mapFirebaseUser(user),
33
+ };
34
+ }
35
+
36
+ function authError(error: unknown): AuthActionResult {
37
+ const message = error instanceof Error ? error.message : String(error);
38
+ return { ok: false, error: message };
39
+ }
40
+
41
+ export function AuthAdapterProvider({ children }: { children: ReactNode }) {
42
+ return <>{children}</>;
43
+ }
44
+
45
+ export function useAuthAdapter(): AuthAdapter {
46
+ const configurationError = firebaseAuth
47
+ ? null
48
+ : 'Set the EXPO_PUBLIC_FIREBASE_* variables before using Firebase auth.';
49
+ const [session, setSession] = useState<AuthSession | null>(null);
50
+ const [isLoading, setIsLoading] = useState(() => Boolean(firebaseAuth));
51
+ const [error, setError] = useState<string | null>(configurationError);
52
+
53
+ useEffect(() => {
54
+ if (!firebaseAuth) {
55
+ return;
56
+ }
57
+ return onAuthStateChanged(
58
+ firebaseAuth,
59
+ (user) => {
60
+ setSession(mapFirebaseSession(user));
61
+ setError(null);
62
+ setIsLoading(false);
63
+ },
64
+ (nextError) => {
65
+ setError(nextError.message);
66
+ setIsLoading(false);
67
+ },
68
+ );
69
+ }, []);
70
+
71
+ const signInWithEmailPassword = useCallback(async ({ email, password }: AuthActionInput) => {
72
+ try {
73
+ await signInWithEmailAndPassword(getFirebaseAuth(), email.trim(), password);
74
+ return { ok: true } satisfies AuthActionResult;
75
+ } catch (error) {
76
+ return authError(error);
77
+ }
78
+ }, []);
79
+
80
+ const signUpWithEmailPassword = useCallback(async ({ email, password }: AuthActionInput) => {
81
+ try {
82
+ await createUserWithEmailAndPassword(getFirebaseAuth(), email.trim(), password);
83
+ return { ok: true } satisfies AuthActionResult;
84
+ } catch (error) {
85
+ return authError(error);
86
+ }
87
+ }, []);
88
+
89
+ const requestPasswordReset = useCallback(async (email: string) => {
90
+ try {
91
+ await sendPasswordResetEmail(getFirebaseAuth(), email.trim());
92
+ return { ok: true, message: 'Password reset email sent.' } satisfies AuthActionResult;
93
+ } catch (error) {
94
+ return authError(error);
95
+ }
96
+ }, []);
97
+
98
+ const signOut = useCallback(async () => {
99
+ try {
100
+ await firebaseSignOut(getFirebaseAuth());
101
+ return { ok: true } satisfies AuthActionResult;
102
+ } catch (error) {
103
+ return authError(error);
104
+ }
105
+ }, []);
106
+
107
+ const refreshSession = useCallback(async () => {
108
+ if (!firebaseAuth) {
109
+ setError(configurationError);
110
+ setSession(null);
111
+ setIsLoading(false);
112
+ return;
113
+ }
114
+ setIsLoading(true);
115
+ try {
116
+ await firebaseAuth.currentUser?.reload();
117
+ setSession(mapFirebaseSession(firebaseAuth.currentUser));
118
+ setError(null);
119
+ } catch (error) {
120
+ setError(error instanceof Error ? error.message : String(error));
121
+ } finally {
122
+ setIsLoading(false);
123
+ }
124
+ }, [configurationError]);
125
+
126
+ return useMemo<AuthAdapter>(
127
+ () => ({
128
+ provider: 'firebase',
129
+ state: { isLoading, session, error },
130
+ refreshSession,
131
+ signInWithEmailPassword,
132
+ signUpWithEmailPassword,
133
+ requestPasswordReset,
134
+ signOut,
135
+ }),
136
+ [
137
+ error,
138
+ isLoading,
139
+ refreshSession,
140
+ requestPasswordReset,
141
+ session,
142
+ signInWithEmailPassword,
143
+ signOut,
144
+ signUpWithEmailPassword,
145
+ ],
146
+ );
147
+ }
@@ -0,0 +1,166 @@
1
+ import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react';
2
+
3
+ import { getSupabaseClient, supabase } from '../../services/supabase';
4
+
5
+ import type {
6
+ AuthActionInput,
7
+ AuthActionResult,
8
+ AuthAdapter,
9
+ AuthSession,
10
+ AuthUser,
11
+ } from './auth-types';
12
+ import type { Session, User } from '@supabase/supabase-js';
13
+
14
+ function mapSupabaseUser(user: User): AuthUser {
15
+ return {
16
+ id: user.id,
17
+ email: user.email,
18
+ name:
19
+ typeof user.user_metadata?.name === 'string'
20
+ ? user.user_metadata.name
21
+ : typeof user.user_metadata?.full_name === 'string'
22
+ ? user.user_metadata.full_name
23
+ : null,
24
+ provider: 'supabase',
25
+ metadata: user.user_metadata,
26
+ };
27
+ }
28
+
29
+ function mapSupabaseSession(session: Session | null): AuthSession | null {
30
+ if (!session?.user) return null;
31
+ return {
32
+ user: mapSupabaseUser(session.user),
33
+ accessToken: session.access_token,
34
+ };
35
+ }
36
+
37
+ function authError(error: unknown): AuthActionResult {
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ return { ok: false, error: message };
40
+ }
41
+
42
+ export function AuthAdapterProvider({ children }: { children: ReactNode }) {
43
+ return <>{children}</>;
44
+ }
45
+
46
+ export function useAuthAdapter(): AuthAdapter {
47
+ const configurationError = supabase
48
+ ? null
49
+ : 'Set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY before using Supabase auth. EXPO_PUBLIC_SUPABASE_KEY and EXPO_PUBLIC_SUPABASE_ANON_KEY are accepted fallbacks.';
50
+ const [session, setSession] = useState<AuthSession | null>(null);
51
+ const [isLoading, setIsLoading] = useState(() => Boolean(supabase));
52
+ const [error, setError] = useState<string | null>(configurationError);
53
+
54
+ useEffect(() => {
55
+ let mounted = true;
56
+ if (!supabase) {
57
+ return;
58
+ }
59
+
60
+ void supabase.auth.getSession().then(({ data, error: sessionError }) => {
61
+ if (!mounted) return;
62
+ setError(sessionError?.message ?? null);
63
+ setSession(mapSupabaseSession(data.session));
64
+ setIsLoading(false);
65
+ });
66
+
67
+ const {
68
+ data: { subscription },
69
+ } = supabase.auth.onAuthStateChange((_event, nextSession) => {
70
+ setSession(mapSupabaseSession(nextSession));
71
+ setError(null);
72
+ setIsLoading(false);
73
+ });
74
+
75
+ return () => {
76
+ mounted = false;
77
+ subscription.unsubscribe();
78
+ };
79
+ }, []);
80
+
81
+ const signInWithEmailPassword = useCallback(async ({ email, password }: AuthActionInput) => {
82
+ try {
83
+ const client = getSupabaseClient();
84
+ const { error } = await client.auth.signInWithPassword({ email, password });
85
+ if (error) return { ok: false, error: error.message };
86
+ return { ok: true } satisfies AuthActionResult;
87
+ } catch (error) {
88
+ return authError(error);
89
+ }
90
+ }, []);
91
+
92
+ const signUpWithEmailPassword = useCallback(async ({ email, password }: AuthActionInput) => {
93
+ try {
94
+ const client = getSupabaseClient();
95
+ const { data, error } = await client.auth.signUp({ email, password });
96
+ if (error) return { ok: false, error: error.message };
97
+ if (data.user && !data.session) {
98
+ return {
99
+ ok: true,
100
+ message: 'Check your email to confirm this account, then sign in.',
101
+ } satisfies AuthActionResult;
102
+ }
103
+ return { ok: true } satisfies AuthActionResult;
104
+ } catch (error) {
105
+ return authError(error);
106
+ }
107
+ }, []);
108
+
109
+ const requestPasswordReset = useCallback(async (email: string) => {
110
+ try {
111
+ const client = getSupabaseClient();
112
+ const { error } = await client.auth.resetPasswordForEmail(email.trim());
113
+ if (error) return { ok: false, error: error.message };
114
+ return { ok: true, message: 'Password reset email sent.' } satisfies AuthActionResult;
115
+ } catch (error) {
116
+ return authError(error);
117
+ }
118
+ }, []);
119
+
120
+ const signOut = useCallback(async () => {
121
+ try {
122
+ const client = getSupabaseClient();
123
+ const { error } = await client.auth.signOut();
124
+ if (error) return { ok: false, error: error.message };
125
+ return { ok: true } satisfies AuthActionResult;
126
+ } catch (error) {
127
+ return authError(error);
128
+ }
129
+ }, []);
130
+
131
+ const refreshSession = useCallback(async () => {
132
+ if (!supabase) {
133
+ setError(configurationError);
134
+ setSession(null);
135
+ setIsLoading(false);
136
+ return;
137
+ }
138
+ setIsLoading(true);
139
+ const { data, error: sessionError } = await supabase.auth.getSession();
140
+ setError(sessionError?.message ?? null);
141
+ setSession(mapSupabaseSession(data.session));
142
+ setIsLoading(false);
143
+ }, [configurationError]);
144
+
145
+ return useMemo<AuthAdapter>(
146
+ () => ({
147
+ provider: 'supabase',
148
+ state: { isLoading, session, error },
149
+ refreshSession,
150
+ signInWithEmailPassword,
151
+ signUpWithEmailPassword,
152
+ requestPasswordReset,
153
+ signOut,
154
+ }),
155
+ [
156
+ error,
157
+ isLoading,
158
+ refreshSession,
159
+ requestPasswordReset,
160
+ session,
161
+ signInWithEmailPassword,
162
+ signOut,
163
+ signUpWithEmailPassword,
164
+ ]
165
+ );
166
+ }
@@ -0,0 +1,19 @@
1
+ import type { AuthAdapterState } from './auth-types';
2
+
3
+ export type AuthGuardDecision = 'loading' | 'authorized' | 'fallback' | 'redirect';
4
+
5
+ export function resolveAuthGuardDecision(
6
+ state: AuthAdapterState,
7
+ options: { fallback?: unknown; fallbackHref?: string }
8
+ ): AuthGuardDecision {
9
+ if (state.isLoading) {
10
+ return 'loading';
11
+ }
12
+ if (state.session?.user) {
13
+ return 'authorized';
14
+ }
15
+ if (options.fallback !== undefined && options.fallback !== null) {
16
+ return 'fallback';
17
+ }
18
+ return options.fallbackHref ? 'redirect' : 'fallback';
19
+ }
@@ -0,0 +1,77 @@
1
+ import { Redirect, useFocusEffect } from 'expo-router';
2
+ import { useCallback, useEffect, type ReactNode } from 'react';
3
+ import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
4
+
5
+ import { useAppTheme } from '../../theme/provider';
6
+ import { resolveAuthGuardDecision, type AuthGuardDecision } from './auth-guard-logic';
7
+
8
+ import type { AuthAdapter } from './auth-types';
9
+
10
+ export interface AuthGuardProps {
11
+ auth: AuthAdapter;
12
+ children: ReactNode;
13
+ fallbackHref?: string;
14
+ fallback?: ReactNode;
15
+ }
16
+
17
+ export function AuthGuard({
18
+ auth,
19
+ children,
20
+ fallbackHref = '/sign-in',
21
+ fallback,
22
+ }: AuthGuardProps) {
23
+ const theme = useAppTheme();
24
+ const colors = theme.activeColors;
25
+
26
+ const refreshAuth = useCallback(() => {
27
+ void auth.refreshSession();
28
+ }, [auth]);
29
+
30
+ useEffect(() => {
31
+ refreshAuth();
32
+ }, [refreshAuth]);
33
+
34
+ useFocusEffect(
35
+ useCallback(() => {
36
+ refreshAuth();
37
+ }, [refreshAuth])
38
+ );
39
+
40
+ const decision: AuthGuardDecision = resolveAuthGuardDecision(auth.state, {
41
+ fallback,
42
+ fallbackHref,
43
+ });
44
+
45
+ if (decision === 'loading') {
46
+ return (
47
+ <View style={[styles.loading, { backgroundColor: colors.background }]}>
48
+ <ActivityIndicator color={colors.primary} />
49
+ <Text style={[styles.loadingText, { color: colors.text }]}>Checking session...</Text>
50
+ </View>
51
+ );
52
+ }
53
+
54
+ if (decision === 'authorized') {
55
+ return <>{children}</>;
56
+ }
57
+
58
+ if (decision === 'redirect' && fallbackHref) {
59
+ return <Redirect href={fallbackHref} />;
60
+ }
61
+
62
+ return <>{fallback ?? null}</>;
63
+ }
64
+
65
+ const styles = StyleSheet.create({
66
+ loading: {
67
+ alignItems: 'center',
68
+ flex: 1,
69
+ gap: 12,
70
+ justifyContent: 'center',
71
+ padding: 24,
72
+ },
73
+ loadingText: {
74
+ fontSize: 14,
75
+ fontWeight: '700',
76
+ },
77
+ });
@@ -0,0 +1,60 @@
1
+ import { createContext, useContext, useMemo, type ReactNode } from 'react';
2
+
3
+ import { AuthAdapterProvider, useAuthAdapter } from './auth-adapter';
4
+
5
+ import type { AuthActionInput, AuthActionResult, AuthProviderId, AuthUser } from './auth-types';
6
+
7
+ export type AuthStatus = 'loading' | 'authenticated' | 'anonymous';
8
+
9
+ export interface AuthContextValue {
10
+ provider: AuthProviderId;
11
+ status: AuthStatus;
12
+ isLoading: boolean;
13
+ isAuthenticated: boolean;
14
+ user: AuthUser | null;
15
+ error?: string | null;
16
+ signIn(input: AuthActionInput): Promise<AuthActionResult>;
17
+ signUp(input: AuthActionInput): Promise<AuthActionResult>;
18
+ requestPasswordReset(email: string): Promise<AuthActionResult>;
19
+ signOut(): Promise<AuthActionResult>;
20
+ }
21
+
22
+ const AuthContext = createContext<AuthContextValue | null>(null);
23
+
24
+ function AuthStateProvider({ children }: { children: ReactNode }) {
25
+ const adapter = useAuthAdapter();
26
+ const { state } = adapter;
27
+ const value = useMemo<AuthContextValue>(() => {
28
+ const isAuthenticated = Boolean(state.session?.user);
29
+ return {
30
+ provider: adapter.provider,
31
+ status: state.isLoading ? 'loading' : isAuthenticated ? 'authenticated' : 'anonymous',
32
+ isLoading: state.isLoading,
33
+ isAuthenticated,
34
+ user: state.session?.user ?? null,
35
+ error: state.error,
36
+ signIn: adapter.signInWithEmailPassword,
37
+ signUp: adapter.signUpWithEmailPassword,
38
+ requestPasswordReset: adapter.requestPasswordReset,
39
+ signOut: adapter.signOut,
40
+ };
41
+ }, [adapter, state]);
42
+
43
+ return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
44
+ }
45
+
46
+ export function AuthProvider({ children }: { children: ReactNode }) {
47
+ return (
48
+ <AuthAdapterProvider>
49
+ <AuthStateProvider>{children}</AuthStateProvider>
50
+ </AuthAdapterProvider>
51
+ );
52
+ }
53
+
54
+ export function useAuth(): AuthContextValue {
55
+ const value = useContext(AuthContext);
56
+ if (!value) {
57
+ throw new Error('useAuth must be used inside AuthProvider.');
58
+ }
59
+ return value;
60
+ }