@marktiderman/genesis-native 0.0.0-stage → 0.2.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.
@@ -0,0 +1,813 @@
1
+ import { createContext, useContext, useEffect, useSyncExternalStore, useMemo, useState } from 'react';
2
+ import { View } from 'react-native';
3
+ import { renderSlot } from '@marktiderman/genesis-core/slots';
4
+ import { SafeAreaView } from 'react-native-safe-area-context';
5
+ import { NativeEmptyState, NativeButton, NativeBanner, NativeSheet, NativeText } from '@marktiderman/genesis-ui-native/components';
6
+ import { jsx, jsxs } from 'react/jsx-runtime';
7
+
8
+ // src/updates/block.tsx
9
+
10
+ // src/updates/store.ts
11
+ function createStore(initial) {
12
+ let state = initial;
13
+ const listeners = /* @__PURE__ */ new Set();
14
+ return {
15
+ getState: () => state,
16
+ setState(next) {
17
+ const merged = { ...state, ...next };
18
+ if (Object.keys(merged).every((key) => Object.is(merged[key], state[key]))) return;
19
+ state = merged;
20
+ for (const listener of [...listeners]) listener();
21
+ },
22
+ subscribe(listener) {
23
+ listeners.add(listener);
24
+ return () => {
25
+ listeners.delete(listener);
26
+ };
27
+ }
28
+ };
29
+ }
30
+ function delay(ms) {
31
+ return new Promise((resolve) => {
32
+ if (Number.isFinite(ms)) setTimeout(resolve, Math.max(0, ms));
33
+ });
34
+ }
35
+
36
+ // src/updates/modules.ts
37
+ function toError(value) {
38
+ return value instanceof Error ? value : new Error(typeof value === "string" ? value : "Unknown error");
39
+ }
40
+ function manifestField(manifest, field) {
41
+ if (!manifest || typeof manifest !== "object") return null;
42
+ const value = manifest[field];
43
+ return typeof value === "string" && value.length > 0 ? value : null;
44
+ }
45
+
46
+ // src/updates/checker.ts
47
+ function createReloadGuard(canReload) {
48
+ let blockers = 0;
49
+ const listeners = /* @__PURE__ */ new Set();
50
+ const notify = () => {
51
+ for (const listener of [...listeners]) listener();
52
+ };
53
+ return {
54
+ block() {
55
+ blockers++;
56
+ notify();
57
+ let released = false;
58
+ return () => {
59
+ if (released) return;
60
+ released = true;
61
+ blockers--;
62
+ notify();
63
+ };
64
+ },
65
+ canReload() {
66
+ if (blockers > 0) return false;
67
+ try {
68
+ return canReload ? canReload() === true : true;
69
+ } catch {
70
+ return false;
71
+ }
72
+ },
73
+ subscribe(listener) {
74
+ listeners.add(listener);
75
+ return () => {
76
+ listeners.delete(listener);
77
+ };
78
+ }
79
+ };
80
+ }
81
+ function createUpdateChecker(options) {
82
+ const { updates, guard, reloadScreenOptions, now = Date.now, onError } = options;
83
+ const enabled = Boolean(updates?.isEnabled);
84
+ const store = createStore({
85
+ status: enabled ? "idle" : "disabled",
86
+ updateId: null,
87
+ dismissed: false,
88
+ blocked: false,
89
+ error: null
90
+ });
91
+ let inflight = null;
92
+ let lastCheck = null;
93
+ const report = (error, from) => {
94
+ try {
95
+ onError?.(error, from);
96
+ } catch {
97
+ }
98
+ };
99
+ const stopGuard = guard.subscribe(() => {
100
+ if (store.getState().blocked && guard.canReload()) store.setState({ blocked: false });
101
+ });
102
+ const check = () => {
103
+ if (!updates || !enabled) return Promise.resolve();
104
+ const { status } = store.getState();
105
+ if (inflight || status === "ready" || status === "reloading") return inflight ?? Promise.resolve();
106
+ lastCheck = now();
107
+ let run;
108
+ run = (async () => {
109
+ await null;
110
+ try {
111
+ store.setState({ status: "checking", error: null });
112
+ const result = await updates.checkForUpdateAsync();
113
+ if (!result.isAvailable && !result.isRollBackToEmbedded) {
114
+ store.setState({ status: "idle" });
115
+ return;
116
+ }
117
+ const runtime = manifestField(result.manifest, "runtimeVersion");
118
+ if (runtime && updates.runtimeVersion && runtime !== updates.runtimeVersion) {
119
+ store.setState({ status: "idle" });
120
+ return;
121
+ }
122
+ store.setState({ status: "downloading" });
123
+ const fetched = await updates.fetchUpdateAsync();
124
+ const updateId = manifestField(fetched.manifest, "id") ?? manifestField(result.manifest, "id");
125
+ if ((fetched.isNew || fetched.isRollBackToEmbedded) && !(updateId && updateId === updates.updateId)) {
126
+ store.setState({ status: "ready", updateId, dismissed: false });
127
+ } else {
128
+ store.setState({ status: "idle" });
129
+ }
130
+ } catch (raw) {
131
+ const error = toError(raw);
132
+ report(error, "check");
133
+ store.setState({ status: "error", error });
134
+ } finally {
135
+ if (inflight === run) inflight = null;
136
+ }
137
+ })();
138
+ inflight = run;
139
+ return run;
140
+ };
141
+ return {
142
+ getState: store.getState,
143
+ subscribe: store.subscribe,
144
+ check,
145
+ checkIfDue(minIntervalMs) {
146
+ const at = now();
147
+ if (lastCheck !== null && at >= lastCheck && at - lastCheck < minIntervalMs) return Promise.resolve();
148
+ return check();
149
+ },
150
+ async reload() {
151
+ if (!updates || store.getState().status !== "ready") return false;
152
+ if (!guard.canReload()) {
153
+ store.setState({ blocked: true });
154
+ return false;
155
+ }
156
+ store.setState({ status: "reloading", blocked: false });
157
+ try {
158
+ await updates.reloadAsync(reloadScreenOptions ? { reloadScreenOptions } : void 0);
159
+ return true;
160
+ } catch (raw) {
161
+ const error = toError(raw);
162
+ report(error, "reload");
163
+ store.setState({ status: "ready", error });
164
+ return false;
165
+ }
166
+ },
167
+ dismiss() {
168
+ store.setState({ dismissed: true, blocked: false });
169
+ },
170
+ dispose: stopGuard
171
+ };
172
+ }
173
+
174
+ // src/updates/channels.ts
175
+ var CHANNEL = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,99}$/;
176
+ function createChannelSwitcher(options) {
177
+ const { updates, enabled, guard, storage, storageKey = "genesis:updates:channel", reloadScreenOptions, onError } = options;
178
+ const report = (error) => {
179
+ try {
180
+ onError?.(error, "channel");
181
+ } catch {
182
+ }
183
+ };
184
+ const unavailable = () => {
185
+ if (!updates?.isEnabled) return { kind: "unavailable", reason: "Updates are off in this build (development or Expo Go)." };
186
+ if (typeof updates.setUpdateRequestHeadersOverride !== "function") {
187
+ return { kind: "unavailable", reason: "This expo-updates can't override the channel." };
188
+ }
189
+ return null;
190
+ };
191
+ const save = async (channel) => {
192
+ if (!storage) return;
193
+ try {
194
+ if (channel === null) await (storage.removeItem ? storage.removeItem(storageKey) : storage.setItem(storageKey, ""));
195
+ else await storage.setItem(storageKey, channel);
196
+ } catch (error) {
197
+ report(toError(error));
198
+ }
199
+ };
200
+ const apply = async () => {
201
+ const u = updates;
202
+ try {
203
+ const check = await u.checkForUpdateAsync();
204
+ if (check.isAvailable || check.isRollBackToEmbedded) await u.fetchUpdateAsync();
205
+ if (!guard.canReload()) return { kind: "blocked" };
206
+ await u.reloadAsync(reloadScreenOptions ? { reloadScreenOptions } : void 0);
207
+ return { kind: "reloading" };
208
+ } catch (raw) {
209
+ const error = toError(raw);
210
+ report(error);
211
+ return { kind: "error", error };
212
+ }
213
+ };
214
+ return {
215
+ enabled,
216
+ async switchTo(channel) {
217
+ if (enabled !== true) {
218
+ throw new Error("Channel switching is off. Pass `channels: { enabled: true }` only in development and preview builds.");
219
+ }
220
+ const name = channel.trim();
221
+ if (!CHANNEL.test(name)) throw new Error(`Not a channel name: "${channel}"`);
222
+ const off = unavailable();
223
+ if (off) return off;
224
+ updates.setUpdateRequestHeadersOverride({ "expo-channel-name": name });
225
+ await save(name === updates.channel ? null : name);
226
+ return apply();
227
+ },
228
+ async reset() {
229
+ const off = unavailable();
230
+ if (off) {
231
+ await save(null);
232
+ return off;
233
+ }
234
+ updates.setUpdateRequestHeadersOverride(null);
235
+ await save(null);
236
+ return apply();
237
+ },
238
+ async requested() {
239
+ if (!storage) return null;
240
+ try {
241
+ const value = await storage.getItem(storageKey);
242
+ return value ? value : null;
243
+ } catch (error) {
244
+ report(toError(error));
245
+ return null;
246
+ }
247
+ }
248
+ };
249
+ }
250
+
251
+ // src/updates/version.ts
252
+ var VERSION = /^\d+(?:\.\d+){0,3}$/;
253
+ function parseVersion(value) {
254
+ if (typeof value !== "string") return null;
255
+ const text = value.trim();
256
+ if (!VERSION.test(text)) return null;
257
+ const parts = text.split(".").map(Number);
258
+ if (parts.some((part) => !Number.isSafeInteger(part))) return null;
259
+ while (parts.length < 4) parts.push(0);
260
+ return parts;
261
+ }
262
+ function compareVersions(a, b) {
263
+ const left = parseVersion(a);
264
+ const right = parseVersion(b);
265
+ if (!left || !right) return null;
266
+ for (let i = 0; i < 4; i++) {
267
+ const l = left[i] ?? 0;
268
+ const r = right[i] ?? 0;
269
+ if (l !== r) return l < r ? -1 : 1;
270
+ }
271
+ return 0;
272
+ }
273
+ function isBelowMinimum(installed, minimum) {
274
+ return compareVersions(installed, minimum) === -1;
275
+ }
276
+
277
+ // src/updates/policy.ts
278
+ function staticPolicySource(policy) {
279
+ return { load: async () => policy };
280
+ }
281
+ var COLUMNS = "min_version_ios,min_version_android,store_url_ios,store_url_android,maintenance,maintenance_message,notes";
282
+ function supabasePolicySource(client, options = {}) {
283
+ const { appId = "default", table = "app_release_policy" } = options;
284
+ return {
285
+ async load() {
286
+ const query = client.from(table);
287
+ const { data, error } = await query.select(COLUMNS).eq("app_id", appId).maybeSingle();
288
+ if (error) throw toError(error instanceof Error ? error : error.message ?? error);
289
+ if (!data || typeof data !== "object") return {};
290
+ const row = data;
291
+ return {
292
+ minimumVersion: pick({ ios: row.min_version_ios, android: row.min_version_android }),
293
+ storeUrl: pick({ ios: row.store_url_ios, android: row.store_url_android }),
294
+ maintenance: { active: row.maintenance === true, message: typeof row.maintenance_message === "string" ? row.maintenance_message : null },
295
+ notes: Array.isArray(row.notes) ? row.notes : []
296
+ };
297
+ }
298
+ };
299
+ }
300
+ function pick(values) {
301
+ const out = {};
302
+ for (const platform of ["ios", "android"]) {
303
+ const value = values[platform];
304
+ if (typeof value === "string" && value.trim() !== "") out[platform] = value.trim();
305
+ }
306
+ return out;
307
+ }
308
+ var STORE_URL = /^(https:\/\/|itms-apps:\/\/|market:\/\/)/i;
309
+ function sanitizePolicy(raw) {
310
+ if (raw === null || raw === void 0) return {};
311
+ if (typeof raw !== "object" || Array.isArray(raw)) throw new Error("The release policy is not an object");
312
+ const value = raw;
313
+ const policy = {};
314
+ const minimum = value.minimumVersion && typeof value.minimumVersion === "object" ? value.minimumVersion : null;
315
+ if (minimum) policy.minimumVersion = pick(minimum);
316
+ const store = value.storeUrl && typeof value.storeUrl === "object" ? pick(value.storeUrl) : null;
317
+ if (store) {
318
+ for (const platform of ["ios", "android"]) if (store[platform] && !STORE_URL.test(store[platform])) delete store[platform];
319
+ policy.storeUrl = store;
320
+ }
321
+ const maintenance = value.maintenance;
322
+ if (maintenance && typeof maintenance === "object") {
323
+ policy.maintenance = {
324
+ active: maintenance.active === true,
325
+ message: typeof maintenance.message === "string" && maintenance.message.trim() !== "" ? maintenance.message : null
326
+ };
327
+ }
328
+ if (Array.isArray(value.notes)) {
329
+ policy.notes = value.notes.flatMap((note) => {
330
+ if (!note || typeof note !== "object") return [];
331
+ const { version, title, body } = note;
332
+ if (typeof version !== "string" || typeof body !== "string") return [];
333
+ return [{ version: version.trim(), body, ...typeof title === "string" ? { title } : {} }];
334
+ });
335
+ }
336
+ return policy;
337
+ }
338
+ function createPolicyStore(options) {
339
+ const { source, storage, cacheKey = "genesis:updates:policy", now = Date.now, onError } = options;
340
+ const timeoutMs = validTimeout(options.timeoutMs, 3e3);
341
+ const store = createStore({ status: "loading", policy: null, origin: "none", fetchedAt: null, error: null, refreshing: false });
342
+ let inflight = null;
343
+ let firstLoad = null;
344
+ let lastAttempt = null;
345
+ const report = (error, from) => {
346
+ try {
347
+ onError?.(toError(error), from);
348
+ } catch {
349
+ }
350
+ };
351
+ const fetchNetwork = () => {
352
+ if (inflight) return inflight;
353
+ lastAttempt = now();
354
+ store.setState({ refreshing: true });
355
+ let run;
356
+ run = (async () => {
357
+ await null;
358
+ try {
359
+ const policy = sanitizePolicy(await source.load());
360
+ const fetchedAt = now();
361
+ store.setState({ status: "ready", policy, origin: "network", fetchedAt, error: null });
362
+ if (storage) {
363
+ const entry = { v: 1, savedAt: fetchedAt, policy };
364
+ Promise.resolve().then(() => storage.setItem(cacheKey, JSON.stringify(entry))).catch((error) => report(error, "storage"));
365
+ }
366
+ return true;
367
+ } catch (error) {
368
+ report(error, "policy");
369
+ store.setState({ error: toError(error) });
370
+ return false;
371
+ } finally {
372
+ if (inflight === run) inflight = null;
373
+ store.setState({ refreshing: false });
374
+ }
375
+ })();
376
+ inflight = run;
377
+ return run;
378
+ };
379
+ const readCache = async () => {
380
+ if (!storage) return;
381
+ try {
382
+ const entry = parseCache(await storage.getItem(cacheKey));
383
+ if (entry && store.getState().origin !== "network") {
384
+ store.setState({ policy: entry.policy, origin: "cache", fetchedAt: entry.savedAt });
385
+ }
386
+ } catch (error) {
387
+ report(error, "storage");
388
+ }
389
+ };
390
+ return {
391
+ getState: store.getState,
392
+ subscribe: store.subscribe,
393
+ load() {
394
+ firstLoad ??= (async () => {
395
+ const cache = readCache();
396
+ const network = fetchNetwork();
397
+ const answered = network.then(async (ok) => {
398
+ if (!ok) await cache;
399
+ });
400
+ await Promise.race([answered, delay(timeoutMs)]);
401
+ store.setState({ status: "ready" });
402
+ })();
403
+ return firstLoad;
404
+ },
405
+ refresh: fetchNetwork,
406
+ refreshIfDue(minIntervalMs) {
407
+ const at = now();
408
+ if (lastAttempt !== null && at >= lastAttempt && at - lastAttempt < minIntervalMs) return Promise.resolve(false);
409
+ return fetchNetwork();
410
+ }
411
+ };
412
+ }
413
+ function validTimeout(value, fallback) {
414
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
415
+ }
416
+ function parseCache(raw) {
417
+ if (!raw) return null;
418
+ try {
419
+ const entry = JSON.parse(raw);
420
+ if (entry?.v !== 1 || typeof entry.savedAt !== "number" || !Number.isFinite(entry.savedAt)) return null;
421
+ return { v: 1, savedAt: entry.savedAt, policy: sanitizePolicy(entry.policy) };
422
+ } catch {
423
+ return null;
424
+ }
425
+ }
426
+ var CLOCK_SKEW_MS = 5 * 6e4;
427
+ var WEEK_MS = 7 * 24 * 60 * 6e4;
428
+ function evaluatePolicy(state, options) {
429
+ const { policy, origin, fetchedAt } = state;
430
+ if (!policy) return { kind: "ok" };
431
+ const { platform, installedVersion, now, cacheMaxAgeMs = WEEK_MS } = options;
432
+ const age = fetchedAt === null ? null : now - fetchedAt;
433
+ const trusted = origin === "network" || origin === "cache" && age !== null && age >= -CLOCK_SKEW_MS && age <= cacheMaxAgeMs;
434
+ if (trusted && (platform === "ios" || platform === "android")) {
435
+ const minimum = policy.minimumVersion?.[platform];
436
+ const storeUrl = policy.storeUrl?.[platform];
437
+ if (minimum && storeUrl && installedVersion && isBelowMinimum(installedVersion, minimum)) {
438
+ return { kind: "forceUpgrade", minimumVersion: minimum.trim(), installedVersion, storeUrl };
439
+ }
440
+ }
441
+ if (origin === "network" && policy.maintenance?.active === true) {
442
+ return { kind: "maintenance", message: policy.maintenance.message };
443
+ }
444
+ return { kind: "ok" };
445
+ }
446
+
447
+ // src/updates/signature.ts
448
+ var UNKNOWN = "unknown";
449
+ function versionSignature({ application, updates, platform, requestedChannel = null }) {
450
+ const version = application?.nativeApplicationVersion ?? null;
451
+ const build = application?.nativeBuildVersion ?? null;
452
+ const on = Boolean(updates?.isEnabled);
453
+ const channel = on ? updates?.channel ?? null : null;
454
+ const updateId = on ? updates?.updateId ?? null : null;
455
+ const runtimeVersion = updates?.runtimeVersion ?? null;
456
+ const launch = !on ? "off" : updates?.isEmbeddedLaunch ? "embedded" : "update";
457
+ const emergency = on && updates?.isEmergencyLaunch === true;
458
+ const shownChannel = requestedChannel ?? channel;
459
+ const head = `${version ?? UNKNOWN} (${build ?? UNKNOWN})`;
460
+ const parts = [head];
461
+ if (launch === "off") {
462
+ parts.push("updates off");
463
+ } else {
464
+ parts.push(shownChannel ?? "no channel");
465
+ parts.push(launch === "embedded" ? "embedded" : updateId?.slice(0, 8) ?? UNKNOWN);
466
+ }
467
+ if (emergency) parts.push("emergency launch");
468
+ const short = parts.join(" \xB7 ");
469
+ const lines = [
470
+ `Version ${head}`,
471
+ `Platform ${platform}`,
472
+ `Channel ${channel ?? (on ? UNKNOWN : "none")}${requestedChannel && requestedChannel !== channel ? ` (switched to ${requestedChannel})` : ""}`,
473
+ `Runtime ${runtimeVersion ?? UNKNOWN}`,
474
+ `Update ${launch === "off" ? "updates off" : launch === "embedded" ? "embedded" : updateId ?? UNKNOWN}`
475
+ ];
476
+ if (emergency) lines.push(`Emergency launch: ${updates?.emergencyLaunchReason ?? UNKNOWN}`);
477
+ return { version, build, platform, channel, requestedChannel, runtimeVersion, updateId, launch, emergency, short, text: lines.join("\n") };
478
+ }
479
+
480
+ // src/updates/whats-new.ts
481
+ function notesSince(notes, seen, version) {
482
+ return notes.filter((note) => {
483
+ if (note.version === version) return seen !== version;
484
+ const afterSeen = compareVersions(note.version, seen);
485
+ const upToNow = compareVersions(note.version, version);
486
+ return afterSeen === 1 && (upToNow === -1 || upToNow === 0);
487
+ }).sort((a, b) => compareVersions(b.version, a.version) ?? 0);
488
+ }
489
+ function createWhatsNewStore(options) {
490
+ const { storage, version, storageKey = "genesis:updates:seen-version", onError } = options;
491
+ const store = createStore({ status: "loading", notes: [] });
492
+ let seen;
493
+ let loading = null;
494
+ let pending = null;
495
+ const remember = () => {
496
+ if (!storage || !version) return;
497
+ seen = version;
498
+ Promise.resolve().then(() => storage.setItem(storageKey, version)).catch((error) => {
499
+ try {
500
+ onError?.(toError(error), "storage");
501
+ } catch {
502
+ }
503
+ });
504
+ };
505
+ const compute = (notes, final) => {
506
+ if (seen === void 0) {
507
+ pending = { notes, final };
508
+ return;
509
+ }
510
+ if (!version || seen === false) return store.setState({ status: "hidden", notes: [] });
511
+ if (seen === null) {
512
+ remember();
513
+ return store.setState({ status: "hidden", notes: [] });
514
+ }
515
+ const due = notesSince(notes, seen, version);
516
+ if (due.length > 0) return store.setState({ status: "showing", notes: due });
517
+ if (final && seen !== version) remember();
518
+ store.setState({ status: "hidden", notes: [] });
519
+ };
520
+ return {
521
+ getState: store.getState,
522
+ subscribe: store.subscribe,
523
+ load() {
524
+ loading ??= (async () => {
525
+ if (!storage || !version) {
526
+ seen = false;
527
+ } else {
528
+ try {
529
+ seen = await storage.getItem(storageKey);
530
+ } catch (error) {
531
+ seen = false;
532
+ try {
533
+ onError?.(toError(error), "storage");
534
+ } catch {
535
+ }
536
+ }
537
+ }
538
+ const next = pending ?? { notes: [], final: false };
539
+ pending = null;
540
+ compute(next.notes, next.final);
541
+ })();
542
+ return loading;
543
+ },
544
+ update: compute,
545
+ dismiss() {
546
+ remember();
547
+ store.setState({ status: "hidden", notes: [] });
548
+ }
549
+ };
550
+ }
551
+ function ForceUpgradeScreen({ minimumVersion, installedVersion, openStore, retry, checking }) {
552
+ return /* @__PURE__ */ jsx(View, { testID: "genesis-force-upgrade", className: "flex-1 justify-center bg-background", children: /* @__PURE__ */ jsx(
553
+ NativeEmptyState,
554
+ {
555
+ variant: "first-run",
556
+ title: "Update required",
557
+ body: `This version (${installedVersion}) is no longer supported. Update to ${minimumVersion} or later to keep going.`,
558
+ primaryAction: /* @__PURE__ */ jsx(NativeButton, { onPress: openStore, testID: "genesis-force-upgrade-store", children: "Update" }),
559
+ secondaryAction: /* @__PURE__ */ jsx(NativeButton, { variant: "ghost", onPress: retry, loading: checking, testID: "genesis-force-upgrade-retry", children: "Check again" }),
560
+ testID: "genesis-force-upgrade-state"
561
+ }
562
+ ) });
563
+ }
564
+ function MaintenanceScreen({ message, retry, retrying }) {
565
+ return /* @__PURE__ */ jsx(View, { testID: "genesis-maintenance", className: "flex-1 justify-center bg-background", children: /* @__PURE__ */ jsx(
566
+ NativeEmptyState,
567
+ {
568
+ variant: "first-run",
569
+ title: "Down for maintenance",
570
+ body: message ?? "We're doing some work on the app. Try again in a few minutes.",
571
+ primaryAction: /* @__PURE__ */ jsx(NativeButton, { onPress: retry, loading: retrying, testID: "genesis-maintenance-retry", children: "Try again" }),
572
+ testID: "genesis-maintenance-state"
573
+ }
574
+ ) });
575
+ }
576
+ function UpdateBanner({ reload, dismiss, canReload, blocked, reloading }) {
577
+ return /* @__PURE__ */ jsx(SafeAreaView, { edges: ["top"], pointerEvents: "box-none", style: { position: "absolute", top: 0, left: 0, right: 0 }, children: /* @__PURE__ */ jsxs(View, { testID: "genesis-update-banner", className: "mx-3 mt-2 overflow-hidden rounded-lg border border-border bg-secondary", children: [
578
+ /* @__PURE__ */ jsx(
579
+ NativeBanner,
580
+ {
581
+ variant: "info",
582
+ title: "An update is ready",
583
+ description: blocked || !canReload ? "Finish what you're doing, then restart to use it." : "Restart to use the latest version.",
584
+ className: "border-b-0",
585
+ testID: "genesis-update-banner-message"
586
+ }
587
+ ),
588
+ /* @__PURE__ */ jsxs(View, { className: "flex-row justify-end gap-2 px-4 pb-3", children: [
589
+ /* @__PURE__ */ jsx(NativeButton, { variant: "ghost", size: "sm", onPress: dismiss, testID: "genesis-update-banner-later", children: "Later" }),
590
+ /* @__PURE__ */ jsx(NativeButton, { size: "sm", onPress: reload, disabled: !canReload, loading: reloading, testID: "genesis-update-banner-restart", children: "Restart" })
591
+ ] })
592
+ ] }) });
593
+ }
594
+ function WhatsNewSheet({ notes, dismiss }) {
595
+ return /* @__PURE__ */ jsx(NativeSheet, { open: true, onOpenChange: (open) => open ? void 0 : dismiss(), testID: "genesis-whats-new", children: /* @__PURE__ */ jsxs(View, { className: "gap-4 p-6", children: [
596
+ /* @__PURE__ */ jsx(NativeText, { preset: "h3", accessibilityRole: "header", children: "What's new" }),
597
+ notes.map((note) => /* @__PURE__ */ jsxs(View, { className: "gap-1", testID: `genesis-whats-new-${note.version}`, children: [
598
+ /* @__PURE__ */ jsx(NativeText, { preset: "label", children: note.title ?? `Version ${note.version}` }),
599
+ /* @__PURE__ */ jsx(NativeText, { preset: "body-sm", className: "text-muted-foreground", children: note.body })
600
+ ] }, note.version)),
601
+ /* @__PURE__ */ jsx(NativeButton, { onPress: dismiss, testID: "genesis-whats-new-done", children: "Got it" })
602
+ ] }) });
603
+ }
604
+ function createUpdates(options) {
605
+ const {
606
+ platform,
607
+ application,
608
+ updates,
609
+ policy: source,
610
+ storage,
611
+ appState,
612
+ canReload,
613
+ channels,
614
+ policyRefreshMs = 5 * 6e4,
615
+ checkIntervalMs = 15 * 6e4,
616
+ reloadScreenOptions,
617
+ storageKeyPrefix = "genesis:updates",
618
+ onError,
619
+ now = Date.now
620
+ } = options;
621
+ const policyTimeoutMs = validTimeout(options.policyTimeoutMs, 3e3);
622
+ const guard = createReloadGuard(canReload);
623
+ const policy = createPolicyStore({
624
+ source: source ?? { load: async () => null },
625
+ storage: source ? storage : void 0,
626
+ cacheKey: `${storageKeyPrefix}:policy`,
627
+ timeoutMs: policyTimeoutMs,
628
+ now,
629
+ onError
630
+ });
631
+ const checker = createUpdateChecker({ updates, guard, reloadScreenOptions, now, onError });
632
+ const whatsNew = createWhatsNewStore({
633
+ storage: options.whatsNew === false ? void 0 : storage,
634
+ version: options.whatsNew === false ? null : application?.nativeApplicationVersion ?? null,
635
+ storageKey: `${storageKeyPrefix}:seen-version`,
636
+ onError
637
+ });
638
+ const switcher = createChannelSwitcher({
639
+ updates,
640
+ enabled: channels?.enabled === true,
641
+ guard,
642
+ storage,
643
+ storageKey: `${storageKeyPrefix}:channel`,
644
+ reloadScreenOptions,
645
+ onError
646
+ });
647
+ const signature = versionSignature({ application, updates, platform });
648
+ const allNotes = () => [...options.notes ?? [], ...policy.getState().policy?.notes ?? []];
649
+ const syncNotes = () => {
650
+ const state = policy.getState();
651
+ whatsNew.update(allNotes(), !source || state.origin === "network");
652
+ };
653
+ policy.subscribe(syncNotes);
654
+ let loaded = null;
655
+ const load = () => {
656
+ loaded ??= Promise.race([Promise.all([policy.load(), whatsNew.load()]), delay(policyTimeoutMs)]).then(syncNotes).catch((error) => {
657
+ try {
658
+ onError?.(toError(error), "storage");
659
+ } catch {
660
+ }
661
+ });
662
+ return loaded;
663
+ };
664
+ const block = {
665
+ options,
666
+ policy,
667
+ checker,
668
+ guard,
669
+ whatsNew,
670
+ channels: switcher,
671
+ signature,
672
+ bootStep: { id: "genesis:updates", run: load, timeoutMs: policyTimeoutMs + 5e3 },
673
+ start() {
674
+ void load();
675
+ void checker.check();
676
+ const subscription = appState?.addEventListener("change", (state) => {
677
+ if (state !== "active") return;
678
+ void policy.refreshIfDue(policyRefreshMs);
679
+ void checker.checkIfDue(checkIntervalMs);
680
+ });
681
+ return () => subscription?.remove();
682
+ },
683
+ Root: function UpdatesBlockRoot(props) {
684
+ return /* @__PURE__ */ jsx(UpdatesRoot, { block, ...props });
685
+ }
686
+ };
687
+ return block;
688
+ }
689
+ var UpdatesContext = createContext(null);
690
+ function UpdatesProvider({ block, children }) {
691
+ return /* @__PURE__ */ jsx(UpdatesContext.Provider, { value: block, children });
692
+ }
693
+ function useUpdates() {
694
+ const block = useContext(UpdatesContext);
695
+ if (!block) throw new Error("useUpdates: no updates block. Pass `updates={createUpdates(\u2026)}` to GenesisNativeApp, or wrap in <UpdatesProvider>.");
696
+ return block;
697
+ }
698
+ function useBlockReload(active) {
699
+ const block = useContext(UpdatesContext);
700
+ useEffect(() => active && block ? block.guard.block() : void 0, [active, block]);
701
+ }
702
+ function useUpdatePolicy() {
703
+ const { policy } = useUpdates();
704
+ return useSyncExternalStore(policy.subscribe, policy.getState, policy.getState);
705
+ }
706
+ function useUpdateVerdict() {
707
+ const block = useUpdates();
708
+ const state = useUpdatePolicy();
709
+ const { platform, application, cacheMaxAgeMs = WEEK_MS, now = Date.now } = block.options;
710
+ return useMemo(
711
+ () => evaluatePolicy(state, { platform, installedVersion: application?.nativeApplicationVersion ?? null, now: now(), cacheMaxAgeMs }),
712
+ [state, platform, application, cacheMaxAgeMs, now]
713
+ );
714
+ }
715
+ function useForceUpgrade() {
716
+ const block = useUpdates();
717
+ const verdict = useUpdateVerdict();
718
+ const { refreshing } = useUpdatePolicy();
719
+ if (verdict.kind !== "forceUpgrade") return null;
720
+ const { storeUrl } = verdict;
721
+ return {
722
+ minimumVersion: verdict.minimumVersion,
723
+ installedVersion: verdict.installedVersion,
724
+ storeUrl,
725
+ checking: refreshing,
726
+ retry: () => void block.policy.refresh(),
727
+ openStore: () => {
728
+ if (!block.options.openURL) return;
729
+ Promise.resolve().then(() => block.options.openURL?.(storeUrl)).catch((error) => block.options.onError?.(toError(error), "policy"));
730
+ }
731
+ };
732
+ }
733
+ function useMaintenance() {
734
+ const block = useUpdates();
735
+ const verdict = useUpdateVerdict();
736
+ const { refreshing } = useUpdatePolicy();
737
+ if (verdict.kind !== "maintenance") return null;
738
+ return { message: verdict.message, retrying: refreshing, retry: () => void block.policy.refresh() };
739
+ }
740
+ function useUpdateCheck() {
741
+ const { checker } = useUpdates();
742
+ return useSyncExternalStore(checker.subscribe, checker.getState, checker.getState);
743
+ }
744
+ function useCanReload() {
745
+ const { guard } = useUpdates();
746
+ return useSyncExternalStore(guard.subscribe, guard.canReload, guard.canReload);
747
+ }
748
+ function useUpdateBanner() {
749
+ const { checker } = useUpdates();
750
+ const state = useUpdateCheck();
751
+ const canReload = useCanReload();
752
+ if (state.status !== "ready" && state.status !== "reloading" || state.dismissed) return null;
753
+ return {
754
+ canReload,
755
+ blocked: state.blocked,
756
+ reloading: state.status === "reloading",
757
+ reload: () => void checker.reload(),
758
+ dismiss: () => checker.dismiss()
759
+ };
760
+ }
761
+ function useWhatsNew() {
762
+ const block = useUpdates();
763
+ const state = useSyncExternalStore(block.whatsNew.subscribe, block.whatsNew.getState, block.whatsNew.getState);
764
+ if (state.status !== "showing") return null;
765
+ return { notes: state.notes, version: block.options.application?.nativeApplicationVersion ?? null, dismiss: () => block.whatsNew.dismiss() };
766
+ }
767
+ function useVersionSignature() {
768
+ const block = useUpdates();
769
+ const [requested, setRequested] = useState(null);
770
+ useEffect(() => {
771
+ let alive = true;
772
+ void block.channels.requested().then((value) => alive && setRequested(value));
773
+ return () => {
774
+ alive = false;
775
+ };
776
+ }, [block]);
777
+ return useMemo(() => {
778
+ if (!requested) return block.signature;
779
+ const { application, updates, platform } = block.options;
780
+ return versionSignature({ application, updates, platform, requestedChannel: requested });
781
+ }, [block, requested]);
782
+ }
783
+ function UpdatesRoot({ block, slots, slotProps, children }) {
784
+ useEffect(() => block.start(), [block]);
785
+ return /* @__PURE__ */ jsx(UpdatesProvider, { block, children: /* @__PURE__ */ jsx(UpdatesParts, { slots, slotProps, children }) });
786
+ }
787
+ var FILL = { position: "absolute", top: 0, right: 0, bottom: 0, left: 0 };
788
+ function UpdatesParts({ slots, slotProps, children }) {
789
+ const forceUpgrade = useForceUpgrade();
790
+ const maintenance = useMaintenance();
791
+ const banner = useUpdateBanner();
792
+ const whatsNew = useWhatsNew();
793
+ const blocking = forceUpgrade && slots?.forceUpgrade !== null ? renderSlot(ForceUpgradeScreen, slots?.forceUpgrade, forceUpgrade, slotProps?.forceUpgrade) : maintenance && slots?.maintenance !== null ? renderSlot(MaintenanceScreen, slots?.maintenance, maintenance, slotProps?.maintenance) : null;
794
+ return /* @__PURE__ */ jsxs(View, { style: { flex: 1 }, children: [
795
+ /* @__PURE__ */ jsx(
796
+ View,
797
+ {
798
+ style: { flex: 1 },
799
+ pointerEvents: blocking ? "none" : "auto",
800
+ accessibilityElementsHidden: Boolean(blocking),
801
+ importantForAccessibility: blocking ? "no-hide-descendants" : "auto",
802
+ children
803
+ }
804
+ ),
805
+ !blocking && banner ? renderSlot(UpdateBanner, slots?.updateBanner, banner, slotProps?.updateBanner) : null,
806
+ !blocking && whatsNew ? renderSlot(WhatsNewSheet, slots?.whatsNew, whatsNew, slotProps?.whatsNew) : null,
807
+ blocking ? /* @__PURE__ */ jsx(View, { style: FILL, testID: "genesis-updates-blocking", children: blocking }) : null
808
+ ] });
809
+ }
810
+
811
+ export { CLOCK_SKEW_MS, ForceUpgradeScreen, MaintenanceScreen, UpdateBanner, UpdatesProvider, UpdatesRoot, WEEK_MS, WhatsNewSheet, compareVersions, createChannelSwitcher, createPolicyStore, createReloadGuard, createUpdateChecker, createUpdates, createWhatsNewStore, evaluatePolicy, isBelowMinimum, notesSince, parseVersion, sanitizePolicy, staticPolicySource, supabasePolicySource, useBlockReload, useCanReload, useForceUpgrade, useMaintenance, useUpdateBanner, useUpdateCheck, useUpdatePolicy, useUpdateVerdict, useUpdates, useVersionSignature, useWhatsNew, versionSignature };
812
+ //# sourceMappingURL=updates.js.map
813
+ //# sourceMappingURL=updates.js.map