@umituz/react-native-auth 3.4.22 → 3.4.24
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/__tests__/services/AnonymousModeService.test.ts +22 -153
- package/src/__tests__/services/AuthCoreInitialization.test.ts +45 -0
- package/src/__tests__/services/AuthCoreOperations.test.ts +71 -0
- package/src/__tests__/services/AuthPackage.test.ts +24 -171
- package/src/__tests__/utils/AuthDisplayNameValidation.test.ts +44 -0
- package/src/__tests__/utils/AuthEmailValidation.test.ts +38 -0
- package/src/__tests__/utils/AuthPasswordValidation.test.ts +90 -0
- package/src/index.ts +11 -95
- package/src/infrastructure/services/UserDocument.types.ts +44 -0
- package/src/infrastructure/services/UserDocumentService.ts +28 -179
- package/src/infrastructure/utils/AuthValidation.ts +37 -156
- package/src/infrastructure/utils/userDocumentBuilder.util.ts +107 -0
- package/src/presentation/hooks/useAccountManagement.ts +2 -106
- package/src/presentation/utils/accountDeleteHandler.util.ts +147 -0
- package/src/__tests__/services/AuthCoreService.test.ts +0 -247
- package/src/__tests__/utils/AuthValidation.test.ts +0 -270
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validatePasswordForLogin,
|
|
3
|
+
validatePasswordForRegister,
|
|
4
|
+
validatePasswordConfirmation,
|
|
5
|
+
} from '../../../src/infrastructure/utils/AuthValidation';
|
|
6
|
+
import { DEFAULT_PASSWORD_CONFIG } from '../../../src/domain/value-objects/AuthConfig';
|
|
7
|
+
|
|
8
|
+
describe('AuthPasswordValidation', () => {
|
|
9
|
+
describe('validatePasswordForLogin', () => {
|
|
10
|
+
it('should reject empty password', () => {
|
|
11
|
+
const result = validatePasswordForLogin('');
|
|
12
|
+
expect(result.isValid).toBe(false);
|
|
13
|
+
expect(result.error).toBe('Password is required');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should accept any non-empty password', () => {
|
|
17
|
+
const result = validatePasswordForLogin('any');
|
|
18
|
+
expect(result.isValid).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('validatePasswordForRegister', () => {
|
|
23
|
+
const config = DEFAULT_PASSWORD_CONFIG;
|
|
24
|
+
|
|
25
|
+
it('should reject empty password', () => {
|
|
26
|
+
const result = validatePasswordForRegister('', config);
|
|
27
|
+
expect(result.isValid).toBe(false);
|
|
28
|
+
expect(result.requirements.hasMinLength).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should reject password that is too short', () => {
|
|
32
|
+
const result = validatePasswordForRegister('123', config);
|
|
33
|
+
expect(result.isValid).toBe(false);
|
|
34
|
+
expect(result.requirements.hasMinLength).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should validate uppercase requirement', () => {
|
|
38
|
+
const configReq = { ...config, requireUppercase: true };
|
|
39
|
+
expect(validatePasswordForRegister('password', configReq).isValid).toBe(false);
|
|
40
|
+
expect(validatePasswordForRegister('Password', configReq).requirements.hasUppercase).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should validate lowercase requirement', () => {
|
|
44
|
+
const configReq = { ...config, requireLowercase: true };
|
|
45
|
+
expect(validatePasswordForRegister('PASSWORD', configReq).isValid).toBe(false);
|
|
46
|
+
expect(validatePasswordForRegister('Password', configReq).requirements.hasLowercase).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should validate number requirement', () => {
|
|
50
|
+
const configReq = { ...config, requireNumber: true };
|
|
51
|
+
expect(validatePasswordForRegister('Password', configReq).isValid).toBe(false);
|
|
52
|
+
expect(validatePasswordForRegister('Password1', configReq).requirements.hasNumber).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should validate special character requirement', () => {
|
|
56
|
+
const configReq = { ...config, requireSpecialChar: true };
|
|
57
|
+
expect(validatePasswordForRegister('Password1', configReq).isValid).toBe(false);
|
|
58
|
+
expect(validatePasswordForRegister('Password1!', configReq).requirements.hasSpecialChar).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should accept password that meets all requirements', () => {
|
|
62
|
+
const strictConfig = {
|
|
63
|
+
...config,
|
|
64
|
+
requireUppercase: true,
|
|
65
|
+
requireLowercase: true,
|
|
66
|
+
requireNumber: true,
|
|
67
|
+
requireSpecialChar: true,
|
|
68
|
+
};
|
|
69
|
+
const result = validatePasswordForRegister('Password1!', strictConfig);
|
|
70
|
+
expect(result.isValid).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('validatePasswordConfirmation', () => {
|
|
75
|
+
it('should reject empty confirmation', () => {
|
|
76
|
+
const result = validatePasswordConfirmation('password', '');
|
|
77
|
+
expect(result.isValid).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should reject mismatched passwords', () => {
|
|
81
|
+
const result = validatePasswordConfirmation('password', 'different');
|
|
82
|
+
expect(result.isValid).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should accept matching passwords', () => {
|
|
86
|
+
const result = validatePasswordConfirmation('password', 'password');
|
|
87
|
+
expect(result.isValid).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,31 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* React Native Auth - Public API
|
|
3
|
-
*
|
|
4
|
-
* Domain-Driven Design (DDD) Architecture
|
|
5
|
-
*
|
|
6
|
-
* This is the SINGLE SOURCE OF TRUTH for all Auth operations.
|
|
7
|
-
* ALL imports from the Auth package MUST go through this file.
|
|
8
|
-
*
|
|
9
|
-
* Architecture:
|
|
10
|
-
* - domain: Entities, value objects, errors (business logic)
|
|
11
|
-
* - application: Ports (interfaces)
|
|
12
|
-
* - infrastructure: Auth providers and service implementation
|
|
13
|
-
* - presentation: Hooks (React integration)
|
|
14
|
-
*
|
|
15
|
-
* Usage:
|
|
16
|
-
* import { initializeAuthService, useAuth } from '@umituz/react-native-auth';
|
|
3
|
+
* Single source of truth for all Auth operations.
|
|
17
4
|
*/
|
|
18
5
|
|
|
19
|
-
//
|
|
20
|
-
// DOMAIN LAYER - Entities
|
|
21
|
-
// =============================================================================
|
|
22
|
-
|
|
6
|
+
// DOMAIN LAYER
|
|
23
7
|
export type { AuthUser, AuthProviderType } from './domain/entities/AuthUser';
|
|
24
|
-
|
|
25
|
-
// =============================================================================
|
|
26
|
-
// DOMAIN LAYER - Errors
|
|
27
|
-
// =============================================================================
|
|
28
|
-
|
|
29
8
|
export {
|
|
30
9
|
AuthError,
|
|
31
10
|
AuthInitializationError,
|
|
@@ -39,10 +18,6 @@ export {
|
|
|
39
18
|
AuthInvalidEmailError,
|
|
40
19
|
} from './domain/errors/AuthError';
|
|
41
20
|
|
|
42
|
-
// =============================================================================
|
|
43
|
-
// DOMAIN LAYER - Value Objects
|
|
44
|
-
// =============================================================================
|
|
45
|
-
|
|
46
21
|
export type {
|
|
47
22
|
AuthConfig,
|
|
48
23
|
PasswordConfig,
|
|
@@ -58,10 +33,7 @@ export {
|
|
|
58
33
|
DEFAULT_SOCIAL_CONFIG,
|
|
59
34
|
} from './domain/value-objects/AuthConfig';
|
|
60
35
|
|
|
61
|
-
//
|
|
62
|
-
// APPLICATION LAYER - Ports
|
|
63
|
-
// =============================================================================
|
|
64
|
-
|
|
36
|
+
// APPLICATION LAYER
|
|
65
37
|
export type { IAuthService, SignUpParams, SignInParams } from './application/ports/IAuthService';
|
|
66
38
|
export type {
|
|
67
39
|
IAuthProvider,
|
|
@@ -70,55 +42,37 @@ export type {
|
|
|
70
42
|
SocialSignInResult,
|
|
71
43
|
} from './application/ports/IAuthProvider';
|
|
72
44
|
|
|
73
|
-
//
|
|
74
|
-
// INFRASTRUCTURE LAYER - Providers
|
|
75
|
-
// =============================================================================
|
|
76
|
-
|
|
45
|
+
// INFRASTRUCTURE LAYER
|
|
77
46
|
export { FirebaseAuthProvider } from './infrastructure/providers/FirebaseAuthProvider';
|
|
78
|
-
|
|
79
|
-
// =============================================================================
|
|
80
|
-
// INFRASTRUCTURE LAYER - Services
|
|
81
|
-
// =============================================================================
|
|
82
|
-
|
|
83
47
|
export {
|
|
84
48
|
AuthService,
|
|
85
49
|
initializeAuthService,
|
|
86
50
|
getAuthService,
|
|
87
51
|
resetAuthService,
|
|
88
52
|
} from './infrastructure/services/AuthService';
|
|
89
|
-
|
|
90
53
|
export {
|
|
91
54
|
createStorageProvider,
|
|
92
55
|
StorageProviderAdapter,
|
|
93
56
|
} from './infrastructure/adapters/StorageProviderAdapter';
|
|
94
|
-
|
|
95
57
|
export type { IStorageProvider } from './infrastructure/services/AuthPackage';
|
|
96
|
-
|
|
97
58
|
export {
|
|
98
59
|
ensureUserDocument,
|
|
99
60
|
markUserDeleted,
|
|
100
61
|
configureUserDocumentService,
|
|
101
62
|
} from './infrastructure/services/UserDocumentService';
|
|
102
|
-
|
|
103
|
-
// Unified Auth Initialization (RECOMMENDED)
|
|
104
63
|
export {
|
|
105
64
|
initializeAuth,
|
|
106
65
|
isAuthInitialized,
|
|
107
66
|
resetAuthInitialization,
|
|
108
67
|
} from './infrastructure/services/initializeAuth';
|
|
109
|
-
|
|
110
68
|
export type { InitializeAuthOptions } from './infrastructure/services/initializeAuth';
|
|
111
|
-
|
|
112
69
|
export type {
|
|
113
70
|
UserDocumentConfig,
|
|
114
71
|
UserDocumentExtras,
|
|
115
72
|
UserDocumentUser,
|
|
116
73
|
} from './infrastructure/services/UserDocumentService';
|
|
117
74
|
|
|
118
|
-
//
|
|
119
|
-
// INFRASTRUCTURE LAYER - Validation
|
|
120
|
-
// =============================================================================
|
|
121
|
-
|
|
75
|
+
// VALIDATION
|
|
122
76
|
export {
|
|
123
77
|
validateEmail,
|
|
124
78
|
validatePasswordForLogin,
|
|
@@ -126,86 +80,57 @@ export {
|
|
|
126
80
|
validatePasswordConfirmation,
|
|
127
81
|
validateDisplayName,
|
|
128
82
|
} from './infrastructure/utils/AuthValidation';
|
|
129
|
-
|
|
130
83
|
export type {
|
|
131
84
|
ValidationResult,
|
|
132
85
|
PasswordStrengthResult,
|
|
133
86
|
PasswordRequirements,
|
|
134
87
|
} from './infrastructure/utils/AuthValidation';
|
|
135
88
|
|
|
136
|
-
//
|
|
137
|
-
// PRESENTATION LAYER - Provider
|
|
138
|
-
// =============================================================================
|
|
139
|
-
|
|
89
|
+
// PRESENTATION LAYER
|
|
140
90
|
export { AuthProvider } from './presentation/providers/AuthProvider';
|
|
141
|
-
|
|
142
|
-
// =============================================================================
|
|
143
|
-
// PRESENTATION LAYER - Hooks
|
|
144
|
-
// =============================================================================
|
|
145
|
-
|
|
146
91
|
export { useAuth } from './presentation/hooks/useAuth';
|
|
147
92
|
export type { UseAuthResult } from './presentation/hooks/useAuth';
|
|
148
|
-
|
|
149
93
|
export { useAuthRequired } from './presentation/hooks/useAuthRequired';
|
|
150
94
|
export type { UseAuthRequiredResult } from './presentation/hooks/useAuthRequired';
|
|
151
|
-
|
|
152
95
|
export { useRequireAuth, useUserId } from './presentation/hooks/useRequireAuth';
|
|
153
|
-
|
|
154
96
|
export { useUserProfile } from './presentation/hooks/useUserProfile';
|
|
155
97
|
export type { UserProfileData, UseUserProfileParams } from './presentation/hooks/useUserProfile';
|
|
156
|
-
|
|
157
98
|
export { useAccountManagement } from './presentation/hooks/useAccountManagement';
|
|
158
99
|
export type { UseAccountManagementReturn } from './presentation/hooks/useAccountManagement';
|
|
159
|
-
|
|
160
100
|
export { useProfileUpdate } from './presentation/hooks/useProfileUpdate';
|
|
161
101
|
export type { UseProfileUpdateReturn } from './presentation/hooks/useProfileUpdate';
|
|
162
|
-
|
|
163
102
|
export { useProfileEdit } from './presentation/hooks/useProfileEdit';
|
|
164
103
|
export type { UseProfileEditReturn, ProfileEditFormState } from './presentation/hooks/useProfileEdit';
|
|
165
|
-
|
|
166
104
|
export { useSocialLogin } from './presentation/hooks/useSocialLogin';
|
|
167
105
|
export type { UseSocialLoginConfig, UseSocialLoginResult } from './presentation/hooks/useSocialLogin';
|
|
168
|
-
|
|
169
106
|
export { useGoogleAuth } from './presentation/hooks/useGoogleAuth';
|
|
170
107
|
export type { UseGoogleAuthResult, GoogleAuthConfig as GoogleAuthHookConfig } from './presentation/hooks/useGoogleAuth';
|
|
171
|
-
|
|
172
108
|
export { useAppleAuth } from './presentation/hooks/useAppleAuth';
|
|
173
109
|
export type { UseAppleAuthResult } from './presentation/hooks/useAppleAuth';
|
|
174
|
-
|
|
175
110
|
export { useAuthBottomSheet } from './presentation/hooks/useAuthBottomSheet';
|
|
176
111
|
export type { SocialAuthConfiguration } from './presentation/hooks/useAuthBottomSheet';
|
|
177
112
|
|
|
113
|
+
// DOMAIN ENTITIES & UTILS
|
|
178
114
|
export type { UserProfile, UpdateProfileParams } from './domain/entities/UserProfile';
|
|
179
|
-
|
|
180
|
-
// Domain Utils - Anonymous Names
|
|
181
115
|
export { generateAnonymousName, getAnonymousDisplayName } from './domain/utils/anonymousNameGenerator';
|
|
182
116
|
export type { AnonymousNameConfig } from './domain/utils/anonymousNameGenerator';
|
|
183
|
-
|
|
184
|
-
// Domain Utils - Migration
|
|
185
117
|
export { migrateUserData, configureMigration } from './domain/utils/migration';
|
|
186
118
|
export type { MigrationConfig } from './domain/utils/migration';
|
|
187
119
|
|
|
188
|
-
//
|
|
189
|
-
// PRESENTATION LAYER - Screens & Navigation
|
|
190
|
-
// =============================================================================
|
|
191
|
-
|
|
120
|
+
// SCREENS & NAVIGATION
|
|
192
121
|
export { LoginScreen } from './presentation/screens/LoginScreen';
|
|
193
122
|
export { RegisterScreen } from './presentation/screens/RegisterScreen';
|
|
194
123
|
export { AccountScreen } from './presentation/screens/AccountScreen';
|
|
195
124
|
export type { AccountScreenConfig, AccountScreenProps } from './presentation/screens/AccountScreen';
|
|
196
125
|
export { EditProfileScreen } from './presentation/screens/EditProfileScreen';
|
|
197
126
|
export type { EditProfileConfig, EditProfileScreenProps } from './presentation/screens/EditProfileScreen';
|
|
198
|
-
|
|
199
127
|
export { AuthNavigator } from './presentation/navigation/AuthNavigator';
|
|
200
128
|
export type {
|
|
201
129
|
AuthStackParamList,
|
|
202
130
|
AuthNavigatorProps,
|
|
203
131
|
} from './presentation/navigation/AuthNavigator';
|
|
204
132
|
|
|
205
|
-
//
|
|
206
|
-
// PRESENTATION LAYER - Components
|
|
207
|
-
// =============================================================================
|
|
208
|
-
|
|
133
|
+
// COMPONENTS
|
|
209
134
|
export { AuthContainer } from './presentation/components/AuthContainer';
|
|
210
135
|
export { AuthHeader } from './presentation/components/AuthHeader';
|
|
211
136
|
export { AuthFormCard } from './presentation/components/AuthFormCard';
|
|
@@ -226,13 +151,9 @@ export type { ProfileSectionConfig, ProfileSectionProps } from './presentation/c
|
|
|
226
151
|
export { AccountActions } from './presentation/components/AccountActions';
|
|
227
152
|
export type { AccountActionsConfig, AccountActionsProps } from './presentation/components/AccountActions';
|
|
228
153
|
|
|
229
|
-
//
|
|
230
|
-
// PRESENTATION LAYER - Stores
|
|
231
|
-
// =============================================================================
|
|
232
|
-
|
|
154
|
+
// STORES
|
|
233
155
|
export { useAuthModalStore } from './presentation/stores/authModalStore';
|
|
234
156
|
export type { AuthModalMode } from './presentation/stores/authModalStore';
|
|
235
|
-
|
|
236
157
|
export {
|
|
237
158
|
useAuthStore,
|
|
238
159
|
initializeAuthListener,
|
|
@@ -248,13 +169,8 @@ export {
|
|
|
248
169
|
getIsAuthenticated,
|
|
249
170
|
getIsAnonymous,
|
|
250
171
|
} from './presentation/stores/authStore';
|
|
251
|
-
|
|
252
172
|
export type { UserType, AuthState, AuthActions } from './presentation/stores/authStore';
|
|
253
173
|
export type { AuthListenerOptions } from './types/auth-store.types';
|
|
254
174
|
|
|
255
|
-
//
|
|
256
|
-
// PRESENTATION LAYER - Utilities
|
|
257
|
-
// =============================================================================
|
|
258
|
-
|
|
175
|
+
// UTILITIES
|
|
259
176
|
export { getAuthErrorLocalizationKey } from './presentation/utils/getAuthErrorMessage';
|
|
260
|
-
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Document Types and Configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal user interface for document creation
|
|
7
|
+
* Compatible with both Firebase User and AuthUser
|
|
8
|
+
*/
|
|
9
|
+
export interface UserDocumentUser {
|
|
10
|
+
uid: string;
|
|
11
|
+
displayName?: string | null;
|
|
12
|
+
email?: string | null;
|
|
13
|
+
photoURL?: string | null;
|
|
14
|
+
isAnonymous?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Configuration for user document service
|
|
19
|
+
*/
|
|
20
|
+
export interface UserDocumentConfig {
|
|
21
|
+
/** Firestore collection name (default: "users") */
|
|
22
|
+
collectionName?: string;
|
|
23
|
+
/** Additional fields to store with user document */
|
|
24
|
+
extraFields?: Record<string, unknown>;
|
|
25
|
+
/** Callback to collect device/app info */
|
|
26
|
+
collectExtras?: () => Promise<Record<string, unknown>>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* User document extras from device/app
|
|
31
|
+
*/
|
|
32
|
+
export interface UserDocumentExtras {
|
|
33
|
+
deviceId?: string;
|
|
34
|
+
platform?: string;
|
|
35
|
+
deviceModel?: string;
|
|
36
|
+
deviceBrand?: string;
|
|
37
|
+
osVersion?: string;
|
|
38
|
+
appVersion?: string;
|
|
39
|
+
buildNumber?: string;
|
|
40
|
+
locale?: string;
|
|
41
|
+
timezone?: string;
|
|
42
|
+
previousAnonymousUserId?: string;
|
|
43
|
+
signUpMethod?: string;
|
|
44
|
+
}
|
|
@@ -1,206 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* User Document Service
|
|
3
3
|
* Generic service for creating/updating user documents in Firestore
|
|
4
|
-
* Called automatically on auth state changes
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
import { doc, getDoc, setDoc, serverTimestamp } from "firebase/firestore";
|
|
8
7
|
import type { User } from "firebase/auth";
|
|
9
8
|
import { getFirestore } from "@umituz/react-native-firebase";
|
|
9
|
+
import type {
|
|
10
|
+
UserDocumentUser,
|
|
11
|
+
UserDocumentConfig,
|
|
12
|
+
UserDocumentExtras,
|
|
13
|
+
} from "./UserDocument.types";
|
|
14
|
+
import {
|
|
15
|
+
getSignUpMethod,
|
|
16
|
+
buildBaseData,
|
|
17
|
+
buildCreateData,
|
|
18
|
+
buildUpdateData,
|
|
19
|
+
} from "../utils/userDocumentBuilder.util";
|
|
20
|
+
|
|
21
|
+
export type {
|
|
22
|
+
UserDocumentUser,
|
|
23
|
+
UserDocumentConfig,
|
|
24
|
+
UserDocumentExtras,
|
|
25
|
+
} from "./UserDocument.types";
|
|
10
26
|
|
|
11
27
|
declare const __DEV__: boolean;
|
|
12
28
|
|
|
13
|
-
/**
|
|
14
|
-
* Minimal user interface for document creation
|
|
15
|
-
* Compatible with both Firebase User and AuthUser
|
|
16
|
-
*/
|
|
17
|
-
export interface UserDocumentUser {
|
|
18
|
-
uid: string;
|
|
19
|
-
displayName?: string | null;
|
|
20
|
-
email?: string | null;
|
|
21
|
-
photoURL?: string | null;
|
|
22
|
-
isAnonymous?: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Configuration for user document service
|
|
27
|
-
*/
|
|
28
|
-
export interface UserDocumentConfig {
|
|
29
|
-
/** Firestore collection name (default: "users") */
|
|
30
|
-
collectionName?: string;
|
|
31
|
-
/** Additional fields to store with user document */
|
|
32
|
-
extraFields?: Record<string, unknown>;
|
|
33
|
-
/** Callback to collect device/app info */
|
|
34
|
-
collectExtras?: () => Promise<Record<string, unknown>>;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* User document extras from device/app
|
|
39
|
-
*/
|
|
40
|
-
export interface UserDocumentExtras {
|
|
41
|
-
deviceId?: string;
|
|
42
|
-
platform?: string;
|
|
43
|
-
deviceModel?: string;
|
|
44
|
-
deviceBrand?: string;
|
|
45
|
-
osVersion?: string;
|
|
46
|
-
appVersion?: string;
|
|
47
|
-
buildNumber?: string;
|
|
48
|
-
locale?: string;
|
|
49
|
-
timezone?: string;
|
|
50
|
-
previousAnonymousUserId?: string;
|
|
51
|
-
signUpMethod?: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
29
|
let userDocumentConfig: UserDocumentConfig = {};
|
|
55
30
|
|
|
56
|
-
/**
|
|
57
|
-
* Configure user document service
|
|
58
|
-
*/
|
|
59
31
|
export function configureUserDocumentService(config: UserDocumentConfig): void {
|
|
60
32
|
userDocumentConfig = { ...config };
|
|
61
33
|
}
|
|
62
34
|
|
|
63
|
-
/**
|
|
64
|
-
* Get sign-up method from auth user
|
|
65
|
-
*/
|
|
66
|
-
function getSignUpMethod(user: UserDocumentUser): string | undefined {
|
|
67
|
-
if (user.isAnonymous) return "anonymous";
|
|
68
|
-
if (user.email) {
|
|
69
|
-
const providerData = (
|
|
70
|
-
user as unknown as { providerData?: { providerId: string }[] }
|
|
71
|
-
).providerData;
|
|
72
|
-
if (providerData && providerData.length > 0) {
|
|
73
|
-
const providerId = providerData[0].providerId;
|
|
74
|
-
if (providerId === "google.com") return "google";
|
|
75
|
-
if (providerId === "apple.com") return "apple";
|
|
76
|
-
if (providerId === "password") return "email";
|
|
77
|
-
}
|
|
78
|
-
return "email";
|
|
79
|
-
}
|
|
80
|
-
return undefined;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Build base user data from auth user
|
|
85
|
-
*/
|
|
86
|
-
function buildBaseData(
|
|
87
|
-
user: UserDocumentUser,
|
|
88
|
-
extras?: UserDocumentExtras,
|
|
89
|
-
): Record<string, unknown> {
|
|
90
|
-
const data: Record<string, unknown> = {
|
|
91
|
-
displayName: user.displayName,
|
|
92
|
-
email: user.email,
|
|
93
|
-
photoURL: user.photoURL,
|
|
94
|
-
isAnonymous: user.isAnonymous,
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
if (extras?.deviceId) data.deviceId = extras.deviceId;
|
|
98
|
-
if (extras?.platform) data.platform = extras.platform;
|
|
99
|
-
if (extras?.deviceModel) data.deviceModel = extras.deviceModel;
|
|
100
|
-
if (extras?.deviceBrand) data.deviceBrand = extras.deviceBrand;
|
|
101
|
-
if (extras?.osVersion) data.osVersion = extras.osVersion;
|
|
102
|
-
if (extras?.appVersion) data.appVersion = extras.appVersion;
|
|
103
|
-
if (extras?.buildNumber) data.buildNumber = extras.buildNumber;
|
|
104
|
-
if (extras?.locale) data.locale = extras.locale;
|
|
105
|
-
if (extras?.timezone) data.timezone = extras.timezone;
|
|
106
|
-
|
|
107
|
-
return data;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Build create data for new user document
|
|
112
|
-
*/
|
|
113
|
-
function buildCreateData(
|
|
114
|
-
baseData: Record<string, unknown>,
|
|
115
|
-
extras?: UserDocumentExtras,
|
|
116
|
-
): Record<string, unknown> {
|
|
117
|
-
const createData: Record<string, unknown> = {
|
|
118
|
-
...baseData,
|
|
119
|
-
...userDocumentConfig.extraFields,
|
|
120
|
-
createdAt: serverTimestamp(),
|
|
121
|
-
updatedAt: serverTimestamp(),
|
|
122
|
-
lastLoginAt: serverTimestamp(),
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
if (extras?.previousAnonymousUserId) {
|
|
126
|
-
createData.previousAnonymousUserId = extras.previousAnonymousUserId;
|
|
127
|
-
createData.convertedFromAnonymous = true;
|
|
128
|
-
createData.convertedAt = serverTimestamp();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (extras?.signUpMethod) {
|
|
132
|
-
createData.signUpMethod = extras.signUpMethod;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return createData;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Build update data for existing user document
|
|
140
|
-
*/
|
|
141
|
-
function buildUpdateData(
|
|
142
|
-
baseData: Record<string, unknown>,
|
|
143
|
-
extras?: UserDocumentExtras,
|
|
144
|
-
): Record<string, unknown> {
|
|
145
|
-
const updateData: Record<string, unknown> = {
|
|
146
|
-
...baseData,
|
|
147
|
-
lastLoginAt: serverTimestamp(),
|
|
148
|
-
updatedAt: serverTimestamp(),
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
if (extras?.previousAnonymousUserId) {
|
|
152
|
-
updateData.previousAnonymousUserId = extras.previousAnonymousUserId;
|
|
153
|
-
updateData.convertedFromAnonymous = true;
|
|
154
|
-
updateData.convertedAt = serverTimestamp();
|
|
155
|
-
if (extras?.signUpMethod) {
|
|
156
|
-
updateData.signUpMethod = extras.signUpMethod;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return updateData;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Ensure user document exists in Firestore
|
|
165
|
-
* Creates new document or updates existing one
|
|
166
|
-
*/
|
|
167
35
|
export async function ensureUserDocument(
|
|
168
36
|
user: UserDocumentUser | User,
|
|
169
37
|
extras?: UserDocumentExtras,
|
|
170
38
|
): Promise<boolean> {
|
|
171
39
|
const db = getFirestore();
|
|
172
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
173
|
-
// eslint-disable-next-line no-console
|
|
174
|
-
console.log("[UserDocumentService] db:", !!db, "type:", typeof db, "constructor:", db?.constructor?.name);
|
|
175
|
-
}
|
|
176
40
|
if (!db || !user.uid) return false;
|
|
177
41
|
|
|
178
42
|
try {
|
|
179
|
-
// Collect additional extras if configured
|
|
180
43
|
let allExtras = extras || {};
|
|
181
44
|
if (userDocumentConfig.collectExtras) {
|
|
182
45
|
const collectedExtras = await userDocumentConfig.collectExtras();
|
|
183
46
|
allExtras = { ...collectedExtras, ...allExtras };
|
|
184
47
|
}
|
|
185
48
|
|
|
186
|
-
|
|
187
|
-
if (!allExtras.signUpMethod) {
|
|
188
|
-
allExtras.signUpMethod = getSignUpMethod(user);
|
|
189
|
-
}
|
|
49
|
+
if (!allExtras.signUpMethod) allExtras.signUpMethod = getSignUpMethod(user);
|
|
190
50
|
|
|
191
51
|
const collectionName = userDocumentConfig.collectionName || "users";
|
|
192
52
|
const userRef = doc(db, collectionName, user.uid);
|
|
193
53
|
const userDoc = await getDoc(userRef);
|
|
194
54
|
const baseData = buildBaseData(user, allExtras);
|
|
195
55
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
} else {
|
|
200
|
-
const updateData = buildUpdateData(baseData, allExtras);
|
|
201
|
-
await setDoc(userRef, updateData, { merge: true });
|
|
202
|
-
}
|
|
56
|
+
const docData = !userDoc.exists()
|
|
57
|
+
? buildCreateData(baseData, userDocumentConfig.extraFields, allExtras)
|
|
58
|
+
: buildUpdateData(baseData, allExtras);
|
|
203
59
|
|
|
60
|
+
await setDoc(userRef, docData, { merge: true });
|
|
204
61
|
return true;
|
|
205
62
|
} catch (error) {
|
|
206
63
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -211,25 +68,17 @@ export async function ensureUserDocument(
|
|
|
211
68
|
}
|
|
212
69
|
}
|
|
213
70
|
|
|
214
|
-
/**
|
|
215
|
-
* Mark user as deleted (soft delete)
|
|
216
|
-
*/
|
|
217
71
|
export async function markUserDeleted(userId: string): Promise<boolean> {
|
|
218
72
|
const db = getFirestore();
|
|
219
73
|
if (!db || !userId) return false;
|
|
220
74
|
|
|
221
75
|
try {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
deletedAt: serverTimestamp(),
|
|
229
|
-
updatedAt: serverTimestamp(),
|
|
230
|
-
},
|
|
231
|
-
{ merge: true },
|
|
232
|
-
);
|
|
76
|
+
const userRef = doc(db, userDocumentConfig.collectionName || "users", userId);
|
|
77
|
+
await setDoc(userRef, {
|
|
78
|
+
isDeleted: true,
|
|
79
|
+
deletedAt: serverTimestamp(),
|
|
80
|
+
updatedAt: serverTimestamp(),
|
|
81
|
+
}, { merge: true });
|
|
233
82
|
return true;
|
|
234
83
|
} catch {
|
|
235
84
|
return false;
|