@umituz/react-native-firebase 1.13.2 → 1.13.4
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 +10 -2
- package/src/auth/domain/entities/AnonymousUser.ts +44 -0
- package/src/auth/domain/errors/FirebaseAuthError.ts +18 -0
- package/src/auth/domain/value-objects/FirebaseAuthConfig.ts +45 -0
- package/src/auth/index.ts +146 -0
- package/src/auth/infrastructure/config/FirebaseAuthClient.ts +210 -0
- package/src/auth/infrastructure/config/initializers/FirebaseAuthInitializer.ts +148 -0
- package/src/auth/infrastructure/services/account-deletion.service.ts +250 -0
- package/src/auth/infrastructure/services/anonymous-auth.service.ts +135 -0
- package/src/auth/infrastructure/services/apple-auth.service.ts +146 -0
- package/src/auth/infrastructure/services/auth-guard.service.ts +97 -0
- package/src/auth/infrastructure/services/auth-utils.service.ts +168 -0
- package/src/auth/infrastructure/services/firestore-utils.service.ts +155 -0
- package/src/auth/infrastructure/services/google-auth.service.ts +100 -0
- package/src/auth/infrastructure/services/reauthentication.service.ts +216 -0
- package/src/auth/presentation/hooks/useAnonymousAuth.ts +201 -0
- package/src/auth/presentation/hooks/useFirebaseAuth.ts +84 -0
- package/src/auth/presentation/hooks/useSocialAuth.ts +162 -0
- package/src/firestore/__tests__/BaseRepository.test.ts +133 -0
- package/src/firestore/__tests__/QueryDeduplicationMiddleware.test.ts +147 -0
- package/src/firestore/__tests__/mocks/react-native-firebase.ts +23 -0
- package/src/firestore/__tests__/setup.ts +36 -0
- package/src/firestore/domain/constants/QuotaLimits.ts +97 -0
- package/src/firestore/domain/entities/QuotaMetrics.ts +28 -0
- package/src/firestore/domain/entities/RequestLog.ts +30 -0
- package/src/firestore/domain/errors/FirebaseFirestoreError.ts +52 -0
- package/src/firestore/domain/services/QuotaCalculator.ts +70 -0
- package/src/firestore/index.ts +174 -0
- package/src/firestore/infrastructure/config/FirestoreClient.ts +181 -0
- package/src/firestore/infrastructure/config/initializers/FirebaseFirestoreInitializer.ts +46 -0
- package/src/firestore/infrastructure/middleware/QueryDeduplicationMiddleware.ts +153 -0
- package/src/firestore/infrastructure/middleware/QuotaTrackingMiddleware.ts +165 -0
- package/src/firestore/infrastructure/repositories/BasePaginatedRepository.ts +90 -0
- package/src/firestore/infrastructure/repositories/BaseQueryRepository.ts +80 -0
- package/src/firestore/infrastructure/repositories/BaseRepository.ts +147 -0
- package/src/firestore/infrastructure/services/QuotaMonitorService.ts +108 -0
- package/src/firestore/infrastructure/services/RequestLoggerService.ts +139 -0
- package/src/firestore/types/pagination.types.ts +60 -0
- package/src/firestore/utils/dateUtils.ts +31 -0
- package/src/firestore/utils/document-mapper.helper.ts +145 -0
- package/src/firestore/utils/pagination.helper.ts +93 -0
- package/src/firestore/utils/query-builder.ts +188 -0
- package/src/firestore/utils/quota-error-detector.util.ts +100 -0
- package/src/index.ts +16 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAnonymousAuth Hook
|
|
3
|
+
* React hook for anonymous authentication state
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
7
|
+
import { onAuthStateChanged, type Auth } from "firebase/auth";
|
|
8
|
+
import { checkAuthState, type AuthCheckResult } from "../../infrastructure/services/auth-utils.service";
|
|
9
|
+
import { anonymousAuthService, type AnonymousAuthResult } from "../../infrastructure/services/anonymous-auth.service";
|
|
10
|
+
|
|
11
|
+
declare const __DEV__: boolean;
|
|
12
|
+
|
|
13
|
+
export interface UseAnonymousAuthResult extends AuthCheckResult {
|
|
14
|
+
/**
|
|
15
|
+
* Sign in anonymously
|
|
16
|
+
*/
|
|
17
|
+
signInAnonymously: () => Promise<AnonymousAuthResult>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Loading state
|
|
21
|
+
*/
|
|
22
|
+
readonly loading: boolean;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Error state
|
|
26
|
+
*/
|
|
27
|
+
readonly error: Error | null;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Clear error
|
|
31
|
+
*/
|
|
32
|
+
clearError: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Hook for anonymous authentication
|
|
37
|
+
*/
|
|
38
|
+
export function useAnonymousAuth(auth: Auth | null): UseAnonymousAuthResult {
|
|
39
|
+
const [authState, setAuthState] = useState<AuthCheckResult>(() =>
|
|
40
|
+
checkAuthState(auth),
|
|
41
|
+
);
|
|
42
|
+
const [loading, setLoading] = useState(true);
|
|
43
|
+
const [error, setError] = useState<Error | null>(null);
|
|
44
|
+
const authRef = useRef(auth);
|
|
45
|
+
const unsubscribeRef = useRef<(() => void) | null>(null);
|
|
46
|
+
|
|
47
|
+
// Update ref when auth changes
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
authRef.current = auth;
|
|
50
|
+
}, [auth]);
|
|
51
|
+
|
|
52
|
+
// Clear error helper
|
|
53
|
+
const clearError = useCallback(() => {
|
|
54
|
+
setError(null);
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
// Auth state change handler - accepts user from onAuthStateChanged callback
|
|
58
|
+
const handleAuthStateChange = useCallback((user: import("firebase/auth").User | null) => {
|
|
59
|
+
try {
|
|
60
|
+
// Use the user from the callback, NOT auth.currentUser
|
|
61
|
+
// This ensures we have the correct user from Firebase's persistence
|
|
62
|
+
if (!user) {
|
|
63
|
+
setAuthState({
|
|
64
|
+
isAuthenticated: false,
|
|
65
|
+
isAnonymous: false,
|
|
66
|
+
isGuest: false,
|
|
67
|
+
currentUser: null,
|
|
68
|
+
userId: null,
|
|
69
|
+
});
|
|
70
|
+
} else {
|
|
71
|
+
const anonymous = user.isAnonymous === true;
|
|
72
|
+
setAuthState({
|
|
73
|
+
isAuthenticated: true,
|
|
74
|
+
isAnonymous: anonymous,
|
|
75
|
+
isGuest: anonymous,
|
|
76
|
+
currentUser: user,
|
|
77
|
+
userId: user.uid,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
setError(null);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
const authError = err instanceof Error ? err : new Error('Auth state check failed');
|
|
83
|
+
setError(authError);
|
|
84
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
85
|
+
// eslint-disable-next-line no-console
|
|
86
|
+
console.error("[useAnonymousAuth] Auth state change error", authError);
|
|
87
|
+
}
|
|
88
|
+
} finally {
|
|
89
|
+
setLoading(false);
|
|
90
|
+
}
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
// Setup auth state listener
|
|
94
|
+
// IMPORTANT: Do NOT call handleAuthStateChange() immediately!
|
|
95
|
+
// Wait for onAuthStateChanged to fire - it will have the correct user from persistence
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
// Cleanup previous listener
|
|
98
|
+
if (unsubscribeRef.current) {
|
|
99
|
+
unsubscribeRef.current();
|
|
100
|
+
unsubscribeRef.current = null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!auth) {
|
|
104
|
+
setAuthState({
|
|
105
|
+
isAuthenticated: false,
|
|
106
|
+
isAnonymous: false,
|
|
107
|
+
isGuest: false,
|
|
108
|
+
currentUser: null,
|
|
109
|
+
userId: null,
|
|
110
|
+
});
|
|
111
|
+
setLoading(false);
|
|
112
|
+
setError(null);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Keep loading true until onAuthStateChanged fires
|
|
117
|
+
setLoading(true);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// Listen to auth state changes - this is the ONLY source of truth
|
|
121
|
+
// The first callback will have the user restored from persistence (or null)
|
|
122
|
+
unsubscribeRef.current = onAuthStateChanged(auth, (user) => {
|
|
123
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
124
|
+
// eslint-disable-next-line no-console
|
|
125
|
+
console.log("[useAnonymousAuth] onAuthStateChanged fired", {
|
|
126
|
+
hasUser: !!user,
|
|
127
|
+
uid: user?.uid,
|
|
128
|
+
isAnonymous: user?.isAnonymous,
|
|
129
|
+
email: user?.email,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// IMPORTANT: Pass the user from the callback, not auth.currentUser!
|
|
133
|
+
handleAuthStateChange(user);
|
|
134
|
+
});
|
|
135
|
+
} catch (err) {
|
|
136
|
+
const authError = err instanceof Error ? err : new Error('Auth listener setup failed');
|
|
137
|
+
setError(authError);
|
|
138
|
+
setLoading(false);
|
|
139
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
140
|
+
// eslint-disable-next-line no-console
|
|
141
|
+
console.error("[useAnonymousAuth] Auth listener setup error", authError);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Cleanup function
|
|
146
|
+
return () => {
|
|
147
|
+
if (unsubscribeRef.current) {
|
|
148
|
+
unsubscribeRef.current();
|
|
149
|
+
unsubscribeRef.current = null;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}, [auth, handleAuthStateChange]);
|
|
153
|
+
|
|
154
|
+
// Sign in anonymously
|
|
155
|
+
const signInAnonymously = useCallback(async (): Promise<AnonymousAuthResult> => {
|
|
156
|
+
if (!auth) {
|
|
157
|
+
const authError = new Error("Firebase Auth not initialized");
|
|
158
|
+
setError(authError);
|
|
159
|
+
throw authError;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
setLoading(true);
|
|
163
|
+
setError(null);
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const result = await anonymousAuthService.signInAnonymously(auth);
|
|
167
|
+
|
|
168
|
+
// Update auth state after successful sign in
|
|
169
|
+
// Pass the user from the result, not from auth.currentUser
|
|
170
|
+
handleAuthStateChange(result.user);
|
|
171
|
+
|
|
172
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
173
|
+
// eslint-disable-next-line no-console
|
|
174
|
+
console.log("[useAnonymousAuth] Successfully signed in anonymously", {
|
|
175
|
+
uid: result.anonymousUser.uid,
|
|
176
|
+
wasAlreadySignedIn: result.wasAlreadySignedIn,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return result;
|
|
181
|
+
} catch (err) {
|
|
182
|
+
const authError = err instanceof Error ? err : new Error('Anonymous sign in failed');
|
|
183
|
+
setError(authError);
|
|
184
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
185
|
+
// eslint-disable-next-line no-console
|
|
186
|
+
console.error("[useAnonymousAuth] Sign in error", authError);
|
|
187
|
+
}
|
|
188
|
+
throw authError;
|
|
189
|
+
} finally {
|
|
190
|
+
setLoading(false);
|
|
191
|
+
}
|
|
192
|
+
}, [auth, handleAuthStateChange]);
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
...authState,
|
|
196
|
+
signInAnonymously,
|
|
197
|
+
loading,
|
|
198
|
+
error,
|
|
199
|
+
clearError,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFirebaseAuth Hook
|
|
3
|
+
* React hook for Firebase Auth state management
|
|
4
|
+
*
|
|
5
|
+
* Directly uses Firebase Auth's built-in state management via onAuthStateChanged
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useEffect, useState } from "react";
|
|
9
|
+
import { onAuthStateChanged, type User } from "firebase/auth";
|
|
10
|
+
import { getFirebaseAuth, isFirebaseAuthInitialized } from "../../infrastructure/config/FirebaseAuthClient";
|
|
11
|
+
|
|
12
|
+
export interface UseFirebaseAuthResult {
|
|
13
|
+
/** Current authenticated user from Firebase Auth */
|
|
14
|
+
user: User | null;
|
|
15
|
+
/** Whether auth state is loading (initial check) */
|
|
16
|
+
loading: boolean;
|
|
17
|
+
/** Whether Firebase Auth is initialized */
|
|
18
|
+
initialized: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Hook for Firebase Auth state management
|
|
23
|
+
*
|
|
24
|
+
* Directly uses Firebase Auth's built-in state management.
|
|
25
|
+
* No additional state management layer needed.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const { user, loading } = useFirebaseAuth();
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function useFirebaseAuth(): UseFirebaseAuthResult {
|
|
33
|
+
const [user, setUser] = useState<User | null>(null);
|
|
34
|
+
const [loading, setLoading] = useState(true);
|
|
35
|
+
const [initialized, setInitialized] = useState(false);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
// Check if Firebase Auth is initialized
|
|
39
|
+
const isInitialized = isFirebaseAuthInitialized();
|
|
40
|
+
setInitialized(isInitialized);
|
|
41
|
+
|
|
42
|
+
if (!isInitialized) {
|
|
43
|
+
setLoading(false);
|
|
44
|
+
setUser(null);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const auth = getFirebaseAuth();
|
|
50
|
+
|
|
51
|
+
if (!auth) {
|
|
52
|
+
setUser(null);
|
|
53
|
+
setLoading(false);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Subscribe to auth state changes
|
|
58
|
+
const unsubscribe = onAuthStateChanged(auth, (currentUser: User | null) => {
|
|
59
|
+
setUser(currentUser);
|
|
60
|
+
setLoading(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Set initial state
|
|
64
|
+
setUser(auth.currentUser);
|
|
65
|
+
setLoading(false);
|
|
66
|
+
|
|
67
|
+
return () => {
|
|
68
|
+
unsubscribe();
|
|
69
|
+
};
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// Firebase Auth not initialized or error
|
|
72
|
+
setUser(null);
|
|
73
|
+
setLoading(false);
|
|
74
|
+
return () => {};
|
|
75
|
+
}
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
user,
|
|
80
|
+
loading,
|
|
81
|
+
initialized,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSocialAuth Hook
|
|
3
|
+
* Provides Google and Apple Sign-In functionality
|
|
4
|
+
*
|
|
5
|
+
* Note: This hook handles the Firebase authentication part.
|
|
6
|
+
* The OAuth flow (expo-auth-session for Google) should be set up in the consuming app.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useState, useCallback, useEffect } from "react";
|
|
10
|
+
import { getFirebaseAuth } from "../../infrastructure/config/FirebaseAuthClient";
|
|
11
|
+
import {
|
|
12
|
+
googleAuthService,
|
|
13
|
+
type GoogleAuthConfig,
|
|
14
|
+
} from "../../infrastructure/services/google-auth.service";
|
|
15
|
+
import { appleAuthService } from "../../infrastructure/services/apple-auth.service";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Social auth configuration
|
|
19
|
+
*/
|
|
20
|
+
export interface SocialAuthConfig {
|
|
21
|
+
google?: GoogleAuthConfig;
|
|
22
|
+
apple?: { enabled: boolean };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Social auth result
|
|
27
|
+
*/
|
|
28
|
+
export interface SocialAuthResult {
|
|
29
|
+
success: boolean;
|
|
30
|
+
isNewUser?: boolean;
|
|
31
|
+
error?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Hook result
|
|
36
|
+
*/
|
|
37
|
+
export interface UseSocialAuthResult {
|
|
38
|
+
/** Sign in with Google using ID token (call after OAuth flow) */
|
|
39
|
+
signInWithGoogleToken: (idToken: string) => Promise<SocialAuthResult>;
|
|
40
|
+
/** Sign in with Apple (handles full flow) */
|
|
41
|
+
signInWithApple: () => Promise<SocialAuthResult>;
|
|
42
|
+
/** Whether Google is loading */
|
|
43
|
+
googleLoading: boolean;
|
|
44
|
+
/** Whether Apple is loading */
|
|
45
|
+
appleLoading: boolean;
|
|
46
|
+
/** Whether Google is configured */
|
|
47
|
+
googleConfigured: boolean;
|
|
48
|
+
/** Whether Apple is available */
|
|
49
|
+
appleAvailable: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Hook for social authentication
|
|
54
|
+
*
|
|
55
|
+
* Usage:
|
|
56
|
+
* 1. For Google: Set up expo-auth-session in your app, get idToken, then call signInWithGoogleToken
|
|
57
|
+
* 2. For Apple: Just call signInWithApple (handles complete flow)
|
|
58
|
+
*/
|
|
59
|
+
export function useSocialAuth(config?: SocialAuthConfig): UseSocialAuthResult {
|
|
60
|
+
const [googleLoading, setGoogleLoading] = useState(false);
|
|
61
|
+
const [appleLoading, setAppleLoading] = useState(false);
|
|
62
|
+
const [appleAvailable, setAppleAvailable] = useState(false);
|
|
63
|
+
|
|
64
|
+
// Configure Google Auth
|
|
65
|
+
const googleConfig = config?.google;
|
|
66
|
+
const googleConfigured = !!(
|
|
67
|
+
googleConfig?.webClientId ||
|
|
68
|
+
googleConfig?.iosClientId ||
|
|
69
|
+
googleConfig?.androidClientId
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Configure Google service on mount
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (googleConfig) {
|
|
75
|
+
googleAuthService.configure(googleConfig);
|
|
76
|
+
}
|
|
77
|
+
}, [googleConfig]);
|
|
78
|
+
|
|
79
|
+
// Check Apple availability on mount
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const checkApple = async () => {
|
|
82
|
+
const available = await appleAuthService.isAvailable();
|
|
83
|
+
setAppleAvailable(available && (config?.apple?.enabled ?? false));
|
|
84
|
+
};
|
|
85
|
+
checkApple();
|
|
86
|
+
}, [config?.apple?.enabled]);
|
|
87
|
+
|
|
88
|
+
// Sign in with Google using ID token
|
|
89
|
+
const signInWithGoogleToken = useCallback(
|
|
90
|
+
async (idToken: string): Promise<SocialAuthResult> => {
|
|
91
|
+
if (!googleConfigured) {
|
|
92
|
+
return { success: false, error: "Google Sign-In is not configured" };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setGoogleLoading(true);
|
|
96
|
+
try {
|
|
97
|
+
const auth = getFirebaseAuth();
|
|
98
|
+
if (!auth) {
|
|
99
|
+
return { success: false, error: "Firebase Auth not initialized" };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const signInResult = await googleAuthService.signInWithIdToken(
|
|
103
|
+
auth,
|
|
104
|
+
idToken,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
success: signInResult.success,
|
|
109
|
+
isNewUser: signInResult.isNewUser,
|
|
110
|
+
error: signInResult.error,
|
|
111
|
+
};
|
|
112
|
+
} catch (error) {
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
error: error instanceof Error ? error.message : "Google sign-in failed",
|
|
116
|
+
};
|
|
117
|
+
} finally {
|
|
118
|
+
setGoogleLoading(false);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
[googleConfigured],
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Sign in with Apple
|
|
125
|
+
const signInWithApple = useCallback(async (): Promise<SocialAuthResult> => {
|
|
126
|
+
if (!appleAvailable) {
|
|
127
|
+
return { success: false, error: "Apple Sign-In is not available" };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
setAppleLoading(true);
|
|
131
|
+
try {
|
|
132
|
+
const auth = getFirebaseAuth();
|
|
133
|
+
if (!auth) {
|
|
134
|
+
return { success: false, error: "Firebase Auth not initialized" };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const result = await appleAuthService.signIn(auth);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
success: result.success,
|
|
141
|
+
isNewUser: result.isNewUser,
|
|
142
|
+
error: result.error,
|
|
143
|
+
};
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return {
|
|
146
|
+
success: false,
|
|
147
|
+
error: error instanceof Error ? error.message : "Apple sign-in failed",
|
|
148
|
+
};
|
|
149
|
+
} finally {
|
|
150
|
+
setAppleLoading(false);
|
|
151
|
+
}
|
|
152
|
+
}, [appleAvailable]);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
signInWithGoogleToken,
|
|
156
|
+
signInWithApple,
|
|
157
|
+
googleLoading,
|
|
158
|
+
appleLoading,
|
|
159
|
+
googleConfigured,
|
|
160
|
+
appleAvailable,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for BaseRepository
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
|
6
|
+
import { BaseRepository } from '../infrastructure/repositories/BaseRepository';
|
|
7
|
+
import { getFirestore, resetFirestoreClient } from '../infrastructure/config/FirestoreClient';
|
|
8
|
+
|
|
9
|
+
// Mock Firestore client
|
|
10
|
+
jest.mock('../infrastructure/config/FirestoreClient', () => ({
|
|
11
|
+
getFirestore: jest.fn(),
|
|
12
|
+
resetFirestoreClient: jest.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const mockGetFirestore = getFirestore as jest.MockedFunction<typeof getFirestore>;
|
|
16
|
+
const mockResetFirestoreClient = resetFirestoreClient as jest.MockedFunction<typeof resetFirestoreClient>;
|
|
17
|
+
|
|
18
|
+
describe('BaseRepository', () => {
|
|
19
|
+
let repository: BaseRepository;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
repository = new BaseRepository();
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
repository.destroy();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('getDb', () => {
|
|
31
|
+
it('should return Firestore instance when available', () => {
|
|
32
|
+
const mockFirestore = {} as any;
|
|
33
|
+
mockGetFirestore.mockReturnValue(mockFirestore);
|
|
34
|
+
|
|
35
|
+
const result = repository.getDb();
|
|
36
|
+
expect(result).toBe(mockFirestore);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should return null when Firestore is not available', () => {
|
|
40
|
+
mockGetFirestore.mockReturnValue(null);
|
|
41
|
+
|
|
42
|
+
const result = repository.getDb();
|
|
43
|
+
expect(result).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should return null when repository is destroyed', () => {
|
|
47
|
+
repository.destroy();
|
|
48
|
+
const result = repository.getDb();
|
|
49
|
+
expect(result).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('getDbOrThrow', () => {
|
|
54
|
+
it('should return Firestore instance when available', () => {
|
|
55
|
+
const mockFirestore = {} as any;
|
|
56
|
+
mockGetFirestore.mockReturnValue(mockFirestore);
|
|
57
|
+
|
|
58
|
+
const result = repository.getDbOrThrow();
|
|
59
|
+
expect(result).toBe(mockFirestore);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should throw error when Firestore is not available', () => {
|
|
63
|
+
mockGetFirestore.mockReturnValue(null);
|
|
64
|
+
|
|
65
|
+
expect(() => repository.getDbOrThrow()).toThrow(
|
|
66
|
+
'Firestore is not initialized. Please initialize Firebase App first.'
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('isDbInitialized', () => {
|
|
72
|
+
it('should return true when Firestore is available', () => {
|
|
73
|
+
mockGetFirestore.mockReturnValue({} as any);
|
|
74
|
+
|
|
75
|
+
const result = repository.isDbInitialized();
|
|
76
|
+
expect(result).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should return false when Firestore is not available', () => {
|
|
80
|
+
mockGetFirestore.mockReturnValue(null);
|
|
81
|
+
|
|
82
|
+
const result = repository.isDbInitialized();
|
|
83
|
+
expect(result).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should return false when getFirestore throws', () => {
|
|
87
|
+
mockGetFirestore.mockImplementation(() => {
|
|
88
|
+
throw new Error('Test error');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const result = repository.isDbInitialized();
|
|
92
|
+
expect(result).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('executeWithQuotaHandling', () => {
|
|
97
|
+
it('should execute operation successfully', async () => {
|
|
98
|
+
const mockOperation = jest.fn().mockResolvedValue('success');
|
|
99
|
+
const result = await repository.executeWithQuotaHandling(mockOperation);
|
|
100
|
+
expect(result).toBe('success');
|
|
101
|
+
expect(mockOperation).toHaveBeenCalledTimes(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should handle quota errors', async () => {
|
|
105
|
+
const quotaError = new Error('Quota exceeded');
|
|
106
|
+
const mockOperation = jest.fn().mockRejectedValue(quotaError);
|
|
107
|
+
|
|
108
|
+
// Mock quota error detection
|
|
109
|
+
jest.spyOn(repository, 'isQuotaError').mockReturnValue(true);
|
|
110
|
+
jest.spyOn(repository, 'handleQuotaError').mockImplementation(() => {
|
|
111
|
+
throw new Error('Quota error handled');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await expect(repository.executeWithQuotaHandling(mockOperation)).rejects.toThrow('Quota error handled');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should re-throw non-quota errors', async () => {
|
|
118
|
+
const regularError = new Error('Regular error');
|
|
119
|
+
const mockOperation = jest.fn().mockRejectedValue(regularError);
|
|
120
|
+
|
|
121
|
+
jest.spyOn(repository, 'isQuotaError').mockReturnValue(false);
|
|
122
|
+
|
|
123
|
+
await expect(repository.executeWithQuotaHandling(mockOperation)).rejects.toThrow('Regular error');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('destroy', () => {
|
|
128
|
+
it('should mark repository as destroyed', () => {
|
|
129
|
+
repository.destroy();
|
|
130
|
+
expect(repository.getDb()).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|