expo-release 2.0.0 → 2.2.1

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.
package/dist/index.js CHANGED
@@ -1,60 +1,60 @@
1
1
  // @bun
2
2
  // src/args.ts
3
- import { parseArgs } from "util";
3
+ import { z } from "zod";
4
4
  var DEFAULT_IMPACT_PATHS = ["apps/expo/"];
5
- var USAGE = "Usage: expo-release <release|decide|operate> --profile <profile> [--base <sha>] [--head <sha>] [--platform ios|android] [--impact-paths apps/expo/] [--operation deploy|force-native|retry-ota|republish|rollback-embedded] [--deploy-type none|ota|native] [--channel <channel>] [--environment <env>] [--group <id>] [--runtime-version <hash>] [--message <msg>] [--fingerprint <hash>] [--auto-submit true] [--pre-update <cmd>] [--post-update <cmd>] [--update-extra-args <args>] [--dry-run] [--ignore-expo-impact true]";
6
- function fromEnv(env, name) {
7
- const value = env[name]?.trim();
8
- return value ? value : undefined;
9
- }
10
- function firstDefined(...values) {
11
- return values.find((value) => value !== undefined && value.length > 0);
12
- }
13
- function optionalString(value) {
14
- const trimmed = value?.trim();
15
- return trimmed ? trimmed : undefined;
16
- }
17
- function parseImpactPaths(value) {
18
- const paths = (value ?? DEFAULT_IMPACT_PATHS.join(",")).split(",").map((path) => path.trim()).filter(Boolean);
19
- if (paths.length === 0) {
20
- throw new Error("At least one --impact-paths entry is required");
21
- }
22
- return paths;
23
- }
24
- function parsePlatform(value) {
25
- if (value === undefined || value === "ios")
26
- return "ios";
27
- if (value === "android")
28
- return "android";
29
- throw new Error("Usage: --platform ios|android");
30
- }
31
- function parseBoolean(value, fallback) {
32
- if (value === undefined)
33
- return fallback;
34
- if (value === "true")
35
- return true;
36
- if (value === "false")
37
- return false;
38
- throw new Error(`Expected true or false, received: ${value}`);
39
- }
40
- function parseOperation(value) {
41
- if (value === undefined || value === "deploy" || value === "force-native" || value === "retry-ota" || value === "republish" || value === "rollback-embedded") {
42
- return value ?? "deploy";
43
- }
44
- throw new Error("Usage: --operation deploy|force-native|retry-ota|republish|rollback-embedded");
45
- }
46
- function parseDeployType(value) {
47
- if (value === undefined || value === "none")
48
- return "none";
49
- if (value === "ota" || value === "native")
50
- return value;
51
- throw new Error("Usage: --deploy-type none|ota|native");
52
- }
53
- function parseCommandName(value) {
54
- if (value === "release" || value === "decide" || value === "operate") {
55
- return value;
56
- }
57
- throw new Error(USAGE);
5
+ var text = (env) => z.string().transform((value) => value.trim() || undefined).optional().meta({ env: `EXPO_RELEASE_${env}` });
6
+ var trueFalse = (env, fallback) => z.enum(["true", "false"]).default(fallback).transform((value) => value === "true").meta({ env: `EXPO_RELEASE_${env}` });
7
+ var releaseFlags = z.object({
8
+ profile: z.string().trim().min(1).meta({ env: "EXPO_RELEASE_PROFILE" }).describe("EAS build profile, also the default update channel"),
9
+ base: text("BASE").describe("Git base SHA for the impact diff (falls back to GITHUB_EVENT_BEFORE)"),
10
+ head: text("HEAD").describe("Git head SHA for the impact diff (falls back to GITHUB_SHA)"),
11
+ platform: z.enum(["ios", "android"]).default("ios").meta({ env: "EXPO_RELEASE_PLATFORM" }).describe("Native platform"),
12
+ impactPaths: z.string().default(DEFAULT_IMPACT_PATHS.join(",")).transform((value, ctx) => {
13
+ const paths = value.split(",").map((path) => path.trim()).filter(Boolean);
14
+ if (paths.length === 0)
15
+ ctx.addIssue({
16
+ code: "custom",
17
+ message: "At least one --impact-paths entry is required"
18
+ });
19
+ return paths;
20
+ }).meta({ env: "EXPO_RELEASE_IMPACT_PATHS" }).describe("Comma-separated repo-root paths that count as an Expo change"),
21
+ ignoreExpoImpact: trueFalse("IGNORE_EXPO_IMPACT", "false").describe("Check the fingerprint even when no impact path changed"),
22
+ operation: z.enum([
23
+ "deploy",
24
+ "force-native",
25
+ "retry-ota",
26
+ "republish",
27
+ "rollback-embedded"
28
+ ]).default("deploy").meta({ env: "EXPO_RELEASE_OPERATION" }).describe("What to run"),
29
+ deployType: z.enum(["none", "ota", "native"]).default("none").meta({ env: "EXPO_RELEASE_DEPLOY_TYPE" }).describe("Decision type for operate"),
30
+ channel: text("CHANNEL").describe("EAS Update channel (default: the profile)"),
31
+ environment: text("ENVIRONMENT").describe("EAS environment for eas update (required for Expo SDK 55+)"),
32
+ autoSubmit: trueFalse("AUTO_SUBMIT", "true").describe("Pass --auto-submit on native builds"),
33
+ preUpdate: text("PRE_UPDATE").describe("Shell command run before eas update"),
34
+ postUpdate: text("POST_UPDATE").describe("Shell command run after a successful eas update"),
35
+ updateExtraArgs: text("UPDATE_EXTRA_ARGS").describe("Extra tokens appended to eas update"),
36
+ exportSourceMaps: trueFalse("EXPORT_SOURCE_MAPS", "false").describe("On OTA, expo export with external source maps, then publish dist"),
37
+ group: text("GROUP").describe("Update group ID for --operation republish"),
38
+ runtimeVersion: text("RUNTIME_VERSION").describe("Runtime version for rollback-embedded (default: the fingerprint)"),
39
+ message: text("MESSAGE").describe("Override the eas update message"),
40
+ fingerprint: text("FINGERPRINT").describe("Fingerprint hash to use instead of generating one"),
41
+ dryRun: z.boolean().meta({ env: "EXPO_RELEASE_DRY_RUN" }).describe("Print mutating EAS commands without running them")
42
+ }).superRefine((flags, ctx) => {
43
+ if (flags.operation === "republish" && !flags.group)
44
+ ctx.addIssue({
45
+ code: "custom",
46
+ path: ["group"],
47
+ message: "required by --operation republish"
48
+ });
49
+ });
50
+ function toReleaseOptions(command, { context: flags, env }) {
51
+ return {
52
+ ...flags,
53
+ command,
54
+ base: flags.base ?? env.GITHUB_EVENT_BEFORE,
55
+ head: flags.head ?? env.GITHUB_SHA,
56
+ channel: flags.channel ?? flags.profile
57
+ };
58
58
  }
59
59
  function needsDecide(operation) {
60
60
  switch (operation) {
@@ -78,80 +78,6 @@ function shouldIgnoreExpoImpact(options) {
78
78
  return true;
79
79
  return process.env.GITHUB_EVENT_NAME === "workflow_dispatch" && options.operation === "deploy";
80
80
  }
81
- function parseReleaseOptions(args, env = process.env) {
82
- let values;
83
- let positionals;
84
- try {
85
- const parsed = parseArgs({
86
- args,
87
- allowPositionals: true,
88
- strict: true,
89
- options: {
90
- profile: { type: "string" },
91
- base: { type: "string" },
92
- head: { type: "string" },
93
- platform: { type: "string" },
94
- "impact-paths": { type: "string" },
95
- "ignore-expo-impact": { type: "string" },
96
- operation: { type: "string" },
97
- "deploy-type": { type: "string" },
98
- channel: { type: "string" },
99
- environment: { type: "string" },
100
- "auto-submit": { type: "string" },
101
- "pre-update": { type: "string" },
102
- "post-update": { type: "string" },
103
- "update-extra-args": { type: "string" },
104
- group: { type: "string" },
105
- "runtime-version": { type: "string" },
106
- message: { type: "string" },
107
- fingerprint: { type: "string" },
108
- "dry-run": { type: "boolean", default: false }
109
- }
110
- });
111
- values = parsed.values;
112
- positionals = parsed.positionals;
113
- } catch (error) {
114
- const message = error instanceof Error ? error.message : String(error);
115
- throw new Error(`${USAGE}
116
- ${message}`);
117
- }
118
- if (positionals.length > 1) {
119
- throw new Error(USAGE);
120
- }
121
- const command = parseCommandName(positionals[0] ?? fromEnv(env, "EXPO_RELEASE_COMMAND"));
122
- const profile = firstDefined(values.profile, fromEnv(env, "EXPO_RELEASE_PROFILE"));
123
- if (!profile) {
124
- throw new Error(USAGE);
125
- }
126
- const operation = parseOperation(firstDefined(values.operation, fromEnv(env, "EXPO_RELEASE_OPERATION")));
127
- const group = optionalString(firstDefined(values.group, fromEnv(env, "EXPO_RELEASE_GROUP")));
128
- if (operation === "republish" && !group) {
129
- throw new Error("Usage: --operation republish requires --group <update-group-id>");
130
- }
131
- const channel = firstDefined(values.channel, fromEnv(env, "EXPO_RELEASE_CHANNEL")) ?? profile;
132
- return {
133
- command,
134
- profile,
135
- base: optionalString(firstDefined(values.base, fromEnv(env, "EXPO_RELEASE_BASE"), fromEnv(env, "GITHUB_EVENT_BEFORE"))),
136
- head: optionalString(firstDefined(values.head, fromEnv(env, "EXPO_RELEASE_HEAD"), fromEnv(env, "GITHUB_SHA"))),
137
- platform: parsePlatform(firstDefined(values.platform, fromEnv(env, "EXPO_RELEASE_PLATFORM"))),
138
- impactPaths: parseImpactPaths(firstDefined(values["impact-paths"], fromEnv(env, "EXPO_RELEASE_IMPACT_PATHS"))),
139
- ignoreExpoImpact: parseBoolean(firstDefined(values["ignore-expo-impact"], fromEnv(env, "EXPO_RELEASE_IGNORE_EXPO_IMPACT")), false),
140
- operation,
141
- deployType: parseDeployType(firstDefined(values["deploy-type"], fromEnv(env, "EXPO_RELEASE_DEPLOY_TYPE"))),
142
- channel,
143
- environment: optionalString(firstDefined(values.environment, fromEnv(env, "EXPO_RELEASE_ENVIRONMENT"))),
144
- autoSubmit: parseBoolean(firstDefined(values["auto-submit"], fromEnv(env, "EXPO_RELEASE_AUTO_SUBMIT")), true),
145
- preUpdate: optionalString(firstDefined(values["pre-update"], fromEnv(env, "EXPO_RELEASE_PRE_UPDATE"))),
146
- postUpdate: optionalString(firstDefined(values["post-update"], fromEnv(env, "EXPO_RELEASE_POST_UPDATE"))),
147
- updateExtraArgs: optionalString(firstDefined(values["update-extra-args"], fromEnv(env, "EXPO_RELEASE_UPDATE_EXTRA_ARGS"))),
148
- group,
149
- runtimeVersion: optionalString(firstDefined(values["runtime-version"], fromEnv(env, "EXPO_RELEASE_RUNTIME_VERSION"))),
150
- message: optionalString(firstDefined(values.message, fromEnv(env, "EXPO_RELEASE_MESSAGE"))),
151
- fingerprint: optionalString(firstDefined(values.fingerprint, fromEnv(env, "EXPO_RELEASE_FINGERPRINT"))),
152
- dryRun: values["dry-run"] === true || parseBoolean(fromEnv(env, "EXPO_RELEASE_DRY_RUN"), false)
153
- };
154
- }
155
81
  // src/github-output.ts
156
82
  import { appendFile } from "fs/promises";
157
83
  async function appendGitHubOutput(entries) {
@@ -185,16 +111,44 @@ async function runCaptured(command) {
185
111
  child.exited
186
112
  ]);
187
113
  if (exitCode !== 0) {
188
- throw new Error(`Command failed (${command.join(" ")}):
189
- ${stderr || stdout}`);
114
+ throw new Error(describeFailure(command, exitCode, { stderr, stdout }));
190
115
  }
191
116
  return stdout;
192
117
  }
118
+ var OUTPUT_TAIL_LINES = 100;
119
+ function tail(output) {
120
+ const lines = output.trimEnd().split(`
121
+ `);
122
+ if (lines.length <= OUTPUT_TAIL_LINES)
123
+ return lines.join(`
124
+ `);
125
+ const omitted = lines.length - OUTPUT_TAIL_LINES;
126
+ return [
127
+ `... (${omitted} earlier lines omitted)`,
128
+ ...lines.slice(-OUTPUT_TAIL_LINES)
129
+ ].join(`
130
+ `);
131
+ }
132
+ function describeFailure(command, exitCode, output) {
133
+ const sections = [
134
+ `Command failed with exit code ${exitCode}: ${quoteCommand(command)}`
135
+ ];
136
+ if (output.stderr.trim())
137
+ sections.push(`stderr:
138
+ ${tail(output.stderr)}`);
139
+ if (output.stdout.trim())
140
+ sections.push(`stdout:
141
+ ${tail(output.stdout)}`);
142
+ if (sections.length === 1)
143
+ sections.push("(no output)");
144
+ return sections.join(`
145
+ `);
146
+ }
193
147
  async function runInherited(command) {
194
148
  const child = Bun.spawn(command, { stdout: "inherit", stderr: "inherit" });
195
149
  const exitCode = await child.exited;
196
150
  if (exitCode !== 0) {
197
- throw new Error(`Command failed (${command.join(" ")})`);
151
+ throw new Error(`Command failed with exit code ${exitCode}: ${quoteCommand(command)} (output above)`);
198
152
  }
199
153
  return "";
200
154
  }
@@ -390,6 +344,44 @@ async function writeDecideGitHubOutput(decision) {
390
344
  build_url: decision.build?.buildUrl ?? ""
391
345
  });
392
346
  }
347
+ // src/source-maps.ts
348
+ import { exists } from "fs/promises";
349
+ var SOURCE_MAP_OUTPUT_DIR = "dist";
350
+ function expoCommand(...args) {
351
+ const bin = process.env.EXPO_BIN?.trim();
352
+ if (bin && bin.length > 0)
353
+ return [bin, ...args];
354
+ return ["bunx", "expo", ...args];
355
+ }
356
+ function sourceMapExportCommand(platform) {
357
+ return expoCommand("export", "--platform", platform, "--output-dir", SOURCE_MAP_OUTPUT_DIR, "--source-maps", "external");
358
+ }
359
+ function mergeUpdateArgs(extra, exportSourceMaps) {
360
+ const args = extra ? [...extra] : [];
361
+ if (!exportSourceMaps)
362
+ return args;
363
+ if (!args.includes("--input-dir")) {
364
+ args.push("--input-dir", SOURCE_MAP_OUTPUT_DIR);
365
+ }
366
+ if (!args.includes("--skip-bundler")) {
367
+ args.push("--skip-bundler");
368
+ }
369
+ return args;
370
+ }
371
+ async function defaultHasSourceMaps(directory = SOURCE_MAP_OUTPUT_DIR) {
372
+ if (!await exists(directory))
373
+ return false;
374
+ for await (const _ of new Bun.Glob("**/*.map").scan(directory)) {
375
+ return true;
376
+ }
377
+ return false;
378
+ }
379
+ async function assertSourceMaps(hasSourceMaps = defaultHasSourceMaps) {
380
+ if (await hasSourceMaps())
381
+ return;
382
+ throw new Error("Expo export did not produce Hermes source maps in dist; refusing to publish an OTA update.");
383
+ }
384
+
393
385
  // src/operate.ts
394
386
  function extraUpdateArgs(value) {
395
387
  return value?.split(/\s+/).filter(Boolean) ?? [];
@@ -447,12 +439,18 @@ function mutatingRunner(commandRunner, dryRun) {
447
439
  }
448
440
  async function publishUpdate(input, commandRunner) {
449
441
  await requireEnvironmentIfNeeded(input);
442
+ if (input.exportSourceMaps) {
443
+ await commandRunner(sourceMapExportCommand(input.platform));
444
+ if (input.dryRun !== true) {
445
+ await assertSourceMaps(input.hasSourceMaps);
446
+ }
447
+ }
450
448
  await runHook(input.preUpdate, commandRunner);
451
449
  const command = easCommand("update", "--channel", input.channel, "--platform", input.platform, "--message", updateMessage(input), "--non-interactive");
452
450
  if (input.environment) {
453
451
  command.push("--environment", input.environment);
454
452
  }
455
- command.push(...extraUpdateArgs(input.updateExtraArgs));
453
+ command.push(...mergeUpdateArgs(extraUpdateArgs(input.updateExtraArgs), input.exportSourceMaps === true));
456
454
  await commandRunner(command);
457
455
  await runHook(input.postUpdate, commandRunner);
458
456
  }
@@ -519,7 +517,7 @@ async function writeOperateGitHubOutput(result) {
519
517
  await appendGitHubOutput({ result });
520
518
  }
521
519
  export {
522
- operateRelease,
520
+ DEFAULT_IMPACT_PATHS,
523
521
  decideRelease,
524
- DEFAULT_IMPACT_PATHS
522
+ operateRelease
525
523
  };
package/dist/operate.d.ts CHANGED
@@ -16,8 +16,10 @@ export type OperationInput = {
16
16
  preUpdate?: string;
17
17
  postUpdate?: string;
18
18
  updateExtraArgs?: string;
19
+ exportSourceMaps?: boolean;
19
20
  dryRun?: boolean;
20
21
  readPackageJson?: () => Promise<unknown>;
22
+ hasSourceMaps?: () => Promise<boolean>;
21
23
  };
22
24
  export type OperationResult = 'skipped' | 'ota' | 'native-build' | 'recovery';
23
25
  export declare function updateMessage(input: {
package/dist/run.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  export type CommandRunner = (command: string[]) => Promise<string>;
2
2
  export declare function easCommand(...args: string[]): string[];
3
3
  export declare function runCaptured(command: string[]): Promise<string>;
4
+ export declare function describeFailure(command: string[], exitCode: number, output: {
5
+ stderr: string;
6
+ stdout: string;
7
+ }): string;
4
8
  export declare function runInherited(command: string[]): Promise<string>;
5
9
  export declare function parseJson(output: string): unknown;
6
10
  export declare function getRecord(value: unknown, context: string): Record<string, unknown>;
@@ -0,0 +1,27 @@
1
+ import { Component, type ErrorInfo, type ReactNode } from 'react';
2
+ export interface ErrorBoundaryFallbackProps {
3
+ error: Error;
4
+ /** Clears the error and renders the children again. */
5
+ reset: () => void;
6
+ }
7
+ export interface ErrorBoundaryProps {
8
+ children?: ReactNode;
9
+ fallback: (props: ErrorBoundaryFallbackProps) => ReactNode;
10
+ /**
11
+ * Report the error here. Render errors never reach global handlers or
12
+ * error-tracking autocapture, so capture them explicitly.
13
+ */
14
+ onError?: (error: Error, info: ErrorInfo) => void;
15
+ }
16
+ interface ErrorBoundaryState {
17
+ error: Error | null;
18
+ }
19
+ /** Error boundary without UI: `fallback` renders the error screen. */
20
+ export declare class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
21
+ state: ErrorBoundaryState;
22
+ static getDerivedStateFromError(error: unknown): ErrorBoundaryState;
23
+ componentDidCatch(error: unknown, info: ErrorInfo): void;
24
+ reset: () => void;
25
+ render(): ReactNode;
26
+ }
27
+ export {};
@@ -0,0 +1,3 @@
1
+ export { ErrorBoundary, type ErrorBoundaryFallbackProps, type ErrorBoundaryProps } from './error-boundary';
2
+ export { checkForOTAUpdate, type ReloadApp, type UseOTAUpdatesOptions, useOTAUpdates } from './ota';
3
+ export { createPushNotifications, type KeyValueStorage, type NotificationListeners, type PushNotifications, type PushNotificationsOptions, usePushNotifications } from './push';
@@ -0,0 +1,214 @@
1
+ // src/runtime/error-boundary.ts
2
+ import { Component } from "react";
3
+ var toError = (value) => value instanceof Error ? value : new Error(String(value));
4
+
5
+ class ErrorBoundary extends Component {
6
+ state = { error: null };
7
+ static getDerivedStateFromError(error) {
8
+ return { error: toError(error) };
9
+ }
10
+ componentDidCatch(error, info) {
11
+ this.props.onError?.(toError(error), info);
12
+ }
13
+ reset = () => {
14
+ this.setState({ error: null });
15
+ };
16
+ render() {
17
+ const { error } = this.state;
18
+ return error ? this.props.fallback({ error, reset: this.reset }) : this.props.children;
19
+ }
20
+ }
21
+ // src/runtime/ota.ts
22
+ import * as Updates from "expo-updates";
23
+ import { useEffect, useRef } from "react";
24
+ async function checkForOTAUpdate(onUpdateReady) {
25
+ const update = await Updates.checkForUpdateAsync();
26
+ if (!update.isAvailable)
27
+ return false;
28
+ const result = await Updates.fetchUpdateAsync();
29
+ if (!result.isNew)
30
+ return false;
31
+ onUpdateReady(() => Updates.reloadAsync());
32
+ return true;
33
+ }
34
+ function useOTAUpdates(onUpdateReady, options = {}) {
35
+ const {
36
+ enabled = Updates.isEnabled && !(typeof __DEV__ !== "undefined" && __DEV__)
37
+ } = options;
38
+ const callbacks = useRef({ onUpdateReady, onError: options.onError });
39
+ callbacks.current = { onUpdateReady, onError: options.onError };
40
+ const checked = useRef(false);
41
+ useEffect(() => {
42
+ if (!enabled || checked.current)
43
+ return;
44
+ checked.current = true;
45
+ checkForOTAUpdate((reload) => callbacks.current.onUpdateReady(reload)).catch((error) => callbacks.current.onError?.(error));
46
+ }, [enabled]);
47
+ }
48
+ // src/runtime/push.ts
49
+ import * as Device from "expo-device";
50
+ import * as Notifications from "expo-notifications";
51
+ import { useCallback, useEffect as useEffect2, useState } from "react";
52
+ function createPushNotifications(options) {
53
+ const {
54
+ storage,
55
+ registerDevice,
56
+ confirm,
57
+ onUnavailable,
58
+ onError = (message, error) => console.error(message, error),
59
+ projectId,
60
+ storageKey = "@notifications_permission_asked"
61
+ } = options;
62
+ Notifications.setNotificationHandler({
63
+ handleNotification: async () => options.foregroundBehavior ?? {
64
+ shouldShowBanner: true,
65
+ shouldShowList: true,
66
+ shouldPlaySound: true,
67
+ shouldSetBadge: false
68
+ }
69
+ });
70
+ Notifications.setNotificationChannelAsync("default", options.androidChannel ?? {
71
+ name: "Default",
72
+ importance: Notifications.AndroidImportance.HIGH,
73
+ vibrationPattern: [0, 250, 250, 250]
74
+ }).catch((error) => onError("Failed to create the notification channel", error));
75
+ let pendingPrompt = null;
76
+ const getPermissionStatus = () => Notifications.getPermissionsAsync();
77
+ async function hasPrompted() {
78
+ try {
79
+ return await storage.getItem(storageKey) === "true";
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+ async function requestPermissions() {
85
+ if (!Device.isDevice) {
86
+ onUnavailable?.("not-a-device");
87
+ return false;
88
+ }
89
+ try {
90
+ const { granted } = await Notifications.requestPermissionsAsync();
91
+ if (!granted)
92
+ onUnavailable?.("denied");
93
+ return granted;
94
+ } catch (error) {
95
+ onError("Failed to request notification permissions", error);
96
+ return false;
97
+ }
98
+ }
99
+ async function getPushToken() {
100
+ if (!Device.isDevice)
101
+ return null;
102
+ try {
103
+ const token = await Notifications.getExpoPushTokenAsync(projectId ? { projectId } : undefined);
104
+ return token.data;
105
+ } catch (error) {
106
+ onError("Failed to get the Expo push token", error);
107
+ return null;
108
+ }
109
+ }
110
+ async function syncToken() {
111
+ if (!(await getPermissionStatus()).granted)
112
+ return false;
113
+ const token = await getPushToken();
114
+ if (!token)
115
+ return false;
116
+ try {
117
+ await registerDevice(token);
118
+ return true;
119
+ } catch (error) {
120
+ onError("Failed to register the push token", error);
121
+ return false;
122
+ }
123
+ }
124
+ async function runPrompt() {
125
+ if (await hasPrompted())
126
+ return false;
127
+ try {
128
+ await storage.setItem(storageKey, "true");
129
+ } catch (error) {
130
+ onError("Failed to remember the notification prompt", error);
131
+ }
132
+ if (confirm && !await confirm())
133
+ return false;
134
+ if (!await requestPermissions())
135
+ return false;
136
+ await syncToken();
137
+ return true;
138
+ }
139
+ return {
140
+ getPermissionStatus,
141
+ hasPrompted,
142
+ requestPermissions,
143
+ getPushToken,
144
+ syncToken,
145
+ prompt() {
146
+ pendingPrompt ??= runPrompt().catch((error) => {
147
+ onError("Failed to prompt for notifications", error);
148
+ return false;
149
+ }).finally(() => {
150
+ pendingPrompt = null;
151
+ });
152
+ return pendingPrompt;
153
+ },
154
+ async enable() {
155
+ if (!await requestPermissions())
156
+ return false;
157
+ await syncToken();
158
+ return true;
159
+ },
160
+ disable() {
161
+ return registerDevice(null);
162
+ },
163
+ listen({ onReceived, onResponse }) {
164
+ const subscriptions = [
165
+ onReceived && Notifications.addNotificationReceivedListener(onReceived),
166
+ onResponse && Notifications.addNotificationResponseReceivedListener(onResponse)
167
+ ];
168
+ const launchResponse = onResponse ? Notifications.getLastNotificationResponse() : null;
169
+ if (launchResponse)
170
+ onResponse?.(launchResponse);
171
+ return () => {
172
+ for (const subscription of subscriptions)
173
+ subscription?.remove();
174
+ };
175
+ }
176
+ };
177
+ }
178
+ function usePushNotifications(push, { enabled = false } = {}) {
179
+ const [permission, setPermission] = useState(null);
180
+ const [isLoading, setIsLoading] = useState(false);
181
+ const refresh = useCallback(async () => {
182
+ setPermission(await push.getPermissionStatus());
183
+ }, [push]);
184
+ useEffect2(() => {
185
+ refresh();
186
+ if (enabled)
187
+ push.syncToken();
188
+ }, [push, enabled, refresh]);
189
+ const withLoading = useCallback((action) => async () => {
190
+ setIsLoading(true);
191
+ try {
192
+ return await action();
193
+ } finally {
194
+ await refresh();
195
+ setIsLoading(false);
196
+ }
197
+ }, [refresh]);
198
+ return {
199
+ permission,
200
+ isEnabled: enabled && permission?.granted === true,
201
+ isLoading,
202
+ prompt: withLoading(push.prompt),
203
+ enable: withLoading(push.enable),
204
+ disable: withLoading(push.disable),
205
+ refresh
206
+ };
207
+ }
208
+ export {
209
+ ErrorBoundary,
210
+ checkForOTAUpdate,
211
+ createPushNotifications,
212
+ useOTAUpdates,
213
+ usePushNotifications
214
+ };
@@ -0,0 +1,19 @@
1
+ /** Restarts the app on the downloaded update. */
2
+ export type ReloadApp = () => Promise<void>;
3
+ /**
4
+ * Checks for an EAS update and downloads it. Calls `onUpdateReady` once a new
5
+ * update is on disk; show a "Restart" prompt there and call `reload` to apply.
6
+ * Returns whether a new update was downloaded.
7
+ */
8
+ export declare function checkForOTAUpdate(onUpdateReady: (reload: ReloadApp) => void): Promise<boolean>;
9
+ export interface UseOTAUpdatesOptions {
10
+ /** Defaults to off in development and wherever expo-updates is disabled (Expo Go). */
11
+ enabled?: boolean;
12
+ /** A failed check is retried on the next launch; defaults to ignoring it. */
13
+ onError?: (error: unknown) => void;
14
+ }
15
+ /**
16
+ * Checks for an update once per mount. The callbacks are read through refs, so
17
+ * an unstable toast function never re-runs the check or stacks prompts.
18
+ */
19
+ export declare function useOTAUpdates(onUpdateReady: (reload: ReloadApp) => void, options?: UseOTAUpdatesOptions): void;