@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.
- package/package.json +5 -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/index.ts +8 -0
|
@@ -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
|
+
|