@umituz/react-native-auth 3.5.2 → 3.5.5
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.5.
|
|
3
|
+
"version": "3.5.5",
|
|
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",
|
|
@@ -1,95 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unified Auth Initialization
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Combines:
|
|
6
|
-
* - AuthService (email/password auth)
|
|
7
|
-
* - Auth Listener (state management)
|
|
8
|
-
* - User Document Service (Firestore)
|
|
9
|
-
* - Anonymous-to-authenticated conversion detection
|
|
3
|
+
* Initializes Firebase auth with user document sync and conversion tracking
|
|
10
4
|
*/
|
|
11
5
|
|
|
12
6
|
import type { Auth, User } from "firebase/auth";
|
|
13
7
|
import { getFirebaseAuth } from "@umituz/react-native-firebase";
|
|
14
8
|
import { initializeAuthService } from "./AuthService";
|
|
15
|
-
import { configureUserDocumentService
|
|
16
|
-
import type { UserDocumentConfig } from "./UserDocumentService";
|
|
9
|
+
import { configureUserDocumentService } from "./UserDocumentService";
|
|
17
10
|
import { collectDeviceExtras } from "@umituz/react-native-design-system";
|
|
18
11
|
import { initializeAuthListener } from "../../presentation/stores/initializeAuthListener";
|
|
12
|
+
import { createAuthStateHandler } from "../utils/authStateHandler";
|
|
13
|
+
import type { ConversionState } from "../utils/authConversionDetector";
|
|
19
14
|
import type { AuthConfig } from "../../domain/value-objects/AuthConfig";
|
|
20
|
-
|
|
21
15
|
import type { IStorageProvider } from "../types/Storage.types";
|
|
22
16
|
|
|
23
|
-
/**
|
|
24
|
-
* Unified auth initialization options
|
|
25
|
-
*/
|
|
26
17
|
export interface InitializeAuthOptions {
|
|
27
|
-
/** User document collection name (default: "users") */
|
|
28
18
|
userCollection?: string;
|
|
29
|
-
|
|
30
|
-
/** Additional fields to store with user documents */
|
|
31
19
|
extraFields?: Record<string, unknown>;
|
|
32
|
-
|
|
33
|
-
/** Callback to collect device/app info for user documents */
|
|
34
20
|
collectExtras?: () => Promise<Record<string, unknown>>;
|
|
35
|
-
|
|
36
|
-
/** Storage provider for persisting auth state (e.g. anonymous mode) */
|
|
37
21
|
storageProvider?: IStorageProvider;
|
|
38
|
-
|
|
39
|
-
/** Enable auto anonymous sign-in (default: true) */
|
|
40
22
|
autoAnonymousSignIn?: boolean;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Callback when user converts from anonymous to authenticated
|
|
44
|
-
* Use this to migrate user data (e.g., call Cloud Function)
|
|
45
|
-
*
|
|
46
|
-
* @param anonymousUserId - The previous anonymous user ID
|
|
47
|
-
* @param authenticatedUserId - The new authenticated user ID
|
|
48
|
-
*/
|
|
49
|
-
onUserConverted?: (
|
|
50
|
-
anonymousUserId: string,
|
|
51
|
-
authenticatedUserId: string
|
|
52
|
-
) => void | Promise<void>;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Callback when auth state changes (after user document is ensured)
|
|
56
|
-
* Called for every auth state change including initial load
|
|
57
|
-
*/
|
|
23
|
+
onUserConverted?: (anonymousId: string, authenticatedId: string) => void | Promise<void>;
|
|
58
24
|
onAuthStateChange?: (user: User | null) => void | Promise<void>;
|
|
59
|
-
|
|
60
|
-
/** Auth configuration (password rules, etc.) */
|
|
61
25
|
authConfig?: Partial<AuthConfig>;
|
|
62
26
|
}
|
|
63
27
|
|
|
64
28
|
let isInitialized = false;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
let wasAnonymous = false;
|
|
29
|
+
const conversionState: { current: ConversionState } = {
|
|
30
|
+
current: { previousUserId: null, wasAnonymous: false },
|
|
31
|
+
};
|
|
69
32
|
|
|
70
33
|
/**
|
|
71
|
-
* Initialize
|
|
72
|
-
*
|
|
73
|
-
* @example
|
|
74
|
-
* ```typescript
|
|
75
|
-
* import { initializeAuth } from '@umituz/react-native-auth';
|
|
76
|
-
* import { migrateUserData } from '@domains/migration';
|
|
77
|
-
*
|
|
78
|
-
* await initializeAuth({
|
|
79
|
-
* userCollection: 'users',
|
|
80
|
-
* autoAnonymousSignIn: true,
|
|
81
|
-
* onUserConverted: async (anonymousId, authId) => {
|
|
82
|
-
* await migrateUserData(anonymousId, authId);
|
|
83
|
-
* },
|
|
84
|
-
* });
|
|
85
|
-
* ```
|
|
34
|
+
* Initialize auth services
|
|
86
35
|
*/
|
|
87
36
|
export async function initializeAuth(
|
|
88
37
|
options: InitializeAuthOptions = {}
|
|
89
38
|
): Promise<{ success: boolean; auth: Auth | null }> {
|
|
90
39
|
if (isInitialized) {
|
|
91
|
-
|
|
92
|
-
return { success: true, auth };
|
|
40
|
+
return { success: true, auth: getFirebaseAuth() };
|
|
93
41
|
}
|
|
94
42
|
|
|
95
43
|
const {
|
|
@@ -103,94 +51,40 @@ export async function initializeAuth(
|
|
|
103
51
|
authConfig,
|
|
104
52
|
} = options;
|
|
105
53
|
|
|
106
|
-
// 1. Configure user document service
|
|
107
|
-
const userDocConfig: UserDocumentConfig = {
|
|
108
|
-
collectionName: userCollection,
|
|
109
|
-
};
|
|
110
|
-
if (extraFields) userDocConfig.extraFields = extraFields;
|
|
111
|
-
// Use provided collectExtras or default to design system's implementation
|
|
112
|
-
userDocConfig.collectExtras = collectExtras || collectDeviceExtras;
|
|
113
|
-
|
|
114
|
-
configureUserDocumentService(userDocConfig);
|
|
115
|
-
|
|
116
|
-
// 2. Get Firebase Auth
|
|
117
54
|
const auth = getFirebaseAuth();
|
|
118
|
-
if (!auth) {
|
|
119
|
-
|
|
120
|
-
|
|
55
|
+
if (!auth) return { success: false, auth: null };
|
|
56
|
+
|
|
57
|
+
configureUserDocumentService({
|
|
58
|
+
collectionName: userCollection,
|
|
59
|
+
extraFields,
|
|
60
|
+
collectExtras: collectExtras || collectDeviceExtras,
|
|
61
|
+
});
|
|
121
62
|
|
|
122
|
-
// 3. Initialize AuthService (for email/password auth)
|
|
123
63
|
try {
|
|
124
64
|
await initializeAuthService(auth, authConfig, storageProvider);
|
|
125
65
|
} catch {
|
|
126
|
-
//
|
|
127
|
-
// Email/password auth won't work, but social/anonymous will
|
|
66
|
+
// Continue without email/password auth
|
|
128
67
|
}
|
|
129
68
|
|
|
130
|
-
|
|
69
|
+
const handleAuthStateChange = createAuthStateHandler(conversionState, {
|
|
70
|
+
onUserConverted,
|
|
71
|
+
onAuthStateChange,
|
|
72
|
+
});
|
|
73
|
+
|
|
131
74
|
initializeAuthListener({
|
|
132
75
|
autoAnonymousSignIn,
|
|
133
|
-
onAuthStateChange: (user) =>
|
|
134
|
-
void (async () => {
|
|
135
|
-
if (!user) {
|
|
136
|
-
// User signed out
|
|
137
|
-
previousUserId = null;
|
|
138
|
-
wasAnonymous = false;
|
|
139
|
-
await onAuthStateChange?.(null);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const currentUserId = user.uid;
|
|
144
|
-
const isCurrentlyAnonymous = user.isAnonymous ?? false;
|
|
145
|
-
|
|
146
|
-
// Detect anonymous-to-authenticated conversion
|
|
147
|
-
// This happens in two scenarios:
|
|
148
|
-
// 1. linkWithCredential: same UID, but isAnonymous changes from true to false
|
|
149
|
-
// 2. New account after anonymous: different UID (legacy flow)
|
|
150
|
-
const isConversionSameUser = previousUserId === currentUserId && wasAnonymous && !isCurrentlyAnonymous;
|
|
151
|
-
const isConversionNewUser = previousUserId && previousUserId !== currentUserId && wasAnonymous && !isCurrentlyAnonymous;
|
|
152
|
-
const isConversion = isConversionSameUser || isConversionNewUser;
|
|
153
|
-
|
|
154
|
-
if (isConversion && onUserConverted) {
|
|
155
|
-
try {
|
|
156
|
-
await onUserConverted(previousUserId!, currentUserId);
|
|
157
|
-
} catch {
|
|
158
|
-
// Migration failed but don't block user flow
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Create/update user document in Firestore with conversion info
|
|
163
|
-
const extras = isConversion
|
|
164
|
-
? { previousAnonymousUserId: previousUserId! }
|
|
165
|
-
: undefined;
|
|
166
|
-
await ensureUserDocument(user, extras);
|
|
167
|
-
|
|
168
|
-
// Update tracking state
|
|
169
|
-
previousUserId = currentUserId;
|
|
170
|
-
wasAnonymous = isCurrentlyAnonymous;
|
|
171
|
-
|
|
172
|
-
// Call app's custom callback
|
|
173
|
-
await onAuthStateChange?.(user);
|
|
174
|
-
})();
|
|
175
|
-
},
|
|
76
|
+
onAuthStateChange: (user) => void handleAuthStateChange(user),
|
|
176
77
|
});
|
|
177
78
|
|
|
178
79
|
isInitialized = true;
|
|
179
80
|
return { success: true, auth };
|
|
180
81
|
}
|
|
181
82
|
|
|
182
|
-
/**
|
|
183
|
-
* Check if auth is initialized
|
|
184
|
-
*/
|
|
185
83
|
export function isAuthInitialized(): boolean {
|
|
186
84
|
return isInitialized;
|
|
187
85
|
}
|
|
188
86
|
|
|
189
|
-
/**
|
|
190
|
-
* Reset auth initialization state (for testing)
|
|
191
|
-
*/
|
|
192
87
|
export function resetAuthInitialization(): void {
|
|
193
88
|
isInitialized = false;
|
|
194
|
-
|
|
195
|
-
wasAnonymous = false;
|
|
89
|
+
conversionState.current = { previousUserId: null, wasAnonymous: false };
|
|
196
90
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Conversion Detector
|
|
3
|
+
* Detects anonymous-to-authenticated user conversions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ConversionState {
|
|
7
|
+
previousUserId: string | null;
|
|
8
|
+
wasAnonymous: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ConversionResult {
|
|
12
|
+
isConversion: boolean;
|
|
13
|
+
isSameUser: boolean;
|
|
14
|
+
isNewUser: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Detects if current auth state represents a conversion
|
|
19
|
+
* from anonymous to authenticated user
|
|
20
|
+
*/
|
|
21
|
+
export function detectConversion(
|
|
22
|
+
state: ConversionState,
|
|
23
|
+
currentUserId: string,
|
|
24
|
+
isCurrentlyAnonymous: boolean
|
|
25
|
+
): ConversionResult {
|
|
26
|
+
const { previousUserId, wasAnonymous } = state;
|
|
27
|
+
|
|
28
|
+
// Same UID, anonymous flag changed (linkWithCredential)
|
|
29
|
+
const isSameUser =
|
|
30
|
+
previousUserId === currentUserId && wasAnonymous && !isCurrentlyAnonymous;
|
|
31
|
+
|
|
32
|
+
// Different UID after anonymous (legacy flow)
|
|
33
|
+
const isNewUser =
|
|
34
|
+
!!previousUserId &&
|
|
35
|
+
previousUserId !== currentUserId &&
|
|
36
|
+
wasAnonymous &&
|
|
37
|
+
!isCurrentlyAnonymous;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
isConversion: isSameUser || isNewUser,
|
|
41
|
+
isSameUser,
|
|
42
|
+
isNewUser,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth State Handler
|
|
3
|
+
* Handles auth state changes with conversion detection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { User } from "firebase/auth";
|
|
7
|
+
import { ensureUserDocument } from "../services/UserDocumentService";
|
|
8
|
+
import { detectConversion, type ConversionState } from "./authConversionDetector";
|
|
9
|
+
|
|
10
|
+
export interface AuthStateHandlerOptions {
|
|
11
|
+
onUserConverted?: (anonymousId: string, authenticatedId: string) => void | Promise<void>;
|
|
12
|
+
onAuthStateChange?: (user: User | null) => void | Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates auth state change handler with conversion detection
|
|
17
|
+
*/
|
|
18
|
+
export function createAuthStateHandler(
|
|
19
|
+
state: { current: ConversionState },
|
|
20
|
+
options: AuthStateHandlerOptions
|
|
21
|
+
) {
|
|
22
|
+
return async (user: User | null): Promise<void> => {
|
|
23
|
+
const { onUserConverted, onAuthStateChange } = options;
|
|
24
|
+
|
|
25
|
+
if (!user) {
|
|
26
|
+
state.current = { previousUserId: null, wasAnonymous: false };
|
|
27
|
+
await onAuthStateChange?.(null);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const currentUserId = user.uid;
|
|
32
|
+
const isCurrentlyAnonymous = user.isAnonymous ?? false;
|
|
33
|
+
|
|
34
|
+
const conversion = detectConversion(state.current, currentUserId, isCurrentlyAnonymous);
|
|
35
|
+
|
|
36
|
+
if (conversion.isConversion && onUserConverted && state.current.previousUserId) {
|
|
37
|
+
try {
|
|
38
|
+
await onUserConverted(state.current.previousUserId, currentUserId);
|
|
39
|
+
} catch {
|
|
40
|
+
// Migration failed but don't block user flow
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const extras = conversion.isConversion && state.current.previousUserId
|
|
45
|
+
? { previousAnonymousUserId: state.current.previousUserId }
|
|
46
|
+
: undefined;
|
|
47
|
+
|
|
48
|
+
await ensureUserDocument(user, extras);
|
|
49
|
+
|
|
50
|
+
state.current = {
|
|
51
|
+
previousUserId: currentUserId,
|
|
52
|
+
wasAnonymous: isCurrentlyAnonymous,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
await onAuthStateChange?.(user);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth Listener Initialization
|
|
3
|
-
* Sets up Firebase auth
|
|
3
|
+
* Sets up Firebase auth token listener with optional auto anonymous sign-in
|
|
4
|
+
* Uses onIdTokenChanged for profile updates (displayName, email)
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
+
import { onIdTokenChanged } from "firebase/auth";
|
|
7
8
|
import {
|
|
8
9
|
getFirebaseAuth,
|
|
9
10
|
anonymousAuthService,
|
|
@@ -64,12 +65,13 @@ export function initializeAuthListener(
|
|
|
64
65
|
|
|
65
66
|
listenerInitialized = true;
|
|
66
67
|
|
|
67
|
-
const unsubscribe =
|
|
68
|
+
const unsubscribe = onIdTokenChanged(auth, (user) => {
|
|
68
69
|
if (__DEV__) {
|
|
69
|
-
console.log("[AuthListener]
|
|
70
|
+
console.log("[AuthListener] onIdTokenChanged:", {
|
|
70
71
|
uid: user?.uid ?? null,
|
|
71
72
|
isAnonymous: user?.isAnonymous ?? null,
|
|
72
73
|
email: user?.email ?? null,
|
|
74
|
+
displayName: user?.displayName ?? null,
|
|
73
75
|
});
|
|
74
76
|
}
|
|
75
77
|
|