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/README.md +269 -11
- package/action.yml +11 -0
- package/dist/args.d.ts +49 -9
- package/dist/cli.js +178 -171
- package/dist/index.js +132 -134
- package/dist/operate.d.ts +2 -0
- package/dist/run.d.ts +4 -0
- package/dist/runtime/error-boundary.d.ts +27 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +214 -0
- package/dist/runtime/ota.d.ts +19 -0
- package/dist/runtime/push.d.ts +70 -0
- package/dist/source-maps.d.ts +8 -0
- package/package.json +45 -8
- package/src/args.ts +129 -250
- package/src/cli.ts +78 -41
- package/src/operate.ts +19 -1
- package/src/run.ts +33 -4
- package/src/runtime/error-boundary.ts +51 -0
- package/src/runtime/index.ts +19 -0
- package/src/runtime/ota.ts +54 -0
- package/src/runtime/push.ts +243 -0
- package/src/source-maps.ts +61 -0
package/dist/index.js
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/args.ts
|
|
3
|
-
import {
|
|
3
|
+
import { z } from "zod";
|
|
4
4
|
var DEFAULT_IMPACT_PATHS = ["apps/expo/"];
|
|
5
|
-
var
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
520
|
+
DEFAULT_IMPACT_PATHS,
|
|
523
521
|
decideRelease,
|
|
524
|
-
|
|
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;
|