@marktiderman/genesis-native 0.1.0 → 0.3.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 (87) hide show
  1. package/README.md +354 -10
  2. package/dist/block-C5-noMhB.d.ts +223 -0
  3. package/dist/blocks-a1YQ1Elf.d.ts +1540 -0
  4. package/dist/chunk-65P7RU4V.js +49 -0
  5. package/dist/chunk-65P7RU4V.js.map +1 -0
  6. package/dist/chunk-6BT4PTGH.js +37 -0
  7. package/dist/chunk-6BT4PTGH.js.map +1 -0
  8. package/dist/chunk-75YP72G6.js +374 -0
  9. package/dist/chunk-75YP72G6.js.map +1 -0
  10. package/dist/chunk-NGYNYJPL.js +90 -0
  11. package/dist/chunk-NGYNYJPL.js.map +1 -0
  12. package/dist/chunk-VKTIHNAV.js +95 -0
  13. package/dist/chunk-VKTIHNAV.js.map +1 -0
  14. package/dist/{chunk-MMTPWUWI.js → chunk-VTJDIKFC.js} +9 -6
  15. package/dist/chunk-VTJDIKFC.js.map +1 -0
  16. package/dist/chunk-XE7LC7K6.js +29 -0
  17. package/dist/chunk-XE7LC7K6.js.map +1 -0
  18. package/dist/dev-warnings.d.ts +34 -0
  19. package/dist/dev-warnings.js +15 -0
  20. package/dist/dev-warnings.js.map +1 -0
  21. package/dist/devtools.d.ts +144 -0
  22. package/dist/devtools.js +390 -0
  23. package/dist/devtools.js.map +1 -0
  24. package/dist/index.d.ts +12 -368
  25. package/dist/index.js +88 -191
  26. package/dist/index.js.map +1 -1
  27. package/dist/observability.d.ts +170 -0
  28. package/dist/observability.js +123 -0
  29. package/dist/observability.js.map +1 -0
  30. package/dist/persist.d.ts +1 -1
  31. package/dist/persist.js +1 -1
  32. package/dist/push.d.ts +169 -0
  33. package/dist/push.js +996 -0
  34. package/dist/push.js.map +1 -0
  35. package/dist/{query-DhH3lLFa.d.ts → query-DUmVAfsF.d.ts} +11 -6
  36. package/dist/updates.d.ts +8 -1
  37. package/dist/updates.js +44 -69
  38. package/dist/updates.js.map +1 -1
  39. package/package.json +222 -5
  40. package/src/app.tsx +491 -0
  41. package/src/auth.tsx +176 -0
  42. package/src/blocks.tsx +144 -0
  43. package/src/boot.tsx +188 -0
  44. package/src/dev-warnings.ts +43 -0
  45. package/src/devtools/block.tsx +488 -0
  46. package/src/devtools/gestures.ts +107 -0
  47. package/src/devtools/index.ts +22 -0
  48. package/src/devtools/info.ts +45 -0
  49. package/src/devtools/slots.ts +21 -0
  50. package/src/errors.tsx +195 -0
  51. package/src/held-link.ts +90 -0
  52. package/src/hydration-gate.ts +88 -0
  53. package/src/index.ts +83 -0
  54. package/src/links.ts +140 -0
  55. package/src/nativewind.d.ts +41 -0
  56. package/src/observability/block.tsx +300 -0
  57. package/src/observability/events.ts +89 -0
  58. package/src/observability/index.ts +44 -0
  59. package/src/observability/scrub.ts +166 -0
  60. package/src/observability/sentry.ts +222 -0
  61. package/src/observability/sink.ts +147 -0
  62. package/src/persist.ts +83 -0
  63. package/src/push/backend.ts +129 -0
  64. package/src/push/block.tsx +545 -0
  65. package/src/push/index.ts +102 -0
  66. package/src/push/modules.ts +111 -0
  67. package/src/push/permission.ts +191 -0
  68. package/src/push/preferences.ts +136 -0
  69. package/src/push/quiet-hours.ts +71 -0
  70. package/src/push/registration.ts +189 -0
  71. package/src/push/routing.ts +27 -0
  72. package/src/push/screens.tsx +46 -0
  73. package/src/push/settings.tsx +378 -0
  74. package/src/query.ts +100 -0
  75. package/src/updates/block.tsx +442 -0
  76. package/src/updates/channels.ts +131 -0
  77. package/src/updates/checker.ts +190 -0
  78. package/src/updates/index.ts +87 -0
  79. package/src/updates/modules.ts +64 -0
  80. package/src/updates/policy.ts +314 -0
  81. package/src/updates/screens.tsx +159 -0
  82. package/src/updates/signature.ts +80 -0
  83. package/src/updates/store.ts +33 -0
  84. package/src/updates/version.ts +39 -0
  85. package/src/updates/whats-new.ts +122 -0
  86. package/dist/block-Dh5ny06W.d.ts +0 -535
  87. package/dist/chunk-MMTPWUWI.js.map +0 -1
@@ -0,0 +1,1540 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, ComponentType, Component, ErrorInfo } from 'react';
3
+ import { Slots, SlotPropsFor } from '@marktiderman/genesis-core/slots';
4
+ import { BlockDef, Contributed, BlockSettingsRow, ResolvedBlocks } from '@marktiderman/genesis-features/blocks';
5
+ import { QueryClient, QueryClientConfig } from '@tanstack/react-query';
6
+ import { CanonicalBrand } from '@marktiderman/genesis-design-system';
7
+ import { SupabaseClientInput, DataProvider } from '@marktiderman/genesis-core/provider';
8
+ import { KeyboardScrollProps, KeyboardToolbarProps } from '@marktiderman/genesis-ui-native/layouts';
9
+ import { GenesisModules, NativeSheetLibrary, NativeToastProviderProps } from '@marktiderman/genesis-ui-native/components';
10
+ import { b as QueryPersistence, Q as QueryLifecycleOptions } from './query-DUmVAfsF.js';
11
+
12
+ interface BootStepContext {
13
+ /**
14
+ * Aborted when the step times out. Stop work when it fires: a retry waits
15
+ * for a timed-out run to settle before running the step again.
16
+ */
17
+ signal: AbortSignal;
18
+ }
19
+ interface BootStep {
20
+ /** Unique within the list; names the step in errors. */
21
+ id: string;
22
+ run: (context: BootStepContext) => Promise<unknown> | unknown;
23
+ /** Overrides the boot's `timeoutMs` for this step. */
24
+ timeoutMs?: number;
25
+ }
26
+ type BootStatus = "idle" | "booting" | "ready" | "failed";
27
+ interface BootState {
28
+ status: BootStatus;
29
+ /** Set when `status` is `"failed"`. */
30
+ error: Error | null;
31
+ /** The id of the step that failed. */
32
+ failedStep: string | null;
33
+ /** Ids of the steps that have finished, in order. */
34
+ completed: readonly string[];
35
+ }
36
+ interface BootOptions {
37
+ /** Shortest time the boot takes, so the splash doesn't flash. Default 0. */
38
+ minimumMs?: number;
39
+ /** Longest time one step may take. Default 10 000. */
40
+ timeoutMs?: number;
41
+ }
42
+ interface BootController {
43
+ getState(): BootState;
44
+ subscribe(listener: () => void): () => void;
45
+ /** Run the steps not yet finished. Calling it while a run is in flight returns that run. */
46
+ start(): Promise<void>;
47
+ /** After a failure: run again from the step that failed. */
48
+ retry(): Promise<void>;
49
+ }
50
+ /** Thrown when a step takes longer than its timeout. */
51
+ declare class BootTimeoutError extends Error {
52
+ readonly step: string;
53
+ constructor(step: string, ms: number, reason?: string);
54
+ }
55
+ /** The boot controller: runs `steps` once each, in order, with a timeout per step. */
56
+ declare function createBoot(steps: readonly BootStep[], options?: BootOptions): BootController;
57
+ /** Makes a boot readable with `useBoot()`. */
58
+ declare function BootProvider({ boot, children }: {
59
+ boot: BootController;
60
+ children: ReactNode;
61
+ }): react.JSX.Element;
62
+ interface UseBootReturn extends BootState {
63
+ retry: () => void;
64
+ }
65
+ /** The boot's status, and `retry` after a failure. Inside `GenesisNativeApp` or a `BootProvider`. */
66
+ declare function useBoot(): UseBootReturn;
67
+
68
+ interface FatalScreenProps {
69
+ error: Error;
70
+ /** Clears the error and remounts the app below the shell (and re-runs a failed boot). */
71
+ retry: () => void;
72
+ }
73
+ /** The default fatal screen: what went wrong and "Try again". */
74
+ declare function FatalScreen({ error, retry }: FatalScreenProps): react.JSX.Element;
75
+ /**
76
+ * The last-resort fatal screen: React Native primitives only, no theme, no
77
+ * context, nothing that can itself fail. Shown when the providers could not
78
+ * start, or when the `fatal` slot threw.
79
+ */
80
+ declare function PlainFatalScreen({ error, retry }: FatalScreenProps): react.JSX.Element;
81
+ interface ErrorReport {
82
+ /** True when the app could not go on (a render error, a failed boot, a fatal global error). */
83
+ fatal: boolean;
84
+ source: "boundary" | "global" | "boot" | "rejection";
85
+ componentStack?: string;
86
+ }
87
+ type OnError = (error: Error, report: ErrorReport) => void;
88
+ interface GenesisErrorBoundaryProps {
89
+ children?: ReactNode;
90
+ /** What to render instead of the tree that threw. */
91
+ fallback: ComponentType<FatalScreenProps>;
92
+ onError?: OnError;
93
+ /** Change it to clear a caught error (the tree remounts). */
94
+ resetKey?: unknown;
95
+ /** Called by the fallback's retry, after the error is cleared. */
96
+ onRetry?: () => void;
97
+ }
98
+ interface BoundaryState {
99
+ error: Error | null;
100
+ }
101
+ /** An error boundary whose fallback gets `{ error, retry }`. */
102
+ declare class GenesisErrorBoundary extends Component<GenesisErrorBoundaryProps, BoundaryState> {
103
+ state: BoundaryState;
104
+ static getDerivedStateFromError(error: unknown): BoundaryState;
105
+ componentDidCatch(error: unknown, info: ErrorInfo): void;
106
+ componentDidUpdate(previous: GenesisErrorBoundaryProps): void;
107
+ retry: () => void;
108
+ render(): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | react.JSX.Element | null;
109
+ }
110
+ /** React Native's `ErrorUtils` (a global in every RN app), or a stand-in. */
111
+ interface ErrorUtilsLike {
112
+ getGlobalHandler(): ((error: unknown, isFatal?: boolean) => void) | undefined;
113
+ setGlobalHandler(handler: (error: unknown, isFatal?: boolean) => void): void;
114
+ }
115
+ interface GlobalErrorHandlerOptions {
116
+ errorUtils: ErrorUtilsLike;
117
+ onError?: OnError;
118
+ /** A fatal error: show the fatal screen. The previous handler (which would end the app) is not called. */
119
+ onFatal: (error: Error) => void;
120
+ /**
121
+ * True while a fatal screen is already up. Another fatal error then goes to
122
+ * the previous handler, which ends the app (it restarts clean) rather than
123
+ * leaving it stuck behind a screen that may itself be broken.
124
+ */
125
+ alreadyFatal?: () => boolean;
126
+ }
127
+ /**
128
+ * Route global errors to `onError`. A fatal one goes to `onFatal` instead of
129
+ * React Native's default handler, which would close the app; a non-fatal one
130
+ * still reaches the previous handler (LogBox in development). Returns a
131
+ * function that puts the previous handler back.
132
+ */
133
+ declare function installGlobalErrorHandler({ errorUtils, onError, onFatal, alreadyFatal }: GlobalErrorHandlerOptions): () => void;
134
+ /** `globalThis.ErrorUtils` in a React Native runtime, undefined elsewhere. */
135
+ declare function defaultErrorUtils(): ErrorUtilsLike | undefined;
136
+ /** Hermes's `HermesInternal.enablePromiseRejectionTracker`, or a stand-in. */
137
+ type EnableRejectionTracker = (options: {
138
+ allRejections: boolean;
139
+ onUnhandled: (id: number, rejection: unknown) => void;
140
+ onHandled: (id: number) => void;
141
+ }) => void;
142
+ /**
143
+ * Hermes's rejection tracker, when the app runs on Hermes. React Native sends
144
+ * unhandled rejections to `ExceptionsManager` (LogBox in development), never
145
+ * to `ErrorUtils`' global handler, so without this they never reach `onError`.
146
+ */
147
+ declare function defaultRejectionTracker(): EnableRejectionTracker | undefined;
148
+ /**
149
+ * Report unhandled promise rejections to `onError` (non-fatal). This replaces
150
+ * React Native's own tracker, so each rejection is also written with
151
+ * `console.warn`, which LogBox shows in development as before.
152
+ */
153
+ declare function installRejectionTracker({ enable, onError }: {
154
+ enable: EnableRejectionTracker;
155
+ onError: OnError;
156
+ }): void;
157
+
158
+ /** The parts of a Supabase client the gate uses; a `SupabaseClient` fits. */
159
+ interface AuthClientLike<S = unknown> {
160
+ auth: {
161
+ getSession(): Promise<{
162
+ data: {
163
+ session: S | null;
164
+ };
165
+ error?: unknown;
166
+ }>;
167
+ onAuthStateChange(callback: (event: string, session: S | null) => void): {
168
+ data: {
169
+ subscription: {
170
+ unsubscribe(): void;
171
+ };
172
+ };
173
+ };
174
+ };
175
+ }
176
+ type SessionStatus = "loading" | "signedIn" | "signedOut";
177
+ interface SessionState<S = unknown> {
178
+ status: SessionStatus;
179
+ session: S | null;
180
+ /** The signed-in user's id, or null. */
181
+ userId: string | null;
182
+ /** True when the session came from the phone because Supabase couldn't be reached. */
183
+ offline: boolean;
184
+ }
185
+ interface SessionStore<S = unknown> {
186
+ getState(): SessionState<S>;
187
+ subscribe(listener: () => void): () => void;
188
+ /** Read the current session (the boot step). Throws only on a non-network error. */
189
+ load(): Promise<void>;
190
+ /** Follow sign-in and sign-out; returns the unsubscribe. */
191
+ start(): () => void;
192
+ }
193
+ interface SessionStoreOptions<S = unknown> {
194
+ /**
195
+ * The signed-in user changed after the session was first known: signed
196
+ * out, signed in, or switched account. `previous` and `next` are user ids
197
+ * (null when nobody). Clear caches here.
198
+ */
199
+ onUserChange?: (previous: string | null, next: string | null) => void;
200
+ /** Longest wait for `getSession()` before using the stored session. Default 5000 ms. */
201
+ waitMs?: number;
202
+ /**
203
+ * Read the session saved on the phone without refreshing it. Default: from
204
+ * the Supabase client's own storage (`auth.storage` under `auth.storageKey`).
205
+ */
206
+ readStoredSession?: () => Promise<S | null>;
207
+ /** The user id in a session. Default `session.user.id`. */
208
+ userIdOf?: (session: S) => string | null;
209
+ }
210
+ /** The Supabase session as a store: `load` reads it once, `start` follows auth events. */
211
+ declare function createSessionStore<S = unknown>(client: AuthClientLike<S>, options?: SessionStoreOptions<S>): SessionStore<S>;
212
+ /** Makes a session store readable with `useSession()`. */
213
+ declare function SessionProvider({ store, children }: {
214
+ store: SessionStore;
215
+ children: ReactNode;
216
+ }): react.JSX.Element;
217
+ /** The session and whether it is known yet. Inside `GenesisNativeApp` or a `SessionProvider`. */
218
+ declare function useSession<S = unknown>(): SessionState<S>;
219
+ interface AuthGateProps {
220
+ signedIn: ReactNode;
221
+ signedOut: ReactNode;
222
+ /** While the session is still being read. Default nothing (the splash is up). */
223
+ loading?: ReactNode;
224
+ }
225
+ /** Render one tree for a signed-in user and another for everyone else. */
226
+ declare function AuthGate({ signedIn, signedOut, loading }: AuthGateProps): react.JSX.Element;
227
+
228
+ interface LinkHold {
229
+ /** Hold `path`, replacing any held one. */
230
+ hold(path: string): void;
231
+ peek(): string | null;
232
+ /** Return the held path and forget it. */
233
+ take(): string | null;
234
+ clear(): void;
235
+ subscribe(listener: () => void): () => void;
236
+ }
237
+ /** A place to hold one deep link until sign-in completes; lives outside React. */
238
+ declare function createLinkHold(): LinkHold;
239
+ /** `"pending"` while booting or reading the session. */
240
+ type HeldLinkAuth = "pending" | "signedOut" | "signedIn";
241
+ interface UseHeldLinkOptions {
242
+ auth: HeldLinkAuth;
243
+ /** Go there, e.g. `(path) => router.replace(path)`. Called once per link held across a sign-in. */
244
+ onRelease: (path: string) => void;
245
+ }
246
+ /** Follow the held link after a sign-in, drop it otherwise; returns the link still held. */
247
+ declare function useHeldLink(hold: LinkHold, { auth, onRelease }: UseHeldLinkOptions): string | null;
248
+
249
+ /**
250
+ * The devtools overlay's slot on `GenesisNativeApp`. Types only, so the
251
+ * shell can name it without bundling the overlay.
252
+ */
253
+ /** What the `devtoolsFeedback` slot is given: the support block's "send feedback" fills it. */
254
+ interface DevtoolsFeedbackProps {
255
+ /** The overlay's debug report (version signature, user id, blocks, recent activity). */
256
+ debugInfo: string;
257
+ /**
258
+ * Closes the overlay (before a screenshot, say). Resolves once the sheet has finished sliding
259
+ * away, so a capture after it shows the app, not the closing sheet.
260
+ */
261
+ close: () => Promise<void> | void;
262
+ }
263
+ type DevtoolsSlots = {
264
+ /** A "send feedback" action in the devtools overlay. Default nothing; the support block fills it. */
265
+ devtoolsFeedback: DevtoolsFeedbackProps;
266
+ };
267
+
268
+ /** A tiny external store for `useSyncExternalStore`: immutable state, one notify per change. */
269
+ interface Store<T> {
270
+ getState(): T;
271
+ setState(next: Partial<T>): void;
272
+ subscribe(listener: () => void): () => void;
273
+ }
274
+
275
+ /**
276
+ * The native modules the updates block reads, as the shapes it needs. Pass
277
+ * the real ones (`import * as Updates from "expo-updates"`,
278
+ * `import * as Application from "expo-application"`); tests pass fakes.
279
+ * Both are optional peers: without `expo-updates` there is no update banner
280
+ * and no channel switching, and without `expo-application` no force upgrade.
281
+ */
282
+ /** `expo-updates` (SDK 58): the constants and calls this block uses. */
283
+ interface UpdatesModuleLike {
284
+ /** False in development, Expo Go, or a build without updates configured. */
285
+ readonly isEnabled: boolean;
286
+ /** The running update's id; `null` when updates are off. */
287
+ readonly updateId: string | null;
288
+ /** The build's channel; `null` in development builds and Expo Go. */
289
+ readonly channel: string | null;
290
+ readonly runtimeVersion: string | null;
291
+ /** True when the code that shipped in the binary is running, not a downloaded update. */
292
+ readonly isEmbeddedLaunch: boolean;
293
+ /** True when expo-updates fell back to the embedded update after a failed launch. */
294
+ readonly isEmergencyLaunch?: boolean;
295
+ readonly emergencyLaunchReason?: string | null;
296
+ checkForUpdateAsync(): Promise<{
297
+ isAvailable: boolean;
298
+ isRollBackToEmbedded?: boolean;
299
+ manifest?: unknown;
300
+ }>;
301
+ fetchUpdateAsync(): Promise<{
302
+ isNew: boolean;
303
+ isRollBackToEmbedded?: boolean;
304
+ manifest?: unknown;
305
+ }>;
306
+ reloadAsync(options?: {
307
+ reloadScreenOptions?: object;
308
+ }): Promise<void>;
309
+ /** Overrides the `expo-channel-name` header (persists on the device); `null` removes the override. */
310
+ setUpdateRequestHeadersOverride?(requestHeaders: Record<string, string> | null): void;
311
+ }
312
+ /** `expo-application`: the installed store version and build number. */
313
+ interface ApplicationLike {
314
+ /** `CFBundleShortVersionString` / `versionName`, e.g. `"1.4.0"`. */
315
+ readonly nativeApplicationVersion: string | null;
316
+ /** `CFBundleVersion` / `versionCode`, e.g. `"37"`. */
317
+ readonly nativeBuildVersion: string | null;
318
+ }
319
+ /** AsyncStorage, MMKV's async adapter, or a stand-in. */
320
+ interface UpdatesStorageLike {
321
+ getItem(key: string): Promise<string | null>;
322
+ setItem(key: string, value: string): Promise<void>;
323
+ removeItem?(key: string): Promise<void>;
324
+ }
325
+ /** React Native's `AppState`. */
326
+ interface UpdatesAppStateLike {
327
+ addEventListener(type: "change", listener: (state: string) => void): {
328
+ remove(): void;
329
+ };
330
+ }
331
+ /** Where a reported error came from. */
332
+ type UpdatesErrorSource = "policy" | "check" | "reload" | "storage" | "channel";
333
+ type UpdatesOnError = (error: Error, source: UpdatesErrorSource) => void;
334
+
335
+ /**
336
+ * The update banner's engine: ask expo-updates for a newer update, download
337
+ * it, and reload only when the person taps and nothing is mid-edit.
338
+ *
339
+ * - It never reloads on its own. A downloaded update also applies on the next
340
+ * cold start, so dismissing the banner loses nothing.
341
+ * - The reload guard says no while any `useBlockReload(true)` is mounted (a
342
+ * form with unsaved changes) or your `canReload()` returns false (or
343
+ * throws). The banner then asks the person to finish first.
344
+ * - expo-updates only serves updates built for this binary's runtime
345
+ * version; an update whose manifest names another runtime is ignored too.
346
+ */
347
+
348
+ interface ReloadGuard {
349
+ /** Hold reloads off until the returned function is called. */
350
+ block(): () => void;
351
+ /** True when nothing blocks and your `canReload` agrees. */
352
+ canReload(): boolean;
353
+ /** Fires when a block starts or ends. */
354
+ subscribe(listener: () => void): () => void;
355
+ }
356
+ declare function createReloadGuard(canReload?: () => boolean): ReloadGuard;
357
+ type UpdateCheckStatus = "disabled" | "idle" | "checking" | "downloading" | "ready" | "reloading" | "error";
358
+ interface UpdateCheckState {
359
+ status: UpdateCheckStatus;
360
+ /** The downloaded update's id, when the manifest has one. */
361
+ updateId: string | null;
362
+ /** The person closed the banner; the update still applies on the next cold start. */
363
+ dismissed: boolean;
364
+ /** The last tap was refused by the reload guard; cleared when the guard opens. */
365
+ blocked: boolean;
366
+ error: Error | null;
367
+ }
368
+ interface UpdateCheckerOptions {
369
+ updates?: UpdatesModuleLike;
370
+ guard: ReloadGuard;
371
+ /** Passed to `reloadAsync` (a branded reload screen). */
372
+ reloadScreenOptions?: object;
373
+ now?: () => number;
374
+ onError?: UpdatesOnError;
375
+ }
376
+ interface UpdateChecker extends Pick<Store<UpdateCheckState>, "getState" | "subscribe"> {
377
+ /** Check and download. Never rejects; does nothing while one is in flight or an update is ready. */
378
+ check(): Promise<void>;
379
+ /** `check()` unless the last one started less than `minIntervalMs` ago. */
380
+ checkIfDue(minIntervalMs: number): Promise<void>;
381
+ /** Reload into the downloaded update. Resolves false when there is none or the guard refuses. */
382
+ reload(): Promise<boolean>;
383
+ dismiss(): void;
384
+ /** Stop following the guard. */
385
+ dispose(): void;
386
+ }
387
+ declare function createUpdateChecker(options: UpdateCheckerOptions): UpdateChecker;
388
+
389
+ /**
390
+ * Switching a test build to another update channel (EAS "channel surfing"):
391
+ * override the `expo-channel-name` header, check, fetch, reload. The
392
+ * override persists on the device until it is reset or the app is removed.
393
+ *
394
+ * Off unless you turn it on, and turn it on only in development and preview
395
+ * builds (`enabled: __DEV__ || isPreviewBuild`): a store build that can be
396
+ * pointed at any channel can be pointed at an unreviewed one. Going back
397
+ * (`reset`) always works, for everyone, because it only removes the
398
+ * override.
399
+ *
400
+ * Switching reloads, so it respects the reload guard: with unsaved changes
401
+ * it sets the channel and says `blocked`; the channel applies on the next
402
+ * launch.
403
+ */
404
+
405
+ type ChannelSwitchResult =
406
+ /** The reload was sent; JS may stop at any moment. */
407
+ {
408
+ kind: "reloading";
409
+ }
410
+ /** The channel is set, but something is mid-edit: it applies on the next launch. */
411
+ | {
412
+ kind: "blocked";
413
+ }
414
+ /** expo-updates is off or too old here (development, Expo Go). Nothing changed. */
415
+ | {
416
+ kind: "unavailable";
417
+ reason: string;
418
+ }
419
+ /** The channel is set but checking, fetching or reloading failed. */
420
+ | {
421
+ kind: "error";
422
+ error: Error;
423
+ };
424
+ interface ChannelSwitcher {
425
+ /** True when this build may switch channels (`channels.enabled`). */
426
+ readonly enabled: boolean;
427
+ /** Switch to `channel` and reload. Throws when switching is off. */
428
+ switchTo(channel: string): Promise<ChannelSwitchResult>;
429
+ /** Back to the build's own channel. Always allowed. */
430
+ reset(): Promise<ChannelSwitchResult>;
431
+ /** The channel picked on this device, or `null` when it follows the build. */
432
+ requested(): Promise<string | null>;
433
+ }
434
+ interface ChannelSwitcherOptions {
435
+ updates?: UpdatesModuleLike;
436
+ /** Must be explicitly true (development and preview builds only). */
437
+ enabled: boolean;
438
+ guard: ReloadGuard;
439
+ /** Remembers the picked channel, for the version signature. */
440
+ storage?: UpdatesStorageLike;
441
+ /** Default `"genesis:updates:channel"`. */
442
+ storageKey?: string;
443
+ reloadScreenOptions?: object;
444
+ onError?: UpdatesOnError;
445
+ }
446
+ declare function createChannelSwitcher(options: ChannelSwitcherOptions): ChannelSwitcher;
447
+
448
+ /**
449
+ * The release policy: the oldest store version each platform may run, a
450
+ * maintenance switch, and release notes. It comes from an
451
+ * `UpdatePolicySource` (a static object, Supabase, or your own), is cached on
452
+ * the phone, and **fails open**: no answer, a timeout, a malformed value or
453
+ * an untrusted cache never blocks anyone.
454
+ *
455
+ * What may block, and from where:
456
+ * - Force upgrade: a policy fetched this session, or a cached one no older
457
+ * than `cacheMaxAgeMs` (7 days) by the phone's clock. A cache whose time is
458
+ * in the future (the clock moved back) is not trusted. A minimum without a
459
+ * store link for the platform forces nothing (there would be no way out).
460
+ * - Maintenance: only a policy fetched from the network this session. A
461
+ * cached maintenance flag never blocks a cold start.
462
+ */
463
+
464
+ type UpdatePlatform = "ios" | "android";
465
+ interface ReleaseNote {
466
+ /** The store version these notes describe, e.g. `"1.4.0"`. */
467
+ version: string;
468
+ title?: string;
469
+ body: string;
470
+ }
471
+ interface UpdatePolicy {
472
+ /** The oldest store version still supported, per platform. Missing or malformed: no minimum. */
473
+ minimumVersion?: Partial<Record<UpdatePlatform, string>>;
474
+ /** Where "Update" goes (`https://apps.apple.com/…`, `itms-apps://…`, `market://…`, `https://play.google.com/…`). */
475
+ storeUrl?: Partial<Record<UpdatePlatform, string>>;
476
+ maintenance?: {
477
+ active: boolean;
478
+ message: string | null;
479
+ };
480
+ notes?: readonly ReleaseNote[];
481
+ }
482
+ /** Where the policy comes from. `load` throws (or rejects) when it can't answer; resolving `null` means "no policy". */
483
+ interface UpdatePolicySource {
484
+ load(): Promise<UpdatePolicy | null | undefined>;
485
+ }
486
+ /** A policy written in the app (or a test). */
487
+ declare function staticPolicySource(policy: UpdatePolicy): UpdatePolicySource;
488
+ /** A Supabase client: anything with `from(table)` (a real `SupabaseClient` fits without a cast). */
489
+ interface PolicyClientLike {
490
+ from(table: string): unknown;
491
+ }
492
+ interface SupabasePolicySourceOptions {
493
+ /** The `app_id` row to read. Default `"default"`. */
494
+ appId?: string;
495
+ /** Default `"app_release_policy"` (migration `0002_genesis_app_release_policy.sql` in `@marktiderman/genesis-supabase`). */
496
+ table?: string;
497
+ }
498
+ /**
499
+ * Reads one row of `public.app_release_policy` (readable by anon and
500
+ * authenticated, written only with the service role). No row is no policy.
501
+ */
502
+ declare function supabasePolicySource(client: PolicyClientLike, options?: SupabasePolicySourceOptions): UpdatePolicySource;
503
+ /** Keeps only well-formed fields; throws when `raw` isn't an object at all (so the cache answers instead). */
504
+ declare function sanitizePolicy(raw: unknown): UpdatePolicy;
505
+ type PolicyOrigin = "network" | "cache" | "none";
506
+ interface PolicyState {
507
+ /** `loading` until the first answer (or the timeout); then `ready`, with or without a policy. */
508
+ status: "loading" | "ready";
509
+ policy: UpdatePolicy | null;
510
+ /** `network`: fetched this session. `cache`: read from the phone. `none`: no answer yet (fail open). */
511
+ origin: PolicyOrigin;
512
+ /** When it was fetched, by the phone's clock (ms). */
513
+ fetchedAt: number | null;
514
+ /** The last failed fetch, if the most recent one failed. */
515
+ error: Error | null;
516
+ /** A fetch is in flight. */
517
+ refreshing: boolean;
518
+ }
519
+ interface PolicyStoreOptions {
520
+ source: UpdatePolicySource;
521
+ storage?: UpdatesStorageLike;
522
+ /** Default `"genesis:updates:policy"`. */
523
+ cacheKey?: string;
524
+ /** The longest `load()` waits before carrying on without an answer. Default 3000 ms. */
525
+ timeoutMs?: number;
526
+ now?: () => number;
527
+ onError?: UpdatesOnError;
528
+ }
529
+ interface PolicyStore extends Pick<Store<PolicyState>, "getState" | "subscribe"> {
530
+ /** The first load: the cache and the network together, for at most `timeoutMs`. Never rejects; runs once. */
531
+ load(): Promise<void>;
532
+ /** Fetch again (maintenance "Try again", the app coming to the front). Never rejects; resolves true on an answer. */
533
+ refresh(): Promise<boolean>;
534
+ /** `refresh()` unless one ran less than `minIntervalMs` ago. */
535
+ refreshIfDue(minIntervalMs: number): Promise<boolean>;
536
+ }
537
+ declare function createPolicyStore(options: PolicyStoreOptions): PolicyStore;
538
+ /** What the policy says this install must do. */
539
+ type UpdateVerdict = {
540
+ kind: "ok";
541
+ } | {
542
+ kind: "forceUpgrade";
543
+ minimumVersion: string;
544
+ installedVersion: string;
545
+ storeUrl: string;
546
+ } | {
547
+ kind: "maintenance";
548
+ message: string | null;
549
+ };
550
+ interface EvaluatePolicyOptions {
551
+ /** `Platform.OS`. Only `ios` and `android` can be forced to upgrade. */
552
+ platform: string;
553
+ /** The installed store version (`Application.nativeApplicationVersion`). `null`: never forced. */
554
+ installedVersion: string | null;
555
+ now: number;
556
+ /** How old a cached policy may be and still force an upgrade. Default 7 days. */
557
+ cacheMaxAgeMs?: number;
558
+ }
559
+ /** How far in the future a cache's time may be (clocks drift) before it isn't trusted. */
560
+ declare const CLOCK_SKEW_MS: number;
561
+ declare const WEEK_MS: number;
562
+ /** Force upgrade first (the store is the way out), then maintenance, else ok. Fails open. */
563
+ declare function evaluatePolicy(state: PolicyState, options: EvaluatePolicyOptions): UpdateVerdict;
564
+
565
+ /**
566
+ * The version signature: which binary and which update this is, in one
567
+ * line for Settings "About" and in a few lines for a support report.
568
+ *
569
+ * 1.4.0 (37) · preview · 3f2a9c1b
570
+ * 1.4.0 (37) · production · embedded
571
+ * 1.4.0 (37) · updates off
572
+ *
573
+ * A value the modules can't give is printed as `unknown`, never left out
574
+ * (an empty string counts as not given: expo-updates gives `""` for the
575
+ * runtime version in a development build); one that doesn't apply, with
576
+ * updates off, as `none`.
577
+ */
578
+
579
+ interface VersionSignature {
580
+ version: string | null;
581
+ build: string | null;
582
+ platform: string;
583
+ /** The build's channel (expo-updates). */
584
+ channel: string | null;
585
+ /** A channel picked on this device (a test build switched channels); `null` when it follows the build. */
586
+ requestedChannel: string | null;
587
+ runtimeVersion: string | null;
588
+ updateId: string | null;
589
+ /** `update`: a downloaded update runs. `embedded`: the binary's own code. `off`: updates are off (development, Expo Go). */
590
+ launch: "update" | "embedded" | "off";
591
+ /** expo-updates fell back to the embedded code after a failed launch. */
592
+ emergency: boolean;
593
+ /** One line, for an About row. */
594
+ short: string;
595
+ /** Several lines, for a support report. */
596
+ text: string;
597
+ }
598
+ interface VersionSignatureInput {
599
+ application?: ApplicationLike;
600
+ updates?: UpdatesModuleLike;
601
+ /** `Platform.OS`. */
602
+ platform: string;
603
+ requestedChannel?: string | null;
604
+ }
605
+ declare function versionSignature({ application, updates, platform, requestedChannel }: VersionSignatureInput): VersionSignature;
606
+
607
+ /**
608
+ * What's new: release notes shown once per version, remembered in storage.
609
+ *
610
+ * - First launch after an install (nothing stored): nothing shows; the
611
+ * version is remembered, so a new install never opens on a sheet.
612
+ * - A newer version than the one remembered: the notes for every version
613
+ * after it, up to this one, newest first. Closing them remembers this
614
+ * version. With no notes for it, the version is remembered once the policy
615
+ * has answered from the network (or there is no policy source), so notes
616
+ * that arrive late still show.
617
+ * - Storage that can't be read shows nothing (rather than every launch).
618
+ */
619
+
620
+ interface WhatsNewState {
621
+ status: "loading" | "hidden" | "showing";
622
+ notes: readonly ReleaseNote[];
623
+ }
624
+ interface WhatsNewOptions {
625
+ storage?: UpdatesStorageLike;
626
+ /** The installed version notes are matched against. `null`: never shows. */
627
+ version: string | null;
628
+ /** Default `"genesis:updates:seen-version"`. */
629
+ storageKey?: string;
630
+ onError?: UpdatesOnError;
631
+ }
632
+ interface WhatsNewStore extends Pick<Store<WhatsNewState>, "getState" | "subscribe"> {
633
+ /** Read the remembered version. Never rejects; runs once. */
634
+ load(): Promise<void>;
635
+ /** Recompute with the notes known now; `final` once no more notes can arrive. */
636
+ update(notes: readonly ReleaseNote[], final: boolean): void;
637
+ /** Close the notes and remember this version. */
638
+ dismiss(): void;
639
+ }
640
+ /** The notes after `seen`, up to `version`, newest first. */
641
+ declare function notesSince(notes: readonly ReleaseNote[], seen: string, version: string): ReleaseNote[];
642
+ declare function createWhatsNewStore(options: WhatsNewOptions): WhatsNewStore;
643
+
644
+ interface ForceUpgradeProps {
645
+ minimumVersion: string;
646
+ installedVersion: string;
647
+ /** The policy's store link for this platform (a minimum without one forces nothing). */
648
+ storeUrl: string;
649
+ /** Opens `storeUrl`. */
650
+ openStore: () => void;
651
+ /** Ask the policy again (it may have been lowered). */
652
+ retry: () => void;
653
+ checking: boolean;
654
+ }
655
+ /** Blocking: this version is below the minimum. "Update" opens the store. */
656
+ declare function ForceUpgradeScreen({ minimumVersion, installedVersion, openStore, retry, checking }: ForceUpgradeProps): react.JSX.Element;
657
+ interface MaintenanceProps {
658
+ /** The policy's message, or `null` for the default copy. */
659
+ message: string | null;
660
+ retry: () => void;
661
+ retrying: boolean;
662
+ }
663
+ /** Blocking: the policy says maintenance is on. "Try again" asks again. */
664
+ declare function MaintenanceScreen({ message, retry, retrying }: MaintenanceProps): react.JSX.Element;
665
+ interface UpdateBannerProps {
666
+ /** Reload into the downloaded update (refused while `canReload` is false). */
667
+ reload: () => void;
668
+ /** Hide the banner; the update still applies on the next cold start. */
669
+ dismiss: () => void;
670
+ /** False while something is mid-edit (`useBlockReload`) or your `canReload` says no. */
671
+ canReload: boolean;
672
+ /** The last tap was refused because something is mid-edit. */
673
+ blocked: boolean;
674
+ reloading: boolean;
675
+ }
676
+ /**
677
+ * Non-blocking: floats over the top of the screens while an update is
678
+ * downloaded. Liquid Glass on iOS 26 when the app passed expo-glass-effect
679
+ * (`modules.glass`); the solid secondary surface otherwise.
680
+ */
681
+ declare function UpdateBanner({ reload, dismiss, canReload, blocked, reloading }: UpdateBannerProps): react.JSX.Element;
682
+ interface WhatsNewProps {
683
+ /** Newest first. */
684
+ notes: readonly ReleaseNote[];
685
+ /** The installed version. */
686
+ version: string | null;
687
+ /** Close and remember this version. */
688
+ dismiss: () => void;
689
+ }
690
+ /** The release notes for this version, once, in a sheet. */
691
+ declare function WhatsNewSheet({ notes, dismiss }: WhatsNewProps): react.JSX.Element;
692
+
693
+ type UpdatesSlots = {
694
+ /** Blocking. Default `ForceUpgradeScreen`. */
695
+ forceUpgrade: ForceUpgradeProps;
696
+ /** Blocking. Default `MaintenanceScreen`. */
697
+ maintenance: MaintenanceProps;
698
+ /** Over your screens while an update is downloaded. Default `UpdateBanner`. */
699
+ updateBanner: UpdateBannerProps;
700
+ /** Once per version. Default `WhatsNewSheet`. */
701
+ whatsNew: WhatsNewProps;
702
+ };
703
+ interface UpdatesOptions {
704
+ /** `Platform.OS`. Force upgrade applies to `ios` and `android` only. */
705
+ platform: string;
706
+ /** `expo-application`: the installed version. Without it, nothing is ever forced and notes never show. */
707
+ application?: ApplicationLike;
708
+ /** `expo-updates`. Without it (or with updates off), no banner and no channel switching. */
709
+ updates?: UpdatesModuleLike;
710
+ /** Min versions, maintenance and notes. Without it, only `notes` and the banner. */
711
+ policy?: UpdatePolicySource;
712
+ /** AsyncStorage: the policy cache, the version whose notes were seen, the picked channel. */
713
+ storage?: UpdatesStorageLike;
714
+ /** `AppState`: ask again when the app comes to the front. */
715
+ appState?: UpdatesAppStateLike;
716
+ /** `Linking.openURL`, for the store link. */
717
+ openURL?: (url: string) => unknown;
718
+ /** Extra say over reloading (return false to hold it off); `useBlockReload` covers forms. */
719
+ canReload?: () => boolean;
720
+ /** Release notes written in the app, merged with the policy's. */
721
+ notes?: readonly ReleaseNote[];
722
+ /** `false` turns what's new off. */
723
+ whatsNew?: boolean;
724
+ /** Channel switching, for development and preview builds only: `{ enabled: __DEV__ || isPreview }`. */
725
+ channels?: {
726
+ enabled: boolean;
727
+ };
728
+ /** The longest the first policy load holds the splash. Default 3000 ms. */
729
+ policyTimeoutMs?: number;
730
+ /** How old a cached policy may be and still force an upgrade. Default 7 days. */
731
+ cacheMaxAgeMs?: number;
732
+ /** Ask for the policy again on coming to the front at most this often. Default 5 minutes. */
733
+ policyRefreshMs?: number;
734
+ /** Check for an update on coming to the front at most this often. Default 15 minutes. */
735
+ checkIntervalMs?: number;
736
+ /** `reloadAsync`'s reload screen (background color, spinner). */
737
+ reloadScreenOptions?: object;
738
+ /**
739
+ * The Settings row with the version signature the block contributes
740
+ * (`useBlockSettings`): its title and section, or `false` for none.
741
+ * Default `{ title: "Version", section: "about" }`.
742
+ */
743
+ versionRow?: false | {
744
+ title?: string;
745
+ section?: string;
746
+ };
747
+ /** Storage key prefix. Default `"genesis:updates"`. */
748
+ storageKeyPrefix?: string;
749
+ /** Every caught error, for reporting. None of them blocks anyone. */
750
+ onError?: UpdatesOnError;
751
+ now?: () => number;
752
+ }
753
+ /** The FeatureId of the updates block. */
754
+ declare const UPDATES_BLOCK_ID = "app-updates";
755
+ interface UpdatesBlock extends NativeBlock<typeof UPDATES_BLOCK_ID> {
756
+ readonly options: UpdatesOptions;
757
+ readonly policy: PolicyStore;
758
+ readonly checker: UpdateChecker;
759
+ readonly guard: ReloadGuard;
760
+ readonly whatsNew: WhatsNewStore;
761
+ /** Channel switching: `switchTo` throws unless `channels.enabled` was passed; `reset` always works. */
762
+ readonly channels: ChannelSwitcher;
763
+ /** The signature from the modules, without the picked channel (see `useVersionSignature`). */
764
+ readonly signature: VersionSignature;
765
+ /** Loads the policy and the seen version behind the splash; never fails, done within `policyTimeoutMs`. */
766
+ readonly bootStep: {
767
+ id: string;
768
+ run: () => Promise<void>;
769
+ timeoutMs: number;
770
+ };
771
+ /** `[bootStep]`, for a shell that runs blocks. */
772
+ readonly bootSteps: readonly {
773
+ id: string;
774
+ run: () => Promise<void>;
775
+ timeoutMs: number;
776
+ }[];
777
+ /** Start following the app's state; returns the stop. `UpdatesRoot` calls it. */
778
+ start(): () => void;
779
+ /** The block's root, for `GenesisNativeApp`. */
780
+ readonly Root: ComponentType<Omit<UpdatesRootProps, "block">>;
781
+ }
782
+ /** Build the block once, at module scope, next to your Supabase client. */
783
+ declare function createUpdates(options: UpdatesOptions): UpdatesBlock;
784
+ /** Provides the block to the hooks, without rendering any of its parts (rung 4). */
785
+ declare function UpdatesProvider({ block, children }: {
786
+ block: UpdatesBlock;
787
+ children?: ReactNode;
788
+ }): react.JSX.Element;
789
+ /** The block from `UpdatesProvider` / `UpdatesRoot` / `GenesisNativeApp`. Throws outside one. */
790
+ declare function useUpdates(): UpdatesBlock;
791
+ /**
792
+ * Hold reloads off while `active` (a form with unsaved changes):
793
+ * `useBlockReload(form.formState.isDirty)`. Does nothing without an updates
794
+ * block, so a screen can call it whether or not the block is on.
795
+ */
796
+ declare function useBlockReload(active: boolean): void;
797
+ declare function useUpdatePolicy(): PolicyState;
798
+ /** What the policy says now: `ok`, `forceUpgrade` or `maintenance`. */
799
+ declare function useUpdateVerdict(): UpdateVerdict;
800
+ /** The force-upgrade screen's props, or `null` when this version is supported. */
801
+ declare function useForceUpgrade(): ForceUpgradeProps | null;
802
+ /** The maintenance screen's props, or `null` when maintenance is off (or not known live). */
803
+ declare function useMaintenance(): MaintenanceProps | null;
804
+ declare function useUpdateCheck(): UpdateCheckState;
805
+ /** True when a reload would be allowed now. */
806
+ declare function useCanReload(): boolean;
807
+ /** The banner's props, or `null` when there is nothing to show. */
808
+ declare function useUpdateBanner(): UpdateBannerProps | null;
809
+ /** The notes to show now, or `null`. */
810
+ declare function useWhatsNew(): WhatsNewProps | null;
811
+ /** The version signature, with the channel picked on this device once it has been read. */
812
+ declare function useVersionSignature(): VersionSignature;
813
+ interface UpdatesRootProps {
814
+ block: UpdatesBlock;
815
+ slots?: Slots<UpdatesSlots>;
816
+ slotProps?: SlotPropsFor<UpdatesSlots>;
817
+ children?: ReactNode;
818
+ }
819
+ /** The block's parts around your screens. */
820
+ declare function UpdatesRoot({ block, slots, slotProps, children }: UpdatesRootProps): react.JSX.Element;
821
+
822
+ /**
823
+ * Deep links: turn an incoming universal link or app link into an app path.
824
+ *
825
+ * Meant for expo-router's `app/+native-intent.ts`:
826
+ *
827
+ * export function redirectSystemPath({ path }: { path: string }) {
828
+ * return normalizeLink(path, links);
829
+ * }
830
+ *
831
+ * A link to a path the map doesn't know goes to `"/"` (and `onUnknown`), so a
832
+ * stale or mistyped link opens the app instead of a "not found" screen. It
833
+ * never throws: anything it can't read is unknown.
834
+ */
835
+ /** A target path (`"/clients/:id"`, params filled in) or a function of the params. */
836
+ type LinkTarget = string | ((params: Record<string, string>) => string);
837
+ interface LinkOptions {
838
+ /** App schemes, without `://` (`["myapp"]`). `myapp://clients/1` is the path `/clients/1`. */
839
+ schemes?: readonly string[];
840
+ /** Hosts of universal / app links (`["example.com"]`). Other hosts are unknown. */
841
+ hosts?: readonly string[];
842
+ /**
843
+ * Incoming path pattern → app path. A segment `:name` captures one
844
+ * segment, a final `*` captures the rest (as `params["*"]`). The first
845
+ * match wins; `"/"` is always known.
846
+ */
847
+ map: Readonly<Record<string, LinkTarget>>;
848
+ /** Patterns to ignore (return `null`, so expo-router doesn't navigate), e.g. an auth callback. */
849
+ ignore?: readonly string[];
850
+ /** Told about each link that went to `"/"` because nothing matched. */
851
+ onUnknown?: (url: string) => void;
852
+ }
853
+ /** The app path for `url`, `"/"` when it is unknown, or `null` when it is ignored. */
854
+ declare function normalizeLink(url: string, options: LinkOptions): string | null;
855
+
856
+ /**
857
+ * Where the push block keeps devices and preferences: migration 0005 of
858
+ * @marktiderman/genesis-supabase, through the app's Supabase client and row
859
+ * level security. `supabasePushBackend(supabase)` is the one to use; tests
860
+ * (and an app with its own server) pass their own `PushBackend`.
861
+ */
862
+ /** A person's notification preferences, as the screens edit them. */
863
+ interface NotificationPreferences {
864
+ /** Category id → on or off. A category not listed has its default (`NotificationCategory.defaultOn`). */
865
+ categories: Record<string, boolean>;
866
+ quietHours: QuietHours;
867
+ /** Hide what a push says on the lock screen: the sender sends neutral text instead. */
868
+ hidePreviews: boolean;
869
+ }
870
+ interface QuietHours {
871
+ enabled: boolean;
872
+ /** Local time, `"HH:MM"` (24-hour). */
873
+ start: string;
874
+ /** Local time, `"HH:MM"`; before `start` means the quiet runs past midnight. */
875
+ end: string;
876
+ /** IANA time zone (`"Europe/London"`) the times are read in; required while enabled. */
877
+ timeZone: string | null;
878
+ }
879
+ declare const DEFAULT_PREFERENCES: NotificationPreferences;
880
+ interface PushBackend {
881
+ /** Keep this device's token against the signed-in person. */
882
+ register(token: string, platform: string, appVersion: string | null): Promise<void>;
883
+ /** Forget this device's token; works signed out (holding the token is the proof). */
884
+ forget(token: string): Promise<void>;
885
+ /** The signed-in person's preferences, or `null` before they have saved any. */
886
+ loadPreferences(userId: string): Promise<NotificationPreferences | null>;
887
+ savePreferences(userId: string, preferences: NotificationPreferences): Promise<void>;
888
+ }
889
+ type Result = {
890
+ data: unknown;
891
+ error: {
892
+ message?: string;
893
+ code?: string | null;
894
+ } | null;
895
+ };
896
+ /** The parts of a Supabase client the backend uses; a `SupabaseClient` fits, typed by your Database or not. */
897
+ interface PushSupabaseLike {
898
+ rpc: (fn: any, ...rest: any[]) => PromiseLike<Result>;
899
+ from(table: string): unknown;
900
+ }
901
+ /** A database refusal, with its SQLSTATE; the message is the database's own, never a token. */
902
+ declare class PushBackendError extends Error {
903
+ readonly operation: string;
904
+ readonly code: string | null;
905
+ constructor(operation: string, code: string | null);
906
+ }
907
+ /** Migration 0005's functions and tables, under the signed-in person's row level security. */
908
+ declare function supabasePushBackend(client: PushSupabaseLike): PushBackend;
909
+
910
+ /**
911
+ * What the push block reads from `expo-notifications` (SDK 58) and the app's
912
+ * backend, as the shapes it needs. Pass the real module
913
+ * (`import * as Notifications from "expo-notifications"`); tests pass fakes.
914
+ * Every call here was checked against expo-notifications 58.1's own type
915
+ * declarations.
916
+ */
917
+ /** `{ remove() }`, as every expo-notifications listener returns. */
918
+ interface SubscriptionLike {
919
+ remove(): void;
920
+ }
921
+ /** `NotificationPermissionsStatus`: Expo's `PermissionResponse` plus the iOS detail. */
922
+ interface PermissionStatusLike {
923
+ status: "granted" | "denied" | "undetermined" | string;
924
+ granted: boolean;
925
+ canAskAgain: boolean;
926
+ /** `IosAuthorizationStatus`: 0 not determined, 1 denied, 2 authorized, 3 provisional, 4 ephemeral. */
927
+ ios?: {
928
+ status: number;
929
+ } | null;
930
+ }
931
+ /** `NotificationPermissionsRequest`: iOS options; Android asks for posting notifications. */
932
+ interface PermissionRequestLike {
933
+ ios?: {
934
+ allowAlert?: boolean;
935
+ allowBadge?: boolean;
936
+ allowSound?: boolean;
937
+ allowProvisional?: boolean;
938
+ };
939
+ }
940
+ /** The parts of a received notification the block reads. */
941
+ interface NotificationLike {
942
+ request: {
943
+ identifier: string;
944
+ content: {
945
+ data?: Record<string, unknown> | null;
946
+ title?: string | null;
947
+ };
948
+ };
949
+ }
950
+ /** A tap (or an action) on a notification. */
951
+ interface NotificationResponseLike {
952
+ notification: NotificationLike;
953
+ actionIdentifier: string;
954
+ }
955
+ /** `NotificationBehavior` without the deprecated `shouldShowAlert`. */
956
+ interface NotificationBehaviorLike {
957
+ shouldShowBanner: boolean;
958
+ shouldShowList: boolean;
959
+ shouldPlaySound: boolean;
960
+ shouldSetBadge: boolean;
961
+ }
962
+ /** `NotificationChannelInput`, the fields the block sets. Importance and visibility are Expo's enum numbers. */
963
+ interface ChannelInputLike {
964
+ name: string;
965
+ importance: number;
966
+ description?: string | null;
967
+ /** `AndroidNotificationVisibility`: 1 public, 2 private (content hidden on a secure lock screen), 3 secret. */
968
+ lockscreenVisibility?: number;
969
+ showBadge?: boolean;
970
+ }
971
+ /** `expo-notifications` (SDK 58). */
972
+ interface NotificationsModuleLike {
973
+ getPermissionsAsync(): Promise<PermissionStatusLike>;
974
+ requestPermissionsAsync(request?: PermissionRequestLike): Promise<PermissionStatusLike>;
975
+ /** Pass the EAS project id: without one there is no Expo push token. */
976
+ getExpoPushTokenAsync(options: {
977
+ projectId: string;
978
+ }): Promise<{
979
+ data: string;
980
+ }>;
981
+ /** Fires when the OS hands the app a new device token; the Expo token is fetched again then. */
982
+ addPushTokenListener(listener: (token: unknown) => void): SubscriptionLike;
983
+ /** Android only (a no-op elsewhere). Must run before the permission prompt and the token on Android 13+. */
984
+ setNotificationChannelAsync(channelId: string, channel: ChannelInputLike): Promise<unknown>;
985
+ setNotificationHandler(handler: {
986
+ handleNotification: (notification: NotificationLike) => Promise<NotificationBehaviorLike>;
987
+ } | null): void;
988
+ addNotificationResponseReceivedListener(listener: (response: NotificationResponseLike) => void): SubscriptionLike;
989
+ /** The tap that launched the app (synchronous since SDK 53). */
990
+ getLastNotificationResponse(): NotificationResponseLike | null;
991
+ clearLastNotificationResponse(): void;
992
+ setBadgeCountAsync(count: number): Promise<boolean>;
993
+ getBadgeCountAsync(): Promise<number>;
994
+ /** `"expo.modules.notifications.actions.DEFAULT"`: the person tapped the notification itself. */
995
+ DEFAULT_ACTION_IDENTIFIER: string;
996
+ }
997
+ /** AsyncStorage, MMKV's async adapter, or a stand-in. */
998
+ interface PushStorageLike {
999
+ getItem(key: string): Promise<string | null>;
1000
+ setItem(key: string, value: string): Promise<void>;
1001
+ removeItem?(key: string): Promise<void>;
1002
+ }
1003
+ /** React Native's `AppState`. */
1004
+ interface PushAppStateLike {
1005
+ addEventListener(type: "change", listener: (state: string) => void): {
1006
+ remove(): void;
1007
+ };
1008
+ }
1009
+ /** Where a reported error came from. */
1010
+ type PushErrorSource = "permission" | "register" | "forget" | "preferences" | "badge" | "channels" | "routing" | "storage" | "setup";
1011
+ type PushOnError = (error: Error, source: PushErrorSource) => void;
1012
+
1013
+ /**
1014
+ * Notification permission, and the soft pre-prompt that comes before the
1015
+ * system's.
1016
+ *
1017
+ * The system prompt can be shown once: a person who says no there has to find
1018
+ * the app in Settings to change it. So the block never asks at launch. The
1019
+ * app calls `prompt.offer()` at the moment push is worth it (they sent their
1020
+ * first message, set a reminder) and the block shows its own sheet first,
1021
+ * saying why; only "Turn on" in that sheet (a tap) brings up the system
1022
+ * prompt. "Not now" waits `cooldownMs` before the sheet can show again, and
1023
+ * after `maxDeclines` it never offers again (Settings still can).
1024
+ */
1025
+
1026
+ /**
1027
+ * `unknown` before the first read; `undetermined` the system can still ask;
1028
+ * `granted`; `provisional` (iOS delivers quietly to Notification Center);
1029
+ * `denied` only Settings can change; `unavailable` no notifications module.
1030
+ */
1031
+ type PushPermissionStatus = "unknown" | "undetermined" | "granted" | "provisional" | "denied" | "unavailable";
1032
+ interface PermissionState {
1033
+ status: PushPermissionStatus;
1034
+ /** The system prompt is up. */
1035
+ requesting: boolean;
1036
+ }
1037
+ interface PermissionStore extends Store<PermissionState> {
1038
+ /** Read the status without asking (no prompt). */
1039
+ refresh(): Promise<PushPermissionStatus>;
1040
+ /** Show the system prompt (only after a tap); resolves to the new status. */
1041
+ request(): Promise<PushPermissionStatus>;
1042
+ }
1043
+ /** Expo's answer as one status. Android "denied but can ask again" is still askable. */
1044
+ declare function statusOf(response: PermissionStatusLike): PushPermissionStatus;
1045
+ declare const canPush: (status: PushPermissionStatus) => status is "granted" | "provisional";
1046
+ declare function createPermissionStore(options: {
1047
+ notifications?: NotificationsModuleLike;
1048
+ request?: PermissionRequestLike;
1049
+ /** Runs before the system prompt (Android 13 shows it only once a channel exists). */
1050
+ beforeRequest?: () => Promise<void>;
1051
+ onError?: PushOnError;
1052
+ }): PermissionStore;
1053
+ interface PromptOptions {
1054
+ /** After "Not now", how long before the sheet may show again. Default 3 days. */
1055
+ cooldownMs?: number;
1056
+ /** After this many "Not now"s it never offers again. Default 3. */
1057
+ maxDeclines?: number;
1058
+ /** `offer()` is ignored this long after the block starts, so no screen can ask at launch. Default 10 s. */
1059
+ launchGraceMs?: number;
1060
+ }
1061
+ interface PromptState {
1062
+ visible: boolean;
1063
+ /** What `offer(reason)` said push is for here, for the sheet's copy. */
1064
+ reason: string | null;
1065
+ }
1066
+ interface PromptStore extends Store<PromptState> {
1067
+ /**
1068
+ * Show the soft prompt if it is a good moment: signed in, the system can
1069
+ * still ask, not at launch, not too soon after "Not now". Resolves to
1070
+ * whether it showed.
1071
+ */
1072
+ offer(reason?: string): Promise<boolean>;
1073
+ /** "Turn on": hides the sheet and shows the system prompt. */
1074
+ accept(): Promise<PushPermissionStatus>;
1075
+ /** "Not now": hides the sheet and remembers it. */
1076
+ decline(): Promise<void>;
1077
+ hide(): void;
1078
+ }
1079
+ declare function createPromptStore(options: PromptOptions & {
1080
+ permission: PermissionStore;
1081
+ storage?: PushStorageLike;
1082
+ storageKey: string;
1083
+ signedIn: () => boolean;
1084
+ /** When the block started (ms), or null before. */
1085
+ startedAt: () => number | null;
1086
+ now: () => number;
1087
+ onError?: PushOnError;
1088
+ }): PromptStore;
1089
+
1090
+ /**
1091
+ * The signed-in person's notification preferences: categories, quiet hours
1092
+ * and privacy previews. Loaded when they sign in, saved on every change
1093
+ * (shown at once, put back if the save fails), cleared when they sign out.
1094
+ * The server reads the saved row; nothing here decides what is delivered.
1095
+ */
1096
+
1097
+ /** Android importance for a category's channel (Expo's `AndroidImportance`). */
1098
+ type ChannelImportance = "min" | "low" | "default" | "high" | "max";
1099
+ /** One kind of notification the app sends, which a person can turn off. */
1100
+ interface NotificationCategory {
1101
+ /** Lower case letters, digits, ".", "_" or "-" (`messages`). Also its Android channel id, and the sender's `category`. */
1102
+ id: string;
1103
+ title: string;
1104
+ description?: string;
1105
+ /** On for someone who never chose. Default true; pass the same `defaultOn` to the sender. */
1106
+ defaultOn?: boolean;
1107
+ /** Android channel importance. Default `"default"`. */
1108
+ importance?: ChannelImportance;
1109
+ }
1110
+ type PreferencesStatus = "idle" | "loading" | "ready" | "error";
1111
+ interface PreferencesState {
1112
+ status: PreferencesStatus;
1113
+ preferences: NotificationPreferences;
1114
+ saving: boolean;
1115
+ /** The last load or save that failed (the change was put back). */
1116
+ error: Error | null;
1117
+ }
1118
+ interface PreferencesStore extends Store<PreferencesState> {
1119
+ load(userId: string): Promise<void>;
1120
+ /** Apply a change now and save it; on failure it is put back and `error` set. */
1121
+ update(change: (current: NotificationPreferences) => NotificationPreferences): Promise<boolean>;
1122
+ reset(): void;
1123
+ isOn(categoryId: string): boolean;
1124
+ }
1125
+ /** Checks a change before it is shown or saved, so the server never has to refuse it. */
1126
+ declare function validPreferences(p: NotificationPreferences): boolean;
1127
+ declare function createPreferencesStore(options: {
1128
+ backend: PushBackend;
1129
+ categories: readonly NotificationCategory[];
1130
+ onError?: PushOnError;
1131
+ }): PreferencesStore;
1132
+
1133
+ /**
1134
+ * This device's Expo push token, kept against whoever is signed in.
1135
+ *
1136
+ * sync(userId) permission granted → (Android) channels → token → backend.register,
1137
+ * again when the token changes and at most hourly otherwise (last seen)
1138
+ * forget() backend.forget(token) on sign-out, an account switch or deletion;
1139
+ * a forget that fails offline is kept and retried at the next start
1140
+ *
1141
+ * The token is remembered on the phone (storage) with the user it was
1142
+ * registered for, so it can be forgotten after the session has ended. Every
1143
+ * step runs in order on one queue, so a sign-out never races a registration.
1144
+ */
1145
+
1146
+ type RegistrationStatus = "idle" | "registering" | "registered" | "unavailable" | "error";
1147
+ interface RegistrationState {
1148
+ status: RegistrationStatus;
1149
+ /**
1150
+ * Why it isn't registered: `no_project_id` (the build has no EAS project id, so there is no
1151
+ * Expo push token), `no_token` (the OS gave none: a simulator without push, or no network),
1152
+ * `backend` (the server refused or couldn't be reached).
1153
+ */
1154
+ problem: "no_project_id" | "no_token" | "backend" | null;
1155
+ }
1156
+ interface Registration extends Store<RegistrationState> {
1157
+ sync(userId: string, options?: {
1158
+ force?: boolean;
1159
+ }): Promise<void>;
1160
+ forget(): Promise<void>;
1161
+ /** Retry forgets that failed earlier (offline sign-out). */
1162
+ flush(): Promise<void>;
1163
+ /** This device's token, once known (the person's own; never shown in logs). */
1164
+ token(): string | null;
1165
+ }
1166
+ declare function createRegistration(options: {
1167
+ notifications?: NotificationsModuleLike;
1168
+ backend: PushBackend;
1169
+ projectId?: string | null;
1170
+ platform: string;
1171
+ appVersion?: string | null;
1172
+ storage?: PushStorageLike;
1173
+ storageKeyPrefix: string;
1174
+ /** Creates the Android channels; runs before the token is asked for. */
1175
+ ensureChannels: () => Promise<void>;
1176
+ /** Is this user still the one signed in? A sync for someone who has signed out is dropped. */
1177
+ isCurrent: (userId: string) => boolean;
1178
+ now: () => number;
1179
+ onError?: PushOnError;
1180
+ }): Registration;
1181
+
1182
+ interface PushPromptProps {
1183
+ /** What `offer(reason)` said push is for here ("to hear when someone replies"), or null. */
1184
+ reason: string | null;
1185
+ /** "Turn on": shows the system prompt. */
1186
+ accept: () => void;
1187
+ /** "Not now": asks again after the cooldown, never more than `maxDeclines` times. */
1188
+ decline: () => void;
1189
+ title?: string;
1190
+ body?: string;
1191
+ acceptLabel?: string;
1192
+ declineLabel?: string;
1193
+ }
1194
+ declare function PushPromptSheet({ reason, accept, decline, title, body, acceptLabel, declineLabel }: PushPromptProps): react.JSX.Element;
1195
+
1196
+ /** An Android channel beyond the categories' own (one per category is made for you). */
1197
+ interface PushChannel {
1198
+ id: string;
1199
+ name: string;
1200
+ description?: string;
1201
+ importance?: ChannelImportance;
1202
+ }
1203
+ type PushSlots = {
1204
+ /** The soft pre-prompt, shown after `usePushPrompt().offer()`. Default `PushPromptSheet`. */
1205
+ pushPrompt: PushPromptProps;
1206
+ };
1207
+ interface PushOptions {
1208
+ /** `import * as Notifications from "expo-notifications"`. Without it the block does nothing (web, tests). */
1209
+ notifications?: NotificationsModuleLike;
1210
+ /** `Platform.OS`. */
1211
+ platform: string;
1212
+ /** The EAS project id (`Constants.expoConfig?.extra?.eas?.projectId`). Without it there is no push token; the block says so once through `onError`. */
1213
+ projectId?: string | null;
1214
+ /** The installed version, kept with the device row (`Application.nativeApplicationVersion`). */
1215
+ appVersion?: string | null;
1216
+ /** Devices and preferences: `supabasePushBackend(supabase)`. */
1217
+ backend: PushBackend;
1218
+ /** The kinds of notification the app sends; each is a toggle in NotificationSettings and an Android channel. */
1219
+ categories?: readonly NotificationCategory[];
1220
+ /** More Android channels. A `default` channel is always made. */
1221
+ channels?: readonly PushChannel[];
1222
+ /** AsyncStorage: this device's token (to forget it after sign-out), the prompt's "Not now"s. */
1223
+ storage?: PushStorageLike;
1224
+ /** `AppState`: read the permission again on coming to the front (the person may have changed it in Settings). */
1225
+ appState?: PushAppStateLike;
1226
+ /** `Linking.openSettings`, for "Turn on in Settings" once the system won't ask again. */
1227
+ openSettings?: () => unknown;
1228
+ /** Where a tap goes: the app's deep-link options, so a pushed path is normalized like any link. */
1229
+ links?: LinkOptions;
1230
+ /** Open a tapped notification's path (`router.push`). Without it a tap opens nothing, and says so through `onError`. */
1231
+ onOpen?: (path: string) => void;
1232
+ /** Where a tap that arrives signed out waits for sign-in: the same `linkHold` as GenesisNativeApp's `links.hold`. */
1233
+ hold?: LinkHold;
1234
+ /**
1235
+ * The Settings row the block contributes (`useBlockSettings`), or `false`
1236
+ * for none. Default `{ title: "Notifications", section: "notifications", href: "/settings/notifications" }`;
1237
+ * its subtitle is the permission's status (`statusLabels`).
1238
+ */
1239
+ settingsRow?: false | {
1240
+ title?: string;
1241
+ section?: string;
1242
+ href?: string;
1243
+ };
1244
+ /** The words for each permission status (the Settings row's subtitle, NotificationSettings' default). */
1245
+ statusLabels?: Partial<Record<PushPermissionStatus, string>>;
1246
+ /** Every tap and action, after routing (an action button, analytics). */
1247
+ onResponse?: (response: NotificationResponseLike) => void;
1248
+ /** Show a notification that arrives while the app is open? Default yes; `useSuppressNotifications` adds rules. */
1249
+ foreground?: (notification: NotificationLike) => boolean;
1250
+ /** The iOS permissions asked for. Default alert, badge and sound. */
1251
+ permissions?: PermissionRequestLike;
1252
+ prompt?: PromptOptions;
1253
+ /** Clear the badge whenever the app comes to the front. Default false (the server sets it). */
1254
+ clearBadgeOnForeground?: boolean;
1255
+ /** Storage key prefix. Default `"genesis:push"`. */
1256
+ storageKeyPrefix?: string;
1257
+ /** Every caught error, for reporting; none of them breaks the app. */
1258
+ onError?: PushOnError;
1259
+ now?: () => number;
1260
+ }
1261
+ /** The FeatureId of the push block. */
1262
+ declare const PUSH_BLOCK_ID = "push";
1263
+ interface PushBlock extends NativeBlock<typeof PUSH_BLOCK_ID> {
1264
+ readonly options: PushOptions;
1265
+ readonly permission: PermissionStore;
1266
+ readonly registration: Registration;
1267
+ readonly preferences: PreferencesStore;
1268
+ readonly prompt: PromptStore;
1269
+ readonly categories: readonly NotificationCategory[];
1270
+ readonly badge: {
1271
+ set(count: number): Promise<void>;
1272
+ clear(): Promise<void>;
1273
+ };
1274
+ /** Follow this signed-in user (null when signed out). `PushRoot` calls it from the session. */
1275
+ setUser(userId: string | null): void;
1276
+ /** Hide notifications that match while the app is open (the thread on screen); returns the removal. */
1277
+ suppress(match: (data: Record<string, unknown>, notification: NotificationLike) => boolean): () => void;
1278
+ /** Make the Android channels (once; again after a failure). */
1279
+ ensureChannels(): Promise<void>;
1280
+ /** Start the foreground handler and the listeners; returns the stop. `PushRoot` calls it. */
1281
+ start(): () => void;
1282
+ /** Reads the permission behind the splash, at most a second; never fails the boot. */
1283
+ readonly bootSteps: readonly BootStep[];
1284
+ readonly Root: ComponentType<NativeBlockRootProps>;
1285
+ }
1286
+ /** Build the block once, at module scope, next to your Supabase client. */
1287
+ declare function createPush(options: PushOptions): PushBlock;
1288
+ /** Provides the block to the hooks, without rendering any of its parts (rung 4). */
1289
+ declare function PushProvider({ block, children }: {
1290
+ block: PushBlock;
1291
+ children?: ReactNode;
1292
+ }): react.JSX.Element;
1293
+ /** The block from `PushProvider` / `PushRoot` / `GenesisNativeApp`. Throws outside one. */
1294
+ declare function usePush(): PushBlock;
1295
+ /** The block, or null when push is off: for a screen that works either way. */
1296
+ declare function useOptionalPush(): PushBlock | null;
1297
+ declare function usePushPermissionState(): PermissionState;
1298
+ declare function usePushRegistration(): RegistrationState;
1299
+ declare function useNotificationPreferences(): PreferencesState & {
1300
+ update: PreferencesStore["update"];
1301
+ };
1302
+ /** The soft prompt: `offer("to hear when someone replies")` at the right moment. */
1303
+ declare function usePushPrompt(): PromptState & Pick<PromptStore, "offer" | "accept" | "decline">;
1304
+ /** The badge: `set(unread)`, `clear()`. */
1305
+ declare function useBadge(): PushBlock["badge"];
1306
+ /**
1307
+ * Hide notifications that match while this screen is open (`data` is the
1308
+ * notification's `data`): a chat screen hides pushes for its own thread.
1309
+ * Does nothing without a push block.
1310
+ */
1311
+ declare function useSuppressNotifications(match: ((data: Record<string, unknown>) => boolean) | null): void;
1312
+ interface PushRootProps {
1313
+ block: PushBlock;
1314
+ /** Where a tap that arrives signed out waits for sign-in (GenesisNativeApp passes its `links.hold`). */
1315
+ hold?: LinkHold;
1316
+ /** Open a path; used when the block's own `onOpen` isn't given. */
1317
+ onOpen?: (path: string) => void;
1318
+ slots?: Slots<PushSlots>;
1319
+ slotProps?: SlotPropsFor<PushSlots>;
1320
+ children?: ReactNode;
1321
+ }
1322
+ /** The block's parts around your screens. Must be inside GenesisNativeApp's session (or a `SessionProvider`). */
1323
+ declare function PushRoot({ block, hold, onOpen, slots, slotProps, children }: PushRootProps): react.JSX.Element;
1324
+
1325
+ /** `expo-splash-screen`, or a stand-in. Call `preventAutoHideAsync()` at module scope yourself too. */
1326
+ interface SplashModule {
1327
+ preventAutoHideAsync?: () => Promise<unknown>;
1328
+ hideAsync: () => Promise<unknown>;
1329
+ }
1330
+ interface BootScreenProps {
1331
+ status: BootStatus;
1332
+ }
1333
+ interface SignedOutProps {
1334
+ /** Your screens. The default renders them, for an expo-router layout that redirects. */
1335
+ children: ReactNode;
1336
+ }
1337
+ type NativeAppSlots = UpdatesSlots & PushSlots & DevtoolsSlots & {
1338
+ /** Shown while booting: under the splash at launch, on its own after "Try again". Default `BootScreen`. */
1339
+ boot: BootScreenProps;
1340
+ /** A failed boot, a fatal global error or a render error. Default `FatalScreen`. */
1341
+ fatal: FatalScreenProps;
1342
+ /** The boundary around your screens. Default `GenesisErrorBoundary`. */
1343
+ errorBoundary: GenesisErrorBoundaryProps;
1344
+ /** What a signed-out user sees. Default your screens (gate them with `useSession`). */
1345
+ signedOut: SignedOutProps;
1346
+ };
1347
+ interface NativeQueryClientOptions {
1348
+ /** Your own client. Default `createNativeQueryClient(config)`. */
1349
+ client?: QueryClient;
1350
+ config?: QueryClientConfig;
1351
+ }
1352
+ interface GenesisNativeAppProps {
1353
+ brand: CanonicalBrand;
1354
+ /** A Supabase client (from `createGenesisSupabase` in `@marktiderman/genesis-supabase`). */
1355
+ supabase: AuthClientLike & SupabaseClientInput;
1356
+ /** The data provider. Default `createSupabaseProvider(supabase)`. */
1357
+ provider?: DataProvider;
1358
+ queryClient?: NativeQueryClientOptions;
1359
+ /**
1360
+ * Save queries to the phone: `persistQueries({ storage, keys, buster })`
1361
+ * from `@marktiderman/genesis-native/persist`. The saved cache belongs to
1362
+ * one user: it is restored only for that user and cleared when the user
1363
+ * changes (sign-out, a session that ended while the app was closed, an
1364
+ * account switch).
1365
+ */
1366
+ persist?: (client: QueryClient) => QueryPersistence;
1367
+ /** `expo-splash-screen`: hidden once boot has finished and the session is known. */
1368
+ splash?: SplashModule;
1369
+ /**
1370
+ * `KeyboardProvider` from react-native-keyboard-controller, mounted once
1371
+ * around the app. Its keyboard views (`keyboardScroll` included) do nothing
1372
+ * without it: give both or neither. Read once: it wraps the whole app, so
1373
+ * a different component later would remount everything below it. To pass
1374
+ * it props, define the wrapper at module scope, not inline.
1375
+ */
1376
+ keyboardProvider?: ComponentType<{
1377
+ children: ReactNode;
1378
+ }>;
1379
+ /** `KeyboardAwareScrollView` from react-native-keyboard-controller, for form screens. Needs `keyboardProvider`. */
1380
+ keyboardScroll?: ComponentType<KeyboardScrollProps>;
1381
+ /**
1382
+ * `KeyboardToolbar` from react-native-keyboard-controller: previous / next /
1383
+ * done above the keyboard on Genesis's form layouts. Needs `keyboardScroll`
1384
+ * (and so `keyboardProvider`); without them it is ignored.
1385
+ */
1386
+ keyboardToolbar?: ComponentType<KeyboardToolbarProps>;
1387
+ /**
1388
+ * The optional native modules the app installed, for Genesis components to
1389
+ * use (`GenesisModulesProvider` from `@marktiderman/genesis-ui-native`):
1390
+ * FlashList, expo-image, Reanimated, expo-glass-effect, expo-symbols, a
1391
+ * lucide icon set, the @expo/ui date picker. Each one left out falls back
1392
+ * to plain React Native. Read once.
1393
+ */
1394
+ modules?: GenesisModules;
1395
+ /**
1396
+ * `@gorhom/bottom-sheet` v5, to opt every `NativeSheet` into it (snap
1397
+ * points, drag between heights): `import * as BottomSheet from "@gorhom/bottom-sheet"`,
1398
+ * then `bottomSheet={BottomSheet}`. The app mounts `NativeSheetProvider`
1399
+ * (and so the library's `BottomSheetModalProvider`) for you; the app's root
1400
+ * must still be inside gesture-handler's `GestureHandlerRootView`, with
1401
+ * Reanimated set up. Leave it out and sheets are React Native's own modal
1402
+ * (an iOS page sheet), which needs neither. Read once.
1403
+ */
1404
+ bottomSheet?: NativeSheetLibrary;
1405
+ /**
1406
+ * Toasts (`useToast()` from `@marktiderman/genesis-ui-native`): `NativeToastProvider` is
1407
+ * mounted around the screens, at the top of the screen by default (a bottom toast would sit on
1408
+ * a tab bar). Pass its options to place it, or `null` to mount your own. Read once.
1409
+ */
1410
+ toast?: Pick<NativeToastProviderProps, "position" | "disableAnimation"> | null;
1411
+ /** Follow a link held while signed out, once the user signs in. */
1412
+ links?: {
1413
+ hold: LinkHold;
1414
+ onRelease: (path: string) => void;
1415
+ };
1416
+ /** Run before first render, in order (wipe-on-reinstall, hydrate stores, fonts). Read once. */
1417
+ steps?: readonly BootStep[];
1418
+ bootOptions?: BootOptions;
1419
+ /** Longest wait for Supabase's `getSession()` before using the stored session (offline). Default 5000 ms. */
1420
+ sessionWaitMs?: number;
1421
+ /** Default `globalThis.ErrorUtils`. */
1422
+ errorUtils?: ErrorUtilsLike;
1423
+ /** Report unhandled promise rejections to `onError`. Default Hermes's tracker; `null` turns it off. */
1424
+ rejectionTracker?: EnableRejectionTracker | null;
1425
+ /** `AppState` and NetInfo, so queries refetch on focus and know the network. */
1426
+ lifecycle?: QueryLifecycleOptions;
1427
+ /**
1428
+ * Every caught error, after the blocks' own `onError` (the observability
1429
+ * block, `createObservability` from `/observability`, reports to Sentry
1430
+ * there). A throwing reporter is ignored.
1431
+ */
1432
+ onError?: OnError;
1433
+ /** After a signed-in user signs out, once the query cache is cleared. */
1434
+ onSignedOut?: () => void;
1435
+ /**
1436
+ * The updates block: `createUpdates(…)` from `@marktiderman/genesis-native/updates`
1437
+ * (force upgrade, maintenance, the update banner, what's new). Its policy
1438
+ * loads behind the splash for at most `policyTimeoutMs` and never fails the
1439
+ * boot. Read once; leave it out to turn the block off.
1440
+ */
1441
+ updates?: UpdatesBlock;
1442
+ /**
1443
+ * Blocks to run (`createUpdates(…)` is one). Resolved once at start: a
1444
+ * block whose requirement is missing, or two blocks contributing the same
1445
+ * slot, nav entry or Settings row, shows the fatal screen and reports a
1446
+ * `BlockError` to `onError`. Read once.
1447
+ */
1448
+ blocks?: readonly NativeBlock[];
1449
+ /**
1450
+ * Your features (FeatureIds) that blocks may require but that aren't
1451
+ * blocks. A block requiring anything that is neither one of `blocks` nor
1452
+ * listed here is a `BlockError`. Read once.
1453
+ */
1454
+ appFeatures?: readonly string[];
1455
+ slots?: Slots<NativeAppSlots>;
1456
+ slotProps?: SlotPropsFor<NativeAppSlots>;
1457
+ children?: ReactNode;
1458
+ }
1459
+ /** The default boot slot: a plain loading view (the splash covers it at launch). */
1460
+ declare function BootScreen(_props: BootScreenProps): react.JSX.Element;
1461
+ /**
1462
+ * The root of an Expo app: theme, keyboard, data, boot behind the splash, errors, session and held links.
1463
+ *
1464
+ * @stability Beta
1465
+ */
1466
+ declare function GenesisNativeApp(props: GenesisNativeAppProps): react.JSX.Element;
1467
+
1468
+ /** What a block's `Root` is given: the app's slots, with every block's fills merged under them. @stability Beta */
1469
+ interface NativeBlockRootProps {
1470
+ slots?: Slots<NativeAppSlots>;
1471
+ slotProps?: SlotPropsFor<NativeAppSlots>;
1472
+ children?: ReactNode;
1473
+ }
1474
+ /**
1475
+ * A block a native shell can run. `contributes.slots` fills `GenesisNativeApp`
1476
+ * slots by name (the app's own `slots` win).
1477
+ *
1478
+ * @stability Beta
1479
+ */
1480
+ interface NativeBlock<Id extends string = string> extends BlockDef<Id, ComponentType<never>> {
1481
+ /** Run behind the splash after the app's own `steps`, before the session. A step that fails fails the boot, so keep them bounded. */
1482
+ readonly bootSteps?: readonly BootStep[];
1483
+ /** Mounted around the screens, inside the shell's error boundary. The first block's root is the outermost. */
1484
+ readonly Root?: ComponentType<NativeBlockRootProps>;
1485
+ /**
1486
+ * Every error the shell catches (render, global, boot, rejection), before
1487
+ * the app's own `onError`. The observability block reports them here. A
1488
+ * throw is ignored.
1489
+ */
1490
+ readonly onError?: (error: Error, report: ErrorReport) => void;
1491
+ }
1492
+ /**
1493
+ * The resolved blocks: their ids, nav entries, Settings rows, routes and the
1494
+ * rest, each tagged with its FeatureId. Throws outside `GenesisNativeApp`.
1495
+ *
1496
+ * @stability Beta
1497
+ */
1498
+ declare function useBlocks(): ResolvedBlocks<NativeBlock>;
1499
+ /** A Settings section as `SettingsPage` from `@marktiderman/genesis-ui-native` takes it. @stability Beta */
1500
+ interface BlockSettingsSection<R extends {
1501
+ id: string;
1502
+ title: string;
1503
+ } = SettingsRowLike> {
1504
+ id: string;
1505
+ title?: string;
1506
+ footnote?: string;
1507
+ rows: R[];
1508
+ }
1509
+ /** The row fields a block can fill. @stability Beta */
1510
+ interface SettingsRowLike {
1511
+ id: string;
1512
+ title: string;
1513
+ subtitle?: string;
1514
+ showChevron?: boolean;
1515
+ onPress?: () => void;
1516
+ testID?: string;
1517
+ }
1518
+ /** @stability Beta */
1519
+ interface UseBlockSettingsOptions {
1520
+ /** Open a row's `href` (expo-router's `router.push`). Without it, rows with an `href` aren't pressable. */
1521
+ navigate?: (href: string) => void;
1522
+ /** Titles for sections a block names that `sections` doesn't have. Default the section id. */
1523
+ sectionTitles?: Readonly<Record<string, string>>;
1524
+ }
1525
+ /**
1526
+ * The app's Settings sections with every block's rows added: each row goes
1527
+ * at the end of the section its `section` names (by `order`, then block
1528
+ * order), and a section no one has yet is added after the rest.
1529
+ *
1530
+ * Calls each row's `useSubtitle` hook, so call it unconditionally at the top
1531
+ * of the Settings screen. The blocks are fixed for the life of the app, so
1532
+ * the hooks run in the same order on every render.
1533
+ *
1534
+ * @stability Beta
1535
+ */
1536
+ declare function useBlockSettings<R extends SettingsRowLike>(sections: readonly BlockSettingsSection<R>[], options?: UseBlockSettingsOptions): BlockSettingsSection<R | SettingsRowLike>[];
1537
+ /** The merge `useBlockSettings` does, without hooks (subtitles already read). @stability Beta */
1538
+ declare function mergeBlockSettings<R extends SettingsRowLike>(sections: readonly BlockSettingsSection<R>[], rows: readonly Contributed<BlockSettingsRow>[], subtitles?: readonly (string | null | undefined)[], options?: UseBlockSettingsOptions): BlockSettingsSection<R | SettingsRowLike>[];
1539
+
1540
+ export { defaultRejectionTracker as $, type AuthClientLike as A, type BlockSettingsSection as B, type SessionStatus as C, type DevtoolsFeedbackProps as D, type EnableRejectionTracker as E, FatalScreen as F, GenesisErrorBoundary as G, type HeldLinkAuth as H, type SessionStore as I, type SessionStoreOptions as J, type SettingsRowLike as K, type LinkHold as L, type SignedOutProps as M, type NativeBlock as N, type OnError as O, PlainFatalScreen as P, type SplashModule as Q, type UseBlockSettingsOptions as R, SessionProvider as S, type UseBootReturn as T, type UpdatesBlock as U, type VersionSignature as V, type UseHeldLinkOptions as W, createBoot as X, createLinkHold as Y, createSessionStore as Z, defaultErrorUtils as _, type DevtoolsSlots as a, usePushPrompt as a$, installGlobalErrorHandler as a0, installRejectionTracker as a1, mergeBlockSettings as a2, normalizeLink as a3, useBlockSettings as a4, useBlocks as a5, useBoot as a6, useHeldLink as a7, useSession as a8, type PushPermissionStatus as a9, type PushOnError as aA, type PushOptions as aB, type PushPromptProps as aC, PushPromptSheet as aD, PushProvider as aE, PushRoot as aF, type PushRootProps as aG, type PushSlots as aH, type PushStorageLike as aI, type PushSupabaseLike as aJ, type Registration as aK, type RegistrationState as aL, type RegistrationStatus as aM, type SubscriptionLike as aN, canPush as aO, createPermissionStore as aP, createPreferencesStore as aQ, createPromptStore as aR, createPush as aS, createRegistration as aT, statusOf as aU, supabasePushBackend as aV, useBadge as aW, useNotificationPreferences as aX, useOptionalPush as aY, usePush as aZ, usePushPermissionState as a_, type NotificationLike as aa, type NotificationResponseLike as ab, type QuietHours as ac, type ChannelImportance as ad, type ChannelInputLike as ae, DEFAULT_PREFERENCES as af, type NotificationBehaviorLike as ag, type NotificationCategory as ah, type NotificationPreferences as ai, type NotificationsModuleLike as aj, type PermissionRequestLike as ak, type PermissionState as al, type PermissionStatusLike as am, type PermissionStore as an, type PreferencesState as ao, type PreferencesStatus as ap, type PreferencesStore as aq, type PromptOptions as ar, type PromptState as as, type PromptStore as at, type PushAppStateLike as au, type PushBackend as av, PushBackendError as aw, type PushBlock as ax, type PushChannel as ay, type PushErrorSource as az, AuthGate as b, useMaintenance as b$, usePushRegistration as b0, useSuppressNotifications as b1, validPreferences as b2, type ApplicationLike as b3, CLOCK_SKEW_MS as b4, type ChannelSwitchResult as b5, type ChannelSwitcher as b6, type ChannelSwitcherOptions as b7, type EvaluatePolicyOptions as b8, type ForceUpgradeProps as b9, type UpdatesOptions as bA, UpdatesProvider as bB, UpdatesRoot as bC, type UpdatesRootProps as bD, type UpdatesSlots as bE, type UpdatesStorageLike as bF, type VersionSignatureInput as bG, WEEK_MS as bH, type WhatsNewOptions as bI, type WhatsNewProps as bJ, WhatsNewSheet as bK, type WhatsNewState as bL, type WhatsNewStore as bM, createChannelSwitcher as bN, createPolicyStore as bO, createReloadGuard as bP, createUpdateChecker as bQ, createUpdates as bR, createWhatsNewStore as bS, evaluatePolicy as bT, notesSince as bU, sanitizePolicy as bV, staticPolicySource as bW, supabasePolicySource as bX, useBlockReload as bY, useCanReload as bZ, useForceUpgrade as b_, ForceUpgradeScreen as ba, type MaintenanceProps as bb, MaintenanceScreen as bc, type PolicyClientLike as bd, type PolicyOrigin as be, type PolicyState as bf, type PolicyStore as bg, type PolicyStoreOptions as bh, type ReleaseNote as bi, type ReloadGuard as bj, type SupabasePolicySourceOptions as bk, UPDATES_BLOCK_ID as bl, UpdateBanner as bm, type UpdateBannerProps as bn, type UpdateCheckState as bo, type UpdateCheckStatus as bp, type UpdateChecker as bq, type UpdateCheckerOptions as br, type UpdatePlatform as bs, type UpdatePolicy as bt, type UpdatePolicySource as bu, type UpdateVerdict as bv, type UpdatesAppStateLike as bw, type UpdatesErrorSource as bx, type UpdatesModuleLike as by, type UpdatesOnError as bz, type AuthGateProps as c, useUpdateBanner as c0, useUpdateCheck as c1, useUpdatePolicy as c2, useUpdateVerdict as c3, useUpdates as c4, useVersionSignature as c5, useWhatsNew as c6, versionSignature as c7, type BootController as d, type BootOptions as e, BootProvider as f, BootScreen as g, type BootScreenProps as h, type BootState as i, type BootStatus as j, type BootStep as k, type BootStepContext as l, BootTimeoutError as m, type ErrorReport as n, type ErrorUtilsLike as o, type FatalScreenProps as p, type GenesisErrorBoundaryProps as q, GenesisNativeApp as r, type GenesisNativeAppProps as s, type GlobalErrorHandlerOptions as t, type LinkOptions as u, type LinkTarget as v, type NativeAppSlots as w, type NativeBlockRootProps as x, type NativeQueryClientOptions as y, type SessionState as z };