@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 +1 -1
- package/src/infrastructure/utils/AuthErrorMapper.ts +4 -76
- package/src/infrastructure/utils/error/errorExtraction.ts +80 -0
- package/src/infrastructure/utils/listener/anonymousSignInHandler.ts +156 -0
- package/src/presentation/components/AccountActions.tsx +7 -20
- package/src/presentation/hooks/useProfileEdit.ts +6 -2
- package/src/presentation/hooks/useRegisterForm.ts +5 -21
- package/src/presentation/screens/AccountScreen.tsx +3 -16
- package/src/presentation/screens/EditProfileScreen.tsx +14 -13
- package/src/presentation/stores/initializeAuthListener.ts +8 -56
- package/src/presentation/utils/commonStyles.ts +68 -0
- package/src/presentation/utils/form/formErrorUtils.ts +99 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-auth",
|
|
3
|
-
"version": "3.6.
|
|
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
|
-
|
|
23
|
-
|
|
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={[
|
|
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={
|
|
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={[
|
|
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={
|
|
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={[
|
|
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={
|
|
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
|
|
67
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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={[
|
|
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={[
|
|
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 {
|
|
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
|
-
<
|
|
48
|
+
<ScreenLayout
|
|
49
|
+
backgroundColor={tokens.colors.backgroundPrimary}
|
|
50
|
+
contentContainerStyle={styles.loadingContainer}
|
|
51
|
+
>
|
|
49
52
|
<AtomicSpinner size="lg" color="primary" fullContainer />
|
|
50
|
-
</
|
|
53
|
+
</ScreenLayout>
|
|
51
54
|
);
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
return (
|
|
55
|
-
<
|
|
56
|
-
|
|
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
|
-
</
|
|
86
|
+
</ScreenLayout>
|
|
83
87
|
);
|
|
84
88
|
};
|
|
85
89
|
|
|
86
90
|
const styles = StyleSheet.create({
|
|
87
|
-
|
|
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
|
-
|
|
137
|
-
store.setLoading(true);
|
|
130
|
+
|
|
138
131
|
anonymousSignInInProgress = true;
|
|
139
132
|
|
|
140
|
-
//
|
|
141
|
-
|
|
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
|
-
|
|
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
|
+
}
|