@umituz/react-native-auth 3.6.63 → 3.6.64

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.63",
3
+ "version": "3.6.64",
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",
@@ -17,82 +17,10 @@ import {
17
17
  AuthWrongPasswordError,
18
18
  AuthNetworkError,
19
19
  } from "../../domain/errors/AuthError";
20
-
21
- /**
22
- * Firebase Auth error structure
23
- * Based on Firebase Auth SDK error format
24
- */
25
- interface FirebaseAuthError {
26
- code: string;
27
- message: string;
28
- name?: string;
29
- stack?: string;
30
- }
31
-
32
- /**
33
- * Type guard to check if error is a valid Firebase Auth error
34
- * @param error - Unknown error to check
35
- * @returns True if error matches Firebase Auth error structure
36
- */
37
- function isFirebaseAuthError(error: unknown): error is FirebaseAuthError {
38
- if (!error || typeof error !== 'object') {
39
- return false;
40
- }
41
-
42
- const err = error as Partial<FirebaseAuthError>;
43
- return (
44
- typeof err.code === 'string' &&
45
- typeof err.message === 'string' &&
46
- err.code.startsWith('auth/')
47
- );
48
- }
49
-
50
- /**
51
- * Extract error code from error object
52
- * @param error - Unknown error
53
- * @returns Error code or empty string
54
- */
55
- function extractErrorCode(error: unknown): string {
56
- if (isFirebaseAuthError(error)) {
57
- return error.code;
58
- }
59
-
60
- // Fallback for non-Firebase errors
61
- if (error && typeof error === 'object' && 'code' in error) {
62
- const code = (error as { code?: unknown }).code;
63
- if (typeof code === 'string') {
64
- return code;
65
- }
66
- }
67
-
68
- return '';
69
- }
70
-
71
- /**
72
- * Extract error message from error object
73
- * @param error - Unknown error
74
- * @returns Error message or default message
75
- */
76
- function extractErrorMessage(error: unknown): string {
77
- if (isFirebaseAuthError(error)) {
78
- return error.message;
79
- }
80
-
81
- // Fallback for Error objects
82
- if (error instanceof Error) {
83
- return error.message;
84
- }
85
-
86
- // Fallback for objects with message property
87
- if (error && typeof error === 'object' && 'message' in error) {
88
- const message = (error as { message?: unknown }).message;
89
- if (typeof message === 'string') {
90
- return message;
91
- }
92
- }
93
-
94
- return "Authentication failed";
95
- }
20
+ import {
21
+ extractErrorCode,
22
+ extractErrorMessage,
23
+ } from "./error/errorExtraction";
96
24
 
97
25
  /**
98
26
  * Map Firebase Auth errors to domain errors
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Error Extraction Utilities
3
+ * Utilities for extracting error information from various error types
4
+ */
5
+
6
+ /**
7
+ * Firebase Auth error structure
8
+ * Based on Firebase Auth SDK error format
9
+ */
10
+ interface FirebaseAuthError {
11
+ code: string;
12
+ message: string;
13
+ name?: string;
14
+ stack?: string;
15
+ }
16
+
17
+ /**
18
+ * Type guard to check if error is a valid Firebase Auth error
19
+ * @param error - Unknown error to check
20
+ * @returns True if error matches Firebase Auth error structure
21
+ */
22
+ export function isFirebaseAuthError(error: unknown): error is FirebaseAuthError {
23
+ if (!error || typeof error !== 'object') {
24
+ return false;
25
+ }
26
+
27
+ const err = error as Partial<FirebaseAuthError>;
28
+ return (
29
+ typeof err.code === 'string' &&
30
+ typeof err.message === 'string' &&
31
+ err.code.startsWith('auth/')
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Extract error code from error object
37
+ * @param error - Unknown error
38
+ * @returns Error code or empty string
39
+ */
40
+ export function extractErrorCode(error: unknown): string {
41
+ if (isFirebaseAuthError(error)) {
42
+ return error.code;
43
+ }
44
+
45
+ // Fallback for non-Firebase errors
46
+ if (error && typeof error === 'object' && 'code' in error) {
47
+ const code = (error as { code?: unknown }).code;
48
+ if (typeof code === 'string') {
49
+ return code;
50
+ }
51
+ }
52
+
53
+ return '';
54
+ }
55
+
56
+ /**
57
+ * Extract error message from error object
58
+ * @param error - Unknown error
59
+ * @returns Error message or default message
60
+ */
61
+ export function extractErrorMessage(error: unknown): string {
62
+ if (isFirebaseAuthError(error)) {
63
+ return error.message;
64
+ }
65
+
66
+ // Fallback for Error objects
67
+ if (error instanceof Error) {
68
+ return error.message;
69
+ }
70
+
71
+ // Fallback for objects with message property
72
+ if (error && typeof error === 'object' && 'message' in error) {
73
+ const message = (error as { message?: unknown }).message;
74
+ if (typeof message === 'string') {
75
+ return message;
76
+ }
77
+ }
78
+
79
+ return "Authentication failed";
80
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Anonymous Sign-In Handler
3
+ * Handles anonymous authentication retry logic with timeout protection
4
+ */
5
+
6
+ import { anonymousAuthService } from "@umituz/react-native-firebase";
7
+ import type { Auth } from "firebase/auth";
8
+ import type { User } from "firebase/auth";
9
+
10
+ const MAX_ANONYMOUS_RETRIES = 2;
11
+ const ANONYMOUS_RETRY_DELAY_MS = 1000;
12
+ const ANONYMOUS_SIGNIN_TIMEOUT_MS = 10000;
13
+
14
+ export interface AnonymousSignInCallbacks {
15
+ onSignInStart: () => void;
16
+ onSignInSuccess: () => void;
17
+ onSignInFailure: (error: Error) => void;
18
+ }
19
+
20
+ export interface AnonymousSignInOptions {
21
+ maxRetries?: number;
22
+ retryDelay?: number;
23
+ timeout?: number;
24
+ }
25
+
26
+ /**
27
+ * Attempt anonymous sign-in with retry logic and timeout protection
28
+ * @param auth - Firebase Auth instance
29
+ * @param callbacks - Callback functions for sign-in events
30
+ * @param options - Configuration options
31
+ */
32
+ export async function attemptAnonymousSignIn(
33
+ auth: Auth,
34
+ callbacks: AnonymousSignInCallbacks,
35
+ options: AnonymousSignInOptions = {}
36
+ ): Promise<void> {
37
+ const {
38
+ maxRetries = MAX_ANONYMOUS_RETRIES,
39
+ retryDelay = ANONYMOUS_RETRY_DELAY_MS,
40
+ timeout = ANONYMOUS_SIGNIN_TIMEOUT_MS,
41
+ } = options;
42
+
43
+ callbacks.onSignInStart();
44
+
45
+ try {
46
+ // Add timeout protection
47
+ const timeoutPromise = new Promise<never>((_, reject) =>
48
+ setTimeout(() => reject(new Error("Anonymous sign-in timeout")), timeout)
49
+ );
50
+
51
+ // Race between sign-in and timeout
52
+ await Promise.race([
53
+ performAnonymousSignIn(auth, maxRetries, retryDelay),
54
+ timeoutPromise,
55
+ ]);
56
+
57
+ callbacks.onSignInSuccess();
58
+ } catch (error) {
59
+ const signInError = error instanceof Error ? error : new Error("Unknown sign-in error");
60
+ callbacks.onSignInFailure(signInError);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Perform anonymous sign-in with retry logic
66
+ */
67
+ async function performAnonymousSignIn(
68
+ auth: Auth,
69
+ maxRetries: number,
70
+ retryDelay: number
71
+ ): Promise<void> {
72
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
73
+ try {
74
+ await anonymousAuthService.signInAnonymously(auth);
75
+
76
+ if (__DEV__) {
77
+ console.log("[AnonymousSignInHandler] Anonymous sign-in successful");
78
+ }
79
+
80
+ return;
81
+ } catch (error) {
82
+ if (__DEV__) {
83
+ console.warn(`[AnonymousSignInHandler] Attempt ${attempt + 1}/${maxRetries} failed:`, error);
84
+ }
85
+
86
+ // If not last attempt, wait and retry
87
+ if (attempt < maxRetries - 1) {
88
+ await new Promise(resolve => setTimeout(resolve, retryDelay));
89
+ continue;
90
+ }
91
+
92
+ // All attempts failed
93
+ throw error;
94
+ }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Create anonymous sign-in handler for auth listener
100
+ * Returns a function that can be called when no user is detected
101
+ */
102
+ export function createAnonymousSignInHandler(
103
+ auth: Auth | null,
104
+ store: {
105
+ setFirebaseUser: (user: User | null) => void;
106
+ setLoading: (loading: boolean) => void;
107
+ setInitialized: (initialized: boolean) => void;
108
+ setError: (error: string | null) => void;
109
+ }
110
+ ): () => Promise<void> {
111
+ return async () => {
112
+ if (!auth) {
113
+ if (__DEV__) {
114
+ console.warn("[AnonymousSignInHandler] No auth instance");
115
+ }
116
+ store.setFirebaseUser(null);
117
+ store.setLoading(false);
118
+ store.setInitialized(true);
119
+ return;
120
+ }
121
+
122
+ store.setLoading(true);
123
+
124
+ await attemptAnonymousSignIn(
125
+ auth,
126
+ {
127
+ onSignInStart: () => {
128
+ if (__DEV__) {
129
+ console.log("[AnonymousSignInHandler] Starting anonymous sign-in");
130
+ }
131
+ },
132
+ onSignInSuccess: () => {
133
+ if (__DEV__) {
134
+ console.log("[AnonymousSignInHandler] Anonymous sign-in successful");
135
+ }
136
+ // Listener will be triggered again with the new user
137
+ store.setFirebaseUser(null);
138
+ },
139
+ onSignInFailure: (error) => {
140
+ if (__DEV__) {
141
+ console.error("[AnonymousSignInHandler] All attempts failed:", error);
142
+ }
143
+ store.setFirebaseUser(null);
144
+ store.setLoading(false);
145
+ store.setInitialized(true);
146
+ store.setError("Failed to sign in anonymously. Please check your connection.");
147
+ },
148
+ },
149
+ {
150
+ maxRetries: MAX_ANONYMOUS_RETRIES,
151
+ retryDelay: ANONYMOUS_RETRY_DELAY_MS,
152
+ timeout: ANONYMOUS_SIGNIN_TIMEOUT_MS,
153
+ }
154
+ );
155
+ };
156
+ }
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  import { View, TouchableOpacity, StyleSheet } from "react-native";
3
3
  import { useAppDesignTokens, AtomicIcon, AtomicText, useAlert, AlertType, AlertMode } from "@umituz/react-native-design-system";
4
+ import { actionButtonStyle } from "../utils/commonStyles";
4
5
 
5
6
  export interface AccountActionsConfig {
6
7
  logoutText: string;
@@ -99,12 +100,12 @@ export const AccountActions: React.FC<AccountActionsProps> = ({ config }) => {
99
100
  <View style={styles.container}>
100
101
  {showChangePassword && onChangePassword && changePasswordText && (
101
102
  <TouchableOpacity
102
- style={[styles.actionButton, { borderColor: tokens.colors.border }]}
103
+ style={[actionButtonStyle.container, { borderColor: tokens.colors.border }]}
103
104
  onPress={onChangePassword}
104
105
  activeOpacity={0.7}
105
106
  >
106
107
  <AtomicIcon name="key-outline" size="md" color="textPrimary" />
107
- <AtomicText style={styles.actionText} color="textPrimary">
108
+ <AtomicText style={actionButtonStyle.text} color="textPrimary">
108
109
  {changePasswordText}
109
110
  </AtomicText>
110
111
  <AtomicIcon name="chevron-forward" size="sm" color="textSecondary" />
@@ -112,24 +113,24 @@ export const AccountActions: React.FC<AccountActionsProps> = ({ config }) => {
112
113
  )}
113
114
 
114
115
  <TouchableOpacity
115
- style={[styles.actionButton, { borderColor: tokens.colors.border }]}
116
+ style={[actionButtonStyle.container, { borderColor: tokens.colors.border }]}
116
117
  onPress={handleLogout}
117
118
  activeOpacity={0.7}
118
119
  >
119
120
  <AtomicIcon name="log-out-outline" size="md" color="error" />
120
- <AtomicText style={styles.actionText} color="error">
121
+ <AtomicText style={actionButtonStyle.text} color="error">
121
122
  {logoutText}
122
123
  </AtomicText>
123
124
  <AtomicIcon name="chevron-forward" size="sm" color="textSecondary" />
124
125
  </TouchableOpacity>
125
126
 
126
127
  <TouchableOpacity
127
- style={[styles.actionButton, { borderColor: tokens.colors.border }]}
128
+ style={[actionButtonStyle.container, { borderColor: tokens.colors.border }]}
128
129
  onPress={handleDeleteAccount}
129
130
  activeOpacity={0.7}
130
131
  >
131
132
  <AtomicIcon name="trash-outline" size="md" color="error" />
132
- <AtomicText style={styles.actionText} color="error">
133
+ <AtomicText style={actionButtonStyle.text} color="error">
133
134
  {deleteAccountText}
134
135
  </AtomicText>
135
136
  <AtomicIcon name="chevron-forward" size="sm" color="textSecondary" />
@@ -142,18 +143,4 @@ const styles = StyleSheet.create({
142
143
  container: {
143
144
  gap: 12,
144
145
  },
145
- actionButton: {
146
- flexDirection: "row",
147
- alignItems: "center",
148
- paddingVertical: 16,
149
- paddingHorizontal: 16,
150
- borderRadius: 12,
151
- borderWidth: 1,
152
- gap: 12,
153
- },
154
- actionText: {
155
- flex: 1,
156
- fontSize: 16,
157
- fontWeight: "500",
158
- },
159
146
  });
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { useState, useCallback } from "react";
8
+ import { validateEmail } from "../../infrastructure/utils/AuthValidation";
8
9
 
9
10
  export interface ProfileEditFormState {
10
11
  displayName: string;
@@ -63,8 +64,11 @@ export const useProfileEdit = (
63
64
  errors.push("Display name is required");
64
65
  }
65
66
 
66
- if (formState.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formState.email)) {
67
- errors.push("Invalid email format");
67
+ if (formState.email) {
68
+ const emailResult = validateEmail(formState.email);
69
+ if (!emailResult.isValid && emailResult.error) {
70
+ errors.push(emailResult.error);
71
+ }
68
72
  }
69
73
 
70
74
  return {
@@ -9,6 +9,7 @@ import { useAuth } from "./useAuth";
9
9
  import { getAuthErrorLocalizationKey, resolveErrorMessage } from "../utils/getAuthErrorMessage";
10
10
  import type { PasswordRequirements } from "../../infrastructure/utils/AuthValidation";
11
11
  import { alertService } from "@umituz/react-native-design-system";
12
+ import { clearFieldErrors, clearFieldError } from "../utils/form/formErrorUtils";
12
13
 
13
14
  export interface RegisterFormTranslations {
14
15
  successTitle: string;
@@ -77,42 +78,25 @@ export function useRegisterForm(config?: UseRegisterFormConfig): UseRegisterForm
77
78
 
78
79
  const handleDisplayNameChange = useCallback((text: string) => {
79
80
  setDisplayName(text);
80
- setFieldErrors((prev) => {
81
- const next = { ...prev };
82
- if (next.displayName) delete next.displayName;
83
- return next;
84
- });
81
+ clearFieldError(setFieldErrors, "displayName");
85
82
  setLocalError(null);
86
83
  }, []);
87
84
 
88
85
  const handleEmailChange = useCallback((text: string) => {
89
86
  setEmail(text);
90
- setFieldErrors((prev) => {
91
- const next = { ...prev };
92
- if (next.email) delete next.email;
93
- return next;
94
- });
87
+ clearFieldError(setFieldErrors, "email");
95
88
  setLocalError(null);
96
89
  }, []);
97
90
 
98
91
  const handlePasswordChange = useCallback((text: string) => {
99
92
  setPassword(text);
100
- setFieldErrors((prev) => {
101
- const next = { ...prev };
102
- if (next.password) delete next.password;
103
- if (next.confirmPassword) delete next.confirmPassword;
104
- return next;
105
- });
93
+ clearFieldErrors(setFieldErrors, ["password", "confirmPassword"]);
106
94
  setLocalError(null);
107
95
  }, []);
108
96
 
109
97
  const handleConfirmPasswordChange = useCallback((text: string) => {
110
98
  setConfirmPassword(text);
111
- setFieldErrors((prev) => {
112
- const next = { ...prev };
113
- if (next.confirmPassword) delete next.confirmPassword;
114
- return next;
115
- });
99
+ clearFieldError(setFieldErrors, "confirmPassword");
116
100
  setLocalError(null);
117
101
  }, []);
118
102
 
@@ -7,6 +7,7 @@
7
7
  import React from "react";
8
8
  import { View, TouchableOpacity, StyleSheet } from "react-native";
9
9
  import { useAppDesignTokens, ScreenLayout, AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
10
+ import { actionButtonStyle } from "../utils/commonStyles";
10
11
 
11
12
  import { ProfileSection, type ProfileSectionConfig } from "../components/ProfileSection";
12
13
  import { AccountActions, type AccountActionsConfig } from "../components/AccountActions";
@@ -46,12 +47,12 @@ export const AccountScreen: React.FC<AccountScreenProps> = ({ config }) => {
46
47
  <>
47
48
  <View style={styles.divider} />
48
49
  <TouchableOpacity
49
- style={[styles.actionButton, { borderColor: tokens.colors.border }]}
50
+ style={[actionButtonStyle.container, { borderColor: tokens.colors.border }]}
50
51
  onPress={config.onEditProfile}
51
52
  activeOpacity={0.7}
52
53
  >
53
54
  <AtomicIcon name="person-outline" size="md" customColor={tokens.colors.textPrimary} />
54
- <AtomicText style={[styles.actionText, { color: tokens.colors.textPrimary }]}>
55
+ <AtomicText style={[actionButtonStyle.text, { color: tokens.colors.textPrimary }]}>
55
56
  {config.editProfileText}
56
57
  </AtomicText>
57
58
  <AtomicIcon name="chevron-forward" size="sm" color="secondary" />
@@ -76,19 +77,5 @@ const styles = StyleSheet.create({
76
77
  divider: {
77
78
  height: 24,
78
79
  },
79
- actionButton: {
80
- flexDirection: "row",
81
- alignItems: "center",
82
- paddingVertical: 16,
83
- paddingHorizontal: 16,
84
- borderRadius: 12,
85
- borderWidth: 1,
86
- gap: 12,
87
- },
88
- actionText: {
89
- flex: 1,
90
- fontSize: 16,
91
- fontWeight: "500",
92
- },
93
80
  });
94
81
 
@@ -4,8 +4,8 @@
4
4
  */
5
5
 
6
6
  import React from "react";
7
- import { View, ScrollView, StyleSheet } from "react-native";
8
- import { useAppDesignTokens, AtomicText, AtomicSpinner } from "@umituz/react-native-design-system";
7
+ import { StyleSheet } from "react-native";
8
+ import { useAppDesignTokens, AtomicText, AtomicSpinner, ScreenLayout } from "@umituz/react-native-design-system";
9
9
  import { EditProfileAvatar } from "../components/EditProfileAvatar";
10
10
  import { EditProfileForm } from "../components/EditProfileForm";
11
11
  import { EditProfileActions } from "../components/EditProfileActions";
@@ -45,15 +45,19 @@ export const EditProfileScreen: React.FC<EditProfileScreenProps> = ({ config })
45
45
 
46
46
  if (config.isLoading) {
47
47
  return (
48
- <View style={[styles.loading, { backgroundColor: tokens.colors.backgroundPrimary }]}>
48
+ <ScreenLayout
49
+ backgroundColor={tokens.colors.backgroundPrimary}
50
+ contentContainerStyle={styles.loadingContainer}
51
+ >
49
52
  <AtomicSpinner size="lg" color="primary" fullContainer />
50
- </View>
53
+ </ScreenLayout>
51
54
  );
52
55
  }
53
56
 
54
57
  return (
55
- <ScrollView
56
- style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
58
+ <ScreenLayout
59
+ scrollable
60
+ backgroundColor={tokens.colors.backgroundPrimary}
57
61
  contentContainerStyle={styles.content}
58
62
  >
59
63
  <AtomicText type="headlineSmall" style={styles.title}>
@@ -79,22 +83,19 @@ export const EditProfileScreen: React.FC<EditProfileScreenProps> = ({ config })
79
83
  onCancel={config.onCancel}
80
84
  labels={config.labels}
81
85
  />
82
- </ScrollView>
86
+ </ScreenLayout>
83
87
  );
84
88
  };
85
89
 
86
90
  const styles = StyleSheet.create({
87
- container: {
91
+ loadingContainer: {
88
92
  flex: 1,
93
+ justifyContent: "center",
94
+ alignItems: "center",
89
95
  },
90
96
  content: {
91
97
  padding: 16,
92
98
  },
93
- loading: {
94
- flex: 1,
95
- justifyContent: "center",
96
- alignItems: "center",
97
- },
98
99
  title: {
99
100
  marginBottom: 24,
100
101
  },
@@ -5,17 +5,11 @@
5
5
  */
6
6
 
7
7
  import { onIdTokenChanged } from "firebase/auth";
8
- import {
9
- getFirebaseAuth,
10
- anonymousAuthService,
11
- } from "@umituz/react-native-firebase";
8
+ import { getFirebaseAuth } from "@umituz/react-native-firebase";
12
9
  import { useAuthStore } from "./authStore";
13
10
  import { getAuthService } from "../../infrastructure/services/AuthService";
14
11
  import type { AuthListenerOptions } from "../../types/auth-store.types";
15
-
16
- const MAX_ANONYMOUS_RETRIES = 2;
17
- const ANONYMOUS_RETRY_DELAY_MS = 1000;
18
- const ANONYMOUS_SIGNIN_TIMEOUT_MS = 10000;
12
+ import { createAnonymousSignInHandler } from "../../infrastructure/utils/listener/anonymousSignInHandler";
19
13
 
20
14
  let listenerInitialized = false;
21
15
  let listenerRefCount = 0;
@@ -133,57 +127,16 @@ export function initializeAuthListener(
133
127
  if (__DEV__) {
134
128
  console.log("[AuthListener] No user, auto signing in anonymously...");
135
129
  }
136
- // Set loading state while attempting sign-in
137
- store.setLoading(true);
130
+
138
131
  anonymousSignInInProgress = true;
139
132
 
140
- // Start anonymous sign-in without blocking the listener
141
- // The listener will be triggered again when sign-in completes
133
+ // Create and execute anonymous sign-in handler
134
+ const handleAnonymousSignIn = createAnonymousSignInHandler(auth, store);
135
+
136
+ // Start sign-in without blocking the listener
142
137
  void (async () => {
143
138
  try {
144
- // Add timeout protection
145
- const timeoutPromise = new Promise((_, reject) =>
146
- setTimeout(() => reject(new Error("Anonymous sign-in timeout")), ANONYMOUS_SIGNIN_TIMEOUT_MS)
147
- );
148
-
149
- // Race between sign-in and timeout
150
- await Promise.race([
151
- (async () => {
152
- for (let attempt = 0; attempt < MAX_ANONYMOUS_RETRIES; attempt++) {
153
- try {
154
- await anonymousAuthService.signInAnonymously(auth);
155
- if (__DEV__) {
156
- console.log("[AuthListener] Anonymous sign-in successful");
157
- }
158
- // Success - the listener will fire again with the new user
159
- return;
160
- } catch (error) {
161
- if (__DEV__) {
162
- console.warn(`[AuthListener] Anonymous sign-in attempt ${attempt + 1} failed:`, error);
163
- }
164
-
165
- // If not last attempt, wait and retry
166
- if (attempt < MAX_ANONYMOUS_RETRIES - 1) {
167
- await new Promise(resolve => setTimeout(resolve, ANONYMOUS_RETRY_DELAY_MS));
168
- continue;
169
- }
170
-
171
- // All attempts failed
172
- throw error;
173
- }
174
- }
175
- })(),
176
- timeoutPromise,
177
- ]);
178
- } catch (error) {
179
- // All attempts failed or timeout - set error state
180
- if (__DEV__) {
181
- console.error("[AuthListener] All anonymous sign-in attempts failed:", error);
182
- }
183
- store.setFirebaseUser(null);
184
- store.setLoading(false);
185
- store.setInitialized(true);
186
- store.setError("Failed to sign in anonymously. Please check your connection.");
139
+ await handleAnonymousSignIn();
187
140
  } finally {
188
141
  anonymousSignInInProgress = false;
189
142
  }
@@ -191,7 +144,6 @@ export function initializeAuthListener(
191
144
 
192
145
  // Continue execution - don't return early
193
146
  // The listener will be triggered again when sign-in succeeds
194
- // For now, set null user and let loading state indicate in-progress
195
147
  store.setFirebaseUser(null);
196
148
  initializationInProgress = false;
197
149
  return;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Common Styles
3
+ * Shared style definitions for auth components
4
+ */
5
+
6
+ import { StyleSheet } from "react-native";
7
+
8
+ /**
9
+ * Action button style - used in account actions and profile sections
10
+ */
11
+ export const actionButtonStyle = StyleSheet.create({
12
+ container: {
13
+ flexDirection: "row" as const,
14
+ alignItems: "center" as const,
15
+ paddingVertical: 16,
16
+ paddingHorizontal: 16,
17
+ borderRadius: 12,
18
+ borderWidth: 1,
19
+ gap: 12,
20
+ },
21
+ text: {
22
+ flex: 1,
23
+ fontSize: 16,
24
+ fontWeight: "500",
25
+ },
26
+ });
27
+
28
+ /**
29
+ * Input field styles - used in forms
30
+ */
31
+ export const inputFieldStyle = StyleSheet.create({
32
+ container: {
33
+ marginBottom: 20,
34
+ },
35
+ });
36
+
37
+ /**
38
+ * Card content styles
39
+ */
40
+ export const cardContentStyle = StyleSheet.create({
41
+ container: {
42
+ padding: 16,
43
+ },
44
+ divider: {
45
+ height: 24,
46
+ },
47
+ });
48
+
49
+ /**
50
+ * Avatar container styles
51
+ */
52
+ export const avatarContainerStyle = StyleSheet.create({
53
+ container: {
54
+ marginRight: 12,
55
+ },
56
+ });
57
+
58
+ /**
59
+ * Info text styles
60
+ */
61
+ export const infoTextStyle = StyleSheet.create({
62
+ container: {
63
+ flex: 1,
64
+ },
65
+ displayName: {
66
+ marginBottom: 2,
67
+ },
68
+ });
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Form Error Utilities
3
+ * Shared utilities for form error state management
4
+ */
5
+
6
+ import { useCallback } from "react";
7
+
8
+ export type FieldErrors<T extends string> = Partial<Record<T, string>>;
9
+
10
+ /**
11
+ * Create a field error updater that clears the specified field error
12
+ * @param fieldErrors - Current field errors state
13
+ * @param setFieldErrors - Function to update field errors
14
+ * @param field - Field to clear
15
+ * @returns Function to clear the specified field error
16
+ */
17
+ export function createFieldErrorUpdater<T extends string>(
18
+ fieldErrors: FieldErrors<T>,
19
+ setFieldErrors: (errors: FieldErrors<T> | ((prev: FieldErrors<T>) => FieldErrors<T>)) => void,
20
+ localError: string | null,
21
+ setLocalError: (error: string | null) => void
22
+ ) {
23
+ return useCallback((field: T) => {
24
+ if (fieldErrors[field]) {
25
+ setFieldErrors((prev) => {
26
+ const next = { ...prev };
27
+ delete next[field];
28
+ return next;
29
+ });
30
+ }
31
+ if (localError) {
32
+ setLocalError(null);
33
+ }
34
+ }, [fieldErrors, setFieldErrors, localError, setLocalError]);
35
+ }
36
+
37
+ /**
38
+ * Create a single field error updater
39
+ * @param setFieldErrors - Function to update field errors
40
+ * @param setLocalError - Function to update local error
41
+ * @param fields - Fields to clear when this updater is called
42
+ * @returns Function to clear specified field errors
43
+ */
44
+ export function useFieldErrorClearer<T extends string>(
45
+ setFieldErrors: (errors: FieldErrors<T> | ((prev: FieldErrors<T>) => FieldErrors<T>)) => void,
46
+ setLocalError: (error: string | null) => void,
47
+ fields: T[]
48
+ ) {
49
+ return useCallback(() => {
50
+ setFieldErrors((prev) => {
51
+ const next = { ...prev };
52
+ fields.forEach((field) => {
53
+ if (next[field]) {
54
+ delete next[field];
55
+ }
56
+ });
57
+ return next;
58
+ });
59
+ setLocalError(null);
60
+ }, [setFieldErrors, setLocalError, fields]);
61
+ }
62
+
63
+ /**
64
+ * Clear a specific field error
65
+ * @param setFieldErrors - Function to update field errors
66
+ * @param field - Field to clear
67
+ */
68
+ export function clearFieldError<T extends string>(
69
+ setFieldErrors: (errors: FieldErrors<T> | ((prev: FieldErrors<T>) => FieldErrors<T>)) => void,
70
+ field: T
71
+ ) {
72
+ setFieldErrors((prev) => {
73
+ const next = { ...prev };
74
+ if (next[field]) {
75
+ delete next[field];
76
+ }
77
+ return next;
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Clear multiple field errors
83
+ * @param setFieldErrors - Function to update field errors
84
+ * @param fields - Fields to clear
85
+ */
86
+ export function clearFieldErrors<T extends string>(
87
+ setFieldErrors: (errors: FieldErrors<T> | ((prev: FieldErrors<T>) => FieldErrors<T>)) => void,
88
+ fields: T[]
89
+ ) {
90
+ setFieldErrors((prev) => {
91
+ const next = { ...prev };
92
+ fields.forEach((field) => {
93
+ if (next[field]) {
94
+ delete next[field];
95
+ }
96
+ });
97
+ return next;
98
+ });
99
+ }