@umituz/react-native-auth 3.6.73 → 3.6.75
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/domain/value-objects/AuthConfig.ts +9 -56
- package/src/index.ts +14 -111
- package/src/infrastructure/providers/FirebaseAuthProvider.ts +17 -68
- package/src/infrastructure/services/AuthService.ts +1 -8
- package/src/presentation/components/AccountActions.tsx +9 -39
- package/src/presentation/components/ProfileSection.tsx +78 -108
- package/src/presentation/components/RegisterForm.tsx +6 -28
- package/src/presentation/hooks/useAuth.ts +5 -48
- package/src/presentation/hooks/useGoogleAuth.ts +12 -46
- package/src/presentation/utils/authOperation.util.ts +68 -0
- package/src/presentation/utils/form/formValidation.util.ts +9 -65
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.75",
|
|
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",
|
|
@@ -7,9 +7,6 @@ export interface PasswordConfig {
|
|
|
7
7
|
minLength: number;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
* Social authentication provider configuration
|
|
12
|
-
*/
|
|
13
10
|
export interface SocialProviderConfig {
|
|
14
11
|
enabled: boolean;
|
|
15
12
|
}
|
|
@@ -20,21 +17,13 @@ export interface GoogleAuthConfig extends SocialProviderConfig {
|
|
|
20
17
|
androidClientId?: string;
|
|
21
18
|
}
|
|
22
19
|
|
|
23
|
-
export interface AppleAuthConfig extends SocialProviderConfig {
|
|
24
|
-
// Apple Sign In doesn't require additional config for basic usage
|
|
25
|
-
}
|
|
20
|
+
export interface AppleAuthConfig extends SocialProviderConfig {}
|
|
26
21
|
|
|
27
|
-
/**
|
|
28
|
-
* Social authentication configuration
|
|
29
|
-
*/
|
|
30
22
|
export interface SocialAuthConfig {
|
|
31
23
|
google?: GoogleAuthConfig;
|
|
32
24
|
apple?: AppleAuthConfig;
|
|
33
25
|
}
|
|
34
26
|
|
|
35
|
-
/**
|
|
36
|
-
* Supported social auth providers
|
|
37
|
-
*/
|
|
38
27
|
export type SocialAuthProvider = "google" | "apple";
|
|
39
28
|
|
|
40
29
|
export interface AuthConfig {
|
|
@@ -42,23 +31,16 @@ export interface AuthConfig {
|
|
|
42
31
|
social?: SocialAuthConfig;
|
|
43
32
|
}
|
|
44
33
|
|
|
45
|
-
export const DEFAULT_PASSWORD_CONFIG: PasswordConfig = {
|
|
46
|
-
minLength: 6,
|
|
47
|
-
};
|
|
48
|
-
|
|
34
|
+
export const DEFAULT_PASSWORD_CONFIG: PasswordConfig = { minLength: 6 };
|
|
49
35
|
export const DEFAULT_SOCIAL_CONFIG: SocialAuthConfig = {
|
|
50
36
|
google: { enabled: false },
|
|
51
37
|
apple: { enabled: false },
|
|
52
38
|
};
|
|
53
|
-
|
|
54
39
|
export const DEFAULT_AUTH_CONFIG: AuthConfig = {
|
|
55
40
|
password: DEFAULT_PASSWORD_CONFIG,
|
|
56
41
|
social: DEFAULT_SOCIAL_CONFIG,
|
|
57
42
|
};
|
|
58
43
|
|
|
59
|
-
/**
|
|
60
|
-
* Configuration validation error
|
|
61
|
-
*/
|
|
62
44
|
export class AuthConfigValidationError extends Error {
|
|
63
45
|
constructor(message: string, public readonly field: string) {
|
|
64
46
|
super(message);
|
|
@@ -66,58 +48,29 @@ export class AuthConfigValidationError extends Error {
|
|
|
66
48
|
}
|
|
67
49
|
}
|
|
68
50
|
|
|
69
|
-
/**
|
|
70
|
-
* Validate authentication configuration
|
|
71
|
-
* @throws {AuthConfigValidationError} if configuration is invalid
|
|
72
|
-
*/
|
|
73
51
|
export function validateAuthConfig(config: Partial<AuthConfig>): void {
|
|
74
|
-
// Validate password config
|
|
75
52
|
if (config.password) {
|
|
76
53
|
if (typeof config.password.minLength !== "number") {
|
|
77
|
-
throw new AuthConfigValidationError(
|
|
78
|
-
"Password minLength must be a number",
|
|
79
|
-
"password.minLength"
|
|
80
|
-
);
|
|
54
|
+
throw new AuthConfigValidationError("Password minLength must be a number", "password.minLength");
|
|
81
55
|
}
|
|
82
56
|
if (config.password.minLength < 4) {
|
|
83
|
-
throw new AuthConfigValidationError(
|
|
84
|
-
"Password minLength must be at least 4 characters",
|
|
85
|
-
"password.minLength"
|
|
86
|
-
);
|
|
57
|
+
throw new AuthConfigValidationError("Password minLength must be at least 4 characters", "password.minLength");
|
|
87
58
|
}
|
|
88
59
|
if (config.password.minLength > 128) {
|
|
89
|
-
throw new AuthConfigValidationError(
|
|
90
|
-
"Password minLength must not exceed 128 characters",
|
|
91
|
-
"password.minLength"
|
|
92
|
-
);
|
|
60
|
+
throw new AuthConfigValidationError("Password minLength must not exceed 128 characters", "password.minLength");
|
|
93
61
|
}
|
|
94
62
|
}
|
|
95
63
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
if (googleConfig.enabled) {
|
|
101
|
-
// At least one client ID should be provided if enabled
|
|
102
|
-
if (!googleConfig.webClientId && !googleConfig.iosClientId && !googleConfig.androidClientId) {
|
|
103
|
-
throw new AuthConfigValidationError(
|
|
104
|
-
"At least one Google client ID (web, iOS, or Android) must be provided when Google Sign-In is enabled",
|
|
105
|
-
"social.google"
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
64
|
+
if (config.social?.google?.enabled) {
|
|
65
|
+
const googleConfig = config.social.google;
|
|
66
|
+
if (!googleConfig.webClientId && !googleConfig.iosClientId && !googleConfig.androidClientId) {
|
|
67
|
+
throw new AuthConfigValidationError("At least one Google client ID must be provided when Google Sign-In is enabled", "social.google");
|
|
109
68
|
}
|
|
110
69
|
}
|
|
111
70
|
}
|
|
112
71
|
|
|
113
|
-
/**
|
|
114
|
-
* Sanitize and merge auth config with defaults
|
|
115
|
-
* Ensures valid configuration values
|
|
116
|
-
*/
|
|
117
72
|
export function sanitizeAuthConfig(config: Partial<AuthConfig> = {}): AuthConfig {
|
|
118
|
-
// Validate first
|
|
119
73
|
validateAuthConfig(config);
|
|
120
|
-
|
|
121
74
|
return {
|
|
122
75
|
password: {
|
|
123
76
|
minLength: config.password?.minLength ?? DEFAULT_PASSWORD_CONFIG.minLength,
|
package/src/index.ts
CHANGED
|
@@ -10,97 +10,30 @@
|
|
|
10
10
|
// =============================================================================
|
|
11
11
|
|
|
12
12
|
export type { AuthUser, AuthProviderType } from './domain/entities/AuthUser';
|
|
13
|
-
export {
|
|
14
|
-
AuthError,
|
|
15
|
-
AuthInitializationError,
|
|
16
|
-
AuthConfigurationError,
|
|
17
|
-
AuthValidationError,
|
|
18
|
-
AuthNetworkError,
|
|
19
|
-
AuthUserNotFoundError,
|
|
20
|
-
AuthWrongPasswordError,
|
|
21
|
-
AuthEmailAlreadyInUseError,
|
|
22
|
-
AuthWeakPasswordError,
|
|
23
|
-
AuthInvalidEmailError,
|
|
24
|
-
} from './domain/errors/AuthError';
|
|
25
|
-
|
|
26
|
-
export type {
|
|
27
|
-
AuthConfig,
|
|
28
|
-
PasswordConfig,
|
|
29
|
-
SocialAuthConfig,
|
|
30
|
-
SocialProviderConfig,
|
|
31
|
-
GoogleAuthConfig,
|
|
32
|
-
AppleAuthConfig,
|
|
33
|
-
SocialAuthProvider,
|
|
34
|
-
} from './domain/value-objects/AuthConfig';
|
|
35
|
-
export {
|
|
36
|
-
DEFAULT_AUTH_CONFIG,
|
|
37
|
-
DEFAULT_PASSWORD_CONFIG,
|
|
38
|
-
DEFAULT_SOCIAL_CONFIG,
|
|
39
|
-
} from './domain/value-objects/AuthConfig';
|
|
40
|
-
|
|
41
13
|
export type { UserProfile, UpdateProfileParams } from './domain/entities/UserProfile';
|
|
14
|
+
export { AuthError, AuthInitializationError, AuthConfigurationError, AuthValidationError, AuthNetworkError, AuthUserNotFoundError, AuthWrongPasswordError, AuthEmailAlreadyInUseError, AuthWeakPasswordError, AuthInvalidEmailError } from './domain/errors/AuthError';
|
|
15
|
+
export type { AuthConfig, PasswordConfig, SocialAuthConfig, SocialProviderConfig, GoogleAuthConfig, AppleAuthConfig, SocialAuthProvider } from './domain/value-objects/AuthConfig';
|
|
16
|
+
export { DEFAULT_AUTH_CONFIG, DEFAULT_PASSWORD_CONFIG, DEFAULT_SOCIAL_CONFIG } from './domain/value-objects/AuthConfig';
|
|
42
17
|
|
|
43
18
|
// =============================================================================
|
|
44
19
|
// APPLICATION LAYER
|
|
45
20
|
// =============================================================================
|
|
46
21
|
|
|
47
|
-
export type {
|
|
48
|
-
IAuthProvider,
|
|
49
|
-
AuthCredentials,
|
|
50
|
-
SignUpCredentials,
|
|
51
|
-
SocialSignInResult,
|
|
52
|
-
} from './application/ports/IAuthProvider';
|
|
22
|
+
export type { IAuthProvider, AuthCredentials, SignUpCredentials, SocialSignInResult } from './application/ports/IAuthProvider';
|
|
53
23
|
|
|
54
24
|
// =============================================================================
|
|
55
25
|
// INFRASTRUCTURE LAYER
|
|
56
26
|
// =============================================================================
|
|
57
27
|
|
|
58
28
|
export { FirebaseAuthProvider } from './infrastructure/providers/FirebaseAuthProvider';
|
|
59
|
-
export {
|
|
60
|
-
AuthService,
|
|
61
|
-
initializeAuthService,
|
|
62
|
-
getAuthService,
|
|
63
|
-
resetAuthService,
|
|
64
|
-
} from './infrastructure/services/AuthService';
|
|
29
|
+
export { AuthService, initializeAuthService, getAuthService, resetAuthService } from './infrastructure/services/AuthService';
|
|
65
30
|
export type { IStorageProvider } from './infrastructure/types/Storage.types';
|
|
66
|
-
export {
|
|
67
|
-
|
|
68
|
-
StorageProviderAdapter,
|
|
69
|
-
} from './infrastructure/adapters/StorageProviderAdapter';
|
|
70
|
-
export {
|
|
71
|
-
initializeAuth,
|
|
72
|
-
isAuthInitialized,
|
|
73
|
-
resetAuthInitialization,
|
|
74
|
-
} from './infrastructure/services/initializeAuth';
|
|
31
|
+
export { createStorageProvider, StorageProviderAdapter } from './infrastructure/adapters/StorageProviderAdapter';
|
|
32
|
+
export { initializeAuth, isAuthInitialized, resetAuthInitialization } from './infrastructure/services/initializeAuth';
|
|
75
33
|
export type { InitializeAuthOptions } from './infrastructure/services/initializeAuth';
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// =============================================================================
|
|
80
|
-
|
|
81
|
-
export {
|
|
82
|
-
validateEmail,
|
|
83
|
-
validatePasswordForLogin,
|
|
84
|
-
validatePasswordForRegister,
|
|
85
|
-
validatePasswordConfirmation,
|
|
86
|
-
validateDisplayName,
|
|
87
|
-
} from './infrastructure/utils/AuthValidation';
|
|
88
|
-
export type {
|
|
89
|
-
ValidationResult,
|
|
90
|
-
PasswordStrengthResult,
|
|
91
|
-
PasswordRequirements,
|
|
92
|
-
} from './infrastructure/utils/AuthValidation';
|
|
93
|
-
|
|
94
|
-
export {
|
|
95
|
-
SECURITY_LIMITS,
|
|
96
|
-
sanitizeWhitespace,
|
|
97
|
-
sanitizeEmail,
|
|
98
|
-
sanitizePassword,
|
|
99
|
-
sanitizeName,
|
|
100
|
-
sanitizeText,
|
|
101
|
-
containsDangerousChars,
|
|
102
|
-
isWithinLengthLimit,
|
|
103
|
-
} from './infrastructure/utils/validation/sanitization';
|
|
34
|
+
export { validateEmail, validatePasswordForLogin, validatePasswordForRegister, validatePasswordConfirmation, validateDisplayName } from './infrastructure/utils/AuthValidation';
|
|
35
|
+
export type { ValidationResult, PasswordStrengthResult, PasswordRequirements } from './infrastructure/utils/AuthValidation';
|
|
36
|
+
export { SECURITY_LIMITS, sanitizeWhitespace, sanitizeEmail, sanitizePassword, sanitizeName, sanitizeText, containsDangerousChars, isWithinLengthLimit } from './infrastructure/utils/validation/sanitization';
|
|
104
37
|
export type { SecurityLimitKey } from './infrastructure/utils/validation/sanitization';
|
|
105
38
|
|
|
106
39
|
// =============================================================================
|
|
@@ -109,37 +42,26 @@ export type { SecurityLimitKey } from './infrastructure/utils/validation/sanitiz
|
|
|
109
42
|
|
|
110
43
|
export { useAuth } from './presentation/hooks/useAuth';
|
|
111
44
|
export type { UseAuthResult } from './presentation/hooks/useAuth';
|
|
112
|
-
|
|
113
45
|
export { useLoginForm } from './presentation/hooks/useLoginForm';
|
|
114
46
|
export type { UseLoginFormConfig, UseLoginFormResult } from './presentation/hooks/useLoginForm';
|
|
115
|
-
|
|
116
47
|
export { useRegisterForm } from './presentation/hooks/useRegisterForm';
|
|
117
48
|
export type { UseRegisterFormConfig, UseRegisterFormResult } from './presentation/hooks/useRegisterForm';
|
|
118
|
-
|
|
119
49
|
export { useAuthRequired } from './presentation/hooks/useAuthRequired';
|
|
120
50
|
export { useRequireAuth, useUserId } from './presentation/hooks/useRequireAuth';
|
|
121
|
-
|
|
122
51
|
export { useUserProfile } from './presentation/hooks/useUserProfile';
|
|
123
52
|
export type { UserProfileData, UseUserProfileParams } from './presentation/hooks/useUserProfile';
|
|
124
|
-
|
|
125
53
|
export { useAccountManagement } from './presentation/hooks/useAccountManagement';
|
|
126
54
|
export type { UseAccountManagementReturn } from './presentation/hooks/useAccountManagement';
|
|
127
|
-
|
|
128
55
|
export { useProfileUpdate } from './presentation/hooks/useProfileUpdate';
|
|
129
56
|
export type { UseProfileUpdateReturn } from './presentation/hooks/useProfileUpdate';
|
|
130
|
-
|
|
131
57
|
export { useProfileEdit } from './presentation/hooks/useProfileEdit';
|
|
132
58
|
export type { ProfileEditFormState, UseProfileEditReturn } from './presentation/hooks/useProfileEdit';
|
|
133
|
-
|
|
134
59
|
export { useSocialLogin } from './presentation/hooks/useSocialLogin';
|
|
135
60
|
export type { UseSocialLoginConfig, UseSocialLoginResult } from './presentation/hooks/useSocialLogin';
|
|
136
|
-
|
|
137
61
|
export { useGoogleAuth } from './presentation/hooks/useGoogleAuth';
|
|
138
62
|
export type { UseGoogleAuthResult } from './presentation/hooks/useGoogleAuth';
|
|
139
|
-
|
|
140
63
|
export { useAppleAuth } from './presentation/hooks/useAppleAuth';
|
|
141
64
|
export type { UseAppleAuthResult } from './presentation/hooks/useAppleAuth';
|
|
142
|
-
|
|
143
65
|
export { useAuthBottomSheet } from './presentation/hooks/useAuthBottomSheet';
|
|
144
66
|
export type { SocialAuthConfiguration } from './presentation/hooks/useAuthBottomSheet';
|
|
145
67
|
|
|
@@ -149,22 +71,16 @@ export type { SocialAuthConfiguration } from './presentation/hooks/useAuthBottom
|
|
|
149
71
|
|
|
150
72
|
export { AuthProvider } from './presentation/providers/AuthProvider';
|
|
151
73
|
export type { ErrorFallbackProps } from './presentation/providers/AuthProvider';
|
|
152
|
-
|
|
153
74
|
export { LoginScreen } from './presentation/screens/LoginScreen';
|
|
154
75
|
export type { LoginScreenProps } from './presentation/screens/LoginScreen';
|
|
155
|
-
|
|
156
76
|
export { RegisterScreen } from './presentation/screens/RegisterScreen';
|
|
157
77
|
export type { RegisterScreenProps } from './presentation/screens/RegisterScreen';
|
|
158
|
-
|
|
159
78
|
export { AccountScreen } from './presentation/screens/AccountScreen';
|
|
160
79
|
export type { AccountScreenProps, AccountScreenConfig } from './presentation/screens/AccountScreen';
|
|
161
|
-
|
|
162
80
|
export { EditProfileScreen } from './presentation/screens/EditProfileScreen';
|
|
163
81
|
export type { EditProfileScreenProps } from './presentation/screens/EditProfileScreen';
|
|
164
|
-
|
|
165
82
|
export { AuthNavigator } from './presentation/navigation/AuthNavigator';
|
|
166
83
|
export type { AuthStackParamList } from './presentation/navigation/AuthNavigator';
|
|
167
|
-
|
|
168
84
|
export { AuthBottomSheet } from './presentation/components/AuthBottomSheet';
|
|
169
85
|
export { ProfileSection } from './presentation/components/ProfileSection';
|
|
170
86
|
export type { ProfileSectionProps, ProfileSectionConfig } from './presentation/components/ProfileSection';
|
|
@@ -175,28 +91,15 @@ export type { ProfileSectionProps, ProfileSectionConfig } from './presentation/c
|
|
|
175
91
|
|
|
176
92
|
export { useAuthStore } from './presentation/stores/authStore';
|
|
177
93
|
export { useAuthModalStore } from './presentation/stores/authModalStore';
|
|
178
|
-
export {
|
|
179
|
-
|
|
180
|
-
resetAuthListener,
|
|
181
|
-
isAuthListenerInitialized,
|
|
182
|
-
} from './presentation/stores/initializeAuthListener';
|
|
183
|
-
export type { AuthState, AuthActions, UserType } from './types/auth-store.types';
|
|
184
|
-
export type { AuthListenerOptions } from './types/auth-store.types';
|
|
94
|
+
export { initializeAuthListener, resetAuthListener, isAuthListenerInitialized } from './presentation/stores/initializeAuthListener';
|
|
95
|
+
export type { AuthState, AuthActions, UserType, AuthListenerOptions } from './types/auth-store.types';
|
|
185
96
|
export * from './presentation/stores/auth.selectors';
|
|
186
97
|
|
|
187
98
|
// =============================================================================
|
|
188
|
-
// UTILITIES
|
|
189
|
-
// =============================================================================
|
|
190
|
-
|
|
191
|
-
export {
|
|
192
|
-
getAuthErrorLocalizationKey,
|
|
193
|
-
resolveErrorMessage,
|
|
194
|
-
} from './presentation/utils/getAuthErrorMessage';
|
|
195
|
-
|
|
196
|
-
// =============================================================================
|
|
197
|
-
// INIT MODULE
|
|
99
|
+
// UTILITIES & INIT
|
|
198
100
|
// =============================================================================
|
|
199
101
|
|
|
102
|
+
export { getAuthErrorLocalizationKey, resolveErrorMessage } from './presentation/utils/getAuthErrorMessage';
|
|
200
103
|
export { createAuthInitModule } from './init';
|
|
201
104
|
export type { AuthInitModuleConfig } from './init/createAuthInitModule';
|
|
202
105
|
|
|
@@ -13,12 +13,7 @@ import {
|
|
|
13
13
|
linkWithCredential,
|
|
14
14
|
type Auth,
|
|
15
15
|
} from "firebase/auth";
|
|
16
|
-
|
|
17
|
-
import type {
|
|
18
|
-
IAuthProvider,
|
|
19
|
-
AuthCredentials,
|
|
20
|
-
SignUpCredentials,
|
|
21
|
-
} from "../../application/ports/IAuthProvider";
|
|
16
|
+
import type { IAuthProvider, AuthCredentials, SignUpCredentials } from "../../application/ports/IAuthProvider";
|
|
22
17
|
import type { AuthUser } from "../../domain/entities/AuthUser";
|
|
23
18
|
import { mapFirebaseAuthError } from "../utils/AuthErrorMapper";
|
|
24
19
|
import { mapToAuthUser } from "../utils/UserMapper";
|
|
@@ -27,15 +22,11 @@ export class FirebaseAuthProvider implements IAuthProvider {
|
|
|
27
22
|
private auth: Auth | null = null;
|
|
28
23
|
|
|
29
24
|
constructor(auth?: Auth) {
|
|
30
|
-
if (auth)
|
|
31
|
-
this.auth = auth;
|
|
32
|
-
}
|
|
25
|
+
if (auth) this.auth = auth;
|
|
33
26
|
}
|
|
34
27
|
|
|
35
28
|
initialize(): Promise<void> {
|
|
36
|
-
if (!this.auth)
|
|
37
|
-
throw new Error("Firebase Auth instance must be provided");
|
|
38
|
-
}
|
|
29
|
+
if (!this.auth) throw new Error("Firebase Auth instance must be provided");
|
|
39
30
|
return Promise.resolve();
|
|
40
31
|
}
|
|
41
32
|
|
|
@@ -48,20 +39,12 @@ export class FirebaseAuthProvider implements IAuthProvider {
|
|
|
48
39
|
}
|
|
49
40
|
|
|
50
41
|
async signIn(credentials: AuthCredentials): Promise<AuthUser> {
|
|
51
|
-
if (!this.auth)
|
|
52
|
-
throw new Error("Firebase Auth is not initialized");
|
|
53
|
-
}
|
|
42
|
+
if (!this.auth) throw new Error("Firebase Auth is not initialized");
|
|
54
43
|
|
|
55
44
|
try {
|
|
56
|
-
const userCredential = await signInWithEmailAndPassword(
|
|
57
|
-
this.auth,
|
|
58
|
-
credentials.email.trim(),
|
|
59
|
-
credentials.password
|
|
60
|
-
);
|
|
45
|
+
const userCredential = await signInWithEmailAndPassword(this.auth, credentials.email.trim(), credentials.password);
|
|
61
46
|
const user = mapToAuthUser(userCredential.user);
|
|
62
|
-
if (!user)
|
|
63
|
-
throw new Error("Failed to sign in");
|
|
64
|
-
}
|
|
47
|
+
if (!user) throw new Error("Failed to sign in");
|
|
65
48
|
return user;
|
|
66
49
|
} catch (error: unknown) {
|
|
67
50
|
throw mapFirebaseAuthError(error);
|
|
@@ -69,55 +52,31 @@ export class FirebaseAuthProvider implements IAuthProvider {
|
|
|
69
52
|
}
|
|
70
53
|
|
|
71
54
|
async signUp(credentials: SignUpCredentials): Promise<AuthUser> {
|
|
72
|
-
if (!this.auth)
|
|
73
|
-
throw new Error("Firebase Auth is not initialized");
|
|
74
|
-
}
|
|
55
|
+
if (!this.auth) throw new Error("Firebase Auth is not initialized");
|
|
75
56
|
|
|
76
57
|
try {
|
|
77
58
|
const currentUser = this.auth.currentUser;
|
|
78
59
|
const isAnonymous = currentUser?.isAnonymous ?? false;
|
|
79
|
-
|
|
80
60
|
let userCredential;
|
|
81
61
|
|
|
82
|
-
// Convert anonymous user to permanent account
|
|
83
62
|
if (currentUser && isAnonymous) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
await currentUser.reload();
|
|
87
|
-
} catch {
|
|
88
|
-
// Reload failed, proceed with link anyway
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const credential = EmailAuthProvider.credential(
|
|
92
|
-
credentials.email.trim(),
|
|
93
|
-
credentials.password
|
|
94
|
-
);
|
|
95
|
-
|
|
63
|
+
try { await currentUser.reload(); } catch { /* Reload failed, proceed */ }
|
|
64
|
+
const credential = EmailAuthProvider.credential(credentials.email.trim(), credentials.password);
|
|
96
65
|
userCredential = await linkWithCredential(currentUser, credential);
|
|
97
66
|
} else {
|
|
98
|
-
|
|
99
|
-
userCredential = await createUserWithEmailAndPassword(
|
|
100
|
-
this.auth,
|
|
101
|
-
credentials.email.trim(),
|
|
102
|
-
credentials.password
|
|
103
|
-
);
|
|
67
|
+
userCredential = await createUserWithEmailAndPassword(this.auth, credentials.email.trim(), credentials.password);
|
|
104
68
|
}
|
|
105
69
|
|
|
106
70
|
if (credentials.displayName && userCredential.user) {
|
|
107
71
|
try {
|
|
108
|
-
await updateProfile(userCredential.user, {
|
|
109
|
-
displayName: credentials.displayName.trim(),
|
|
110
|
-
});
|
|
72
|
+
await updateProfile(userCredential.user, { displayName: credentials.displayName.trim() });
|
|
111
73
|
} catch {
|
|
112
|
-
|
|
113
|
-
// only the display name update failed. User can update it later.
|
|
74
|
+
/* Display name update failed, account created successfully */
|
|
114
75
|
}
|
|
115
76
|
}
|
|
116
77
|
|
|
117
78
|
const user = mapToAuthUser(userCredential.user);
|
|
118
|
-
if (!user)
|
|
119
|
-
throw new Error("Failed to create user account");
|
|
120
|
-
}
|
|
79
|
+
if (!user) throw new Error("Failed to create user account");
|
|
121
80
|
return user;
|
|
122
81
|
} catch (error: unknown) {
|
|
123
82
|
throw mapFirebaseAuthError(error);
|
|
@@ -125,10 +84,7 @@ export class FirebaseAuthProvider implements IAuthProvider {
|
|
|
125
84
|
}
|
|
126
85
|
|
|
127
86
|
async signOut(): Promise<void> {
|
|
128
|
-
if (!this.auth)
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
87
|
+
if (!this.auth) return;
|
|
132
88
|
try {
|
|
133
89
|
await firebaseSignOut(this.auth);
|
|
134
90
|
} catch (error: unknown) {
|
|
@@ -137,13 +93,9 @@ export class FirebaseAuthProvider implements IAuthProvider {
|
|
|
137
93
|
}
|
|
138
94
|
|
|
139
95
|
getCurrentUser(): AuthUser | null {
|
|
140
|
-
if (!this.auth)
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
96
|
+
if (!this.auth) return null;
|
|
143
97
|
const currentUser = this.auth.currentUser;
|
|
144
|
-
if (!currentUser)
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
98
|
+
if (!currentUser) return null;
|
|
147
99
|
return mapToAuthUser(currentUser);
|
|
148
100
|
}
|
|
149
101
|
|
|
@@ -152,9 +104,6 @@ export class FirebaseAuthProvider implements IAuthProvider {
|
|
|
152
104
|
callback(null);
|
|
153
105
|
return () => {};
|
|
154
106
|
}
|
|
155
|
-
|
|
156
|
-
return onAuthStateChanged(this.auth, (user) => {
|
|
157
|
-
callback(mapToAuthUser(user));
|
|
158
|
-
});
|
|
107
|
+
return onAuthStateChanged(this.auth, (user) => callback(mapToAuthUser(user)));
|
|
159
108
|
}
|
|
160
109
|
}
|
|
@@ -23,9 +23,7 @@ export class AuthService {
|
|
|
23
23
|
private config: AuthConfig;
|
|
24
24
|
|
|
25
25
|
constructor(config: Partial<AuthConfig> = {}, storageProvider?: IStorageProvider) {
|
|
26
|
-
// Validate and sanitize configuration
|
|
27
26
|
this.config = sanitizeAuthConfig(config);
|
|
28
|
-
|
|
29
27
|
this.anonymousModeService = new AnonymousModeService();
|
|
30
28
|
this.storageProvider = storageProvider;
|
|
31
29
|
}
|
|
@@ -87,17 +85,12 @@ export class AuthService {
|
|
|
87
85
|
}
|
|
88
86
|
|
|
89
87
|
async setAnonymousMode(): Promise<void> {
|
|
90
|
-
if (!this.storageProvider)
|
|
91
|
-
throw new Error("Storage provider is required for anonymous mode");
|
|
92
|
-
}
|
|
88
|
+
if (!this.storageProvider) throw new Error("Storage provider is required for anonymous mode");
|
|
93
89
|
await this.anonymousModeService.enable(this.storageProvider);
|
|
94
90
|
}
|
|
95
91
|
|
|
96
92
|
getCurrentUser(): AuthUser | null {
|
|
97
93
|
if (!this.initialized) return null;
|
|
98
|
-
// Return the actual Firebase user regardless of anonymous mode
|
|
99
|
-
// The caller should check the user's isAnonymous property if needed
|
|
100
|
-
// This ensures proper anonymous to registered user conversion
|
|
101
94
|
return this.repositoryInstance.getCurrentUser();
|
|
102
95
|
}
|
|
103
96
|
|
|
@@ -47,12 +47,7 @@ export const AccountActions: React.FC<AccountActionsProps> = ({ config }) => {
|
|
|
47
47
|
const handleLogout = () => {
|
|
48
48
|
alert.show(AlertType.WARNING, AlertMode.MODAL, logoutConfirmTitle, logoutConfirmMessage, {
|
|
49
49
|
actions: [
|
|
50
|
-
{
|
|
51
|
-
id: "cancel",
|
|
52
|
-
label: cancelText,
|
|
53
|
-
style: "secondary",
|
|
54
|
-
onPress: () => {},
|
|
55
|
-
},
|
|
50
|
+
{ id: "cancel", label: cancelText, style: "secondary", onPress: () => {} },
|
|
56
51
|
{
|
|
57
52
|
id: "confirm",
|
|
58
53
|
label: logoutText,
|
|
@@ -72,12 +67,7 @@ export const AccountActions: React.FC<AccountActionsProps> = ({ config }) => {
|
|
|
72
67
|
const handleDeleteAccount = () => {
|
|
73
68
|
alert.show(AlertType.ERROR, AlertMode.MODAL, deleteConfirmTitle, deleteConfirmMessage, {
|
|
74
69
|
actions: [
|
|
75
|
-
{
|
|
76
|
-
id: "cancel",
|
|
77
|
-
label: cancelText,
|
|
78
|
-
style: "secondary",
|
|
79
|
-
onPress: () => {},
|
|
80
|
-
},
|
|
70
|
+
{ id: "cancel", label: cancelText, style: "secondary", onPress: () => {} },
|
|
81
71
|
{
|
|
82
72
|
id: "confirm",
|
|
83
73
|
label: deleteAccountText,
|
|
@@ -97,40 +87,22 @@ export const AccountActions: React.FC<AccountActionsProps> = ({ config }) => {
|
|
|
97
87
|
return (
|
|
98
88
|
<View style={styles.container}>
|
|
99
89
|
{showChangePassword && onChangePassword && changePasswordText && (
|
|
100
|
-
<TouchableOpacity
|
|
101
|
-
style={[actionButtonStyle.container, { borderColor: tokens.colors.border }]}
|
|
102
|
-
onPress={onChangePassword}
|
|
103
|
-
activeOpacity={0.7}
|
|
104
|
-
>
|
|
90
|
+
<TouchableOpacity style={[actionButtonStyle.container, { borderColor: tokens.colors.border }]} onPress={onChangePassword} activeOpacity={0.7}>
|
|
105
91
|
<AtomicIcon name="key-outline" size="md" color="textPrimary" />
|
|
106
|
-
<AtomicText style={actionButtonStyle.text} color="textPrimary">
|
|
107
|
-
{changePasswordText}
|
|
108
|
-
</AtomicText>
|
|
92
|
+
<AtomicText style={actionButtonStyle.text} color="textPrimary">{changePasswordText}</AtomicText>
|
|
109
93
|
<AtomicIcon name="chevron-forward" size="sm" color="textSecondary" />
|
|
110
94
|
</TouchableOpacity>
|
|
111
95
|
)}
|
|
112
96
|
|
|
113
|
-
<TouchableOpacity
|
|
114
|
-
style={[actionButtonStyle.container, { borderColor: tokens.colors.border }]}
|
|
115
|
-
onPress={handleLogout}
|
|
116
|
-
activeOpacity={0.7}
|
|
117
|
-
>
|
|
97
|
+
<TouchableOpacity style={[actionButtonStyle.container, { borderColor: tokens.colors.border }]} onPress={handleLogout} activeOpacity={0.7}>
|
|
118
98
|
<AtomicIcon name="log-out-outline" size="md" color="error" />
|
|
119
|
-
<AtomicText style={actionButtonStyle.text} color="error">
|
|
120
|
-
{logoutText}
|
|
121
|
-
</AtomicText>
|
|
99
|
+
<AtomicText style={actionButtonStyle.text} color="error">{logoutText}</AtomicText>
|
|
122
100
|
<AtomicIcon name="chevron-forward" size="sm" color="textSecondary" />
|
|
123
101
|
</TouchableOpacity>
|
|
124
102
|
|
|
125
|
-
<TouchableOpacity
|
|
126
|
-
style={[actionButtonStyle.container, { borderColor: tokens.colors.border }]}
|
|
127
|
-
onPress={handleDeleteAccount}
|
|
128
|
-
activeOpacity={0.7}
|
|
129
|
-
>
|
|
103
|
+
<TouchableOpacity style={[actionButtonStyle.container, { borderColor: tokens.colors.border }]} onPress={handleDeleteAccount} activeOpacity={0.7}>
|
|
130
104
|
<AtomicIcon name="trash-outline" size="md" color="error" />
|
|
131
|
-
<AtomicText style={actionButtonStyle.text} color="error">
|
|
132
|
-
{deleteAccountText}
|
|
133
|
-
</AtomicText>
|
|
105
|
+
<AtomicText style={actionButtonStyle.text} color="error">{deleteAccountText}</AtomicText>
|
|
134
106
|
<AtomicIcon name="chevron-forward" size="sm" color="textSecondary" />
|
|
135
107
|
</TouchableOpacity>
|
|
136
108
|
</View>
|
|
@@ -138,7 +110,5 @@ export const AccountActions: React.FC<AccountActionsProps> = ({ config }) => {
|
|
|
138
110
|
};
|
|
139
111
|
|
|
140
112
|
const styles = StyleSheet.create({
|
|
141
|
-
container: {
|
|
142
|
-
gap: 12,
|
|
143
|
-
},
|
|
113
|
+
container: { gap: 12 },
|
|
144
114
|
});
|
|
@@ -9,128 +9,98 @@ import { useAppDesignTokens, AtomicText, AtomicIcon, AtomicAvatar } from "@umitu
|
|
|
9
9
|
import { ProfileBenefitsList } from "./ProfileBenefitsList";
|
|
10
10
|
|
|
11
11
|
export interface ProfileSectionConfig {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
displayName?: string;
|
|
13
|
+
userId?: string;
|
|
14
|
+
isAnonymous: boolean;
|
|
15
|
+
avatarUrl?: string;
|
|
16
|
+
accountSettingsRoute?: string;
|
|
17
|
+
benefits?: string[];
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export interface ProfileSectionProps {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
profile: ProfileSectionConfig;
|
|
22
|
+
onPress?: () => void;
|
|
23
|
+
onSignIn?: () => void;
|
|
24
|
+
signInText?: string;
|
|
25
|
+
anonymousText?: string;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export const ProfileSection: React.FC<ProfileSectionProps> = ({
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
profile,
|
|
30
|
+
onPress,
|
|
31
|
+
onSignIn,
|
|
32
|
+
signInText,
|
|
33
|
+
anonymousText,
|
|
34
34
|
}) => {
|
|
35
|
-
|
|
35
|
+
const tokens = useAppDesignTokens();
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
const handlePress = () => {
|
|
38
|
+
if (profile.isAnonymous && onSignIn) {
|
|
39
|
+
onSignIn();
|
|
40
|
+
} else if (onPress) {
|
|
41
|
+
onPress();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
return (
|
|
46
|
+
<TouchableOpacity
|
|
47
|
+
style={[styles.container, { backgroundColor: tokens.colors.surface }]}
|
|
48
|
+
onPress={handlePress}
|
|
49
|
+
activeOpacity={0.7}
|
|
50
|
+
disabled={!onPress && !onSignIn}
|
|
51
|
+
>
|
|
52
|
+
<View style={styles.content}>
|
|
53
|
+
<View style={styles.avatarContainer}>
|
|
54
|
+
<AtomicAvatar
|
|
55
|
+
source={profile.avatarUrl ? { uri: profile.avatarUrl } : undefined}
|
|
56
|
+
name={profile.displayName || (profile.isAnonymous ? anonymousText : signInText)}
|
|
57
|
+
size="md"
|
|
58
|
+
/>
|
|
59
|
+
</View>
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
<AtomicText
|
|
72
|
-
type="bodySmall"
|
|
73
|
-
color="textSecondary"
|
|
74
|
-
numberOfLines={1}
|
|
75
|
-
>
|
|
76
|
-
{profile.userId}
|
|
77
|
-
</AtomicText>
|
|
78
|
-
)}
|
|
79
|
-
</View>
|
|
61
|
+
<View style={styles.info}>
|
|
62
|
+
<AtomicText type="titleMedium" color="textPrimary" numberOfLines={1} style={styles.displayName}>
|
|
63
|
+
{profile.displayName}
|
|
64
|
+
</AtomicText>
|
|
65
|
+
{profile.userId && (
|
|
66
|
+
<AtomicText type="bodySmall" color="textSecondary" numberOfLines={1}>
|
|
67
|
+
{profile.userId}
|
|
68
|
+
</AtomicText>
|
|
69
|
+
)}
|
|
70
|
+
</View>
|
|
80
71
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
72
|
+
{onPress && !profile.isAnonymous && (
|
|
73
|
+
<AtomicIcon name="chevron-forward" size="sm" color="textSecondary" />
|
|
74
|
+
)}
|
|
75
|
+
</View>
|
|
85
76
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
77
|
+
{profile.isAnonymous && onSignIn && (
|
|
78
|
+
<View style={[styles.ctaContainer, { borderTopColor: tokens.colors.border }]}>
|
|
79
|
+
{profile.benefits && profile.benefits.length > 0 && (
|
|
80
|
+
<ProfileBenefitsList benefits={profile.benefits} />
|
|
81
|
+
)}
|
|
91
82
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
83
|
+
<TouchableOpacity
|
|
84
|
+
style={[styles.ctaButton, { backgroundColor: tokens.colors.primary }]}
|
|
85
|
+
onPress={onSignIn}
|
|
86
|
+
activeOpacity={0.8}
|
|
87
|
+
>
|
|
88
|
+
<AtomicText type="labelLarge" style={{ color: tokens.colors.onPrimary }}>
|
|
89
|
+
{signInText}
|
|
90
|
+
</AtomicText>
|
|
91
|
+
</TouchableOpacity>
|
|
92
|
+
</View>
|
|
93
|
+
)}
|
|
94
|
+
</TouchableOpacity>
|
|
95
|
+
);
|
|
105
96
|
};
|
|
106
97
|
|
|
107
98
|
const styles = StyleSheet.create({
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
alignItems: "center",
|
|
116
|
-
},
|
|
117
|
-
avatarContainer: {
|
|
118
|
-
marginRight: 12,
|
|
119
|
-
},
|
|
120
|
-
info: {
|
|
121
|
-
flex: 1,
|
|
122
|
-
},
|
|
123
|
-
displayName: {
|
|
124
|
-
marginBottom: 2,
|
|
125
|
-
},
|
|
126
|
-
ctaContainer: {
|
|
127
|
-
marginTop: 12,
|
|
128
|
-
paddingTop: 12,
|
|
129
|
-
borderTopWidth: 1,
|
|
130
|
-
},
|
|
131
|
-
ctaButton: {
|
|
132
|
-
paddingVertical: 12,
|
|
133
|
-
borderRadius: 8,
|
|
134
|
-
alignItems: "center",
|
|
135
|
-
},
|
|
99
|
+
container: { borderRadius: 12, padding: 16, marginBottom: 16 },
|
|
100
|
+
content: { flexDirection: "row", alignItems: "center" },
|
|
101
|
+
avatarContainer: { marginRight: 12 },
|
|
102
|
+
info: { flex: 1 },
|
|
103
|
+
displayName: { marginBottom: 2 },
|
|
104
|
+
ctaContainer: { marginTop: 12, paddingTop: 12, borderTopWidth: 1 },
|
|
105
|
+
ctaButton: { paddingVertical: 12, borderRadius: 8, alignItems: "center" },
|
|
136
106
|
});
|
|
@@ -119,10 +119,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|
|
119
119
|
style={styles.input}
|
|
120
120
|
/>
|
|
121
121
|
{password.length > 0 && (
|
|
122
|
-
<PasswordStrengthIndicator
|
|
123
|
-
translations={translations.passwordStrength}
|
|
124
|
-
requirements={passwordRequirements}
|
|
125
|
-
/>
|
|
122
|
+
<PasswordStrengthIndicator translations={translations.passwordStrength} requirements={passwordRequirements} />
|
|
126
123
|
)}
|
|
127
124
|
|
|
128
125
|
<AtomicInput
|
|
@@ -144,10 +141,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|
|
144
141
|
style={styles.input}
|
|
145
142
|
/>
|
|
146
143
|
{confirmPassword.length > 0 && (
|
|
147
|
-
<PasswordMatchIndicator
|
|
148
|
-
translations={translations.passwordMatch}
|
|
149
|
-
isMatch={passwordsMatch}
|
|
150
|
-
/>
|
|
144
|
+
<PasswordMatchIndicator translations={translations.passwordMatch} isMatch={passwordsMatch} />
|
|
151
145
|
)}
|
|
152
146
|
|
|
153
147
|
<AuthErrorDisplay error={displayError} />
|
|
@@ -155,24 +149,14 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|
|
155
149
|
<AtomicButton
|
|
156
150
|
variant="primary"
|
|
157
151
|
onPress={() => { void handleSignUp(); }}
|
|
158
|
-
disabled={
|
|
159
|
-
loading ||
|
|
160
|
-
!email.trim() ||
|
|
161
|
-
!password.trim() ||
|
|
162
|
-
!confirmPassword.trim()
|
|
163
|
-
}
|
|
152
|
+
disabled={loading || !email.trim() || !password.trim() || !confirmPassword.trim()}
|
|
164
153
|
fullWidth
|
|
165
154
|
style={styles.signUpButton}
|
|
166
155
|
>
|
|
167
156
|
{translations.signUp}
|
|
168
157
|
</AtomicButton>
|
|
169
158
|
|
|
170
|
-
<AuthLink
|
|
171
|
-
text={translations.alreadyHaveAccount}
|
|
172
|
-
linkText={translations.signIn}
|
|
173
|
-
onPress={onNavigateToLogin}
|
|
174
|
-
disabled={loading}
|
|
175
|
-
/>
|
|
159
|
+
<AuthLink text={translations.alreadyHaveAccount} linkText={translations.signIn} onPress={onNavigateToLogin} disabled={loading} />
|
|
176
160
|
|
|
177
161
|
<AuthLegalLinks
|
|
178
162
|
translations={translations.legal}
|
|
@@ -187,12 +171,6 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|
|
187
171
|
};
|
|
188
172
|
|
|
189
173
|
const styles = StyleSheet.create({
|
|
190
|
-
input: {
|
|
191
|
-
|
|
192
|
-
},
|
|
193
|
-
signUpButton: {
|
|
194
|
-
minHeight: 52,
|
|
195
|
-
marginBottom: 16,
|
|
196
|
-
marginTop: 8,
|
|
197
|
-
},
|
|
174
|
+
input: { marginBottom: 20 },
|
|
175
|
+
signUpButton: { minHeight: 52, marginBottom: 16, marginTop: 8 },
|
|
198
176
|
});
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useAuth Hook
|
|
3
3
|
* React hook for authentication state management
|
|
4
|
-
*
|
|
5
|
-
* Uses centralized Zustand store for auth state.
|
|
6
|
-
* Single source of truth - no duplicate subscriptions.
|
|
7
4
|
*/
|
|
8
5
|
|
|
9
6
|
import { useCallback } from "react";
|
|
@@ -32,44 +29,23 @@ import {
|
|
|
32
29
|
import type { AuthUser } from "../../domain/entities/AuthUser";
|
|
33
30
|
|
|
34
31
|
export interface UseAuthResult {
|
|
35
|
-
/** Current authenticated user */
|
|
36
32
|
user: AuthUser | null;
|
|
37
|
-
/** Current user ID (uid) */
|
|
38
33
|
userId: string | null;
|
|
39
|
-
/** Current user type */
|
|
40
34
|
userType: UserType;
|
|
41
|
-
/** Whether auth state is loading */
|
|
42
35
|
loading: boolean;
|
|
43
|
-
/** Whether auth is ready (initialized and not loading) */
|
|
44
36
|
isAuthReady: boolean;
|
|
45
|
-
/** Whether user is anonymous */
|
|
46
37
|
isAnonymous: boolean;
|
|
47
|
-
/** Whether user is authenticated (not anonymous) */
|
|
48
38
|
isAuthenticated: boolean;
|
|
49
|
-
/** Whether user is a registered user (authenticated AND not anonymous) */
|
|
50
39
|
isRegisteredUser: boolean;
|
|
51
|
-
/** Current error message */
|
|
52
40
|
error: string | null;
|
|
53
|
-
/** Sign up function */
|
|
54
41
|
signUp: (email: string, password: string, displayName?: string) => Promise<void>;
|
|
55
|
-
/** Sign in function */
|
|
56
42
|
signIn: (email: string, password: string) => Promise<void>;
|
|
57
|
-
/** Sign out function */
|
|
58
43
|
signOut: () => Promise<void>;
|
|
59
|
-
/** Continue anonymously function */
|
|
60
44
|
continueAnonymously: () => Promise<void>;
|
|
61
|
-
/** Set error manually (for form validation, etc.) */
|
|
62
45
|
setError: (error: string | null) => void;
|
|
63
46
|
}
|
|
64
47
|
|
|
65
|
-
/**
|
|
66
|
-
* Hook for authentication state management
|
|
67
|
-
*
|
|
68
|
-
* Uses centralized Zustand store - all components share the same state.
|
|
69
|
-
* Must call initializeAuthListener() once in app root.
|
|
70
|
-
*/
|
|
71
48
|
export function useAuth(): UseAuthResult {
|
|
72
|
-
// State from store - using typed selectors
|
|
73
49
|
const user = useAuthStore(selectUser);
|
|
74
50
|
const loading = useAuthStore(selectLoading);
|
|
75
51
|
const error = useAuthStore(selectError);
|
|
@@ -79,13 +55,10 @@ export function useAuth(): UseAuthResult {
|
|
|
79
55
|
const isAnonymous = useAuthStore(selectIsAnonymous);
|
|
80
56
|
const isAuthReady = useAuthStore(selectIsAuthReady);
|
|
81
57
|
const isRegisteredUser = useAuthStore(selectIsRegisteredUser);
|
|
82
|
-
|
|
83
|
-
// Actions from store - using typed selectors
|
|
84
58
|
const setLoading = useAuthStore(selectSetLoading);
|
|
85
59
|
const setError = useAuthStore(selectSetError);
|
|
86
60
|
const setIsAnonymous = useAuthStore(selectSetIsAnonymous);
|
|
87
61
|
|
|
88
|
-
// Mutations
|
|
89
62
|
const signInMutation = useSignInMutation();
|
|
90
63
|
const signUpMutation = useSignUpMutation();
|
|
91
64
|
const signOutMutation = useSignOutMutation();
|
|
@@ -99,8 +72,7 @@ export function useAuth(): UseAuthResult {
|
|
|
99
72
|
await signUpMutation.mutateAsync({ email, password, displayName });
|
|
100
73
|
setIsAnonymous(false);
|
|
101
74
|
} catch (err: unknown) {
|
|
102
|
-
|
|
103
|
-
setError(errorMessage);
|
|
75
|
+
setError(err instanceof Error ? err.message : "Sign up failed");
|
|
104
76
|
throw err;
|
|
105
77
|
} finally {
|
|
106
78
|
setLoading(false);
|
|
@@ -117,8 +89,7 @@ export function useAuth(): UseAuthResult {
|
|
|
117
89
|
await signInMutation.mutateAsync({ email, password });
|
|
118
90
|
setIsAnonymous(false);
|
|
119
91
|
} catch (err: unknown) {
|
|
120
|
-
|
|
121
|
-
setError(errorMessage);
|
|
92
|
+
setError(err instanceof Error ? err.message : "Sign in failed");
|
|
122
93
|
throw err;
|
|
123
94
|
} finally {
|
|
124
95
|
setLoading(false);
|
|
@@ -133,8 +104,7 @@ export function useAuth(): UseAuthResult {
|
|
|
133
104
|
setError(null);
|
|
134
105
|
await signOutMutation.mutateAsync();
|
|
135
106
|
} catch (err: unknown) {
|
|
136
|
-
|
|
137
|
-
setError(errorMessage);
|
|
107
|
+
setError(err instanceof Error ? err.message : "Sign out failed");
|
|
138
108
|
throw err;
|
|
139
109
|
} finally {
|
|
140
110
|
setLoading(false);
|
|
@@ -147,7 +117,6 @@ export function useAuth(): UseAuthResult {
|
|
|
147
117
|
await anonymousModeMutation.mutateAsync();
|
|
148
118
|
setIsAnonymous(true);
|
|
149
119
|
} catch {
|
|
150
|
-
// Silently fail - anonymous mode is optional
|
|
151
120
|
setIsAnonymous(true);
|
|
152
121
|
} finally {
|
|
153
122
|
setLoading(false);
|
|
@@ -155,19 +124,7 @@ export function useAuth(): UseAuthResult {
|
|
|
155
124
|
}, [setIsAnonymous, setLoading, anonymousModeMutation]);
|
|
156
125
|
|
|
157
126
|
return {
|
|
158
|
-
user,
|
|
159
|
-
|
|
160
|
-
userType,
|
|
161
|
-
loading,
|
|
162
|
-
isAuthReady,
|
|
163
|
-
isAnonymous,
|
|
164
|
-
isAuthenticated,
|
|
165
|
-
isRegisteredUser,
|
|
166
|
-
error,
|
|
167
|
-
signUp,
|
|
168
|
-
signIn,
|
|
169
|
-
signOut,
|
|
170
|
-
continueAnonymously,
|
|
171
|
-
setError,
|
|
127
|
+
user, userId, userType, loading, isAuthReady, isAnonymous, isAuthenticated, isRegisteredUser, error,
|
|
128
|
+
signUp, signIn, signOut, continueAnonymously, setError,
|
|
172
129
|
};
|
|
173
130
|
}
|
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useGoogleAuth Hook
|
|
3
3
|
* Handles Google OAuth flow using expo-auth-session and Firebase auth
|
|
4
|
-
*
|
|
5
|
-
* This hook provides complete Google sign-in flow:
|
|
6
|
-
* 1. OAuth flow via expo-auth-session
|
|
7
|
-
* 2. Firebase authentication with the obtained token
|
|
8
4
|
*/
|
|
9
5
|
|
|
10
6
|
import { useState, useCallback, useEffect } from "react";
|
|
11
|
-
import {
|
|
12
|
-
useSocialAuth,
|
|
13
|
-
type SocialAuthConfig,
|
|
14
|
-
type SocialAuthResult,
|
|
15
|
-
} from "@umituz/react-native-firebase";
|
|
16
|
-
|
|
7
|
+
import { useSocialAuth, type SocialAuthConfig, type SocialAuthResult } from "@umituz/react-native-firebase";
|
|
17
8
|
import * as Google from "expo-auth-session/providers/google";
|
|
18
9
|
import * as WebBrowser from "expo-web-browser";
|
|
19
10
|
|
|
@@ -34,38 +25,25 @@ export interface UseGoogleAuthResult {
|
|
|
34
25
|
|
|
35
26
|
const PLACEHOLDER_CLIENT_ID = "000000000000-placeholder.apps.googleusercontent.com";
|
|
36
27
|
|
|
37
|
-
/**
|
|
38
|
-
* Validate Google auth config
|
|
39
|
-
*/
|
|
40
28
|
function validateGoogleConfig(config?: GoogleAuthConfig): boolean {
|
|
41
29
|
if (!config) return false;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return hasValidClientId;
|
|
30
|
+
return !!(
|
|
31
|
+
(config.iosClientId && config.iosClientId !== PLACEHOLDER_CLIENT_ID) ||
|
|
32
|
+
(config.webClientId && config.webClientId !== PLACEHOLDER_CLIENT_ID) ||
|
|
33
|
+
(config.androidClientId && config.androidClientId !== PLACEHOLDER_CLIENT_ID)
|
|
34
|
+
);
|
|
49
35
|
}
|
|
50
36
|
|
|
51
|
-
/**
|
|
52
|
-
* Hook for Google authentication with expo-auth-session
|
|
53
|
-
*/
|
|
54
37
|
export function useGoogleAuth(config?: GoogleAuthConfig): UseGoogleAuthResult {
|
|
55
38
|
const [isLoading, setIsLoading] = useState(false);
|
|
56
39
|
const [googleError, setGoogleError] = useState<string | null>(null);
|
|
57
|
-
|
|
58
40
|
const googleConfigured = validateGoogleConfig(config);
|
|
59
41
|
|
|
60
|
-
const
|
|
42
|
+
const { signInWithGoogleToken, googleLoading: firebaseLoading } = useSocialAuth({
|
|
61
43
|
google: config,
|
|
62
44
|
apple: { enabled: false },
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const { signInWithGoogleToken, googleLoading: firebaseLoading } =
|
|
66
|
-
useSocialAuth(socialAuthConfig);
|
|
45
|
+
} as SocialAuthConfig);
|
|
67
46
|
|
|
68
|
-
// Use Google auth request if available
|
|
69
47
|
const authRequest = Google?.useAuthRequest({
|
|
70
48
|
iosClientId: config?.iosClientId || PLACEHOLDER_CLIENT_ID,
|
|
71
49
|
webClientId: config?.webClientId || PLACEHOLDER_CLIENT_ID,
|
|
@@ -76,7 +54,6 @@ export function useGoogleAuth(config?: GoogleAuthConfig): UseGoogleAuthResult {
|
|
|
76
54
|
const googleResponse = authRequest?.[1] ?? null;
|
|
77
55
|
const promptGoogleAsync = authRequest?.[2];
|
|
78
56
|
|
|
79
|
-
// Handle Google OAuth response
|
|
80
57
|
useEffect(() => {
|
|
81
58
|
if (googleResponse?.type === "success") {
|
|
82
59
|
const idToken = googleResponse.authentication?.idToken;
|
|
@@ -84,13 +61,8 @@ export function useGoogleAuth(config?: GoogleAuthConfig): UseGoogleAuthResult {
|
|
|
84
61
|
setIsLoading(true);
|
|
85
62
|
setGoogleError(null);
|
|
86
63
|
signInWithGoogleToken(idToken)
|
|
87
|
-
.catch((error) => {
|
|
88
|
-
|
|
89
|
-
setGoogleError(errorMessage);
|
|
90
|
-
})
|
|
91
|
-
.finally(() => {
|
|
92
|
-
setIsLoading(false);
|
|
93
|
-
});
|
|
64
|
+
.catch((error) => { setGoogleError(error instanceof Error ? error.message : "Firebase sign-in failed"); })
|
|
65
|
+
.finally(() => { setIsLoading(false); });
|
|
94
66
|
}
|
|
95
67
|
} else if (googleResponse?.type === "error") {
|
|
96
68
|
setGoogleError("Google authentication failed");
|
|
@@ -123,10 +95,7 @@ export function useGoogleAuth(config?: GoogleAuthConfig): UseGoogleAuthResult {
|
|
|
123
95
|
const result = await promptGoogleAsync();
|
|
124
96
|
|
|
125
97
|
if (result.type === "success" && result.authentication?.idToken) {
|
|
126
|
-
|
|
127
|
-
result.authentication.idToken,
|
|
128
|
-
);
|
|
129
|
-
return firebaseResult;
|
|
98
|
+
return await signInWithGoogleToken(result.authentication.idToken);
|
|
130
99
|
}
|
|
131
100
|
|
|
132
101
|
if (result.type === "cancel") {
|
|
@@ -141,10 +110,7 @@ export function useGoogleAuth(config?: GoogleAuthConfig): UseGoogleAuthResult {
|
|
|
141
110
|
} catch (error) {
|
|
142
111
|
const errorMessage = error instanceof Error ? error.message : "Google sign-in failed";
|
|
143
112
|
setGoogleError(errorMessage);
|
|
144
|
-
return {
|
|
145
|
-
success: false,
|
|
146
|
-
error: errorMessage,
|
|
147
|
-
};
|
|
113
|
+
return { success: false, error: errorMessage };
|
|
148
114
|
} finally {
|
|
149
115
|
setIsLoading(false);
|
|
150
116
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Operation Utilities
|
|
3
|
+
* Shared error handling for authentication operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback } from "react";
|
|
7
|
+
import type { MutationFunction } from "@tanstack/react-query";
|
|
8
|
+
|
|
9
|
+
export interface AuthOperationOptions {
|
|
10
|
+
setLoading: (loading: boolean) => void;
|
|
11
|
+
setError: (error: string | null) => void;
|
|
12
|
+
onSuccess?: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create an auth operation wrapper with consistent error handling
|
|
17
|
+
*/
|
|
18
|
+
export function createAuthOperation<T>(
|
|
19
|
+
mutation: MutationFunction<unknown, T>,
|
|
20
|
+
options: AuthOperationOptions
|
|
21
|
+
) {
|
|
22
|
+
const { setLoading, setError, onSuccess } = options;
|
|
23
|
+
|
|
24
|
+
return useCallback(
|
|
25
|
+
async (params: T) => {
|
|
26
|
+
try {
|
|
27
|
+
setLoading(true);
|
|
28
|
+
setError(null);
|
|
29
|
+
await mutation.mutateAsync(params);
|
|
30
|
+
onSuccess?.();
|
|
31
|
+
} catch (err: unknown) {
|
|
32
|
+
const errorMessage = err instanceof Error ? err.message : "Operation failed";
|
|
33
|
+
setError(errorMessage);
|
|
34
|
+
throw err;
|
|
35
|
+
} finally {
|
|
36
|
+
setLoading(false);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
[setLoading, setError, onSuccess, mutation]
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create auth operation that doesn't throw on failure
|
|
45
|
+
*/
|
|
46
|
+
export function createSilentAuthOperation<T>(
|
|
47
|
+
mutation: MutationFunction<unknown, T>,
|
|
48
|
+
options: AuthOperationOptions
|
|
49
|
+
) {
|
|
50
|
+
const { setLoading, setError, onSuccess } = options;
|
|
51
|
+
|
|
52
|
+
return useCallback(
|
|
53
|
+
async (params?: T) => {
|
|
54
|
+
try {
|
|
55
|
+
setLoading(true);
|
|
56
|
+
setError(null);
|
|
57
|
+
await mutation.mutateAsync(params);
|
|
58
|
+
onSuccess?.();
|
|
59
|
+
} catch {
|
|
60
|
+
// Silently fail
|
|
61
|
+
onSuccess?.();
|
|
62
|
+
} finally {
|
|
63
|
+
setLoading(false);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
[setLoading, setError, onSuccess, mutation]
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -39,16 +39,7 @@ export interface ProfileFormValues {
|
|
|
39
39
|
email: string;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
* Validate login form fields
|
|
44
|
-
* @param values - Form values to validate
|
|
45
|
-
* @param getErrorMessage - Function to get localized error messages
|
|
46
|
-
* @returns Validation result
|
|
47
|
-
*/
|
|
48
|
-
export function validateLoginForm(
|
|
49
|
-
values: LoginFormValues,
|
|
50
|
-
getErrorMessage: (key: string) => string
|
|
51
|
-
): FormValidationResult {
|
|
42
|
+
export function validateLoginForm(values: LoginFormValues, getErrorMessage: (key: string) => string): FormValidationResult {
|
|
52
43
|
const errors: FormValidationError[] = [];
|
|
53
44
|
|
|
54
45
|
const emailResult = validateEmail(values.email.trim());
|
|
@@ -61,19 +52,9 @@ export function validateLoginForm(
|
|
|
61
52
|
errors.push({ field: "password", message: getErrorMessage(passwordResult.error) });
|
|
62
53
|
}
|
|
63
54
|
|
|
64
|
-
return {
|
|
65
|
-
isValid: errors.length === 0,
|
|
66
|
-
errors,
|
|
67
|
-
};
|
|
55
|
+
return { isValid: errors.length === 0, errors };
|
|
68
56
|
}
|
|
69
57
|
|
|
70
|
-
/**
|
|
71
|
-
* Validate register form fields
|
|
72
|
-
* @param values - Form values to validate
|
|
73
|
-
* @param getErrorMessage - Function to get localized error messages
|
|
74
|
-
* @param passwordConfig - Password configuration
|
|
75
|
-
* @returns Validation result
|
|
76
|
-
*/
|
|
77
58
|
export function validateRegisterForm(
|
|
78
59
|
values: RegisterFormValues,
|
|
79
60
|
getErrorMessage: (key: string) => string,
|
|
@@ -96,17 +77,9 @@ export function validateRegisterForm(
|
|
|
96
77
|
errors.push({ field: "confirmPassword", message: getErrorMessage(confirmResult.error) });
|
|
97
78
|
}
|
|
98
79
|
|
|
99
|
-
return {
|
|
100
|
-
isValid: errors.length === 0,
|
|
101
|
-
errors,
|
|
102
|
-
};
|
|
80
|
+
return { isValid: errors.length === 0, errors };
|
|
103
81
|
}
|
|
104
82
|
|
|
105
|
-
/**
|
|
106
|
-
* Validate profile form fields
|
|
107
|
-
* @param values - Form values to validate
|
|
108
|
-
* @returns Validation result
|
|
109
|
-
*/
|
|
110
83
|
export function validateProfileForm(values: ProfileFormValues): FormValidationResult {
|
|
111
84
|
const errors: FormValidationError[] = [];
|
|
112
85
|
|
|
@@ -121,20 +94,10 @@ export function validateProfileForm(values: ProfileFormValues): FormValidationRe
|
|
|
121
94
|
}
|
|
122
95
|
}
|
|
123
96
|
|
|
124
|
-
return {
|
|
125
|
-
isValid: errors.length === 0,
|
|
126
|
-
errors,
|
|
127
|
-
};
|
|
97
|
+
return { isValid: errors.length === 0, errors };
|
|
128
98
|
}
|
|
129
99
|
|
|
130
|
-
|
|
131
|
-
* Convert validation errors to field error object
|
|
132
|
-
* @param errors - Validation errors
|
|
133
|
-
* @returns Object mapping field names to error messages
|
|
134
|
-
*/
|
|
135
|
-
export function errorsToFieldErrors(
|
|
136
|
-
errors: FormValidationError[]
|
|
137
|
-
): Record<string, string> {
|
|
100
|
+
export function errorsToFieldErrors(errors: FormValidationError[]): Record<string, string> {
|
|
138
101
|
const result: Record<string, string> = {};
|
|
139
102
|
for (const error of errors) {
|
|
140
103
|
result[error.field] = error.message;
|
|
@@ -142,32 +105,13 @@ export function errorsToFieldErrors(
|
|
|
142
105
|
return result;
|
|
143
106
|
}
|
|
144
107
|
|
|
145
|
-
/**
|
|
146
|
-
* Hook for form validation with error message resolution
|
|
147
|
-
* @param getErrorMessage - Function to get localized error messages
|
|
148
|
-
* @returns Validation functions
|
|
149
|
-
*/
|
|
150
108
|
export function useFormValidation(getErrorMessage: (key: string) => string) {
|
|
151
|
-
const validateLogin = useCallback(
|
|
152
|
-
(values: LoginFormValues) => validateLoginForm(values, getErrorMessage),
|
|
153
|
-
[getErrorMessage]
|
|
154
|
-
);
|
|
155
|
-
|
|
109
|
+
const validateLogin = useCallback((values: LoginFormValues) => validateLoginForm(values, getErrorMessage), [getErrorMessage]);
|
|
156
110
|
const validateRegister = useCallback(
|
|
157
|
-
(values: RegisterFormValues, passwordConfig: PasswordConfig) =>
|
|
158
|
-
validateRegisterForm(values, getErrorMessage, passwordConfig),
|
|
111
|
+
(values: RegisterFormValues, passwordConfig: PasswordConfig) => validateRegisterForm(values, getErrorMessage, passwordConfig),
|
|
159
112
|
[getErrorMessage]
|
|
160
113
|
);
|
|
114
|
+
const validateProfile = useCallback((values: ProfileFormValues) => validateProfileForm(values), []);
|
|
161
115
|
|
|
162
|
-
|
|
163
|
-
(values: ProfileFormValues) => validateProfileForm(values),
|
|
164
|
-
[]
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
validateLogin,
|
|
169
|
-
validateRegister,
|
|
170
|
-
validateProfile,
|
|
171
|
-
errorsToFieldErrors,
|
|
172
|
-
};
|
|
116
|
+
return { validateLogin, validateRegister, validateProfile, errorsToFieldErrors };
|
|
173
117
|
}
|