@umituz/react-native-firebase 1.13.27 → 1.13.29
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 +12 -3
- package/src/auth/infrastructure/services/apple-auth.service.ts +4 -1
- package/src/auth/infrastructure/services/reauthentication.service.ts +5 -2
- package/src/auth/infrastructure/stores/auth.store.ts +73 -0
- package/src/auth/presentation/hooks/useFirebaseAuth.ts +10 -35
- package/src/firestore/__tests__/setup.ts +21 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-firebase",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.29",
|
|
4
4
|
"description": "Unified Firebase package for React Native apps - Auth and Firestore services using Firebase JS SDK (no native modules).",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -35,14 +35,23 @@
|
|
|
35
35
|
"expo-crypto": ">=13.0.0",
|
|
36
36
|
"firebase": ">=10.0.0",
|
|
37
37
|
"react": ">=18.2.0",
|
|
38
|
-
"react-native": ">=0.74.0"
|
|
38
|
+
"react-native": ">=0.74.0",
|
|
39
|
+
"zustand": ">=5.0.0"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
42
|
+
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
43
|
+
"@sentry/react-native": "^7.8.0",
|
|
44
|
+
"@sentry/types": "^10.32.1",
|
|
45
|
+
"@types/jest": "^30.0.0",
|
|
41
46
|
"@types/react": "~19.1.10",
|
|
47
|
+
"@umituz/react-native-sentry": "^1.4.2",
|
|
48
|
+
"expo-apple-authentication": "^8.0.8",
|
|
49
|
+
"expo-crypto": "^15.0.8",
|
|
42
50
|
"firebase": "^12.6.0",
|
|
43
51
|
"react": "19.1.0",
|
|
44
52
|
"react-native": "0.81.5",
|
|
45
|
-
"typescript": "~5.9.2"
|
|
53
|
+
"typescript": "~5.9.2",
|
|
54
|
+
"zustand": "^5.0.0"
|
|
46
55
|
},
|
|
47
56
|
"publishConfig": {
|
|
48
57
|
"access": "public"
|
|
@@ -167,7 +167,10 @@ export class AppleAuthService {
|
|
|
167
167
|
let result = "";
|
|
168
168
|
|
|
169
169
|
for (let i = 0; i < randomBytes.length; i++) {
|
|
170
|
-
|
|
170
|
+
const byte = randomBytes[i];
|
|
171
|
+
if (byte !== undefined) {
|
|
172
|
+
result += chars.charAt(byte % chars.length);
|
|
173
|
+
}
|
|
171
174
|
}
|
|
172
175
|
|
|
173
176
|
return result;
|
|
@@ -126,7 +126,10 @@ async function generateNonce(length: number = 32): Promise<string> {
|
|
|
126
126
|
let result = "";
|
|
127
127
|
|
|
128
128
|
for (let i = 0; i < randomBytes.length; i++) {
|
|
129
|
-
|
|
129
|
+
const byte = randomBytes[i];
|
|
130
|
+
if (byte !== undefined) {
|
|
131
|
+
result += chars.charAt(byte % chars.length);
|
|
132
|
+
}
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
return result;
|
|
@@ -223,7 +226,7 @@ export async function getAppleReauthCredential(): Promise<{
|
|
|
223
226
|
*/
|
|
224
227
|
export async function reauthenticateWithApple(user: User): Promise<ReauthenticationResult> {
|
|
225
228
|
const result = await getAppleReauthCredential();
|
|
226
|
-
|
|
229
|
+
|
|
227
230
|
if (!result.success || !result.credential) {
|
|
228
231
|
return {
|
|
229
232
|
success: false,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Auth Store
|
|
3
|
+
* Shared Zustand store for Firebase Auth state
|
|
4
|
+
*
|
|
5
|
+
* CRITICAL: This store ensures only ONE auth listener is created,
|
|
6
|
+
* preventing performance issues from multiple subscriptions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { create } from "zustand";
|
|
10
|
+
import { onAuthStateChanged, type User, type Auth } from "firebase/auth";
|
|
11
|
+
|
|
12
|
+
declare const __DEV__: boolean;
|
|
13
|
+
|
|
14
|
+
interface AuthState {
|
|
15
|
+
user: User | null;
|
|
16
|
+
loading: boolean;
|
|
17
|
+
initialized: boolean;
|
|
18
|
+
listenerSetup: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface AuthActions {
|
|
22
|
+
setupListener: (auth: Auth) => void;
|
|
23
|
+
cleanup: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type AuthStore = AuthState & AuthActions;
|
|
27
|
+
|
|
28
|
+
let unsubscribe: (() => void) | null = null;
|
|
29
|
+
|
|
30
|
+
export const useAuthStore = create<AuthStore>((set, get) => ({
|
|
31
|
+
user: null,
|
|
32
|
+
loading: true,
|
|
33
|
+
initialized: false,
|
|
34
|
+
listenerSetup: false,
|
|
35
|
+
|
|
36
|
+
setupListener: (auth: Auth) => {
|
|
37
|
+
const state = get();
|
|
38
|
+
|
|
39
|
+
// Only setup listener once
|
|
40
|
+
if (state.listenerSetup || unsubscribe) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
set({ listenerSetup: true });
|
|
45
|
+
|
|
46
|
+
unsubscribe = onAuthStateChanged(auth, (currentUser: User | null) => {
|
|
47
|
+
if (__DEV__) {
|
|
48
|
+
console.log(
|
|
49
|
+
"[AuthStore] Auth state changed:",
|
|
50
|
+
currentUser?.uid || "null"
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
set({
|
|
54
|
+
user: currentUser,
|
|
55
|
+
loading: false,
|
|
56
|
+
initialized: true,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
cleanup: () => {
|
|
62
|
+
if (unsubscribe) {
|
|
63
|
+
unsubscribe();
|
|
64
|
+
unsubscribe = null;
|
|
65
|
+
}
|
|
66
|
+
set({
|
|
67
|
+
user: null,
|
|
68
|
+
loading: true,
|
|
69
|
+
initialized: false,
|
|
70
|
+
listenerSetup: false,
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
}));
|
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
* useFirebaseAuth Hook
|
|
3
3
|
* React hook for Firebase Auth state management
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Uses shared Zustand store to ensure only ONE auth listener exists.
|
|
6
|
+
* This prevents performance issues from multiple subscriptions.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { useEffect
|
|
10
|
-
import {
|
|
9
|
+
import { useEffect } from "react";
|
|
10
|
+
import type { User } from "firebase/auth";
|
|
11
11
|
import { getFirebaseAuth } from "../../infrastructure/config/FirebaseAuthClient";
|
|
12
|
-
|
|
13
|
-
declare const __DEV__: boolean;
|
|
12
|
+
import { useAuthStore } from "../../infrastructure/stores/auth.store";
|
|
14
13
|
|
|
15
14
|
export interface UseFirebaseAuthResult {
|
|
16
15
|
/** Current authenticated user from Firebase Auth */
|
|
@@ -24,7 +23,7 @@ export interface UseFirebaseAuthResult {
|
|
|
24
23
|
/**
|
|
25
24
|
* Hook for Firebase Auth state management
|
|
26
25
|
*
|
|
27
|
-
*
|
|
26
|
+
* Uses shared store to ensure only one listener is active.
|
|
28
27
|
* Auth is pre-initialized in appInitializer, so no retry needed.
|
|
29
28
|
*
|
|
30
29
|
* @example
|
|
@@ -33,41 +32,18 @@ export interface UseFirebaseAuthResult {
|
|
|
33
32
|
* ```
|
|
34
33
|
*/
|
|
35
34
|
export function useFirebaseAuth(): UseFirebaseAuthResult {
|
|
36
|
-
const
|
|
37
|
-
const [loading, setLoading] = useState(true);
|
|
38
|
-
const [initialized, setInitialized] = useState(false);
|
|
39
|
-
|
|
40
|
-
const unsubscribeRef = useRef<(() => void) | null>(null);
|
|
35
|
+
const { user, loading, initialized, setupListener } = useAuthStore();
|
|
41
36
|
|
|
42
37
|
useEffect(() => {
|
|
43
38
|
const auth = getFirebaseAuth();
|
|
44
39
|
|
|
45
40
|
if (!auth) {
|
|
46
|
-
// Auth not available (offline mode or error)
|
|
47
|
-
setLoading(false);
|
|
48
|
-
setUser(null);
|
|
49
|
-
setInitialized(false);
|
|
50
41
|
return;
|
|
51
42
|
}
|
|
52
43
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
console.log('[useFirebaseAuth] Auth state changed:', currentUser?.uid || 'null');
|
|
57
|
-
}
|
|
58
|
-
setUser(currentUser);
|
|
59
|
-
setLoading(false);
|
|
60
|
-
setInitialized(true);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// Cleanup on unmount
|
|
64
|
-
return () => {
|
|
65
|
-
if (unsubscribeRef.current) {
|
|
66
|
-
unsubscribeRef.current();
|
|
67
|
-
unsubscribeRef.current = null;
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
}, []); // Empty deps - subscribe once on mount
|
|
44
|
+
// Setup listener (will only run once due to store check)
|
|
45
|
+
setupListener(auth);
|
|
46
|
+
}, [setupListener]);
|
|
71
47
|
|
|
72
48
|
return {
|
|
73
49
|
user,
|
|
@@ -75,4 +51,3 @@ export function useFirebaseAuth(): UseFirebaseAuthResult {
|
|
|
75
51
|
initialized,
|
|
76
52
|
};
|
|
77
53
|
}
|
|
78
|
-
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
// Mock __DEV__ for tests
|
|
6
|
-
export {};
|
|
6
|
+
export { };
|
|
7
7
|
|
|
8
8
|
declare global {
|
|
9
|
-
var
|
|
9
|
+
var mockFirestore: () => any;
|
|
10
|
+
var mockFirebaseError: (code: string, message: string) => any;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
if (typeof (global as any).__DEV__ === 'undefined') {
|
|
@@ -16,20 +17,28 @@ if (typeof (global as any).__DEV__ === 'undefined') {
|
|
|
16
17
|
// Mock console methods to avoid noise in tests
|
|
17
18
|
const originalConsole = { ...console };
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const globalAny = global as any;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* We check if beforeEach is available before using it
|
|
24
|
+
* This avoids errors when running tsc or in environments without Jest
|
|
25
|
+
*/
|
|
26
|
+
if (typeof globalAny.beforeEach !== 'undefined') {
|
|
27
|
+
globalAny.beforeEach(() => {
|
|
28
|
+
// Restore console methods before each test
|
|
29
|
+
Object.assign(console, originalConsole);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
23
32
|
|
|
24
33
|
// Set up global test utilities
|
|
25
|
-
|
|
26
|
-
collection: jest
|
|
27
|
-
doc: jest
|
|
28
|
-
runTransaction: jest
|
|
29
|
-
batch: jest
|
|
34
|
+
globalAny.mockFirestore = () => ({
|
|
35
|
+
collection: globalAny.jest?.fn() || (() => ({})),
|
|
36
|
+
doc: globalAny.jest?.fn() || (() => ({})),
|
|
37
|
+
runTransaction: globalAny.jest?.fn() || (() => Promise.resolve()),
|
|
38
|
+
batch: globalAny.jest?.fn() || (() => ({})),
|
|
30
39
|
});
|
|
31
40
|
|
|
32
|
-
|
|
41
|
+
globalAny.mockFirebaseError = (code: string, message: string) => {
|
|
33
42
|
const error = new Error(message) as any;
|
|
34
43
|
error.code = code;
|
|
35
44
|
return error;
|