@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-auth",
3
- "version": "3.6.57",
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.82",
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
- export {
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
- DEFAULT_VAL_CONFIG,
127
- } from './infrastructure/utils/AuthValidation';
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 { LoginFormTranslations as UseLoginFormTranslations, UseLoginFormConfig, UseLoginFormResult } from './presentation/hooks/useLoginForm';
128
+ export type { UseLoginFormConfig, UseLoginFormResult } from './presentation/hooks/useLoginForm';
129
+
135
130
  export { useRegisterForm } from './presentation/hooks/useRegisterForm';
136
- export type { RegisterFormTranslations as UseRegisterFormTranslations, UseRegisterFormConfig, UseRegisterFormResult } from './presentation/hooks/useRegisterForm';
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 { UseProfileEditReturn, ProfileEditFormState } from './presentation/hooks/useProfileEdit';
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, GoogleAuthConfig as GoogleAuthHookConfig } from './presentation/hooks/useGoogleAuth';
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
- // DOMAIN ENTITIES & UTILS
158
- export type { UserProfile, UpdateProfileParams } from './domain/entities/UserProfile';
159
- export { migrateUserData, configureMigration } from './domain/utils/migration';
160
- export type { MigrationConfig } from './domain/utils/migration';
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 { LoginScreenTranslations, LoginScreenProps } from './presentation/screens/LoginScreen';
168
+ export type { LoginScreenProps } from './presentation/screens/LoginScreen';
169
+
165
170
  export { RegisterScreen } from './presentation/screens/RegisterScreen';
166
- export type { RegisterScreenTranslations, RegisterScreenProps } from './presentation/screens/RegisterScreen';
171
+ export type { RegisterScreenProps } from './presentation/screens/RegisterScreen';
172
+
167
173
  export { AccountScreen } from './presentation/screens/AccountScreen';
168
- export type { AccountScreenConfig, AccountScreenProps } from './presentation/screens/AccountScreen';
174
+ export type { AccountScreenProps } from './presentation/screens/AccountScreen';
175
+
169
176
  export { EditProfileScreen } from './presentation/screens/EditProfileScreen';
170
- export type { EditProfileConfig, EditProfileScreenProps } from './presentation/screens/EditProfileScreen';
177
+ export type { EditProfileScreenProps } from './presentation/screens/EditProfileScreen';
178
+
171
179
  export { ChangePasswordScreen } from './presentation/screens/change-password';
172
- export type { ChangePasswordTranslations, ChangePasswordScreenProps } from './presentation/screens/change-password';
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
- export { useAuthModalStore } from './presentation/stores/authModalStore';
204
- export type { AuthModalMode } from './presentation/stores/authModalStore';
187
+ // =============================================================================
188
+
189
+ export { useAuthStore } from './presentation/stores/authStore';
205
190
  export {
206
- useAuthStore,
207
191
  initializeAuthListener,
208
192
  resetAuthListener,
209
193
  isAuthListenerInitialized,
210
- selectIsAuthenticated,
211
- selectUserId,
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
- export { getAuthErrorLocalizationKey, resolveErrorMessage } from './presentation/utils/getAuthErrorMessage';
200
+ // =============================================================================
227
201
 
228
- // App Service Helper (for configureAppServices)
229
202
  export {
230
- createAuthService,
231
- type IAppAuthServiceHelper,
232
- } from './infrastructure/services/app-service-helpers';
203
+ getAuthErrorLocalizationKey,
204
+ resolveErrorMessage,
205
+ } from './presentation/utils/getAuthErrorMessage';
233
206
 
234
- // Init Module Factory
235
- export {
236
- createAuthInitModule,
237
- type AuthInitModuleConfig,
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
- async initialize(): Promise<void> {
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
- if (__DEV__) {
134
- console.warn("[FirebaseAuthProvider] Failed to update display name:", error);
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<void> {
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<void> {
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
- // Don't update if in anonymous mode
83
- if (!this.isAnonymousMode) {
84
- callback(user);
85
- } else {
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 { DEFAULT_AUTH_CONFIG } from "../../domain/value-objects/AuthConfig";
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
- this.config = {
28
- ...DEFAULT_AUTH_CONFIG,
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 as Auth);
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
- return this.anonymousModeService.getIsAnonymousMode() ? null : this.repositoryInstance.getCurrentUser();
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 errorObj = error as { code?: string; message?: string };
26
- const code = errorObj.code || "";
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
- * - Only trim (preserve case and special chars)
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
- // Don't trim password to preserve intentional spaces
42
- // Only limit length
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
- // Note: App should implement this via Firebase SDK
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 { initializeAuthListener } from "../stores/authStore";
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
- const unsubscribe = initializeAuthListener();
28
- return unsubscribe;
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
- // Also update user.isAnonymous when converting from anonymous
100
- if (user && !isAnonymous && user.isAnonymous) {
101
- set({ isAnonymous, user: { ...user, isAnonymous: false } });
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
- for (let attempt = 0; attempt <= MAX_ANONYMOUS_RETRIES; attempt++) {
111
- try {
112
- await anonymousAuthService.signInAnonymously(auth);
113
- if (__DEV__) {
114
- console.log("[AuthListener] Anonymous sign-in successful");
115
- }
116
- // Success - the listener will fire again with the new user
117
- return;
118
- } catch (error) {
119
- if (__DEV__) {
120
- console.warn(`[AuthListener] Anonymous sign-in attempt ${attempt + 1} failed:`, error);
121
- }
122
-
123
- // If not last attempt, wait and retry
124
- if (attempt < MAX_ANONYMOUS_RETRIES) {
125
- await new Promise(resolve => setTimeout(resolve, ANONYMOUS_RETRY_DELAY_MS));
126
- continue;
127
- }
128
-
129
- // Last attempt failed - set error state
130
- if (__DEV__) {
131
- console.error("[AuthListener] All anonymous sign-in attempts failed");
132
- }
133
- store.setFirebaseUser(null);
134
- store.setLoading(false);
135
- store.setInitialized(true);
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