@umituz/react-native-auth 1.6.8 → 1.7.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-auth",
3
- "version": "1.6.8",
3
+ "version": "1.7.0",
4
4
  "description": "Authentication service for React Native apps - Secure, type-safe, and production-ready. Provider-agnostic design supports Firebase Auth and can be adapted for Supabase or other providers.",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -14,119 +14,17 @@ import {
14
14
  } from "firebase/auth";
15
15
  import type { IAuthService, SignUpParams, SignInParams } from "../../application/ports/IAuthService";
16
16
  import {
17
- AuthError,
18
17
  AuthInitializationError,
19
- AuthConfigurationError,
20
18
  AuthValidationError,
21
19
  AuthWeakPasswordError,
22
20
  AuthInvalidEmailError,
23
- AuthEmailAlreadyInUseError,
24
- AuthWrongPasswordError,
25
- AuthUserNotFoundError,
26
- AuthNetworkError,
27
21
  } from "../../domain/errors/AuthError";
28
22
  import type { AuthConfig } from "../../domain/value-objects/AuthConfig";
29
23
  import { DEFAULT_AUTH_CONFIG } from "../../domain/value-objects/AuthConfig";
30
- import { DeviceEventEmitter } from "react-native";
31
- import { storageRepository } from "@umituz/react-native-storage";
32
-
33
- const GUEST_MODE_KEY = "@auth_guest_mode";
34
-
35
- /**
36
- * Validate email format
37
- */
38
- function validateEmail(email: string): boolean {
39
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
40
- return emailRegex.test(email.trim());
41
- }
42
-
43
- /**
44
- * Validate password strength
45
- */
46
- function validatePassword(
47
- password: string,
48
- config: Required<Omit<AuthConfig, "onUserCreated" | "onUserUpdated" | "onSignOut">>
49
- ): { valid: boolean; error?: string } {
50
- if (password.length < config.minPasswordLength) {
51
- return {
52
- valid: false,
53
- error: `Password must be at least ${config.minPasswordLength} characters long`,
54
- };
55
- }
56
-
57
- if (config.requireUppercase && !/[A-Z]/.test(password)) {
58
- return {
59
- valid: false,
60
- error: "Password must contain at least one uppercase letter",
61
- };
62
- }
63
-
64
- if (config.requireLowercase && !/[a-z]/.test(password)) {
65
- return {
66
- valid: false,
67
- error: "Password must contain at least one lowercase letter",
68
- };
69
- }
70
-
71
- if (config.requireNumbers && !/[0-9]/.test(password)) {
72
- return {
73
- valid: false,
74
- error: "Password must contain at least one number",
75
- };
76
- }
77
-
78
- if (config.requireSpecialChars && !/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
79
- return {
80
- valid: false,
81
- error: "Password must contain at least one special character",
82
- };
83
- }
84
-
85
- return { valid: true };
86
- }
87
-
88
- /**
89
- * Map Firebase Auth errors to domain errors
90
- */
91
- function mapFirebaseAuthError(error: any): Error {
92
- // Extract error code and message
93
- const code = error?.code || "";
94
- const message = error?.message || "Authentication failed";
95
-
96
- // Firebase Auth error codes
97
- if (code === "auth/email-already-in-use") {
98
- return new AuthEmailAlreadyInUseError();
99
- }
100
- if (code === "auth/invalid-email") {
101
- return new AuthInvalidEmailError();
102
- }
103
- if (code === "auth/weak-password") {
104
- return new AuthWeakPasswordError();
105
- }
106
- if (code === "auth/user-disabled") {
107
- return new AuthError("User account has been disabled", "AUTH_USER_DISABLED");
108
- }
109
- if (code === "auth/user-not-found") {
110
- return new AuthUserNotFoundError();
111
- }
112
- if (code === "auth/wrong-password") {
113
- return new AuthWrongPasswordError();
114
- }
115
- if (code === "auth/network-request-failed") {
116
- return new AuthNetworkError();
117
- }
118
- if (code === "auth/too-many-requests") {
119
- return new AuthError("Too many requests. Please try again later.", "AUTH_TOO_MANY_REQUESTS");
120
- }
121
- if (code === "auth/configuration-not-found" || code === "auth/app-not-authorized") {
122
- return new AuthConfigurationError("Authentication is not properly configured. Please contact support.");
123
- }
124
- if (code === "auth/operation-not-allowed") {
125
- return new AuthConfigurationError("Email/password authentication is not enabled. Please contact support.");
126
- }
127
-
128
- return new AuthError(message, code);
129
- }
24
+ import { validateEmail, validatePassword } from "../utils/AuthValidation";
25
+ import { mapFirebaseAuthError } from "../utils/AuthErrorMapper";
26
+ import { emitUserAuthenticated, emitGuestModeEnabled } from "../utils/AuthEventEmitter";
27
+ import { loadGuestMode, saveGuestMode, clearGuestMode } from "../storage/GuestModeStorage";
130
28
 
131
29
  export class AuthService implements IAuthService {
132
30
  private auth: Auth | null = null;
@@ -146,15 +44,7 @@ export class AuthService implements IAuthService {
146
44
  throw new AuthInitializationError("Auth instance is required");
147
45
  }
148
46
  this.auth = auth;
149
-
150
- // Restore guest mode from storage
151
- try {
152
- const guestModeValue = await storageRepository.getString(GUEST_MODE_KEY, "false");
153
- this.isGuestMode = guestModeValue === "true";
154
- } catch (error) {
155
- // If storage fails, default to false
156
- this.isGuestMode = false;
157
- }
47
+ this.isGuestMode = await loadGuestMode();
158
48
  }
159
49
 
160
50
  /**
@@ -218,15 +108,10 @@ export class AuthService implements IAuthService {
218
108
  // Clear guest mode when user signs up
219
109
  if (this.isGuestMode) {
220
110
  this.isGuestMode = false;
221
- try {
222
- await storageRepository.removeItem(GUEST_MODE_KEY);
223
- } catch (error) {
224
- // Ignore storage errors
225
- }
111
+ await clearGuestMode();
226
112
  }
227
113
 
228
- // Emit event for AppNavigator to handle navigation
229
- DeviceEventEmitter.emit("user-authenticated", { userId: userCredential.user.uid });
114
+ emitUserAuthenticated(userCredential.user.uid);
230
115
 
231
116
  return userCredential.user;
232
117
  } catch (error: any) {
@@ -276,23 +161,10 @@ export class AuthService implements IAuthService {
276
161
  // Clear guest mode when user signs in
277
162
  if (this.isGuestMode) {
278
163
  this.isGuestMode = false;
279
- try {
280
- await storageRepository.removeItem(GUEST_MODE_KEY);
281
- /* eslint-disable-next-line no-console */
282
- if (__DEV__) {
283
- console.log("[AuthService] Guest mode cleared from storage");
284
- }
285
- } catch (error) {
286
- // Ignore storage errors
287
- }
164
+ await clearGuestMode();
288
165
  }
289
166
 
290
- // Emit event for AppNavigator to handle navigation
291
- /* eslint-disable-next-line no-console */
292
- if (__DEV__) {
293
- console.log("[AuthService] Emitting user-authenticated event");
294
- }
295
- DeviceEventEmitter.emit("user-authenticated", { userId: userCredential.user.uid });
167
+ emitUserAuthenticated(userCredential.user.uid);
296
168
 
297
169
  return userCredential.user;
298
170
  } catch (error: any) {
@@ -310,20 +182,13 @@ export class AuthService implements IAuthService {
310
182
  async signOut(): Promise<void> {
311
183
  const auth = this.getAuth();
312
184
  if (!auth) {
313
- // If auth is not initialized, just clear guest mode
314
185
  this.isGuestMode = false;
315
- try {
316
- await storageRepository.removeItem(GUEST_MODE_KEY);
317
- } catch (error) {
318
- // Ignore storage errors
319
- }
186
+ await clearGuestMode();
320
187
  return;
321
188
  }
322
189
 
323
190
  try {
324
191
  await firebaseSignOut(auth);
325
- // Don't clear guest mode on sign out - user might want to continue as guest
326
- // Guest mode will be cleared when user signs in or signs up
327
192
  } catch (error: any) {
328
193
  throw mapFirebaseAuthError(error);
329
194
  }
@@ -340,41 +205,17 @@ export class AuthService implements IAuthService {
340
205
  const auth = this.getAuth();
341
206
 
342
207
  // Sign out from Firebase if logged in
343
- if (auth && auth.currentUser) {
208
+ if (auth?.currentUser) {
344
209
  try {
345
210
  await firebaseSignOut(auth);
346
- } catch (error) {
211
+ } catch {
347
212
  // Ignore sign out errors when switching to guest mode
348
213
  }
349
214
  }
350
215
 
351
216
  this.isGuestMode = true;
352
- /* eslint-disable-next-line no-console */
353
- if (__DEV__) {
354
- console.log("[AuthService] isGuestMode set to true");
355
- }
356
-
357
- // Persist guest mode to storage
358
- try {
359
- await storageRepository.setString(GUEST_MODE_KEY, "true");
360
- /* eslint-disable-next-line no-console */
361
- if (__DEV__) {
362
- console.log("[AuthService] Guest mode persisted to storage");
363
- }
364
- } catch (error) {
365
- // Ignore storage errors - guest mode will still work in memory
366
- /* eslint-disable-next-line no-console */
367
- if (__DEV__) {
368
- console.warn("[AuthService] Failed to persist guest mode to storage:", error);
369
- }
370
- }
371
-
372
- // Emit event for AppNavigator to handle navigation
373
- /* eslint-disable-next-line no-console */
374
- if (__DEV__) {
375
- console.log("[AuthService] Emitting guest-mode-enabled event");
376
- }
377
- DeviceEventEmitter.emit("guest-mode-enabled");
217
+ await saveGuestMode(true);
218
+ emitGuestModeEnabled();
378
219
  }
379
220
 
380
221
  /**
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Guest Mode Storage
3
+ * Single Responsibility: Manage guest mode persistence
4
+ */
5
+
6
+ import { storageRepository } from "@umituz/react-native-storage";
7
+
8
+ const GUEST_MODE_KEY = "@auth_guest_mode";
9
+
10
+ /**
11
+ * Load guest mode from storage
12
+ */
13
+ export async function loadGuestMode(): Promise<boolean> {
14
+ try {
15
+ const guestModeValue = await storageRepository.getString(GUEST_MODE_KEY, "false");
16
+ return guestModeValue === "true";
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Save guest mode to storage
24
+ */
25
+ export async function saveGuestMode(isGuest: boolean): Promise<void> {
26
+ try {
27
+ if (isGuest) {
28
+ await storageRepository.setString(GUEST_MODE_KEY, "true");
29
+ /* eslint-disable-next-line no-console */
30
+ if (__DEV__) {
31
+ console.log("[GuestModeStorage] Guest mode persisted to storage");
32
+ }
33
+ } else {
34
+ await storageRepository.removeItem(GUEST_MODE_KEY);
35
+ }
36
+ } catch (error) {
37
+ /* eslint-disable-next-line no-console */
38
+ if (__DEV__) {
39
+ console.warn("[GuestModeStorage] Failed to persist guest mode:", error);
40
+ }
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Clear guest mode from storage
46
+ */
47
+ export async function clearGuestMode(): Promise<void> {
48
+ try {
49
+ await storageRepository.removeItem(GUEST_MODE_KEY);
50
+ } catch {
51
+ // Ignore storage errors
52
+ }
53
+ }
54
+
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Auth Error Mapper
3
+ * Single Responsibility: Map Firebase Auth errors to domain errors
4
+ */
5
+
6
+ import {
7
+ AuthError,
8
+ AuthConfigurationError,
9
+ AuthEmailAlreadyInUseError,
10
+ AuthInvalidEmailError,
11
+ AuthWeakPasswordError,
12
+ AuthUserNotFoundError,
13
+ AuthWrongPasswordError,
14
+ AuthNetworkError,
15
+ } from "../../domain/errors/AuthError";
16
+
17
+ /**
18
+ * Map Firebase Auth errors to domain errors
19
+ */
20
+ export function mapFirebaseAuthError(error: any): Error {
21
+ const code = error?.code || "";
22
+ const message = error?.message || "Authentication failed";
23
+
24
+ if (code === "auth/email-already-in-use") {
25
+ return new AuthEmailAlreadyInUseError();
26
+ }
27
+ if (code === "auth/invalid-email") {
28
+ return new AuthInvalidEmailError();
29
+ }
30
+ if (code === "auth/weak-password") {
31
+ return new AuthWeakPasswordError();
32
+ }
33
+ if (code === "auth/user-disabled") {
34
+ return new AuthError("User account has been disabled", "AUTH_USER_DISABLED");
35
+ }
36
+ if (code === "auth/user-not-found") {
37
+ return new AuthUserNotFoundError();
38
+ }
39
+ if (code === "auth/wrong-password") {
40
+ return new AuthWrongPasswordError();
41
+ }
42
+ if (code === "auth/network-request-failed") {
43
+ return new AuthNetworkError();
44
+ }
45
+ if (code === "auth/too-many-requests") {
46
+ return new AuthError("Too many requests. Please try again later.", "AUTH_TOO_MANY_REQUESTS");
47
+ }
48
+ if (code === "auth/configuration-not-found" || code === "auth/app-not-authorized") {
49
+ return new AuthConfigurationError("Authentication is not properly configured. Please contact support.");
50
+ }
51
+ if (code === "auth/operation-not-allowed") {
52
+ return new AuthConfigurationError("Email/password authentication is not enabled. Please contact support.");
53
+ }
54
+
55
+ return new AuthError(message, code);
56
+ }
57
+
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Auth Event Emitter
3
+ * Single Responsibility: Emit auth-related events
4
+ */
5
+
6
+ import { DeviceEventEmitter } from "react-native";
7
+
8
+ /**
9
+ * Emit user authenticated event
10
+ */
11
+ export function emitUserAuthenticated(userId: string): void {
12
+ /* eslint-disable-next-line no-console */
13
+ if (__DEV__) {
14
+ console.log("[AuthEventEmitter] Emitting user-authenticated event");
15
+ }
16
+ DeviceEventEmitter.emit("user-authenticated", { userId });
17
+ }
18
+
19
+ /**
20
+ * Emit guest mode enabled event
21
+ */
22
+ export function emitGuestModeEnabled(): void {
23
+ /* eslint-disable-next-line no-console */
24
+ if (__DEV__) {
25
+ console.log("[AuthEventEmitter] Emitting guest-mode-enabled event");
26
+ }
27
+ DeviceEventEmitter.emit("guest-mode-enabled");
28
+ }
29
+
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Auth Validation Utilities
3
+ * Single Responsibility: Email and password validation
4
+ */
5
+
6
+ import type { AuthConfig } from "../../domain/value-objects/AuthConfig";
7
+
8
+ /**
9
+ * Validate email format
10
+ */
11
+ export function validateEmail(email: string): boolean {
12
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
13
+ return emailRegex.test(email.trim());
14
+ }
15
+
16
+ /**
17
+ * Validate password strength
18
+ */
19
+ export function validatePassword(
20
+ password: string,
21
+ config: Required<Omit<AuthConfig, "onUserCreated" | "onUserUpdated" | "onSignOut">>
22
+ ): { valid: boolean; error?: string } {
23
+ if (password.length < config.minPasswordLength) {
24
+ return {
25
+ valid: false,
26
+ error: `Password must be at least ${config.minPasswordLength} characters long`,
27
+ };
28
+ }
29
+
30
+ if (config.requireUppercase && !/[A-Z]/.test(password)) {
31
+ return {
32
+ valid: false,
33
+ error: "Password must contain at least one uppercase letter",
34
+ };
35
+ }
36
+
37
+ if (config.requireLowercase && !/[a-z]/.test(password)) {
38
+ return {
39
+ valid: false,
40
+ error: "Password must contain at least one lowercase letter",
41
+ };
42
+ }
43
+
44
+ if (config.requireNumbers && !/[0-9]/.test(password)) {
45
+ return {
46
+ valid: false,
47
+ error: "Password must contain at least one number",
48
+ };
49
+ }
50
+
51
+ if (config.requireSpecialChars && !/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
52
+ return {
53
+ valid: false,
54
+ error: "Password must contain at least one special character",
55
+ };
56
+ }
57
+
58
+ return { valid: true };
59
+ }
60
+
@@ -1,17 +1,16 @@
1
1
  /**
2
2
  * Login Form Component
3
- * Form fields and actions for login
3
+ * Single Responsibility: Render login form UI
4
4
  */
5
5
 
6
- import React, { useState } from "react";
6
+ import React from "react";
7
7
  import { View, StyleSheet } from "react-native";
8
8
  import { AtomicInput, AtomicButton } from "@umituz/react-native-design-system-atoms";
9
9
  import { useLocalization } from "@umituz/react-native-localization";
10
- import { useAuth } from "../hooks/useAuth";
10
+ import { useLoginForm } from "../hooks/useLoginForm";
11
11
  import { AuthErrorDisplay } from "./AuthErrorDisplay";
12
12
  import { AuthDivider } from "./AuthDivider";
13
13
  import { AuthLink } from "./AuthLink";
14
- import { getAuthErrorLocalizationKey } from "../utils/getAuthErrorMessage";
15
14
 
16
15
  interface LoginFormProps {
17
16
  onNavigateToRegister: () => void;
@@ -21,108 +20,18 @@ export const LoginForm: React.FC<LoginFormProps> = ({
21
20
  onNavigateToRegister,
22
21
  }) => {
23
22
  const { t } = useLocalization();
24
- const { signIn, loading, error, continueAsGuest } = useAuth();
25
-
26
- const [email, setEmail] = useState("");
27
- const [password, setPassword] = useState("");
28
- const [emailError, setEmailError] = useState<string | null>(null);
29
- const [passwordError, setPasswordError] = useState<string | null>(null);
30
- const [localError, setLocalError] = useState<string | null>(null);
31
-
32
- const validateEmail = (emailValue: string): boolean => {
33
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
34
- return emailRegex.test(emailValue);
35
- };
36
-
37
- const handleEmailChange = (text: string) => {
38
- setEmail(text);
39
- if (emailError) setEmailError(null);
40
- if (localError) setLocalError(null);
41
- };
42
-
43
- const handlePasswordChange = (text: string) => {
44
- setPassword(text);
45
- if (passwordError) setPasswordError(null);
46
- if (localError) setLocalError(null);
47
- };
48
-
49
- const handleSignIn = async () => {
50
- /* eslint-disable-next-line no-console */
51
- if (__DEV__) {
52
- console.log("[LoginForm] handleSignIn called");
53
- }
54
- setEmailError(null);
55
- setPasswordError(null);
56
- setLocalError(null);
57
-
58
- let hasError = false;
59
-
60
- if (!email.trim()) {
61
- setEmailError(t("auth.errors.invalidEmail"));
62
- hasError = true;
63
- } else if (!validateEmail(email.trim())) {
64
- setEmailError(t("auth.errors.invalidEmail"));
65
- hasError = true;
66
- }
67
-
68
- if (!password.trim()) {
69
- setPasswordError(t("auth.errors.weakPassword"));
70
- hasError = true;
71
- } else if (password.length < 6) {
72
- setPasswordError(t("auth.errors.weakPassword"));
73
- hasError = true;
74
- }
75
-
76
- if (hasError) {
77
- /* eslint-disable-next-line no-console */
78
- if (__DEV__) {
79
- console.log("[LoginForm] Validation errors, returning early");
80
- }
81
- return;
82
- }
83
-
84
- try {
85
- /* eslint-disable-next-line no-console */
86
- if (__DEV__) {
87
- console.log("[LoginForm] Calling signIn with email:", email.trim());
88
- }
89
- await signIn(email.trim(), password);
90
- /* eslint-disable-next-line no-console */
91
- if (__DEV__) {
92
- console.log("[LoginForm] signIn completed successfully");
93
- }
94
- } catch (err: any) {
95
- /* eslint-disable-next-line no-console */
96
- if (__DEV__) {
97
- console.error("[LoginForm] signIn error:", err);
98
- }
99
- const localizationKey = getAuthErrorLocalizationKey(err);
100
- const errorMessage = t(localizationKey);
101
- setLocalError(errorMessage);
102
- }
103
- };
104
-
105
- const handleContinueAsGuest = async () => {
106
- /* eslint-disable-next-line no-console */
107
- if (__DEV__) {
108
- console.log("[LoginForm] Continue as Guest button pressed");
109
- }
110
- try {
111
- await continueAsGuest();
112
- /* eslint-disable-next-line no-console */
113
- if (__DEV__) {
114
- console.log("[LoginForm] continueAsGuest completed");
115
- }
116
- } catch (err) {
117
- /* eslint-disable-next-line no-console */
118
- if (__DEV__) {
119
- console.error("[LoginForm] Error in continueAsGuest:", err);
120
- }
121
- // Error handling is done in the hook
122
- }
123
- };
124
-
125
- const displayError = localError || error;
23
+ const {
24
+ email,
25
+ password,
26
+ emailError,
27
+ passwordError,
28
+ loading,
29
+ handleEmailChange,
30
+ handlePasswordChange,
31
+ handleSignIn,
32
+ handleContinueAsGuest,
33
+ displayError,
34
+ } = useLoginForm();
126
35
 
127
36
  return (
128
37
  <>
@@ -177,6 +86,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
177
86
  disabled={loading}
178
87
  fullWidth
179
88
  style={styles.guestButton}
89
+ testID="continue-as-guest-button"
180
90
  >
181
91
  {t("auth.continueAsGuest")}
182
92
  </AtomicButton>