svelte-firekit 0.2.3 → 0.2.5

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.
@@ -45,9 +45,11 @@
45
45
  return firekitAuth.signOut();
46
46
  }
47
47
 
48
- // React to auth state changes runs whenever isAuthenticated or loading changes.
48
+ // Only react after Firebase Auth has fully initialized (first onAuthStateChanged callback).
49
+ // Uses `initialized` instead of `loading` because `loading` gets reused for profile
50
+ // updates and could cause false redirects mid-operation.
49
51
  $effect(() => {
50
- if (firekitUser.loading) return;
52
+ if (!firekitUser.initialized) return;
51
53
 
52
54
  const isAuth = firekitUser.isAuthenticated;
53
55
  const shouldBlock = requireAuth ? !isAuth : isAuth;
@@ -55,7 +57,7 @@
55
57
  });
56
58
  </script>
57
59
 
58
- {#if firekitUser.loading}
60
+ {#if !firekitUser.initialized}
59
61
  {#if fallback}
60
62
  {@render fallback()}
61
63
  {/if}
@@ -53,8 +53,11 @@
53
53
  }
54
54
  }
55
55
 
56
+ // Only react after Firebase Auth has fully initialized (first onAuthStateChanged callback).
57
+ // Uses `initialized` instead of `loading` because `loading` gets reused for profile
58
+ // updates and could cause false redirects mid-operation.
56
59
  $effect(() => {
57
- if (firekitUser.loading) return;
60
+ if (!firekitUser.initialized) return;
58
61
 
59
62
  const isAuth = firekitUser.isAuthenticated;
60
63
  const shouldBlock = requireAuth ? !isAuth : isAuth;
@@ -78,7 +81,7 @@
78
81
  });
79
82
  </script>
80
83
 
81
- {#if firekitUser.loading || isVerifying}
84
+ {#if !firekitUser.initialized || isVerifying}
82
85
  {#if fallback}
83
86
  {@render fallback()}
84
87
  {/if}
@@ -8,6 +8,7 @@
8
8
  import { firekitAppCheck } from '../services/app-check.svelte.js';
9
9
  import { firekitAnalytics } from '../services/analytics.js';
10
10
  import { firekitMessaging } from '../services/messaging.svelte.js';
11
+ import { firekitUser } from '../services/user.svelte.js';
11
12
 
12
13
  /**
13
14
  * Root provider component. Initializes Firebase (and optionally App Check) and
@@ -75,6 +76,9 @@
75
76
  setContext('firebase/app-check', firekitAppCheck.initialized ? firebaseService.appCheck : null);
76
77
  setContext('firebase/analytics', firekitAnalytics);
77
78
  setContext('firebase/messaging', firekitMessaging);
79
+
80
+ // Now that Firebase is configured, start the auth state listener.
81
+ firekitUser.initialize();
78
82
  }
79
83
  </script>
80
84
 
@@ -27,7 +27,7 @@
27
27
  } = $props();
28
28
  </script>
29
29
 
30
- {#if firekitUser.loading}
30
+ {#if !firekitUser.initialized}
31
31
  {#if fallback}
32
32
  {@render fallback()}
33
33
  {/if}
@@ -35,7 +35,7 @@
35
35
  }
36
36
  </script>
37
37
 
38
- {#if firekitUser.loading}
38
+ {#if !firekitUser.initialized}
39
39
  {#if fallback}
40
40
  {@render fallback()}
41
41
  {/if}
@@ -210,7 +210,19 @@ export class FirekitChat {
210
210
  // Seed history from initialHistory
211
211
  for (const turn of initialHistory) {
212
212
  const text = turn.parts
213
- .map((p) => ('text' in p ? p.text : ''))
213
+ .map((p) => {
214
+ if ('text' in p)
215
+ return p.text;
216
+ if ('inlineData' in p)
217
+ return '[image]';
218
+ if ('fileData' in p)
219
+ return '[file]';
220
+ if ('functionCall' in p)
221
+ return `[function: ${p.functionCall.name}]`;
222
+ if ('functionResponse' in p)
223
+ return `[function response: ${p.functionResponse.name}]`;
224
+ return '[unknown part]';
225
+ })
214
226
  .filter(Boolean)
215
227
  .join('');
216
228
  this._history.push({
@@ -41,7 +41,11 @@ class FirekitAnalytics {
41
41
  if (!supported)
42
42
  return;
43
43
  this._analytics = getAnalytics(getApp());
44
- })();
44
+ })().catch((err) => {
45
+ // Reset so next call retries instead of returning a rejected promise forever
46
+ this._initPromise = null;
47
+ throw err;
48
+ });
45
49
  return this._initPromise;
46
50
  }
47
51
  async _get() {
@@ -18,8 +18,8 @@ declare class FirekitAuth {
18
18
  private recaptchaVerifiers;
19
19
  private constructor();
20
20
  static getInstance(): FirekitAuth;
21
- private bootstrap;
22
21
  private getAuth;
22
+ private getFirestore;
23
23
  private syncToFirestore;
24
24
  private profile;
25
25
  signInWithEmail(email: string, password: string): Promise<SignInResult>;
@@ -20,9 +20,8 @@ class FirekitAuth {
20
20
  firestore = null;
21
21
  recaptchaVerifiers = new Map();
22
22
  constructor() {
23
- if (typeof window !== 'undefined') {
24
- this.bootstrap();
25
- }
23
+ // Do NOT bootstrap here — Firebase config may not be set yet.
24
+ // Auth and Firestore instances are resolved lazily via getAuth() / getFirestore().
26
25
  }
27
26
  static getInstance() {
28
27
  if (!FirekitAuth.instance) {
@@ -30,20 +29,6 @@ class FirekitAuth {
30
29
  }
31
30
  return FirekitAuth.instance;
32
31
  }
33
- bootstrap() {
34
- try {
35
- this.auth = firebaseService.getAuthInstance();
36
- try {
37
- this.firestore = firebaseService.getDbInstance();
38
- }
39
- catch {
40
- this.firestore = null;
41
- }
42
- }
43
- catch {
44
- // Firebase not yet configured — services will be accessed lazily
45
- }
46
- }
47
32
  getAuth() {
48
33
  if (!this.auth) {
49
34
  this.auth = firebaseService.getAuthInstance();
@@ -53,10 +38,22 @@ class FirekitAuth {
53
38
  }
54
39
  return this.auth;
55
40
  }
41
+ getFirestore() {
42
+ if (!this.firestore) {
43
+ try {
44
+ this.firestore = firebaseService.getDbInstance();
45
+ }
46
+ catch {
47
+ this.firestore = null;
48
+ }
49
+ }
50
+ return this.firestore;
51
+ }
56
52
  async syncToFirestore(user) {
57
- if (!this.firestore)
53
+ const fs = this.getFirestore();
54
+ if (!fs)
58
55
  return;
59
- await updateUserInFirestore(this.firestore, user);
56
+ await updateUserInFirestore(fs, user);
60
57
  }
61
58
  profile(user) {
62
59
  return mapFirebaseUserToProfile(user);
@@ -327,9 +324,10 @@ class FirekitAuth {
327
324
  if (currentPassword)
328
325
  await this.reauthenticate(currentPassword);
329
326
  // Soft-delete record in Firestore before removing auth
330
- if (this.firestore) {
327
+ const fs = this.getFirestore();
328
+ if (fs) {
331
329
  try {
332
- await setDoc(doc(this.firestore, 'users', user.uid), { deleted: true, deletedAt: serverTimestamp() }, { merge: true });
330
+ await setDoc(doc(fs, 'users', user.uid), { deleted: true, deletedAt: serverTimestamp() }, { merge: true });
333
331
  }
334
332
  catch {
335
333
  // Non-blocking
@@ -560,16 +558,22 @@ class FirekitAuth {
560
558
  }
561
559
  // ─── Utility getters ─────────────────────────────────────────────────────────
562
560
  getCurrentUser() {
563
- return this.auth?.currentUser ?? null;
561
+ try {
562
+ return this.getAuth().currentUser;
563
+ }
564
+ catch {
565
+ return null;
566
+ }
564
567
  }
565
568
  isAuthenticated() {
566
- return this.auth?.currentUser !== null && !this.auth?.currentUser?.isAnonymous;
569
+ const user = this.getCurrentUser();
570
+ return user !== null && !user.isAnonymous;
567
571
  }
568
572
  isAnonymous() {
569
- return this.auth?.currentUser?.isAnonymous ?? false;
573
+ return this.getCurrentUser()?.isAnonymous ?? false;
570
574
  }
571
575
  isEmailVerified() {
572
- return this.auth?.currentUser?.emailVerified ?? false;
576
+ return this.getCurrentUser()?.emailVerified ?? false;
573
577
  }
574
578
  async cleanup() {
575
579
  this.recaptchaVerifiers.forEach((v) => v.clear());
@@ -33,6 +33,7 @@ declare class FirekitMessaging {
33
33
  private _messaging;
34
34
  private _unsubscribeMessage;
35
35
  private _requesting;
36
+ private _listening;
36
37
  private constructor();
37
38
  static getInstance(): FirekitMessaging;
38
39
  get token(): string | null;
@@ -40,6 +40,7 @@ class FirekitMessaging {
40
40
  _messaging = null;
41
41
  _unsubscribeMessage = null;
42
42
  _requesting = false;
43
+ _listening = false;
43
44
  constructor() {
44
45
  this._initPermissionState();
45
46
  }
@@ -161,7 +162,10 @@ class FirekitMessaging {
161
162
  }
162
163
  // ── Foreground messages ───────────────────────────────────────────────────
163
164
  _listenForMessages(msg) {
165
+ if (this._listening)
166
+ return;
164
167
  this._unsubscribeMessage?.();
168
+ this._listening = true;
165
169
  this._unsubscribeMessage = onMessage(msg, (payload) => {
166
170
  this._lastMessage = payload;
167
171
  this._messages = [...this._messages, payload];
@@ -185,6 +189,7 @@ class FirekitMessaging {
185
189
  dispose() {
186
190
  this._unsubscribeMessage?.();
187
191
  this._unsubscribeMessage = null;
192
+ this._listening = false;
188
193
  }
189
194
  }
190
195
  export const firekitMessaging = FirekitMessaging.getInstance();
@@ -46,7 +46,7 @@ async function withRetry(fn, retryConfig) {
46
46
  }
47
47
  }
48
48
  }
49
- throw lastError;
49
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
50
50
  }
51
51
  /**
52
52
  * Firestore document mutations — add, set, update, delete, batch, transaction.
@@ -23,6 +23,7 @@ declare class FirekitPresence {
23
23
  private _geo;
24
24
  private _connectedUnsub;
25
25
  private _currentUser;
26
+ private _visibilityListenerAdded;
26
27
  private constructor();
27
28
  static getInstance(): FirekitPresence;
28
29
  get initialized(): boolean;
@@ -174,9 +174,11 @@ class FirekitPresence {
174
174
  _geo = null;
175
175
  _connectedUnsub = null;
176
176
  _currentUser = null;
177
+ _visibilityListenerAdded = false;
177
178
  constructor() {
178
179
  if (typeof window !== 'undefined') {
179
180
  document.addEventListener('visibilitychange', this._onVisibilityChange);
181
+ this._visibilityListenerAdded = true;
180
182
  }
181
183
  }
182
184
  static getInstance() {
@@ -236,10 +238,15 @@ class FirekitPresence {
236
238
  if (!db)
237
239
  throw new PresenceError(PresenceErrorCode.DATABASE_ERROR, 'Realtime Database is not initialized.');
238
240
  const connectedRef = ref(db, '.info/connected');
239
- this._connectedUnsub = onValue(connectedRef, async (snap) => {
241
+ this._connectedUnsub = onValue(connectedRef, (snap) => {
240
242
  if (snap.val() === true) {
241
- await this.setPresence('online');
242
- await this._setupDisconnectHandler();
243
+ this.setPresence('online')
244
+ .then(() => this._setupDisconnectHandler())
245
+ .catch((err) => {
246
+ this._error = err instanceof PresenceError
247
+ ? err
248
+ : new PresenceError(PresenceErrorCode.DATABASE_ERROR, err.message, err);
249
+ });
243
250
  }
244
251
  else {
245
252
  this._status = 'offline';
@@ -377,8 +384,9 @@ class FirekitPresence {
377
384
  this._geo?.dispose();
378
385
  this._connectedUnsub?.();
379
386
  this._connectedUnsub = null;
380
- if (typeof document !== 'undefined') {
387
+ if (this._visibilityListenerAdded) {
381
388
  document.removeEventListener('visibilitychange', this._onVisibilityChange);
389
+ this._visibilityListenerAdded = false;
382
390
  }
383
391
  this._initialized = false;
384
392
  this._status = 'offline';
@@ -92,6 +92,9 @@ export class FirekitRemoteConfig {
92
92
  },
93
93
  error: (err) => {
94
94
  this._error = err instanceof Error ? err : new Error(String(err));
95
+ // Clean up the broken subscription so it doesn't keep firing errors
96
+ this._unsubscribe?.();
97
+ this._unsubscribe = null;
95
98
  },
96
99
  complete: () => { }
97
100
  });
@@ -197,6 +197,7 @@ export class FirekitUploadTask {
197
197
  if (this._storageRef) {
198
198
  this._downloadURL = await getDownloadURL(this._storageRef);
199
199
  }
200
+ this._error = null;
200
201
  this._state = 'success';
201
202
  this._progress = 100;
202
203
  });
@@ -36,9 +36,20 @@ declare class FirekitUserStore {
36
36
  private _photoURL;
37
37
  private _uid;
38
38
  private _phoneNumber;
39
+ private _listening;
39
40
  private constructor();
40
41
  static getInstance(): FirekitUserStore;
41
- private bootstrap;
42
+ /**
43
+ * Called by FirebaseApp after initFirekit() to start the auth listener.
44
+ * Safe to call multiple times — only the first call has an effect.
45
+ */
46
+ initialize(): void;
47
+ /**
48
+ * Ensures the onAuthStateChanged listener is registered.
49
+ * Called lazily from initialize() or from any public getter/method
50
+ * so the store self-heals if Firebase was configured after import.
51
+ */
52
+ private ensureListening;
42
53
  private listenToAuthState;
43
54
  private syncToFirestore;
44
55
  private currentFirebaseUser;
@@ -66,9 +77,10 @@ declare class FirekitUserStore {
66
77
  updateExtendedData(data: Partial<ExtendedUserData>): Promise<void>;
67
78
  /**
68
79
  * Resolves once Firebase Auth has initialized (first `onAuthStateChanged` callback).
80
+ * Rejects after `timeoutMs` (default 10 000 ms) if auth never initializes.
69
81
  * Safe to call server-side — will resolve immediately with null.
70
82
  */
71
- waitForAuth(): Promise<UserProfile | null>;
83
+ waitForAuth(timeoutMs?: number): Promise<UserProfile | null>;
72
84
  clearError(): void;
73
85
  }
74
86
  export declare const firekitUser: FirekitUserStore;
@@ -34,10 +34,10 @@ class FirekitUserStore {
34
34
  _photoURL = $derived(this._user?.photoURL ?? null);
35
35
  _uid = $derived(this._user?.uid ?? null);
36
36
  _phoneNumber = $derived(this._user?.phoneNumber ?? null);
37
+ _listening = false;
37
38
  constructor() {
38
- if (typeof window !== 'undefined') {
39
- this.bootstrap();
40
- }
39
+ // Do NOT bootstrap here — Firebase config may not be set yet.
40
+ // Auth listener is set up lazily via initialize() or ensureListening().
41
41
  }
42
42
  static getInstance() {
43
43
  if (!FirekitUserStore.instance) {
@@ -45,7 +45,23 @@ class FirekitUserStore {
45
45
  }
46
46
  return FirekitUserStore.instance;
47
47
  }
48
- bootstrap() {
48
+ /**
49
+ * Called by FirebaseApp after initFirekit() to start the auth listener.
50
+ * Safe to call multiple times — only the first call has an effect.
51
+ */
52
+ initialize() {
53
+ if (typeof window === 'undefined')
54
+ return;
55
+ this.ensureListening();
56
+ }
57
+ /**
58
+ * Ensures the onAuthStateChanged listener is registered.
59
+ * Called lazily from initialize() or from any public getter/method
60
+ * so the store self-heals if Firebase was configured after import.
61
+ */
62
+ ensureListening() {
63
+ if (this._listening)
64
+ return;
49
65
  try {
50
66
  this.auth = firebaseService.getAuthInstance();
51
67
  try {
@@ -54,12 +70,11 @@ class FirekitUserStore {
54
70
  catch {
55
71
  this.firestore = null;
56
72
  }
73
+ this._listening = true;
57
74
  this.listenToAuthState();
58
75
  }
59
- catch (err) {
60
- this._error = err instanceof Error ? err : new Error(String(err));
61
- this._loading = false;
62
- this._initialized = true;
76
+ catch {
77
+ // Firebase not yet configured will retry on next access
63
78
  }
64
79
  }
65
80
  listenToAuthState() {
@@ -85,18 +100,20 @@ class FirekitUserStore {
85
100
  return validateCurrentUser(this.auth);
86
101
  }
87
102
  // ── Public getters (reactive) ────────────────────────────────────────────────
88
- get user() { return this._user; }
89
- get loading() { return this._loading; }
90
- get initialized() { return this._initialized; }
91
- get error() { return this._error; }
92
- get isAuthenticated() { return this._isAuthenticated; }
93
- get isAnonymous() { return this._isAnonymous; }
94
- get isEmailVerified() { return this._isEmailVerified; }
95
- get email() { return this._email; }
96
- get displayName() { return this._displayName; }
97
- get photoURL() { return this._photoURL; }
98
- get uid() { return this._uid; }
99
- get phoneNumber() { return this._phoneNumber; }
103
+ // Each getter calls ensureListening() so the auth listener is registered
104
+ // on first access, even if initialize() hasn't been called yet.
105
+ get user() { this.ensureListening(); return this._user; }
106
+ get loading() { this.ensureListening(); return this._loading; }
107
+ get initialized() { this.ensureListening(); return this._initialized; }
108
+ get error() { this.ensureListening(); return this._error; }
109
+ get isAuthenticated() { this.ensureListening(); return this._isAuthenticated; }
110
+ get isAnonymous() { this.ensureListening(); return this._isAnonymous; }
111
+ get isEmailVerified() { this.ensureListening(); return this._isEmailVerified; }
112
+ get email() { this.ensureListening(); return this._email; }
113
+ get displayName() { this.ensureListening(); return this._displayName; }
114
+ get photoURL() { this.ensureListening(); return this._photoURL; }
115
+ get uid() { this.ensureListening(); return this._uid; }
116
+ get phoneNumber() { this.ensureListening(); return this._phoneNumber; }
100
117
  // ── Profile updates ──────────────────────────────────────────────────────────
101
118
  async updateDisplayName(displayName) {
102
119
  const user = this.currentFirebaseUser();
@@ -253,17 +270,24 @@ class FirekitUserStore {
253
270
  // ── Utility ──────────────────────────────────────────────────────────────────
254
271
  /**
255
272
  * Resolves once Firebase Auth has initialized (first `onAuthStateChanged` callback).
273
+ * Rejects after `timeoutMs` (default 10 000 ms) if auth never initializes.
256
274
  * Safe to call server-side — will resolve immediately with null.
257
275
  */
258
- waitForAuth() {
276
+ waitForAuth(timeoutMs = 10_000) {
277
+ if (typeof window === 'undefined')
278
+ return Promise.resolve(null);
279
+ this.ensureListening();
259
280
  if (this._initialized)
260
281
  return Promise.resolve(this._user);
261
- // $effect.root creates a reactive scope outside of component initialization,
262
- // so this works safely whether called inside or outside a Svelte component.
263
- return new Promise((resolve) => {
282
+ return new Promise((resolve, reject) => {
283
+ const timer = setTimeout(() => {
284
+ stop();
285
+ reject(new Error('waitForAuth timed out — Firebase Auth did not initialize.'));
286
+ }, timeoutMs);
264
287
  const stop = $effect.root(() => {
265
288
  $effect(() => {
266
289
  if (this._initialized) {
290
+ clearTimeout(timer);
267
291
  stop();
268
292
  resolve(this._user);
269
293
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-firekit",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "A Svelte library for Firebase integration",
5
5
  "license": "MIT",
6
6
  "author": "Giovani Rodriguez",