@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,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Utils Service
|
|
3
|
+
* Single Responsibility: Provide utility functions for auth state checking
|
|
4
|
+
*
|
|
5
|
+
* SOLID: Single Responsibility - Only handles auth state utilities
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Auth, User } from 'firebase/auth';
|
|
9
|
+
import { getFirebaseAuth } from '../config/FirebaseAuthClient';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check if user is anonymous
|
|
13
|
+
*/
|
|
14
|
+
function isAnonymousUser(user: User): boolean {
|
|
15
|
+
return user.isAnonymous === true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Auth check result interface
|
|
20
|
+
*/
|
|
21
|
+
export interface AuthCheckResult {
|
|
22
|
+
isAuthenticated: boolean;
|
|
23
|
+
isAnonymous: boolean;
|
|
24
|
+
isGuest: boolean;
|
|
25
|
+
currentUser: User | null;
|
|
26
|
+
userId: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check authentication state
|
|
31
|
+
* Returns comprehensive auth state information
|
|
32
|
+
*/
|
|
33
|
+
export function checkAuthState(auth: Auth | null): AuthCheckResult {
|
|
34
|
+
if (!auth) {
|
|
35
|
+
return {
|
|
36
|
+
isAuthenticated: false,
|
|
37
|
+
isAnonymous: false,
|
|
38
|
+
isGuest: false,
|
|
39
|
+
currentUser: null,
|
|
40
|
+
userId: null,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const currentUser = auth.currentUser;
|
|
45
|
+
|
|
46
|
+
if (!currentUser) {
|
|
47
|
+
return {
|
|
48
|
+
isAuthenticated: false,
|
|
49
|
+
isAnonymous: false,
|
|
50
|
+
isGuest: false,
|
|
51
|
+
currentUser: null,
|
|
52
|
+
userId: null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const anonymous = isAnonymousUser(currentUser);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
isAuthenticated: true,
|
|
60
|
+
isAnonymous: anonymous,
|
|
61
|
+
isGuest: anonymous,
|
|
62
|
+
currentUser,
|
|
63
|
+
userId: currentUser.uid,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if user is authenticated (including anonymous)
|
|
69
|
+
*/
|
|
70
|
+
export function isAuthenticated(auth: Auth | null): boolean {
|
|
71
|
+
return checkAuthState(auth).isAuthenticated;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if user is guest (anonymous)
|
|
76
|
+
*/
|
|
77
|
+
export function isGuest(auth: Auth | null): boolean {
|
|
78
|
+
return checkAuthState(auth).isGuest;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get current user ID (null if not authenticated)
|
|
83
|
+
*/
|
|
84
|
+
export function getCurrentUserId(auth: Auth | null): string | null {
|
|
85
|
+
return checkAuthState(auth).userId;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get current user (null if not authenticated)
|
|
90
|
+
*/
|
|
91
|
+
export function getCurrentUser(auth: Auth | null): User | null {
|
|
92
|
+
return checkAuthState(auth).currentUser;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get current authenticated user ID from global auth instance
|
|
97
|
+
* Convenience function that uses getFirebaseAuth()
|
|
98
|
+
*/
|
|
99
|
+
export function getCurrentUserIdFromGlobal(): string | null {
|
|
100
|
+
const auth = getFirebaseAuth();
|
|
101
|
+
return getCurrentUserId(auth);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get current user from global auth instance
|
|
106
|
+
* Convenience function that uses getFirebaseAuth()
|
|
107
|
+
*/
|
|
108
|
+
export function getCurrentUserFromGlobal(): User | null {
|
|
109
|
+
const auth = getFirebaseAuth();
|
|
110
|
+
return getCurrentUser(auth);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if current user is authenticated (including anonymous)
|
|
115
|
+
* Convenience function that uses getFirebaseAuth()
|
|
116
|
+
*/
|
|
117
|
+
export function isCurrentUserAuthenticated(): boolean {
|
|
118
|
+
const auth = getFirebaseAuth();
|
|
119
|
+
return isAuthenticated(auth);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if current user is guest (anonymous)
|
|
124
|
+
* Convenience function that uses getFirebaseAuth()
|
|
125
|
+
*/
|
|
126
|
+
export function isCurrentUserGuest(): boolean {
|
|
127
|
+
const auth = getFirebaseAuth();
|
|
128
|
+
return isGuest(auth);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Verify userId matches current authenticated user
|
|
133
|
+
*/
|
|
134
|
+
export function verifyUserId(auth: Auth | null, userId: string): boolean {
|
|
135
|
+
if (!auth || !userId) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const state = checkAuthState(auth);
|
|
141
|
+
return state.isAuthenticated && state.userId === userId;
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get safe user ID (null if not authenticated)
|
|
149
|
+
*/
|
|
150
|
+
export function getSafeUserId(auth: Auth | null): string | null {
|
|
151
|
+
if (!auth) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
return getCurrentUserId(auth);
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Check if user exists and is valid
|
|
164
|
+
*/
|
|
165
|
+
export function isValidUser(user: User | null | undefined): user is User {
|
|
166
|
+
return user !== null && user !== undefined && typeof user.uid === 'string' && user.uid.length > 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firestore Utilities
|
|
3
|
+
* Centralized utilities for Firestore operations with anonymous user support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Auth } from "firebase/auth";
|
|
7
|
+
import { checkAuthState, verifyUserId } from "./auth-utils.service";
|
|
8
|
+
|
|
9
|
+
declare const __DEV__: boolean;
|
|
10
|
+
|
|
11
|
+
export interface FirestoreQueryOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Skip query if user is anonymous/guest
|
|
14
|
+
* Default: true (guest users don't have Firestore data)
|
|
15
|
+
*/
|
|
16
|
+
readonly skipForGuest?: boolean;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Skip query if user is not authenticated
|
|
20
|
+
* Default: true
|
|
21
|
+
*/
|
|
22
|
+
readonly skipIfNotAuthenticated?: boolean;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Verify userId matches current user
|
|
26
|
+
* Default: true
|
|
27
|
+
*/
|
|
28
|
+
readonly verifyUserId?: boolean;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* User ID to verify (required if verifyUserId is true)
|
|
32
|
+
*/
|
|
33
|
+
readonly userId?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type FirestoreQuerySkipReason =
|
|
37
|
+
| "not_authenticated"
|
|
38
|
+
| "is_guest"
|
|
39
|
+
| "user_id_mismatch"
|
|
40
|
+
| "no_auth"
|
|
41
|
+
| "invalid_options";
|
|
42
|
+
|
|
43
|
+
export interface FirestoreQueryResult {
|
|
44
|
+
readonly shouldSkip: boolean;
|
|
45
|
+
readonly reason?: FirestoreQuerySkipReason;
|
|
46
|
+
readonly userId: string | null;
|
|
47
|
+
readonly isGuest: boolean;
|
|
48
|
+
readonly isAuthenticated: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if Firestore query should be skipped
|
|
53
|
+
* Centralized logic for all Firestore operations
|
|
54
|
+
*/
|
|
55
|
+
export function shouldSkipFirestoreQuery(
|
|
56
|
+
auth: Auth | null,
|
|
57
|
+
options: FirestoreQueryOptions = {},
|
|
58
|
+
): FirestoreQueryResult {
|
|
59
|
+
const {
|
|
60
|
+
skipForGuest = true,
|
|
61
|
+
skipIfNotAuthenticated = true,
|
|
62
|
+
verifyUserId: shouldVerify = true,
|
|
63
|
+
userId,
|
|
64
|
+
} = options;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// No auth available
|
|
68
|
+
if (!auth) {
|
|
69
|
+
return {
|
|
70
|
+
shouldSkip: true,
|
|
71
|
+
reason: "no_auth",
|
|
72
|
+
userId: null,
|
|
73
|
+
isGuest: false,
|
|
74
|
+
isAuthenticated: false,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check auth state
|
|
79
|
+
const authState = checkAuthState(auth);
|
|
80
|
+
|
|
81
|
+
// Not authenticated
|
|
82
|
+
if (!authState.isAuthenticated) {
|
|
83
|
+
if (skipIfNotAuthenticated) {
|
|
84
|
+
return {
|
|
85
|
+
shouldSkip: true,
|
|
86
|
+
reason: "not_authenticated",
|
|
87
|
+
userId: null,
|
|
88
|
+
isGuest: false,
|
|
89
|
+
isAuthenticated: false,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Guest/anonymous user
|
|
95
|
+
if (authState.isGuest) {
|
|
96
|
+
if (skipForGuest) {
|
|
97
|
+
return {
|
|
98
|
+
shouldSkip: true,
|
|
99
|
+
reason: "is_guest",
|
|
100
|
+
userId: authState.userId,
|
|
101
|
+
isGuest: true,
|
|
102
|
+
isAuthenticated: false,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Verify userId if provided
|
|
108
|
+
if (shouldVerify && userId) {
|
|
109
|
+
if (!verifyUserId(auth, userId)) {
|
|
110
|
+
return {
|
|
111
|
+
shouldSkip: true,
|
|
112
|
+
reason: "user_id_mismatch",
|
|
113
|
+
userId: authState.userId,
|
|
114
|
+
isGuest: authState.isGuest,
|
|
115
|
+
isAuthenticated: authState.isAuthenticated,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Don't skip
|
|
121
|
+
return {
|
|
122
|
+
shouldSkip: false,
|
|
123
|
+
userId: authState.userId,
|
|
124
|
+
isGuest: authState.isGuest,
|
|
125
|
+
isAuthenticated: authState.isAuthenticated,
|
|
126
|
+
};
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
129
|
+
// eslint-disable-next-line no-console
|
|
130
|
+
console.error("[FirestoreUtils] Error checking if query should be skipped", error);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
shouldSkip: true,
|
|
135
|
+
reason: "invalid_options",
|
|
136
|
+
userId: null,
|
|
137
|
+
isGuest: false,
|
|
138
|
+
isAuthenticated: false,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create safe query options with defaults
|
|
145
|
+
*/
|
|
146
|
+
export function createFirestoreQueryOptions(
|
|
147
|
+
options: Partial<FirestoreQueryOptions> = {},
|
|
148
|
+
): FirestoreQueryOptions {
|
|
149
|
+
return {
|
|
150
|
+
skipForGuest: options.skipForGuest ?? true,
|
|
151
|
+
skipIfNotAuthenticated: options.skipIfNotAuthenticated ?? true,
|
|
152
|
+
verifyUserId: options.verifyUserId ?? true,
|
|
153
|
+
userId: options.userId,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Auth Service
|
|
3
|
+
* Handles Google Sign-In with Firebase Authentication
|
|
4
|
+
* Uses expo-auth-session for OAuth flow
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
GoogleAuthProvider,
|
|
9
|
+
signInWithCredential,
|
|
10
|
+
type Auth,
|
|
11
|
+
type UserCredential,
|
|
12
|
+
} from "firebase/auth";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Google Auth configuration
|
|
16
|
+
*/
|
|
17
|
+
export interface GoogleAuthConfig {
|
|
18
|
+
webClientId?: string;
|
|
19
|
+
iosClientId?: string;
|
|
20
|
+
androidClientId?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Google Auth result
|
|
25
|
+
*/
|
|
26
|
+
export interface GoogleAuthResult {
|
|
27
|
+
success: boolean;
|
|
28
|
+
userCredential?: UserCredential;
|
|
29
|
+
error?: string;
|
|
30
|
+
isNewUser?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Google Auth Service
|
|
35
|
+
* Provides Google Sign-In functionality for Firebase
|
|
36
|
+
*/
|
|
37
|
+
export class GoogleAuthService {
|
|
38
|
+
private config: GoogleAuthConfig | null = null;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Configure Google Auth with client IDs
|
|
42
|
+
*/
|
|
43
|
+
configure(config: GoogleAuthConfig): void {
|
|
44
|
+
this.config = config;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if Google Auth is configured
|
|
49
|
+
*/
|
|
50
|
+
isConfigured(): boolean {
|
|
51
|
+
return (
|
|
52
|
+
this.config !== null &&
|
|
53
|
+
(!!this.config.webClientId ||
|
|
54
|
+
!!this.config.iosClientId ||
|
|
55
|
+
!!this.config.androidClientId)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the current configuration
|
|
61
|
+
*/
|
|
62
|
+
getConfig(): GoogleAuthConfig | null {
|
|
63
|
+
return this.config;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Sign in with Google ID token
|
|
68
|
+
* Called after successful Google OAuth flow
|
|
69
|
+
*/
|
|
70
|
+
async signInWithIdToken(
|
|
71
|
+
auth: Auth,
|
|
72
|
+
idToken: string,
|
|
73
|
+
): Promise<GoogleAuthResult> {
|
|
74
|
+
try {
|
|
75
|
+
const credential = GoogleAuthProvider.credential(idToken);
|
|
76
|
+
const userCredential = await signInWithCredential(auth, credential);
|
|
77
|
+
|
|
78
|
+
// Check if this is a new user
|
|
79
|
+
const isNewUser =
|
|
80
|
+
userCredential.user.metadata.creationTime ===
|
|
81
|
+
userCredential.user.metadata.lastSignInTime;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
success: true,
|
|
85
|
+
userCredential,
|
|
86
|
+
isNewUser,
|
|
87
|
+
};
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return {
|
|
90
|
+
success: false,
|
|
91
|
+
error: error instanceof Error ? error.message : "Google sign-in failed",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Singleton instance
|
|
99
|
+
*/
|
|
100
|
+
export const googleAuthService = new GoogleAuthService();
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reauthentication Service
|
|
3
|
+
* Handles Firebase Auth reauthentication for sensitive operations
|
|
4
|
+
*
|
|
5
|
+
* SOLID: Single Responsibility - Only handles reauthentication
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
reauthenticateWithCredential,
|
|
10
|
+
GoogleAuthProvider,
|
|
11
|
+
OAuthProvider,
|
|
12
|
+
type User,
|
|
13
|
+
type AuthCredential,
|
|
14
|
+
} from "firebase/auth";
|
|
15
|
+
import * as AppleAuthentication from "expo-apple-authentication";
|
|
16
|
+
import * as Crypto from "expo-crypto";
|
|
17
|
+
import { Platform } from "react-native";
|
|
18
|
+
|
|
19
|
+
export interface ReauthenticationResult {
|
|
20
|
+
success: boolean;
|
|
21
|
+
error?: {
|
|
22
|
+
code: string;
|
|
23
|
+
message: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type AuthProviderType = "google.com" | "apple.com" | "password" | "anonymous" | "unknown";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the primary auth provider for a user
|
|
31
|
+
*/
|
|
32
|
+
export function getUserAuthProvider(user: User): AuthProviderType {
|
|
33
|
+
if (user.isAnonymous) {
|
|
34
|
+
return "anonymous";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const providerData = user.providerData;
|
|
38
|
+
if (!providerData || providerData.length === 0) {
|
|
39
|
+
return "unknown";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check for Google
|
|
43
|
+
const googleProvider = providerData.find((p) => p.providerId === "google.com");
|
|
44
|
+
if (googleProvider) {
|
|
45
|
+
return "google.com";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check for Apple
|
|
49
|
+
const appleProvider = providerData.find((p) => p.providerId === "apple.com");
|
|
50
|
+
if (appleProvider) {
|
|
51
|
+
return "apple.com";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check for password
|
|
55
|
+
const passwordProvider = providerData.find((p) => p.providerId === "password");
|
|
56
|
+
if (passwordProvider) {
|
|
57
|
+
return "password";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return "unknown";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Reauthenticate with Google
|
|
65
|
+
*/
|
|
66
|
+
export async function reauthenticateWithGoogle(
|
|
67
|
+
user: User,
|
|
68
|
+
idToken: string
|
|
69
|
+
): Promise<ReauthenticationResult> {
|
|
70
|
+
try {
|
|
71
|
+
const credential = GoogleAuthProvider.credential(idToken);
|
|
72
|
+
await reauthenticateWithCredential(user, credential);
|
|
73
|
+
return { success: true };
|
|
74
|
+
} catch (error) {
|
|
75
|
+
const firebaseError = error as { code?: string; message?: string };
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
error: {
|
|
79
|
+
code: firebaseError.code || "auth/reauthentication-failed",
|
|
80
|
+
message: firebaseError.message || "Google reauthentication failed",
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Generate a random nonce for Apple Sign-In security
|
|
88
|
+
*/
|
|
89
|
+
async function generateNonce(length: number = 32): Promise<string> {
|
|
90
|
+
const randomBytes = await Crypto.getRandomBytesAsync(length);
|
|
91
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
92
|
+
let result = "";
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < randomBytes.length; i++) {
|
|
95
|
+
result += chars.charAt(randomBytes[i] % chars.length);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Reauthenticate with Apple
|
|
103
|
+
* Returns the credential that can be used for reauthentication
|
|
104
|
+
*/
|
|
105
|
+
export async function getAppleReauthCredential(): Promise<{
|
|
106
|
+
success: boolean;
|
|
107
|
+
credential?: AuthCredential;
|
|
108
|
+
error?: { code: string; message: string };
|
|
109
|
+
}> {
|
|
110
|
+
if (Platform.OS !== "ios") {
|
|
111
|
+
return {
|
|
112
|
+
success: false,
|
|
113
|
+
error: {
|
|
114
|
+
code: "auth/platform-not-supported",
|
|
115
|
+
message: "Apple Sign-In is only available on iOS",
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const isAvailable = await AppleAuthentication.isAvailableAsync();
|
|
122
|
+
if (!isAvailable) {
|
|
123
|
+
return {
|
|
124
|
+
success: false,
|
|
125
|
+
error: {
|
|
126
|
+
code: "auth/apple-signin-unavailable",
|
|
127
|
+
message: "Apple Sign-In is not available on this device",
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Generate nonce
|
|
133
|
+
const nonce = await generateNonce();
|
|
134
|
+
const hashedNonce = await Crypto.digestStringAsync(
|
|
135
|
+
Crypto.CryptoDigestAlgorithm.SHA256,
|
|
136
|
+
nonce
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Request Apple Sign-In
|
|
140
|
+
const appleCredential = await AppleAuthentication.signInAsync({
|
|
141
|
+
requestedScopes: [
|
|
142
|
+
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
|
|
143
|
+
AppleAuthentication.AppleAuthenticationScope.EMAIL,
|
|
144
|
+
],
|
|
145
|
+
nonce: hashedNonce,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (!appleCredential.identityToken) {
|
|
149
|
+
return {
|
|
150
|
+
success: false,
|
|
151
|
+
error: {
|
|
152
|
+
code: "auth/no-identity-token",
|
|
153
|
+
message: "No identity token received from Apple",
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Create Firebase credential
|
|
159
|
+
const provider = new OAuthProvider("apple.com");
|
|
160
|
+
const credential = provider.credential({
|
|
161
|
+
idToken: appleCredential.identityToken,
|
|
162
|
+
rawNonce: nonce,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return { success: true, credential };
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error instanceof Error && error.message.includes("ERR_CANCELED")) {
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
error: {
|
|
171
|
+
code: "auth/cancelled",
|
|
172
|
+
message: "Apple Sign-In was cancelled",
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
success: false,
|
|
179
|
+
error: {
|
|
180
|
+
code: "auth/apple-reauthentication-failed",
|
|
181
|
+
message: error instanceof Error ? error.message : "Apple reauthentication failed",
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Reauthenticate with Apple
|
|
189
|
+
*/
|
|
190
|
+
export async function reauthenticateWithApple(user: User): Promise<ReauthenticationResult> {
|
|
191
|
+
const result = await getAppleReauthCredential();
|
|
192
|
+
|
|
193
|
+
if (!result.success || !result.credential) {
|
|
194
|
+
return {
|
|
195
|
+
success: false,
|
|
196
|
+
error: result.error || {
|
|
197
|
+
code: "auth/no-credential",
|
|
198
|
+
message: "Failed to get Apple credential",
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
await reauthenticateWithCredential(user, result.credential);
|
|
205
|
+
return { success: true };
|
|
206
|
+
} catch (error) {
|
|
207
|
+
const firebaseError = error as { code?: string; message?: string };
|
|
208
|
+
return {
|
|
209
|
+
success: false,
|
|
210
|
+
error: {
|
|
211
|
+
code: firebaseError.code || "auth/reauthentication-failed",
|
|
212
|
+
message: firebaseError.message || "Apple reauthentication failed",
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|