svelte-firekit 0.0.25 → 0.1.1
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/README.md +445 -213
- package/dist/components/Collection.svelte +150 -0
- package/dist/components/Collection.svelte.d.ts +27 -0
- package/dist/components/Ddoc.svelte +131 -0
- package/dist/components/Ddoc.svelte.d.ts +28 -0
- package/dist/components/Node.svelte +97 -0
- package/dist/components/Node.svelte.d.ts +23 -0
- package/dist/components/auth-guard.svelte +89 -0
- package/dist/components/auth-guard.svelte.d.ts +26 -0
- package/dist/components/custom-guard.svelte +122 -0
- package/dist/components/custom-guard.svelte.d.ts +31 -0
- package/dist/components/download-url.svelte +92 -0
- package/dist/components/download-url.svelte.d.ts +19 -0
- package/dist/components/firebase-app.svelte +30 -0
- package/dist/components/firebase-app.svelte.d.ts +7 -0
- package/dist/components/node-list.svelte +102 -0
- package/dist/components/node-list.svelte.d.ts +27 -0
- package/dist/components/signed-in.svelte +42 -0
- package/dist/components/signed-in.svelte.d.ts +11 -0
- package/dist/components/signed-out.svelte +42 -0
- package/dist/components/signed-out.svelte.d.ts +11 -0
- package/dist/components/storage-list.svelte +97 -0
- package/dist/components/storage-list.svelte.d.ts +26 -0
- package/dist/components/upload-task.svelte +108 -0
- package/dist/components/upload-task.svelte.d.ts +24 -0
- package/dist/config.js +17 -39
- package/dist/firebase.d.ts +43 -21
- package/dist/firebase.js +121 -35
- package/dist/index.d.ts +21 -13
- package/dist/index.js +27 -15
- package/dist/services/auth.d.ts +397 -0
- package/dist/services/auth.js +882 -0
- package/dist/services/collection.svelte.d.ts +286 -0
- package/dist/services/collection.svelte.js +871 -0
- package/dist/services/document.svelte.d.ts +288 -0
- package/dist/services/document.svelte.js +555 -0
- package/dist/services/mutations.d.ts +336 -0
- package/dist/services/mutations.js +1079 -0
- package/dist/services/presence.svelte.d.ts +141 -0
- package/dist/services/presence.svelte.js +727 -0
- package/dist/{realtime → services}/realtime.svelte.d.ts +3 -1
- package/dist/{realtime → services}/realtime.svelte.js +13 -7
- package/dist/services/storage.svelte.d.ts +257 -0
- package/dist/services/storage.svelte.js +374 -0
- package/dist/services/user.svelte.d.ts +296 -0
- package/dist/services/user.svelte.js +609 -0
- package/dist/types/auth.d.ts +158 -0
- package/dist/types/auth.js +106 -0
- package/dist/types/collection.d.ts +360 -0
- package/dist/types/collection.js +167 -0
- package/dist/types/document.d.ts +342 -0
- package/dist/types/document.js +148 -0
- package/dist/types/firebase.d.ts +44 -0
- package/dist/types/firebase.js +33 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.js +4 -0
- package/dist/types/mutations.d.ts +387 -0
- package/dist/types/mutations.js +205 -0
- package/dist/types/presence.d.ts +282 -0
- package/dist/types/presence.js +80 -0
- package/dist/utils/errors.d.ts +21 -0
- package/dist/utils/errors.js +35 -0
- package/dist/utils/firestore.d.ts +9 -0
- package/dist/utils/firestore.js +33 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/providers.d.ts +16 -0
- package/dist/utils/providers.js +30 -0
- package/dist/utils/user.d.ts +8 -0
- package/dist/utils/user.js +29 -0
- package/package.json +64 -64
- package/dist/auth/auth.d.ts +0 -117
- package/dist/auth/auth.js +0 -194
- package/dist/auth/presence.svelte.d.ts +0 -139
- package/dist/auth/presence.svelte.js +0 -373
- package/dist/auth/user.svelte.d.ts +0 -112
- package/dist/auth/user.svelte.js +0 -155
- package/dist/firestore/awaitable-doc.svelte.d.ts +0 -141
- package/dist/firestore/awaitable-doc.svelte.js +0 -183
- package/dist/firestore/batch-mutations.svelte.d.ts +0 -140
- package/dist/firestore/batch-mutations.svelte.js +0 -218
- package/dist/firestore/collection-group.svelte.d.ts +0 -78
- package/dist/firestore/collection-group.svelte.js +0 -120
- package/dist/firestore/collection.svelte.d.ts +0 -96
- package/dist/firestore/collection.svelte.js +0 -137
- package/dist/firestore/doc.svelte.d.ts +0 -90
- package/dist/firestore/doc.svelte.js +0 -131
- package/dist/firestore/document-mutations.svelte.d.ts +0 -164
- package/dist/firestore/document-mutations.svelte.js +0 -273
- package/dist/storage/download-url.svelte.d.ts +0 -83
- package/dist/storage/download-url.svelte.js +0 -114
- package/dist/storage/storage-list.svelte.d.ts +0 -89
- package/dist/storage/storage-list.svelte.js +0 -123
- package/dist/storage/upload-task.svelte.d.ts +0 -94
- package/dist/storage/upload-task.svelte.js +0 -138
|
@@ -0,0 +1,882 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview FirekitAuth - Complete Firebase Authentication Service for Svelte
|
|
3
|
+
* @module FirekitAuth
|
|
4
|
+
* @version 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
import { EmailAuthProvider, PhoneAuthProvider, RecaptchaVerifier, signInWithEmailAndPassword, signInWithPopup, signInWithPhoneNumber as firebaseSignInWithPhoneNumber, signInWithCredential, signInAnonymously as firebaseSignInAnonymously, createUserWithEmailAndPassword, signOut, sendPasswordResetEmail, confirmPasswordReset, sendEmailVerification, updateProfile, updateEmail, updatePassword, reauthenticateWithCredential, deleteUser, reload, getIdToken, onAuthStateChanged } from 'firebase/auth';
|
|
7
|
+
import { doc, setDoc, serverTimestamp } from 'firebase/firestore';
|
|
8
|
+
import { firebaseService } from '../firebase.js';
|
|
9
|
+
import { AuthErrorCode, FirekitAuthError } from '../types/auth.js';
|
|
10
|
+
import { mapFirebaseUserToProfile, updateUserInFirestore, createGoogleProvider, createFacebookProvider, createAppleProvider, handleAuthError } from '../utils/index.js';
|
|
11
|
+
/**
|
|
12
|
+
* Comprehensive Firebase Authentication service for Svelte applications.
|
|
13
|
+
* Provides a complete authentication solution with automatic Firestore integration,
|
|
14
|
+
* error handling, and support for all major authentication methods.
|
|
15
|
+
*
|
|
16
|
+
* @class FirekitAuth
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import { firekitAuth } from 'svelte-firekit';
|
|
20
|
+
*
|
|
21
|
+
* // Sign in with Google
|
|
22
|
+
* await firekitAuth.signInWithGoogle();
|
|
23
|
+
*
|
|
24
|
+
* // Register new user
|
|
25
|
+
* await firekitAuth.registerWithEmail("user@example.com", "password123", "John Doe");
|
|
26
|
+
*
|
|
27
|
+
* // Listen to auth state changes
|
|
28
|
+
* const unsubscribe = firekitAuth.onAuthStateChanged((user) => {
|
|
29
|
+
* console.log('User:', user);
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
class FirekitAuth {
|
|
34
|
+
static instance;
|
|
35
|
+
auth = null;
|
|
36
|
+
firestore = null;
|
|
37
|
+
_servicesInitialized = false;
|
|
38
|
+
authState = {
|
|
39
|
+
user: null,
|
|
40
|
+
loading: true,
|
|
41
|
+
initialized: false
|
|
42
|
+
};
|
|
43
|
+
stateListeners = new Set();
|
|
44
|
+
recaptchaVerifiers = new Map();
|
|
45
|
+
constructor() {
|
|
46
|
+
// Don't initialize Firebase services in constructor
|
|
47
|
+
// They will be initialized lazily when needed
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Gets singleton instance of FirekitAuth
|
|
51
|
+
* @returns {FirekitAuth} The FirekitAuth instance
|
|
52
|
+
*/
|
|
53
|
+
static getInstance() {
|
|
54
|
+
if (!FirekitAuth.instance) {
|
|
55
|
+
FirekitAuth.instance = new FirekitAuth();
|
|
56
|
+
}
|
|
57
|
+
return FirekitAuth.instance;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Initializes Firebase services and auth state listener
|
|
61
|
+
* @private
|
|
62
|
+
*/
|
|
63
|
+
initializeServices() {
|
|
64
|
+
if (this._servicesInitialized)
|
|
65
|
+
return;
|
|
66
|
+
try {
|
|
67
|
+
this.auth = firebaseService.getAuthInstance();
|
|
68
|
+
this.firestore = firebaseService.getDbInstance();
|
|
69
|
+
this._servicesInitialized = true;
|
|
70
|
+
this.initializeAuthStateListener();
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
console.error('Failed to initialize Firebase services:', error);
|
|
74
|
+
this.authState = {
|
|
75
|
+
user: null,
|
|
76
|
+
loading: false,
|
|
77
|
+
initialized: true
|
|
78
|
+
};
|
|
79
|
+
this.notifyStateListeners();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Initializes the authentication state listener
|
|
84
|
+
* @private
|
|
85
|
+
*/
|
|
86
|
+
initializeAuthStateListener() {
|
|
87
|
+
if (!this.auth) {
|
|
88
|
+
console.error('Auth instance not available');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
onAuthStateChanged(this.auth, (user) => {
|
|
92
|
+
this.authState = {
|
|
93
|
+
user: user ? this.mapFirebaseUserToProfile(user) : null,
|
|
94
|
+
loading: false,
|
|
95
|
+
initialized: true
|
|
96
|
+
};
|
|
97
|
+
this.notifyStateListeners();
|
|
98
|
+
}, (error) => {
|
|
99
|
+
console.error('Auth state change error:', error);
|
|
100
|
+
this.authState = {
|
|
101
|
+
user: null,
|
|
102
|
+
loading: false,
|
|
103
|
+
initialized: true
|
|
104
|
+
};
|
|
105
|
+
this.notifyStateListeners();
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Notifies all state listeners of auth state changes
|
|
110
|
+
* @private
|
|
111
|
+
*/
|
|
112
|
+
notifyStateListeners() {
|
|
113
|
+
this.stateListeners.forEach((listener) => listener(this.authState));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Maps Firebase User to UserProfile interface
|
|
117
|
+
* @private
|
|
118
|
+
*/
|
|
119
|
+
mapFirebaseUserToProfile(user) {
|
|
120
|
+
return mapFirebaseUserToProfile(user);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Updates user data in Firestore with comprehensive profile information
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
async updateUserInFirestore(user) {
|
|
127
|
+
if (!this.firestore) {
|
|
128
|
+
throw new Error('Firestore instance not available');
|
|
129
|
+
}
|
|
130
|
+
await updateUserInFirestore(this.firestore, user);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Handles Firebase authentication errors and throws FirekitAuthError
|
|
134
|
+
* @private
|
|
135
|
+
*/
|
|
136
|
+
handleAuthError(error) {
|
|
137
|
+
handleAuthError(error);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Gets the current authentication state
|
|
141
|
+
* @returns {AuthState} Current authentication state
|
|
142
|
+
*/
|
|
143
|
+
getState() {
|
|
144
|
+
this.initializeServices();
|
|
145
|
+
return { ...this.authState };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Gets the current authenticated user
|
|
149
|
+
* @returns {User | null} Current Firebase user or null
|
|
150
|
+
*/
|
|
151
|
+
getCurrentUser() {
|
|
152
|
+
this.initializeServices();
|
|
153
|
+
return this.auth?.currentUser ?? null;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Gets the current user profile
|
|
157
|
+
* @returns {UserProfile | null} Current user profile or null
|
|
158
|
+
*/
|
|
159
|
+
getCurrentUserProfile() {
|
|
160
|
+
this.initializeServices();
|
|
161
|
+
return this.authState.user;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Waits for auth initialization to complete
|
|
165
|
+
* @returns {Promise<UserProfile | null>} Promise that resolves when auth is initialized
|
|
166
|
+
*/
|
|
167
|
+
async waitForAuth() {
|
|
168
|
+
this.initializeServices();
|
|
169
|
+
return new Promise((resolve) => {
|
|
170
|
+
if (this.authState.initialized) {
|
|
171
|
+
resolve(this.authState.user);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const unsubscribe = this.onAuthStateChanged((state) => {
|
|
175
|
+
if (state.initialized) {
|
|
176
|
+
unsubscribe();
|
|
177
|
+
resolve(state.user);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Subscribes to authentication state changes
|
|
184
|
+
* @param {Function} callback Callback function to handle state changes
|
|
185
|
+
* @returns {Function} Unsubscribe function
|
|
186
|
+
*/
|
|
187
|
+
onAuthStateChanged(callback) {
|
|
188
|
+
this.initializeServices();
|
|
189
|
+
this.stateListeners.add(callback);
|
|
190
|
+
// Immediately call with current state
|
|
191
|
+
callback(this.authState);
|
|
192
|
+
return () => {
|
|
193
|
+
this.stateListeners.delete(callback);
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// ========================================
|
|
197
|
+
// SIGN IN METHODS
|
|
198
|
+
// ========================================
|
|
199
|
+
/**
|
|
200
|
+
* Signs in user with email and password
|
|
201
|
+
* @param {string} email User's email address
|
|
202
|
+
* @param {string} password User's password
|
|
203
|
+
* @returns {Promise<UserProfile>} Promise resolving to user profile
|
|
204
|
+
* @throws {FirekitAuthError} If sign-in fails
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* ```typescript
|
|
208
|
+
* try {
|
|
209
|
+
* const user = await firekitAuth.signInWithEmail("user@example.com", "password123");
|
|
210
|
+
* console.log("Signed in:", user.displayName);
|
|
211
|
+
* } catch (error) {
|
|
212
|
+
* console.error("Sign-in failed:", error.message);
|
|
213
|
+
* }
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
async signInWithEmail(email, password) {
|
|
217
|
+
this.initializeServices();
|
|
218
|
+
if (!this.auth) {
|
|
219
|
+
throw new Error('Auth instance not available');
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
this.authState.loading = true;
|
|
223
|
+
this.notifyStateListeners();
|
|
224
|
+
const userCredential = await signInWithEmailAndPassword(this.auth, email, password);
|
|
225
|
+
await this.updateUserInFirestore(userCredential.user);
|
|
226
|
+
return this.mapFirebaseUserToProfile(userCredential.user);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
this.handleAuthError(error);
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
this.authState.loading = false;
|
|
233
|
+
this.notifyStateListeners();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Signs in user with Google popup
|
|
238
|
+
* @returns {Promise<UserProfile>} Promise resolving to user profile
|
|
239
|
+
* @throws {FirekitAuthError} If sign-in fails
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```typescript
|
|
243
|
+
* try {
|
|
244
|
+
* const user = await firekitAuth.signInWithGoogle();
|
|
245
|
+
* console.log("Signed in with Google:", user.email);
|
|
246
|
+
* } catch (error) {
|
|
247
|
+
* if (error.code === 'auth/popup-closed-by-user') {
|
|
248
|
+
* console.log("User cancelled sign-in");
|
|
249
|
+
* }
|
|
250
|
+
* }
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
async signInWithGoogle() {
|
|
254
|
+
this.initializeServices();
|
|
255
|
+
if (!this.auth) {
|
|
256
|
+
throw new Error('Auth instance not available');
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
this.authState.loading = true;
|
|
260
|
+
this.notifyStateListeners();
|
|
261
|
+
const provider = createGoogleProvider();
|
|
262
|
+
const result = await signInWithPopup(this.auth, provider);
|
|
263
|
+
await this.updateUserInFirestore(result.user);
|
|
264
|
+
return this.mapFirebaseUserToProfile(result.user);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
this.handleAuthError(error);
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
this.authState.loading = false;
|
|
271
|
+
this.notifyStateListeners();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Signs in user with Facebook popup
|
|
276
|
+
* @returns {Promise<UserProfile>} Promise resolving to user profile
|
|
277
|
+
* @throws {FirekitAuthError} If sign-in fails
|
|
278
|
+
*/
|
|
279
|
+
async signInWithFacebook() {
|
|
280
|
+
this.initializeServices();
|
|
281
|
+
if (!this.auth) {
|
|
282
|
+
throw new Error('Auth instance not available');
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
this.authState.loading = true;
|
|
286
|
+
this.notifyStateListeners();
|
|
287
|
+
const provider = createFacebookProvider();
|
|
288
|
+
const result = await signInWithPopup(this.auth, provider);
|
|
289
|
+
await this.updateUserInFirestore(result.user);
|
|
290
|
+
return this.mapFirebaseUserToProfile(result.user);
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
this.handleAuthError(error);
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
this.authState.loading = false;
|
|
297
|
+
this.notifyStateListeners();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Signs in user with Apple popup
|
|
302
|
+
* @returns {Promise<UserProfile>} Promise resolving to user profile
|
|
303
|
+
* @throws {FirekitAuthError} If sign-in fails
|
|
304
|
+
*/
|
|
305
|
+
async signInWithApple() {
|
|
306
|
+
this.initializeServices();
|
|
307
|
+
if (!this.auth) {
|
|
308
|
+
throw new Error('Auth instance not available');
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
this.authState.loading = true;
|
|
312
|
+
this.notifyStateListeners();
|
|
313
|
+
const provider = createAppleProvider();
|
|
314
|
+
const result = await signInWithPopup(this.auth, provider);
|
|
315
|
+
await this.updateUserInFirestore(result.user);
|
|
316
|
+
return this.mapFirebaseUserToProfile(result.user);
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
this.handleAuthError(error);
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
this.authState.loading = false;
|
|
323
|
+
this.notifyStateListeners();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Signs in user anonymously
|
|
328
|
+
* @returns {Promise<UserProfile>} Promise resolving to user profile
|
|
329
|
+
* @throws {FirekitAuthError} If sign-in fails
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* ```typescript
|
|
333
|
+
* const user = await firekitAuth.signInAnonymously();
|
|
334
|
+
* console.log("Anonymous user:", user.uid);
|
|
335
|
+
* ```
|
|
336
|
+
*/
|
|
337
|
+
async signInAnonymously() {
|
|
338
|
+
this.initializeServices();
|
|
339
|
+
if (!this.auth) {
|
|
340
|
+
throw new Error('Auth instance not available');
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
this.authState.loading = true;
|
|
344
|
+
this.notifyStateListeners();
|
|
345
|
+
const result = await firebaseSignInAnonymously(this.auth);
|
|
346
|
+
await this.updateUserInFirestore(result.user);
|
|
347
|
+
return this.mapFirebaseUserToProfile(result.user);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
this.handleAuthError(error);
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
this.authState.loading = false;
|
|
354
|
+
this.notifyStateListeners();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Initiates phone number sign-in process
|
|
359
|
+
* @param {string} phoneNumber Phone number in international format
|
|
360
|
+
* @param {string} recaptchaContainerId ID of the reCAPTCHA container element
|
|
361
|
+
* @returns {Promise<PhoneVerificationResult>} Promise resolving to verification result
|
|
362
|
+
* @throws {FirekitAuthError} If verification initiation fails
|
|
363
|
+
*
|
|
364
|
+
* @example
|
|
365
|
+
* ```typescript
|
|
366
|
+
* const verification = await firekitAuth.signInWithPhoneNumber("+1234567890", "recaptcha-container");
|
|
367
|
+
* const user = await verification.confirm("123456");
|
|
368
|
+
* ```
|
|
369
|
+
*/
|
|
370
|
+
async signInWithPhoneNumber(phoneNumber, recaptchaContainerId) {
|
|
371
|
+
this.initializeServices();
|
|
372
|
+
if (!this.auth) {
|
|
373
|
+
throw new Error('Auth instance not available');
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
this.authState.loading = true;
|
|
377
|
+
this.notifyStateListeners();
|
|
378
|
+
// Clean up existing verifier if any
|
|
379
|
+
const existingVerifier = this.recaptchaVerifiers.get(recaptchaContainerId);
|
|
380
|
+
if (existingVerifier) {
|
|
381
|
+
existingVerifier.clear();
|
|
382
|
+
this.recaptchaVerifiers.delete(recaptchaContainerId);
|
|
383
|
+
}
|
|
384
|
+
const recaptchaVerifier = new RecaptchaVerifier(this.auth, recaptchaContainerId, {
|
|
385
|
+
size: 'normal',
|
|
386
|
+
callback: () => {
|
|
387
|
+
console.log('reCAPTCHA solved');
|
|
388
|
+
},
|
|
389
|
+
'expired-callback': () => {
|
|
390
|
+
console.log('reCAPTCHA expired');
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
this.recaptchaVerifiers.set(recaptchaContainerId, recaptchaVerifier);
|
|
394
|
+
const confirmationResult = await firebaseSignInWithPhoneNumber(this.auth, phoneNumber, recaptchaVerifier);
|
|
395
|
+
return {
|
|
396
|
+
verificationId: confirmationResult.verificationId,
|
|
397
|
+
confirm: async (verificationCode) => {
|
|
398
|
+
try {
|
|
399
|
+
const userCredential = await confirmationResult.confirm(verificationCode);
|
|
400
|
+
await this.updateUserInFirestore(userCredential.user);
|
|
401
|
+
return this.mapFirebaseUserToProfile(userCredential.user);
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
this.handleAuthError(error);
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
// Clean up verifier after use
|
|
408
|
+
recaptchaVerifier.clear();
|
|
409
|
+
this.recaptchaVerifiers.delete(recaptchaContainerId);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
this.handleAuthError(error);
|
|
416
|
+
}
|
|
417
|
+
finally {
|
|
418
|
+
this.authState.loading = false;
|
|
419
|
+
this.notifyStateListeners();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// ========================================
|
|
423
|
+
// REGISTRATION METHODS
|
|
424
|
+
// ========================================
|
|
425
|
+
/**
|
|
426
|
+
* Registers new user with email and password
|
|
427
|
+
* @param {string} email User's email address
|
|
428
|
+
* @param {string} password User's password
|
|
429
|
+
* @param {string} [displayName] User's display name
|
|
430
|
+
* @param {boolean} [sendVerification=true] Whether to send email verification
|
|
431
|
+
* @returns {Promise<UserProfile>} Promise resolving to user profile
|
|
432
|
+
* @throws {FirekitAuthError} If registration fails
|
|
433
|
+
*
|
|
434
|
+
* @example
|
|
435
|
+
* ```typescript
|
|
436
|
+
* const user = await firekitAuth.registerWithEmail(
|
|
437
|
+
* "user@example.com",
|
|
438
|
+
* "password123",
|
|
439
|
+
* "John Doe"
|
|
440
|
+
* );
|
|
441
|
+
* console.log("Registered:", user.displayName);
|
|
442
|
+
* ```
|
|
443
|
+
*/
|
|
444
|
+
async registerWithEmail(email, password, displayName, sendVerification = true) {
|
|
445
|
+
this.initializeServices();
|
|
446
|
+
if (!this.auth) {
|
|
447
|
+
throw new Error('Auth instance not available');
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
this.authState.loading = true;
|
|
451
|
+
this.notifyStateListeners();
|
|
452
|
+
const userCredential = await createUserWithEmailAndPassword(this.auth, email, password);
|
|
453
|
+
const user = userCredential.user;
|
|
454
|
+
// Update profile if displayName provided
|
|
455
|
+
if (displayName) {
|
|
456
|
+
await updateProfile(user, { displayName });
|
|
457
|
+
}
|
|
458
|
+
// Send email verification
|
|
459
|
+
if (sendVerification) {
|
|
460
|
+
await sendEmailVerification(user);
|
|
461
|
+
}
|
|
462
|
+
await this.updateUserInFirestore(user);
|
|
463
|
+
return this.mapFirebaseUserToProfile(user);
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
this.handleAuthError(error);
|
|
467
|
+
}
|
|
468
|
+
finally {
|
|
469
|
+
this.authState.loading = false;
|
|
470
|
+
this.notifyStateListeners();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// ========================================
|
|
474
|
+
// PASSWORD METHODS
|
|
475
|
+
// ========================================
|
|
476
|
+
/**
|
|
477
|
+
* Sends password reset email
|
|
478
|
+
* @param {string} email User's email address
|
|
479
|
+
* @returns {Promise<void>} Promise that resolves when email is sent
|
|
480
|
+
* @throws {FirekitAuthError} If sending fails
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* ```typescript
|
|
484
|
+
* await firekitAuth.sendPasswordReset("user@example.com");
|
|
485
|
+
* console.log("Password reset email sent");
|
|
486
|
+
* ```
|
|
487
|
+
*/
|
|
488
|
+
async sendPasswordReset(email) {
|
|
489
|
+
this.initializeServices();
|
|
490
|
+
if (!this.auth) {
|
|
491
|
+
throw new Error('Auth instance not available');
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
await sendPasswordResetEmail(this.auth, email);
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
this.handleAuthError(error);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Confirms password reset with code
|
|
502
|
+
* @param {string} code Password reset code from email
|
|
503
|
+
* @param {string} newPassword New password
|
|
504
|
+
* @returns {Promise<void>} Promise that resolves when password is reset
|
|
505
|
+
* @throws {FirekitAuthError} If reset fails
|
|
506
|
+
*/
|
|
507
|
+
async confirmPasswordReset(code, newPassword) {
|
|
508
|
+
this.initializeServices();
|
|
509
|
+
if (!this.auth) {
|
|
510
|
+
throw new Error('Auth instance not available');
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
await confirmPasswordReset(this.auth, code, newPassword);
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
this.handleAuthError(error);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Updates user password with reauthentication
|
|
521
|
+
* @param {string} newPassword New password
|
|
522
|
+
* @param {string} currentPassword Current password for reauthentication
|
|
523
|
+
* @returns {Promise<PasswordUpdateResult>} Promise resolving to update result
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* ```typescript
|
|
527
|
+
* const result = await firekitAuth.updatePassword("newPassword123", "oldPassword123");
|
|
528
|
+
* if (result.success) {
|
|
529
|
+
* console.log("Password updated successfully");
|
|
530
|
+
* } else {
|
|
531
|
+
* console.error("Update failed:", result.message);
|
|
532
|
+
* }
|
|
533
|
+
* ```
|
|
534
|
+
*/
|
|
535
|
+
async updatePassword(newPassword, currentPassword) {
|
|
536
|
+
this.initializeServices();
|
|
537
|
+
if (!this.auth?.currentUser) {
|
|
538
|
+
return {
|
|
539
|
+
success: false,
|
|
540
|
+
message: 'No authenticated user found.',
|
|
541
|
+
code: 'auth/no-current-user'
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
await this.reauthenticateUser(currentPassword);
|
|
546
|
+
await updatePassword(this.auth.currentUser, newPassword);
|
|
547
|
+
return {
|
|
548
|
+
success: true,
|
|
549
|
+
message: 'Password successfully updated.'
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
const code = error.code;
|
|
554
|
+
if (code === AuthErrorCode.WRONG_PASSWORD) {
|
|
555
|
+
return {
|
|
556
|
+
success: false,
|
|
557
|
+
code,
|
|
558
|
+
message: 'Current password is incorrect.'
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
success: false,
|
|
563
|
+
code: code || 'unknown_error',
|
|
564
|
+
message: error.message || 'Failed to update password.'
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// ========================================
|
|
569
|
+
// PROFILE METHODS
|
|
570
|
+
// ========================================
|
|
571
|
+
/**
|
|
572
|
+
* Updates user profile
|
|
573
|
+
* @param {Object} profile Profile update data
|
|
574
|
+
* @param {string} [profile.displayName] New display name
|
|
575
|
+
* @param {string} [profile.photoURL] New photo URL
|
|
576
|
+
* @returns {Promise<void>} Promise that resolves when profile is updated
|
|
577
|
+
* @throws {FirekitAuthError} If update fails
|
|
578
|
+
*
|
|
579
|
+
* @example
|
|
580
|
+
* ```typescript
|
|
581
|
+
* await firekitAuth.updateUserProfile({
|
|
582
|
+
* displayName: "John Smith",
|
|
583
|
+
* photoURL: "https://example.com/photo.jpg"
|
|
584
|
+
* });
|
|
585
|
+
* ```
|
|
586
|
+
*/
|
|
587
|
+
async updateUserProfile(profile) {
|
|
588
|
+
this.initializeServices();
|
|
589
|
+
if (!this.auth?.currentUser) {
|
|
590
|
+
throw new FirekitAuthError('auth/no-current-user', 'No authenticated user found.');
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
await updateProfile(this.auth.currentUser, profile);
|
|
594
|
+
await this.updateUserInFirestore(this.auth.currentUser);
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
this.handleAuthError(error);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Updates user email address
|
|
602
|
+
* @param {string} newEmail New email address
|
|
603
|
+
* @returns {Promise<void>} Promise that resolves when email is updated
|
|
604
|
+
* @throws {FirekitAuthError} If update fails
|
|
605
|
+
*
|
|
606
|
+
* @example
|
|
607
|
+
* ```typescript
|
|
608
|
+
* await firekitAuth.updateEmail("newemail@example.com");
|
|
609
|
+
* ```
|
|
610
|
+
*/
|
|
611
|
+
async updateEmail(newEmail) {
|
|
612
|
+
this.initializeServices();
|
|
613
|
+
if (!this.auth?.currentUser) {
|
|
614
|
+
throw new FirekitAuthError('auth/no-current-user', 'No authenticated user found.');
|
|
615
|
+
}
|
|
616
|
+
try {
|
|
617
|
+
await updateEmail(this.auth.currentUser, newEmail);
|
|
618
|
+
await this.updateUserInFirestore(this.auth.currentUser);
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
this.handleAuthError(error);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Sends email verification to current user
|
|
626
|
+
* @returns {Promise<void>} Promise that resolves when verification email is sent
|
|
627
|
+
* @throws {FirekitAuthError} If sending fails
|
|
628
|
+
*
|
|
629
|
+
* @example
|
|
630
|
+
* ```typescript
|
|
631
|
+
* await firekitAuth.sendEmailVerification();
|
|
632
|
+
* ```
|
|
633
|
+
*/
|
|
634
|
+
async sendEmailVerification() {
|
|
635
|
+
this.initializeServices();
|
|
636
|
+
if (!this.auth?.currentUser) {
|
|
637
|
+
throw new FirekitAuthError('auth/no-current-user', 'No authenticated user found.');
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
await sendEmailVerification(this.auth.currentUser);
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
this.handleAuthError(error);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Reloads user to get updated data
|
|
648
|
+
* @returns {Promise<void>} Promise that resolves when user is reloaded
|
|
649
|
+
* @throws {FirekitAuthError} If reload fails
|
|
650
|
+
*
|
|
651
|
+
* @example
|
|
652
|
+
* ```typescript
|
|
653
|
+
* await firekitAuth.reloadUser();
|
|
654
|
+
* ```
|
|
655
|
+
*/
|
|
656
|
+
async reloadUser() {
|
|
657
|
+
this.initializeServices();
|
|
658
|
+
if (!this.auth?.currentUser) {
|
|
659
|
+
throw new FirekitAuthError('auth/no-current-user', 'No authenticated user found.');
|
|
660
|
+
}
|
|
661
|
+
try {
|
|
662
|
+
await reload(this.auth.currentUser);
|
|
663
|
+
await this.updateUserInFirestore(this.auth.currentUser);
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
this.handleAuthError(error);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Gets the current user's ID token
|
|
671
|
+
* @param {boolean} [forceRefresh=false] Whether to force token refresh
|
|
672
|
+
* @returns {Promise<string>} Promise resolving to ID token
|
|
673
|
+
* @throws {FirekitAuthError} If getting token fails
|
|
674
|
+
*
|
|
675
|
+
* @example
|
|
676
|
+
* ```typescript
|
|
677
|
+
* const token = await firekitAuth.getIdToken();
|
|
678
|
+
* ```
|
|
679
|
+
*/
|
|
680
|
+
async getIdToken(forceRefresh = false) {
|
|
681
|
+
this.initializeServices();
|
|
682
|
+
if (!this.auth?.currentUser) {
|
|
683
|
+
throw new FirekitAuthError('auth/no-current-user', 'No authenticated user found.');
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
return await getIdToken(this.auth.currentUser, forceRefresh);
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
this.handleAuthError(error);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Reauthenticates user with current password
|
|
694
|
+
* @private
|
|
695
|
+
*/
|
|
696
|
+
async reauthenticateUser(currentPassword) {
|
|
697
|
+
this.initializeServices();
|
|
698
|
+
if (!this.auth?.currentUser || !this.auth.currentUser.email) {
|
|
699
|
+
throw new FirekitAuthError('auth/no-current-user', 'No authenticated user with email found.');
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
const credential = EmailAuthProvider.credential(this.auth.currentUser.email, currentPassword);
|
|
703
|
+
await reauthenticateWithCredential(this.auth.currentUser, credential);
|
|
704
|
+
}
|
|
705
|
+
catch (error) {
|
|
706
|
+
this.handleAuthError(error);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Deletes user account
|
|
711
|
+
* @param {string} [currentPassword] Current password for reauthentication
|
|
712
|
+
* @returns {Promise<AccountDeletionResult>} Promise resolving to deletion result
|
|
713
|
+
*
|
|
714
|
+
* @example
|
|
715
|
+
* ```typescript
|
|
716
|
+
* const result = await firekitAuth.deleteAccount("currentPassword123");
|
|
717
|
+
* if (result.success) {
|
|
718
|
+
* console.log("Account deleted successfully");
|
|
719
|
+
* } else {
|
|
720
|
+
* console.error("Deletion failed:", result.message);
|
|
721
|
+
* }
|
|
722
|
+
* ```
|
|
723
|
+
*/
|
|
724
|
+
async deleteAccount(currentPassword) {
|
|
725
|
+
this.initializeServices();
|
|
726
|
+
if (!this.auth?.currentUser) {
|
|
727
|
+
return {
|
|
728
|
+
success: false,
|
|
729
|
+
message: 'No authenticated user found.'
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
try {
|
|
733
|
+
const user = this.auth.currentUser;
|
|
734
|
+
// Reauthenticate if password provided
|
|
735
|
+
if (currentPassword) {
|
|
736
|
+
await this.reauthenticateUser(currentPassword);
|
|
737
|
+
}
|
|
738
|
+
// Delete user data from Firestore first
|
|
739
|
+
try {
|
|
740
|
+
const userRef = doc(this.firestore, 'users', user.uid);
|
|
741
|
+
await setDoc(userRef, { deleted: true, deletedAt: serverTimestamp() }, { merge: true });
|
|
742
|
+
}
|
|
743
|
+
catch (firestoreError) {
|
|
744
|
+
console.warn('Failed to update Firestore before account deletion:', firestoreError);
|
|
745
|
+
}
|
|
746
|
+
// Delete the user account
|
|
747
|
+
await deleteUser(user);
|
|
748
|
+
return {
|
|
749
|
+
success: true,
|
|
750
|
+
message: 'Account successfully deleted.'
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
catch (error) {
|
|
754
|
+
return {
|
|
755
|
+
success: false,
|
|
756
|
+
message: error.message || 'Failed to delete account.'
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Signs out the current user
|
|
762
|
+
* @returns {Promise<void>} Promise that resolves when sign-out completes
|
|
763
|
+
* @throws {FirekitAuthError} If sign-out fails
|
|
764
|
+
*
|
|
765
|
+
* @example
|
|
766
|
+
* ```typescript
|
|
767
|
+
* await firekitAuth.signOut();
|
|
768
|
+
* console.log("User signed out");
|
|
769
|
+
* ```
|
|
770
|
+
*/
|
|
771
|
+
async signOut() {
|
|
772
|
+
this.initializeServices();
|
|
773
|
+
if (!this.auth) {
|
|
774
|
+
throw new Error('Auth instance not available');
|
|
775
|
+
}
|
|
776
|
+
try {
|
|
777
|
+
// Clear reCAPTCHA verifiers
|
|
778
|
+
this.recaptchaVerifiers.forEach((verifier) => verifier.clear());
|
|
779
|
+
this.recaptchaVerifiers.clear();
|
|
780
|
+
await signOut(this.auth);
|
|
781
|
+
}
|
|
782
|
+
catch (error) {
|
|
783
|
+
this.handleAuthError(error);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
// ========================================
|
|
787
|
+
// UTILITY METHODS
|
|
788
|
+
// ========================================
|
|
789
|
+
/**
|
|
790
|
+
* Checks if user is authenticated
|
|
791
|
+
* @returns {boolean} True if user is authenticated
|
|
792
|
+
*/
|
|
793
|
+
isAuthenticated() {
|
|
794
|
+
this.initializeServices();
|
|
795
|
+
return this.authState.user !== null && !this.authState.user.isAnonymous;
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Checks if user is anonymous
|
|
799
|
+
* @returns {boolean} True if user is anonymous
|
|
800
|
+
*/
|
|
801
|
+
isAnonymous() {
|
|
802
|
+
this.initializeServices();
|
|
803
|
+
return this.authState.user?.isAnonymous ?? false;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Checks if user's email is verified
|
|
807
|
+
* @returns {boolean} True if email is verified
|
|
808
|
+
*/
|
|
809
|
+
isEmailVerified() {
|
|
810
|
+
this.initializeServices();
|
|
811
|
+
return this.authState.user?.emailVerified ?? false;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Gets user's email address
|
|
815
|
+
* @returns {string | null} User's email or null
|
|
816
|
+
*/
|
|
817
|
+
getUserEmail() {
|
|
818
|
+
this.initializeServices();
|
|
819
|
+
return this.authState.user?.email ?? null;
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Gets user's display name
|
|
823
|
+
* @returns {string | null} User's display name or null
|
|
824
|
+
*/
|
|
825
|
+
getUserDisplayName() {
|
|
826
|
+
this.initializeServices();
|
|
827
|
+
return this.authState.user?.displayName ?? null;
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Gets user's photo URL
|
|
831
|
+
* @returns {string | null} User's photo URL or null
|
|
832
|
+
*/
|
|
833
|
+
getUserPhotoURL() {
|
|
834
|
+
this.initializeServices();
|
|
835
|
+
return this.authState.user?.photoURL ?? null;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Gets user's unique ID
|
|
839
|
+
* @returns {string | null} User's UID or null
|
|
840
|
+
*/
|
|
841
|
+
getUserId() {
|
|
842
|
+
this.initializeServices();
|
|
843
|
+
return this.authState.user?.uid ?? null;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Cleans up resources and listeners
|
|
847
|
+
* @returns {Promise<void>} Promise that resolves when cleanup completes
|
|
848
|
+
*/
|
|
849
|
+
async cleanup() {
|
|
850
|
+
this.initializeServices();
|
|
851
|
+
// Clear reCAPTCHA verifiers
|
|
852
|
+
this.recaptchaVerifiers.forEach((verifier) => verifier.clear());
|
|
853
|
+
this.recaptchaVerifiers.clear();
|
|
854
|
+
// Clear state listeners
|
|
855
|
+
this.stateListeners.clear();
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Pre-initialized singleton instance of FirekitAuth.
|
|
860
|
+
* This is the main export that should be used throughout your application.
|
|
861
|
+
*
|
|
862
|
+
* @example
|
|
863
|
+
* ```typescript
|
|
864
|
+
* import { firekitAuth } from 'svelte-firekit';
|
|
865
|
+
*
|
|
866
|
+
* // Sign in with email
|
|
867
|
+
* await firekitAuth.signInWithEmail("user@example.com", "password");
|
|
868
|
+
*
|
|
869
|
+
* // Listen to auth state
|
|
870
|
+
* const unsubscribe = firekitAuth.onAuthStateChanged((state) => {
|
|
871
|
+
* if (state.user) {
|
|
872
|
+
* console.log("User is signed in:", state.user.email);
|
|
873
|
+
* } else {
|
|
874
|
+
* console.log("User is signed out");
|
|
875
|
+
* }
|
|
876
|
+
* });
|
|
877
|
+
*
|
|
878
|
+
* // Clean up listener
|
|
879
|
+
* unsubscribe();
|
|
880
|
+
* ```
|
|
881
|
+
*/
|
|
882
|
+
export const firekitAuth = FirekitAuth.getInstance();
|