@umituz/react-native-auth 3.4.21 → 3.4.23
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 +12 -99
- package/src/infrastructure/services/UserDocument.types.ts +44 -0
- package/src/infrastructure/services/UserDocumentService.ts +33 -106
- package/src/infrastructure/utils/AuthValidation.ts +37 -156
- package/src/presentation/components/AuthBottomSheet.tsx +10 -10
- package/src/presentation/hooks/useAuthBottomSheet.ts +54 -16
- package/src/__tests__/services/AuthCoreService.test.ts +0 -247
- package/src/__tests__/utils/AuthValidation.test.ts +0 -270
- package/src/presentation/components/AuthBottomSheetWrapper.tsx +0 -63
- package/src/presentation/hooks/useAuthBottomSheetWrapper.ts +0 -70
|
@@ -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
|
-
export {
|
|
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';
|
|
@@ -219,9 +144,6 @@ export { PasswordMatchIndicator } from './presentation/components/PasswordMatchI
|
|
|
219
144
|
export type { PasswordMatchIndicatorProps } from './presentation/components/PasswordMatchIndicator';
|
|
220
145
|
export { AuthBottomSheet } from './presentation/components/AuthBottomSheet';
|
|
221
146
|
export type { AuthBottomSheetProps } from './presentation/components/AuthBottomSheet';
|
|
222
|
-
export { AuthBottomSheetWrapper } from './presentation/components/AuthBottomSheetWrapper';
|
|
223
|
-
export type { AuthBottomSheetWrapperProps } from './presentation/components/AuthBottomSheetWrapper';
|
|
224
|
-
export type { SocialAuthConfiguration } from './presentation/hooks/useAuthBottomSheetWrapper';
|
|
225
147
|
export { SocialLoginButtons } from './presentation/components/SocialLoginButtons';
|
|
226
148
|
export type { SocialLoginButtonsProps } from './presentation/components/SocialLoginButtons';
|
|
227
149
|
export { ProfileSection } from './presentation/components/ProfileSection';
|
|
@@ -229,13 +151,9 @@ export type { ProfileSectionConfig, ProfileSectionProps } from './presentation/c
|
|
|
229
151
|
export { AccountActions } from './presentation/components/AccountActions';
|
|
230
152
|
export type { AccountActionsConfig, AccountActionsProps } from './presentation/components/AccountActions';
|
|
231
153
|
|
|
232
|
-
//
|
|
233
|
-
// PRESENTATION LAYER - Stores
|
|
234
|
-
// =============================================================================
|
|
235
|
-
|
|
154
|
+
// STORES
|
|
236
155
|
export { useAuthModalStore } from './presentation/stores/authModalStore';
|
|
237
156
|
export type { AuthModalMode } from './presentation/stores/authModalStore';
|
|
238
|
-
|
|
239
157
|
export {
|
|
240
158
|
useAuthStore,
|
|
241
159
|
initializeAuthListener,
|
|
@@ -251,13 +169,8 @@ export {
|
|
|
251
169
|
getIsAuthenticated,
|
|
252
170
|
getIsAnonymous,
|
|
253
171
|
} from './presentation/stores/authStore';
|
|
254
|
-
|
|
255
172
|
export type { UserType, AuthState, AuthActions } from './presentation/stores/authStore';
|
|
256
173
|
export type { AuthListenerOptions } from './types/auth-store.types';
|
|
257
174
|
|
|
258
|
-
//
|
|
259
|
-
// PRESENTATION LAYER - Utilities
|
|
260
|
-
// =============================================================================
|
|
261
|
-
|
|
175
|
+
// UTILITIES
|
|
262
176
|
export { getAuthErrorLocalizationKey } from './presentation/utils/getAuthErrorMessage';
|
|
263
|
-
|
|
@@ -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,68 +1,31 @@
|
|
|
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
|
+
|
|
15
|
+
export type {
|
|
16
|
+
UserDocumentUser,
|
|
17
|
+
UserDocumentConfig,
|
|
18
|
+
UserDocumentExtras,
|
|
19
|
+
} from "./UserDocument.types";
|
|
10
20
|
|
|
11
21
|
declare const __DEV__: boolean;
|
|
12
22
|
|
|
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
23
|
let userDocumentConfig: UserDocumentConfig = {};
|
|
55
24
|
|
|
56
|
-
/**
|
|
57
|
-
* Configure user document service
|
|
58
|
-
*/
|
|
59
25
|
export function configureUserDocumentService(config: UserDocumentConfig): void {
|
|
60
26
|
userDocumentConfig = { ...config };
|
|
61
27
|
}
|
|
62
28
|
|
|
63
|
-
/**
|
|
64
|
-
* Get sign-up method from auth user
|
|
65
|
-
*/
|
|
66
29
|
function getSignUpMethod(user: UserDocumentUser): string | undefined {
|
|
67
30
|
if (user.isAnonymous) return "anonymous";
|
|
68
31
|
if (user.email) {
|
|
@@ -80,9 +43,6 @@ function getSignUpMethod(user: UserDocumentUser): string | undefined {
|
|
|
80
43
|
return undefined;
|
|
81
44
|
}
|
|
82
45
|
|
|
83
|
-
/**
|
|
84
|
-
* Build base user data from auth user
|
|
85
|
-
*/
|
|
86
46
|
function buildBaseData(
|
|
87
47
|
user: UserDocumentUser,
|
|
88
48
|
extras?: UserDocumentExtras,
|
|
@@ -94,22 +54,19 @@ function buildBaseData(
|
|
|
94
54
|
isAnonymous: user.isAnonymous,
|
|
95
55
|
};
|
|
96
56
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
57
|
+
const fields: (keyof UserDocumentExtras)[] = [
|
|
58
|
+
'deviceId', 'platform', 'deviceModel', 'deviceBrand',
|
|
59
|
+
'osVersion', 'appVersion', 'buildNumber', 'locale', 'timezone'
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
fields.forEach(field => {
|
|
63
|
+
const val = extras?.[field];
|
|
64
|
+
if (val) data[field] = val;
|
|
65
|
+
});
|
|
106
66
|
|
|
107
67
|
return data;
|
|
108
68
|
}
|
|
109
69
|
|
|
110
|
-
/**
|
|
111
|
-
* Build create data for new user document
|
|
112
|
-
*/
|
|
113
70
|
function buildCreateData(
|
|
114
71
|
baseData: Record<string, unknown>,
|
|
115
72
|
extras?: UserDocumentExtras,
|
|
@@ -128,16 +85,11 @@ function buildCreateData(
|
|
|
128
85
|
createData.convertedAt = serverTimestamp();
|
|
129
86
|
}
|
|
130
87
|
|
|
131
|
-
if (extras?.signUpMethod)
|
|
132
|
-
createData.signUpMethod = extras.signUpMethod;
|
|
133
|
-
}
|
|
88
|
+
if (extras?.signUpMethod) createData.signUpMethod = extras.signUpMethod;
|
|
134
89
|
|
|
135
90
|
return createData;
|
|
136
91
|
}
|
|
137
92
|
|
|
138
|
-
/**
|
|
139
|
-
* Build update data for existing user document
|
|
140
|
-
*/
|
|
141
93
|
function buildUpdateData(
|
|
142
94
|
baseData: Record<string, unknown>,
|
|
143
95
|
extras?: UserDocumentExtras,
|
|
@@ -152,55 +104,38 @@ function buildUpdateData(
|
|
|
152
104
|
updateData.previousAnonymousUserId = extras.previousAnonymousUserId;
|
|
153
105
|
updateData.convertedFromAnonymous = true;
|
|
154
106
|
updateData.convertedAt = serverTimestamp();
|
|
155
|
-
if (extras?.signUpMethod)
|
|
156
|
-
updateData.signUpMethod = extras.signUpMethod;
|
|
157
|
-
}
|
|
107
|
+
if (extras?.signUpMethod) updateData.signUpMethod = extras.signUpMethod;
|
|
158
108
|
}
|
|
159
109
|
|
|
160
110
|
return updateData;
|
|
161
111
|
}
|
|
162
112
|
|
|
163
|
-
/**
|
|
164
|
-
* Ensure user document exists in Firestore
|
|
165
|
-
* Creates new document or updates existing one
|
|
166
|
-
*/
|
|
167
113
|
export async function ensureUserDocument(
|
|
168
114
|
user: UserDocumentUser | User,
|
|
169
115
|
extras?: UserDocumentExtras,
|
|
170
116
|
): Promise<boolean> {
|
|
171
117
|
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
118
|
if (!db || !user.uid) return false;
|
|
177
119
|
|
|
178
120
|
try {
|
|
179
|
-
// Collect additional extras if configured
|
|
180
121
|
let allExtras = extras || {};
|
|
181
122
|
if (userDocumentConfig.collectExtras) {
|
|
182
123
|
const collectedExtras = await userDocumentConfig.collectExtras();
|
|
183
124
|
allExtras = { ...collectedExtras, ...allExtras };
|
|
184
125
|
}
|
|
185
126
|
|
|
186
|
-
|
|
187
|
-
if (!allExtras.signUpMethod) {
|
|
188
|
-
allExtras.signUpMethod = getSignUpMethod(user);
|
|
189
|
-
}
|
|
127
|
+
if (!allExtras.signUpMethod) allExtras.signUpMethod = getSignUpMethod(user);
|
|
190
128
|
|
|
191
129
|
const collectionName = userDocumentConfig.collectionName || "users";
|
|
192
130
|
const userRef = doc(db, collectionName, user.uid);
|
|
193
131
|
const userDoc = await getDoc(userRef);
|
|
194
132
|
const baseData = buildBaseData(user, allExtras);
|
|
195
133
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
} else {
|
|
200
|
-
const updateData = buildUpdateData(baseData, allExtras);
|
|
201
|
-
await setDoc(userRef, updateData, { merge: true });
|
|
202
|
-
}
|
|
134
|
+
const docData = !userDoc.exists()
|
|
135
|
+
? buildCreateData(baseData, allExtras)
|
|
136
|
+
: buildUpdateData(baseData, allExtras);
|
|
203
137
|
|
|
138
|
+
await setDoc(userRef, docData, { merge: true });
|
|
204
139
|
return true;
|
|
205
140
|
} catch (error) {
|
|
206
141
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -211,25 +146,17 @@ export async function ensureUserDocument(
|
|
|
211
146
|
}
|
|
212
147
|
}
|
|
213
148
|
|
|
214
|
-
/**
|
|
215
|
-
* Mark user as deleted (soft delete)
|
|
216
|
-
*/
|
|
217
149
|
export async function markUserDeleted(userId: string): Promise<boolean> {
|
|
218
150
|
const db = getFirestore();
|
|
219
151
|
if (!db || !userId) return false;
|
|
220
152
|
|
|
221
153
|
try {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
deletedAt: serverTimestamp(),
|
|
229
|
-
updatedAt: serverTimestamp(),
|
|
230
|
-
},
|
|
231
|
-
{ merge: true },
|
|
232
|
-
);
|
|
154
|
+
const userRef = doc(db, userDocumentConfig.collectionName || "users", userId);
|
|
155
|
+
await setDoc(userRef, {
|
|
156
|
+
isDeleted: true,
|
|
157
|
+
deletedAt: serverTimestamp(),
|
|
158
|
+
updatedAt: serverTimestamp(),
|
|
159
|
+
}, { merge: true });
|
|
233
160
|
return true;
|
|
234
161
|
} catch {
|
|
235
162
|
return false;
|