@croacroa/react-native-template 1.0.0 → 2.0.1
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.
- package/.github/workflows/ci.yml +187 -184
- package/.github/workflows/eas-build.yml +55 -55
- package/.github/workflows/eas-update.yml +50 -50
- package/CHANGELOG.md +106 -106
- package/CONTRIBUTING.md +377 -377
- package/README.md +399 -399
- package/__tests__/components/snapshots.test.tsx +131 -0
- package/__tests__/integration/auth-api.test.tsx +227 -0
- package/__tests__/performance/VirtualizedList.perf.test.tsx +362 -0
- package/app/(public)/onboarding.tsx +5 -5
- package/app.config.ts +45 -2
- package/assets/images/.gitkeep +7 -7
- package/components/onboarding/OnboardingScreen.tsx +370 -370
- package/components/onboarding/index.ts +2 -2
- package/components/providers/SuspenseBoundary.tsx +357 -0
- package/components/providers/index.ts +21 -0
- package/components/ui/Avatar.tsx +316 -316
- package/components/ui/Badge.tsx +416 -416
- package/components/ui/BottomSheet.tsx +307 -307
- package/components/ui/Checkbox.tsx +261 -261
- package/components/ui/OptimizedImage.tsx +369 -369
- package/components/ui/Select.tsx +240 -240
- package/components/ui/VirtualizedList.tsx +285 -0
- package/components/ui/index.ts +23 -18
- package/constants/config.ts +97 -54
- package/docs/adr/001-state-management.md +79 -79
- package/docs/adr/002-styling-approach.md +130 -130
- package/docs/adr/003-data-fetching.md +155 -155
- package/docs/adr/004-auth-adapter-pattern.md +144 -144
- package/docs/adr/README.md +78 -78
- package/hooks/index.ts +27 -25
- package/hooks/useApi.ts +102 -5
- package/hooks/useAuth.tsx +82 -0
- package/hooks/useBiometrics.ts +295 -295
- package/hooks/useDeepLinking.ts +256 -256
- package/hooks/useMFA.ts +499 -0
- package/hooks/useNotifications.ts +39 -0
- package/hooks/useOffline.ts +60 -6
- package/hooks/usePerformance.ts +434 -434
- package/hooks/useTheme.tsx +76 -0
- package/hooks/useUpdates.ts +358 -358
- package/i18n/index.ts +194 -77
- package/i18n/locales/ar.json +101 -0
- package/i18n/locales/de.json +101 -0
- package/i18n/locales/en.json +101 -101
- package/i18n/locales/es.json +101 -0
- package/i18n/locales/fr.json +101 -101
- package/jest.config.js +4 -4
- package/maestro/README.md +113 -113
- package/maestro/config.yaml +35 -35
- package/maestro/flows/login.yaml +62 -62
- package/maestro/flows/mfa-login.yaml +92 -0
- package/maestro/flows/mfa-setup.yaml +86 -0
- package/maestro/flows/navigation.yaml +68 -68
- package/maestro/flows/offline-conflict.yaml +101 -0
- package/maestro/flows/offline-sync.yaml +128 -0
- package/maestro/flows/offline.yaml +60 -60
- package/maestro/flows/register.yaml +94 -94
- package/package.json +175 -170
- package/services/analytics.ts +428 -428
- package/services/api.ts +340 -340
- package/services/authAdapter.ts +333 -333
- package/services/backgroundSync.ts +626 -0
- package/services/index.ts +54 -22
- package/services/security.ts +286 -0
- package/tailwind.config.js +47 -47
- package/utils/accessibility.ts +446 -446
- package/utils/index.ts +52 -43
- package/utils/validation.ts +2 -1
- package/utils/withAccessibility.tsx +272 -0
package/services/authAdapter.ts
CHANGED
|
@@ -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;
|