@umituz/react-native-firebase 1.13.48 → 1.13.49
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 +1 -1
- package/src/auth/infrastructure/config/FirebaseAuthClient.ts +16 -171
- package/src/auth/infrastructure/services/account-deletion.service.ts +41 -351
- package/src/auth/infrastructure/services/reauthentication.service.ts +47 -207
- package/src/auth/infrastructure/services/reauthentication.types.ts +39 -0
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.49",
|
|
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",
|
|
@@ -1,16 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Firebase Auth Client - Infrastructure Layer
|
|
3
|
-
*
|
|
4
|
-
* Domain-Driven Design: Infrastructure implementation of Firebase Auth client
|
|
5
|
-
* Singleton pattern for managing Firebase Auth instance
|
|
6
|
-
*
|
|
7
|
-
* IMPORTANT: This package requires Firebase App to be initialized first.
|
|
8
|
-
* Use @umituz/react-native-firebase to initialize Firebase App.
|
|
9
|
-
*
|
|
10
|
-
* SOLID Principles:
|
|
11
|
-
* - Single Responsibility: Only manages Auth initialization
|
|
12
|
-
* - Open/Closed: Extensible through configuration, closed for modification
|
|
13
|
-
* - Dependency Inversion: Depends on Firebase App from @umituz/react-native-firebase
|
|
14
3
|
*/
|
|
15
4
|
|
|
16
5
|
import type { Auth } from 'firebase/auth';
|
|
@@ -20,190 +9,46 @@ import type { FirebaseAuthConfig } from '../../domain/value-objects/FirebaseAuth
|
|
|
20
9
|
|
|
21
10
|
declare const __DEV__: boolean;
|
|
22
11
|
|
|
23
|
-
/**
|
|
24
|
-
* Firebase Auth Client Singleton
|
|
25
|
-
* Manages Firebase Auth initialization
|
|
26
|
-
*/
|
|
27
12
|
class FirebaseAuthClientSingleton {
|
|
28
13
|
private static instance: FirebaseAuthClientSingleton | null = null;
|
|
29
14
|
private auth: Auth | null = null;
|
|
30
15
|
private initializationError: string | null = null;
|
|
31
16
|
|
|
32
|
-
private constructor() {
|
|
33
|
-
// Private constructor to enforce singleton pattern
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Get singleton instance
|
|
38
|
-
*/
|
|
39
17
|
static getInstance(): FirebaseAuthClientSingleton {
|
|
40
|
-
if (!
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
return FirebaseAuthClientSingleton.instance;
|
|
18
|
+
if (!this.instance) this.instance = new FirebaseAuthClientSingleton();
|
|
19
|
+
return this.instance;
|
|
44
20
|
}
|
|
45
21
|
|
|
46
|
-
/**
|
|
47
|
-
* Initialize Firebase Auth
|
|
48
|
-
* Requires Firebase App to be initialized first via @umituz/react-native-firebase
|
|
49
|
-
*
|
|
50
|
-
* @param config - Optional Firebase Auth configuration
|
|
51
|
-
* @returns Firebase Auth instance or null if initialization fails
|
|
52
|
-
*/
|
|
53
22
|
initialize(config?: FirebaseAuthConfig): Auth | null {
|
|
54
|
-
|
|
55
|
-
if (this.
|
|
56
|
-
if (__DEV__) {
|
|
57
|
-
console.log('[FirebaseAuth] Already initialized, returning existing instance');
|
|
58
|
-
}
|
|
59
|
-
return this.auth;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Don't retry if initialization already failed
|
|
63
|
-
if (this.initializationError) {
|
|
64
|
-
if (__DEV__) {
|
|
65
|
-
console.warn('[FirebaseAuth] Previous initialization failed:', this.initializationError);
|
|
66
|
-
}
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
23
|
+
if (this.auth) return this.auth;
|
|
24
|
+
if (this.initializationError) return null;
|
|
69
25
|
|
|
70
26
|
try {
|
|
71
|
-
// Get Firebase App instance (must be initialized first)
|
|
72
27
|
const app = getFirebaseApp();
|
|
73
|
-
|
|
74
|
-
// Return null if Firebase App is not available (offline mode)
|
|
75
|
-
if (!app) {
|
|
76
|
-
if (__DEV__) {
|
|
77
|
-
console.log('[FirebaseAuth] Firebase App not available - skipping Auth initialization');
|
|
78
|
-
}
|
|
79
|
-
// Don't set initializationError - this is normal offline mode
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (__DEV__) {
|
|
84
|
-
console.log('[FirebaseAuth] Initializing Firebase Auth...');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Initialize Auth
|
|
28
|
+
if (!app) return null;
|
|
88
29
|
this.auth = FirebaseAuthInitializer.initialize(app, config);
|
|
89
|
-
|
|
90
|
-
if (this.auth) {
|
|
91
|
-
if (__DEV__) {
|
|
92
|
-
console.log('[FirebaseAuth] ✅ Successfully initialized');
|
|
93
|
-
}
|
|
94
|
-
} else {
|
|
95
|
-
if (__DEV__) {
|
|
96
|
-
console.warn('[FirebaseAuth] ⚠️ Initialization returned null');
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
30
|
return this.auth;
|
|
101
|
-
} catch (error) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (__DEV__) {
|
|
105
|
-
console.error('[FirebaseAuth] ❌ Initialization error:', errorMessage);
|
|
106
|
-
}
|
|
107
|
-
if (!errorMessage.includes('not initialized') && !errorMessage.includes('not available')) {
|
|
108
|
-
this.initializationError = errorMessage;
|
|
109
|
-
}
|
|
31
|
+
} catch (error: any) {
|
|
32
|
+
if (__DEV__) console.error('[FirebaseAuth] Init error:', error.message);
|
|
33
|
+
this.initializationError = error.message;
|
|
110
34
|
return null;
|
|
111
35
|
}
|
|
112
36
|
}
|
|
113
37
|
|
|
114
|
-
/**
|
|
115
|
-
* Get the Firebase Auth instance
|
|
116
|
-
* Auto-initializes if Firebase App is available
|
|
117
|
-
* Returns null if config is not available (offline mode - no error)
|
|
118
|
-
* @returns Firebase Auth instance or null if not initialized
|
|
119
|
-
*/
|
|
120
38
|
getAuth(): Auth | null {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
// Try to get Firebase App (will auto-initialize if config is available)
|
|
125
|
-
const app = getFirebaseApp();
|
|
126
|
-
if (app) {
|
|
127
|
-
this.initialize();
|
|
128
|
-
}
|
|
129
|
-
} catch {
|
|
130
|
-
// Firebase App not available, return null (offline mode)
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Return null if not initialized (offline mode - no error)
|
|
136
|
-
return this.auth || null;
|
|
39
|
+
if (!this.auth && !this.initializationError && getFirebaseApp()) this.initialize();
|
|
40
|
+
return this.auth;
|
|
137
41
|
}
|
|
138
42
|
|
|
139
|
-
|
|
140
|
-
* Check if Auth is initialized
|
|
141
|
-
*/
|
|
142
|
-
isInitialized(): boolean {
|
|
143
|
-
return this.auth !== null;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Get initialization error if any
|
|
148
|
-
*/
|
|
149
|
-
getInitializationError(): string | null {
|
|
150
|
-
return this.initializationError;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Reset the Auth client instance
|
|
155
|
-
* Useful for testing
|
|
156
|
-
*/
|
|
157
|
-
reset(): void {
|
|
158
|
-
this.auth = null;
|
|
159
|
-
this.initializationError = null;
|
|
160
|
-
}
|
|
43
|
+
reset(): void { this.auth = null; this.initializationError = null; }
|
|
161
44
|
}
|
|
162
45
|
|
|
163
|
-
/**
|
|
164
|
-
* Singleton instance
|
|
165
|
-
*/
|
|
166
46
|
export const firebaseAuthClient = FirebaseAuthClientSingleton.getInstance();
|
|
47
|
+
export const initializeFirebaseAuth = (c?: FirebaseAuthConfig) => firebaseAuthClient.initialize(c);
|
|
48
|
+
export const getFirebaseAuth = () => firebaseAuthClient.getAuth();
|
|
49
|
+
export const isFirebaseAuthInitialized = () => firebaseAuthClient.getAuth() !== null;
|
|
50
|
+
export const getFirebaseAuthInitializationError = () => firebaseAuthClient.initialize() ? null : "Not initialized";
|
|
51
|
+
export const resetFirebaseAuthClient = () => firebaseAuthClient.reset();
|
|
167
52
|
|
|
168
|
-
export function initializeFirebaseAuth(
|
|
169
|
-
config?: FirebaseAuthConfig
|
|
170
|
-
): Auth | null {
|
|
171
|
-
return firebaseAuthClient.initialize(config);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Get Firebase Auth instance
|
|
176
|
-
* Auto-initializes if Firebase App is available
|
|
177
|
-
* Returns null if config is not available (offline mode - no error)
|
|
178
|
-
* @returns Firebase Auth instance or null if not initialized
|
|
179
|
-
*/
|
|
180
|
-
export function getFirebaseAuth(): Auth | null {
|
|
181
|
-
return firebaseAuthClient.getAuth();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Check if Firebase Auth is initialized
|
|
186
|
-
*/
|
|
187
|
-
export function isFirebaseAuthInitialized(): boolean {
|
|
188
|
-
return firebaseAuthClient.isInitialized();
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Get initialization error if any
|
|
193
|
-
*/
|
|
194
|
-
export function getFirebaseAuthInitializationError(): string | null {
|
|
195
|
-
return firebaseAuthClient.getInitializationError();
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Reset Firebase Auth client instance
|
|
200
|
-
* Useful for testing
|
|
201
|
-
*/
|
|
202
|
-
export function resetFirebaseAuthClient(): void {
|
|
203
|
-
firebaseAuthClient.reset();
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Export types
|
|
207
53
|
export type { Auth } from 'firebase/auth';
|
|
208
54
|
export type { FirebaseAuthConfig } from '../../domain/value-objects/FirebaseAuthConfig';
|
|
209
|
-
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Account Deletion Service
|
|
3
|
-
* Handles Firebase Auth account deletion operations with automatic reauthentication
|
|
4
|
-
*
|
|
5
|
-
* SOLID: Single Responsibility - Only handles account deletion
|
|
6
3
|
*/
|
|
7
4
|
|
|
8
5
|
import { deleteUser, type User } from "firebase/auth";
|
|
@@ -11,381 +8,74 @@ import {
|
|
|
11
8
|
getUserAuthProvider,
|
|
12
9
|
reauthenticateWithApple,
|
|
13
10
|
reauthenticateWithPassword,
|
|
11
|
+
reauthenticateWithGoogle,
|
|
14
12
|
} from "./reauthentication.service";
|
|
13
|
+
import type { AccountDeletionResult, AccountDeletionOptions } from "./reauthentication.types";
|
|
15
14
|
|
|
16
|
-
export
|
|
17
|
-
success: boolean;
|
|
18
|
-
error?: {
|
|
19
|
-
code: string;
|
|
20
|
-
message: string;
|
|
21
|
-
requiresReauth: boolean;
|
|
22
|
-
};
|
|
23
|
-
}
|
|
15
|
+
export type { AccountDeletionResult, AccountDeletionOptions } from "./reauthentication.types";
|
|
24
16
|
|
|
25
|
-
|
|
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
|
-
* Password for reauthentication (required if user signed in with email/password)
|
|
33
|
-
* This must be provided by the calling code after prompting user for password
|
|
34
|
-
*/
|
|
35
|
-
password?: string;
|
|
36
|
-
/**
|
|
37
|
-
* If true, will attempt to reauthenticate with Apple automatically
|
|
38
|
-
* (shows Apple sign-in prompt to user)
|
|
39
|
-
*/
|
|
40
|
-
autoReauthenticate?: boolean;
|
|
41
|
-
}
|
|
17
|
+
declare const __DEV__: boolean;
|
|
42
18
|
|
|
43
|
-
/**
|
|
44
|
-
* Delete the current user's Firebase Auth account
|
|
45
|
-
* Now with automatic reauthentication support for Apple Sign-In
|
|
46
|
-
* Note: This is irreversible and also signs out the user
|
|
47
|
-
*/
|
|
48
19
|
export async function deleteCurrentUser(
|
|
49
20
|
options: AccountDeletionOptions = { autoReauthenticate: true }
|
|
50
21
|
): Promise<AccountDeletionResult> {
|
|
51
|
-
if (__DEV__) {
|
|
52
|
-
console.log("[deleteCurrentUser] Starting with options:", options);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
22
|
const auth = getFirebaseAuth();
|
|
23
|
+
const user = auth?.currentUser;
|
|
56
24
|
|
|
57
|
-
if (!auth) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
message: "Firebase Auth is not initialized",
|
|
66
|
-
requiresReauth: false,
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const user = auth.currentUser;
|
|
72
|
-
|
|
73
|
-
if (!user) {
|
|
74
|
-
if (__DEV__) {
|
|
75
|
-
console.log("[deleteCurrentUser] ❌ No user signed in");
|
|
76
|
-
}
|
|
77
|
-
return {
|
|
78
|
-
success: false,
|
|
79
|
-
error: {
|
|
80
|
-
code: "auth/no-user",
|
|
81
|
-
message: "No user is currently signed in",
|
|
82
|
-
requiresReauth: false,
|
|
83
|
-
},
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (user.isAnonymous) {
|
|
88
|
-
if (__DEV__) {
|
|
89
|
-
console.log("[deleteCurrentUser] ❌ Cannot delete anonymous user");
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
success: false,
|
|
93
|
-
error: {
|
|
94
|
-
code: "auth/anonymous-user",
|
|
95
|
-
message: "Cannot delete anonymous account",
|
|
96
|
-
requiresReauth: false,
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (__DEV__) {
|
|
102
|
-
const provider = getUserAuthProvider(user);
|
|
103
|
-
console.log("[deleteCurrentUser] User info:", {
|
|
104
|
-
uid: user.uid,
|
|
105
|
-
provider,
|
|
106
|
-
providerData: user.providerData?.map(p => p.providerId),
|
|
107
|
-
});
|
|
108
|
-
}
|
|
25
|
+
if (!auth || !user) return {
|
|
26
|
+
success: false,
|
|
27
|
+
error: { code: "auth/not-ready", message: "Auth not ready", requiresReauth: false }
|
|
28
|
+
};
|
|
29
|
+
if (user.isAnonymous) return {
|
|
30
|
+
success: false,
|
|
31
|
+
error: { code: "auth/anonymous", message: "Cannot delete anonymous", requiresReauth: false }
|
|
32
|
+
};
|
|
109
33
|
|
|
110
|
-
// First attempt to delete
|
|
111
34
|
try {
|
|
112
|
-
if (__DEV__) {
|
|
113
|
-
console.log("[deleteCurrentUser] Attempting to delete user...");
|
|
114
|
-
}
|
|
115
35
|
await deleteUser(user);
|
|
116
|
-
if (__DEV__) {
|
|
117
|
-
console.log("[deleteCurrentUser] ✅ User deleted successfully");
|
|
118
|
-
}
|
|
119
36
|
return { success: true };
|
|
120
|
-
} catch (error) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (__DEV__) {
|
|
125
|
-
console.log("[deleteCurrentUser] First delete attempt failed:", {
|
|
126
|
-
code: firebaseError.code,
|
|
127
|
-
message: firebaseError.message,
|
|
128
|
-
requiresReauth,
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// If reauthentication is required and autoReauthenticate is enabled OR password is provided
|
|
133
|
-
if (requiresReauth && (options.autoReauthenticate || options.password)) {
|
|
134
|
-
if (__DEV__) {
|
|
135
|
-
console.log("[deleteCurrentUser] Attempting auto-reauthentication...");
|
|
136
|
-
}
|
|
137
|
-
const reauthResult = await attemptReauthenticationAndDelete(user, options);
|
|
138
|
-
if (reauthResult) {
|
|
139
|
-
if (__DEV__) {
|
|
140
|
-
console.log("[deleteCurrentUser] Auto-reauth result:", reauthResult);
|
|
141
|
-
}
|
|
142
|
-
return reauthResult;
|
|
143
|
-
}
|
|
37
|
+
} catch (error: any) {
|
|
38
|
+
if (error.code === "auth/requires-recent-login" && (options.autoReauthenticate || options.password || options.googleIdToken)) {
|
|
39
|
+
const reauth = await attemptReauth(user, options);
|
|
40
|
+
if (reauth) return reauth;
|
|
144
41
|
}
|
|
145
|
-
|
|
146
42
|
return {
|
|
147
43
|
success: false,
|
|
148
|
-
error: {
|
|
149
|
-
code: firebaseError.code || "auth/unknown",
|
|
150
|
-
message: requiresReauth
|
|
151
|
-
? "Please sign in again before deleting your account"
|
|
152
|
-
: firebaseError.message || "Failed to delete account",
|
|
153
|
-
requiresReauth,
|
|
154
|
-
},
|
|
44
|
+
error: { code: error.code || "auth/failed", message: error.message, requiresReauth: error.code === "auth/requires-recent-login" }
|
|
155
45
|
};
|
|
156
46
|
}
|
|
157
47
|
}
|
|
158
48
|
|
|
159
|
-
|
|
160
|
-
* Attempt to reauthenticate based on provider and then delete the account
|
|
161
|
-
*/
|
|
162
|
-
async function attemptReauthenticationAndDelete(
|
|
163
|
-
user: User,
|
|
164
|
-
options: AccountDeletionOptions
|
|
165
|
-
): Promise<AccountDeletionResult | null> {
|
|
49
|
+
async function attemptReauth(user: User, options: AccountDeletionOptions): Promise<AccountDeletionResult | null> {
|
|
166
50
|
const provider = getUserAuthProvider(user);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (provider === "
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (reauthResult.success) {
|
|
185
|
-
// Retry deletion after successful reauthentication
|
|
186
|
-
try {
|
|
187
|
-
if (__DEV__) {
|
|
188
|
-
console.log("[attemptReauthenticationAndDelete] Deleting user after Apple reauth...");
|
|
189
|
-
}
|
|
190
|
-
await deleteUser(user);
|
|
191
|
-
if (__DEV__) {
|
|
192
|
-
console.log("[attemptReauthenticationAndDelete] ✅ User deleted after Apple reauth");
|
|
193
|
-
}
|
|
194
|
-
return { success: true };
|
|
195
|
-
} catch (deleteError) {
|
|
196
|
-
const firebaseError = deleteError as { code?: string; message?: string };
|
|
197
|
-
if (__DEV__) {
|
|
198
|
-
console.log("[attemptReauthenticationAndDelete] ❌ Delete failed after Apple reauth:", firebaseError);
|
|
199
|
-
}
|
|
200
|
-
return {
|
|
201
|
-
success: false,
|
|
202
|
-
error: {
|
|
203
|
-
code: firebaseError.code || "auth/deletion-failed-after-reauth",
|
|
204
|
-
message: firebaseError.message || "Failed to delete account after reauthentication",
|
|
205
|
-
requiresReauth: false,
|
|
206
|
-
},
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
} else {
|
|
210
|
-
// Reauthentication failed
|
|
211
|
-
if (__DEV__) {
|
|
212
|
-
console.log("[attemptReauthenticationAndDelete] ❌ Apple reauth failed");
|
|
213
|
-
}
|
|
214
|
-
return {
|
|
215
|
-
success: false,
|
|
216
|
-
error: {
|
|
217
|
-
code: reauthResult.error?.code || "auth/reauthentication-failed",
|
|
218
|
-
message: reauthResult.error?.message || "Reauthentication failed",
|
|
219
|
-
requiresReauth: true,
|
|
220
|
-
},
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Handle Google reauthentication (requires ID token from caller)
|
|
226
|
-
if (provider === "google.com") {
|
|
227
|
-
// For Google, we need the caller to provide the ID token
|
|
228
|
-
// This is because we need to trigger Google Sign-In UI which must be done at the presentation layer
|
|
229
|
-
if (!options.googleIdToken) {
|
|
230
|
-
return {
|
|
231
|
-
success: false,
|
|
232
|
-
error: {
|
|
233
|
-
code: "auth/google-reauth-required",
|
|
234
|
-
message: "Please sign in with Google again to delete your account",
|
|
235
|
-
requiresReauth: true,
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// If we have a Google ID token, reauthenticate with it
|
|
241
|
-
const { reauthenticateWithGoogle } = await import("./reauthentication.service");
|
|
242
|
-
const reauthResult = await reauthenticateWithGoogle(user, options.googleIdToken);
|
|
243
|
-
|
|
244
|
-
if (reauthResult.success) {
|
|
245
|
-
try {
|
|
246
|
-
await deleteUser(user);
|
|
247
|
-
return { success: true };
|
|
248
|
-
} catch (deleteError) {
|
|
249
|
-
const firebaseError = deleteError as { code?: string; message?: string };
|
|
250
|
-
return {
|
|
251
|
-
success: false,
|
|
252
|
-
error: {
|
|
253
|
-
code: firebaseError.code || "auth/deletion-failed-after-reauth",
|
|
254
|
-
message: firebaseError.message || "Failed to delete account after reauthentication",
|
|
255
|
-
requiresReauth: false,
|
|
256
|
-
},
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
} else {
|
|
260
|
-
return {
|
|
261
|
-
success: false,
|
|
262
|
-
error: {
|
|
263
|
-
code: reauthResult.error?.code || "auth/reauthentication-failed",
|
|
264
|
-
message: reauthResult.error?.message || "Google reauthentication failed",
|
|
265
|
-
requiresReauth: true,
|
|
266
|
-
},
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Handle Password reauthentication (requires password from caller)
|
|
272
|
-
if (provider === "password") {
|
|
273
|
-
if (__DEV__) {
|
|
274
|
-
console.log("[attemptReauthenticationAndDelete] Password provider detected");
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// For password, we need the caller to provide the password
|
|
278
|
-
if (!options.password) {
|
|
279
|
-
if (__DEV__) {
|
|
280
|
-
console.log("[attemptReauthenticationAndDelete] No password provided, requesting reauth");
|
|
281
|
-
}
|
|
282
|
-
return {
|
|
283
|
-
success: false,
|
|
284
|
-
error: {
|
|
285
|
-
code: "auth/password-reauth-required",
|
|
286
|
-
message: "Please enter your password to delete your account",
|
|
287
|
-
requiresReauth: true,
|
|
288
|
-
},
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (__DEV__) {
|
|
293
|
-
console.log("[attemptReauthenticationAndDelete] Attempting password reauthentication...");
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const reauthResult = await reauthenticateWithPassword(user, options.password);
|
|
297
|
-
|
|
298
|
-
if (__DEV__) {
|
|
299
|
-
console.log("[attemptReauthenticationAndDelete] Password reauth result:", reauthResult);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (reauthResult.success) {
|
|
303
|
-
try {
|
|
304
|
-
if (__DEV__) {
|
|
305
|
-
console.log("[attemptReauthenticationAndDelete] Deleting user after password reauth...");
|
|
306
|
-
}
|
|
307
|
-
await deleteUser(user);
|
|
308
|
-
if (__DEV__) {
|
|
309
|
-
console.log("[attemptReauthenticationAndDelete] ✅ User deleted after password reauth");
|
|
310
|
-
}
|
|
311
|
-
return { success: true };
|
|
312
|
-
} catch (deleteError) {
|
|
313
|
-
const firebaseError = deleteError as { code?: string; message?: string };
|
|
314
|
-
if (__DEV__) {
|
|
315
|
-
console.log("[attemptReauthenticationAndDelete] ❌ Delete failed after password reauth:", firebaseError);
|
|
316
|
-
}
|
|
317
|
-
return {
|
|
318
|
-
success: false,
|
|
319
|
-
error: {
|
|
320
|
-
code: firebaseError.code || "auth/deletion-failed-after-reauth",
|
|
321
|
-
message: firebaseError.message || "Failed to delete account after reauthentication",
|
|
322
|
-
requiresReauth: false,
|
|
323
|
-
},
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
} else {
|
|
327
|
-
if (__DEV__) {
|
|
328
|
-
console.log("[attemptReauthenticationAndDelete] ❌ Password reauth failed");
|
|
329
|
-
}
|
|
330
|
-
return {
|
|
331
|
-
success: false,
|
|
332
|
-
error: {
|
|
333
|
-
code: reauthResult.error?.code || "auth/reauthentication-failed",
|
|
334
|
-
message: reauthResult.error?.message || "Password reauthentication failed",
|
|
335
|
-
requiresReauth: true,
|
|
336
|
-
},
|
|
337
|
-
};
|
|
51
|
+
let res: { success: boolean, error?: any };
|
|
52
|
+
|
|
53
|
+
if (provider === "apple.com") res = await reauthenticateWithApple(user);
|
|
54
|
+
else if (provider === "google.com") {
|
|
55
|
+
if (!options.googleIdToken) return { success: false, error: { code: "auth/google-reauth", message: "Google reauth required", requiresReauth: true } };
|
|
56
|
+
res = await reauthenticateWithGoogle(user, options.googleIdToken);
|
|
57
|
+
} else if (provider === "password") {
|
|
58
|
+
if (!options.password) return { success: false, error: { code: "auth/password-reauth", message: "Password required", requiresReauth: true } };
|
|
59
|
+
res = await reauthenticateWithPassword(user, options.password);
|
|
60
|
+
} else return null;
|
|
61
|
+
|
|
62
|
+
if (res.success) {
|
|
63
|
+
try {
|
|
64
|
+
await deleteUser(user);
|
|
65
|
+
return { success: true };
|
|
66
|
+
} catch (err: any) {
|
|
67
|
+
return { success: false, error: { code: err.code || "auth/post-reauth-failed", message: err.message, requiresReauth: false } };
|
|
338
68
|
}
|
|
339
69
|
}
|
|
340
|
-
|
|
341
|
-
// For other providers, return null to use the default error handling
|
|
342
|
-
return null;
|
|
70
|
+
return { success: false, error: { code: res.error?.code || "auth/reauth-failed", message: res.error?.message, requiresReauth: true } };
|
|
343
71
|
}
|
|
344
72
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
*/
|
|
348
|
-
export async function deleteUserAccount(
|
|
349
|
-
user: User
|
|
350
|
-
): Promise<AccountDeletionResult> {
|
|
351
|
-
if (!user) {
|
|
352
|
-
return {
|
|
353
|
-
success: false,
|
|
354
|
-
error: {
|
|
355
|
-
code: "auth/no-user",
|
|
356
|
-
message: "No user provided",
|
|
357
|
-
requiresReauth: false,
|
|
358
|
-
},
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (user.isAnonymous) {
|
|
363
|
-
return {
|
|
364
|
-
success: false,
|
|
365
|
-
error: {
|
|
366
|
-
code: "auth/anonymous-user",
|
|
367
|
-
message: "Cannot delete anonymous account",
|
|
368
|
-
requiresReauth: false,
|
|
369
|
-
},
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
|
|
73
|
+
export async function deleteUserAccount(user: User): Promise<AccountDeletionResult> {
|
|
74
|
+
if (!user || user.isAnonymous) return { success: false, error: { code: "auth/invalid", message: "Invalid user", requiresReauth: false } };
|
|
373
75
|
try {
|
|
374
76
|
await deleteUser(user);
|
|
375
77
|
return { success: true };
|
|
376
|
-
} catch (error) {
|
|
377
|
-
|
|
378
|
-
const requiresReauth = firebaseError.code === "auth/requires-recent-login";
|
|
379
|
-
|
|
380
|
-
return {
|
|
381
|
-
success: false,
|
|
382
|
-
error: {
|
|
383
|
-
code: firebaseError.code || "auth/unknown",
|
|
384
|
-
message: requiresReauth
|
|
385
|
-
? "Please sign in again before deleting your account"
|
|
386
|
-
: firebaseError.message || "Failed to delete account",
|
|
387
|
-
requiresReauth,
|
|
388
|
-
},
|
|
389
|
-
};
|
|
78
|
+
} catch (error: any) {
|
|
79
|
+
return { success: false, error: { code: error.code || "auth/failed", message: error.message, requiresReauth: error.code === "auth/requires-recent-login" } };
|
|
390
80
|
}
|
|
391
81
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Reauthentication Service
|
|
3
3
|
* Handles Firebase Auth reauthentication for sensitive operations
|
|
4
|
-
*
|
|
5
|
-
* SOLID: Single Responsibility - Only handles reauthentication
|
|
6
4
|
*/
|
|
7
5
|
|
|
8
6
|
import {
|
|
@@ -11,243 +9,85 @@ import {
|
|
|
11
9
|
OAuthProvider,
|
|
12
10
|
EmailAuthProvider,
|
|
13
11
|
type User,
|
|
14
|
-
type AuthCredential,
|
|
15
12
|
} from "firebase/auth";
|
|
16
13
|
import * as AppleAuthentication from "expo-apple-authentication";
|
|
17
14
|
import * as Crypto from "expo-crypto";
|
|
18
15
|
import { Platform } from "react-native";
|
|
16
|
+
import type {
|
|
17
|
+
ReauthenticationResult,
|
|
18
|
+
AuthProviderType,
|
|
19
|
+
ReauthCredentialResult
|
|
20
|
+
} from "./reauthentication.types";
|
|
21
|
+
|
|
22
|
+
export type {
|
|
23
|
+
ReauthenticationResult,
|
|
24
|
+
AuthProviderType,
|
|
25
|
+
ReauthCredentialResult
|
|
26
|
+
} from "./reauthentication.types";
|
|
19
27
|
|
|
20
|
-
export interface ReauthenticationResult {
|
|
21
|
-
success: boolean;
|
|
22
|
-
error?: {
|
|
23
|
-
code: string;
|
|
24
|
-
message: string;
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export type AuthProviderType = "google.com" | "apple.com" | "password" | "anonymous" | "unknown";
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Get the primary auth provider for a user
|
|
32
|
-
*/
|
|
33
28
|
export function getUserAuthProvider(user: User): AuthProviderType {
|
|
34
|
-
if (user.isAnonymous)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
return "unknown";
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Check for Google
|
|
44
|
-
const googleProvider = providerData.find((p) => p.providerId === "google.com");
|
|
45
|
-
if (googleProvider) {
|
|
46
|
-
return "google.com";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Check for Apple
|
|
50
|
-
const appleProvider = providerData.find((p) => p.providerId === "apple.com");
|
|
51
|
-
if (appleProvider) {
|
|
52
|
-
return "apple.com";
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Check for password
|
|
56
|
-
const passwordProvider = providerData.find((p) => p.providerId === "password");
|
|
57
|
-
if (passwordProvider) {
|
|
58
|
-
return "password";
|
|
59
|
-
}
|
|
60
|
-
|
|
29
|
+
if (user.isAnonymous) return "anonymous";
|
|
30
|
+
const data = user.providerData;
|
|
31
|
+
if (!data?.length) return "unknown";
|
|
32
|
+
if (data.find(p => p.providerId === "google.com")) return "google.com";
|
|
33
|
+
if (data.find(p => p.providerId === "apple.com")) return "apple.com";
|
|
34
|
+
if (data.find(p => p.providerId === "password")) return "password";
|
|
61
35
|
return "unknown";
|
|
62
36
|
}
|
|
63
37
|
|
|
64
|
-
|
|
65
|
-
* Reauthenticate with Google
|
|
66
|
-
*/
|
|
67
|
-
export async function reauthenticateWithGoogle(
|
|
68
|
-
user: User,
|
|
69
|
-
idToken: string
|
|
70
|
-
): Promise<ReauthenticationResult> {
|
|
38
|
+
export async function reauthenticateWithGoogle(user: User, idToken: string): Promise<ReauthenticationResult> {
|
|
71
39
|
try {
|
|
72
|
-
|
|
73
|
-
await reauthenticateWithCredential(user, credential);
|
|
40
|
+
await reauthenticateWithCredential(user, GoogleAuthProvider.credential(idToken));
|
|
74
41
|
return { success: true };
|
|
75
|
-
} catch (error) {
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
success: false,
|
|
79
|
-
error: {
|
|
80
|
-
code: firebaseError.code || "auth/reauthentication-failed",
|
|
81
|
-
message: firebaseError.message || "Google reauthentication failed",
|
|
82
|
-
},
|
|
83
|
-
};
|
|
42
|
+
} catch (error: any) {
|
|
43
|
+
return { success: false, error: { code: error.code || "auth/failed", message: error.message } };
|
|
84
44
|
}
|
|
85
45
|
}
|
|
86
46
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
*/
|
|
90
|
-
export async function reauthenticateWithPassword(
|
|
91
|
-
user: User,
|
|
92
|
-
password: string
|
|
93
|
-
): Promise<ReauthenticationResult> {
|
|
47
|
+
export async function reauthenticateWithPassword(user: User, pass: string): Promise<ReauthenticationResult> {
|
|
48
|
+
if (!user.email) return { success: false, error: { code: "auth/no-email", message: "User has no email" } };
|
|
94
49
|
try {
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
success: false,
|
|
98
|
-
error: {
|
|
99
|
-
code: "auth/no-email",
|
|
100
|
-
message: "User has no email address",
|
|
101
|
-
},
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const credential = EmailAuthProvider.credential(user.email, password);
|
|
106
|
-
await reauthenticateWithCredential(user, credential);
|
|
50
|
+
await reauthenticateWithCredential(user, EmailAuthProvider.credential(user.email, pass));
|
|
107
51
|
return { success: true };
|
|
108
|
-
} catch (error) {
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
success: false,
|
|
112
|
-
error: {
|
|
113
|
-
code: firebaseError.code || "auth/reauthentication-failed",
|
|
114
|
-
message: firebaseError.message || "Password reauthentication failed",
|
|
115
|
-
},
|
|
116
|
-
};
|
|
52
|
+
} catch (error: any) {
|
|
53
|
+
return { success: false, error: { code: error.code || "auth/failed", message: error.message } };
|
|
117
54
|
}
|
|
118
55
|
}
|
|
119
56
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
*/
|
|
123
|
-
async function generateNonce(length: number = 32): Promise<string> {
|
|
124
|
-
const randomBytes = await Crypto.getRandomBytesAsync(length);
|
|
57
|
+
async function generateNonce(len: number = 32): Promise<string> {
|
|
58
|
+
const bytes = await Crypto.getRandomBytesAsync(len);
|
|
125
59
|
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
for (let i = 0; i < randomBytes.length; i++) {
|
|
129
|
-
const byte = randomBytes[i];
|
|
130
|
-
if (byte !== undefined) {
|
|
131
|
-
result += chars.charAt(byte % chars.length);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return result;
|
|
60
|
+
return Array.from(bytes).map(b => chars.charAt(b % chars.length)).join("");
|
|
136
61
|
}
|
|
137
62
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
* Returns the credential that can be used for reauthentication
|
|
141
|
-
*/
|
|
142
|
-
export async function getAppleReauthCredential(): Promise<{
|
|
143
|
-
success: boolean;
|
|
144
|
-
credential?: AuthCredential;
|
|
145
|
-
error?: { code: string; message: string };
|
|
146
|
-
}> {
|
|
147
|
-
if (Platform.OS !== "ios") {
|
|
148
|
-
return {
|
|
149
|
-
success: false,
|
|
150
|
-
error: {
|
|
151
|
-
code: "auth/platform-not-supported",
|
|
152
|
-
message: "Apple Sign-In is only available on iOS",
|
|
153
|
-
},
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
63
|
+
export async function getAppleReauthCredential(): Promise<ReauthCredentialResult> {
|
|
64
|
+
if (Platform.OS !== "ios") return { success: false, error: { code: "auth/ios-only", message: "iOS only" } };
|
|
157
65
|
try {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return {
|
|
161
|
-
success: false,
|
|
162
|
-
error: {
|
|
163
|
-
code: "auth/apple-signin-unavailable",
|
|
164
|
-
message: "Apple Sign-In is not available on this device",
|
|
165
|
-
},
|
|
166
|
-
};
|
|
167
|
-
}
|
|
66
|
+
if (!(await AppleAuthentication.isAvailableAsync()))
|
|
67
|
+
return { success: false, error: { code: "auth/unavailable", message: "Unavailable" } };
|
|
168
68
|
|
|
169
|
-
// Generate nonce
|
|
170
69
|
const nonce = await generateNonce();
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// Request Apple Sign-In
|
|
177
|
-
const appleCredential = await AppleAuthentication.signInAsync({
|
|
178
|
-
requestedScopes: [
|
|
179
|
-
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
|
|
180
|
-
AppleAuthentication.AppleAuthenticationScope.EMAIL,
|
|
181
|
-
],
|
|
182
|
-
nonce: hashedNonce,
|
|
70
|
+
const hashed = await Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, nonce);
|
|
71
|
+
const apple = await AppleAuthentication.signInAsync({
|
|
72
|
+
requestedScopes: [AppleAuthentication.AppleAuthenticationScope.FULL_NAME, AppleAuthentication.AppleAuthenticationScope.EMAIL],
|
|
73
|
+
nonce: hashed,
|
|
183
74
|
});
|
|
184
75
|
|
|
185
|
-
if (!
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
message: "No identity token received from Apple",
|
|
191
|
-
},
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Create Firebase credential
|
|
196
|
-
const provider = new OAuthProvider("apple.com");
|
|
197
|
-
const credential = provider.credential({
|
|
198
|
-
idToken: appleCredential.identityToken,
|
|
199
|
-
rawNonce: nonce,
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
return { success: true, credential };
|
|
203
|
-
} catch (error) {
|
|
204
|
-
if (error instanceof Error && error.message.includes("ERR_CANCELED")) {
|
|
205
|
-
return {
|
|
206
|
-
success: false,
|
|
207
|
-
error: {
|
|
208
|
-
code: "auth/cancelled",
|
|
209
|
-
message: "Apple Sign-In was cancelled",
|
|
210
|
-
},
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return {
|
|
215
|
-
success: false,
|
|
216
|
-
error: {
|
|
217
|
-
code: "auth/apple-reauthentication-failed",
|
|
218
|
-
message: error instanceof Error ? error.message : "Apple reauthentication failed",
|
|
219
|
-
},
|
|
220
|
-
};
|
|
76
|
+
if (!apple.identityToken) return { success: false, error: { code: "auth/no-token", message: "No token" } };
|
|
77
|
+
return { success: true, credential: new OAuthProvider("apple.com").credential({ idToken: apple.identityToken, rawNonce: nonce }) };
|
|
78
|
+
} catch (error: any) {
|
|
79
|
+
const code = error.message?.includes("ERR_CANCELED") ? "auth/cancelled" : "auth/failed";
|
|
80
|
+
return { success: false, error: { code, message: error.message } };
|
|
221
81
|
}
|
|
222
82
|
}
|
|
223
83
|
|
|
224
|
-
/**
|
|
225
|
-
* Reauthenticate with Apple
|
|
226
|
-
*/
|
|
227
84
|
export async function reauthenticateWithApple(user: User): Promise<ReauthenticationResult> {
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
if (!result.success || !result.credential) {
|
|
231
|
-
return {
|
|
232
|
-
success: false,
|
|
233
|
-
error: result.error || {
|
|
234
|
-
code: "auth/no-credential",
|
|
235
|
-
message: "Failed to get Apple credential",
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
85
|
+
const res = await getAppleReauthCredential();
|
|
86
|
+
if (!res.success || !res.credential) return { success: false, error: res.error };
|
|
240
87
|
try {
|
|
241
|
-
await reauthenticateWithCredential(user,
|
|
88
|
+
await reauthenticateWithCredential(user, res.credential);
|
|
242
89
|
return { success: true };
|
|
243
|
-
} catch (error) {
|
|
244
|
-
|
|
245
|
-
return {
|
|
246
|
-
success: false,
|
|
247
|
-
error: {
|
|
248
|
-
code: firebaseError.code || "auth/reauthentication-failed",
|
|
249
|
-
message: firebaseError.message || "Apple reauthentication failed",
|
|
250
|
-
},
|
|
251
|
-
};
|
|
90
|
+
} catch (error: any) {
|
|
91
|
+
return { success: false, error: { code: error.code || "auth/failed", message: error.message } };
|
|
252
92
|
}
|
|
253
93
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reauthentication Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AuthCredential } from "firebase/auth";
|
|
6
|
+
|
|
7
|
+
export interface ReauthenticationResult {
|
|
8
|
+
success: boolean;
|
|
9
|
+
error?: {
|
|
10
|
+
code: string;
|
|
11
|
+
message: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type AuthProviderType = "google.com" | "apple.com" | "password" | "anonymous" | "unknown";
|
|
16
|
+
|
|
17
|
+
export interface ReauthCredentialResult {
|
|
18
|
+
success: boolean;
|
|
19
|
+
credential?: AuthCredential;
|
|
20
|
+
error?: { code: string; message: string };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AccountDeletionResult {
|
|
24
|
+
success: boolean;
|
|
25
|
+
error?: {
|
|
26
|
+
code: string;
|
|
27
|
+
message: string;
|
|
28
|
+
requiresReauth: boolean;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AccountDeletionOptions {
|
|
33
|
+
/** Google ID token for reauthentication */
|
|
34
|
+
googleIdToken?: string;
|
|
35
|
+
/** Password for reauthentication */
|
|
36
|
+
password?: string;
|
|
37
|
+
/** Attempt Apple reauth automatically */
|
|
38
|
+
autoReauthenticate?: boolean;
|
|
39
|
+
}
|