@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,90 @@
1
+ import { createContext, useContext, useSyncExternalStore } from 'react';
2
+ import { jsx, Fragment } from 'react/jsx-runtime';
3
+
4
+ // src/auth.tsx
5
+ var DEFAULT_WAIT_MS = 5e3;
6
+ function defaultUserId(session) {
7
+ const id = session?.user?.id;
8
+ return typeof id === "string" ? id : null;
9
+ }
10
+ function isNetworkError(error) {
11
+ const e = error;
12
+ return e?.name === "AuthRetryableFetchError" || e?.status === 0;
13
+ }
14
+ function storedSessionReader(client) {
15
+ return async () => {
16
+ const auth = client.auth;
17
+ if (!auth.storage || typeof auth.storageKey !== "string") return null;
18
+ const raw = await auth.storage.getItem(auth.storageKey);
19
+ if (typeof raw !== "string") return raw ?? null;
20
+ try {
21
+ return JSON.parse(raw);
22
+ } catch {
23
+ return null;
24
+ }
25
+ };
26
+ }
27
+ function createSessionStore(client, options = {}) {
28
+ const { onUserChange, waitMs = DEFAULT_WAIT_MS, userIdOf = defaultUserId } = options;
29
+ const readStored = options.readStoredSession ?? storedSessionReader(client);
30
+ const listeners = /* @__PURE__ */ new Set();
31
+ let state = { status: "loading", session: null, userId: null, offline: false };
32
+ let version = 0;
33
+ const set = (session, offline = false) => {
34
+ const userId = session ? userIdOf(session) : null;
35
+ const previous = state;
36
+ if (previous.status !== "loading" && previous.session === session && previous.offline === offline) return;
37
+ state = { status: session ? "signedIn" : "signedOut", session, userId, offline };
38
+ for (const listener of listeners) listener();
39
+ if (previous.status !== "loading" && previous.userId !== userId) onUserChange?.(previous.userId, userId);
40
+ };
41
+ return {
42
+ getState: () => state,
43
+ subscribe(listener) {
44
+ listeners.add(listener);
45
+ return () => listeners.delete(listener);
46
+ },
47
+ async load() {
48
+ const startedAt = version;
49
+ let timer;
50
+ const timedOut = new Promise((resolve) => {
51
+ timer = setTimeout(() => resolve("timeout"), waitMs);
52
+ });
53
+ const result = await Promise.race([client.auth.getSession(), timedOut]).finally(() => clearTimeout(timer));
54
+ if (version !== startedAt) return;
55
+ if (result !== "timeout" && !result.error) return set(result.data.session);
56
+ if (result !== "timeout" && !isNetworkError(result.error)) {
57
+ const error = result.error;
58
+ throw error instanceof Error ? error : new Error("Could not read the session.", { cause: error });
59
+ }
60
+ const stored = await readStored().catch(() => null);
61
+ if (version !== startedAt) return;
62
+ set(stored, stored !== null);
63
+ },
64
+ start() {
65
+ const { data } = client.auth.onAuthStateChange((_event, session) => {
66
+ version++;
67
+ set(session);
68
+ });
69
+ return () => data.subscription.unsubscribe();
70
+ }
71
+ };
72
+ }
73
+ var SessionContext = createContext(null);
74
+ function SessionProvider({ store, children }) {
75
+ return /* @__PURE__ */ jsx(SessionContext.Provider, { value: store, children });
76
+ }
77
+ function useSession() {
78
+ const store = useContext(SessionContext);
79
+ if (!store) throw new Error("useSession must be used inside <GenesisNativeApp> or <SessionProvider>.");
80
+ return useSyncExternalStore(store.subscribe, store.getState, store.getState);
81
+ }
82
+ function AuthGate({ signedIn, signedOut, loading = null }) {
83
+ const { status } = useSession();
84
+ if (status === "loading") return /* @__PURE__ */ jsx(Fragment, { children: loading });
85
+ return /* @__PURE__ */ jsx(Fragment, { children: status === "signedIn" ? signedIn : signedOut });
86
+ }
87
+
88
+ export { AuthGate, SessionProvider, createSessionStore, useSession };
89
+ //# sourceMappingURL=chunk-NGYNYJPL.js.map
90
+ //# sourceMappingURL=chunk-NGYNYJPL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/auth.tsx"],"names":[],"mappings":"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","file":"chunk-NGYNYJPL.js","sourcesContent":["/**\n * Auth gate: the Supabase session as app state.\n *\n * `createSessionStore(supabase)` reads the session once (`load`, a boot step,\n * so the splash stays up until it is known) and follows `onAuthStateChange`.\n * `useSession()` reads it; `AuthGate` renders `signedIn` or `signedOut`.\n *\n * Offline, `getSession()` can wait about 30 seconds on a token refresh that\n * can't succeed and then fail with a retryable error, although the session is\n * still on the phone. So `load` waits at most `waitMs` (5 s) and, on a timeout\n * or a network error, uses the stored session: the user stays signed in and\n * sees their cached data, and Supabase's own events correct it once online.\n *\n * With expo-router, gate in a layout instead and let the router render:\n *\n * const { status } = useSession();\n * if (status === \"signedOut\") return <Redirect href=\"/sign-in\" />;\n * return <Stack />;\n */\nimport { createContext, useContext, useSyncExternalStore, type ReactNode } from \"react\";\n\n/** The parts of a Supabase client the gate uses; a `SupabaseClient` fits. */\nexport interface AuthClientLike<S = unknown> {\n auth: {\n getSession(): Promise<{ data: { session: S | null }; error?: unknown }>;\n onAuthStateChange(callback: (event: string, session: S | null) => void): {\n data: { subscription: { unsubscribe(): void } };\n };\n };\n}\n\nexport type SessionStatus = \"loading\" | \"signedIn\" | \"signedOut\";\n\nexport interface SessionState<S = unknown> {\n status: SessionStatus;\n session: S | null;\n /** The signed-in user's id, or null. */\n userId: string | null;\n /** True when the session came from the phone because Supabase couldn't be reached. */\n offline: boolean;\n}\n\nexport interface SessionStore<S = unknown> {\n getState(): SessionState<S>;\n subscribe(listener: () => void): () => void;\n /** Read the current session (the boot step). Throws only on a non-network error. */\n load(): Promise<void>;\n /** Follow sign-in and sign-out; returns the unsubscribe. */\n start(): () => void;\n}\n\nexport interface SessionStoreOptions<S = unknown> {\n /**\n * The signed-in user changed after the session was first known: signed\n * out, signed in, or switched account. `previous` and `next` are user ids\n * (null when nobody). Clear caches here.\n */\n onUserChange?: (previous: string | null, next: string | null) => void;\n /** Longest wait for `getSession()` before using the stored session. Default 5000 ms. */\n waitMs?: number;\n /**\n * Read the session saved on the phone without refreshing it. Default: from\n * the Supabase client's own storage (`auth.storage` under `auth.storageKey`).\n */\n readStoredSession?: () => Promise<S | null>;\n /** The user id in a session. Default `session.user.id`. */\n userIdOf?: (session: S) => string | null;\n}\n\nconst DEFAULT_WAIT_MS = 5000;\n\nfunction defaultUserId(session: unknown): string | null {\n const id = (session as { user?: { id?: unknown } } | null)?.user?.id;\n return typeof id === \"string\" ? id : null;\n}\n\n/** A network failure (supabase-js `AuthRetryableFetchError`, status 0), as opposed to a real refusal. */\nfunction isNetworkError(error: unknown): boolean {\n const e = error as { name?: unknown; status?: unknown } | null;\n return e?.name === \"AuthRetryableFetchError\" || e?.status === 0;\n}\n\n/** The session supabase-js keeps under `auth.storageKey`, read without a refresh. */\nfunction storedSessionReader<S>(client: AuthClientLike<S>): () => Promise<S | null> {\n return async () => {\n const auth = client.auth as { storage?: { getItem(key: string): unknown }; storageKey?: unknown };\n if (!auth.storage || typeof auth.storageKey !== \"string\") return null;\n const raw = await auth.storage.getItem(auth.storageKey);\n if (typeof raw !== \"string\") return (raw as S | null) ?? null;\n try {\n return JSON.parse(raw) as S;\n } catch {\n return null;\n }\n };\n}\n\n/** The Supabase session as a store: `load` reads it once, `start` follows auth events. */\nexport function createSessionStore<S = unknown>(client: AuthClientLike<S>, options: SessionStoreOptions<S> = {}): SessionStore<S> {\n const { onUserChange, waitMs = DEFAULT_WAIT_MS, userIdOf = defaultUserId } = options;\n const readStored = options.readStoredSession ?? storedSessionReader(client);\n const listeners = new Set<() => void>();\n let state: SessionState<S> = { status: \"loading\", session: null, userId: null, offline: false };\n // Bumped by every auth event, so a slow `getSession()` can't overwrite a newer one.\n let version = 0;\n\n const set = (session: S | null, offline = false) => {\n const userId = session ? userIdOf(session) : null;\n const previous = state;\n if (previous.status !== \"loading\" && previous.session === session && previous.offline === offline) return;\n state = { status: session ? \"signedIn\" : \"signedOut\", session, userId, offline };\n for (const listener of listeners) listener();\n if (previous.status !== \"loading\" && previous.userId !== userId) onUserChange?.(previous.userId, userId);\n };\n\n return {\n getState: () => state,\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n async load() {\n const startedAt = version;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<\"timeout\">((resolve) => {\n timer = setTimeout(() => resolve(\"timeout\"), waitMs);\n });\n const result = await Promise.race([client.auth.getSession(), timedOut]).finally(() => clearTimeout(timer));\n if (version !== startedAt) return; // an auth event already told us\n if (result !== \"timeout\" && !result.error) return set(result.data.session);\n if (result !== \"timeout\" && !isNetworkError(result.error)) {\n const error = result.error;\n throw error instanceof Error ? error : new Error(\"Could not read the session.\", { cause: error });\n }\n // Offline (or Supabase is slow): carry on with what the phone has.\n const stored = await readStored().catch(() => null);\n if (version !== startedAt) return;\n set(stored, stored !== null);\n },\n start() {\n const { data } = client.auth.onAuthStateChange((_event, session) => {\n version++;\n set(session);\n });\n return () => data.subscription.unsubscribe();\n },\n };\n}\n\nconst SessionContext = createContext<SessionStore | null>(null);\n\n/** Makes a session store readable with `useSession()`. */\nexport function SessionProvider({ store, children }: { store: SessionStore; children: ReactNode }) {\n return <SessionContext.Provider value={store}>{children}</SessionContext.Provider>;\n}\n\n/** The session and whether it is known yet. Inside `GenesisNativeApp` or a `SessionProvider`. */\nexport function useSession<S = unknown>(): SessionState<S> {\n const store = useContext(SessionContext);\n if (!store) throw new Error(\"useSession must be used inside <GenesisNativeApp> or <SessionProvider>.\");\n return useSyncExternalStore(store.subscribe, store.getState, store.getState) as SessionState<S>;\n}\n\nexport interface AuthGateProps {\n signedIn: ReactNode;\n signedOut: ReactNode;\n /** While the session is still being read. Default nothing (the splash is up). */\n loading?: ReactNode;\n}\n\n/** Render one tree for a signed-in user and another for everyone else. */\nexport function AuthGate({ signedIn, signedOut, loading = null }: AuthGateProps) {\n const { status } = useSession();\n if (status === \"loading\") return <>{loading}</>;\n return <>{status === \"signedIn\" ? signedIn : signedOut}</>;\n}\n"]}
@@ -0,0 +1,95 @@
1
+ // src/links.ts
2
+ var URL_PATTERN = /^([a-z][a-z0-9+.-]*):\/\/([^/?#]*)([^?#]*)(\?[^#]*)?(#.*)?$/i;
3
+ function parse(url, schemes, hosts) {
4
+ const trimmed = url.trim();
5
+ const match = URL_PATTERN.exec(trimmed);
6
+ if (!match) {
7
+ if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) return null;
8
+ const noHash = trimmed.split("#")[0] ?? "";
9
+ const at = noHash.indexOf("?");
10
+ return at < 0 ? { path: noHash, query: "" } : { path: noHash.slice(0, at), query: noHash.slice(at) };
11
+ }
12
+ const [, scheme = "", host = "", path = "", query = ""] = match;
13
+ const lowerScheme = scheme.toLowerCase();
14
+ if (lowerScheme === "http" || lowerScheme === "https") {
15
+ const bareHost = host.toLowerCase().replace(/:\d+$/, "");
16
+ if (!hosts.some((h) => h.toLowerCase() === bareHost)) return null;
17
+ return { path, query };
18
+ }
19
+ if (!schemes.some((s) => s.toLowerCase() === lowerScheme)) return null;
20
+ return { path: `/${host}${path}`, query };
21
+ }
22
+ function clean(path) {
23
+ const joined = `/${path}`.replace(/\/{2,}/g, "/");
24
+ return joined.length > 1 ? joined.replace(/\/$/, "") : joined;
25
+ }
26
+ function matchPattern(pattern, path) {
27
+ const want = clean(pattern).split("/").slice(1);
28
+ const have = path.split("/").slice(1);
29
+ const params = {};
30
+ for (let i = 0; i < want.length; i++) {
31
+ const segment = want[i];
32
+ if (segment === "*" && i === want.length - 1) {
33
+ params["*"] = have.slice(i).join("/");
34
+ return params;
35
+ }
36
+ const value = have[i];
37
+ if (value === void 0) return null;
38
+ if (segment.startsWith(":")) params[segment.slice(1)] = value;
39
+ else if (segment !== value) return null;
40
+ }
41
+ return want.length === have.length ? params : null;
42
+ }
43
+ function fill(target, params) {
44
+ if (typeof target === "function") return target(params);
45
+ return target.replace(
46
+ /:([A-Za-z_][A-Za-z0-9_]*)|\*$/g,
47
+ (token, name) => name === void 0 ? params["*"] ?? "" : params[name] ?? token
48
+ );
49
+ }
50
+ function isDotSegment(segment) {
51
+ let decoded = segment;
52
+ try {
53
+ decoded = decodeURIComponent(segment);
54
+ } catch {
55
+ }
56
+ return decoded === "." || decoded === "..";
57
+ }
58
+ function withQuery(path, query) {
59
+ if (!query || query === "?") return path;
60
+ return path.includes("?") ? `${path}&${query.slice(1)}` : `${path}${query}`;
61
+ }
62
+ function normalizeLink(url, options) {
63
+ const { schemes = [], hosts = [], map, ignore = [], onUnknown } = options;
64
+ const unknown = () => {
65
+ onUnknown?.(url);
66
+ return "/";
67
+ };
68
+ let parsed;
69
+ try {
70
+ parsed = parse(url, schemes, hosts);
71
+ } catch {
72
+ parsed = null;
73
+ }
74
+ if (!parsed) return unknown();
75
+ const path = clean(parsed.path);
76
+ if (path.split("/").some(isDotSegment)) {
77
+ return unknown();
78
+ }
79
+ if (ignore.some((pattern) => matchPattern(pattern, path))) return null;
80
+ if (path === "/") return withQuery("/", parsed.query);
81
+ for (const [pattern, target] of Object.entries(map)) {
82
+ const params = matchPattern(pattern, path);
83
+ if (!params) continue;
84
+ try {
85
+ return withQuery(fill(target, params), parsed.query);
86
+ } catch {
87
+ return unknown();
88
+ }
89
+ }
90
+ return unknown();
91
+ }
92
+
93
+ export { normalizeLink };
94
+ //# sourceMappingURL=chunk-VKTIHNAV.js.map
95
+ //# sourceMappingURL=chunk-VKTIHNAV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/links.ts"],"names":[],"mappings":"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","file":"chunk-VKTIHNAV.js","sourcesContent":["/**\n * Deep links: turn an incoming universal link or app link into an app path.\n *\n * Meant for expo-router's `app/+native-intent.ts`:\n *\n * export function redirectSystemPath({ path }: { path: string }) {\n * return normalizeLink(path, links);\n * }\n *\n * A link to a path the map doesn't know goes to `\"/\"` (and `onUnknown`), so a\n * stale or mistyped link opens the app instead of a \"not found\" screen. It\n * never throws: anything it can't read is unknown.\n */\n\n/** A target path (`\"/clients/:id\"`, params filled in) or a function of the params. */\nexport type LinkTarget = string | ((params: Record<string, string>) => string);\n\nexport interface LinkOptions {\n /** App schemes, without `://` (`[\"myapp\"]`). `myapp://clients/1` is the path `/clients/1`. */\n schemes?: readonly string[];\n /** Hosts of universal / app links (`[\"example.com\"]`). Other hosts are unknown. */\n hosts?: readonly string[];\n /**\n * Incoming path pattern → app path. A segment `:name` captures one\n * segment, a final `*` captures the rest (as `params[\"*\"]`). The first\n * match wins; `\"/\"` is always known.\n */\n map: Readonly<Record<string, LinkTarget>>;\n /** Patterns to ignore (return `null`, so expo-router doesn't navigate), e.g. an auth callback. */\n ignore?: readonly string[];\n /** Told about each link that went to `\"/\"` because nothing matched. */\n onUnknown?: (url: string) => void;\n}\n\nconst URL_PATTERN = /^([a-z][a-z0-9+.-]*):\\/\\/([^/?#]*)([^?#]*)(\\?[^#]*)?(#.*)?$/i;\n\ninterface Parsed {\n path: string;\n query: string;\n}\n\nfunction parse(url: string, schemes: readonly string[], hosts: readonly string[]): Parsed | null {\n const trimmed = url.trim();\n const match = URL_PATTERN.exec(trimmed);\n if (!match) {\n // Already a path (expo-router passes paths as well as URLs).\n if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) return null;\n const noHash = trimmed.split(\"#\")[0] ?? \"\";\n const at = noHash.indexOf(\"?\");\n return at < 0 ? { path: noHash, query: \"\" } : { path: noHash.slice(0, at), query: noHash.slice(at) };\n }\n const [, scheme = \"\", host = \"\", path = \"\", query = \"\"] = match;\n const lowerScheme = scheme.toLowerCase();\n if (lowerScheme === \"http\" || lowerScheme === \"https\") {\n const bareHost = host.toLowerCase().replace(/:\\d+$/, \"\");\n if (!hosts.some((h) => h.toLowerCase() === bareHost)) return null;\n return { path, query };\n }\n if (!schemes.some((s) => s.toLowerCase() === lowerScheme)) return null;\n // In `myapp://clients/1`, \"clients\" sits where a host would.\n return { path: `/${host}${path}`, query };\n}\n\nfunction clean(path: string): string {\n const joined = `/${path}`.replace(/\\/{2,}/g, \"/\");\n return joined.length > 1 ? joined.replace(/\\/$/, \"\") : joined;\n}\n\nfunction matchPattern(pattern: string, path: string): Record<string, string> | null {\n const want = clean(pattern).split(\"/\").slice(1);\n const have = path.split(\"/\").slice(1);\n const params: Record<string, string> = {};\n for (let i = 0; i < want.length; i++) {\n const segment = want[i]!;\n if (segment === \"*\" && i === want.length - 1) {\n params[\"*\"] = have.slice(i).join(\"/\");\n return params;\n }\n const value = have[i];\n if (value === undefined) return null;\n if (segment.startsWith(\":\")) params[segment.slice(1)] = value;\n else if (segment !== value) return null;\n }\n return want.length === have.length ? params : null;\n}\n\nfunction fill(target: LinkTarget, params: Record<string, string>): string {\n if (typeof target === \"function\") return target(params);\n return target.replace(/:([A-Za-z_][A-Za-z0-9_]*)|\\*$/g, (token, name: string | undefined) =>\n name === undefined ? (params[\"*\"] ?? \"\") : (params[name] ?? token),\n );\n}\n\nfunction isDotSegment(segment: string): boolean {\n let decoded = segment;\n try {\n decoded = decodeURIComponent(segment);\n } catch {\n // Malformed escapes stay as they are.\n }\n return decoded === \".\" || decoded === \"..\";\n}\n\nfunction withQuery(path: string, query: string): string {\n if (!query || query === \"?\") return path;\n return path.includes(\"?\") ? `${path}&${query.slice(1)}` : `${path}${query}`;\n}\n\n/** The app path for `url`, `\"/\"` when it is unknown, or `null` when it is ignored. */\nexport function normalizeLink(url: string, options: LinkOptions): string | null {\n const { schemes = [], hosts = [], map, ignore = [], onUnknown } = options;\n const unknown = () => {\n onUnknown?.(url);\n return \"/\";\n };\n let parsed: Parsed | null;\n try {\n parsed = parse(url, schemes, hosts);\n } catch {\n parsed = null;\n }\n if (!parsed) return unknown();\n const path = clean(parsed.path);\n // \".\" and \"..\" would let a link climb out of the path it matched (a `*` capture included).\n if (path.split(\"/\").some(isDotSegment)) {\n return unknown();\n }\n if (ignore.some((pattern) => matchPattern(pattern, path))) return null;\n if (path === \"/\") return withQuery(\"/\", parsed.query);\n for (const [pattern, target] of Object.entries(map)) {\n const params = matchPattern(pattern, path);\n if (!params) continue;\n try {\n return withQuery(fill(target, params), parsed.query);\n } catch {\n return unknown();\n }\n }\n return unknown();\n}\n"]}
@@ -8,8 +8,8 @@ function createNativeQueryClient(config = {}) {
8
8
  ...config,
9
9
  defaultOptions: {
10
10
  ...rest,
11
- queries: { networkMode: "always", gcTime: DAY_MS, staleTime: 6e4, ...queries },
12
- mutations: { networkMode: "always", ...mutations }
11
+ queries: { networkMode: "offlineFirst", gcTime: DAY_MS, staleTime: 6e4, ...queries },
12
+ mutations: { networkMode: "offlineFirst", ...mutations }
13
13
  }
14
14
  });
15
15
  }
@@ -26,11 +26,14 @@ function connectQueryLifecycle({ appState, netInfo }) {
26
26
  return () => {
27
27
  if (appState) focusManager.setEventListener(() => () => {
28
28
  });
29
- if (netInfo) onlineManager.setEventListener(() => () => {
30
- });
29
+ if (netInfo) {
30
+ onlineManager.setEventListener(() => () => {
31
+ });
32
+ onlineManager.setOnline(true);
33
+ }
31
34
  };
32
35
  }
33
36
 
34
37
  export { DAY_MS, connectQueryLifecycle, createNativeQueryClient };
35
- //# sourceMappingURL=chunk-MMTPWUWI.js.map
36
- //# sourceMappingURL=chunk-MMTPWUWI.js.map
38
+ //# sourceMappingURL=chunk-VTJDIKFC.js.map
39
+ //# sourceMappingURL=chunk-VTJDIKFC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/query.ts"],"names":[],"mappings":";;;AAgBO,IAAM,MAAA,GAAS,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK;AAc9B,SAAS,uBAAA,CAAwB,MAAA,GAA4B,EAAC,EAAgB;AACnF,EAAA,MAAM,EAAE,SAAS,SAAA,EAAW,GAAG,MAAK,GAAI,MAAA,CAAO,kBAAkB,EAAC;AAClE,EAAA,OAAO,IAAI,WAAA,CAAY;AAAA,IACrB,GAAG,MAAA;AAAA,IACH,cAAA,EAAgB;AAAA,MACd,GAAG,IAAA;AAAA,MACH,OAAA,EAAS,EAAE,WAAA,EAAa,cAAA,EAAgB,QAAQ,MAAA,EAAQ,SAAA,EAAW,GAAA,EAAQ,GAAG,OAAA,EAAQ;AAAA,MACtF,SAAA,EAAW,EAAE,WAAA,EAAa,cAAA,EAAgB,GAAG,SAAA;AAAU;AACzD,GACD,CAAA;AACH;AAuCO,SAAS,qBAAA,CAAsB,EAAE,QAAA,EAAU,OAAA,EAAQ,EAAsC;AAC9F,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,YAAA,CAAa,gBAAA,CAAiB,CAAC,UAAA,KAAe;AAC5C,MAAA,MAAM,YAAA,GAAe,SAAS,gBAAA,CAAiB,QAAA,EAAU,CAAC,KAAA,KAAU,UAAA,CAAW,KAAA,KAAU,QAAQ,CAAC,CAAA;AAClG,MAAA,OAAO,MAAM,aAAa,MAAA,EAAO;AAAA,IACnC,CAAC,CAAA;AAAA,EACH;AACA,EAAA,IAAI,OAAA,EAAS;AACX,IAAA,aAAA,CAAc,gBAAA,CAAiB,CAAC,SAAA,KAAc,OAAA,CAAQ,gBAAA,CAAiB,CAAC,KAAA,KAAU,SAAA,CAAU,KAAA,CAAM,WAAA,KAAgB,KAAK,CAAC,CAAC,CAAA;AAAA,EAC3H;AACA,EAAA,OAAO,MAAM;AAEX,IAAA,IAAI,QAAA,EAAU,YAAA,CAAa,gBAAA,CAAiB,MAAM,MAAM;AAAA,IAAC,CAAC,CAAA;AAC1D,IAAA,IAAI,OAAA,EAAS;AACX,MAAA,aAAA,CAAc,gBAAA,CAAiB,MAAM,MAAM;AAAA,MAAC,CAAC,CAAA;AAG7C,MAAA,aAAA,CAAc,UAAU,IAAI,CAAA;AAAA,IAC9B;AAAA,EACF,CAAA;AACF","file":"chunk-VTJDIKFC.js","sourcesContent":["/**\n * Query: a TanStack Query client with offline-safe defaults, and the contract\n * for saving it to the phone. The persister itself is in\n * `@marktiderman/genesis-native/persist`, so an app that saves nothing doesn't\n * install TanStack's persister packages.\n */\nimport { QueryClient, focusManager, onlineManager, type QueryClientConfig } from \"@tanstack/react-query\";\n\n/** The part of AsyncStorage (or any async key-value store) Genesis uses. */\nexport interface AsyncStorageLike {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n removeItem(key: string): Promise<void>;\n}\n\n/** One day in milliseconds: the default max age of the saved query cache. */\nexport const DAY_MS = 24 * 60 * 60 * 1000;\n\n/**\n * A QueryClient for a phone. `networkMode: \"offlineFirst\"` runs a query or\n * mutation once whatever the network says, then pauses its retries while the\n * device is offline (`onlineManager`, which `connectQueryLifecycle` feeds from\n * NetInfo), continues them when it is back, and refetches stale queries on\n * reconnect. So offline, a query with saved data keeps showing it\n * (`fetchStatus: \"paused\"`); one with nothing saved stays `pending` and paused\n * until the network returns (check `isPaused`, not only `isPending`, before\n * showing a spinner); and a mutation, which TanStack doesn't retry by default,\n * fails at once instead of waiting. `gcTime` matches the persisted max age, or\n * restored queries would be collected before they are used. Your `config` wins.\n */\nexport function createNativeQueryClient(config: QueryClientConfig = {}): QueryClient {\n const { queries, mutations, ...rest } = config.defaultOptions ?? {};\n return new QueryClient({\n ...config,\n defaultOptions: {\n ...rest,\n queries: { networkMode: \"offlineFirst\", gcTime: DAY_MS, staleTime: 60_000, ...queries },\n mutations: { networkMode: \"offlineFirst\", ...mutations },\n },\n });\n}\n\n/**\n * The saved query cache. A `scope` (the signed-in user's id, `\"\"` for nobody)\n * is part of what is saved: a cache saved for one user is discarded, on disk\n * too, when another scope restores it.\n */\nexport interface QueryPersistence {\n /** Load the saved cache into the client, if it was saved for `scope`. */\n restore(scope: string): Promise<void>;\n /** Save on every cache change, for `scope`; returns the unsubscribe. */\n subscribe(scope: string): () => void;\n /**\n * Delete the saved cache. Clear the QueryClient first: the subscription\n * then saves the empty cache, so a save already in flight can't leave the\n * old queries on disk.\n */\n clear(): Promise<void>;\n}\n\n/** React Native's `AppState`, or a stand-in. */\nexport interface AppStateLike {\n addEventListener(type: \"change\", listener: (state: string) => void): { remove(): void };\n}\n\n/** `@react-native-community/netinfo`'s default export, or a stand-in. */\nexport interface NetInfoLike {\n addEventListener(listener: (state: { isConnected: boolean | null }) => void): () => void;\n}\n\nexport interface QueryLifecycleOptions {\n appState?: AppStateLike;\n netInfo?: NetInfoLike;\n}\n\n/**\n * Tell TanStack Query when the app comes to the front (refetch stale\n * queries) and when the network comes and goes. Returns the cleanup.\n */\nexport function connectQueryLifecycle({ appState, netInfo }: QueryLifecycleOptions): () => void {\n if (appState) {\n focusManager.setEventListener((setFocused) => {\n const subscription = appState.addEventListener(\"change\", (state) => setFocused(state === \"active\"));\n return () => subscription.remove();\n });\n }\n if (netInfo) {\n onlineManager.setEventListener((setOnline) => netInfo.addEventListener((state) => setOnline(state.isConnected !== false)));\n }\n return () => {\n // Replacing the listener runs the cleanup of the one set above.\n if (appState) focusManager.setEventListener(() => () => {});\n if (netInfo) {\n onlineManager.setEventListener(() => () => {});\n // Nothing reports the network any more: don't leave paused queries waiting on the last\n // \"offline\" NetInfo said (offlineFirst pauses retries while offline).\n onlineManager.setOnline(true);\n }\n };\n}\n"]}
@@ -0,0 +1,29 @@
1
+ // src/updates/store.ts
2
+ function createStore(initial) {
3
+ let state = initial;
4
+ const listeners = /* @__PURE__ */ new Set();
5
+ return {
6
+ getState: () => state,
7
+ setState(next) {
8
+ const merged = { ...state, ...next };
9
+ if (Object.keys(merged).every((key) => Object.is(merged[key], state[key]))) return;
10
+ state = merged;
11
+ for (const listener of [...listeners]) listener();
12
+ },
13
+ subscribe(listener) {
14
+ listeners.add(listener);
15
+ return () => {
16
+ listeners.delete(listener);
17
+ };
18
+ }
19
+ };
20
+ }
21
+ function delay(ms) {
22
+ return new Promise((resolve) => {
23
+ if (Number.isFinite(ms)) setTimeout(resolve, Math.max(0, ms));
24
+ });
25
+ }
26
+
27
+ export { createStore, delay };
28
+ //# sourceMappingURL=chunk-XE7LC7K6.js.map
29
+ //# sourceMappingURL=chunk-XE7LC7K6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/updates/store.ts"],"names":[],"mappings":";AAOO,SAAS,YAA8B,OAAA,EAAsB;AAClE,EAAA,IAAI,KAAA,GAAQ,OAAA;AACZ,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAgB;AACtC,EAAA,OAAO;AAAA,IACL,UAAU,MAAM,KAAA;AAAA,IAChB,SAAS,IAAA,EAAM;AACb,MAAA,MAAM,MAAA,GAAS,EAAE,GAAG,KAAA,EAAO,GAAG,IAAA,EAAK;AACnC,MAAA,IAAK,OAAO,IAAA,CAAK,MAAM,CAAA,CAAkB,KAAA,CAAM,CAAC,GAAA,KAAQ,MAAA,CAAO,EAAA,CAAG,MAAA,CAAO,GAAG,CAAA,EAAG,KAAA,CAAM,GAAG,CAAC,CAAC,CAAA,EAAG;AAC7F,MAAA,KAAA,GAAQ,MAAA;AACR,MAAA,KAAA,MAAW,QAAA,IAAY,CAAC,GAAG,SAAS,GAAG,QAAA,EAAS;AAAA,IAClD,CAAA;AAAA,IACA,UAAU,QAAA,EAAU;AAClB,MAAA,SAAA,CAAU,IAAI,QAAQ,CAAA;AACtB,MAAA,OAAO,MAAM;AACX,QAAA,SAAA,CAAU,OAAO,QAAQ,CAAA;AAAA,MAC3B,CAAA;AAAA,IACF;AAAA,GACF;AACF;AAGO,SAAS,MAAM,EAAA,EAA2B;AAC/C,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,KAAY;AAC9B,IAAA,IAAI,MAAA,CAAO,QAAA,CAAS,EAAE,CAAA,EAAG,UAAA,CAAW,SAAS,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,EAAE,CAAC,CAAA;AAAA,EAC9D,CAAC,CAAA;AACH","file":"chunk-XE7LC7K6.js","sourcesContent":["/** A tiny external store for `useSyncExternalStore`: immutable state, one notify per change. */\nexport interface Store<T> {\n getState(): T;\n setState(next: Partial<T>): void;\n subscribe(listener: () => void): () => void;\n}\n\nexport function createStore<T extends object>(initial: T): Store<T> {\n let state = initial;\n const listeners = new Set<() => void>();\n return {\n getState: () => state,\n setState(next) {\n const merged = { ...state, ...next };\n if ((Object.keys(merged) as (keyof T)[]).every((key) => Object.is(merged[key], state[key]))) return;\n state = merged;\n for (const listener of [...listeners]) listener();\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n\n/** Resolves after `ms`, or never when `ms` is not finite. */\nexport function delay(ms: number): Promise<void> {\n return new Promise((resolve) => {\n if (Number.isFinite(ms)) setTimeout(resolve, Math.max(0, ms));\n });\n}\n"]}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Development-only warnings from the libraries a Genesis native app is built on, that the app
3
+ * can't act on, kept out of LogBox.
4
+ *
5
+ * Why it matters: in React Native 0.88 (Expo SDK 58), LogBox no longer lists warnings while no
6
+ * debugger is attached. The first one instead shows a pill, "Open debugger to view warnings.", pinned to the bottom of the screen,
7
+ * over the native tab bar (`expo-router/native-tabs`). A tap meant for a tab lands on the pill
8
+ * (which opens the debugger and goes away), so the first tab tap after a launch, or after a link
9
+ * opened the app, seems to do nothing. In the starter on Expo SDK 58 the warning was React
10
+ * Native's own deprecation of `ImageBackground`, read by NativeWind's runtime
11
+ * (`react-native-css-interop` 0.2 registers every core component at start-up).
12
+ *
13
+ * Only warnings with no fix in the app belong here: one the app causes should be fixed, and still
14
+ * shows. Each pattern names where it comes from and when to remove it. Release builds have no
15
+ * LogBox, so this does nothing there.
16
+ *
17
+ * It has to run before any library loads: NativeWind's runtime warns while the first screen
18
+ * module is still being required, long before `GenesisNativeApp` renders. So the app's entry
19
+ * file calls it, then requires expo-router's entry (the starter's `index.ts`):
20
+ *
21
+ * import { LogBox } from "react-native";
22
+ * import { ignoreKnownDependencyWarnings } from "@marktiderman/genesis-native/dev-warnings";
23
+ * ignoreKnownDependencyWarnings(LogBox, __DEV__);
24
+ * require("expo-router/entry");
25
+ */
26
+ interface LogBoxLike {
27
+ ignoreLogs(patterns: ReadonlyArray<string | RegExp>): void;
28
+ }
29
+ /** The warnings, matched on their text. */
30
+ declare const KNOWN_DEPENDENCY_WARNINGS: readonly RegExp[];
31
+ /** Keep the known dependency warnings out of LogBox (development only). Returns whether it did. */
32
+ declare function ignoreKnownDependencyWarnings(logBox: LogBoxLike | undefined, dev: boolean): boolean;
33
+
34
+ export { KNOWN_DEPENDENCY_WARNINGS, type LogBoxLike, ignoreKnownDependencyWarnings };
@@ -0,0 +1,15 @@
1
+ // src/dev-warnings.ts
2
+ var KNOWN_DEPENDENCY_WARNINGS = [
3
+ // react-native-css-interop 0.2.x (NativeWind 4) reads `ImageBackground` from React Native's
4
+ // index, whose getter warns from 0.88. Remove once css-interop stops touching it.
5
+ /^ImageBackground is deprecated and will be removed in a future release\./
6
+ ];
7
+ function ignoreKnownDependencyWarnings(logBox, dev) {
8
+ if (!dev || !logBox) return false;
9
+ logBox.ignoreLogs(KNOWN_DEPENDENCY_WARNINGS);
10
+ return true;
11
+ }
12
+
13
+ export { KNOWN_DEPENDENCY_WARNINGS, ignoreKnownDependencyWarnings };
14
+ //# sourceMappingURL=dev-warnings.js.map
15
+ //# sourceMappingURL=dev-warnings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dev-warnings.ts"],"names":[],"mappings":";AA+BO,IAAM,yBAAA,GAA+C;AAAA;AAAA;AAAA,EAG1D;AACF;AAGO,SAAS,6BAAA,CAA8B,QAAgC,GAAA,EAAuB;AACnG,EAAA,IAAI,CAAC,GAAA,IAAO,CAAC,MAAA,EAAQ,OAAO,KAAA;AAC5B,EAAA,MAAA,CAAO,WAAW,yBAAyB,CAAA;AAC3C,EAAA,OAAO,IAAA;AACT","file":"dev-warnings.js","sourcesContent":["/**\n * Development-only warnings from the libraries a Genesis native app is built on, that the app\n * can't act on, kept out of LogBox.\n *\n * Why it matters: in React Native 0.88 (Expo SDK 58), LogBox no longer lists warnings while no\n * debugger is attached. The first one instead shows a pill, \"Open debugger to view warnings.\", pinned to the bottom of the screen,\n * over the native tab bar (`expo-router/native-tabs`). A tap meant for a tab lands on the pill\n * (which opens the debugger and goes away), so the first tab tap after a launch, or after a link\n * opened the app, seems to do nothing. In the starter on Expo SDK 58 the warning was React\n * Native's own deprecation of `ImageBackground`, read by NativeWind's runtime\n * (`react-native-css-interop` 0.2 registers every core component at start-up).\n *\n * Only warnings with no fix in the app belong here: one the app causes should be fixed, and still\n * shows. Each pattern names where it comes from and when to remove it. Release builds have no\n * LogBox, so this does nothing there.\n *\n * It has to run before any library loads: NativeWind's runtime warns while the first screen\n * module is still being required, long before `GenesisNativeApp` renders. So the app's entry\n * file calls it, then requires expo-router's entry (the starter's `index.ts`):\n *\n * import { LogBox } from \"react-native\";\n * import { ignoreKnownDependencyWarnings } from \"@marktiderman/genesis-native/dev-warnings\";\n * ignoreKnownDependencyWarnings(LogBox, __DEV__);\n * require(\"expo-router/entry\");\n */\n\nexport interface LogBoxLike {\n ignoreLogs(patterns: ReadonlyArray<string | RegExp>): void;\n}\n\n/** The warnings, matched on their text. */\nexport const KNOWN_DEPENDENCY_WARNINGS: readonly RegExp[] = [\n // react-native-css-interop 0.2.x (NativeWind 4) reads `ImageBackground` from React Native's\n // index, whose getter warns from 0.88. Remove once css-interop stops touching it.\n /^ImageBackground is deprecated and will be removed in a future release\\./,\n];\n\n/** Keep the known dependency warnings out of LogBox (development only). Returns whether it did. */\nexport function ignoreKnownDependencyWarnings(logBox: LogBoxLike | undefined, dev: boolean): boolean {\n if (!dev || !logBox) return false;\n logBox.ignoreLogs(KNOWN_DEPENDENCY_WARNINGS);\n return true;\n}\n"]}
@@ -0,0 +1,144 @@
1
+ import { N as NativeBlock, U as UpdatesBlock, V as VersionSignature } from './blocks-a1YQ1Elf.js';
2
+ export { D as DevtoolsFeedbackProps, a as DevtoolsSlots } from './blocks-a1YQ1Elf.js';
3
+ import { O as ObservabilityBlock, R as RecentActivity } from './block-C5-noMhB.js';
4
+ import 'react';
5
+ import '@marktiderman/genesis-core/slots';
6
+ import '@marktiderman/genesis-features/blocks';
7
+ import '@tanstack/react-query';
8
+ import '@marktiderman/genesis-design-system';
9
+ import '@marktiderman/genesis-core/provider';
10
+ import '@marktiderman/genesis-ui-native/layouts';
11
+ import '@marktiderman/genesis-ui-native/components';
12
+ import './query-DUmVAfsF.js';
13
+
14
+ /**
15
+ * The overlay's two ways in, as plain state machines (the root feeds them
16
+ * touches and accelerometer samples; tests feed them numbers).
17
+ *
18
+ * - A hold with three fingers (`fingers`) for `holdMs`. Works in every build
19
+ * with no native module.
20
+ * - A shake, from an accelerometer (`Accelerometer` from expo-sensors, in g):
21
+ * `required` samples above `threshold` g (gravity removed) within
22
+ * `windowMs`, then nothing for `cooldownMs`.
23
+ */
24
+ interface HoldDetectorOptions {
25
+ /** Default 3. */
26
+ fingers?: number;
27
+ /** Default 800 ms. */
28
+ holdMs?: number;
29
+ onTrigger: () => void;
30
+ setTimer?: (run: () => void, ms: number) => unknown;
31
+ clearTimer?: (handle: unknown) => void;
32
+ }
33
+ interface HoldDetector {
34
+ /** The number of fingers down now (`event.nativeEvent.touches.length`). */
35
+ touches(count: number): void;
36
+ cancel(): void;
37
+ }
38
+ declare function createHoldDetector(options: HoldDetectorOptions): HoldDetector;
39
+ interface ShakeDetectorOptions {
40
+ /** g above gravity a sample must reach. Default 1.5. */
41
+ threshold?: number;
42
+ /** Samples over the threshold needed. Default 3. */
43
+ required?: number;
44
+ /** Default 800 ms. */
45
+ windowMs?: number;
46
+ /** Default 2000 ms. */
47
+ cooldownMs?: number;
48
+ onShake: () => void;
49
+ now?: () => number;
50
+ }
51
+ interface ShakeDetector {
52
+ sample(reading: {
53
+ x: number;
54
+ y: number;
55
+ z: number;
56
+ }): void;
57
+ }
58
+ declare function createShakeDetector(options: ShakeDetectorOptions): ShakeDetector;
59
+ /** `Accelerometer` from expo-sensors, or a stand-in. */
60
+ interface AccelerometerLike {
61
+ isAvailableAsync?(): Promise<boolean>;
62
+ setUpdateInterval?(ms: number): void;
63
+ addListener(listener: (reading: {
64
+ x: number;
65
+ y: number;
66
+ z: number;
67
+ }) => void): {
68
+ remove(): void;
69
+ };
70
+ }
71
+
72
+ /** The FeatureId of the devtools overlay. */
73
+ declare const DEVTOOLS_BLOCK_ID = "devtools-overlay";
74
+ /** React Native's `DevSettings` (development builds only), or a stand-in. */
75
+ interface DevSettingsLike {
76
+ addMenuItem(title: string, handler: () => void): void;
77
+ }
78
+ interface DevtoolsOptions {
79
+ /**
80
+ * Must be explicitly true: `__DEV__` or a fact of the binary (a `.dev` or
81
+ * `.preview` bundle id). False mounts nothing.
82
+ */
83
+ enabled: boolean;
84
+ /** The updates block: the signature, the channel and the channel switch. */
85
+ updates?: UpdatesBlock;
86
+ /** Without the updates block: the signature to show (`versionSignature(…)`). */
87
+ signature?: VersionSignature;
88
+ /** The observability block: recent breadcrumbs and errors. */
89
+ observability?: ObservabilityBlock | null;
90
+ /** The server the app talks to (the Supabase URL). Shown without its query string. */
91
+ baseUrl?: string;
92
+ /** The app's switches and flags, shown by name and value. */
93
+ flags?: Readonly<Record<string, unknown>>;
94
+ /** `Accelerometer` from expo-sensors: shake to open. Without it, the hold and the dev menu still work. */
95
+ accelerometer?: AccelerometerLike;
96
+ /** `DevSettings` from react-native: "Open devtools" in the dev menu (development builds). */
97
+ devSettings?: DevSettingsLike;
98
+ /** The hold that opens the overlay, or `false` for none. Default three fingers for 800 ms. */
99
+ hold?: false | {
100
+ fingers?: number;
101
+ holdMs?: number;
102
+ };
103
+ /** Copy text (`Clipboard.setStringAsync` from expo-clipboard). Default the share sheet, which has Copy. */
104
+ copy?: (text: string) => unknown;
105
+ /** More to clear after the query cache (the persisted cache, MMKV). */
106
+ clearCache?: () => unknown;
107
+ /** Sign out (`supabase.auth.signOut()`). Without it, the action is hidden. */
108
+ signOut?: () => unknown;
109
+ }
110
+ interface DevtoolsState {
111
+ open: boolean;
112
+ }
113
+ interface DevtoolsBlock extends NativeBlock<typeof DEVTOOLS_BLOCK_ID> {
114
+ readonly enabled: boolean;
115
+ readonly options: DevtoolsOptions;
116
+ open(): void;
117
+ /** Closes the overlay. Resolves once its sheet has finished sliding away (or at once if it isn't up). */
118
+ close(): Promise<void>;
119
+ getState(): DevtoolsState;
120
+ subscribe(listener: () => void): () => void;
121
+ }
122
+ /** Build the overlay once, at module scope. @stability Beta */
123
+ declare function createDevtools(options: DevtoolsOptions): DevtoolsBlock;
124
+
125
+ /**
126
+ * The overlay's report, as text for "Copy debug info" and the feedback slot.
127
+ * Built from what the overlay shows, already scrubbed where it came from the
128
+ * observability block; the user is an id, never an email.
129
+ */
130
+
131
+ interface DebugInfoInput {
132
+ signature?: VersionSignature | null;
133
+ userId?: string | null;
134
+ blocks?: readonly string[];
135
+ flags?: Readonly<Record<string, unknown>>;
136
+ baseUrl?: string | null;
137
+ recent?: RecentActivity;
138
+ /** Default now. */
139
+ at?: number;
140
+ }
141
+ /** Several lines a person can paste into a bug report. Unknown values are written `unknown`, never left out. */
142
+ declare function buildDebugInfo(input: DebugInfoInput): string;
143
+
144
+ export { type AccelerometerLike, DEVTOOLS_BLOCK_ID, type DebugInfoInput, type DevSettingsLike, type DevtoolsBlock, type DevtoolsOptions, type DevtoolsState, type HoldDetector, type HoldDetectorOptions, type ShakeDetector, type ShakeDetectorOptions, buildDebugInfo, createDevtools, createHoldDetector, createShakeDetector };