svelte-firekit 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,8 +31,8 @@
31
31
  onUnauthorized,
32
32
  fallback
33
33
  }: {
34
- /** Content shown when the auth requirement is satisfied. */
35
- children: Snippet<[UserProfile, () => Promise<void>]>;
34
+ /** Content shown when the auth requirement is satisfied. User is null when requireAuth is false. */
35
+ children: Snippet<[UserProfile | null, () => Promise<void>]>;
36
36
  /** When true (default), requires a signed-in user. When false, requires no user. */
37
37
  requireAuth?: boolean;
38
38
  /** Called when the auth state does not meet the requirement. Use for navigation. */
@@ -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,10 +57,10 @@
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}
62
- {:else if firekitUser.isAuthenticated === requireAuth && firekitUser.user}
64
+ {:else if firekitUser.isAuthenticated === requireAuth}
63
65
  {@render children(firekitUser.user, signOut)}
64
66
  {/if}
@@ -1,8 +1,8 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { UserProfile } from '../types/auth.js';
3
3
  type $$ComponentProps = {
4
- /** Content shown when the auth requirement is satisfied. */
5
- children: Snippet<[UserProfile, () => Promise<void>]>;
4
+ /** Content shown when the auth requirement is satisfied. User is null when requireAuth is false. */
5
+ children: Snippet<[UserProfile | null, () => Promise<void>]>;
6
6
  /** When true (default), requires a signed-in user. When false, requires no user. */
7
7
  requireAuth?: boolean;
8
8
  /** Called when the auth state does not meet the requirement. Use for navigation. */
@@ -28,7 +28,7 @@
28
28
  fallback,
29
29
  verificationChecks = []
30
30
  }: {
31
- children: Snippet<[UserProfile, () => Promise<void>]>;
31
+ children: Snippet<[UserProfile | null, () => Promise<void>]>;
32
32
  requireAuth?: boolean;
33
33
  onUnauthorized?: () => void;
34
34
  fallback?: Snippet;
@@ -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,10 +81,10 @@
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}
85
- {:else if firekitUser.isAuthenticated === requireAuth && verificationPassed && firekitUser.user}
88
+ {:else if firekitUser.isAuthenticated === requireAuth && verificationPassed}
86
89
  {@render children(firekitUser.user, signOut)}
87
90
  {/if}
@@ -1,7 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { UserProfile } from '../types/auth.js';
3
3
  type $$ComponentProps = {
4
- children: Snippet<[UserProfile, () => Promise<void>]>;
4
+ children: Snippet<[UserProfile | null, () => Promise<void>]>;
5
5
  requireAuth?: boolean;
6
6
  onUnauthorized?: () => void;
7
7
  fallback?: Snippet;
@@ -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
 
@@ -5,18 +5,32 @@
5
5
 
6
6
  /**
7
7
  * Renders `children` only when a non-anonymous user is signed in.
8
- * Uses runes no subscription needed.
8
+ * Optionally renders a `fallback` while auth state is loading.
9
9
  *
10
10
  * @example
11
11
  * <SignedIn>
12
12
  * {#snippet children(user)}
13
13
  * <p>Hello {user.displayName}</p>
14
14
  * {/snippet}
15
+ * {#snippet fallback()}
16
+ * <p>Loading…</p>
17
+ * {/snippet}
15
18
  * </SignedIn>
16
19
  */
17
- let { children }: { children: Snippet<[UserProfile]> } = $props();
20
+ let {
21
+ children,
22
+ fallback
23
+ }: {
24
+ children: Snippet<[UserProfile]>;
25
+ /** Shown while auth state is being determined. */
26
+ fallback?: Snippet;
27
+ } = $props();
18
28
  </script>
19
29
 
20
- {#if firekitUser.isAuthenticated && firekitUser.user}
30
+ {#if !firekitUser.initialized}
31
+ {#if fallback}
32
+ {@render fallback()}
33
+ {/if}
34
+ {:else if firekitUser.isAuthenticated && firekitUser.user}
21
35
  {@render children(firekitUser.user)}
22
36
  {/if}
@@ -2,6 +2,8 @@ import type { Snippet } from 'svelte';
2
2
  import type { UserProfile } from '../types/auth.js';
3
3
  type $$ComponentProps = {
4
4
  children: Snippet<[UserProfile]>;
5
+ /** Shown while auth state is being determined. */
6
+ fallback?: Snippet;
5
7
  };
6
8
  declare const SignedIn: import("svelte").Component<$$ComponentProps, {}, "">;
7
9
  type SignedIn = ReturnType<typeof SignedIn>;
@@ -3,23 +3,31 @@
3
3
  import { firekitUser } from '../services/user.svelte.js';
4
4
 
5
5
  /**
6
- * Renders `children` only when no authenticated user exists (including during loading).
7
- * Passes a sign-in trigger function to children wire it up to your auth method of choice.
6
+ * Renders `children` only when no authenticated user exists.
7
+ * Waits for auth to initialize before rendering to avoid flashing
8
+ * a login form to users who are already signed in.
9
+ * Optionally renders a `fallback` while auth state is loading.
8
10
  *
9
11
  * @example
10
12
  * <SignedOut>
11
13
  * {#snippet children(signIn)}
12
14
  * <button onclick={signIn}>Sign in with Google</button>
13
15
  * {/snippet}
16
+ * {#snippet fallback()}
17
+ * <p>Checking auth…</p>
18
+ * {/snippet}
14
19
  * </SignedOut>
15
20
  */
16
21
  let {
17
22
  children,
18
- onSignIn
23
+ onSignIn,
24
+ fallback
19
25
  }: {
20
26
  children: Snippet<[() => void]>;
21
27
  /** Optional callback invoked when the user triggers sign-in from the snippet. */
22
28
  onSignIn?: () => void;
29
+ /** Shown while auth state is being determined. */
30
+ fallback?: Snippet;
23
31
  } = $props();
24
32
 
25
33
  function triggerSignIn() {
@@ -27,6 +35,10 @@
27
35
  }
28
36
  </script>
29
37
 
30
- {#if !firekitUser.isAuthenticated && !firekitUser.loading}
38
+ {#if !firekitUser.initialized}
39
+ {#if fallback}
40
+ {@render fallback()}
41
+ {/if}
42
+ {:else if !firekitUser.isAuthenticated}
31
43
  {@render children(triggerSignIn)}
32
44
  {/if}
@@ -3,6 +3,8 @@ type $$ComponentProps = {
3
3
  children: Snippet<[() => void]>;
4
4
  /** Optional callback invoked when the user triggers sign-in from the snippet. */
5
5
  onSignIn?: () => void;
6
+ /** Shown while auth state is being determined. */
7
+ fallback?: Snippet;
6
8
  };
7
9
  declare const SignedOut: import("svelte").Component<$$ComponentProps, {}, "">;
8
10
  type SignedOut = ReturnType<typeof SignedOut>;
@@ -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.2",
3
+ "version": "0.2.4",
4
4
  "description": "A Svelte library for Firebase integration",
5
5
  "license": "MIT",
6
6
  "author": "Giovani Rodriguez",