@neetru/sdk 3.0.2 → 3.1.0

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.
Files changed (60) hide show
  1. package/CHANGELOG.md +554 -530
  2. package/dist/auth.cjs +60 -39
  3. package/dist/auth.cjs.map +1 -1
  4. package/dist/auth.d.cts +1 -2
  5. package/dist/auth.d.ts +1 -2
  6. package/dist/auth.mjs +60 -39
  7. package/dist/auth.mjs.map +1 -1
  8. package/dist/catalog.d.cts +1 -2
  9. package/dist/catalog.d.ts +1 -2
  10. package/dist/checkout.d.cts +1 -2
  11. package/dist/checkout.d.ts +1 -2
  12. package/dist/db-react.cjs +15 -5
  13. package/dist/db-react.cjs.map +1 -1
  14. package/dist/db-react.d.cts +35 -13
  15. package/dist/db-react.d.ts +35 -13
  16. package/dist/db-react.mjs +15 -5
  17. package/dist/db-react.mjs.map +1 -1
  18. package/dist/db.cjs +71 -50
  19. package/dist/db.cjs.map +1 -1
  20. package/dist/db.d.cts +1 -2
  21. package/dist/db.d.ts +1 -2
  22. package/dist/db.mjs +71 -50
  23. package/dist/db.mjs.map +1 -1
  24. package/dist/entitlements.d.cts +1 -2
  25. package/dist/entitlements.d.ts +1 -2
  26. package/dist/firestore-compat.cjs +399 -0
  27. package/dist/firestore-compat.cjs.map +1 -0
  28. package/dist/firestore-compat.d.cts +294 -0
  29. package/dist/firestore-compat.d.ts +294 -0
  30. package/dist/firestore-compat.mjs +371 -0
  31. package/dist/firestore-compat.mjs.map +1 -0
  32. package/dist/index.cjs +72 -51
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +2 -3
  35. package/dist/index.d.ts +2 -3
  36. package/dist/index.mjs +72 -51
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/mocks.d.cts +1 -2
  39. package/dist/mocks.d.ts +1 -2
  40. package/dist/notifications.d.cts +1 -2
  41. package/dist/notifications.d.ts +1 -2
  42. package/dist/react.cjs +267 -0
  43. package/dist/react.cjs.map +1 -1
  44. package/dist/react.d.cts +117 -3
  45. package/dist/react.d.ts +117 -3
  46. package/dist/react.mjs +265 -1
  47. package/dist/react.mjs.map +1 -1
  48. package/dist/support.d.cts +1 -2
  49. package/dist/support.d.ts +1 -2
  50. package/dist/telemetry.d.cts +1 -2
  51. package/dist/telemetry.d.ts +1 -2
  52. package/dist/{types-D7zVkO3n.d.ts → types-BRv8wBxX.d.ts} +581 -2
  53. package/dist/{types-Dc4bjrrt.d.cts → types-nwErcRX8.d.cts} +581 -2
  54. package/dist/usage.d.cts +1 -2
  55. package/dist/usage.d.ts +1 -2
  56. package/dist/webhooks.d.cts +1 -2
  57. package/dist/webhooks.d.ts +1 -2
  58. package/package.json +156 -151
  59. package/dist/collection-ref-BDdfD87k.d.cts +0 -581
  60. package/dist/collection-ref-BDdfD87k.d.ts +0 -581
package/dist/react.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
- import { N as NeetruClient, g as CheckoutStartResult } from './types-Dc4bjrrt.cjs';
3
- import './collection-ref-BDdfD87k.cjs';
2
+ import { z as NeetruClient, g as CheckoutStartResult, A as AuthNamespace, T as NeetruUser } from './types-nwErcRX8.cjs';
3
+ import { CollectionReference, Query, DocumentReference } from './firestore-compat.cjs';
4
4
  import 'drizzle-orm/node-postgres';
5
5
  import './errors.cjs';
6
6
 
@@ -111,5 +111,119 @@ interface EntitlementGateProps {
111
111
  * deve preferir `mode='readonly'` pra preservar engajamento (não hard-block).
112
112
  */
113
113
  declare function EntitlementGate({ client, feature, productSlug, mode, fallback, loading, children, }: EntitlementGateProps): React.ReactElement;
114
+ /** Resultado de `useCollection`. */
115
+ interface UseCollectionResult<T> {
116
+ /** Documentos (cada um com `id` embutido) ou `null` enquanto carrega/sem ref. */
117
+ data: Array<T & {
118
+ id: string;
119
+ }> | null;
120
+ /** `true` enquanto o primeiro snapshot não chegou. */
121
+ isLoading: boolean;
122
+ /** Erro do último refresh (dados exibidos podem estar defasados). */
123
+ error: Error | null;
124
+ /** `true` quando um refresh falhou ou o snapshot veio do cache/offline. */
125
+ isStale: boolean;
126
+ /** epoch ms do último snapshot bem-sucedido, ou `null`. */
127
+ lastUpdated: number | null;
128
+ }
129
+ /** Resultado de `useDoc`. */
130
+ interface UseDocResult<T> {
131
+ data: (T & {
132
+ id: string;
133
+ }) | null;
134
+ isLoading: boolean;
135
+ error: Error | null;
136
+ isStale: boolean;
137
+ lastUpdated: number | null;
138
+ }
139
+ /** Opções comuns dos hooks de dados. */
140
+ interface UseDataOptions {
141
+ /**
142
+ * Intervalo de polling de fallback em ms (quando o transporte não oferece
143
+ * realtime nativo). Default 5000. O polling pausa quando a aba está oculta.
144
+ */
145
+ intervalMs?: number;
146
+ /**
147
+ * Força polling mesmo quando o `onSnapshot` nativo está disponível. Útil para
148
+ * transportes REST onde `onSnapshot` só entrega o snapshot inicial.
149
+ * Default: `false` (usa realtime quando o transporte oferece).
150
+ */
151
+ poll?: boolean;
152
+ }
153
+ /**
154
+ * Hook de tempo real para uma coleção (ou subconjunto filtrado via `query`).
155
+ *
156
+ * Aceita uma `CollectionReference` ou `Query` do `@neetru/sdk/firestore-compat`.
157
+ * Subscreve ao `onSnapshot` nativo do `client.db`; quando o transporte é REST
158
+ * (sem realtime nativo), faz polling visibility-aware no `intervalMs`.
159
+ *
160
+ * ```tsx
161
+ * import { collection, query, where } from '@neetru/sdk/firestore-compat';
162
+ * import { useCollection } from '@neetru/sdk/react';
163
+ *
164
+ * const ref = useMemo(
165
+ * () => query(collection(client.db, 'orders'), where('status', '==', 'open')),
166
+ * [],
167
+ * );
168
+ * const { data, isLoading, error, isStale } = useCollection<Order>(ref);
169
+ * ```
170
+ *
171
+ * Seguro em React 18/19 StrictMode.
172
+ */
173
+ declare function useCollection<T = Record<string, unknown>>(source: CollectionReference<T> | Query<T> | null | undefined, options?: UseDataOptions): UseCollectionResult<T>;
174
+ /**
175
+ * Hook de tempo real para um documento.
176
+ *
177
+ * Aceita uma `DocumentReference` do `@neetru/sdk/firestore-compat`.
178
+ *
179
+ * ```tsx
180
+ * import { doc } from '@neetru/sdk/firestore-compat';
181
+ * import { useDoc } from '@neetru/sdk/react';
182
+ *
183
+ * const ref = useMemo(() => doc(client.db, 'profiles', uid), [uid]);
184
+ * const { data, isLoading } = useDoc<Profile>(ref);
185
+ * ```
186
+ *
187
+ * Seguro em React 18/19 StrictMode.
188
+ */
189
+ declare function useDoc<T = Record<string, unknown>>(ref: DocumentReference<T> | null | undefined, options?: UseDataOptions): UseDocResult<T>;
190
+ /**
191
+ * Fonte de auth aceita por `useUser`: o `NeetruClient` inteiro (resolve
192
+ * `client.auth`) ou o `AuthNamespace` direto.
193
+ */
194
+ type UseUserInput = NeetruClient | {
195
+ auth: AuthNamespace;
196
+ } | AuthNamespace;
197
+ /** Resultado de `useUser`. */
198
+ interface UseUserResult {
199
+ /** Usuário autenticado ou `null`. */
200
+ user: NeetruUser | null;
201
+ /** `true` enquanto o estado de auth inicial não foi resolvido. */
202
+ isLoading: boolean;
203
+ /** Erro na resolução de auth (raro — listener nunca deveria lançar). */
204
+ error: Error | null;
205
+ }
206
+ /**
207
+ * Hook de estado de autenticação.
208
+ *
209
+ * Subscreve a `client.auth.onAuthStateChanged` e devolve `{ user, isLoading,
210
+ * error }`. Inclui um **timeout de seed** (default 8s): se o estado de auth não
211
+ * resolver nesse prazo, `isLoading` cai para `false` com `user: null` — evita
212
+ * spinners infinitos quando o provedor de auth nunca dispara o callback.
213
+ *
214
+ * ```tsx
215
+ * import { useUser } from '@neetru/sdk/react';
216
+ * const { user, isLoading } = useUser(client);
217
+ * if (isLoading) return <Spinner />;
218
+ * if (!user) return <SignInButton />;
219
+ * return <Avatar user={user} />;
220
+ * ```
221
+ *
222
+ * Aceita o `NeetruClient`, `{ auth }` ou o `AuthNamespace` direto.
223
+ * Seguro em React 18/19 StrictMode.
224
+ */
225
+ declare function useUser(input: UseUserInput, options?: {
226
+ timeoutMs?: number;
227
+ }): UseUserResult;
114
228
 
115
- export { CheckoutLink, type CheckoutLinkProps, EntitlementGate, type EntitlementGateMode, type EntitlementGateProps, useEntitlementContext };
229
+ export { CheckoutLink, type CheckoutLinkProps, EntitlementGate, type EntitlementGateMode, type EntitlementGateProps, type UseCollectionResult, type UseDataOptions, type UseDocResult, type UseUserInput, type UseUserResult, useCollection, useDoc, useEntitlementContext, useUser };
package/dist/react.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
- import { N as NeetruClient, g as CheckoutStartResult } from './types-D7zVkO3n.js';
3
- import './collection-ref-BDdfD87k.js';
2
+ import { z as NeetruClient, g as CheckoutStartResult, A as AuthNamespace, T as NeetruUser } from './types-BRv8wBxX.js';
3
+ import { CollectionReference, Query, DocumentReference } from './firestore-compat.js';
4
4
  import 'drizzle-orm/node-postgres';
5
5
  import './errors.js';
6
6
 
@@ -111,5 +111,119 @@ interface EntitlementGateProps {
111
111
  * deve preferir `mode='readonly'` pra preservar engajamento (não hard-block).
112
112
  */
113
113
  declare function EntitlementGate({ client, feature, productSlug, mode, fallback, loading, children, }: EntitlementGateProps): React.ReactElement;
114
+ /** Resultado de `useCollection`. */
115
+ interface UseCollectionResult<T> {
116
+ /** Documentos (cada um com `id` embutido) ou `null` enquanto carrega/sem ref. */
117
+ data: Array<T & {
118
+ id: string;
119
+ }> | null;
120
+ /** `true` enquanto o primeiro snapshot não chegou. */
121
+ isLoading: boolean;
122
+ /** Erro do último refresh (dados exibidos podem estar defasados). */
123
+ error: Error | null;
124
+ /** `true` quando um refresh falhou ou o snapshot veio do cache/offline. */
125
+ isStale: boolean;
126
+ /** epoch ms do último snapshot bem-sucedido, ou `null`. */
127
+ lastUpdated: number | null;
128
+ }
129
+ /** Resultado de `useDoc`. */
130
+ interface UseDocResult<T> {
131
+ data: (T & {
132
+ id: string;
133
+ }) | null;
134
+ isLoading: boolean;
135
+ error: Error | null;
136
+ isStale: boolean;
137
+ lastUpdated: number | null;
138
+ }
139
+ /** Opções comuns dos hooks de dados. */
140
+ interface UseDataOptions {
141
+ /**
142
+ * Intervalo de polling de fallback em ms (quando o transporte não oferece
143
+ * realtime nativo). Default 5000. O polling pausa quando a aba está oculta.
144
+ */
145
+ intervalMs?: number;
146
+ /**
147
+ * Força polling mesmo quando o `onSnapshot` nativo está disponível. Útil para
148
+ * transportes REST onde `onSnapshot` só entrega o snapshot inicial.
149
+ * Default: `false` (usa realtime quando o transporte oferece).
150
+ */
151
+ poll?: boolean;
152
+ }
153
+ /**
154
+ * Hook de tempo real para uma coleção (ou subconjunto filtrado via `query`).
155
+ *
156
+ * Aceita uma `CollectionReference` ou `Query` do `@neetru/sdk/firestore-compat`.
157
+ * Subscreve ao `onSnapshot` nativo do `client.db`; quando o transporte é REST
158
+ * (sem realtime nativo), faz polling visibility-aware no `intervalMs`.
159
+ *
160
+ * ```tsx
161
+ * import { collection, query, where } from '@neetru/sdk/firestore-compat';
162
+ * import { useCollection } from '@neetru/sdk/react';
163
+ *
164
+ * const ref = useMemo(
165
+ * () => query(collection(client.db, 'orders'), where('status', '==', 'open')),
166
+ * [],
167
+ * );
168
+ * const { data, isLoading, error, isStale } = useCollection<Order>(ref);
169
+ * ```
170
+ *
171
+ * Seguro em React 18/19 StrictMode.
172
+ */
173
+ declare function useCollection<T = Record<string, unknown>>(source: CollectionReference<T> | Query<T> | null | undefined, options?: UseDataOptions): UseCollectionResult<T>;
174
+ /**
175
+ * Hook de tempo real para um documento.
176
+ *
177
+ * Aceita uma `DocumentReference` do `@neetru/sdk/firestore-compat`.
178
+ *
179
+ * ```tsx
180
+ * import { doc } from '@neetru/sdk/firestore-compat';
181
+ * import { useDoc } from '@neetru/sdk/react';
182
+ *
183
+ * const ref = useMemo(() => doc(client.db, 'profiles', uid), [uid]);
184
+ * const { data, isLoading } = useDoc<Profile>(ref);
185
+ * ```
186
+ *
187
+ * Seguro em React 18/19 StrictMode.
188
+ */
189
+ declare function useDoc<T = Record<string, unknown>>(ref: DocumentReference<T> | null | undefined, options?: UseDataOptions): UseDocResult<T>;
190
+ /**
191
+ * Fonte de auth aceita por `useUser`: o `NeetruClient` inteiro (resolve
192
+ * `client.auth`) ou o `AuthNamespace` direto.
193
+ */
194
+ type UseUserInput = NeetruClient | {
195
+ auth: AuthNamespace;
196
+ } | AuthNamespace;
197
+ /** Resultado de `useUser`. */
198
+ interface UseUserResult {
199
+ /** Usuário autenticado ou `null`. */
200
+ user: NeetruUser | null;
201
+ /** `true` enquanto o estado de auth inicial não foi resolvido. */
202
+ isLoading: boolean;
203
+ /** Erro na resolução de auth (raro — listener nunca deveria lançar). */
204
+ error: Error | null;
205
+ }
206
+ /**
207
+ * Hook de estado de autenticação.
208
+ *
209
+ * Subscreve a `client.auth.onAuthStateChanged` e devolve `{ user, isLoading,
210
+ * error }`. Inclui um **timeout de seed** (default 8s): se o estado de auth não
211
+ * resolver nesse prazo, `isLoading` cai para `false` com `user: null` — evita
212
+ * spinners infinitos quando o provedor de auth nunca dispara o callback.
213
+ *
214
+ * ```tsx
215
+ * import { useUser } from '@neetru/sdk/react';
216
+ * const { user, isLoading } = useUser(client);
217
+ * if (isLoading) return <Spinner />;
218
+ * if (!user) return <SignInButton />;
219
+ * return <Avatar user={user} />;
220
+ * ```
221
+ *
222
+ * Aceita o `NeetruClient`, `{ auth }` ou o `AuthNamespace` direto.
223
+ * Seguro em React 18/19 StrictMode.
224
+ */
225
+ declare function useUser(input: UseUserInput, options?: {
226
+ timeoutMs?: number;
227
+ }): UseUserResult;
114
228
 
115
- export { CheckoutLink, type CheckoutLinkProps, EntitlementGate, type EntitlementGateMode, type EntitlementGateProps, useEntitlementContext };
229
+ export { CheckoutLink, type CheckoutLinkProps, EntitlementGate, type EntitlementGateMode, type EntitlementGateProps, type UseCollectionResult, type UseDataOptions, type UseDocResult, type UseUserInput, type UseUserResult, useCollection, useDoc, useEntitlementContext, useUser };
package/dist/react.mjs CHANGED
@@ -1,5 +1,34 @@
1
1
  import * as React from 'react';
2
2
 
3
+ // src/react.ts
4
+
5
+ // src/firestore-compat.ts
6
+ function toDbQuery(constraints) {
7
+ const whereFilters = [];
8
+ let orderByClause;
9
+ let limitValue;
10
+ for (const c of constraints) {
11
+ if (c.__constraint === "where") {
12
+ const w = c;
13
+ whereFilters.push({ field: w.field, op: normalizeOp(w.op), value: w.value });
14
+ } else if (c.__constraint === "orderBy") {
15
+ const o = c;
16
+ orderByClause = { field: o.field, direction: o.direction };
17
+ } else if (c.__constraint === "limit") {
18
+ limitValue = c.value;
19
+ }
20
+ }
21
+ const out = {};
22
+ if (whereFilters.length > 0) out.where = whereFilters;
23
+ if (orderByClause) out.orderBy = orderByClause;
24
+ if (limitValue !== void 0) out.limit = limitValue;
25
+ return out;
26
+ }
27
+ function normalizeOp(op) {
28
+ if (op === "array-contains-any") return "array-contains";
29
+ return op;
30
+ }
31
+
3
32
  // src/react.ts
4
33
  function CheckoutLink({
5
34
  client,
@@ -132,7 +161,242 @@ function EntitlementGate({
132
161
  children
133
162
  );
134
163
  }
164
+ var DEFAULT_INTERVAL_MS = 5e3;
165
+ function isDocumentHidden() {
166
+ return typeof document !== "undefined" && document.hidden === true;
167
+ }
168
+ function serializeSource(source) {
169
+ if (!source) return null;
170
+ if (source.__kind === "collection") {
171
+ return `collection:${source.id}`;
172
+ }
173
+ const dbQuery = toDbQuery(source.constraints);
174
+ return `query:${source.collection}:${JSON.stringify(dbQuery)}`;
175
+ }
176
+ function rowsFromList(snap) {
177
+ return snap.docs.map((d) => {
178
+ const data = d.data;
179
+ const { id: _omit, ...rest } = data;
180
+ return { ...rest, id: d.id };
181
+ });
182
+ }
183
+ function rowFromGet(snap, id) {
184
+ const first = snap?.docs[0];
185
+ if (!first) return null;
186
+ const data = first.data;
187
+ const { id: _omit, ...rest } = data;
188
+ return { ...rest, id };
189
+ }
190
+ function useCollection(source, options) {
191
+ const intervalMs = options?.intervalMs ?? DEFAULT_INTERVAL_MS;
192
+ const poll = options?.poll ?? false;
193
+ const sourceKey = serializeSource(
194
+ source
195
+ );
196
+ const [state, setState] = React.useState({
197
+ data: null,
198
+ isLoading: sourceKey !== null,
199
+ error: null,
200
+ isStale: false,
201
+ lastUpdated: null
202
+ });
203
+ const mountedRef = React.useRef(true);
204
+ const sourceRef = React.useRef(source);
205
+ sourceRef.current = source;
206
+ React.useEffect(() => {
207
+ mountedRef.current = true;
208
+ const current = sourceRef.current;
209
+ if (!current || sourceKey === null) {
210
+ setState({ data: null, isLoading: false, error: null, isStale: false, lastUpdated: null });
211
+ return () => {
212
+ mountedRef.current = false;
213
+ };
214
+ }
215
+ setState((prev) => ({ ...prev, isLoading: prev.data === null }));
216
+ const collectionName = current.__kind === "query" ? current.collection : current.id;
217
+ const dbQuery = current.__kind === "query" ? toDbQuery(current.constraints) : void 0;
218
+ const col = current.db.collection(collectionName);
219
+ const applySnap = (snap) => {
220
+ if (!mountedRef.current) return;
221
+ setState({
222
+ data: rowsFromList(snap),
223
+ isLoading: false,
224
+ error: null,
225
+ isStale: snap.stale === true,
226
+ lastUpdated: Date.now()
227
+ });
228
+ };
229
+ const applyError = (e) => {
230
+ if (!mountedRef.current) return;
231
+ setState((prev) => ({
232
+ ...prev,
233
+ // Mantém o último data bom — não limpa em erro de refresh.
234
+ isLoading: false,
235
+ error: e instanceof Error ? e : new Error(String(e)),
236
+ isStale: true
237
+ }));
238
+ };
239
+ if (poll) {
240
+ let cancelled = false;
241
+ const fetchOnce = async () => {
242
+ if (cancelled || isDocumentHidden()) return;
243
+ try {
244
+ const snap = await col.list(dbQuery);
245
+ if (!cancelled) applySnap(snap);
246
+ } catch (e) {
247
+ if (!cancelled) applyError(e);
248
+ }
249
+ };
250
+ void fetchOnce();
251
+ const timer = setInterval(fetchOnce, intervalMs);
252
+ return () => {
253
+ cancelled = true;
254
+ clearInterval(timer);
255
+ mountedRef.current = false;
256
+ };
257
+ }
258
+ let unsub;
259
+ try {
260
+ unsub = col.onSnapshot(dbQuery, applySnap);
261
+ } catch (e) {
262
+ applyError(e);
263
+ }
264
+ return () => {
265
+ mountedRef.current = false;
266
+ unsub?.();
267
+ };
268
+ }, [sourceKey, intervalMs, poll]);
269
+ return state;
270
+ }
271
+ function useDoc(ref, options) {
272
+ const intervalMs = options?.intervalMs ?? DEFAULT_INTERVAL_MS;
273
+ const poll = options?.poll ?? false;
274
+ const refKey = ref && ref.id ? `${ref.collection}/${ref.id}` : null;
275
+ const [state, setState] = React.useState({
276
+ data: null,
277
+ isLoading: refKey !== null,
278
+ error: null,
279
+ isStale: false,
280
+ lastUpdated: null
281
+ });
282
+ const mountedRef = React.useRef(true);
283
+ const refRef = React.useRef(ref);
284
+ refRef.current = ref;
285
+ React.useEffect(() => {
286
+ mountedRef.current = true;
287
+ const current = refRef.current;
288
+ if (!current || refKey === null) {
289
+ setState({ data: null, isLoading: false, error: null, isStale: false, lastUpdated: null });
290
+ return () => {
291
+ mountedRef.current = false;
292
+ };
293
+ }
294
+ setState((prev) => ({ ...prev, isLoading: prev.data === null }));
295
+ const docRef = current.db.collection(current.collection).doc(current.id);
296
+ const id = current.id;
297
+ const applySnap = (snap) => {
298
+ if (!mountedRef.current) return;
299
+ setState({
300
+ data: rowFromGet(snap, id),
301
+ isLoading: false,
302
+ error: null,
303
+ isStale: snap?.stale === true,
304
+ lastUpdated: Date.now()
305
+ });
306
+ };
307
+ const applyError = (e) => {
308
+ if (!mountedRef.current) return;
309
+ setState((prev) => ({
310
+ ...prev,
311
+ isLoading: false,
312
+ error: e instanceof Error ? e : new Error(String(e)),
313
+ isStale: true
314
+ }));
315
+ };
316
+ if (poll) {
317
+ let cancelled = false;
318
+ const fetchOnce = async () => {
319
+ if (cancelled || isDocumentHidden()) return;
320
+ try {
321
+ const snap = await docRef.get();
322
+ if (!cancelled) applySnap(snap);
323
+ } catch (e) {
324
+ if (!cancelled) applyError(e);
325
+ }
326
+ };
327
+ void fetchOnce();
328
+ const timer = setInterval(fetchOnce, intervalMs);
329
+ return () => {
330
+ cancelled = true;
331
+ clearInterval(timer);
332
+ mountedRef.current = false;
333
+ };
334
+ }
335
+ let unsub;
336
+ try {
337
+ unsub = docRef.onSnapshot(applySnap);
338
+ } catch (e) {
339
+ applyError(e);
340
+ }
341
+ return () => {
342
+ mountedRef.current = false;
343
+ unsub?.();
344
+ };
345
+ }, [refKey, intervalMs, poll]);
346
+ return state;
347
+ }
348
+ function resolveAuth(input) {
349
+ const maybe = input;
350
+ if (maybe.auth && typeof maybe.auth.onAuthStateChanged === "function") {
351
+ return maybe.auth;
352
+ }
353
+ return input;
354
+ }
355
+ function useUser(input, options) {
356
+ const timeoutMs = options?.timeoutMs ?? 8e3;
357
+ const auth = resolveAuth(input);
358
+ const [state, setState] = React.useState(() => {
359
+ let seeded = null;
360
+ try {
361
+ seeded = auth.getUser();
362
+ } catch {
363
+ seeded = null;
364
+ }
365
+ return { user: seeded, isLoading: seeded === null, error: null };
366
+ });
367
+ const mountedRef = React.useRef(true);
368
+ React.useEffect(() => {
369
+ mountedRef.current = true;
370
+ let settled = false;
371
+ const timer = setTimeout(() => {
372
+ if (!mountedRef.current || settled) return;
373
+ setState((prev) => prev.isLoading ? { ...prev, isLoading: false } : prev);
374
+ }, timeoutMs);
375
+ let unsub;
376
+ try {
377
+ unsub = auth.onAuthStateChanged((user) => {
378
+ if (!mountedRef.current) return;
379
+ settled = true;
380
+ setState({ user: user ?? null, isLoading: false, error: null });
381
+ });
382
+ } catch (e) {
383
+ if (mountedRef.current) {
384
+ setState({
385
+ user: null,
386
+ isLoading: false,
387
+ error: e instanceof Error ? e : new Error(String(e))
388
+ });
389
+ }
390
+ }
391
+ return () => {
392
+ mountedRef.current = false;
393
+ clearTimeout(timer);
394
+ unsub?.();
395
+ };
396
+ }, [auth, timeoutMs]);
397
+ return state;
398
+ }
135
399
 
136
- export { CheckoutLink, EntitlementGate, useEntitlementContext };
400
+ export { CheckoutLink, EntitlementGate, useCollection, useDoc, useEntitlementContext, useUser };
137
401
  //# sourceMappingURL=react.mjs.map
138
402
  //# sourceMappingURL=react.mjs.map