@umituz/react-native-firebase 1.13.2 → 1.13.3

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.
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Account Deletion Service
3
+ * Handles Firebase Auth account deletion operations with automatic reauthentication
4
+ *
5
+ * SOLID: Single Responsibility - Only handles account deletion
6
+ */
7
+
8
+ import { deleteUser, type User } from "firebase/auth";
9
+ import { getFirebaseAuth } from "../config/FirebaseAuthClient";
10
+ import {
11
+ getUserAuthProvider,
12
+ reauthenticateWithApple,
13
+ type AuthProviderType,
14
+ } from "./reauthentication.service";
15
+
16
+ export interface AccountDeletionResult {
17
+ success: boolean;
18
+ error?: {
19
+ code: string;
20
+ message: string;
21
+ requiresReauth: boolean;
22
+ };
23
+ }
24
+
25
+ export interface AccountDeletionOptions {
26
+ /**
27
+ * Google ID token for reauthentication (required if user signed in with Google)
28
+ * This must be provided by the calling code after prompting user for Google sign-in
29
+ */
30
+ googleIdToken?: string;
31
+ /**
32
+ * If true, will attempt to reauthenticate with Apple automatically
33
+ * (shows Apple sign-in prompt to user)
34
+ */
35
+ autoReauthenticate?: boolean;
36
+ }
37
+
38
+ /**
39
+ * Delete the current user's Firebase Auth account
40
+ * Now with automatic reauthentication support for Apple Sign-In
41
+ * Note: This is irreversible and also signs out the user
42
+ */
43
+ export async function deleteCurrentUser(
44
+ options: AccountDeletionOptions = { autoReauthenticate: true }
45
+ ): Promise<AccountDeletionResult> {
46
+ const auth = getFirebaseAuth();
47
+
48
+ if (!auth) {
49
+ return {
50
+ success: false,
51
+ error: {
52
+ code: "auth/not-initialized",
53
+ message: "Firebase Auth is not initialized",
54
+ requiresReauth: false,
55
+ },
56
+ };
57
+ }
58
+
59
+ const user = auth.currentUser;
60
+
61
+ if (!user) {
62
+ return {
63
+ success: false,
64
+ error: {
65
+ code: "auth/no-user",
66
+ message: "No user is currently signed in",
67
+ requiresReauth: false,
68
+ },
69
+ };
70
+ }
71
+
72
+ if (user.isAnonymous) {
73
+ return {
74
+ success: false,
75
+ error: {
76
+ code: "auth/anonymous-user",
77
+ message: "Cannot delete anonymous account",
78
+ requiresReauth: false,
79
+ },
80
+ };
81
+ }
82
+
83
+ // First attempt to delete
84
+ try {
85
+ await deleteUser(user);
86
+ return { success: true };
87
+ } catch (error) {
88
+ const firebaseError = error as { code?: string; message?: string };
89
+ const requiresReauth = firebaseError.code === "auth/requires-recent-login";
90
+
91
+ // If reauthentication is required and autoReauthenticate is enabled
92
+ if (requiresReauth && options.autoReauthenticate) {
93
+ const reauthResult = await attemptReauthenticationAndDelete(user, options);
94
+ if (reauthResult) {
95
+ return reauthResult;
96
+ }
97
+ }
98
+
99
+ return {
100
+ success: false,
101
+ error: {
102
+ code: firebaseError.code || "auth/unknown",
103
+ message: requiresReauth
104
+ ? "Please sign in again before deleting your account"
105
+ : firebaseError.message || "Failed to delete account",
106
+ requiresReauth,
107
+ },
108
+ };
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Attempt to reauthenticate based on provider and then delete the account
114
+ */
115
+ async function attemptReauthenticationAndDelete(
116
+ user: User,
117
+ options: AccountDeletionOptions
118
+ ): Promise<AccountDeletionResult | null> {
119
+ const provider = getUserAuthProvider(user);
120
+
121
+ // Handle Apple reauthentication
122
+ if (provider === "apple.com") {
123
+ const reauthResult = await reauthenticateWithApple(user);
124
+
125
+ if (reauthResult.success) {
126
+ // Retry deletion after successful reauthentication
127
+ try {
128
+ await deleteUser(user);
129
+ return { success: true };
130
+ } catch (deleteError) {
131
+ const firebaseError = deleteError as { code?: string; message?: string };
132
+ return {
133
+ success: false,
134
+ error: {
135
+ code: firebaseError.code || "auth/deletion-failed-after-reauth",
136
+ message: firebaseError.message || "Failed to delete account after reauthentication",
137
+ requiresReauth: false,
138
+ },
139
+ };
140
+ }
141
+ } else {
142
+ // Reauthentication failed
143
+ return {
144
+ success: false,
145
+ error: {
146
+ code: reauthResult.error?.code || "auth/reauthentication-failed",
147
+ message: reauthResult.error?.message || "Reauthentication failed",
148
+ requiresReauth: true,
149
+ },
150
+ };
151
+ }
152
+ }
153
+
154
+ // Handle Google reauthentication (requires ID token from caller)
155
+ if (provider === "google.com") {
156
+ // For Google, we need the caller to provide the ID token
157
+ // This is because we need to trigger Google Sign-In UI which must be done at the presentation layer
158
+ if (!options.googleIdToken) {
159
+ return {
160
+ success: false,
161
+ error: {
162
+ code: "auth/google-reauth-required",
163
+ message: "Please sign in with Google again to delete your account",
164
+ requiresReauth: true,
165
+ },
166
+ };
167
+ }
168
+
169
+ // If we have a Google ID token, reauthenticate with it
170
+ const { reauthenticateWithGoogle } = await import("./reauthentication.service");
171
+ const reauthResult = await reauthenticateWithGoogle(user, options.googleIdToken);
172
+
173
+ if (reauthResult.success) {
174
+ try {
175
+ await deleteUser(user);
176
+ return { success: true };
177
+ } catch (deleteError) {
178
+ const firebaseError = deleteError as { code?: string; message?: string };
179
+ return {
180
+ success: false,
181
+ error: {
182
+ code: firebaseError.code || "auth/deletion-failed-after-reauth",
183
+ message: firebaseError.message || "Failed to delete account after reauthentication",
184
+ requiresReauth: false,
185
+ },
186
+ };
187
+ }
188
+ } else {
189
+ return {
190
+ success: false,
191
+ error: {
192
+ code: reauthResult.error?.code || "auth/reauthentication-failed",
193
+ message: reauthResult.error?.message || "Google reauthentication failed",
194
+ requiresReauth: true,
195
+ },
196
+ };
197
+ }
198
+ }
199
+
200
+ // For other providers, return null to use the default error handling
201
+ return null;
202
+ }
203
+
204
+ /**
205
+ * Delete a specific user (must be the current user)
206
+ */
207
+ export async function deleteUserAccount(
208
+ user: User
209
+ ): Promise<AccountDeletionResult> {
210
+ if (!user) {
211
+ return {
212
+ success: false,
213
+ error: {
214
+ code: "auth/no-user",
215
+ message: "No user provided",
216
+ requiresReauth: false,
217
+ },
218
+ };
219
+ }
220
+
221
+ if (user.isAnonymous) {
222
+ return {
223
+ success: false,
224
+ error: {
225
+ code: "auth/anonymous-user",
226
+ message: "Cannot delete anonymous account",
227
+ requiresReauth: false,
228
+ },
229
+ };
230
+ }
231
+
232
+ try {
233
+ await deleteUser(user);
234
+ return { success: true };
235
+ } catch (error) {
236
+ const firebaseError = error as { code?: string; message?: string };
237
+ const requiresReauth = firebaseError.code === "auth/requires-recent-login";
238
+
239
+ return {
240
+ success: false,
241
+ error: {
242
+ code: firebaseError.code || "auth/unknown",
243
+ message: requiresReauth
244
+ ? "Please sign in again before deleting your account"
245
+ : firebaseError.message || "Failed to delete account",
246
+ requiresReauth,
247
+ },
248
+ };
249
+ }
250
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Anonymous Auth Service
3
+ * Service for managing anonymous/guest authentication
4
+ */
5
+
6
+ import { signInAnonymously, type Auth, type User } from "firebase/auth";
7
+ import { toAnonymousUser, type AnonymousUser } from "../../domain/entities/AnonymousUser";
8
+ import { checkAuthState } from "./auth-utils.service";
9
+
10
+ declare const __DEV__: boolean;
11
+
12
+ export interface AnonymousAuthResult {
13
+ readonly user: User;
14
+ readonly anonymousUser: AnonymousUser;
15
+ readonly wasAlreadySignedIn: boolean;
16
+ }
17
+
18
+ export interface AnonymousAuthServiceInterface {
19
+ signInAnonymously(auth: Auth): Promise<AnonymousAuthResult>;
20
+ getCurrentAnonymousUser(auth: Auth | null): User | null;
21
+ isCurrentUserAnonymous(auth: Auth | null): boolean;
22
+ }
23
+
24
+ /**
25
+ * Anonymous Auth Service
26
+ * Handles anonymous authentication operations
27
+ */
28
+ export class AnonymousAuthService implements AnonymousAuthServiceInterface {
29
+ /**
30
+ * Sign in anonymously
31
+ * IMPORTANT: Only signs in if NO user exists (preserves email/password sessions)
32
+ */
33
+ async signInAnonymously(auth: Auth): Promise<AnonymousAuthResult> {
34
+ if (!auth) {
35
+ throw new Error("Firebase Auth instance is required");
36
+ }
37
+
38
+ const currentUser = auth.currentUser;
39
+
40
+ // If user is already signed in with email/password, preserve that session
41
+ if (currentUser && !currentUser.isAnonymous) {
42
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
43
+ // eslint-disable-next-line no-console
44
+ console.log("[AnonymousAuthService] User already signed in with email/password, skipping anonymous auth");
45
+ }
46
+ // Return a "fake" anonymous result to maintain API compatibility
47
+ // The actual user is NOT anonymous
48
+ return {
49
+ user: currentUser,
50
+ anonymousUser: toAnonymousUser(currentUser),
51
+ wasAlreadySignedIn: true,
52
+ };
53
+ }
54
+
55
+ // If already signed in anonymously, return existing user
56
+ if (currentUser && currentUser.isAnonymous) {
57
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
58
+ // eslint-disable-next-line no-console
59
+ console.log("[AnonymousAuthService] User already signed in anonymously");
60
+ }
61
+ return {
62
+ user: currentUser,
63
+ anonymousUser: toAnonymousUser(currentUser),
64
+ wasAlreadySignedIn: true,
65
+ };
66
+ }
67
+
68
+ // No user exists, sign in anonymously
69
+ try {
70
+ const userCredential = await signInAnonymously(auth);
71
+ const anonymousUser = toAnonymousUser(userCredential.user);
72
+
73
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
74
+ // eslint-disable-next-line no-console
75
+ console.log("[AnonymousAuthService] Successfully signed in anonymously", { uid: anonymousUser.uid });
76
+ }
77
+
78
+ return {
79
+ user: userCredential.user,
80
+ anonymousUser,
81
+ wasAlreadySignedIn: false,
82
+ };
83
+ } catch (error) {
84
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
85
+ // eslint-disable-next-line no-console
86
+ console.error("[AnonymousAuthService] Failed to sign in anonymously", error);
87
+ }
88
+ throw error;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Get current anonymous user
94
+ */
95
+ getCurrentAnonymousUser(auth: Auth | null): User | null {
96
+ if (!auth) {
97
+ return null;
98
+ }
99
+
100
+ try {
101
+ const state = checkAuthState(auth);
102
+ if (state.isAnonymous && state.currentUser) {
103
+ return state.currentUser;
104
+ }
105
+ return null;
106
+ } catch (error) {
107
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
108
+ // eslint-disable-next-line no-console
109
+ console.error("[AnonymousAuthService] Error getting current anonymous user", error);
110
+ }
111
+ return null;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Check if current user is anonymous
117
+ */
118
+ isCurrentUserAnonymous(auth: Auth | null): boolean {
119
+ if (!auth) {
120
+ return false;
121
+ }
122
+
123
+ try {
124
+ return checkAuthState(auth).isAnonymous;
125
+ } catch (error) {
126
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
127
+ // eslint-disable-next-line no-console
128
+ console.error("[AnonymousAuthService] Error checking if user is anonymous", error);
129
+ }
130
+ return false;
131
+ }
132
+ }
133
+ }
134
+
135
+ export const anonymousAuthService = new AnonymousAuthService();
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Apple Auth Service
3
+ * Handles Apple Sign-In with Firebase Authentication
4
+ * Uses expo-apple-authentication for native Apple Sign-In
5
+ */
6
+
7
+ import {
8
+ OAuthProvider,
9
+ signInWithCredential,
10
+ type Auth,
11
+ type UserCredential,
12
+ } from "firebase/auth";
13
+ import * as AppleAuthentication from "expo-apple-authentication";
14
+ import * as Crypto from "expo-crypto";
15
+ import { Platform } from "react-native";
16
+
17
+ /**
18
+ * Apple Auth result
19
+ */
20
+ export interface AppleAuthResult {
21
+ success: boolean;
22
+ userCredential?: UserCredential;
23
+ error?: string;
24
+ isNewUser?: boolean;
25
+ }
26
+
27
+ /**
28
+ * Apple Auth Service
29
+ * Provides Apple Sign-In functionality for Firebase (iOS only)
30
+ */
31
+ export class AppleAuthService {
32
+ /**
33
+ * Check if Apple Sign-In is available
34
+ * Only available on iOS 13+
35
+ */
36
+ async isAvailable(): Promise<boolean> {
37
+ if (Platform.OS !== "ios") {
38
+ return false;
39
+ }
40
+
41
+ try {
42
+ return await AppleAuthentication.isAvailableAsync();
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Sign in with Apple
50
+ * Handles the complete Apple Sign-In flow
51
+ */
52
+ async signIn(auth: Auth): Promise<AppleAuthResult> {
53
+ try {
54
+ // Check availability
55
+ const isAvailable = await this.isAvailable();
56
+ if (!isAvailable) {
57
+ return {
58
+ success: false,
59
+ error: "Apple Sign-In is not available on this device",
60
+ };
61
+ }
62
+
63
+ // Generate nonce for security
64
+ const nonce = await this.generateNonce();
65
+ const hashedNonce = await Crypto.digestStringAsync(
66
+ Crypto.CryptoDigestAlgorithm.SHA256,
67
+ nonce,
68
+ );
69
+
70
+ // Request Apple Sign-In
71
+ const appleCredential = await AppleAuthentication.signInAsync({
72
+ requestedScopes: [
73
+ AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
74
+ AppleAuthentication.AppleAuthenticationScope.EMAIL,
75
+ ],
76
+ nonce: hashedNonce,
77
+ });
78
+
79
+ // Check for identity token
80
+ if (!appleCredential.identityToken) {
81
+ return {
82
+ success: false,
83
+ error: "No identity token received from Apple",
84
+ };
85
+ }
86
+
87
+ // Create Firebase credential
88
+ const provider = new OAuthProvider("apple.com");
89
+ const credential = provider.credential({
90
+ idToken: appleCredential.identityToken,
91
+ rawNonce: nonce,
92
+ });
93
+
94
+ // Sign in to Firebase
95
+ const userCredential = await signInWithCredential(auth, credential);
96
+
97
+ // Check if this is a new user
98
+ const isNewUser =
99
+ userCredential.user.metadata.creationTime ===
100
+ userCredential.user.metadata.lastSignInTime;
101
+
102
+ return {
103
+ success: true,
104
+ userCredential,
105
+ isNewUser,
106
+ };
107
+ } catch (error) {
108
+ // Handle user cancellation
109
+ if (
110
+ error instanceof Error &&
111
+ error.message.includes("ERR_CANCELED")
112
+ ) {
113
+ return {
114
+ success: false,
115
+ error: "Apple Sign-In was cancelled",
116
+ };
117
+ }
118
+
119
+ return {
120
+ success: false,
121
+ error: error instanceof Error ? error.message : "Apple sign-in failed",
122
+ };
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Generate a random nonce for Apple Sign-In security
128
+ */
129
+ private async generateNonce(length: number = 32): Promise<string> {
130
+ const randomBytes = await Crypto.getRandomBytesAsync(length);
131
+ const chars =
132
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
133
+ let result = "";
134
+
135
+ for (let i = 0; i < randomBytes.length; i++) {
136
+ result += chars.charAt(randomBytes[i] % chars.length);
137
+ }
138
+
139
+ return result;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Singleton instance
145
+ */
146
+ export const appleAuthService = new AppleAuthService();
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Auth Guard Service
3
+ * Single Responsibility: Validate authenticated user access
4
+ *
5
+ * SOLID: Single Responsibility - Only handles auth validation
6
+ * Generic implementation for all React Native apps
7
+ */
8
+
9
+ import { getFirebaseAuth } from '../config/FirebaseAuthClient';
10
+ import {
11
+ getCurrentUserId,
12
+ getCurrentUser,
13
+ } from './auth-utils.service';
14
+
15
+ /**
16
+ * Auth Guard Service
17
+ * Provides authentication validation for protected operations
18
+ */
19
+ export class AuthGuardService {
20
+ /**
21
+ * Check if user is authenticated (not guest)
22
+ * @throws Error if user is not authenticated
23
+ */
24
+ async requireAuthenticatedUser(): Promise<string> {
25
+ const auth = getFirebaseAuth();
26
+ if (!auth) {
27
+ throw new Error('Firebase Auth is not initialized');
28
+ }
29
+
30
+ const userId = getCurrentUserId(auth);
31
+ if (!userId) {
32
+ throw new Error('User must be authenticated to perform this action');
33
+ }
34
+
35
+ const currentUser = getCurrentUser(auth);
36
+ if (!currentUser) {
37
+ throw new Error('User must be authenticated to perform this action');
38
+ }
39
+
40
+ // Check if user is anonymous (guest)
41
+ if (currentUser.isAnonymous) {
42
+ throw new Error('Guest users cannot perform this action');
43
+ }
44
+
45
+ return userId;
46
+ }
47
+
48
+ /**
49
+ * Check if user is authenticated (not guest)
50
+ * Returns null if not authenticated instead of throwing
51
+ */
52
+ async getAuthenticatedUserId(): Promise<string | null> {
53
+ try {
54
+ return await this.requireAuthenticatedUser();
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Check if current user is authenticated (not guest)
62
+ */
63
+ isAuthenticated(): boolean {
64
+ const auth = getFirebaseAuth();
65
+ if (!auth) {
66
+ return false;
67
+ }
68
+
69
+ const currentUser = getCurrentUser(auth);
70
+ if (!currentUser) {
71
+ return false;
72
+ }
73
+
74
+ // User must not be anonymous
75
+ return !currentUser.isAnonymous;
76
+ }
77
+
78
+ /**
79
+ * Check if current user is guest (anonymous)
80
+ */
81
+ isGuest(): boolean {
82
+ const auth = getFirebaseAuth();
83
+ if (!auth) {
84
+ return false;
85
+ }
86
+
87
+ const currentUser = getCurrentUser(auth);
88
+ if (!currentUser) {
89
+ return false;
90
+ }
91
+
92
+ return currentUser.isAnonymous;
93
+ }
94
+ }
95
+
96
+ export const authGuardService = new AuthGuardService();
97
+