@croacroa/react-native-template 2.0.1 → 3.2.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 (172) hide show
  1. package/.env.example +5 -0
  2. package/.eslintrc.js +8 -0
  3. package/.github/workflows/ci.yml +187 -187
  4. package/.github/workflows/eas-build.yml +55 -55
  5. package/.github/workflows/eas-update.yml +50 -50
  6. package/.github/workflows/npm-publish.yml +57 -0
  7. package/CHANGELOG.md +195 -106
  8. package/CONTRIBUTING.md +377 -377
  9. package/LICENSE +21 -0
  10. package/README.md +446 -399
  11. package/__tests__/accessibility/components.test.tsx +285 -0
  12. package/__tests__/components/Button.test.tsx +2 -4
  13. package/__tests__/components/__snapshots__/snapshots.test.tsx.snap +512 -0
  14. package/__tests__/components/snapshots.test.tsx +131 -131
  15. package/__tests__/helpers/a11y.ts +54 -0
  16. package/__tests__/hooks/useAnalytics.test.ts +100 -0
  17. package/__tests__/hooks/useAnimations.test.ts +70 -0
  18. package/__tests__/hooks/useAuth.test.tsx +71 -28
  19. package/__tests__/hooks/useMedia.test.ts +318 -0
  20. package/__tests__/hooks/usePayments.test.tsx +307 -0
  21. package/__tests__/hooks/usePermission.test.ts +230 -0
  22. package/__tests__/hooks/useWebSocket.test.ts +329 -0
  23. package/__tests__/integration/auth-api.test.tsx +224 -227
  24. package/__tests__/performance/VirtualizedList.perf.test.tsx +385 -362
  25. package/__tests__/services/api.test.ts +24 -6
  26. package/app/(auth)/home.tsx +11 -9
  27. package/app/(auth)/profile.tsx +8 -6
  28. package/app/(auth)/settings.tsx +11 -9
  29. package/app/(public)/forgot-password.tsx +25 -15
  30. package/app/(public)/login.tsx +48 -12
  31. package/app/(public)/onboarding.tsx +5 -5
  32. package/app/(public)/register.tsx +24 -15
  33. package/app/_layout.tsx +6 -3
  34. package/app.config.ts +27 -2
  35. package/assets/images/.gitkeep +7 -7
  36. package/assets/images/adaptive-icon.png +0 -0
  37. package/assets/images/favicon.png +0 -0
  38. package/assets/images/icon.png +0 -0
  39. package/assets/images/notification-icon.png +0 -0
  40. package/assets/images/splash.png +0 -0
  41. package/components/ErrorBoundary.tsx +73 -28
  42. package/components/auth/SocialLoginButtons.tsx +168 -0
  43. package/components/forms/FormInput.tsx +5 -3
  44. package/components/onboarding/OnboardingScreen.tsx +370 -370
  45. package/components/onboarding/index.ts +2 -2
  46. package/components/providers/AnalyticsProvider.tsx +67 -0
  47. package/components/providers/SuspenseBoundary.tsx +359 -357
  48. package/components/providers/index.ts +24 -21
  49. package/components/ui/AnimatedButton.tsx +1 -9
  50. package/components/ui/AnimatedList.tsx +98 -0
  51. package/components/ui/AnimatedScreen.tsx +89 -0
  52. package/components/ui/Avatar.tsx +319 -316
  53. package/components/ui/Badge.tsx +416 -416
  54. package/components/ui/BottomSheet.tsx +307 -307
  55. package/components/ui/Button.tsx +11 -3
  56. package/components/ui/Checkbox.tsx +261 -261
  57. package/components/ui/FeatureGate.tsx +57 -0
  58. package/components/ui/ForceUpdateScreen.tsx +108 -0
  59. package/components/ui/ImagePickerButton.tsx +180 -0
  60. package/components/ui/Input.stories.tsx +2 -10
  61. package/components/ui/Input.tsx +2 -10
  62. package/components/ui/OptimizedImage.tsx +369 -369
  63. package/components/ui/Paywall.tsx +253 -0
  64. package/components/ui/PermissionGate.tsx +155 -0
  65. package/components/ui/PurchaseButton.tsx +84 -0
  66. package/components/ui/Select.tsx +240 -240
  67. package/components/ui/Skeleton.tsx +3 -1
  68. package/components/ui/Toast.tsx +427 -0
  69. package/components/ui/UploadProgress.tsx +189 -0
  70. package/components/ui/VirtualizedList.tsx +288 -285
  71. package/components/ui/index.ts +28 -23
  72. package/constants/config.ts +135 -97
  73. package/docs/adr/001-state-management.md +79 -79
  74. package/docs/adr/002-styling-approach.md +130 -130
  75. package/docs/adr/003-data-fetching.md +155 -155
  76. package/docs/adr/004-auth-adapter-pattern.md +144 -144
  77. package/docs/adr/README.md +78 -78
  78. package/docs/guides/analytics-posthog.md +121 -0
  79. package/docs/guides/auth-supabase.md +162 -0
  80. package/docs/guides/feature-flags-launchdarkly.md +150 -0
  81. package/docs/guides/payments-revenuecat.md +169 -0
  82. package/docs/plans/2026-02-22-phase6-implementation.md +3222 -0
  83. package/docs/plans/2026-02-22-phase6-template-completion-design.md +196 -0
  84. package/docs/plans/2026-02-23-npm-publish-design.md +31 -0
  85. package/docs/plans/2026-02-23-phase7-polish-documentation-design.md +79 -0
  86. package/docs/plans/2026-02-23-phase8-additional-features-design.md +136 -0
  87. package/eas.json +2 -1
  88. package/hooks/index.ts +70 -27
  89. package/hooks/useAnimatedEntry.ts +204 -0
  90. package/hooks/useApi.ts +64 -4
  91. package/hooks/useAuth.tsx +7 -3
  92. package/hooks/useBiometrics.ts +295 -295
  93. package/hooks/useChannel.ts +111 -0
  94. package/hooks/useDeepLinking.ts +256 -256
  95. package/hooks/useExperiment.ts +36 -0
  96. package/hooks/useFeatureFlag.ts +59 -0
  97. package/hooks/useForceUpdate.ts +91 -0
  98. package/hooks/useImagePicker.ts +281 -0
  99. package/hooks/useInAppReview.ts +64 -0
  100. package/hooks/useMFA.ts +509 -499
  101. package/hooks/useParallax.ts +142 -0
  102. package/hooks/usePerformance.ts +434 -434
  103. package/hooks/usePermission.ts +190 -0
  104. package/hooks/usePresence.ts +129 -0
  105. package/hooks/useProducts.ts +36 -0
  106. package/hooks/usePurchase.ts +103 -0
  107. package/hooks/useRateLimit.ts +70 -0
  108. package/hooks/useSubscription.ts +49 -0
  109. package/hooks/useTrackEvent.ts +52 -0
  110. package/hooks/useTrackScreen.ts +40 -0
  111. package/hooks/useUpdates.ts +358 -358
  112. package/hooks/useUpload.ts +165 -0
  113. package/hooks/useWebSocket.ts +111 -0
  114. package/i18n/index.ts +197 -194
  115. package/i18n/locales/ar.json +170 -101
  116. package/i18n/locales/de.json +170 -101
  117. package/i18n/locales/en.json +170 -101
  118. package/i18n/locales/es.json +170 -101
  119. package/i18n/locales/fr.json +170 -101
  120. package/jest.config.js +1 -1
  121. package/maestro/README.md +113 -113
  122. package/maestro/config.yaml +35 -35
  123. package/maestro/flows/login.yaml +62 -62
  124. package/maestro/flows/mfa-login.yaml +92 -92
  125. package/maestro/flows/mfa-setup.yaml +86 -86
  126. package/maestro/flows/navigation.yaml +68 -68
  127. package/maestro/flows/offline-conflict.yaml +101 -101
  128. package/maestro/flows/offline-sync.yaml +128 -128
  129. package/maestro/flows/offline.yaml +60 -60
  130. package/maestro/flows/register.yaml +94 -94
  131. package/package.json +188 -175
  132. package/scripts/generate-placeholders.js +38 -0
  133. package/services/analytics/adapters/console.ts +50 -0
  134. package/services/analytics/analytics-adapter.ts +94 -0
  135. package/services/analytics/types.ts +73 -0
  136. package/services/analytics.ts +428 -428
  137. package/services/api.ts +419 -340
  138. package/services/auth/social/apple.ts +110 -0
  139. package/services/auth/social/google.ts +159 -0
  140. package/services/auth/social/social-auth.ts +100 -0
  141. package/services/auth/social/types.ts +80 -0
  142. package/services/authAdapter.ts +333 -333
  143. package/services/backgroundSync.ts +652 -626
  144. package/services/feature-flags/adapters/mock.ts +108 -0
  145. package/services/feature-flags/feature-flag-adapter.ts +174 -0
  146. package/services/feature-flags/types.ts +79 -0
  147. package/services/force-update.ts +140 -0
  148. package/services/index.ts +116 -54
  149. package/services/media/compression.ts +91 -0
  150. package/services/media/media-picker.ts +151 -0
  151. package/services/media/media-upload.ts +160 -0
  152. package/services/payments/adapters/mock.ts +159 -0
  153. package/services/payments/payment-adapter.ts +118 -0
  154. package/services/payments/types.ts +131 -0
  155. package/services/permissions/permission-manager.ts +284 -0
  156. package/services/permissions/types.ts +104 -0
  157. package/services/realtime/types.ts +100 -0
  158. package/services/realtime/websocket-manager.ts +441 -0
  159. package/services/security.ts +289 -286
  160. package/services/sentry.ts +4 -4
  161. package/stores/appStore.ts +9 -0
  162. package/stores/notificationStore.ts +3 -1
  163. package/tailwind.config.js +47 -47
  164. package/tsconfig.json +37 -13
  165. package/types/user.ts +1 -1
  166. package/utils/accessibility.ts +446 -446
  167. package/utils/animations/presets.ts +182 -0
  168. package/utils/animations/transitions.ts +62 -0
  169. package/utils/index.ts +63 -52
  170. package/utils/toast.ts +9 -2
  171. package/utils/validation.ts +4 -1
  172. package/utils/withAccessibility.tsx +272 -272
@@ -1,333 +1,333 @@
1
- /**
2
- * Auth Adapter Pattern
3
- *
4
- * This module provides an abstraction layer for authentication providers.
5
- * Replace the mock implementation with your actual provider (Supabase, Firebase, etc.)
6
- */
7
-
8
- import * as SecureStore from "expo-secure-store";
9
- import type { User, AuthTokens } from "@/types";
10
-
11
- // ============================================================================
12
- // Types
13
- // ============================================================================
14
-
15
- export interface AuthResult {
16
- user: User;
17
- tokens: AuthTokens;
18
- }
19
-
20
- export interface AuthError {
21
- code: string;
22
- message: string;
23
- }
24
-
25
- export interface AuthAdapter {
26
- /**
27
- * Sign in with email and password
28
- */
29
- signIn(email: string, password: string): Promise<AuthResult>;
30
-
31
- /**
32
- * Sign up with email, password, and name
33
- */
34
- signUp(email: string, password: string, name: string): Promise<AuthResult>;
35
-
36
- /**
37
- * Sign out the current user
38
- */
39
- signOut(): Promise<void>;
40
-
41
- /**
42
- * Refresh the access token using the refresh token
43
- */
44
- refreshToken(refreshToken: string): Promise<AuthTokens>;
45
-
46
- /**
47
- * Send a password reset email
48
- */
49
- forgotPassword(email: string): Promise<void>;
50
-
51
- /**
52
- * Reset password with token
53
- */
54
- resetPassword(token: string, newPassword: string): Promise<void>;
55
-
56
- /**
57
- * Get current session (useful for providers like Supabase)
58
- */
59
- getSession(): Promise<AuthResult | null>;
60
-
61
- /**
62
- * Subscribe to auth state changes (optional)
63
- */
64
- onAuthStateChange?(callback: (user: User | null) => void): () => void;
65
- }
66
-
67
- // ============================================================================
68
- // Mock Implementation (for development/testing)
69
- // ============================================================================
70
-
71
- export const mockAuthAdapter: AuthAdapter = {
72
- async signIn(email: string, password: string): Promise<AuthResult> {
73
- // Simulate network delay
74
- await new Promise((resolve) => setTimeout(resolve, 1000));
75
-
76
- // Simulate validation
77
- if (!email.includes("@")) {
78
- throw { code: "invalid_email", message: "Invalid email format" };
79
- }
80
- if (password.length < 6) {
81
- throw { code: "weak_password", message: "Password too short" };
82
- }
83
-
84
- return {
85
- user: {
86
- id: "mock_user_1",
87
- email,
88
- name: email.split("@")[0],
89
- createdAt: new Date().toISOString(),
90
- },
91
- tokens: {
92
- accessToken: `mock_access_${Date.now()}`,
93
- refreshToken: `mock_refresh_${Date.now()}`,
94
- expiresAt: Date.now() + 60 * 60 * 1000, // 1 hour
95
- },
96
- };
97
- },
98
-
99
- async signUp(
100
- email: string,
101
- password: string,
102
- name: string
103
- ): Promise<AuthResult> {
104
- await new Promise((resolve) => setTimeout(resolve, 1000));
105
-
106
- if (!email.includes("@")) {
107
- throw { code: "invalid_email", message: "Invalid email format" };
108
- }
109
- if (password.length < 8) {
110
- throw {
111
- code: "weak_password",
112
- message: "Password must be at least 8 characters",
113
- };
114
- }
115
-
116
- return {
117
- user: {
118
- id: "mock_user_new",
119
- email,
120
- name,
121
- createdAt: new Date().toISOString(),
122
- },
123
- tokens: {
124
- accessToken: `mock_access_${Date.now()}`,
125
- refreshToken: `mock_refresh_${Date.now()}`,
126
- expiresAt: Date.now() + 60 * 60 * 1000,
127
- },
128
- };
129
- },
130
-
131
- async signOut(): Promise<void> {
132
- await new Promise((resolve) => setTimeout(resolve, 500));
133
- // Clear any stored tokens
134
- await SecureStore.deleteItemAsync("auth_tokens");
135
- await SecureStore.deleteItemAsync("auth_user");
136
- },
137
-
138
- async refreshToken(refreshToken: string): Promise<AuthTokens> {
139
- await new Promise((resolve) => setTimeout(resolve, 500));
140
-
141
- if (!refreshToken) {
142
- throw { code: "invalid_token", message: "Invalid refresh token" };
143
- }
144
-
145
- return {
146
- accessToken: `mock_access_${Date.now()}`,
147
- refreshToken: `mock_refresh_${Date.now()}`,
148
- expiresAt: Date.now() + 60 * 60 * 1000,
149
- };
150
- },
151
-
152
- async forgotPassword(email: string): Promise<void> {
153
- await new Promise((resolve) => setTimeout(resolve, 1000));
154
-
155
- if (!email.includes("@")) {
156
- throw { code: "invalid_email", message: "Invalid email format" };
157
- }
158
-
159
- console.log(`[Mock] Password reset email sent to ${email}`);
160
- },
161
-
162
- async resetPassword(_token: string, newPassword: string): Promise<void> {
163
- await new Promise((resolve) => setTimeout(resolve, 1000));
164
-
165
- if (newPassword.length < 8) {
166
- throw {
167
- code: "weak_password",
168
- message: "Password must be at least 8 characters",
169
- };
170
- }
171
-
172
- console.log("[Mock] Password reset successful");
173
- },
174
-
175
- async getSession(): Promise<AuthResult | null> {
176
- try {
177
- const storedTokens = await SecureStore.getItemAsync("auth_tokens");
178
- const storedUser = await SecureStore.getItemAsync("auth_user");
179
-
180
- if (storedTokens && storedUser) {
181
- return {
182
- tokens: JSON.parse(storedTokens),
183
- user: JSON.parse(storedUser),
184
- };
185
- }
186
- return null;
187
- } catch {
188
- return null;
189
- }
190
- },
191
- };
192
-
193
- // ============================================================================
194
- // Supabase Implementation Example
195
- // ============================================================================
196
-
197
- /**
198
- * Example Supabase implementation:
199
- *
200
- * import { createClient } from "@supabase/supabase-js";
201
- *
202
- * const supabase = createClient(
203
- * process.env.EXPO_PUBLIC_SUPABASE_URL!,
204
- * process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!
205
- * );
206
- *
207
- * export const supabaseAuthAdapter: AuthAdapter = {
208
- * async signIn(email, password) {
209
- * const { data, error } = await supabase.auth.signInWithPassword({
210
- * email,
211
- * password,
212
- * });
213
- *
214
- * if (error) throw { code: error.name, message: error.message };
215
- *
216
- * return {
217
- * user: {
218
- * id: data.user!.id,
219
- * email: data.user!.email!,
220
- * name: data.user!.user_metadata.name || email.split("@")[0],
221
- * avatar: data.user!.user_metadata.avatar_url,
222
- * createdAt: data.user!.created_at,
223
- * },
224
- * tokens: {
225
- * accessToken: data.session!.access_token,
226
- * refreshToken: data.session!.refresh_token,
227
- * expiresAt: data.session!.expires_at! * 1000,
228
- * },
229
- * };
230
- * },
231
- *
232
- * async signUp(email, password, name) {
233
- * const { data, error } = await supabase.auth.signUp({
234
- * email,
235
- * password,
236
- * options: { data: { name } },
237
- * });
238
- *
239
- * if (error) throw { code: error.name, message: error.message };
240
- *
241
- * return {
242
- * user: {
243
- * id: data.user!.id,
244
- * email: data.user!.email!,
245
- * name,
246
- * createdAt: data.user!.created_at,
247
- * },
248
- * tokens: {
249
- * accessToken: data.session!.access_token,
250
- * refreshToken: data.session!.refresh_token,
251
- * expiresAt: data.session!.expires_at! * 1000,
252
- * },
253
- * };
254
- * },
255
- *
256
- * async signOut() {
257
- * await supabase.auth.signOut();
258
- * },
259
- *
260
- * async refreshToken() {
261
- * const { data, error } = await supabase.auth.refreshSession();
262
- * if (error) throw { code: error.name, message: error.message };
263
- *
264
- * return {
265
- * accessToken: data.session!.access_token,
266
- * refreshToken: data.session!.refresh_token,
267
- * expiresAt: data.session!.expires_at! * 1000,
268
- * };
269
- * },
270
- *
271
- * async forgotPassword(email) {
272
- * const { error } = await supabase.auth.resetPasswordForEmail(email);
273
- * if (error) throw { code: error.name, message: error.message };
274
- * },
275
- *
276
- * async resetPassword(token, newPassword) {
277
- * const { error } = await supabase.auth.updateUser({ password: newPassword });
278
- * if (error) throw { code: error.name, message: error.message };
279
- * },
280
- *
281
- * async getSession() {
282
- * const { data } = await supabase.auth.getSession();
283
- * if (!data.session) return null;
284
- *
285
- * const { data: userData } = await supabase.auth.getUser();
286
- *
287
- * return {
288
- * user: {
289
- * id: userData.user!.id,
290
- * email: userData.user!.email!,
291
- * name: userData.user!.user_metadata.name,
292
- * avatar: userData.user!.user_metadata.avatar_url,
293
- * createdAt: userData.user!.created_at,
294
- * },
295
- * tokens: {
296
- * accessToken: data.session.access_token,
297
- * refreshToken: data.session.refresh_token,
298
- * expiresAt: data.session.expires_at! * 1000,
299
- * },
300
- * };
301
- * },
302
- *
303
- * onAuthStateChange(callback) {
304
- * const { data: { subscription } } = supabase.auth.onAuthStateChange(
305
- * async (event, session) => {
306
- * if (session) {
307
- * callback({
308
- * id: session.user.id,
309
- * email: session.user.email!,
310
- * name: session.user.user_metadata.name,
311
- * avatar: session.user.user_metadata.avatar_url,
312
- * createdAt: session.user.created_at,
313
- * });
314
- * } else {
315
- * callback(null);
316
- * }
317
- * }
318
- * );
319
- *
320
- * return () => subscription.unsubscribe();
321
- * },
322
- * };
323
- */
324
-
325
- // ============================================================================
326
- // Active Adapter
327
- // ============================================================================
328
-
329
- /**
330
- * Change this to use your preferred auth provider
331
- * Options: mockAuthAdapter, supabaseAuthAdapter, firebaseAuthAdapter, etc.
332
- */
333
- export const authAdapter: AuthAdapter = mockAuthAdapter;
1
+ /**
2
+ * Auth Adapter Pattern
3
+ *
4
+ * This module provides an abstraction layer for authentication providers.
5
+ * Replace the mock implementation with your actual provider (Supabase, Firebase, etc.)
6
+ */
7
+
8
+ import * as SecureStore from "expo-secure-store";
9
+ import type { User, AuthTokens } from "@/types";
10
+
11
+ // ============================================================================
12
+ // Types
13
+ // ============================================================================
14
+
15
+ export interface AuthResult {
16
+ user: User;
17
+ tokens: AuthTokens;
18
+ }
19
+
20
+ export interface AuthError {
21
+ code: string;
22
+ message: string;
23
+ }
24
+
25
+ export interface AuthAdapter {
26
+ /**
27
+ * Sign in with email and password
28
+ */
29
+ signIn(email: string, password: string): Promise<AuthResult>;
30
+
31
+ /**
32
+ * Sign up with email, password, and name
33
+ */
34
+ signUp(email: string, password: string, name: string): Promise<AuthResult>;
35
+
36
+ /**
37
+ * Sign out the current user
38
+ */
39
+ signOut(): Promise<void>;
40
+
41
+ /**
42
+ * Refresh the access token using the refresh token
43
+ */
44
+ refreshToken(refreshToken: string): Promise<AuthTokens>;
45
+
46
+ /**
47
+ * Send a password reset email
48
+ */
49
+ forgotPassword(email: string): Promise<void>;
50
+
51
+ /**
52
+ * Reset password with token
53
+ */
54
+ resetPassword(token: string, newPassword: string): Promise<void>;
55
+
56
+ /**
57
+ * Get current session (useful for providers like Supabase)
58
+ */
59
+ getSession(): Promise<AuthResult | null>;
60
+
61
+ /**
62
+ * Subscribe to auth state changes (optional)
63
+ */
64
+ onAuthStateChange?(callback: (user: User | null) => void): () => void;
65
+ }
66
+
67
+ // ============================================================================
68
+ // Mock Implementation (for development/testing)
69
+ // ============================================================================
70
+
71
+ export const mockAuthAdapter: AuthAdapter = {
72
+ async signIn(email: string, password: string): Promise<AuthResult> {
73
+ // Simulate network delay
74
+ await new Promise((resolve) => setTimeout(resolve, 1000));
75
+
76
+ // Simulate validation
77
+ if (!email.includes("@")) {
78
+ throw { code: "invalid_email", message: "Invalid email format" };
79
+ }
80
+ if (password.length < 6) {
81
+ throw { code: "weak_password", message: "Password too short" };
82
+ }
83
+
84
+ return {
85
+ user: {
86
+ id: "mock_user_1",
87
+ email,
88
+ name: email.split("@")[0],
89
+ createdAt: new Date().toISOString(),
90
+ },
91
+ tokens: {
92
+ accessToken: `mock_access_${Date.now()}`,
93
+ refreshToken: `mock_refresh_${Date.now()}`,
94
+ expiresAt: Date.now() + 60 * 60 * 1000, // 1 hour
95
+ },
96
+ };
97
+ },
98
+
99
+ async signUp(
100
+ email: string,
101
+ password: string,
102
+ name: string
103
+ ): Promise<AuthResult> {
104
+ await new Promise((resolve) => setTimeout(resolve, 1000));
105
+
106
+ if (!email.includes("@")) {
107
+ throw { code: "invalid_email", message: "Invalid email format" };
108
+ }
109
+ if (password.length < 8) {
110
+ throw {
111
+ code: "weak_password",
112
+ message: "Password must be at least 8 characters",
113
+ };
114
+ }
115
+
116
+ return {
117
+ user: {
118
+ id: "mock_user_new",
119
+ email,
120
+ name,
121
+ createdAt: new Date().toISOString(),
122
+ },
123
+ tokens: {
124
+ accessToken: `mock_access_${Date.now()}`,
125
+ refreshToken: `mock_refresh_${Date.now()}`,
126
+ expiresAt: Date.now() + 60 * 60 * 1000,
127
+ },
128
+ };
129
+ },
130
+
131
+ async signOut(): Promise<void> {
132
+ await new Promise((resolve) => setTimeout(resolve, 500));
133
+ // Clear any stored tokens
134
+ await SecureStore.deleteItemAsync("auth_tokens");
135
+ await SecureStore.deleteItemAsync("auth_user");
136
+ },
137
+
138
+ async refreshToken(refreshToken: string): Promise<AuthTokens> {
139
+ await new Promise((resolve) => setTimeout(resolve, 500));
140
+
141
+ if (!refreshToken) {
142
+ throw { code: "invalid_token", message: "Invalid refresh token" };
143
+ }
144
+
145
+ return {
146
+ accessToken: `mock_access_${Date.now()}`,
147
+ refreshToken: `mock_refresh_${Date.now()}`,
148
+ expiresAt: Date.now() + 60 * 60 * 1000,
149
+ };
150
+ },
151
+
152
+ async forgotPassword(email: string): Promise<void> {
153
+ await new Promise((resolve) => setTimeout(resolve, 1000));
154
+
155
+ if (!email.includes("@")) {
156
+ throw { code: "invalid_email", message: "Invalid email format" };
157
+ }
158
+
159
+ console.log(`[Mock] Password reset email sent to ${email}`);
160
+ },
161
+
162
+ async resetPassword(_token: string, newPassword: string): Promise<void> {
163
+ await new Promise((resolve) => setTimeout(resolve, 1000));
164
+
165
+ if (newPassword.length < 8) {
166
+ throw {
167
+ code: "weak_password",
168
+ message: "Password must be at least 8 characters",
169
+ };
170
+ }
171
+
172
+ console.log("[Mock] Password reset successful");
173
+ },
174
+
175
+ async getSession(): Promise<AuthResult | null> {
176
+ try {
177
+ const storedTokens = await SecureStore.getItemAsync("auth_tokens");
178
+ const storedUser = await SecureStore.getItemAsync("auth_user");
179
+
180
+ if (storedTokens && storedUser) {
181
+ return {
182
+ tokens: JSON.parse(storedTokens),
183
+ user: JSON.parse(storedUser),
184
+ };
185
+ }
186
+ return null;
187
+ } catch {
188
+ return null;
189
+ }
190
+ },
191
+ };
192
+
193
+ // ============================================================================
194
+ // Supabase Implementation Example
195
+ // ============================================================================
196
+
197
+ /**
198
+ * Example Supabase implementation:
199
+ *
200
+ * import { createClient } from "@supabase/supabase-js";
201
+ *
202
+ * const supabase = createClient(
203
+ * process.env.EXPO_PUBLIC_SUPABASE_URL!,
204
+ * process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!
205
+ * );
206
+ *
207
+ * export const supabaseAuthAdapter: AuthAdapter = {
208
+ * async signIn(email, password) {
209
+ * const { data, error } = await supabase.auth.signInWithPassword({
210
+ * email,
211
+ * password,
212
+ * });
213
+ *
214
+ * if (error) throw { code: error.name, message: error.message };
215
+ *
216
+ * return {
217
+ * user: {
218
+ * id: data.user!.id,
219
+ * email: data.user!.email!,
220
+ * name: data.user!.user_metadata.name || email.split("@")[0],
221
+ * avatar: data.user!.user_metadata.avatar_url,
222
+ * createdAt: data.user!.created_at,
223
+ * },
224
+ * tokens: {
225
+ * accessToken: data.session!.access_token,
226
+ * refreshToken: data.session!.refresh_token,
227
+ * expiresAt: data.session!.expires_at! * 1000,
228
+ * },
229
+ * };
230
+ * },
231
+ *
232
+ * async signUp(email, password, name) {
233
+ * const { data, error } = await supabase.auth.signUp({
234
+ * email,
235
+ * password,
236
+ * options: { data: { name } },
237
+ * });
238
+ *
239
+ * if (error) throw { code: error.name, message: error.message };
240
+ *
241
+ * return {
242
+ * user: {
243
+ * id: data.user!.id,
244
+ * email: data.user!.email!,
245
+ * name,
246
+ * createdAt: data.user!.created_at,
247
+ * },
248
+ * tokens: {
249
+ * accessToken: data.session!.access_token,
250
+ * refreshToken: data.session!.refresh_token,
251
+ * expiresAt: data.session!.expires_at! * 1000,
252
+ * },
253
+ * };
254
+ * },
255
+ *
256
+ * async signOut() {
257
+ * await supabase.auth.signOut();
258
+ * },
259
+ *
260
+ * async refreshToken() {
261
+ * const { data, error } = await supabase.auth.refreshSession();
262
+ * if (error) throw { code: error.name, message: error.message };
263
+ *
264
+ * return {
265
+ * accessToken: data.session!.access_token,
266
+ * refreshToken: data.session!.refresh_token,
267
+ * expiresAt: data.session!.expires_at! * 1000,
268
+ * };
269
+ * },
270
+ *
271
+ * async forgotPassword(email) {
272
+ * const { error } = await supabase.auth.resetPasswordForEmail(email);
273
+ * if (error) throw { code: error.name, message: error.message };
274
+ * },
275
+ *
276
+ * async resetPassword(token, newPassword) {
277
+ * const { error } = await supabase.auth.updateUser({ password: newPassword });
278
+ * if (error) throw { code: error.name, message: error.message };
279
+ * },
280
+ *
281
+ * async getSession() {
282
+ * const { data } = await supabase.auth.getSession();
283
+ * if (!data.session) return null;
284
+ *
285
+ * const { data: userData } = await supabase.auth.getUser();
286
+ *
287
+ * return {
288
+ * user: {
289
+ * id: userData.user!.id,
290
+ * email: userData.user!.email!,
291
+ * name: userData.user!.user_metadata.name,
292
+ * avatar: userData.user!.user_metadata.avatar_url,
293
+ * createdAt: userData.user!.created_at,
294
+ * },
295
+ * tokens: {
296
+ * accessToken: data.session.access_token,
297
+ * refreshToken: data.session.refresh_token,
298
+ * expiresAt: data.session.expires_at! * 1000,
299
+ * },
300
+ * };
301
+ * },
302
+ *
303
+ * onAuthStateChange(callback) {
304
+ * const { data: { subscription } } = supabase.auth.onAuthStateChange(
305
+ * async (event, session) => {
306
+ * if (session) {
307
+ * callback({
308
+ * id: session.user.id,
309
+ * email: session.user.email!,
310
+ * name: session.user.user_metadata.name,
311
+ * avatar: session.user.user_metadata.avatar_url,
312
+ * createdAt: session.user.created_at,
313
+ * });
314
+ * } else {
315
+ * callback(null);
316
+ * }
317
+ * }
318
+ * );
319
+ *
320
+ * return () => subscription.unsubscribe();
321
+ * },
322
+ * };
323
+ */
324
+
325
+ // ============================================================================
326
+ // Active Adapter
327
+ // ============================================================================
328
+
329
+ /**
330
+ * Change this to use your preferred auth provider
331
+ * Options: mockAuthAdapter, supabaseAuthAdapter, firebaseAuthAdapter, etc.
332
+ */
333
+ export const authAdapter: AuthAdapter = mockAuthAdapter;