@umituz/react-native-auth 3.6.75 → 3.6.77

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": "3.6.75",
3
+ "version": "3.6.77",
4
4
  "description": "Authentication service for React Native apps - Secure, type-safe, and production-ready. Provider-agnostic design with dependency injection, configurable validation, and comprehensive error handling.",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -93,7 +93,9 @@
93
93
  "react-native-safe-area-context": "^5.6.2",
94
94
  "react-native-svg": "^15.15.1",
95
95
  "typescript": "^5.3.0",
96
- "zustand": "^4.0.0"
96
+ "zustand": "^4.0.0",
97
+ "@react-native-community/datetimepicker": "^8.2.0",
98
+ "rn-emoji-keyboard": "^1.7.0"
97
99
  },
98
100
  "publishConfig": {
99
101
  "access": "public"
@@ -60,7 +60,13 @@ export class FirebaseAuthProvider implements IAuthProvider {
60
60
  let userCredential;
61
61
 
62
62
  if (currentUser && isAnonymous) {
63
- try { await currentUser.reload(); } catch { /* Reload failed, proceed */ }
63
+ // Try to reload to get fresh user data, but don't fail if it doesn't work
64
+ try {
65
+ await currentUser.reload();
66
+ } catch (reloadError) {
67
+ // Log the reload error but proceed - the user might still be valid
68
+ console.warn("[FirebaseAuthProvider] User reload failed during anonymous upgrade:", reloadError);
69
+ }
64
70
  const credential = EmailAuthProvider.credential(credentials.email.trim(), credentials.password);
65
71
  userCredential = await linkWithCredential(currentUser, credential);
66
72
  } else {
@@ -70,8 +76,10 @@ export class FirebaseAuthProvider implements IAuthProvider {
70
76
  if (credentials.displayName && userCredential.user) {
71
77
  try {
72
78
  await updateProfile(userCredential.user, { displayName: credentials.displayName.trim() });
73
- } catch {
74
- /* Display name update failed, account created successfully */
79
+ } catch (profileError) {
80
+ // Display name update failed, but account was created successfully
81
+ // Log this for debugging but don't fail the entire operation
82
+ console.warn("[FirebaseAuthProvider] Display name update failed:", profileError);
75
83
  }
76
84
  }
77
85
 
@@ -10,17 +10,24 @@
10
10
  import {
11
11
  AuthError,
12
12
  AuthConfigurationError,
13
- AuthEmailAlreadyInUseError,
14
- AuthInvalidEmailError,
15
- AuthWeakPasswordError,
16
- AuthUserNotFoundError,
17
- AuthWrongPasswordError,
18
13
  AuthNetworkError,
19
14
  } from "../../domain/errors/AuthError";
20
15
  import {
21
16
  extractErrorCode,
22
17
  extractErrorMessage,
23
18
  } from "./error/errorExtraction";
19
+ import { ERROR_CODE_MAP, type ErrorConstructor, type ErrorFactory } from "./error/errorCodeMapping.constants";
20
+
21
+ /**
22
+ * Create error from error mapping
23
+ * @param mapping - Error mapping configuration
24
+ * @returns Created error instance
25
+ */
26
+ function createErrorFromMapping(mapping: { type: "class" | "factory"; create: ErrorConstructor | ErrorFactory }): Error {
27
+ return mapping.type === "class"
28
+ ? new (mapping.create as ErrorConstructor)()
29
+ : (mapping.create as ErrorFactory)();
30
+ }
24
31
 
25
32
  /**
26
33
  * Map Firebase Auth errors to domain errors
@@ -54,76 +61,13 @@ export function mapFirebaseAuthError(error: unknown): Error {
54
61
  const message = extractErrorMessage(error);
55
62
 
56
63
  // Map known Firebase Auth error codes to domain errors
57
- switch (code) {
58
- case "auth/email-already-in-use":
59
- return new AuthEmailAlreadyInUseError();
60
-
61
- case "auth/invalid-email":
62
- return new AuthInvalidEmailError();
63
-
64
- case "auth/weak-password":
65
- return new AuthWeakPasswordError();
66
-
67
- case "auth/user-disabled":
68
- return new AuthError(
69
- "Your account has been disabled. Please contact support.",
70
- "AUTH_USER_DISABLED"
71
- );
72
-
73
- case "auth/user-not-found":
74
- return new AuthUserNotFoundError();
75
-
76
- case "auth/wrong-password":
77
- return new AuthWrongPasswordError();
78
-
79
- case "auth/invalid-credential":
80
- case "auth/invalid-login-credentials":
81
- return new AuthError(
82
- "Invalid email or password. Please check your credentials.",
83
- "AUTH_INVALID_CREDENTIAL"
84
- );
85
-
86
- case "auth/network-request-failed":
87
- return new AuthNetworkError();
88
-
89
- case "auth/too-many-requests":
90
- return new AuthError(
91
- "Too many failed attempts. Please wait a few minutes and try again.",
92
- "AUTH_TOO_MANY_REQUESTS"
93
- );
94
-
95
- case "auth/configuration-not-found":
96
- case "auth/app-not-authorized":
97
- return new AuthConfigurationError(
98
- "Authentication is not properly configured. Please contact support."
99
- );
100
-
101
- case "auth/operation-not-allowed":
102
- return new AuthConfigurationError(
103
- "Email/password authentication is not enabled. Please contact support."
104
- );
105
-
106
- case "auth/requires-recent-login":
107
- return new AuthError(
108
- "Please sign in again to complete this action.",
109
- "AUTH_REQUIRES_RECENT_LOGIN"
110
- );
111
-
112
- case "auth/expired-action-code":
113
- return new AuthError(
114
- "This link has expired. Please request a new one.",
115
- "AUTH_EXPIRED_ACTION_CODE"
116
- );
117
-
118
- case "auth/invalid-action-code":
119
- return new AuthError(
120
- "This link is invalid. Please request a new one.",
121
- "AUTH_INVALID_ACTION_CODE"
122
- );
123
-
124
- default:
125
- return new AuthError(message, code || "AUTH_UNKNOWN_ERROR");
64
+ const mapping = ERROR_CODE_MAP[code];
65
+ if (mapping) {
66
+ return createErrorFromMapping(mapping);
126
67
  }
68
+
69
+ // Default fallback for unknown error codes
70
+ return new AuthError(message, code || "AUTH_UNKNOWN_ERROR");
127
71
  }
128
72
 
129
73
  /**
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Firebase Error Code Mapping Constants
3
+ * Centralized configuration for mapping Firebase error codes 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
+ export type ErrorConstructor = new (message?: string) => Error;
18
+ export type ErrorFactory = () => Error;
19
+
20
+ export interface ErrorMapping {
21
+ type: "class" | "factory";
22
+ create: ErrorConstructor | ErrorFactory;
23
+ }
24
+
25
+ /**
26
+ * Firebase error code to domain error mapping
27
+ */
28
+ export const ERROR_CODE_MAP: Record<string, ErrorMapping> = {
29
+ "auth/email-already-in-use": {
30
+ type: "class",
31
+ create: AuthEmailAlreadyInUseError,
32
+ },
33
+ "auth/invalid-email": {
34
+ type: "class",
35
+ create: AuthInvalidEmailError,
36
+ },
37
+ "auth/weak-password": {
38
+ type: "class",
39
+ create: AuthWeakPasswordError,
40
+ },
41
+ "auth/user-disabled": {
42
+ type: "factory",
43
+ create: () => new AuthError(
44
+ "Your account has been disabled. Please contact support.",
45
+ "AUTH_USER_DISABLED"
46
+ ),
47
+ },
48
+ "auth/user-not-found": {
49
+ type: "class",
50
+ create: AuthUserNotFoundError,
51
+ },
52
+ "auth/wrong-password": {
53
+ type: "class",
54
+ create: AuthWrongPasswordError,
55
+ },
56
+ "auth/invalid-credential": {
57
+ type: "factory",
58
+ create: () => new AuthError(
59
+ "Invalid email or password. Please check your credentials.",
60
+ "AUTH_INVALID_CREDENTIAL"
61
+ ),
62
+ },
63
+ "auth/invalid-login-credentials": {
64
+ type: "factory",
65
+ create: () => new AuthError(
66
+ "Invalid email or password. Please check your credentials.",
67
+ "AUTH_INVALID_CREDENTIAL"
68
+ ),
69
+ },
70
+ "auth/network-request-failed": {
71
+ type: "class",
72
+ create: AuthNetworkError,
73
+ },
74
+ "auth/too-many-requests": {
75
+ type: "factory",
76
+ create: () => new AuthError(
77
+ "Too many failed attempts. Please wait a few minutes and try again.",
78
+ "AUTH_TOO_MANY_REQUESTS"
79
+ ),
80
+ },
81
+ "auth/configuration-not-found": {
82
+ type: "factory",
83
+ create: () => new AuthConfigurationError(
84
+ "Authentication is not properly configured. Please contact support."
85
+ ),
86
+ },
87
+ "auth/app-not-authorized": {
88
+ type: "factory",
89
+ create: () => new AuthConfigurationError(
90
+ "Authentication is not properly configured. Please contact support."
91
+ ),
92
+ },
93
+ "auth/operation-not-allowed": {
94
+ type: "factory",
95
+ create: () => new AuthConfigurationError(
96
+ "Email/password authentication is not enabled. Please contact support."
97
+ ),
98
+ },
99
+ "auth/requires-recent-login": {
100
+ type: "factory",
101
+ create: () => new AuthError(
102
+ "Please sign in again to complete this action.",
103
+ "AUTH_REQUIRES_RECENT_LOGIN"
104
+ ),
105
+ },
106
+ "auth/expired-action-code": {
107
+ type: "factory",
108
+ create: () => new AuthError(
109
+ "This link has expired. Please request a new one.",
110
+ "AUTH_EXPIRED_ACTION_CODE"
111
+ ),
112
+ },
113
+ "auth/invalid-action-code": {
114
+ type: "factory",
115
+ create: () => new AuthError(
116
+ "This link is invalid. Please request a new one.",
117
+ "AUTH_INVALID_ACTION_CODE"
118
+ ),
119
+ },
120
+ };
@@ -74,11 +74,21 @@ export function setupAuthListener(
74
74
  }
75
75
  }
76
76
 
77
- const unsubscribe = onIdTokenChanged(auth, (user) => {
78
- handleAuthStateChange(user, store, auth, autoAnonymousSignIn, onAuthStateChange);
79
- });
80
-
81
- setUnsubscribe(unsubscribe);
77
+ try {
78
+ const unsubscribe = onIdTokenChanged(auth, (user) => {
79
+ handleAuthStateChange(user, store, auth, autoAnonymousSignIn, onAuthStateChange);
80
+ });
81
+
82
+ setUnsubscribe(unsubscribe);
83
+ } catch (error) {
84
+ // If listener setup fails, ensure we clean up and mark as initialized
85
+ console.error("[AuthListener] Failed to setup auth listener:", error);
86
+ completeInitialization();
87
+ store.setLoading(false);
88
+ store.setInitialized(true);
89
+ store.setError("Failed to initialize authentication listener");
90
+ throw error;
91
+ }
82
92
  }
83
93
 
84
94
  /**
@@ -91,42 +101,47 @@ function handleAuthStateChange(
91
101
  autoAnonymousSignIn: boolean,
92
102
  onAuthStateChange?: (user: User | null) => void
93
103
  ): void {
94
- if (!user && autoAnonymousSignIn) {
95
- handleAnonymousMode(store, auth);
96
- store.setFirebaseUser(null);
97
- completeInitialization();
98
- return;
99
- }
104
+ try {
105
+ if (!user && autoAnonymousSignIn) {
106
+ // Start anonymous sign-in without blocking
107
+ void handleAnonymousMode(store, auth);
108
+ store.setFirebaseUser(null);
109
+ completeInitialization();
110
+ return;
111
+ }
100
112
 
101
- store.setFirebaseUser(user);
102
- store.setInitialized(true);
113
+ store.setFirebaseUser(user);
114
+ store.setInitialized(true);
103
115
 
104
- // Handle conversion from anonymous
105
- if (user && !user.isAnonymous && store.isAnonymous) {
106
- store.setIsAnonymous(false);
107
- }
116
+ // Handle conversion from anonymous
117
+ if (user && !user.isAnonymous && store.isAnonymous) {
118
+ store.setIsAnonymous(false);
119
+ }
108
120
 
109
- onAuthStateChange?.(user);
121
+ onAuthStateChange?.(user);
122
+ } catch (error) {
123
+ console.error("[AuthListener] Error handling auth state change:", error);
124
+ // Ensure we don't leave the app in a bad state
125
+ store.setInitialized(true);
126
+ store.setLoading(false);
127
+ }
110
128
  }
111
129
 
112
130
  /**
113
131
  * Handle anonymous mode sign-in
114
132
  */
115
- function handleAnonymousMode(store: Store, auth: Auth): void {
133
+ async function handleAnonymousMode(store: Store, auth: Auth): Promise<void> {
116
134
  if (!startAnonymousSignIn()) {
117
135
  return; // Already signing in
118
136
  }
119
137
 
120
138
  const handleAnonymousSignIn = createAnonymousSignInHandler(auth, store);
121
139
 
122
- // Start sign-in without blocking
123
- void (async () => {
124
- try {
125
- await handleAnonymousSignIn();
126
- } finally {
127
- completeAnonymousSignIn();
128
- }
129
- })();
140
+ try {
141
+ await handleAnonymousSignIn();
142
+ } finally {
143
+ completeAnonymousSignIn();
144
+ }
130
145
  }
131
146
 
132
147
  /**
@@ -1,9 +1,10 @@
1
1
  import React, { useRef } from "react";
2
2
  import { StyleSheet, TextInput } from "react-native";
3
- import { AtomicInput, AtomicButton } from "@umituz/react-native-design-system";
3
+ import { AtomicButton } from "@umituz/react-native-design-system";
4
4
  import { useLoginForm } from "../hooks/useLoginForm";
5
5
  import { AuthErrorDisplay } from "./AuthErrorDisplay";
6
6
  import { AuthLink } from "./AuthLink";
7
+ import { FormEmailInput, FormPasswordInput } from "./form";
7
8
 
8
9
  export interface LoginFormTranslations {
9
10
  email: string;
@@ -39,40 +40,27 @@ export const LoginForm: React.FC<LoginFormProps> = ({
39
40
 
40
41
  return (
41
42
  <>
42
- <AtomicInput
43
- label={translations.email}
43
+ <FormEmailInput
44
44
  value={email}
45
45
  onChangeText={handleEmailChange}
46
+ label={translations.email}
46
47
  placeholder={translations.emailPlaceholder}
47
- keyboardType="email-address"
48
- autoCapitalize="none"
48
+ error={emailError}
49
49
  disabled={loading}
50
- state={emailError ? "error" : "default"}
51
- helperText={emailError || undefined}
52
- returnKeyType="next"
53
50
  onSubmitEditing={() => passwordRef.current?.focus()}
54
- blurOnSubmit={false}
55
- textContentType="emailAddress"
56
- style={styles.input}
51
+ returnKeyType="next"
57
52
  />
58
53
 
59
- <AtomicInput
54
+ <FormPasswordInput
60
55
  ref={passwordRef}
61
- label={translations.password}
62
56
  value={password}
63
57
  onChangeText={handlePasswordChange}
58
+ label={translations.password}
64
59
  placeholder={translations.passwordPlaceholder}
65
- secureTextEntry
66
- showPasswordToggle
67
- autoCapitalize="none"
68
- autoCorrect={false}
60
+ error={passwordError}
69
61
  disabled={loading}
70
- state={passwordError ? "error" : "default"}
71
- helperText={passwordError || undefined}
72
- returnKeyType="done"
73
62
  onSubmitEditing={() => { void handleSignIn(); }}
74
- textContentType="oneTimeCode"
75
- style={styles.input}
63
+ returnKeyType="done"
76
64
  />
77
65
 
78
66
  <AuthErrorDisplay error={displayError} />
@@ -98,9 +86,6 @@ export const LoginForm: React.FC<LoginFormProps> = ({
98
86
  };
99
87
 
100
88
  const styles = StyleSheet.create({
101
- input: {
102
- marginBottom: 20,
103
- },
104
89
  signInButton: {
105
90
  minHeight: 52,
106
91
  marginBottom: 16,
@@ -1,12 +1,13 @@
1
1
  import React, { useRef } from "react";
2
2
  import { StyleSheet, TextInput } from "react-native";
3
- import { AtomicInput, AtomicButton } from "@umituz/react-native-design-system";
3
+ import { AtomicButton } from "@umituz/react-native-design-system";
4
4
  import { useRegisterForm } from "../hooks/useRegisterForm";
5
5
  import { AuthErrorDisplay } from "./AuthErrorDisplay";
6
6
  import { AuthLink } from "./AuthLink";
7
7
  import { AuthLegalLinks, type AuthLegalLinksTranslations } from "./AuthLegalLinks";
8
8
  import { PasswordStrengthIndicator, type PasswordStrengthTranslations } from "./PasswordStrengthIndicator";
9
9
  import { PasswordMatchIndicator, type PasswordMatchTranslations } from "./PasswordMatchIndicator";
10
+ import { FormTextInput, FormEmailInput, FormPasswordInput } from "./form";
10
11
 
11
12
  export interface RegisterFormTranslations {
12
13
  displayName: string;
@@ -66,79 +67,57 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
66
67
 
67
68
  return (
68
69
  <>
69
- <AtomicInput
70
- label={translations.displayName}
70
+ <FormTextInput
71
71
  value={displayName}
72
72
  onChangeText={handleDisplayNameChange}
73
+ label={translations.displayName}
73
74
  placeholder={translations.displayNamePlaceholder}
74
- autoCapitalize="words"
75
+ error={fieldErrors.displayName}
75
76
  disabled={loading}
76
- state={fieldErrors.displayName ? "error" : "default"}
77
- helperText={fieldErrors.displayName || undefined}
78
- returnKeyType="next"
77
+ autoCapitalize="words"
79
78
  onSubmitEditing={() => emailRef.current?.focus()}
80
- blurOnSubmit={false}
81
- style={styles.input}
79
+ returnKeyType="next"
82
80
  />
83
81
 
84
- <AtomicInput
82
+ <FormEmailInput
85
83
  ref={emailRef}
86
- label={translations.email}
87
84
  value={email}
88
85
  onChangeText={handleEmailChange}
86
+ label={translations.email}
89
87
  placeholder={translations.emailPlaceholder}
90
- keyboardType="email-address"
91
- autoCapitalize="none"
88
+ error={fieldErrors.email}
92
89
  disabled={loading}
93
- state={fieldErrors.email ? "error" : "default"}
94
- helperText={fieldErrors.email || undefined}
95
- returnKeyType="next"
96
90
  onSubmitEditing={() => passwordRef.current?.focus()}
97
- blurOnSubmit={false}
98
- textContentType="emailAddress"
99
- style={styles.input}
91
+ returnKeyType="next"
100
92
  />
101
93
 
102
- <AtomicInput
94
+ <FormPasswordInput
103
95
  ref={passwordRef}
104
- label={translations.password}
105
96
  value={password}
106
97
  onChangeText={handlePasswordChange}
98
+ label={translations.password}
107
99
  placeholder={translations.passwordPlaceholder}
108
- secureTextEntry
109
- showPasswordToggle
110
- autoCapitalize="none"
111
- autoCorrect={false}
100
+ error={fieldErrors.password}
112
101
  disabled={loading}
113
- state={fieldErrors.password ? "error" : "default"}
114
- helperText={fieldErrors.password || undefined}
115
- returnKeyType="next"
116
102
  onSubmitEditing={() => confirmPasswordRef.current?.focus()}
117
- blurOnSubmit={false}
118
- textContentType="oneTimeCode"
119
- style={styles.input}
103
+ returnKeyType="next"
104
+ style={styles.passwordInput}
120
105
  />
121
106
  {password.length > 0 && (
122
107
  <PasswordStrengthIndicator translations={translations.passwordStrength} requirements={passwordRequirements} />
123
108
  )}
124
109
 
125
- <AtomicInput
110
+ <FormPasswordInput
126
111
  ref={confirmPasswordRef}
127
- label={translations.confirmPassword}
128
112
  value={confirmPassword}
129
113
  onChangeText={handleConfirmPasswordChange}
114
+ label={translations.confirmPassword}
130
115
  placeholder={translations.confirmPasswordPlaceholder}
131
- secureTextEntry
132
- showPasswordToggle
133
- autoCapitalize="none"
134
- autoCorrect={false}
116
+ error={fieldErrors.confirmPassword}
135
117
  disabled={loading}
136
- state={fieldErrors.confirmPassword ? "error" : "default"}
137
- helperText={fieldErrors.confirmPassword || undefined}
138
- returnKeyType="done"
139
118
  onSubmitEditing={() => { void handleSignUp(); }}
140
- textContentType="oneTimeCode"
141
- style={styles.input}
119
+ returnKeyType="done"
120
+ style={styles.confirmPasswordInput}
142
121
  />
143
122
  {confirmPassword.length > 0 && (
144
123
  <PasswordMatchIndicator translations={translations.passwordMatch} isMatch={passwordsMatch} />
@@ -171,6 +150,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
171
150
  };
172
151
 
173
152
  const styles = StyleSheet.create({
174
- input: { marginBottom: 20 },
153
+ passwordInput: { marginBottom: 4 },
154
+ confirmPasswordInput: { marginBottom: 4 },
175
155
  signUpButton: { minHeight: 52, marginBottom: 16, marginTop: 8 },
176
156
  });
@@ -0,0 +1,60 @@
1
+ import React, { forwardRef } from "react";
2
+ import { TextInput, StyleSheet, ViewStyle } from "react-native";
3
+ import { AtomicInput } from "@umituz/react-native-design-system";
4
+
5
+ export interface FormEmailInputProps {
6
+ value: string;
7
+ onChangeText: (text: string) => void;
8
+ label: string;
9
+ placeholder: string;
10
+ error?: string | null;
11
+ disabled?: boolean;
12
+ onSubmitEditing?: () => void;
13
+ returnKeyType?: "next" | "done";
14
+ style?: ViewStyle;
15
+ }
16
+
17
+ export const FormEmailInput = forwardRef<React.ElementRef<typeof TextInput>, FormEmailInputProps>(
18
+ (
19
+ {
20
+ value,
21
+ onChangeText,
22
+ label,
23
+ placeholder,
24
+ error,
25
+ disabled = false,
26
+ onSubmitEditing,
27
+ returnKeyType = "next",
28
+ style,
29
+ },
30
+ ref
31
+ ) => {
32
+ return (
33
+ <AtomicInput
34
+ ref={ref}
35
+ label={label}
36
+ value={value}
37
+ onChangeText={onChangeText}
38
+ placeholder={placeholder}
39
+ keyboardType="email-address"
40
+ autoCapitalize="none"
41
+ disabled={disabled}
42
+ state={error ? "error" : "default"}
43
+ helperText={error || undefined}
44
+ returnKeyType={returnKeyType}
45
+ onSubmitEditing={onSubmitEditing}
46
+ blurOnSubmit={returnKeyType === "done"}
47
+ textContentType="emailAddress"
48
+ style={[styles.input, style]}
49
+ />
50
+ );
51
+ }
52
+ );
53
+
54
+ FormEmailInput.displayName = "FormEmailInput";
55
+
56
+ const styles = StyleSheet.create({
57
+ input: {
58
+ marginBottom: 20,
59
+ },
60
+ });