@umituz/react-native-auth 3.6.57 → 3.6.59
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 +5 -3
- package/src/domain/value-objects/AuthConfig.ts +80 -0
- package/src/index.ts +80 -106
- package/src/infrastructure/providers/FirebaseAuthProvider.ts +9 -4
- package/src/infrastructure/repositories/AuthRepository.ts +5 -0
- package/src/infrastructure/services/AnonymousModeService.ts +16 -10
- package/src/infrastructure/services/AuthEventService.ts +0 -2
- package/src/infrastructure/services/AuthService.ts +8 -8
- package/src/infrastructure/utils/AuthErrorMapper.ts +128 -3
- package/src/infrastructure/utils/validation/sanitization.ts +52 -4
- package/src/presentation/hooks/useAuth.ts +6 -1
- package/src/presentation/hooks/useProfileUpdate.ts +1 -2
- package/src/presentation/providers/AuthProvider.tsx +72 -7
- package/src/presentation/stores/authStore.ts +4 -24
- package/src/presentation/stores/initializeAuthListener.ts +58 -31
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.59",
|
|
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",
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
"type": "git",
|
|
32
32
|
"url": "git+https://github.com/umituz/react-native-auth.git"
|
|
33
33
|
},
|
|
34
|
-
"dependencies": {},
|
|
35
34
|
"peerDependencies": {
|
|
36
35
|
"@react-navigation/native": ">=6.0.0",
|
|
37
36
|
"@react-navigation/stack": ">=6.0.0",
|
|
@@ -62,7 +61,8 @@
|
|
|
62
61
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
63
62
|
"@typescript-eslint/parser": "^7.0.0",
|
|
64
63
|
"@umituz/react-native-design-system": "^2.9.33",
|
|
65
|
-
"@umituz/react-native-firebase": "^1.13.
|
|
64
|
+
"@umituz/react-native-firebase": "^1.13.114",
|
|
65
|
+
"@umituz/react-native-localization": "^3.7.24",
|
|
66
66
|
"eslint": "^8.57.0",
|
|
67
67
|
"expo-apple-authentication": "^6.0.0",
|
|
68
68
|
"expo-application": "^7.0.8",
|
|
@@ -85,7 +85,9 @@
|
|
|
85
85
|
"expo-video": "^3.0.15",
|
|
86
86
|
"expo-web-browser": "^12.0.0",
|
|
87
87
|
"firebase": "^11.0.0",
|
|
88
|
+
"i18next": "^25.8.4",
|
|
88
89
|
"react": "~19.1.0",
|
|
90
|
+
"react-i18next": "^16.5.4",
|
|
89
91
|
"react-native": "~0.81.5",
|
|
90
92
|
"react-native-gesture-handler": "^2.0.0",
|
|
91
93
|
"react-native-safe-area-context": "^4.0.0",
|
|
@@ -56,3 +56,83 @@ export const DEFAULT_AUTH_CONFIG: AuthConfig = {
|
|
|
56
56
|
social: DEFAULT_SOCIAL_CONFIG,
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Configuration validation error
|
|
61
|
+
*/
|
|
62
|
+
export class AuthConfigValidationError extends Error {
|
|
63
|
+
constructor(message: string, public readonly field: string) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = "AuthConfigValidationError";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validate authentication configuration
|
|
71
|
+
* @throws {AuthConfigValidationError} if configuration is invalid
|
|
72
|
+
*/
|
|
73
|
+
export function validateAuthConfig(config: Partial<AuthConfig>): void {
|
|
74
|
+
// Validate password config
|
|
75
|
+
if (config.password) {
|
|
76
|
+
if (typeof config.password.minLength !== "number") {
|
|
77
|
+
throw new AuthConfigValidationError(
|
|
78
|
+
"Password minLength must be a number",
|
|
79
|
+
"password.minLength"
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (config.password.minLength < 4) {
|
|
83
|
+
throw new AuthConfigValidationError(
|
|
84
|
+
"Password minLength must be at least 4 characters",
|
|
85
|
+
"password.minLength"
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (config.password.minLength > 128) {
|
|
89
|
+
throw new AuthConfigValidationError(
|
|
90
|
+
"Password minLength must not exceed 128 characters",
|
|
91
|
+
"password.minLength"
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Validate social auth config
|
|
97
|
+
if (config.social) {
|
|
98
|
+
if (config.social.google) {
|
|
99
|
+
const googleConfig = config.social.google;
|
|
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
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Sanitize and merge auth config with defaults
|
|
115
|
+
* Ensures valid configuration values
|
|
116
|
+
*/
|
|
117
|
+
export function sanitizeAuthConfig(config: Partial<AuthConfig> = {}): AuthConfig {
|
|
118
|
+
// Validate first
|
|
119
|
+
validateAuthConfig(config);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
password: {
|
|
123
|
+
minLength: config.password?.minLength ?? DEFAULT_PASSWORD_CONFIG.minLength,
|
|
124
|
+
},
|
|
125
|
+
social: {
|
|
126
|
+
google: {
|
|
127
|
+
enabled: config.social?.google?.enabled ?? DEFAULT_SOCIAL_CONFIG.google?.enabled ?? false,
|
|
128
|
+
webClientId: config.social?.google?.webClientId,
|
|
129
|
+
iosClientId: config.social?.google?.iosClientId,
|
|
130
|
+
androidClientId: config.social?.google?.androidClientId,
|
|
131
|
+
},
|
|
132
|
+
apple: {
|
|
133
|
+
enabled: config.social?.apple?.enabled ?? DEFAULT_SOCIAL_CONFIG.apple?.enabled ?? false,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* React Native Auth - Public API
|
|
3
3
|
* Single source of truth for all Auth operations.
|
|
4
|
+
*
|
|
5
|
+
* @module Auth
|
|
4
6
|
*/
|
|
5
7
|
|
|
8
|
+
// =============================================================================
|
|
6
9
|
// DOMAIN LAYER
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
7
12
|
export type { AuthUser, AuthProviderType } from './domain/entities/AuthUser';
|
|
8
13
|
export {
|
|
9
14
|
AuthError,
|
|
@@ -33,7 +38,14 @@ export {
|
|
|
33
38
|
DEFAULT_SOCIAL_CONFIG,
|
|
34
39
|
} from './domain/value-objects/AuthConfig';
|
|
35
40
|
|
|
41
|
+
export type { UserProfile, UpdateProfileParams } from './domain/entities/UserProfile';
|
|
42
|
+
export { migrateUserData, configureMigration } from './domain/utils/migration';
|
|
43
|
+
export type { MigrationConfig } from './domain/utils/migration';
|
|
44
|
+
|
|
45
|
+
// =============================================================================
|
|
36
46
|
// APPLICATION LAYER
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
37
49
|
export type { IAuthService, SignUpParams, SignInParams } from './application/ports/IAuthService';
|
|
38
50
|
export type {
|
|
39
51
|
IAuthProvider,
|
|
@@ -42,7 +54,10 @@ export type {
|
|
|
42
54
|
SocialSignInResult,
|
|
43
55
|
} from './application/ports/IAuthProvider';
|
|
44
56
|
|
|
57
|
+
// =============================================================================
|
|
45
58
|
// INFRASTRUCTURE LAYER
|
|
59
|
+
// =============================================================================
|
|
60
|
+
|
|
46
61
|
export { FirebaseAuthProvider } from './infrastructure/providers/FirebaseAuthProvider';
|
|
47
62
|
export {
|
|
48
63
|
AuthService,
|
|
@@ -50,29 +65,33 @@ export {
|
|
|
50
65
|
getAuthService,
|
|
51
66
|
resetAuthService,
|
|
52
67
|
} from './infrastructure/services/AuthService';
|
|
68
|
+
export type { IStorageProvider } from './infrastructure/types/Storage.types';
|
|
53
69
|
export {
|
|
54
70
|
createStorageProvider,
|
|
55
71
|
StorageProviderAdapter,
|
|
56
72
|
} from './infrastructure/adapters/StorageProviderAdapter';
|
|
57
|
-
export type { IStorageProvider } from './infrastructure/types/Storage.types';
|
|
58
73
|
export {
|
|
59
74
|
ensureUserDocument,
|
|
60
75
|
markUserDeleted,
|
|
61
76
|
configureUserDocumentService,
|
|
62
77
|
} from './infrastructure/services/UserDocumentService';
|
|
78
|
+
export type {
|
|
79
|
+
UserDocumentConfig,
|
|
80
|
+
UserDocumentExtras,
|
|
81
|
+
UserDocumentUser,
|
|
82
|
+
} from './infrastructure/services/UserDocumentService';
|
|
83
|
+
|
|
63
84
|
export {
|
|
64
85
|
initializeAuth,
|
|
65
86
|
isAuthInitialized,
|
|
66
87
|
resetAuthInitialization,
|
|
67
88
|
} from './infrastructure/services/initializeAuth';
|
|
68
89
|
export type { InitializeAuthOptions } from './infrastructure/services/initializeAuth';
|
|
69
|
-
export type {
|
|
70
|
-
UserDocumentConfig,
|
|
71
|
-
UserDocumentExtras,
|
|
72
|
-
UserDocumentUser,
|
|
73
|
-
} from './infrastructure/services/UserDocumentService';
|
|
74
90
|
|
|
91
|
+
// =============================================================================
|
|
75
92
|
// VALIDATION
|
|
93
|
+
// =============================================================================
|
|
94
|
+
|
|
76
95
|
export {
|
|
77
96
|
validateEmail,
|
|
78
97
|
validatePasswordForLogin,
|
|
@@ -84,34 +103,8 @@ export type {
|
|
|
84
103
|
ValidationResult,
|
|
85
104
|
PasswordStrengthResult,
|
|
86
105
|
PasswordRequirements,
|
|
87
|
-
ValidationConfig,
|
|
88
106
|
} from './infrastructure/utils/AuthValidation';
|
|
89
|
-
|
|
90
|
-
validateRequired,
|
|
91
|
-
validatePattern,
|
|
92
|
-
} from './infrastructure/utils/validation/BaseValidators';
|
|
93
|
-
export {
|
|
94
|
-
validateMinLength,
|
|
95
|
-
validateMaxLength,
|
|
96
|
-
validatePhone,
|
|
97
|
-
} from './infrastructure/utils/validation/StringValidators';
|
|
98
|
-
export {
|
|
99
|
-
validateNumberRange,
|
|
100
|
-
validatePositiveNumber,
|
|
101
|
-
validateAge,
|
|
102
|
-
} from './infrastructure/utils/validation/NumberValidators';
|
|
103
|
-
export {
|
|
104
|
-
calculateAge,
|
|
105
|
-
validateDateOfBirth,
|
|
106
|
-
validateDateRange,
|
|
107
|
-
} from './infrastructure/utils/validation/DateValidators';
|
|
108
|
-
export {
|
|
109
|
-
validateEnum,
|
|
110
|
-
validateTags,
|
|
111
|
-
} from './infrastructure/utils/validation/CollectionValidators';
|
|
112
|
-
export {
|
|
113
|
-
batchValidate,
|
|
114
|
-
} from './infrastructure/utils/validation/FormValidators';
|
|
107
|
+
|
|
115
108
|
export {
|
|
116
109
|
SECURITY_LIMITS,
|
|
117
110
|
sanitizeWhitespace,
|
|
@@ -122,117 +115,98 @@ export {
|
|
|
122
115
|
containsDangerousChars,
|
|
123
116
|
isWithinLengthLimit,
|
|
124
117
|
} from './infrastructure/utils/validation/sanitization';
|
|
125
|
-
export {
|
|
126
|
-
|
|
127
|
-
|
|
118
|
+
export type { SecurityLimitKey } from './infrastructure/utils/validation/sanitization';
|
|
119
|
+
|
|
120
|
+
// =============================================================================
|
|
121
|
+
// PRESENTATION LAYER - Hooks
|
|
122
|
+
// =============================================================================
|
|
128
123
|
|
|
129
|
-
// PRESENTATION LAYER
|
|
130
|
-
export { AuthProvider } from './presentation/providers/AuthProvider';
|
|
131
124
|
export { useAuth } from './presentation/hooks/useAuth';
|
|
132
125
|
export type { UseAuthResult } from './presentation/hooks/useAuth';
|
|
126
|
+
|
|
133
127
|
export { useLoginForm } from './presentation/hooks/useLoginForm';
|
|
134
|
-
export type {
|
|
128
|
+
export type { UseLoginFormConfig, UseLoginFormResult } from './presentation/hooks/useLoginForm';
|
|
129
|
+
|
|
135
130
|
export { useRegisterForm } from './presentation/hooks/useRegisterForm';
|
|
136
|
-
export type {
|
|
131
|
+
export type { UseRegisterFormConfig, UseRegisterFormResult } from './presentation/hooks/useRegisterForm';
|
|
132
|
+
|
|
137
133
|
export { useAuthRequired } from './presentation/hooks/useAuthRequired';
|
|
138
|
-
export type { UseAuthRequiredResult } from './presentation/hooks/useAuthRequired';
|
|
139
134
|
export { useRequireAuth, useUserId } from './presentation/hooks/useRequireAuth';
|
|
135
|
+
|
|
140
136
|
export { useUserProfile } from './presentation/hooks/useUserProfile';
|
|
141
137
|
export type { UserProfileData, UseUserProfileParams } from './presentation/hooks/useUserProfile';
|
|
138
|
+
|
|
142
139
|
export { useAccountManagement } from './presentation/hooks/useAccountManagement';
|
|
143
140
|
export type { UseAccountManagementReturn } from './presentation/hooks/useAccountManagement';
|
|
141
|
+
|
|
144
142
|
export { useProfileUpdate } from './presentation/hooks/useProfileUpdate';
|
|
145
143
|
export type { UseProfileUpdateReturn } from './presentation/hooks/useProfileUpdate';
|
|
144
|
+
|
|
146
145
|
export { useProfileEdit } from './presentation/hooks/useProfileEdit';
|
|
147
|
-
export type {
|
|
146
|
+
export type { ProfileEditFormState, UseProfileEditReturn } from './presentation/hooks/useProfileEdit';
|
|
147
|
+
|
|
148
148
|
export { useSocialLogin } from './presentation/hooks/useSocialLogin';
|
|
149
149
|
export type { UseSocialLoginConfig, UseSocialLoginResult } from './presentation/hooks/useSocialLogin';
|
|
150
|
+
|
|
150
151
|
export { useGoogleAuth } from './presentation/hooks/useGoogleAuth';
|
|
151
|
-
export type { UseGoogleAuthResult
|
|
152
|
+
export type { UseGoogleAuthResult } from './presentation/hooks/useGoogleAuth';
|
|
153
|
+
|
|
152
154
|
export { useAppleAuth } from './presentation/hooks/useAppleAuth';
|
|
153
155
|
export type { UseAppleAuthResult } from './presentation/hooks/useAppleAuth';
|
|
156
|
+
|
|
154
157
|
export { useAuthBottomSheet } from './presentation/hooks/useAuthBottomSheet';
|
|
155
158
|
export type { SocialAuthConfiguration } from './presentation/hooks/useAuthBottomSheet';
|
|
156
159
|
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
// =============================================================================
|
|
161
|
+
// PRESENTATION LAYER - Components
|
|
162
|
+
// =============================================================================
|
|
163
|
+
|
|
164
|
+
export { AuthProvider } from './presentation/providers/AuthProvider';
|
|
165
|
+
export type { ErrorFallbackProps } from './presentation/providers/AuthProvider';
|
|
161
166
|
|
|
162
|
-
// SCREENS & NAVIGATION
|
|
163
167
|
export { LoginScreen } from './presentation/screens/LoginScreen';
|
|
164
|
-
export type {
|
|
168
|
+
export type { LoginScreenProps } from './presentation/screens/LoginScreen';
|
|
169
|
+
|
|
165
170
|
export { RegisterScreen } from './presentation/screens/RegisterScreen';
|
|
166
|
-
export type {
|
|
171
|
+
export type { RegisterScreenProps } from './presentation/screens/RegisterScreen';
|
|
172
|
+
|
|
167
173
|
export { AccountScreen } from './presentation/screens/AccountScreen';
|
|
168
|
-
export type {
|
|
174
|
+
export type { AccountScreenProps } from './presentation/screens/AccountScreen';
|
|
175
|
+
|
|
169
176
|
export { EditProfileScreen } from './presentation/screens/EditProfileScreen';
|
|
170
|
-
export type {
|
|
177
|
+
export type { EditProfileScreenProps } from './presentation/screens/EditProfileScreen';
|
|
178
|
+
|
|
171
179
|
export { ChangePasswordScreen } from './presentation/screens/change-password';
|
|
172
|
-
export type {
|
|
180
|
+
export type { ChangePasswordScreenProps } from './presentation/screens/change-password';
|
|
181
|
+
|
|
173
182
|
export { AuthNavigator } from './presentation/navigation/AuthNavigator';
|
|
174
|
-
export type {
|
|
175
|
-
AuthStackParamList,
|
|
176
|
-
AuthNavigatorTranslations,
|
|
177
|
-
AuthNavigatorProps,
|
|
178
|
-
} from './presentation/navigation/AuthNavigator';
|
|
179
|
-
|
|
180
|
-
// COMPONENTS
|
|
181
|
-
export { AuthHeader } from './presentation/components/AuthHeader';
|
|
182
|
-
export type { AuthHeaderProps } from './presentation/components/AuthHeader';
|
|
183
|
-
export { LoginForm } from './presentation/components/LoginForm';
|
|
184
|
-
export type { LoginFormTranslations, LoginFormProps } from './presentation/components/LoginForm';
|
|
185
|
-
export { RegisterForm } from './presentation/components/RegisterForm';
|
|
186
|
-
export type { RegisterFormTranslations, RegisterFormProps } from './presentation/components/RegisterForm';
|
|
187
|
-
export { AuthLegalLinks } from './presentation/components/AuthLegalLinks';
|
|
188
|
-
export type { AuthLegalLinksTranslations, AuthLegalLinksProps } from './presentation/components/AuthLegalLinks';
|
|
189
|
-
export { PasswordStrengthIndicator } from './presentation/components/PasswordStrengthIndicator';
|
|
190
|
-
export type { PasswordStrengthTranslations, PasswordStrengthIndicatorProps } from './presentation/components/PasswordStrengthIndicator';
|
|
191
|
-
export { PasswordMatchIndicator } from './presentation/components/PasswordMatchIndicator';
|
|
192
|
-
export type { PasswordMatchTranslations, PasswordMatchIndicatorProps } from './presentation/components/PasswordMatchIndicator';
|
|
193
|
-
export { AuthBottomSheet } from './presentation/components/AuthBottomSheet';
|
|
194
|
-
export type { AuthBottomSheetTranslations, AuthBottomSheetProps } from './presentation/components/AuthBottomSheet';
|
|
195
|
-
export { SocialLoginButtons } from './presentation/components/SocialLoginButtons';
|
|
196
|
-
export type { SocialLoginButtonsTranslations, SocialLoginButtonsProps } from './presentation/components/SocialLoginButtons';
|
|
197
|
-
export { ProfileSection } from './presentation/components/ProfileSection';
|
|
198
|
-
export type { ProfileSectionConfig, ProfileSectionProps } from './presentation/components/ProfileSection';
|
|
199
|
-
export { AccountActions } from './presentation/components/AccountActions';
|
|
200
|
-
export type { AccountActionsConfig, AccountActionsProps } from './presentation/components/AccountActions';
|
|
183
|
+
export type { AuthStackParamList } from './presentation/navigation/AuthNavigator';
|
|
201
184
|
|
|
185
|
+
// =============================================================================
|
|
202
186
|
// STORES
|
|
203
|
-
|
|
204
|
-
|
|
187
|
+
// =============================================================================
|
|
188
|
+
|
|
189
|
+
export { useAuthStore } from './presentation/stores/authStore';
|
|
205
190
|
export {
|
|
206
|
-
useAuthStore,
|
|
207
191
|
initializeAuthListener,
|
|
208
192
|
resetAuthListener,
|
|
209
193
|
isAuthListenerInitialized,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
selectIsAnonymous,
|
|
213
|
-
selectUserType,
|
|
214
|
-
selectIsAuthReady,
|
|
215
|
-
selectIsRegisteredUser,
|
|
216
|
-
getUserId,
|
|
217
|
-
getUserType,
|
|
218
|
-
getIsAuthenticated,
|
|
219
|
-
getIsAnonymous,
|
|
220
|
-
getIsRegisteredUser,
|
|
221
|
-
} from './presentation/stores/authStore';
|
|
222
|
-
export type { UserType, AuthState, AuthActions } from './presentation/stores/authStore';
|
|
194
|
+
} from './presentation/stores/initializeAuthListener';
|
|
195
|
+
export type { AuthState, AuthActions, UserType } from './types/auth-store.types';
|
|
223
196
|
export type { AuthListenerOptions } from './types/auth-store.types';
|
|
224
197
|
|
|
198
|
+
// =============================================================================
|
|
225
199
|
// UTILITIES
|
|
226
|
-
|
|
200
|
+
// =============================================================================
|
|
227
201
|
|
|
228
|
-
// App Service Helper (for configureAppServices)
|
|
229
202
|
export {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
} from './
|
|
203
|
+
getAuthErrorLocalizationKey,
|
|
204
|
+
resolveErrorMessage,
|
|
205
|
+
} from './presentation/utils/getAuthErrorMessage';
|
|
233
206
|
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
} from './init';
|
|
207
|
+
// =============================================================================
|
|
208
|
+
// INIT MODULE
|
|
209
|
+
// =============================================================================
|
|
210
|
+
|
|
211
|
+
export { createAuthInitModule } from './init';
|
|
212
|
+
export type { AuthInitModuleConfig } from './init/createAuthInitModule';
|
|
@@ -32,10 +32,11 @@ export class FirebaseAuthProvider implements IAuthProvider {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
initialize(): Promise<void> {
|
|
36
36
|
if (!this.auth) {
|
|
37
37
|
throw new Error("Firebase Auth instance must be provided");
|
|
38
38
|
}
|
|
39
|
+
return Promise.resolve();
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
setAuth(auth: Auth): void {
|
|
@@ -130,9 +131,13 @@ export class FirebaseAuthProvider implements IAuthProvider {
|
|
|
130
131
|
displayName: credentials.displayName.trim(),
|
|
131
132
|
});
|
|
132
133
|
} catch (error) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
// Log the error but don't fail the entire sign-up process
|
|
135
|
+
// The account was created successfully, only the display name update failed
|
|
136
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
137
|
+
console.warn(
|
|
138
|
+
`[FirebaseAuthProvider] Account created but display name update failed: ${errorMessage}. ` +
|
|
139
|
+
"User can update their display name later from profile settings."
|
|
140
|
+
);
|
|
136
141
|
}
|
|
137
142
|
}
|
|
138
143
|
|
|
@@ -36,6 +36,11 @@ export class AuthRepository implements IAuthRepository {
|
|
|
36
36
|
const password = sanitizePassword(params.password);
|
|
37
37
|
const displayName = params.displayName ? sanitizeName(params.displayName) : undefined;
|
|
38
38
|
|
|
39
|
+
// Log if display name was sanitized
|
|
40
|
+
if (__DEV__ && params.displayName && displayName && params.displayName !== displayName) {
|
|
41
|
+
console.warn("[AuthRepository] Display name was sanitized during sign up. Original:", params.displayName, "Sanitized:", displayName);
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
// Validate email
|
|
40
45
|
const emailResult = validateEmail(email);
|
|
41
46
|
if (!emailResult.isValid) {
|
|
@@ -29,25 +29,30 @@ export class AnonymousModeService {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
private async save(storageProvider: IStorageProvider): Promise<
|
|
32
|
+
private async save(storageProvider: IStorageProvider): Promise<boolean> {
|
|
33
33
|
try {
|
|
34
34
|
await storageProvider.set(this.storageKey, this.isAnonymousMode.toString());
|
|
35
|
+
return true;
|
|
35
36
|
} catch (err) {
|
|
36
37
|
if (__DEV__) {
|
|
37
38
|
console.error("[AnonymousModeService] Storage save failed:", err);
|
|
38
39
|
}
|
|
40
|
+
return false;
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
async clear(storageProvider: IStorageProvider): Promise<
|
|
44
|
+
async clear(storageProvider: IStorageProvider): Promise<boolean> {
|
|
43
45
|
try {
|
|
44
46
|
await storageProvider.remove(this.storageKey);
|
|
47
|
+
this.isAnonymousMode = false;
|
|
48
|
+
return true;
|
|
45
49
|
} catch (err) {
|
|
46
50
|
if (__DEV__) {
|
|
47
51
|
console.error("[AnonymousModeService] Storage clear failed:", err);
|
|
48
52
|
}
|
|
53
|
+
this.isAnonymousMode = false;
|
|
54
|
+
return false;
|
|
49
55
|
}
|
|
50
|
-
this.isAnonymousMode = false;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
async enable(storageProvider: IStorageProvider, provider?: IAuthProvider): Promise<void> {
|
|
@@ -63,7 +68,10 @@ export class AnonymousModeService {
|
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
this.isAnonymousMode = true;
|
|
66
|
-
await this.save(storageProvider);
|
|
71
|
+
const saved = await this.save(storageProvider);
|
|
72
|
+
if (!saved && __DEV__) {
|
|
73
|
+
console.warn("[AnonymousModeService] Anonymous mode enabled but not persisted to storage. Mode will be lost on app restart.");
|
|
74
|
+
}
|
|
67
75
|
emitAnonymousModeEnabled();
|
|
68
76
|
}
|
|
69
77
|
|
|
@@ -79,12 +87,10 @@ export class AnonymousModeService {
|
|
|
79
87
|
callback: (user: AuthUser | null) => void
|
|
80
88
|
): (user: AuthUser | null) => void {
|
|
81
89
|
return (user: AuthUser | null) => {
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
callback(null);
|
|
87
|
-
}
|
|
90
|
+
// In anonymous mode, still pass the actual Firebase user
|
|
91
|
+
// The store will handle setting the isAnonymous flag appropriately
|
|
92
|
+
// This allows proper anonymous to registered user conversion
|
|
93
|
+
callback(user);
|
|
88
94
|
};
|
|
89
95
|
}
|
|
90
96
|
}
|
|
@@ -98,10 +98,8 @@ export class AuthEventService {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
// Export singleton instance for backward compatibility
|
|
102
101
|
export const authEventService = AuthEventService.getInstance();
|
|
103
102
|
|
|
104
|
-
// Helper functions
|
|
105
103
|
export function emitUserAuthenticated(userId: string): void {
|
|
106
104
|
authEventService.emitUserAuthenticated(userId);
|
|
107
105
|
}
|
|
@@ -9,7 +9,7 @@ import type { IAuthProvider } from "../../application/ports/IAuthProvider";
|
|
|
9
9
|
import { FirebaseAuthProvider } from "../providers/FirebaseAuthProvider";
|
|
10
10
|
import type { AuthUser } from "../../domain/entities/AuthUser";
|
|
11
11
|
import type { AuthConfig } from "../../domain/value-objects/AuthConfig";
|
|
12
|
-
import {
|
|
12
|
+
import { sanitizeAuthConfig } from "../../domain/value-objects/AuthConfig";
|
|
13
13
|
import { AuthRepository } from "../repositories/AuthRepository";
|
|
14
14
|
import { AnonymousModeService } from "./AnonymousModeService";
|
|
15
15
|
import { authEventService } from "./AuthEventService";
|
|
@@ -24,11 +24,8 @@ export class AuthService implements IAuthService {
|
|
|
24
24
|
private config: AuthConfig;
|
|
25
25
|
|
|
26
26
|
constructor(config: Partial<AuthConfig> = {}, storageProvider?: IStorageProvider) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
...config,
|
|
30
|
-
password: { ...DEFAULT_AUTH_CONFIG.password, ...config.password },
|
|
31
|
-
};
|
|
27
|
+
// Validate and sanitize configuration
|
|
28
|
+
this.config = sanitizeAuthConfig(config);
|
|
32
29
|
|
|
33
30
|
this.anonymousModeService = new AnonymousModeService();
|
|
34
31
|
this.storageProvider = storageProvider;
|
|
@@ -45,7 +42,7 @@ export class AuthService implements IAuthService {
|
|
|
45
42
|
let provider: IAuthProvider;
|
|
46
43
|
|
|
47
44
|
if ("currentUser" in providerOrAuth) {
|
|
48
|
-
const firebaseProvider = new FirebaseAuthProvider(providerOrAuth
|
|
45
|
+
const firebaseProvider = new FirebaseAuthProvider(providerOrAuth);
|
|
49
46
|
await firebaseProvider.initialize();
|
|
50
47
|
provider = firebaseProvider;
|
|
51
48
|
} else {
|
|
@@ -120,7 +117,10 @@ export class AuthService implements IAuthService {
|
|
|
120
117
|
|
|
121
118
|
getCurrentUser(): AuthUser | null {
|
|
122
119
|
if (!this.initialized) return null;
|
|
123
|
-
|
|
120
|
+
// Return the actual Firebase user regardless of anonymous mode
|
|
121
|
+
// The caller should check the user's isAnonymous property if needed
|
|
122
|
+
// This ensures proper anonymous to registered user conversion
|
|
123
|
+
return this.repositoryInstance.getCurrentUser();
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
getIsAnonymousMode(): boolean {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth Error Mapper
|
|
3
3
|
* Single Responsibility: Map Firebase Auth errors to domain errors
|
|
4
|
+
*
|
|
5
|
+
* @module AuthErrorMapper
|
|
6
|
+
* @description Provides type-safe error mapping from Firebase Auth errors to
|
|
7
|
+
* domain-specific error types. Includes runtime validation and type guards.
|
|
4
8
|
*/
|
|
5
9
|
|
|
6
10
|
import {
|
|
@@ -14,18 +18,114 @@ import {
|
|
|
14
18
|
AuthNetworkError,
|
|
15
19
|
} from "../../domain/errors/AuthError";
|
|
16
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
|
+
}
|
|
96
|
+
|
|
17
97
|
/**
|
|
18
98
|
* Map Firebase Auth errors to domain errors
|
|
99
|
+
*
|
|
100
|
+
* @param error - Unknown error from Firebase Auth or other sources
|
|
101
|
+
* @returns Mapped domain error with appropriate error type and message
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* try {
|
|
106
|
+
* await signInWithEmailAndPassword(auth, email, password);
|
|
107
|
+
* } catch (error) {
|
|
108
|
+
* throw mapFirebaseAuthError(error);
|
|
109
|
+
* }
|
|
110
|
+
* ```
|
|
111
|
+
*
|
|
112
|
+
* @remarks
|
|
113
|
+
* This function handles:
|
|
114
|
+
* - Firebase Auth errors with proper code mapping
|
|
115
|
+
* - Standard JavaScript Error objects
|
|
116
|
+
* - Unknown error types with safe fallbacks
|
|
117
|
+
* - Type-safe error extraction using type guards
|
|
19
118
|
*/
|
|
20
119
|
export function mapFirebaseAuthError(error: unknown): Error {
|
|
120
|
+
// Handle null/undefined
|
|
21
121
|
if (!error || typeof error !== 'object') {
|
|
22
122
|
return new AuthError("Authentication failed", "AUTH_UNKNOWN_ERROR");
|
|
23
123
|
}
|
|
24
124
|
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const message = errorObj.message || "Authentication failed";
|
|
125
|
+
const code = extractErrorCode(error);
|
|
126
|
+
const message = extractErrorMessage(error);
|
|
28
127
|
|
|
128
|
+
// Map known Firebase Auth error codes to domain errors
|
|
29
129
|
switch (code) {
|
|
30
130
|
case "auth/email-already-in-use":
|
|
31
131
|
return new AuthEmailAlreadyInUseError();
|
|
@@ -94,7 +194,32 @@ export function mapFirebaseAuthError(error: unknown): Error {
|
|
|
94
194
|
);
|
|
95
195
|
|
|
96
196
|
default:
|
|
197
|
+
// Log unknown errors in development for debugging
|
|
198
|
+
if (__DEV__ && code) {
|
|
199
|
+
console.warn(`[AuthErrorMapper] Unknown Firebase Auth code: ${code}`, error);
|
|
200
|
+
}
|
|
97
201
|
return new AuthError(message, code || "AUTH_UNKNOWN_ERROR");
|
|
98
202
|
}
|
|
99
203
|
}
|
|
100
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Check if error is a network-related error
|
|
207
|
+
* @param error - Error to check
|
|
208
|
+
* @returns True if error is network-related
|
|
209
|
+
*/
|
|
210
|
+
export function isNetworkError(error: unknown): boolean {
|
|
211
|
+
return (
|
|
212
|
+
error instanceof AuthNetworkError ||
|
|
213
|
+
(error instanceof AuthError && error.code === "auth/network-request-failed")
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Check if error is a configuration-related error
|
|
219
|
+
* @param error - Error to check
|
|
220
|
+
* @returns True if error is configuration-related
|
|
221
|
+
*/
|
|
222
|
+
export function isConfigurationError(error: unknown): boolean {
|
|
223
|
+
return error instanceof AuthConfigurationError;
|
|
224
|
+
}
|
|
225
|
+
|
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sanitization Utilities
|
|
3
3
|
* Secure input cleaning for user data
|
|
4
|
+
*
|
|
5
|
+
* @module Sanitization
|
|
6
|
+
* @description Provides input sanitization functions to prevent XSS attacks,
|
|
7
|
+
* injection attacks, and ensure data integrity. All user inputs should be
|
|
8
|
+
* sanitized before storage or processing.
|
|
9
|
+
*
|
|
10
|
+
* Security Limits:
|
|
11
|
+
* These constants define maximum lengths for various input fields to prevent
|
|
12
|
+
* DoS attacks and ensure database integrity. They are based on industry standards:
|
|
13
|
+
*
|
|
14
|
+
* - EMAIL_MAX_LENGTH: 254 (RFC 5321 - maximum email address length)
|
|
15
|
+
* - PASSWORD_MAX_LENGTH: 128 (NIST recommendations)
|
|
16
|
+
* - NAME_MAX_LENGTH: 100 (Reasonable limit for display names)
|
|
17
|
+
* - GENERAL_TEXT_MAX_LENGTH: 500 (Prevents abuse of text fields)
|
|
18
|
+
*
|
|
19
|
+
* @note These limits are currently hardcoded for security. If you need to
|
|
20
|
+
* customize them for your application, you can:
|
|
21
|
+
* 1. Create your own sanitization functions with custom limits
|
|
22
|
+
* 2. Use the helper functions like `isWithinLengthLimit()` for validation
|
|
23
|
+
* 3. Submit a PR to make these limits configurable via AuthConfig
|
|
4
24
|
*/
|
|
5
25
|
|
|
6
26
|
/**
|
|
7
27
|
* Security constants for input validation
|
|
28
|
+
*
|
|
29
|
+
* @constant
|
|
30
|
+
* @type {readonly [key: string]: number}
|
|
31
|
+
*
|
|
32
|
+
* @property {number} EMAIL_MAX_LENGTH - Maximum email length per RFC 5321
|
|
33
|
+
* @property {number} PASSWORD_MIN_LENGTH - Minimum password length (configurable via AuthConfig)
|
|
34
|
+
* @property {number} PASSWORD_MAX_LENGTH - Maximum password length to prevent DoS
|
|
35
|
+
* @property {number} NAME_MAX_LENGTH - Maximum display name length
|
|
36
|
+
* @property {number} GENERAL_TEXT_MAX_LENGTH - Maximum general text input length
|
|
8
37
|
*/
|
|
9
38
|
export const SECURITY_LIMITS = {
|
|
10
39
|
EMAIL_MAX_LENGTH: 254, // RFC 5321
|
|
@@ -14,6 +43,24 @@ export const SECURITY_LIMITS = {
|
|
|
14
43
|
GENERAL_TEXT_MAX_LENGTH: 500,
|
|
15
44
|
} as const;
|
|
16
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Type for security limit keys
|
|
48
|
+
*/
|
|
49
|
+
export type SecurityLimitKey = keyof typeof SECURITY_LIMITS;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get a specific security limit value
|
|
53
|
+
* @param key - The security limit key
|
|
54
|
+
* @returns The security limit value
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* const maxEmailLength = getSecurityLimit('EMAIL_MAX_LENGTH'); // 254
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function getSecurityLimit(key: SecurityLimitKey): number {
|
|
61
|
+
return SECURITY_LIMITS[key];
|
|
62
|
+
}
|
|
63
|
+
|
|
17
64
|
/**
|
|
18
65
|
* Trim and normalize whitespace
|
|
19
66
|
*/
|
|
@@ -34,13 +81,14 @@ export const sanitizeEmail = (email: string): string => {
|
|
|
34
81
|
|
|
35
82
|
/**
|
|
36
83
|
* Sanitize password
|
|
37
|
-
* -
|
|
84
|
+
* - Trim leading/trailing whitespace to prevent login issues
|
|
85
|
+
* - Preserve case and special chars
|
|
38
86
|
* - Limit length to prevent DoS
|
|
39
87
|
*/
|
|
40
88
|
export const sanitizePassword = (password: string): string => {
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
return password.substring(0, SECURITY_LIMITS.PASSWORD_MAX_LENGTH);
|
|
89
|
+
// Trim leading/trailing spaces to prevent authentication issues
|
|
90
|
+
// Internal spaces are preserved for special use cases
|
|
91
|
+
return password.trim().substring(0, SECURITY_LIMITS.PASSWORD_MAX_LENGTH);
|
|
44
92
|
};
|
|
45
93
|
|
|
46
94
|
/**
|
|
@@ -130,11 +130,16 @@ export function useAuth(): UseAuthResult {
|
|
|
130
130
|
const signOut = useCallback(async () => {
|
|
131
131
|
try {
|
|
132
132
|
setLoading(true);
|
|
133
|
+
setError(null);
|
|
133
134
|
await signOutMutation.mutateAsync();
|
|
135
|
+
} catch (err: unknown) {
|
|
136
|
+
const errorMessage = err instanceof Error ? err.message : "Sign out failed";
|
|
137
|
+
setError(errorMessage);
|
|
138
|
+
throw err;
|
|
134
139
|
} finally {
|
|
135
140
|
setLoading(false);
|
|
136
141
|
}
|
|
137
|
-
}, [setLoading, signOutMutation]);
|
|
142
|
+
}, [setLoading, setError, signOutMutation]);
|
|
138
143
|
|
|
139
144
|
const continueAnonymously = useCallback(async () => {
|
|
140
145
|
try {
|
|
@@ -27,8 +27,7 @@ export const useProfileUpdate = (): UseProfileUpdateReturn => {
|
|
|
27
27
|
return Promise.reject(new Error("Anonymous users cannot update profile"));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
return Promise.reject(new Error("Profile update should be implemented by app"));
|
|
30
|
+
return Promise.reject(new Error("Profile update not implemented - use Firebase SDK directly"));
|
|
32
31
|
},
|
|
33
32
|
[user],
|
|
34
33
|
);
|
|
@@ -10,23 +10,88 @@
|
|
|
10
10
|
* ```
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { useEffect, type ReactNode } from "react";
|
|
14
|
-
import {
|
|
13
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
14
|
+
import { View, Text } from "react-native";
|
|
15
|
+
import { initializeAuthListener } from "../stores/initializeAuthListener";
|
|
15
16
|
|
|
16
|
-
interface AuthProviderProps {
|
|
17
|
+
export interface AuthProviderProps {
|
|
17
18
|
children: ReactNode;
|
|
19
|
+
/**
|
|
20
|
+
* Custom error component to display when auth initialization fails
|
|
21
|
+
*/
|
|
22
|
+
ErrorFallback?: React.ComponentType<{ error: Error; retry: () => void }>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ErrorFallbackProps {
|
|
26
|
+
error: Error;
|
|
27
|
+
retry: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Default error fallback component
|
|
32
|
+
*/
|
|
33
|
+
function DefaultErrorFallback({ error, retry }: { error: Error; retry: () => void }) {
|
|
34
|
+
return (
|
|
35
|
+
<View style={{ flex: 1, justifyContent: "center", alignItems: "center", padding: 20 }}>
|
|
36
|
+
<Text style={{ fontSize: 18, fontWeight: "bold", marginBottom: 10 }}>
|
|
37
|
+
Authentication Error
|
|
38
|
+
</Text>
|
|
39
|
+
<Text style={{ fontSize: 14, textAlign: "center", marginBottom: 20 }}>
|
|
40
|
+
{error.message || "Failed to initialize authentication. Please restart the app."}
|
|
41
|
+
</Text>
|
|
42
|
+
<Text
|
|
43
|
+
style={{ fontSize: 14, color: "#007AFF" }}
|
|
44
|
+
onPress={retry}
|
|
45
|
+
>
|
|
46
|
+
Retry
|
|
47
|
+
</Text>
|
|
48
|
+
</View>
|
|
49
|
+
);
|
|
18
50
|
}
|
|
19
51
|
|
|
20
52
|
/**
|
|
21
53
|
* AuthProvider component
|
|
22
54
|
* Initializes Firebase auth listener on mount
|
|
23
55
|
* Must wrap the app root
|
|
56
|
+
* Includes error boundary for graceful error handling
|
|
24
57
|
*/
|
|
25
|
-
export function AuthProvider({ children }: AuthProviderProps): ReactNode {
|
|
58
|
+
export function AuthProvider({ children, ErrorFallback = DefaultErrorFallback }: AuthProviderProps): ReactNode {
|
|
59
|
+
const [error, setError] = useState<Error | null>(null);
|
|
60
|
+
const [retryCount, setRetryCount] = useState(0);
|
|
61
|
+
|
|
26
62
|
useEffect(() => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
63
|
+
let unsubscribe: (() => void) | undefined;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
unsubscribe = initializeAuthListener();
|
|
67
|
+
} catch (err) {
|
|
68
|
+
const errorObj = err instanceof Error ? err : new Error("Unknown initialization error");
|
|
69
|
+
if (__DEV__) {
|
|
70
|
+
console.error("[AuthProvider] Initialization failed:", errorObj);
|
|
71
|
+
}
|
|
72
|
+
setError(errorObj);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return () => {
|
|
76
|
+
if (unsubscribe) {
|
|
77
|
+
try {
|
|
78
|
+
unsubscribe();
|
|
79
|
+
} catch (cleanupError) {
|
|
80
|
+
if (__DEV__) {
|
|
81
|
+
console.warn("[AuthProvider] Cleanup failed:", cleanupError);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}, [retryCount]); // Re-run on retry
|
|
87
|
+
|
|
88
|
+
// If error occurred, show error fallback
|
|
89
|
+
if (error) {
|
|
90
|
+
return <ErrorFallback error={error} retry={() => {
|
|
91
|
+
setError(null);
|
|
92
|
+
setRetryCount(prev => prev + 1);
|
|
93
|
+
}} />;
|
|
94
|
+
}
|
|
30
95
|
|
|
31
96
|
return <>{children}</>;
|
|
32
97
|
}
|
|
@@ -18,30 +18,9 @@ import {
|
|
|
18
18
|
selectIsAuthenticated,
|
|
19
19
|
selectIsAnonymous,
|
|
20
20
|
selectUserType,
|
|
21
|
-
selectIsAuthReady,
|
|
22
21
|
selectIsRegisteredUser,
|
|
23
22
|
} from "./auth.selectors";
|
|
24
23
|
|
|
25
|
-
// Re-export public selectors (consumed by index.ts)
|
|
26
|
-
export {
|
|
27
|
-
selectUserId,
|
|
28
|
-
selectIsAuthenticated,
|
|
29
|
-
selectIsAnonymous,
|
|
30
|
-
selectUserType,
|
|
31
|
-
selectIsAuthReady,
|
|
32
|
-
selectIsRegisteredUser,
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
// Re-export public types (consumed by index.ts)
|
|
36
|
-
export type { AuthState, AuthActions, UserType };
|
|
37
|
-
|
|
38
|
-
// Re-export listener functions (consumed by index.ts)
|
|
39
|
-
export {
|
|
40
|
-
initializeAuthListener,
|
|
41
|
-
resetAuthListener,
|
|
42
|
-
isAuthListenerInitialized,
|
|
43
|
-
} from "./initializeAuthListener";
|
|
44
|
-
|
|
45
24
|
// =============================================================================
|
|
46
25
|
// STORE
|
|
47
26
|
// =============================================================================
|
|
@@ -96,9 +75,10 @@ export const useAuthStore = createStore<AuthState, AuthActions>({
|
|
|
96
75
|
if (__DEV__) {
|
|
97
76
|
console.log("[AuthStore] setIsAnonymous:", { isAnonymous, hadUser: !!user });
|
|
98
77
|
}
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
78
|
+
// Update user.isAnonymous flag to match the new state
|
|
79
|
+
// This handles both anonymous → registered and registered → anonymous conversions
|
|
80
|
+
if (user && user.isAnonymous !== isAnonymous) {
|
|
81
|
+
set({ isAnonymous, user: { ...user, isAnonymous } });
|
|
102
82
|
} else {
|
|
103
83
|
set({ isAnonymous });
|
|
104
84
|
}
|
|
@@ -15,11 +15,10 @@ import type { AuthListenerOptions } from "../../types/auth-store.types";
|
|
|
15
15
|
|
|
16
16
|
const MAX_ANONYMOUS_RETRIES = 2;
|
|
17
17
|
const ANONYMOUS_RETRY_DELAY_MS = 1000;
|
|
18
|
+
const ANONYMOUS_SIGNIN_TIMEOUT_MS = 10000;
|
|
18
19
|
|
|
19
20
|
let listenerInitialized = false;
|
|
20
|
-
// Reference counter for multiple subscribers
|
|
21
21
|
let listenerRefCount = 0;
|
|
22
|
-
// Actual unsubscribe function from Firebase
|
|
23
22
|
let firebaseUnsubscribe: (() => void) | null = null;
|
|
24
23
|
|
|
25
24
|
/**
|
|
@@ -75,20 +74,24 @@ export function initializeAuthListener(
|
|
|
75
74
|
return () => {};
|
|
76
75
|
}
|
|
77
76
|
|
|
77
|
+
listenerInitialized = true;
|
|
78
|
+
listenerRefCount = 1;
|
|
79
|
+
|
|
80
|
+
// Initialize listener first, then check anonymous mode
|
|
81
|
+
// This prevents race conditions where the listener fires before we set up state
|
|
78
82
|
const service = getAuthService();
|
|
79
83
|
if (service) {
|
|
80
84
|
const isAnonymous = service.getIsAnonymousMode();
|
|
81
85
|
if (__DEV__) {
|
|
82
86
|
console.log("[AuthListener] Service isAnonymousMode:", isAnonymous);
|
|
83
87
|
}
|
|
88
|
+
// Set anonymous mode flag before setting up listener
|
|
89
|
+
// This ensures consistent state when listener first fires
|
|
84
90
|
if (isAnonymous) {
|
|
85
91
|
store.setIsAnonymous(true);
|
|
86
92
|
}
|
|
87
93
|
}
|
|
88
94
|
|
|
89
|
-
listenerInitialized = true;
|
|
90
|
-
listenerRefCount = 1;
|
|
91
|
-
|
|
92
95
|
firebaseUnsubscribe = onIdTokenChanged(auth, (user) => {
|
|
93
96
|
if (__DEV__) {
|
|
94
97
|
console.log("[AuthListener] onIdTokenChanged:", {
|
|
@@ -106,36 +109,60 @@ export function initializeAuthListener(
|
|
|
106
109
|
// Set loading state while attempting sign-in
|
|
107
110
|
store.setLoading(true);
|
|
108
111
|
|
|
112
|
+
// Start anonymous sign-in process
|
|
113
|
+
// Don't return early - let the listener continue to handle state changes
|
|
109
114
|
void (async () => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
115
|
+
// Add timeout protection
|
|
116
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
117
|
+
setTimeout(() => reject(new Error("Anonymous sign-in timeout")), ANONYMOUS_SIGNIN_TIMEOUT_MS)
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
// Race between sign-in and timeout
|
|
122
|
+
await Promise.race([
|
|
123
|
+
(async () => {
|
|
124
|
+
for (let attempt = 0; attempt < MAX_ANONYMOUS_RETRIES; attempt++) {
|
|
125
|
+
try {
|
|
126
|
+
await anonymousAuthService.signInAnonymously(auth);
|
|
127
|
+
if (__DEV__) {
|
|
128
|
+
console.log("[AuthListener] Anonymous sign-in successful");
|
|
129
|
+
}
|
|
130
|
+
// Success - the listener will fire again with the new user
|
|
131
|
+
return;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (__DEV__) {
|
|
134
|
+
console.warn(`[AuthListener] Anonymous sign-in attempt ${attempt + 1} failed:`, error);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// If not last attempt, wait and retry
|
|
138
|
+
if (attempt < MAX_ANONYMOUS_RETRIES - 1) {
|
|
139
|
+
await new Promise(resolve => setTimeout(resolve, ANONYMOUS_RETRY_DELAY_MS));
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// All attempts failed
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
})(),
|
|
148
|
+
timeoutPromise,
|
|
149
|
+
]);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
// All attempts failed or timeout - set error state
|
|
152
|
+
if (__DEV__) {
|
|
153
|
+
console.error("[AuthListener] All anonymous sign-in attempts failed:", error);
|
|
136
154
|
}
|
|
155
|
+
store.setFirebaseUser(null);
|
|
156
|
+
store.setLoading(false);
|
|
157
|
+
store.setInitialized(true);
|
|
158
|
+
store.setError("Failed to sign in anonymously. Please check your connection.");
|
|
137
159
|
}
|
|
138
160
|
})();
|
|
161
|
+
|
|
162
|
+
// Continue execution - don't return early
|
|
163
|
+
// The listener will be triggered again when sign-in succeeds
|
|
164
|
+
// For now, set null user and let loading state indicate in-progress
|
|
165
|
+
store.setFirebaseUser(null);
|
|
139
166
|
return;
|
|
140
167
|
}
|
|
141
168
|
|