@umituz/react-native-auth 4.2.16 → 4.2.17
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/infrastructure/services/AnonymousModeService.ts +14 -2
- package/src/infrastructure/services/AuthService.ts +25 -5
- package/src/infrastructure/utils/authStateHandler.ts +15 -2
- package/src/infrastructure/utils/listener/authStateHandler.ts +15 -2
- package/src/infrastructure/utils/listener/listenerState.util.ts +10 -2
- package/src/infrastructure/utils/listener/setupListener.ts +2 -2
- package/src/presentation/hooks/useLoginForm.ts +1 -1
- package/src/presentation/stores/authModalStore.ts +14 -2
- package/src/presentation/stores/authStore.ts +7 -1
- package/src/types/auth-store.types.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-auth",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.17",
|
|
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",
|
|
@@ -21,6 +21,8 @@ export class AnonymousModeService {
|
|
|
21
21
|
this.isAnonymousMode = value === "true";
|
|
22
22
|
return this.isAnonymousMode;
|
|
23
23
|
} catch {
|
|
24
|
+
// On error, reset to false to maintain consistency
|
|
25
|
+
this.isAnonymousMode = false;
|
|
24
26
|
return false;
|
|
25
27
|
}
|
|
26
28
|
}
|
|
@@ -40,14 +42,24 @@ export class AnonymousModeService {
|
|
|
40
42
|
this.isAnonymousMode = false;
|
|
41
43
|
return true;
|
|
42
44
|
} catch {
|
|
43
|
-
|
|
45
|
+
// Don't update memory state if storage operation failed
|
|
46
|
+
// This maintains consistency between storage and memory
|
|
44
47
|
return false;
|
|
45
48
|
}
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
async enable(storageProvider: IStorageProvider): Promise<void> {
|
|
52
|
+
// Save to storage first, then update memory to maintain consistency
|
|
53
|
+
const previousState = this.isAnonymousMode;
|
|
49
54
|
this.isAnonymousMode = true;
|
|
50
|
-
await this.save(storageProvider);
|
|
55
|
+
const saveSuccess = await this.save(storageProvider);
|
|
56
|
+
|
|
57
|
+
if (!saveSuccess) {
|
|
58
|
+
// Rollback on failure
|
|
59
|
+
this.isAnonymousMode = previousState;
|
|
60
|
+
throw new Error('Failed to save anonymous mode state');
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
emitAnonymousModeEnabled();
|
|
52
64
|
}
|
|
53
65
|
|
|
@@ -16,6 +16,7 @@ export class AuthService {
|
|
|
16
16
|
private anonymousModeService: AnonymousModeService;
|
|
17
17
|
private storageProvider?: IStorageProvider;
|
|
18
18
|
private initialized: boolean = false;
|
|
19
|
+
private initializationPromise: Promise<void> | null = null;
|
|
19
20
|
private config: AuthConfig;
|
|
20
21
|
|
|
21
22
|
constructor(config: Partial<AuthConfig> = {}, storageProvider?: IStorageProvider) {
|
|
@@ -30,14 +31,28 @@ export class AuthService {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
async initialize(): Promise<void> {
|
|
34
|
+
// Return existing promise if initialization is in progress
|
|
35
|
+
if (this.initializationPromise) {
|
|
36
|
+
return this.initializationPromise;
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
if (this.initialized) return;
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
// Create and store initialization promise to prevent concurrent initialization
|
|
42
|
+
this.initializationPromise = (async () => {
|
|
43
|
+
this.repository = new AuthRepository(this.config);
|
|
44
|
+
|
|
45
|
+
if (this.storageProvider) {
|
|
46
|
+
await this.anonymousModeService.load(this.storageProvider);
|
|
47
|
+
}
|
|
48
|
+
this.initialized = true;
|
|
49
|
+
})();
|
|
36
50
|
|
|
37
|
-
|
|
38
|
-
await this.
|
|
51
|
+
try {
|
|
52
|
+
await this.initializationPromise;
|
|
53
|
+
} finally {
|
|
54
|
+
this.initializationPromise = null;
|
|
39
55
|
}
|
|
40
|
-
this.initialized = true;
|
|
41
56
|
}
|
|
42
57
|
|
|
43
58
|
isInitialized(): boolean {
|
|
@@ -65,7 +80,12 @@ export class AuthService {
|
|
|
65
80
|
|
|
66
81
|
private async clearAnonymousModeIfNeeded(): Promise<void> {
|
|
67
82
|
if (this.anonymousModeService.getIsAnonymousMode() && this.storageProvider) {
|
|
68
|
-
await this.anonymousModeService.clear(this.storageProvider);
|
|
83
|
+
const success = await this.anonymousModeService.clear(this.storageProvider);
|
|
84
|
+
if (!success) {
|
|
85
|
+
console.warn('[AuthService] Failed to clear anonymous mode from storage');
|
|
86
|
+
// Force clear in memory to maintain consistency
|
|
87
|
+
this.anonymousModeService.setAnonymousMode(false);
|
|
88
|
+
}
|
|
69
89
|
}
|
|
70
90
|
}
|
|
71
91
|
|
|
@@ -45,13 +45,26 @@ export function createAuthStateHandler(
|
|
|
45
45
|
? { previousAnonymousUserId: state.current.previousUserId }
|
|
46
46
|
: undefined;
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
try {
|
|
49
|
+
await ensureUserDocument(user, extras);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('[AuthStateHandler] Failed to ensure user document:', error);
|
|
52
|
+
// Continue execution - don't let user document creation failure block auth flow
|
|
53
|
+
}
|
|
49
54
|
|
|
50
55
|
state.current = {
|
|
51
56
|
previousUserId: currentUserId,
|
|
52
57
|
wasAnonymous: isCurrentlyAnonymous,
|
|
53
58
|
};
|
|
54
59
|
|
|
55
|
-
|
|
60
|
+
// Call user callback with error handling
|
|
61
|
+
if (onAuthStateChange) {
|
|
62
|
+
try {
|
|
63
|
+
await onAuthStateChange(user);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('[AuthStateHandler] User callback error:', error);
|
|
66
|
+
// Don't propagate user callback errors
|
|
67
|
+
}
|
|
68
|
+
}
|
|
56
69
|
};
|
|
57
70
|
}
|
|
@@ -18,7 +18,7 @@ export function handleAuthStateChange(
|
|
|
18
18
|
store: Store,
|
|
19
19
|
auth: Auth,
|
|
20
20
|
autoAnonymousSignIn: boolean,
|
|
21
|
-
onAuthStateChange?: (user: User | null) => void
|
|
21
|
+
onAuthStateChange?: (user: User | null) => void | Promise<void>
|
|
22
22
|
): void {
|
|
23
23
|
try {
|
|
24
24
|
if (!user && autoAnonymousSignIn) {
|
|
@@ -37,7 +37,20 @@ export function handleAuthStateChange(
|
|
|
37
37
|
store.setIsAnonymous(false);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
// Call user callback with proper error handling for async callbacks
|
|
41
|
+
if (onAuthStateChange) {
|
|
42
|
+
try {
|
|
43
|
+
const result = onAuthStateChange(user);
|
|
44
|
+
// If callback returns a promise, catch rejections
|
|
45
|
+
if (result && typeof result.then === 'function') {
|
|
46
|
+
result.catch((error) => {
|
|
47
|
+
console.error("[AuthListener] User callback promise rejected:", error);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error("[AuthListener] User callback error:", error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
41
54
|
} catch (error) {
|
|
42
55
|
console.error("[AuthListener] Error handling auth state change:", error);
|
|
43
56
|
// Ensure we don't leave the app in a bad state
|
|
@@ -95,7 +95,11 @@ export function incrementRefCount(): number {
|
|
|
95
95
|
* Uses cleanupInProgress flag to prevent concurrent cleanup attempts
|
|
96
96
|
*/
|
|
97
97
|
export function decrementRefCount(): { shouldCleanup: boolean; count: number } {
|
|
98
|
-
|
|
98
|
+
// Prevent refCount from going negative
|
|
99
|
+
if (state.refCount > 0) {
|
|
100
|
+
state.refCount--;
|
|
101
|
+
}
|
|
102
|
+
|
|
99
103
|
const shouldCleanup =
|
|
100
104
|
state.refCount <= 0 &&
|
|
101
105
|
state.unsubscribe !== null &&
|
|
@@ -132,7 +136,11 @@ export function completeAnonymousSignIn(): void {
|
|
|
132
136
|
*/
|
|
133
137
|
export function resetListenerState(): void {
|
|
134
138
|
if (state.unsubscribe) {
|
|
135
|
-
|
|
139
|
+
try {
|
|
140
|
+
state.unsubscribe();
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('[ListenerState] Error during unsubscribe:', error);
|
|
143
|
+
}
|
|
136
144
|
}
|
|
137
145
|
state.initialized = false;
|
|
138
146
|
state.refCount = 0;
|
|
@@ -19,7 +19,7 @@ export function setupAuthListener(
|
|
|
19
19
|
auth: Auth,
|
|
20
20
|
store: Store,
|
|
21
21
|
autoAnonymousSignIn: boolean,
|
|
22
|
-
onAuthStateChange?: (user: User | null) => void
|
|
22
|
+
onAuthStateChange?: (user: User | null) => void | Promise<void>
|
|
23
23
|
): void {
|
|
24
24
|
const service = getAuthService();
|
|
25
25
|
|
|
@@ -58,6 +58,6 @@ export function setupAuthListener(
|
|
|
58
58
|
store.setLoading(false);
|
|
59
59
|
store.setInitialized(true);
|
|
60
60
|
store.setError("Failed to initialize authentication listener");
|
|
61
|
-
throw
|
|
61
|
+
// Don't re-throw - app state is already cleaned up and consistent
|
|
62
62
|
}
|
|
63
63
|
}
|
|
@@ -106,7 +106,7 @@ export function useLoginForm(config?: UseLoginFormConfig): UseLoginFormResult {
|
|
|
106
106
|
const localizationKey = getAuthErrorLocalizationKey(err);
|
|
107
107
|
setLocalError(getErrorMessage(localizationKey));
|
|
108
108
|
}
|
|
109
|
-
}, [fields, signIn, translations, getErrorMessage, clearErrors
|
|
109
|
+
}, [fields, signIn, translations, getErrorMessage, clearErrors]);
|
|
110
110
|
|
|
111
111
|
const handleContinueAnonymously = useCallback(async () => {
|
|
112
112
|
try {
|
|
@@ -61,7 +61,8 @@ export const useAuthModalStore = createStore<AuthModalState, AuthModalActions>({
|
|
|
61
61
|
},
|
|
62
62
|
|
|
63
63
|
hideAuthModal: () => {
|
|
64
|
-
|
|
64
|
+
// Clear pending callback to prevent memory leaks
|
|
65
|
+
set({ isVisible: false, pendingCallback: null });
|
|
65
66
|
},
|
|
66
67
|
|
|
67
68
|
setMode: (mode: AuthModalMode) => {
|
|
@@ -71,7 +72,18 @@ export const useAuthModalStore = createStore<AuthModalState, AuthModalActions>({
|
|
|
71
72
|
executePendingCallback: () => {
|
|
72
73
|
const state = get();
|
|
73
74
|
if (state.pendingCallback) {
|
|
74
|
-
|
|
75
|
+
// Wrap in try-catch to handle promise rejections
|
|
76
|
+
try {
|
|
77
|
+
const result = state.pendingCallback();
|
|
78
|
+
// If it's a promise, catch rejections
|
|
79
|
+
if (result && typeof result.then === 'function') {
|
|
80
|
+
result.catch((error) => {
|
|
81
|
+
console.error('[AuthModalStore] Pending callback error:', error);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('[AuthModalStore] Pending callback error:', error);
|
|
86
|
+
}
|
|
75
87
|
set({ pendingCallback: null });
|
|
76
88
|
}
|
|
77
89
|
},
|
|
@@ -48,7 +48,13 @@ export const useAuthStore = createStore<AuthState, AuthActions>({
|
|
|
48
48
|
initialized: state.initialized ?? false,
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
// Only restore persisted fields (isAnonymous, initialized)
|
|
52
|
+
// Never restore runtime state (user, firebaseUser, loading, error)
|
|
53
|
+
return {
|
|
54
|
+
...initialAuthState,
|
|
55
|
+
isAnonymous: state.isAnonymous ?? false,
|
|
56
|
+
initialized: state.initialized ?? false,
|
|
57
|
+
};
|
|
52
58
|
},
|
|
53
59
|
actions: (set, get) => ({
|
|
54
60
|
setFirebaseUser: (firebaseUser) => {
|
|
@@ -65,6 +65,6 @@ export const initialAuthState: AuthState = {
|
|
|
65
65
|
export interface AuthListenerOptions {
|
|
66
66
|
/** Enable auto anonymous sign-in when no user is logged in */
|
|
67
67
|
autoAnonymousSignIn?: boolean;
|
|
68
|
-
/** Callback when auth state changes */
|
|
69
|
-
onAuthStateChange?: (user: User | null) => void
|
|
68
|
+
/** Callback when auth state changes (can be async) */
|
|
69
|
+
onAuthStateChange?: (user: User | null) => void | Promise<void>;
|
|
70
70
|
}
|