@capuchoo/updater 0.4.0 → 0.5.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.
- package/dist/capacitor-config.d.ts +28 -8
- package/dist/capacitor-config.d.ts.map +1 -1
- package/dist/capacitor-config.js +14 -2
- package/dist/capacitor-config.js.map +1 -1
- package/dist/index.d.ts +81 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{ota.service-B3LrcCPr.d.ts → ota.service-B99qm0Bm.d.ts} +30 -7
- package/dist/ota.service-B99qm0Bm.d.ts.map +1 -0
- package/dist/{ota.service-SyGTRpY2.js → ota.service-DPjhi4rw.js} +185 -22
- package/dist/ota.service-DPjhi4rw.js.map +1 -0
- package/dist/vue.d.ts +50 -3
- package/dist/vue.d.ts.map +1 -1
- package/dist/vue.js +47 -4
- package/dist/vue.js.map +1 -1
- package/package.json +2 -2
- package/dist/ota.service-B3LrcCPr.d.ts.map +0 -1
- package/dist/ota.service-SyGTRpY2.js.map +0 -1
|
@@ -19,16 +19,27 @@
|
|
|
19
19
|
*/
|
|
20
20
|
type UpdaterMode = "onlyDownload" | "manual";
|
|
21
21
|
interface UpdaterPluginOptions {
|
|
22
|
-
/**
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Base URL of the Capuchoo backend. No trailing slash needed.
|
|
24
|
+
*
|
|
25
|
+
* Typed as possibly undefined on purpose. Every real call site writes
|
|
26
|
+
* `process.env.VITE_UPDATE_API_URL`, which is `string | undefined`, and
|
|
27
|
+
* declaring it `string` only moved the failure from the type checker to a
|
|
28
|
+
* `TypeError: Cannot read properties of undefined (reading 'replace')` in the
|
|
29
|
+
* middle of `npx cap sync`.
|
|
30
|
+
*/
|
|
31
|
+
apiUrl: string | undefined;
|
|
24
32
|
/** Channel this build defaults to. */
|
|
25
|
-
channel: string;
|
|
33
|
+
channel: string | undefined;
|
|
26
34
|
/**
|
|
27
|
-
* Version the plugin reports as
|
|
28
|
-
*
|
|
29
|
-
*
|
|
35
|
+
* Version the plugin reports as its built-in bundle.
|
|
36
|
+
*
|
|
37
|
+
* Optional, and usually best left out. Omitted, the plugin reports the
|
|
38
|
+
* binary's own `versionName`, which cannot go stale. Pass something only to
|
|
39
|
+
* deliberately override that - and if it is wrong, the server compares
|
|
40
|
+
* against the wrong baseline and re-serves bundles the device already has.
|
|
30
41
|
*/
|
|
31
|
-
version
|
|
42
|
+
version?: string | undefined;
|
|
32
43
|
mode?: UpdaterMode;
|
|
33
44
|
/** Milliseconds the plugin waits for `notifyAppReady` before rolling back. */
|
|
34
45
|
appReadyTimeout?: number;
|
|
@@ -41,12 +52,21 @@ interface UpdaterPluginOptions {
|
|
|
41
52
|
allowModifyUrl?: boolean;
|
|
42
53
|
}
|
|
43
54
|
interface CapacitorUpdaterPluginConfig {
|
|
55
|
+
/**
|
|
56
|
+
* 7.50.2 also accepts "always", "off", "atBackground", "atInstall" and
|
|
57
|
+
* "onLaunch". Only the two this package is written for are exposed - see
|
|
58
|
+
* docs/CAPGO-PLUGIN.md for what each mode makes the plugin do on its own.
|
|
59
|
+
*/
|
|
44
60
|
autoUpdate: boolean | "onlyDownload";
|
|
45
61
|
updateUrl: string;
|
|
46
62
|
statsUrl: string;
|
|
47
63
|
channelUrl: string;
|
|
48
64
|
defaultChannel: string;
|
|
49
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Omitted when the app does not override it, so the plugin reports the
|
|
67
|
+
* binary's own versionName - which cannot go stale.
|
|
68
|
+
*/
|
|
69
|
+
version?: string;
|
|
50
70
|
directUpdate: boolean;
|
|
51
71
|
appReadyTimeout: number;
|
|
52
72
|
responseTimeout: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capacitor-config.d.ts","names":[],"sources":["../src/capacitor-config.ts"],"mappings":";;;;;;;;;;;;;;;;;;;KAmBY;UAEK;;
|
|
1
|
+
{"version":3,"file":"capacitor-config.d.ts","names":[],"sources":["../src/capacitor-config.ts"],"mappings":";;;;;;;;;;;;;;;;;;;KAmBY;UAEK;;;;;;;;;;EAUf;;EAEA;;;;;;;;;EASA;EACA,OAAO;;EAEP;EACA;;;;;;EAMA;;UAGe;;;;;;EAMf;EACA;EACA;EACA;EACA;;;;;EAKA;EACA;EACA;EACA;EACA;;iBASc,sBAAsB,SAAS,uBAAuB"}
|
package/dist/capacitor-config.js
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
//#region src/capacitor-config.ts
|
|
2
|
+
/** What each required option needs, and where it comes from. */
|
|
3
|
+
const REQUIRED = [{
|
|
4
|
+
key: "apiUrl",
|
|
5
|
+
env: "VITE_UPDATE_API_URL",
|
|
6
|
+
why: "the server the app asks for updates"
|
|
7
|
+
}, {
|
|
8
|
+
key: "channel",
|
|
9
|
+
env: "VITE_UPDATE_CHANNEL",
|
|
10
|
+
why: "the channel this build follows"
|
|
11
|
+
}];
|
|
2
12
|
function capuchooUpdaterConfig(options) {
|
|
13
|
+
const missing = REQUIRED.filter(({ key }) => !options[key]?.trim());
|
|
14
|
+
if (missing.length > 0) throw new Error(`capuchooUpdaterConfig: ${missing.map(({ key }) => key).join(" and ")} ${missing.length === 1 ? "is" : "are"} missing.\n` + missing.map(({ key, env, why }) => ` ${key} set ${env} - ${why}`).join("\n") + "\n\nThese come from the flavour's env file, which the Capuchoo CLI loads during a deploy. A bare `npx cap sync` does not load it: either run the deploy, or export the variables first.");
|
|
3
15
|
const apiUrl = options.apiUrl.replace(/\/+$/, "");
|
|
4
|
-
if (!apiUrl) throw new Error(
|
|
16
|
+
if (!apiUrl) throw new Error(`capuchooUpdaterConfig: apiUrl is "${options.apiUrl}", which is not a URL. Set VITE_UPDATE_API_URL for this flavour, otherwise the app ships with updates disabled.`);
|
|
5
17
|
return {
|
|
6
18
|
autoUpdate: (options.mode ?? "onlyDownload") === "onlyDownload" ? "onlyDownload" : false,
|
|
7
19
|
updateUrl: `${apiUrl}/api/update`,
|
|
8
20
|
statsUrl: `${apiUrl}/api/stats`,
|
|
9
21
|
channelUrl: `${apiUrl}/api/channel_self`,
|
|
10
22
|
defaultChannel: options.channel,
|
|
11
|
-
version: options.version,
|
|
23
|
+
...options.version ? { version: options.version } : {},
|
|
12
24
|
directUpdate: false,
|
|
13
25
|
appReadyTimeout: options.appReadyTimeout ?? 1e4,
|
|
14
26
|
responseTimeout: options.responseTimeout ?? 3e4,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capacitor-config.js","names":[],"sources":["../src/capacitor-config.ts"],"sourcesContent":["/**\n * Builds the `CapacitorUpdater` plugin block for `capacitor.config.ts`.\n *\n * This exists because the plugin's `autoUpdate` mode has to agree with how the\n * app drives updates, and getting it wrong fails in a way that is very hard to\n * diagnose. The app template shipped `autoUpdate: true` while also calling\n * `download()` and `set()` from JavaScript: the plugin applied bundles on its\n * own schedule at the same time as the UI was downloading them, so a device\n * could download the same bundle twice, or reload mid-prompt.\n *\n * `\"onlyDownload\"` is the mode this package is written for. The plugin fetches\n * the bundle in the background and raises `updateAvailable`; the app decides\n * when to apply it. Pass `mode: \"manual\"` to disable background downloads\n * entirely and drive everything from `useUpdater`.\n *\n * Imported from `capacitor.config.ts`, so it must stay free of any runtime or\n * DOM dependency.\n */\n\nexport type UpdaterMode = \"onlyDownload\" | \"manual\";\n\nexport interface UpdaterPluginOptions {\n
|
|
1
|
+
{"version":3,"file":"capacitor-config.js","names":[],"sources":["../src/capacitor-config.ts"],"sourcesContent":["/**\n * Builds the `CapacitorUpdater` plugin block for `capacitor.config.ts`.\n *\n * This exists because the plugin's `autoUpdate` mode has to agree with how the\n * app drives updates, and getting it wrong fails in a way that is very hard to\n * diagnose. The app template shipped `autoUpdate: true` while also calling\n * `download()` and `set()` from JavaScript: the plugin applied bundles on its\n * own schedule at the same time as the UI was downloading them, so a device\n * could download the same bundle twice, or reload mid-prompt.\n *\n * `\"onlyDownload\"` is the mode this package is written for. The plugin fetches\n * the bundle in the background and raises `updateAvailable`; the app decides\n * when to apply it. Pass `mode: \"manual\"` to disable background downloads\n * entirely and drive everything from `useUpdater`.\n *\n * Imported from `capacitor.config.ts`, so it must stay free of any runtime or\n * DOM dependency.\n */\n\nexport type UpdaterMode = \"onlyDownload\" | \"manual\";\n\nexport interface UpdaterPluginOptions {\n /**\n * Base URL of the Capuchoo backend. No trailing slash needed.\n *\n * Typed as possibly undefined on purpose. Every real call site writes\n * `process.env.VITE_UPDATE_API_URL`, which is `string | undefined`, and\n * declaring it `string` only moved the failure from the type checker to a\n * `TypeError: Cannot read properties of undefined (reading 'replace')` in the\n * middle of `npx cap sync`.\n */\n apiUrl: string | undefined;\n /** Channel this build defaults to. */\n channel: string | undefined;\n /**\n * Version the plugin reports as its built-in bundle.\n *\n * Optional, and usually best left out. Omitted, the plugin reports the\n * binary's own `versionName`, which cannot go stale. Pass something only to\n * deliberately override that - and if it is wrong, the server compares\n * against the wrong baseline and re-serves bundles the device already has.\n */\n version?: string | undefined;\n mode?: UpdaterMode;\n /** Milliseconds the plugin waits for `notifyAppReady` before rolling back. */\n appReadyTimeout?: number;\n responseTimeout?: number;\n /**\n * Whether the app may point the plugin at a different server at runtime.\n * Leave off in production: it lets anything running in the WebView redirect\n * update downloads.\n */\n allowModifyUrl?: boolean;\n}\n\nexport interface CapacitorUpdaterPluginConfig {\n /**\n * 7.50.2 also accepts \"always\", \"off\", \"atBackground\", \"atInstall\" and\n * \"onLaunch\". Only the two this package is written for are exposed - see\n * docs/CAPGO-PLUGIN.md for what each mode makes the plugin do on its own.\n */\n autoUpdate: boolean | \"onlyDownload\";\n updateUrl: string;\n statsUrl: string;\n channelUrl: string;\n defaultChannel: string;\n /**\n * Omitted when the app does not override it, so the plugin reports the\n * binary's own versionName - which cannot go stale.\n */\n version?: string;\n directUpdate: boolean;\n appReadyTimeout: number;\n responseTimeout: number;\n allowModifyUrl: boolean;\n}\n\n/** What each required option needs, and where it comes from. */\nconst REQUIRED: Array<{ key: \"apiUrl\" | \"channel\"; env: string; why: string }> = [\n { key: \"apiUrl\", env: \"VITE_UPDATE_API_URL\", why: \"the server the app asks for updates\" },\n { key: \"channel\", env: \"VITE_UPDATE_CHANNEL\", why: \"the channel this build follows\" },\n];\n\nexport function capuchooUpdaterConfig(options: UpdaterPluginOptions): CapacitorUpdaterPluginConfig {\n // Validated before anything is read off `options`.\n //\n // This used to call `options.apiUrl.replace(...)` first and check afterwards,\n // so the helpful message below was unreachable for the one case that actually\n // happens: `process.env.VITE_UPDATE_API_URL` is `undefined` when unset, not\n // \"\". A bare `npx cap sync` died with \"Cannot read properties of undefined\n // (reading 'replace')\" and a stack inside node_modules, which says nothing\n // about the missing variable.\n //\n // Every missing value is named at once, because finding them one build at a\n // time is its own small misery.\n const missing = REQUIRED.filter(({ key }) => !options[key]?.trim());\n\n if (missing.length > 0) {\n throw new Error(\n `capuchooUpdaterConfig: ${missing.map(({ key }) => key).join(\" and \")} ` +\n `${missing.length === 1 ? \"is\" : \"are\"} missing.\\n` +\n missing.map(({ key, env, why }) => ` ${key} set ${env} - ${why}`).join(\"\\n\") +\n \"\\n\\nThese come from the flavour's env file, which the Capuchoo CLI loads \" +\n \"during a deploy. A bare `npx cap sync` does not load it: either run the \" +\n \"deploy, or export the variables first.\",\n );\n }\n\n const apiUrl = options.apiUrl!.replace(/\\/+$/, \"\");\n\n if (!apiUrl) {\n // A URL of only slashes normalises to nothing. The plugin accepts an empty\n // updateUrl and then silently never checks for updates, which is the worst\n // possible outcome, so fail the build instead.\n throw new Error(\n `capuchooUpdaterConfig: apiUrl is \"${options.apiUrl}\", which is not a URL. ` +\n \"Set VITE_UPDATE_API_URL for this flavour, otherwise the app ships with \" +\n \"updates disabled.\",\n );\n }\n\n const mode = options.mode ?? \"onlyDownload\";\n\n return {\n // \"manual\" means the plugin does nothing on its own.\n autoUpdate: mode === \"onlyDownload\" ? \"onlyDownload\" : false,\n updateUrl: `${apiUrl}/api/update`,\n statsUrl: `${apiUrl}/api/stats`,\n channelUrl: `${apiUrl}/api/channel_self`,\n defaultChannel: options.channel!,\n ...(options.version ? { version: options.version } : {}),\n // The app shows a prompt and calls set() itself; letting the plugin apply\n // the bundle immediately would reload the WebView under the user.\n directUpdate: false,\n appReadyTimeout: options.appReadyTimeout ?? 10_000,\n responseTimeout: options.responseTimeout ?? 30_000,\n allowModifyUrl: options.allowModifyUrl ?? false,\n };\n}\n"],"mappings":";;AA8EA,MAAM,WAA2E,CAC/E;CAAE,KAAK;CAAU,KAAK;CAAuB,KAAK;AAAsC,GACxF;CAAE,KAAK;CAAW,KAAK;CAAuB,KAAK;AAAiC,CACtF;AAEA,SAAgB,sBAAsB,SAA6D;CAYjG,MAAM,UAAU,SAAS,QAAQ,EAAE,UAAU,CAAC,QAAQ,IAAI,EAAE,KAAK,CAAC;CAElE,IAAI,QAAQ,SAAS,GACnB,MAAM,IAAI,MACR,0BAA0B,QAAQ,KAAK,EAAE,UAAU,GAAG,CAAC,CAAC,KAAK,OAAO,EAAE,GACjE,QAAQ,WAAW,IAAI,OAAO,MAAM,eACvC,QAAQ,KAAK,EAAE,KAAK,KAAK,UAAU,KAAK,IAAI,QAAQ,IAAI,KAAK,KAAK,CAAC,CAAC,KAAK,IAAI,IAC7E,yLAGJ;CAGF,MAAM,SAAS,QAAQ,OAAQ,QAAQ,QAAQ,EAAE;CAEjD,IAAI,CAAC,QAIH,MAAM,IAAI,MACR,qCAAqC,QAAQ,OAAO,gHAGtD;CAKF,OAAO;EAEL,aAJW,QAAQ,QAAQ,oBAIN,iBAAiB,iBAAiB;EACvD,WAAW,GAAG,OAAO;EACrB,UAAU,GAAG,OAAO;EACpB,YAAY,GAAG,OAAO;EACtB,gBAAgB,QAAQ;EACxB,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;EAGtD,cAAc;EACd,iBAAiB,QAAQ,mBAAmB;EAC5C,iBAAiB,QAAQ,mBAAmB;EAC5C,gBAAgB,QAAQ,kBAAkB;CAC5C;AACF"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as DownloadProgress, i as notifyAppReady, n as discardBundle, o as
|
|
1
|
+
import { a as DownloadProgress, c as pruneApkCache, i as notifyAppReady, n as discardBundle, o as downloadNativeUpdate, r as getCurrentBundle, s as findCachedApk, t as applyOtaUpdate } from "./ota.service-B99qm0Bm.js";
|
|
2
2
|
import { Environment, Platform, ResolvedUpdate, ResolvedUpdate as ResolvedUpdate$1, UpdateCheckRequest, UpdateCheckResponse, UpdateCheckResponse as UpdateCheckResponse$1, UpdateEvent, UpdateEvent as UpdateEvent$1, UpdateKind } from "@capuchoo/core";
|
|
3
3
|
//#region src/device.d.ts
|
|
4
4
|
/**
|
|
@@ -157,6 +157,85 @@ declare function getUpdaterConfig(): UpdaterConfig;
|
|
|
157
157
|
*/
|
|
158
158
|
declare function describeConfigProblems(config: UpdaterConfig): string[];
|
|
159
159
|
//#endregion
|
|
160
|
+
//#region src/apk-cache.d.ts
|
|
161
|
+
/**
|
|
162
|
+
* Which downloaded APK to keep, reuse, or throw away.
|
|
163
|
+
*
|
|
164
|
+
* A native binary is 40-60 MB and users pay for it, sometimes on mobile data.
|
|
165
|
+
* The previous implementation spent that repeatedly and needlessly:
|
|
166
|
+
*
|
|
167
|
+
* - `downloadNativeUpdate` called `cleanApkCache()` as its first step, which
|
|
168
|
+
* deleted every APK for this app - including the exact file it was about to
|
|
169
|
+
* fetch. A second press always re-downloaded.
|
|
170
|
+
* - The only record that a download had finished was `state.cachedPath`, in
|
|
171
|
+
* memory. Closing the app threw it away, so the next launch downloaded the
|
|
172
|
+
* same bytes again while the file sat on disk.
|
|
173
|
+
* - Nothing ever deleted a successfully installed APK, so it stayed in the
|
|
174
|
+
* cache forever.
|
|
175
|
+
*
|
|
176
|
+
* The decisions are here, as pure functions over file names and sizes, because
|
|
177
|
+
* the alternative is discovering the behaviour on a phone with a data plan.
|
|
178
|
+
* `download.service.ts` supplies the filesystem.
|
|
179
|
+
*/
|
|
180
|
+
/** Cache file name prefix, derived from the app id so two flavours never collide. */
|
|
181
|
+
declare function cachePrefix(appId: string): string;
|
|
182
|
+
interface ApkIdentity {
|
|
183
|
+
version: string;
|
|
184
|
+
versionCode: number;
|
|
185
|
+
}
|
|
186
|
+
/** `com.efficy.app-1.0.56-67.apk` */
|
|
187
|
+
declare function apkFileName(appId: string, update: ApkIdentity): string;
|
|
188
|
+
/**
|
|
189
|
+
* Reads a name produced by `apkFileName` back into what it holds.
|
|
190
|
+
*
|
|
191
|
+
* Returns null for anything else in the cache directory - the WebView's own
|
|
192
|
+
* files live there too, and deleting one of those would be a very expensive
|
|
193
|
+
* mistake to make with a regex.
|
|
194
|
+
*/
|
|
195
|
+
declare function parseApkFileName(appId: string, fileName: string): ApkIdentity | null;
|
|
196
|
+
/**
|
|
197
|
+
* Whether a cached file is the update being offered, complete.
|
|
198
|
+
*
|
|
199
|
+
* Size is the check. A download interrupted by a dead connection leaves a
|
|
200
|
+
* partial file at the right path, and reusing that hands the Android installer
|
|
201
|
+
* a truncated APK - which fails with "There was a problem parsing the package",
|
|
202
|
+
* a message that says nothing about the real cause.
|
|
203
|
+
*
|
|
204
|
+
* When the server did not send a size there is nothing to verify against, so
|
|
205
|
+
* the file is not trusted. Re-downloading costs bandwidth; installing a
|
|
206
|
+
* truncated binary costs a support call.
|
|
207
|
+
*/
|
|
208
|
+
declare function isCompleteDownload(cached: {
|
|
209
|
+
size: number;
|
|
210
|
+
} | null, expectedSize: number | undefined): boolean;
|
|
211
|
+
interface CachedApk extends ApkIdentity {
|
|
212
|
+
fileName: string;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Which cached APKs to delete, given what is installed and what is wanted.
|
|
216
|
+
*
|
|
217
|
+
* Two situations, deliberately one function because they differ by one rule:
|
|
218
|
+
*
|
|
219
|
+
* **Pruning** (no `keep`) removes only what the device has outgrown - anything
|
|
220
|
+
* whose build number the installed binary has caught up with. That is how an
|
|
221
|
+
* installed APK finally gets deleted: the Android installer never calls back,
|
|
222
|
+
* but the next launch reports a higher build number, which says the same thing.
|
|
223
|
+
* A *newer* APK is left alone, because it is an update already downloaded and
|
|
224
|
+
* waiting to be installed, and throwing it away would mean paying for it twice.
|
|
225
|
+
*
|
|
226
|
+
* **Making room** (`keep` given) additionally removes other pending downloads,
|
|
227
|
+
* because the file about to be written is tens of megabytes and the OS will
|
|
228
|
+
* evict from a full cache mid-download.
|
|
229
|
+
*
|
|
230
|
+
* `keep` itself is never deleted. Deleting the file that is about to be
|
|
231
|
+
* downloaded is the exact bug this module exists to prevent.
|
|
232
|
+
*/
|
|
233
|
+
declare function apksToDelete(input: {
|
|
234
|
+
cached: CachedApk[];
|
|
235
|
+
installedVersionCode: number;
|
|
236
|
+
keep?: string | undefined;
|
|
237
|
+
}): string[];
|
|
238
|
+
//#endregion
|
|
160
239
|
//#region src/install.service.d.ts
|
|
161
240
|
/**
|
|
162
241
|
* Hands a downloaded APK to the Android package installer.
|
|
@@ -168,5 +247,5 @@ declare function describeConfigProblems(config: UpdaterConfig): string[];
|
|
|
168
247
|
*/
|
|
169
248
|
declare function openNativeInstaller(path: string): Promise<void>;
|
|
170
249
|
//#endregion
|
|
171
|
-
export { type DeviceFacts, type DownloadProgress, type Environment, type Platform, type ResolvedUpdate, UpdateCheckBlockedError, type UpdateCheckResponse, type UpdateEvent, type UpdateKind, type UpdaterConfig, UpdaterConfigError, applyOtaUpdate, buildCheckRequest,
|
|
250
|
+
export { type ApkIdentity, type CachedApk, type DeviceFacts, type DownloadProgress, type Environment, type Platform, type ResolvedUpdate, UpdateCheckBlockedError, type UpdateCheckResponse, type UpdateEvent, type UpdateKind, type UpdaterConfig, UpdaterConfigError, apkFileName, apksToDelete, applyOtaUpdate, buildCheckRequest, cachePrefix, checkForUpdate, configureUpdater, describeConfigProblems, discardBundle, downloadNativeUpdate, findCachedApk, getBuiltinVersion, getBundleVersion, getCurrentBundle, getDeviceId, getOsFacts, getPlatform, getPluginVersion, getUpdaterConfig, getVersionCode, isCompleteDownload, isNative, logUpdateEvent, notifyAppReady, openNativeInstaller, parseApkFileName, pruneApkCache };
|
|
172
251
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/device.ts","../src/api.service.ts","../src/config.ts","../src/install.service.ts"],"mappings":";;;;;;;;;;;;;;iBAgBsB,kBAAkB;;;;;;;;iBAkBlB,oBAAoB;;;;;;;;;iBAmBpB,eAAe;iBAWrB;iBAIA;;;;;;;iBAUM,oBAAoB;;;;;;;;iBAkBpB,qBAAqB;;;;;;;;;iBAmBrB,cAAc;EAAU;EAAoB;;;;;cC7FrD,2BAA2B;WAC7B;EAEG,YAAA;;;cAQD,gCAAgC;WAClC,UAAU;EAEP,YAAA,UAAU;;;;;;;;;;;;UAuCP;EACf;EACA,UAAU,kBAAkB;EAC5B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;;;;;iBAYc,kBAAkB,OAAO,cAAc;iBAyBjC,kBAAkB,QAAQ;;;;;;;;iBAmD1B,eACpB,OAAO,eACP,QAAQ,kBACR;EAAY;IACX;;;;;;;;;;;UC3Kc;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;iBAoBc,iBAAiB,MAAM,QAAQ;iBAS/B,oBAAoB;;;;;;;;;;iBAuBpB,uBAAuB,QAAQ
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/device.ts","../src/api.service.ts","../src/config.ts","../src/apk-cache.ts","../src/install.service.ts"],"mappings":";;;;;;;;;;;;;;iBAgBsB,kBAAkB;;;;;;;;iBAkBlB,oBAAoB;;;;;;;;;iBAmBpB,eAAe;iBAWrB;iBAIA;;;;;;;iBAUM,oBAAoB;;;;;;;;iBAkBpB,qBAAqB;;;;;;;;;iBAmBrB,cAAc;EAAU;EAAoB;;;;;cC7FrD,2BAA2B;WAC7B;EAEG,YAAA;;;cAQD,gCAAgC;WAClC,UAAU;EAEP,YAAA,UAAU;;;;;;;;;;;;UAuCP;EACf;EACA,UAAU,kBAAkB;EAC5B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;;;;;iBAYc,kBAAkB,OAAO,cAAc;iBAyBjC,kBAAkB,QAAQ;;;;;;;;iBAmD1B,eACpB,OAAO,eACP,QAAQ,kBACR;EAAY;IACX;;;;;;;;;;;UC3Kc;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;iBAoBc,iBAAiB,MAAM,QAAQ;iBAS/B,oBAAoB;;;;;;;;;;iBAuBpB,uBAAuB,QAAQ;;;;;;;;;;;;;;;;;;;;;;;iBCnD/B,YAAY;UAIX;EACf;EACA;;;iBAIc,YAAY,eAAe,QAAQ;;;;;;;;iBAWnC,iBAAiB,eAAe,mBAAmB;;;;;;;;;;;;;iBAuBnD,mBACd;EAAU;UACV;UAMe,kBAAkB;EACjC;;;;;;;;;;;;;;;;;;;;;iBAsBc,aAAa;EAC3B,QAAQ;EACR;EACA;;;;;;;;;;;;iBCrFoB,oBAAoB,eAAe"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
export { UpdateCheckBlockedError, UpdaterConfigError, applyOtaUpdate, buildCheckRequest,
|
|
1
|
+
import { C as getPlatform, D as configureUpdater, E as isNative, O as describeConfigProblems, S as getOsFacts, T as getVersionCode, _ as checkForUpdate, a as openNativeInstaller, b as getBundleVersion, c as pruneApkCache, d as cachePrefix, f as isCompleteDownload, g as buildCheckRequest, h as UpdaterConfigError, i as notifyAppReady, k as getUpdaterConfig, l as apkFileName, m as UpdateCheckBlockedError, n as discardBundle, o as downloadNativeUpdate, p as parseApkFileName, r as getCurrentBundle, s as findCachedApk, t as applyOtaUpdate, u as apksToDelete, v as logUpdateEvent, w as getPluginVersion, x as getDeviceId, y as getBuiltinVersion } from "./ota.service-DPjhi4rw.js";
|
|
2
|
+
export { UpdateCheckBlockedError, UpdaterConfigError, apkFileName, apksToDelete, applyOtaUpdate, buildCheckRequest, cachePrefix, checkForUpdate, configureUpdater, describeConfigProblems, discardBundle, downloadNativeUpdate, findCachedApk, getBuiltinVersion, getBundleVersion, getCurrentBundle, getDeviceId, getOsFacts, getPlatform, getPluginVersion, getUpdaterConfig, getVersionCode, isCompleteDownload, isNative, logUpdateEvent, notifyAppReady, openNativeInstaller, parseApkFileName, pruneApkCache };
|
|
@@ -5,16 +5,39 @@ interface DownloadProgress {
|
|
|
5
5
|
total: number;
|
|
6
6
|
percent: number;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* The path of an already-downloaded, complete copy of this update.
|
|
10
|
+
*
|
|
11
|
+
* The only record that a download had finished used to be an in-memory
|
|
12
|
+
* `cachedPath`, so closing the app threw it away and the next launch downloaded
|
|
13
|
+
* the same 45 MB again while the file sat on disk. Asking the filesystem
|
|
14
|
+
* survives a restart.
|
|
15
|
+
*/
|
|
16
|
+
declare function findCachedApk(update: ResolvedUpdate): Promise<string | null>;
|
|
8
17
|
/**
|
|
9
18
|
* Downloads a native APK into the app cache and returns its path.
|
|
10
19
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
20
|
+
* Returns immediately when a complete copy is already there. The previous
|
|
21
|
+
* implementation could not: its first step was `cleanApkCache()`, which deleted
|
|
22
|
+
* every APK for this app - including the exact file it was about to fetch - so
|
|
23
|
+
* pressing update twice always paid for the binary twice.
|
|
14
24
|
*/
|
|
15
25
|
declare function downloadNativeUpdate(update: ResolvedUpdate, onProgress: (progress: DownloadProgress) => void): Promise<string>;
|
|
16
|
-
/**
|
|
17
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Deletes cached APKs the device has outgrown.
|
|
28
|
+
*
|
|
29
|
+
* Called on start-up with the installed build number, which is how an installed
|
|
30
|
+
* APK is finally cleaned up: the Android installer never calls back, but the
|
|
31
|
+
* next launch reports a higher build number and that says the same thing. A
|
|
32
|
+
* *newer* APK is kept - it is an update already paid for and waiting to be
|
|
33
|
+
* installed.
|
|
34
|
+
*
|
|
35
|
+
* Failures are non-fatal. Reclaiming disk space must never break an update.
|
|
36
|
+
*/
|
|
37
|
+
declare function pruneApkCache(options: {
|
|
38
|
+
installedVersionCode: number;
|
|
39
|
+
keep?: string | undefined;
|
|
40
|
+
}): Promise<string[]>;
|
|
18
41
|
//#endregion
|
|
19
42
|
//#region src/ota.service.d.ts
|
|
20
43
|
/**
|
|
@@ -45,5 +68,5 @@ declare function applyOtaUpdate(update: ResolvedUpdate): Promise<void>;
|
|
|
45
68
|
/** Discards a downloaded bundle that will not be applied. */
|
|
46
69
|
declare function discardBundle(bundleId: string): Promise<void>;
|
|
47
70
|
//#endregion
|
|
48
|
-
export { DownloadProgress as a, notifyAppReady as i, discardBundle as n,
|
|
49
|
-
//# sourceMappingURL=ota.service-
|
|
71
|
+
export { DownloadProgress as a, pruneApkCache as c, notifyAppReady as i, discardBundle as n, downloadNativeUpdate as o, getCurrentBundle as r, findCachedApk as s, applyOtaUpdate as t };
|
|
72
|
+
//# sourceMappingURL=ota.service-B99qm0Bm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ota.service-B99qm0Bm.d.ts","names":[],"sources":["../src/download.service.ts","../src/ota.service.ts"],"mappings":";;UAYiB;EACf;EACA;EACA;;;;;;;;;;iBAyCoB,cAAc,QAAQ,iBAAiB;;;;;;;;;iBAyBvC,qBACpB,QAAQ,gBACR,aAAa,UAAU,4BACtB;;;;;;;;;;;;iBAoEmB,cAAc;EAClC;EACA;IACE;;;;;;;;;;;;;;;;;;iBCvIkB,kBAAkB;;iBAWlB,oBAAgB,2CAAA;;;;;;;iBAiBhB,eAAe,QAAQ,iBAAiB;;iBA4BxC,cAAc,mBAAmB"}
|
|
@@ -266,6 +266,7 @@ async function logUpdateEvent(event, update, details) {
|
|
|
266
266
|
const payload = {
|
|
267
267
|
event,
|
|
268
268
|
platform: getPlatform(),
|
|
269
|
+
app_id: config.appId,
|
|
269
270
|
device_id: await getDeviceId(),
|
|
270
271
|
current_version_code: await getVersionCode(),
|
|
271
272
|
new_version: update.version,
|
|
@@ -281,6 +282,97 @@ async function logUpdateEvent(event, update, details) {
|
|
|
281
282
|
}
|
|
282
283
|
}
|
|
283
284
|
//#endregion
|
|
285
|
+
//#region src/apk-cache.ts
|
|
286
|
+
/**
|
|
287
|
+
* Which downloaded APK to keep, reuse, or throw away.
|
|
288
|
+
*
|
|
289
|
+
* A native binary is 40-60 MB and users pay for it, sometimes on mobile data.
|
|
290
|
+
* The previous implementation spent that repeatedly and needlessly:
|
|
291
|
+
*
|
|
292
|
+
* - `downloadNativeUpdate` called `cleanApkCache()` as its first step, which
|
|
293
|
+
* deleted every APK for this app - including the exact file it was about to
|
|
294
|
+
* fetch. A second press always re-downloaded.
|
|
295
|
+
* - The only record that a download had finished was `state.cachedPath`, in
|
|
296
|
+
* memory. Closing the app threw it away, so the next launch downloaded the
|
|
297
|
+
* same bytes again while the file sat on disk.
|
|
298
|
+
* - Nothing ever deleted a successfully installed APK, so it stayed in the
|
|
299
|
+
* cache forever.
|
|
300
|
+
*
|
|
301
|
+
* The decisions are here, as pure functions over file names and sizes, because
|
|
302
|
+
* the alternative is discovering the behaviour on a phone with a data plan.
|
|
303
|
+
* `download.service.ts` supplies the filesystem.
|
|
304
|
+
*/
|
|
305
|
+
/** Cache file name prefix, derived from the app id so two flavours never collide. */
|
|
306
|
+
function cachePrefix(appId) {
|
|
307
|
+
return `${appId.replaceAll(/[^\w.-]/g, "-")}-`;
|
|
308
|
+
}
|
|
309
|
+
/** `com.efficy.app-1.0.56-67.apk` */
|
|
310
|
+
function apkFileName(appId, update) {
|
|
311
|
+
return `${cachePrefix(appId)}${update.version}-${update.versionCode}.apk`;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Reads a name produced by `apkFileName` back into what it holds.
|
|
315
|
+
*
|
|
316
|
+
* Returns null for anything else in the cache directory - the WebView's own
|
|
317
|
+
* files live there too, and deleting one of those would be a very expensive
|
|
318
|
+
* mistake to make with a regex.
|
|
319
|
+
*/
|
|
320
|
+
function parseApkFileName(appId, fileName) {
|
|
321
|
+
const prefix = cachePrefix(appId);
|
|
322
|
+
if (!fileName.startsWith(prefix) || !fileName.endsWith(".apk")) return null;
|
|
323
|
+
const middle = fileName.slice(prefix.length, -4);
|
|
324
|
+
const match = /^(.+)-(\d+)$/.exec(middle);
|
|
325
|
+
if (!match) return null;
|
|
326
|
+
return {
|
|
327
|
+
version: match[1],
|
|
328
|
+
versionCode: Number(match[2])
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Whether a cached file is the update being offered, complete.
|
|
333
|
+
*
|
|
334
|
+
* Size is the check. A download interrupted by a dead connection leaves a
|
|
335
|
+
* partial file at the right path, and reusing that hands the Android installer
|
|
336
|
+
* a truncated APK - which fails with "There was a problem parsing the package",
|
|
337
|
+
* a message that says nothing about the real cause.
|
|
338
|
+
*
|
|
339
|
+
* When the server did not send a size there is nothing to verify against, so
|
|
340
|
+
* the file is not trusted. Re-downloading costs bandwidth; installing a
|
|
341
|
+
* truncated binary costs a support call.
|
|
342
|
+
*/
|
|
343
|
+
function isCompleteDownload(cached, expectedSize) {
|
|
344
|
+
if (!cached || !expectedSize) return false;
|
|
345
|
+
return cached.size === expectedSize;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Which cached APKs to delete, given what is installed and what is wanted.
|
|
349
|
+
*
|
|
350
|
+
* Two situations, deliberately one function because they differ by one rule:
|
|
351
|
+
*
|
|
352
|
+
* **Pruning** (no `keep`) removes only what the device has outgrown - anything
|
|
353
|
+
* whose build number the installed binary has caught up with. That is how an
|
|
354
|
+
* installed APK finally gets deleted: the Android installer never calls back,
|
|
355
|
+
* but the next launch reports a higher build number, which says the same thing.
|
|
356
|
+
* A *newer* APK is left alone, because it is an update already downloaded and
|
|
357
|
+
* waiting to be installed, and throwing it away would mean paying for it twice.
|
|
358
|
+
*
|
|
359
|
+
* **Making room** (`keep` given) additionally removes other pending downloads,
|
|
360
|
+
* because the file about to be written is tens of megabytes and the OS will
|
|
361
|
+
* evict from a full cache mid-download.
|
|
362
|
+
*
|
|
363
|
+
* `keep` itself is never deleted. Deleting the file that is about to be
|
|
364
|
+
* downloaded is the exact bug this module exists to prevent.
|
|
365
|
+
*/
|
|
366
|
+
function apksToDelete(input) {
|
|
367
|
+
const { cached, installedVersionCode, keep } = input;
|
|
368
|
+
const makingRoom = keep !== void 0;
|
|
369
|
+
return cached.filter((apk) => {
|
|
370
|
+
if (apk.fileName === keep) return false;
|
|
371
|
+
if (apk.versionCode <= installedVersionCode) return true;
|
|
372
|
+
return makingRoom;
|
|
373
|
+
}).map((apk) => apk.fileName);
|
|
374
|
+
}
|
|
375
|
+
//#endregion
|
|
284
376
|
//#region src/optional-plugins.ts
|
|
285
377
|
/**
|
|
286
378
|
* Plugins only the native-update path needs, loaded when it runs.
|
|
@@ -331,27 +423,86 @@ const nativePlugins = {
|
|
|
331
423
|
};
|
|
332
424
|
//#endregion
|
|
333
425
|
//#region src/download.service.ts
|
|
334
|
-
|
|
335
|
-
|
|
426
|
+
const DONE = {
|
|
427
|
+
loaded: 0,
|
|
428
|
+
total: 0,
|
|
429
|
+
percent: 100
|
|
430
|
+
};
|
|
431
|
+
function fileNameFor(update) {
|
|
432
|
+
const { appId, appName } = getUpdaterConfig();
|
|
433
|
+
return apkFileName(appId || appName, {
|
|
434
|
+
version: update.version,
|
|
435
|
+
versionCode: update.versionCode ?? 0
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
/** Every APK in the cache that belongs to this app. */
|
|
439
|
+
async function listCachedApks() {
|
|
336
440
|
const { appId, appName } = getUpdaterConfig();
|
|
337
|
-
|
|
441
|
+
const owner = appId || appName;
|
|
442
|
+
try {
|
|
443
|
+
const { Directory, Filesystem } = await nativePlugins.filesystem();
|
|
444
|
+
const { files } = await Filesystem.readdir({
|
|
445
|
+
directory: Directory.Cache,
|
|
446
|
+
path: ""
|
|
447
|
+
});
|
|
448
|
+
return files.flatMap((file) => {
|
|
449
|
+
const identity = parseApkFileName(owner, file.name);
|
|
450
|
+
return identity ? [{
|
|
451
|
+
...identity,
|
|
452
|
+
fileName: file.name
|
|
453
|
+
}] : [];
|
|
454
|
+
});
|
|
455
|
+
} catch {
|
|
456
|
+
return [];
|
|
457
|
+
}
|
|
338
458
|
}
|
|
339
|
-
|
|
340
|
-
|
|
459
|
+
/**
|
|
460
|
+
* The path of an already-downloaded, complete copy of this update.
|
|
461
|
+
*
|
|
462
|
+
* The only record that a download had finished used to be an in-memory
|
|
463
|
+
* `cachedPath`, so closing the app threw it away and the next launch downloaded
|
|
464
|
+
* the same 45 MB again while the file sat on disk. Asking the filesystem
|
|
465
|
+
* survives a restart.
|
|
466
|
+
*/
|
|
467
|
+
async function findCachedApk(update) {
|
|
468
|
+
const fileName = fileNameFor(update);
|
|
469
|
+
try {
|
|
470
|
+
const { Directory, Filesystem } = await nativePlugins.filesystem();
|
|
471
|
+
if (!isCompleteDownload({ size: (await Filesystem.stat({
|
|
472
|
+
directory: Directory.Cache,
|
|
473
|
+
path: fileName
|
|
474
|
+
})).size }, update.fileSize)) return null;
|
|
475
|
+
const { uri } = await Filesystem.getUri({
|
|
476
|
+
directory: Directory.Cache,
|
|
477
|
+
path: fileName
|
|
478
|
+
});
|
|
479
|
+
return uri;
|
|
480
|
+
} catch {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
341
483
|
}
|
|
342
484
|
/**
|
|
343
485
|
* Downloads a native APK into the app cache and returns its path.
|
|
344
486
|
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
487
|
+
* Returns immediately when a complete copy is already there. The previous
|
|
488
|
+
* implementation could not: its first step was `cleanApkCache()`, which deleted
|
|
489
|
+
* every APK for this app - including the exact file it was about to fetch - so
|
|
490
|
+
* pressing update twice always paid for the binary twice.
|
|
348
491
|
*/
|
|
349
492
|
async function downloadNativeUpdate(update, onProgress) {
|
|
350
493
|
if (!update.downloadUrl) throw new Error("This update has no download URL");
|
|
494
|
+
const existing = await findCachedApk(update);
|
|
495
|
+
if (existing) {
|
|
496
|
+
onProgress({ ...DONE });
|
|
497
|
+
return existing;
|
|
498
|
+
}
|
|
351
499
|
const { Network } = await nativePlugins.network();
|
|
352
500
|
if (!(await Network.getStatus()).connected) throw new Error("Connect to the internet to download this update");
|
|
353
|
-
|
|
354
|
-
|
|
501
|
+
const fileName = fileNameFor(update);
|
|
502
|
+
await pruneApkCache({
|
|
503
|
+
installedVersionCode: 0,
|
|
504
|
+
keep: fileName
|
|
505
|
+
});
|
|
355
506
|
const [{ Directory, Filesystem }, { FileTransfer }] = await Promise.all([nativePlugins.filesystem(), nativePlugins.fileTransfer()]);
|
|
356
507
|
const destination = await Filesystem.getUri({
|
|
357
508
|
directory: Directory.Cache,
|
|
@@ -379,21 +530,33 @@ async function downloadNativeUpdate(update, onProgress) {
|
|
|
379
530
|
await progressListener?.remove();
|
|
380
531
|
}
|
|
381
532
|
}
|
|
382
|
-
/**
|
|
383
|
-
|
|
384
|
-
|
|
533
|
+
/**
|
|
534
|
+
* Deletes cached APKs the device has outgrown.
|
|
535
|
+
*
|
|
536
|
+
* Called on start-up with the installed build number, which is how an installed
|
|
537
|
+
* APK is finally cleaned up: the Android installer never calls back, but the
|
|
538
|
+
* next launch reports a higher build number and that says the same thing. A
|
|
539
|
+
* *newer* APK is kept - it is an update already paid for and waiting to be
|
|
540
|
+
* installed.
|
|
541
|
+
*
|
|
542
|
+
* Failures are non-fatal. Reclaiming disk space must never break an update.
|
|
543
|
+
*/
|
|
544
|
+
async function pruneApkCache(options) {
|
|
385
545
|
try {
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
path: ""
|
|
546
|
+
const doomed = apksToDelete({
|
|
547
|
+
cached: await listCachedApks(),
|
|
548
|
+
...options
|
|
390
549
|
});
|
|
391
|
-
|
|
550
|
+
if (doomed.length === 0) return [];
|
|
551
|
+
const { Directory, Filesystem } = await nativePlugins.filesystem();
|
|
552
|
+
await Promise.all(doomed.map((fileName) => Filesystem.deleteFile({
|
|
392
553
|
directory: Directory.Cache,
|
|
393
|
-
path:
|
|
554
|
+
path: fileName
|
|
394
555
|
})));
|
|
556
|
+
return doomed;
|
|
395
557
|
} catch (error) {
|
|
396
|
-
console.warn("[capuchoo] could not
|
|
558
|
+
console.warn("[capuchoo] could not prune the APK cache", error);
|
|
559
|
+
return [];
|
|
397
560
|
}
|
|
398
561
|
}
|
|
399
562
|
//#endregion
|
|
@@ -490,6 +653,6 @@ async function discardBundle(bundleId) {
|
|
|
490
653
|
}
|
|
491
654
|
}
|
|
492
655
|
//#endregion
|
|
493
|
-
export {
|
|
656
|
+
export { getPlatform as C, configureUpdater as D, isNative as E, describeConfigProblems as O, getOsFacts as S, getVersionCode as T, checkForUpdate as _, openNativeInstaller as a, getBundleVersion as b, pruneApkCache as c, cachePrefix as d, isCompleteDownload as f, buildCheckRequest as g, UpdaterConfigError as h, notifyAppReady as i, getUpdaterConfig as k, apkFileName as l, UpdateCheckBlockedError as m, discardBundle as n, downloadNativeUpdate as o, parseApkFileName as p, getCurrentBundle as r, findCachedApk as s, applyOtaUpdate as t, apksToDelete as u, logUpdateEvent as v, getPluginVersion as w, getDeviceId as x, getBuiltinVersion as y };
|
|
494
657
|
|
|
495
|
-
//# sourceMappingURL=ota.service-
|
|
658
|
+
//# sourceMappingURL=ota.service-DPjhi4rw.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ota.service-DPjhi4rw.js","names":[],"sources":["../src/config.ts","../src/device.ts","../src/api.service.ts","../src/apk-cache.ts","../src/optional-plugins.ts","../src/download.service.ts","../src/install.service.ts","../src/ota.service.ts"],"sourcesContent":["/**\n * Runtime configuration for the updater.\n *\n * Values come from the build's `VITE_*` variables, which the CLI injects from\n * the flavour's env file. `configureUpdater` lets an app override any of them\n * at startup - useful for tests and for apps that resolve their endpoint from\n * a login response rather than at build time.\n */\nexport interface UpdaterConfig {\n /** Base URL of the Capuchoo backend, with no trailing slash. */\n apiUrl: string;\n /** Bundle identifier of this build. Must match what the CLI published. */\n appId: string;\n /** Human-readable name, used in prompts and in the APK cache file name. */\n appName: string;\n /** Channel to consult. Bound to an environment server-side. */\n channel: string;\n /** Free-form: an app may use flavours beyond dev/staging/prod. */\n environment: string;\n /** Milliseconds before an update check is abandoned. */\n timeoutMs: number;\n}\n\nfunction env(key: string): string | undefined {\n // `import.meta.env` is replaced at build time by Vite. Guard the access so\n // the module can also be imported from Node (tests, SSR) without throwing.\n const source = (import.meta as ImportMeta & { env?: Record<string, string | undefined> }).env;\n return source?.[key];\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\nlet overrides: Partial<UpdaterConfig> = {};\n\n/**\n * Overrides configuration resolved from the build. Call before `init`.\n * Passing `{}` clears previous overrides.\n */\nexport function configureUpdater(next: Partial<UpdaterConfig>): void {\n overrides = { ...overrides, ...next };\n}\n\n/** @internal exposed for tests. */\nexport function resetUpdaterConfig(): void {\n overrides = {};\n}\n\nexport function getUpdaterConfig(): UpdaterConfig {\n const apiUrl = overrides.apiUrl ?? env(\"VITE_UPDATE_API_URL\") ?? \"\";\n\n return {\n apiUrl: stripTrailingSlash(apiUrl),\n appId: overrides.appId ?? env(\"VITE_APP_ID\") ?? \"\",\n appName: overrides.appName ?? env(\"VITE_APP_NAME\") ?? \"the app\",\n channel: overrides.channel ?? env(\"VITE_UPDATE_CHANNEL\") ?? \"prod\",\n environment:\n overrides.environment ?? env(\"VITE_ENVIRONMENT\") ?? (env(\"PROD\") === \"true\" ? \"prod\" : \"dev\"),\n timeoutMs: overrides.timeoutMs ?? 30_000,\n };\n}\n\n/**\n * Reasons the updater cannot run, as user-facing strings.\n *\n * The previous implementation defaulted `apiUrl` to a hard-coded Render URL and\n * `appId` to a hard-coded bundle id. A build with a missing variable therefore\n * silently pointed at somebody else's backend, or asked for the wrong app, and\n * reported \"you are up to date\". Failing loudly is the whole point of this\n * function.\n */\nexport function describeConfigProblems(config: UpdaterConfig): string[] {\n const problems: string[] = [];\n if (!config.apiUrl) {\n problems.push(\"VITE_UPDATE_API_URL is not set, so updates cannot be checked\");\n }\n if (!config.appId) {\n problems.push(\"VITE_APP_ID is not set, so the server cannot identify this build\");\n }\n return problems;\n}\n","import { App } from \"@capacitor/app\";\nimport { Capacitor } from \"@capacitor/core\";\nimport { CapacitorUpdater } from \"@capgo/capacitor-updater\";\n\n/**\n * Facts about the running build that the server needs in order to decide\n * whether an update applies.\n */\n\n/**\n * Native build number of the installed binary.\n *\n * Returns 0 off-device. The old implementation returned 999999 on web, which\n * meant a browser session claimed to be newer than every published release and\n * so never saw an update - masking the very bug you would be debugging.\n */\nexport async function getVersionCode(): Promise<number> {\n if (!Capacitor.isNativePlatform()) return 0;\n\n try {\n const info = await App.getInfo();\n return Number.parseInt(info.build, 10) || 0;\n } catch {\n return 0;\n }\n}\n\n/**\n * Semantic version of the web bundle currently applied.\n *\n * `\"builtin\"` means no OTA bundle has been applied yet and the app is running\n * the assets compiled into the binary. The server treats it as 0.0.0, so it\n * must be reported honestly rather than sent as a constant.\n */\nexport async function getBundleVersion(): Promise<string> {\n if (!Capacitor.isNativePlatform()) return \"builtin\";\n\n try {\n const current = await CapacitorUpdater.current();\n return current.bundle.version || \"builtin\";\n } catch {\n return \"builtin\";\n }\n}\n\n/**\n * Stable per-install identifier, supplied by the OTA plugin.\n *\n * The plugin persists this natively. Reading `localStorage.device_id` instead -\n * as the app template did - returns null on a fresh install and is wiped\n * whenever the WebView data is cleared, so channel overrides and per-device\n * stats silently stopped working.\n */\nexport async function getDeviceId(): Promise<string> {\n if (!Capacitor.isNativePlatform()) return \"web\";\n\n try {\n const { deviceId } = await CapacitorUpdater.getDeviceId();\n return deviceId || \"unknown\";\n } catch {\n return \"unknown\";\n }\n}\n\nexport function getPlatform(): \"android\" | \"ios\" | \"web\" {\n return Capacitor.getPlatform() as \"android\" | \"ios\" | \"web\";\n}\n\nexport function isNative(): boolean {\n return Capacitor.isNativePlatform();\n}\n\n/**\n * Version of the OTA plugin the app is running.\n *\n * Useful when a device misbehaves: plugin version explains more failures than\n * app version does.\n */\nexport async function getPluginVersion(): Promise<string | undefined> {\n if (!Capacitor.isNativePlatform()) return undefined;\n\n try {\n const { version } = await CapacitorUpdater.getPluginVersion();\n return version || undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Bundle version compiled into the binary.\n *\n * Distinct from `getBundleVersion()`, which reports the OTA bundle currently\n * applied - that one says `\"builtin\"` when none has been, and this says which\n * builtin that is.\n */\nexport async function getBuiltinVersion(): Promise<string | undefined> {\n if (!Capacitor.isNativePlatform()) return undefined;\n\n try {\n const { version } = await CapacitorUpdater.getBuiltinVersion();\n return version || undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * OS version and emulator flag, from `@capacitor/device`.\n *\n * That package is an *optional* peer: it is a separate install, and an app that\n * does not have it should still be able to check for updates. So it is imported\n * dynamically and every failure - not installed, not registered, throwing on an\n * odd platform - resolves to `undefined`, which the request builder omits.\n */\nexport async function getOsFacts(): Promise<{ versionOs?: string; isEmulator?: boolean }> {\n if (!Capacitor.isNativePlatform()) return {};\n\n try {\n const { Device } = await import(\"@capacitor/device\");\n const info = await Device.getInfo();\n return {\n ...(info.osVersion ? { versionOs: info.osVersion } : {}),\n ...(typeof info.isVirtual === \"boolean\" ? { isEmulator: info.isVirtual } : {}),\n };\n } catch {\n return {};\n }\n}\n","import {\n isBlockingResponse,\n resolveUpdate,\n type ResolvedUpdate,\n type UpdateCheckRequest,\n type UpdateCheckResponse,\n type UpdateEvent,\n type UpdateEventPayload,\n} from \"@capuchoo/core\";\nimport { getUpdaterConfig, describeConfigProblems } from \"./config.js\";\nimport {\n getBuiltinVersion,\n getBundleVersion,\n getDeviceId,\n getOsFacts,\n getPlatform,\n getPluginVersion,\n getVersionCode,\n isNative,\n} from \"./device.js\";\n\n/** Raised when the updater is misconfigured, rather than reporting \"up to date\". */\nexport class UpdaterConfigError extends Error {\n readonly problems: string[];\n\n constructor(problems: string[]) {\n super(problems.join(\"; \"));\n this.name = \"UpdaterConfigError\";\n this.problems = problems;\n }\n}\n\n/** Raised when the server says the request itself cannot be served. */\nexport class UpdateCheckBlockedError extends Error {\n readonly response: UpdateCheckResponse;\n\n constructor(response: UpdateCheckResponse) {\n super(response.message ?? \"The update service rejected this request\");\n this.name = \"UpdateCheckBlockedError\";\n this.response = response;\n }\n}\n\nasync function postJson<T>(url: string, body: unknown, timeoutMs: number): Promise<T> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n throw new Error(`${url} responded ${response.status} ${response.statusText}`);\n }\n\n return (await response.json()) as T;\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Asks the server what this device should be running.\n *\n * One request, one endpoint. The app template used to call\n * `GET /api/native-updates/check` for native updates *and*\n * `POST /api/update` for OTA, which meant two sources of truth: the native\n * endpoint ignores the channel's assigned native version and the\n * `min_update_version` gate, so a device could be told to install an OTA\n * bundle its binary was too old to run.\n */\nexport interface DeviceFacts {\n appId: string;\n platform: ReturnType<typeof getPlatform>;\n channel?: string | undefined;\n isProd: boolean;\n versionCode: number;\n versionName: string;\n deviceId: string;\n pluginVersion?: string | undefined;\n versionBuiltin?: string | undefined;\n versionOs?: string | undefined;\n isEmulator?: boolean | undefined;\n customId?: string | undefined;\n}\n\n/**\n * Assemble the check payload.\n *\n * Pure, and separate from the plugin calls that gather the facts, so the shape\n * of what goes on the wire can be tested without a device. Fields the app could\n * not determine are *omitted* rather than sent empty: the server writes only the\n * keys it receives, so a placeholder would overwrite a better value that an\n * earlier, better-informed check had already stored.\n */\nexport function buildCheckRequest(facts: DeviceFacts): UpdateCheckRequest {\n const request: UpdateCheckRequest = {\n appId: facts.appId,\n platform: facts.platform,\n versionCode: String(facts.versionCode),\n // Historical alias. The server accepts either and prefers versionCode.\n versionBuild: String(facts.versionCode),\n version_name: facts.versionName,\n deviceId: facts.deviceId,\n isProd: facts.isProd,\n };\n\n if (facts.channel) {\n request.channel = facts.channel;\n request.defaultChannel = facts.channel;\n }\n if (facts.pluginVersion) request.pluginVersion = facts.pluginVersion;\n if (facts.versionBuiltin) request.versionBuiltin = facts.versionBuiltin;\n if (facts.versionOs) request.versionOs = facts.versionOs;\n if (typeof facts.isEmulator === \"boolean\") request.isEmulator = facts.isEmulator;\n if (facts.customId) request.customId = facts.customId;\n\n return request;\n}\n\nexport async function checkForUpdate(): Promise<ResolvedUpdate | null> {\n if (!isNative()) return null;\n\n const config = getUpdaterConfig();\n const problems = describeConfigProblems(config);\n if (problems.length > 0) throw new UpdaterConfigError(problems);\n\n const [versionCode, versionName, deviceId, pluginVersion, versionBuiltin, osFacts] =\n await Promise.all([\n getVersionCode(),\n getBundleVersion(),\n getDeviceId(),\n getPluginVersion(),\n getBuiltinVersion(),\n getOsFacts(),\n ]);\n\n const request = buildCheckRequest({\n appId: config.appId,\n platform: getPlatform(),\n channel: config.channel,\n isProd: config.environment === \"prod\",\n versionCode,\n versionName,\n deviceId,\n pluginVersion,\n versionBuiltin,\n ...osFacts,\n });\n\n const response = await postJson<UpdateCheckResponse>(\n `${config.apiUrl}/api/update`,\n request,\n config.timeoutMs,\n );\n\n // \"Channel not found\" and \"Environment mismatch\" are deployment mistakes.\n // Treating them as \"no update\" is how a broken channel goes unnoticed for\n // weeks.\n if (isBlockingResponse(response)) throw new UpdateCheckBlockedError(response);\n\n return resolveUpdate(response);\n}\n\n/**\n * Records a native update lifecycle event.\n *\n * Best effort: analytics must never break an update. OTA events are reported\n * by the plugin itself through its `statsUrl`, so only native ones are sent\n * from here.\n */\nexport async function logUpdateEvent(\n event: UpdateEvent,\n update: ResolvedUpdate,\n details?: { error?: string },\n): Promise<void> {\n if (update.kind !== \"native\") return;\n\n const config = getUpdaterConfig();\n if (!config.apiUrl) return;\n\n const payload: UpdateEventPayload = {\n event,\n platform: getPlatform(),\n // Without this the server has no row to attach the event to and answers\n // 400, which is what it did for every native event ever sent.\n app_id: config.appId,\n device_id: await getDeviceId(),\n current_version_code: await getVersionCode(),\n new_version: update.version,\n new_version_code: update.versionCode,\n channel: config.channel,\n environment: String(config.environment),\n ...details,\n };\n\n try {\n await postJson(`${config.apiUrl}/api/native-updates/log`, payload, config.timeoutMs);\n } catch (error) {\n console.warn(\"[capuchoo] could not record update event\", error);\n }\n}\n","/**\n * Which downloaded APK to keep, reuse, or throw away.\n *\n * A native binary is 40-60 MB and users pay for it, sometimes on mobile data.\n * The previous implementation spent that repeatedly and needlessly:\n *\n * - `downloadNativeUpdate` called `cleanApkCache()` as its first step, which\n * deleted every APK for this app - including the exact file it was about to\n * fetch. A second press always re-downloaded.\n * - The only record that a download had finished was `state.cachedPath`, in\n * memory. Closing the app threw it away, so the next launch downloaded the\n * same bytes again while the file sat on disk.\n * - Nothing ever deleted a successfully installed APK, so it stayed in the\n * cache forever.\n *\n * The decisions are here, as pure functions over file names and sizes, because\n * the alternative is discovering the behaviour on a phone with a data plan.\n * `download.service.ts` supplies the filesystem.\n */\n\n/** Cache file name prefix, derived from the app id so two flavours never collide. */\nexport function cachePrefix(appId: string): string {\n return `${appId.replaceAll(/[^\\w.-]/g, \"-\")}-`;\n}\n\nexport interface ApkIdentity {\n version: string;\n versionCode: number;\n}\n\n/** `com.efficy.app-1.0.56-67.apk` */\nexport function apkFileName(appId: string, update: ApkIdentity): string {\n return `${cachePrefix(appId)}${update.version}-${update.versionCode}.apk`;\n}\n\n/**\n * Reads a name produced by `apkFileName` back into what it holds.\n *\n * Returns null for anything else in the cache directory - the WebView's own\n * files live there too, and deleting one of those would be a very expensive\n * mistake to make with a regex.\n */\nexport function parseApkFileName(appId: string, fileName: string): ApkIdentity | null {\n const prefix = cachePrefix(appId);\n if (!fileName.startsWith(prefix) || !fileName.endsWith(\".apk\")) return null;\n\n const middle = fileName.slice(prefix.length, -\".apk\".length);\n const match = /^(.+)-(\\d+)$/.exec(middle);\n if (!match) return null;\n\n return { version: match[1]!, versionCode: Number(match[2]) };\n}\n\n/**\n * Whether a cached file is the update being offered, complete.\n *\n * Size is the check. A download interrupted by a dead connection leaves a\n * partial file at the right path, and reusing that hands the Android installer\n * a truncated APK - which fails with \"There was a problem parsing the package\",\n * a message that says nothing about the real cause.\n *\n * When the server did not send a size there is nothing to verify against, so\n * the file is not trusted. Re-downloading costs bandwidth; installing a\n * truncated binary costs a support call.\n */\nexport function isCompleteDownload(\n cached: { size: number } | null,\n expectedSize: number | undefined,\n): boolean {\n if (!cached || !expectedSize) return false;\n return cached.size === expectedSize;\n}\n\nexport interface CachedApk extends ApkIdentity {\n fileName: string;\n}\n\n/**\n * Which cached APKs to delete, given what is installed and what is wanted.\n *\n * Two situations, deliberately one function because they differ by one rule:\n *\n * **Pruning** (no `keep`) removes only what the device has outgrown - anything\n * whose build number the installed binary has caught up with. That is how an\n * installed APK finally gets deleted: the Android installer never calls back,\n * but the next launch reports a higher build number, which says the same thing.\n * A *newer* APK is left alone, because it is an update already downloaded and\n * waiting to be installed, and throwing it away would mean paying for it twice.\n *\n * **Making room** (`keep` given) additionally removes other pending downloads,\n * because the file about to be written is tens of megabytes and the OS will\n * evict from a full cache mid-download.\n *\n * `keep` itself is never deleted. Deleting the file that is about to be\n * downloaded is the exact bug this module exists to prevent.\n */\nexport function apksToDelete(input: {\n cached: CachedApk[];\n installedVersionCode: number;\n keep?: string | undefined;\n}): string[] {\n const { cached, installedVersionCode, keep } = input;\n const makingRoom = keep !== undefined;\n\n return cached\n .filter((apk) => {\n if (apk.fileName === keep) return false;\n if (apk.versionCode <= installedVersionCode) return true;\n return makingRoom;\n })\n .map((apk) => apk.fileName);\n}\n","/**\n * Plugins only the native-update path needs, loaded when it runs.\n *\n * OTA updates need `@capgo/capacitor-updater` and nothing else. Downloading and\n * installing an APK additionally needs file transfer, filesystem, network and a\n * file opener - four packages an app that only ships web bundles should not have\n * to install.\n *\n * They are optional peers, imported here rather than at module load, so\n * importing this library does not require them. A missing one produces a message\n * naming what to install instead of `Cannot find module` from inside a bundler.\n *\n * They cannot be plain dependencies: `cap sync` discovers plugins by reading the\n * *application's* `dependencies` and `devDependencies` - `getDependencies()` in\n * @capacitor/cli does not recurse - so a plugin pulled in transitively would\n * have its JavaScript installed and its native half never added to the Android\n * or iOS project. `capuchoo setup --native` adds them to the app instead.\n */\n\nconst NATIVE_PACKAGES = [\n \"@capacitor/file-transfer\",\n \"@capacitor/filesystem\",\n \"@capacitor/network\",\n \"@capawesome-team/capacitor-file-opener\",\n] as const;\n\nexport class MissingNativePluginsError extends Error {\n readonly packages: readonly string[];\n\n constructor(missing: string) {\n super(\n `Native updates need ${missing}, which is not installed. ` +\n `Run: npx capuchoo setup --native (adds ${NATIVE_PACKAGES.join(\", \")} and runs cap sync). ` +\n \"OTA updates do not need any of them.\",\n );\n this.name = \"MissingNativePluginsError\";\n this.packages = NATIVE_PACKAGES;\n }\n}\n\nasync function load<T>(specifier: string, importer: () => Promise<T>): Promise<T> {\n try {\n return await importer();\n } catch (error) {\n // A genuine runtime failure inside the plugin should not be reported as a\n // missing install, so only a resolution failure is translated.\n const message = error instanceof Error ? error.message : String(error);\n if (/cannot find module|failed to resolve|module not found/i.test(message)) {\n throw new MissingNativePluginsError(specifier);\n }\n throw error;\n }\n}\n\nexport const nativePlugins = {\n fileTransfer: () => load(\"@capacitor/file-transfer\", () => import(\"@capacitor/file-transfer\")),\n filesystem: () => load(\"@capacitor/filesystem\", () => import(\"@capacitor/filesystem\")),\n network: () => load(\"@capacitor/network\", () => import(\"@capacitor/network\")),\n fileOpener: () =>\n load(\n \"@capawesome-team/capacitor-file-opener\",\n () => import(\"@capawesome-team/capacitor-file-opener\"),\n ),\n};\n","import type { PluginListenerHandle } from \"@capacitor/core\";\nimport type { ResolvedUpdate } from \"@capuchoo/core\";\nimport {\n apkFileName,\n apksToDelete,\n isCompleteDownload,\n parseApkFileName,\n type CachedApk,\n} from \"./apk-cache.js\";\nimport { getUpdaterConfig } from \"./config.js\";\nimport { nativePlugins } from \"./optional-plugins.js\";\n\nexport interface DownloadProgress {\n loaded: number;\n total: number;\n percent: number;\n}\n\nconst DONE = { loaded: 0, total: 0, percent: 100 };\n\nfunction fileNameFor(update: ResolvedUpdate): string {\n const { appId, appName } = getUpdaterConfig();\n return apkFileName(appId || appName, {\n version: update.version,\n versionCode: update.versionCode ?? 0,\n });\n}\n\n/** Every APK in the cache that belongs to this app. */\nasync function listCachedApks(): Promise<CachedApk[]> {\n const { appId, appName } = getUpdaterConfig();\n const owner = appId || appName;\n\n try {\n const { Directory, Filesystem } = await nativePlugins.filesystem();\n const { files } = await Filesystem.readdir({ directory: Directory.Cache, path: \"\" });\n\n return files.flatMap((file) => {\n const identity = parseApkFileName(owner, file.name);\n return identity ? [{ ...identity, fileName: file.name }] : [];\n });\n } catch {\n // A cache we cannot read is a cache we cannot reuse or prune, and neither\n // is worth failing an update over.\n return [];\n }\n}\n\n/**\n * The path of an already-downloaded, complete copy of this update.\n *\n * The only record that a download had finished used to be an in-memory\n * `cachedPath`, so closing the app threw it away and the next launch downloaded\n * the same 45 MB again while the file sat on disk. Asking the filesystem\n * survives a restart.\n */\nexport async function findCachedApk(update: ResolvedUpdate): Promise<string | null> {\n const fileName = fileNameFor(update);\n\n try {\n const { Directory, Filesystem } = await nativePlugins.filesystem();\n\n const stat = await Filesystem.stat({ directory: Directory.Cache, path: fileName });\n if (!isCompleteDownload({ size: stat.size }, update.fileSize)) return null;\n\n const { uri } = await Filesystem.getUri({ directory: Directory.Cache, path: fileName });\n return uri;\n } catch {\n // stat throws when the file is not there, which is the common case.\n return null;\n }\n}\n\n/**\n * Downloads a native APK into the app cache and returns its path.\n *\n * Returns immediately when a complete copy is already there. The previous\n * implementation could not: its first step was `cleanApkCache()`, which deleted\n * every APK for this app - including the exact file it was about to fetch - so\n * pressing update twice always paid for the binary twice.\n */\nexport async function downloadNativeUpdate(\n update: ResolvedUpdate,\n onProgress: (progress: DownloadProgress) => void,\n): Promise<string> {\n if (!update.downloadUrl) {\n throw new Error(\"This update has no download URL\");\n }\n\n const existing = await findCachedApk(update);\n if (existing) {\n onProgress({ ...DONE });\n return existing;\n }\n\n const { Network } = await nativePlugins.network();\n const network = await Network.getStatus();\n if (!network.connected) {\n throw new Error(\"Connect to the internet to download this update\");\n }\n\n const fileName = fileNameFor(update);\n\n // Make room, but never for the file being written. APKs are tens of megabytes\n // and the OS can evict from a full cache mid-download.\n await pruneApkCache({ installedVersionCode: 0, keep: fileName });\n\n const [{ Directory, Filesystem }, { FileTransfer }] = await Promise.all([\n nativePlugins.filesystem(),\n nativePlugins.fileTransfer(),\n ]);\n\n const destination = await Filesystem.getUri({ directory: Directory.Cache, path: fileName });\n\n let progressListener: PluginListenerHandle | null = null;\n\n try {\n progressListener = await FileTransfer.addListener(\"progress\", (event) => {\n if (event.url !== update.downloadUrl) return;\n\n const percent =\n event.lengthComputable && event.contentLength > 0\n ? Math.round((event.bytes / event.contentLength) * 100)\n : 0;\n onProgress({ loaded: event.bytes, total: event.contentLength, percent });\n });\n\n const result = await FileTransfer.downloadFile({\n url: update.downloadUrl,\n path: destination.uri,\n progress: true,\n connectTimeout: 60_000,\n readTimeout: 300_000,\n });\n\n return result.path || destination.uri;\n } finally {\n await progressListener?.remove();\n }\n}\n\n/**\n * Deletes cached APKs the device has outgrown.\n *\n * Called on start-up with the installed build number, which is how an installed\n * APK is finally cleaned up: the Android installer never calls back, but the\n * next launch reports a higher build number and that says the same thing. A\n * *newer* APK is kept - it is an update already paid for and waiting to be\n * installed.\n *\n * Failures are non-fatal. Reclaiming disk space must never break an update.\n */\nexport async function pruneApkCache(options: {\n installedVersionCode: number;\n keep?: string | undefined;\n}): Promise<string[]> {\n try {\n const cached = await listCachedApks();\n const doomed = apksToDelete({ cached, ...options });\n if (doomed.length === 0) return [];\n\n const { Directory, Filesystem } = await nativePlugins.filesystem();\n await Promise.all(\n doomed.map((fileName) =>\n Filesystem.deleteFile({ directory: Directory.Cache, path: fileName }),\n ),\n );\n\n return doomed;\n } catch (error) {\n console.warn(\"[capuchoo] could not prune the APK cache\", error);\n return [];\n }\n}\n","import { Capacitor } from \"@capacitor/core\";\nimport { getUpdaterConfig } from \"./config.js\";\nimport { nativePlugins } from \"./optional-plugins.js\";\n\nconst APK_MIME = \"application/vnd.android.package-archive\";\n\n/**\n * Hands a downloaded APK to the Android package installer.\n *\n * Requires `REQUEST_INSTALL_PACKAGES` in the manifest - the Trapeze config for\n * each flavour merges it in - and the user must have allowed this app to\n * install unknown apps. Both failures surface as opaque platform errors, so\n * they are translated into something a user can act on.\n */\nexport async function openNativeInstaller(path: string): Promise<void> {\n if (Capacitor.getPlatform() !== \"android\") {\n throw new Error(\n \"Installing a native update from inside the app is only possible on Android. \" +\n \"On iOS the update has to come from the App Store or TestFlight.\",\n );\n }\n\n const { FileOpener } = await nativePlugins.fileOpener();\n\n try {\n await FileOpener.openFile({ path, mimeType: APK_MIME });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const { appName } = getUpdaterConfig();\n\n if (/permission|unknown sources|REQUEST_INSTALL_PACKAGES/i.test(message)) {\n throw new Error(\n `Allow ${appName} to install unknown apps in Android settings, then try again`,\n );\n }\n if (/activity|no app/i.test(message)) {\n throw new Error(\n \"No package installer is available on this device, so the update cannot be installed here\",\n );\n }\n\n throw new Error(`Could not open the Android installer: ${message}`);\n }\n}\n","import { CapacitorUpdater } from \"@capgo/capacitor-updater\";\nimport type { ResolvedUpdate } from \"@capuchoo/core\";\nimport { isNative } from \"./device.js\";\n\n/**\n * Thin wrapper over the OTA plugin.\n *\n * Downloading and applying a web bundle stays with `@capgo/capacitor-updater`:\n * it owns the native bundle store, the atomic swap and the rollback. This\n * module only sequences those calls correctly.\n */\n\n/**\n * Confirms the current bundle booted successfully.\n *\n * **This must be called once, early, on every app start.** If the plugin does\n * not hear it within `appReadyTimeout`, it assumes the new bundle crashed and\n * rolls back to the previous one - which looks exactly like \"the update did\n * not install\".\n */\nexport async function notifyAppReady(): Promise<void> {\n if (!isNative()) return;\n\n try {\n await CapacitorUpdater.notifyAppReady();\n } catch (error) {\n console.warn(\"[capuchoo] could not mark this bundle as ready\", error);\n }\n}\n\n/** The bundle currently applied, or null off-device. */\nexport async function getCurrentBundle() {\n if (!isNative()) return null;\n\n try {\n return await CapacitorUpdater.current();\n } catch (error) {\n console.warn(\"[capuchoo] could not read the current bundle\", error);\n return null;\n }\n}\n\n/**\n * Downloads an OTA bundle and applies it.\n *\n * `set` swaps the active bundle and reloads the WebView, so nothing after it\n * runs. It is called last on purpose.\n */\nexport async function applyOtaUpdate(update: ResolvedUpdate): Promise<void> {\n if (update.kind !== \"ota\") {\n throw new Error(\"applyOtaUpdate was given a native update\");\n }\n\n // Already downloaded - either by a previous attempt in this session, or by\n // the plugin's own background download when autoUpdate is \"onlyDownload\".\n if (update.bundleId) {\n await CapacitorUpdater.set({ id: update.bundleId });\n return;\n }\n\n if (!update.downloadUrl) {\n throw new Error(\"This update has no download URL\");\n }\n\n const bundle = await CapacitorUpdater.download({\n url: update.downloadUrl,\n version: update.version,\n ...(update.checksum ? { checksum: update.checksum } : {}),\n ...(update.sessionKey ? { sessionKey: update.sessionKey } : {}),\n });\n\n update.bundleId = bundle.id;\n await CapacitorUpdater.set({ id: bundle.id });\n}\n\n/** Discards a downloaded bundle that will not be applied. */\nexport async function discardBundle(bundleId: string): Promise<void> {\n if (!isNative()) return;\n\n try {\n await CapacitorUpdater.delete({ id: bundleId });\n } catch (error) {\n console.warn(\"[capuchoo] could not delete bundle\", bundleId, error);\n }\n}\n"],"mappings":"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"}
|
package/dist/vue.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as DownloadProgress, r as getCurrentBundle } from "./ota.service-
|
|
1
|
+
import { a as DownloadProgress, r as getCurrentBundle } from "./ota.service-B99qm0Bm.js";
|
|
2
2
|
import { ResolvedUpdate } from "@capuchoo/core";
|
|
3
3
|
//#region src/vue/useUpdater.d.ts
|
|
4
4
|
interface UpdaterState {
|
|
@@ -10,6 +10,18 @@ interface UpdaterState {
|
|
|
10
10
|
progress: DownloadProgress;
|
|
11
11
|
/** Local path of a downloaded APK, ready to install. */
|
|
12
12
|
cachedPath: string | null;
|
|
13
|
+
/**
|
|
14
|
+
* True once the APK has been handed to the Android package installer.
|
|
15
|
+
*
|
|
16
|
+
* Android shows its own confirmation dialog for a sideloaded APK and there is
|
|
17
|
+
* no way around it - it is an OS security boundary, not a styling choice. The
|
|
18
|
+
* handoff returns as soon as the intent is fired, long before the user has
|
|
19
|
+
* decided, so `installing` flips back to false while that dialog is still on
|
|
20
|
+
* screen and the prompt underneath reverted to offering an install the user
|
|
21
|
+
* was already being asked about. This keeps the app honest about what it is
|
|
22
|
+
* waiting for.
|
|
23
|
+
*/
|
|
24
|
+
handedToInstaller: boolean;
|
|
13
25
|
error: string | null;
|
|
14
26
|
/** Transient status for the current operation. */
|
|
15
27
|
statusMessage: string;
|
|
@@ -53,6 +65,7 @@ declare function useUpdater(): {
|
|
|
53
65
|
readonly platform?: import("@capuchoo/core").Platform | undefined;
|
|
54
66
|
readonly checksum?: string | undefined;
|
|
55
67
|
readonly sessionKey?: string | undefined;
|
|
68
|
+
readonly fileSize?: number | undefined;
|
|
56
69
|
readonly bundleId?: string | undefined;
|
|
57
70
|
} | null;
|
|
58
71
|
readonly progress: {
|
|
@@ -61,6 +74,7 @@ declare function useUpdater(): {
|
|
|
61
74
|
readonly percent: number;
|
|
62
75
|
};
|
|
63
76
|
readonly cachedPath: string | null;
|
|
77
|
+
readonly handedToInstaller: boolean;
|
|
64
78
|
readonly error: string | null;
|
|
65
79
|
readonly statusMessage: string;
|
|
66
80
|
readonly lastCheckMessage: string;
|
|
@@ -79,6 +93,7 @@ declare function useUpdater(): {
|
|
|
79
93
|
readonly platform?: import("@capuchoo/core").Platform | undefined;
|
|
80
94
|
readonly checksum?: string | undefined;
|
|
81
95
|
readonly sessionKey?: string | undefined;
|
|
96
|
+
readonly fileSize?: number | undefined;
|
|
82
97
|
readonly bundleId?: string | undefined;
|
|
83
98
|
} | null;
|
|
84
99
|
readonly progress: {
|
|
@@ -87,6 +102,7 @@ declare function useUpdater(): {
|
|
|
87
102
|
readonly percent: number;
|
|
88
103
|
};
|
|
89
104
|
readonly cachedPath: string | null;
|
|
105
|
+
readonly handedToInstaller: boolean;
|
|
90
106
|
readonly error: string | null;
|
|
91
107
|
readonly statusMessage: string;
|
|
92
108
|
readonly lastCheckMessage: string;
|
|
@@ -105,6 +121,7 @@ declare function useUpdater(): {
|
|
|
105
121
|
platform?: import("@capuchoo/core").Platform | undefined;
|
|
106
122
|
checksum?: string | undefined;
|
|
107
123
|
sessionKey?: string | undefined;
|
|
124
|
+
fileSize?: number | undefined;
|
|
108
125
|
bundleId?: string | undefined;
|
|
109
126
|
} | null>;
|
|
110
127
|
progress: import("vue").ComputedRef<{
|
|
@@ -118,6 +135,8 @@ declare function useUpdater(): {
|
|
|
118
135
|
lastCheckMessage: import("vue").ComputedRef<string>;
|
|
119
136
|
/** True when the user may not postpone the update. */
|
|
120
137
|
isRequired: import("vue").ComputedRef<boolean>;
|
|
138
|
+
/** True while Android's own install dialog is waiting on the user. */
|
|
139
|
+
handedToInstaller: import("vue").ComputedRef<boolean>;
|
|
121
140
|
check: typeof check;
|
|
122
141
|
startDownload: typeof startDownload;
|
|
123
142
|
installNativeUpdate: typeof installNativeUpdate;
|
|
@@ -138,16 +157,38 @@ declare function useUpdater(): {
|
|
|
138
157
|
* machine is the reusable part.
|
|
139
158
|
*/
|
|
140
159
|
declare function useUpdatePrompt(): {
|
|
141
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* Whether the prompt should be on screen at all.
|
|
162
|
+
*
|
|
163
|
+
* True for errors as well as updates, so a failed check is not silent - but
|
|
164
|
+
* only useful to a component that renders `title` and `body` from here. A
|
|
165
|
+
* dialog with its own copy ("New version available") must bind to
|
|
166
|
+
* `updateAvailable` instead, or an unreachable server is announced to users
|
|
167
|
+
* as a release with a blank version number. That happened in a real app.
|
|
168
|
+
*/
|
|
142
169
|
visible: import("vue").ComputedRef<boolean>;
|
|
143
170
|
title: import("vue").ComputedRef<"" | "Update problem" | "Update required" | "Update available">;
|
|
144
171
|
subtitle: import("vue").ComputedRef<string>;
|
|
145
172
|
body: import("vue").ComputedRef<string>;
|
|
173
|
+
/**
|
|
174
|
+
* Where the update is in its lifecycle, for an app that writes its own copy.
|
|
175
|
+
*
|
|
176
|
+
* `primaryLabel` below bakes in English, so a localised app cannot use it
|
|
177
|
+
* and writes a fixed string instead - which is how efficy's dialog ended up
|
|
178
|
+
* reading "Mettre a jour maintenant" through a 45 MB download and then
|
|
179
|
+
* again while waiting for a second tap, with nothing on screen changing.
|
|
180
|
+
* Switch on this and supply your own wording.
|
|
181
|
+
*
|
|
182
|
+
* `downloaded` only happens for a native update: the APK is on disk and the
|
|
183
|
+
* next press hands it to the system installer. An OTA bundle applies itself,
|
|
184
|
+
* so it never rests here.
|
|
185
|
+
*/
|
|
186
|
+
phase: import("vue").ComputedRef<"downloading" | "installing" | "idle" | "downloaded" | "awaiting-install">;
|
|
146
187
|
/**
|
|
147
188
|
* Native updates need a download step and then an install step; OTA
|
|
148
189
|
* updates apply themselves once downloaded.
|
|
149
190
|
*/
|
|
150
|
-
primaryLabel: import("vue").ComputedRef<
|
|
191
|
+
primaryLabel: import("vue").ComputedRef<string>;
|
|
151
192
|
primaryAction: () => Promise<void>;
|
|
152
193
|
/** Busy state - the primary button must be disabled. */
|
|
153
194
|
busy: import("vue").ComputedRef<boolean>;
|
|
@@ -169,6 +210,7 @@ declare function useUpdatePrompt(): {
|
|
|
169
210
|
readonly platform?: import("@capuchoo/core").Platform | undefined;
|
|
170
211
|
readonly checksum?: string | undefined;
|
|
171
212
|
readonly sessionKey?: string | undefined;
|
|
213
|
+
readonly fileSize?: number | undefined;
|
|
172
214
|
readonly bundleId?: string | undefined;
|
|
173
215
|
} | null;
|
|
174
216
|
readonly progress: {
|
|
@@ -177,6 +219,7 @@ declare function useUpdatePrompt(): {
|
|
|
177
219
|
readonly percent: number;
|
|
178
220
|
};
|
|
179
221
|
readonly cachedPath: string | null;
|
|
222
|
+
readonly handedToInstaller: boolean;
|
|
180
223
|
readonly error: string | null;
|
|
181
224
|
readonly statusMessage: string;
|
|
182
225
|
readonly lastCheckMessage: string;
|
|
@@ -195,6 +238,7 @@ declare function useUpdatePrompt(): {
|
|
|
195
238
|
readonly platform?: import("@capuchoo/core").Platform | undefined;
|
|
196
239
|
readonly checksum?: string | undefined;
|
|
197
240
|
readonly sessionKey?: string | undefined;
|
|
241
|
+
readonly fileSize?: number | undefined;
|
|
198
242
|
readonly bundleId?: string | undefined;
|
|
199
243
|
} | null;
|
|
200
244
|
readonly progress: {
|
|
@@ -203,6 +247,7 @@ declare function useUpdatePrompt(): {
|
|
|
203
247
|
readonly percent: number;
|
|
204
248
|
};
|
|
205
249
|
readonly cachedPath: string | null;
|
|
250
|
+
readonly handedToInstaller: boolean;
|
|
206
251
|
readonly error: string | null;
|
|
207
252
|
readonly statusMessage: string;
|
|
208
253
|
readonly lastCheckMessage: string;
|
|
@@ -221,6 +266,7 @@ declare function useUpdatePrompt(): {
|
|
|
221
266
|
platform?: import("@capuchoo/core").Platform | undefined;
|
|
222
267
|
checksum?: string | undefined;
|
|
223
268
|
sessionKey?: string | undefined;
|
|
269
|
+
fileSize?: number | undefined;
|
|
224
270
|
bundleId?: string | undefined;
|
|
225
271
|
} | null>;
|
|
226
272
|
progress: import("vue").ComputedRef<{
|
|
@@ -233,6 +279,7 @@ declare function useUpdatePrompt(): {
|
|
|
233
279
|
statusMessage: import("vue").ComputedRef<string>;
|
|
234
280
|
lastCheckMessage: import("vue").ComputedRef<string>;
|
|
235
281
|
isRequired: import("vue").ComputedRef<boolean>;
|
|
282
|
+
handedToInstaller: import("vue").ComputedRef<boolean>;
|
|
236
283
|
check: (silent?: boolean) => Promise<boolean>;
|
|
237
284
|
startDownload: () => Promise<void>;
|
|
238
285
|
installNativeUpdate: () => Promise<void>;
|
package/dist/vue.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue.d.ts","names":[],"sources":["../src/vue/useUpdater.ts","../src/vue/useUpdatePrompt.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"vue.d.ts","names":[],"sources":["../src/vue/useUpdater.ts","../src/vue/useUpdatePrompt.ts"],"mappings":";;;UAqBiB;EACf;EACA;EACA;EACA;EACA,eAAe;EACf,UAAU;;EAEV;;;;;;;;;;;;EAYA;EACA;;EAEA;;EAEA;;;;;;;;iBAuFa,MAAM,mBAAiB;;iBAuDvB,iBAAiB;;iBAuCjB,uBAAuB;;;;;;;iBAkCvB,QAAQ;iBAiBR,WAAW;;iBAMX,WAAW;iBAYV;;aAlRJ;aACG;aACD;aACK;;;;;;;;;;;;;;;;;;;aAIL;aAYO;aACZ;aAEQ;aAEG;;aAxBR;aACG;aACD;aACK;;;;;;;;;;;;;;;;;;;aAIL;aAYO;aACZ;aAEQ;aAEG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBClCJ"}
|
package/dist/vue.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { E as isNative, _ as checkForUpdate, a as openNativeInstaller, c as pruneApkCache, h as UpdaterConfigError, i as notifyAppReady, k as getUpdaterConfig, m as UpdateCheckBlockedError, o as downloadNativeUpdate, r as getCurrentBundle, s as findCachedApk, t as applyOtaUpdate, v as logUpdateEvent } from "./ota.service-DPjhi4rw.js";
|
|
2
2
|
import { CapacitorUpdater } from "@capgo/capacitor-updater";
|
|
3
3
|
import { computed, readonly, ref } from "vue";
|
|
4
4
|
//#region src/vue/useUpdater.ts
|
|
@@ -24,6 +24,7 @@ const state = ref({
|
|
|
24
24
|
currentUpdate: null,
|
|
25
25
|
progress: { ...NO_PROGRESS },
|
|
26
26
|
cachedPath: null,
|
|
27
|
+
handedToInstaller: false,
|
|
27
28
|
error: null,
|
|
28
29
|
statusMessage: "",
|
|
29
30
|
lastCheckMessage: ""
|
|
@@ -35,6 +36,7 @@ function publish(update) {
|
|
|
35
36
|
state.value.currentUpdate = update;
|
|
36
37
|
state.value.updateAvailable = true;
|
|
37
38
|
state.value.cachedPath = null;
|
|
39
|
+
state.value.handedToInstaller = false;
|
|
38
40
|
state.value.progress = { ...NO_PROGRESS };
|
|
39
41
|
state.value.error = null;
|
|
40
42
|
state.value.lastCheckMessage = `Version ${update.version} is available`;
|
|
@@ -85,6 +87,13 @@ async function check(silent = false) {
|
|
|
85
87
|
const update = await checkForUpdate();
|
|
86
88
|
if (update) {
|
|
87
89
|
publish(update);
|
|
90
|
+
if (update.kind === "native") {
|
|
91
|
+
state.value.cachedPath = await findCachedApk(update);
|
|
92
|
+
if (state.value.cachedPath) {
|
|
93
|
+
state.value.progress = { ...DONE_PROGRESS };
|
|
94
|
+
state.value.statusMessage = "Ready to install.";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
88
97
|
await logUpdateEvent("check", update);
|
|
89
98
|
return true;
|
|
90
99
|
}
|
|
@@ -149,8 +158,11 @@ async function installNativeUpdate() {
|
|
|
149
158
|
state.value.error = null;
|
|
150
159
|
try {
|
|
151
160
|
await openNativeInstaller(path);
|
|
161
|
+
state.value.handedToInstaller = true;
|
|
162
|
+
state.value.statusMessage = "Confirm the installation to finish updating.";
|
|
152
163
|
await logUpdateEvent("install", update);
|
|
153
164
|
} catch (error) {
|
|
165
|
+
state.value.handedToInstaller = false;
|
|
154
166
|
state.value.error = error instanceof Error ? error.message : "Installation failed";
|
|
155
167
|
await logUpdateEvent("error", update, { error: state.value.error });
|
|
156
168
|
} finally {
|
|
@@ -168,7 +180,7 @@ async function init() {
|
|
|
168
180
|
initialised = true;
|
|
169
181
|
await notifyAppReady();
|
|
170
182
|
await attachPluginListeners();
|
|
171
|
-
await
|
|
183
|
+
await pruneApkCache({ installedVersionCode: await getVersionCode() });
|
|
172
184
|
await check(true);
|
|
173
185
|
}
|
|
174
186
|
async function cleanup() {
|
|
@@ -201,6 +213,8 @@ function useUpdater() {
|
|
|
201
213
|
lastCheckMessage: computed(() => state.value.lastCheckMessage),
|
|
202
214
|
/** True when the user may not postpone the update. */
|
|
203
215
|
isRequired: computed(() => state.value.currentUpdate?.required === true),
|
|
216
|
+
/** True while Android's own install dialog is waiting on the user. */
|
|
217
|
+
handedToInstaller: computed(() => state.value.handedToInstaller),
|
|
204
218
|
check,
|
|
205
219
|
startDownload,
|
|
206
220
|
installNativeUpdate,
|
|
@@ -227,7 +241,15 @@ function useUpdatePrompt() {
|
|
|
227
241
|
const isNativeUpdate = computed(() => update.value?.kind === "native");
|
|
228
242
|
return {
|
|
229
243
|
...updater,
|
|
230
|
-
/**
|
|
244
|
+
/**
|
|
245
|
+
* Whether the prompt should be on screen at all.
|
|
246
|
+
*
|
|
247
|
+
* True for errors as well as updates, so a failed check is not silent - but
|
|
248
|
+
* only useful to a component that renders `title` and `body` from here. A
|
|
249
|
+
* dialog with its own copy ("New version available") must bind to
|
|
250
|
+
* `updateAvailable` instead, or an unreachable server is announced to users
|
|
251
|
+
* as a release with a blank version number. That happened in a real app.
|
|
252
|
+
*/
|
|
231
253
|
visible: computed(() => updater.updateAvailable.value || updater.error.value !== null),
|
|
232
254
|
title: computed(() => {
|
|
233
255
|
if (updater.error.value) return "Update problem";
|
|
@@ -241,12 +263,33 @@ function useUpdatePrompt() {
|
|
|
241
263
|
}),
|
|
242
264
|
body: computed(() => updater.error.value ?? update.value?.releaseNotes ?? ""),
|
|
243
265
|
/**
|
|
266
|
+
* Where the update is in its lifecycle, for an app that writes its own copy.
|
|
267
|
+
*
|
|
268
|
+
* `primaryLabel` below bakes in English, so a localised app cannot use it
|
|
269
|
+
* and writes a fixed string instead - which is how efficy's dialog ended up
|
|
270
|
+
* reading "Mettre a jour maintenant" through a 45 MB download and then
|
|
271
|
+
* again while waiting for a second tap, with nothing on screen changing.
|
|
272
|
+
* Switch on this and supply your own wording.
|
|
273
|
+
*
|
|
274
|
+
* `downloaded` only happens for a native update: the APK is on disk and the
|
|
275
|
+
* next press hands it to the system installer. An OTA bundle applies itself,
|
|
276
|
+
* so it never rests here.
|
|
277
|
+
*/
|
|
278
|
+
phase: computed(() => {
|
|
279
|
+
if (updater.isInstalling.value) return "installing";
|
|
280
|
+
if (updater.isDownloading.value) return "downloading";
|
|
281
|
+
if (updater.handedToInstaller.value) return "awaiting-install";
|
|
282
|
+
if (isNativeUpdate.value && updater.cachedPath.value) return "downloaded";
|
|
283
|
+
return "idle";
|
|
284
|
+
}),
|
|
285
|
+
/**
|
|
244
286
|
* Native updates need a download step and then an install step; OTA
|
|
245
287
|
* updates apply themselves once downloaded.
|
|
246
288
|
*/
|
|
247
289
|
primaryLabel: computed(() => {
|
|
248
|
-
if (updater.isDownloading.value) return
|
|
290
|
+
if (updater.isDownloading.value) return `Downloading ${updater.progress.value.percent}%`;
|
|
249
291
|
if (updater.isInstalling.value) return "Installing...";
|
|
292
|
+
if (updater.handedToInstaller.value) return "Waiting for Android...";
|
|
250
293
|
if (isNativeUpdate.value && updater.cachedPath.value) return "Install now";
|
|
251
294
|
if (isNativeUpdate.value) return "Download";
|
|
252
295
|
return "Update now";
|
package/dist/vue.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue.js","names":[],"sources":["../src/vue/useUpdater.ts","../src/vue/useUpdatePrompt.ts"],"sourcesContent":["import type { PluginListenerHandle } from \"@capacitor/core\";\nimport { CapacitorUpdater } from \"@capgo/capacitor-updater\";\nimport type { ResolvedUpdate } from \"@capuchoo/core\";\nimport { computed, readonly, ref } from \"vue\";\nimport {\n UpdateCheckBlockedError,\n UpdaterConfigError,\n checkForUpdate,\n logUpdateEvent,\n} from \"../api.service.js\";\nimport { getUpdaterConfig } from \"../config.js\";\nimport { isNative } from \"../device.js\";\nimport { cleanApkCache, downloadNativeUpdate, type DownloadProgress } from \"../download.service.js\";\nimport { openNativeInstaller } from \"../install.service.js\";\nimport { applyOtaUpdate, getCurrentBundle, notifyAppReady } from \"../ota.service.js\";\n\nexport interface UpdaterState {\n checking: boolean;\n downloading: boolean;\n installing: boolean;\n updateAvailable: boolean;\n currentUpdate: ResolvedUpdate | null;\n progress: DownloadProgress;\n /** Local path of a downloaded APK, ready to install. */\n cachedPath: string | null;\n error: string | null;\n /** Transient status for the current operation. */\n statusMessage: string;\n /** Result of the last check, shown when no update is pending. */\n lastCheckMessage: string;\n}\n\nconst NO_PROGRESS: DownloadProgress = { loaded: 0, total: 0, percent: 0 };\nconst DONE_PROGRESS: DownloadProgress = { loaded: 100, total: 100, percent: 100 };\n\n/**\n * Module-level state: one updater per app, shared by every component that calls\n * `useUpdater()`. Two independent copies would race over the same download.\n */\nconst state = ref<UpdaterState>({\n checking: false,\n downloading: false,\n installing: false,\n updateAvailable: false,\n currentUpdate: null,\n progress: { ...NO_PROGRESS },\n cachedPath: null,\n error: null,\n statusMessage: \"\",\n lastCheckMessage: \"\",\n});\n\nconst listeners: PluginListenerHandle[] = [];\nlet initialised = false;\n\nfunction publish(update: ResolvedUpdate): void {\n // A pending native update outranks an OTA bundle: the bundle may well be the\n // one that needs the new binary. Do not let a plugin event downgrade it.\n if (state.value.currentUpdate?.kind === \"native\" && update.kind === \"ota\") return;\n\n state.value.currentUpdate = update;\n state.value.updateAvailable = true;\n state.value.cachedPath = null;\n state.value.progress = { ...NO_PROGRESS };\n state.value.error = null;\n state.value.lastCheckMessage = `Version ${update.version} is available`;\n}\n\nasync function attachPluginListeners(): Promise<void> {\n listeners.push(\n // Raised when the plugin's own background check finds a bundle. With\n // autoUpdate: \"onlyDownload\" it has already been fetched, so the id is\n // enough to apply it without downloading again.\n await CapacitorUpdater.addListener(\"updateAvailable\", ({ bundle }) => {\n publish({\n kind: \"ota\",\n version: bundle.version,\n bundleId: bundle.id,\n required: false,\n });\n }),\n\n await CapacitorUpdater.addListener(\"download\", ({ percent }) => {\n if (state.value.currentUpdate?.kind !== \"ota\") return;\n state.value.downloading = true;\n state.value.progress = { loaded: percent, total: 100, percent };\n }),\n\n await CapacitorUpdater.addListener(\"downloadComplete\", ({ bundle }) => {\n if (state.value.currentUpdate?.kind !== \"ota\") return;\n state.value.downloading = false;\n state.value.currentUpdate.bundleId = bundle.id;\n state.value.progress = { ...DONE_PROGRESS };\n state.value.statusMessage = \"Ready to install\";\n }),\n\n await CapacitorUpdater.addListener(\"downloadFailed\", () => {\n state.value.downloading = false;\n state.value.error = \"The update could not be downloaded\";\n }),\n\n await CapacitorUpdater.addListener(\"updateFailed\", () => {\n const { appName } = getUpdaterConfig();\n state.value.error = `The update failed, so ${appName} restored the previous version`;\n }),\n );\n}\n\n/**\n * Checks for an update.\n *\n * @param silent suppresses console noise for background checks.\n * @returns whether an update is now pending.\n */\nasync function check(silent = false): Promise<boolean> {\n if (!isNative() || state.value.checking) return false;\n\n state.value.checking = true;\n state.value.error = null;\n state.value.lastCheckMessage = \"\";\n state.value.statusMessage = \"Checking for updates...\";\n\n try {\n const update = await checkForUpdate();\n\n if (update) {\n publish(update);\n await logUpdateEvent(\"check\", update);\n return true;\n }\n\n if (!state.value.updateAvailable) {\n const { appName } = getUpdaterConfig();\n state.value.lastCheckMessage = `${appName} is up to date`;\n }\n return false;\n } catch (error) {\n // Configuration and channel errors are the developer's problem, not the\n // user's, but reporting \"up to date\" would hide them completely.\n if (error instanceof UpdaterConfigError) {\n state.value.error = \"Updates are not configured for this build\";\n console.error(\"[capuchoo]\", error.problems.join(\"; \"));\n } else if (error instanceof UpdateCheckBlockedError) {\n state.value.error = `The update service rejected this build: ${error.message}`;\n console.error(\"[capuchoo]\", error.message, error.response);\n } else {\n state.value.error = \"Could not reach the update service\";\n if (!silent) console.error(\"[capuchoo] update check failed\", error);\n }\n return false;\n } finally {\n state.value.checking = false;\n state.value.statusMessage = \"\";\n }\n}\n\n/** Downloads the pending update. For native updates, install is a second step. */\nasync function startDownload(): Promise<void> {\n const update = state.value.currentUpdate;\n if (!update || state.value.downloading || state.value.installing) return;\n\n state.value.error = null;\n\n // Already on disk - go straight to the installer.\n if (update.kind === \"native\" && state.value.cachedPath) {\n await installNativeUpdate();\n return;\n }\n\n state.value.downloading = true;\n state.value.statusMessage =\n update.kind === \"native\" ? \"Downloading the new version...\" : \"Downloading update...\";\n\n try {\n if (update.kind === \"native\") {\n state.value.cachedPath = await downloadNativeUpdate(update, (progress) => {\n state.value.progress = progress;\n });\n state.value.progress = { ...DONE_PROGRESS };\n state.value.statusMessage = \"Download complete. Tap Install to continue.\";\n await logUpdateEvent(\"download_complete\", update);\n return;\n }\n\n // Reloads the WebView on success, so nothing below runs.\n await applyOtaUpdate(update);\n } catch (error) {\n state.value.error = error instanceof Error ? error.message : \"The update failed\";\n await logUpdateEvent(\"error\", update, { error: state.value.error });\n } finally {\n state.value.downloading = false;\n if (!state.value.cachedPath) state.value.statusMessage = \"\";\n }\n}\n\n/** Hands the downloaded APK to the Android installer. */\nasync function installNativeUpdate(): Promise<void> {\n const update = state.value.currentUpdate;\n const path = state.value.cachedPath;\n if (!update || update.kind !== \"native\" || !path) return;\n\n state.value.installing = true;\n state.value.error = null;\n\n try {\n await openNativeInstaller(path);\n await logUpdateEvent(\"install\", update);\n } catch (error) {\n state.value.error = error instanceof Error ? error.message : \"Installation failed\";\n await logUpdateEvent(\"error\", update, { error: state.value.error });\n } finally {\n state.value.installing = false;\n }\n}\n\n/**\n * Wires up the updater. Call once, from the app's Capacitor bootstrap.\n *\n * Also calls `notifyAppReady`, without which the OTA plugin rolls the bundle\n * back after `appReadyTimeout`.\n */\nasync function init(): Promise<void> {\n if (!isNative() || initialised) return;\n initialised = true;\n\n await notifyAppReady();\n await attachPluginListeners();\n await cleanApkCache();\n await check(true);\n}\n\nasync function cleanup(): Promise<void> {\n await Promise.all(listeners.splice(0).map((listener) => listener.remove()));\n initialised = false;\n}\n\n/** Dismisses a pending update. Refuses for required updates and mid-download. */\nasync function dismiss(): Promise<void> {\n const update = state.value.currentUpdate;\n if (!update || update.required || state.value.downloading) return;\n\n await logUpdateEvent(\"cancel\", update);\n state.value.updateAvailable = false;\n state.value.currentUpdate = null;\n state.value.cachedPath = null;\n state.value.statusMessage = \"\";\n state.value.progress = { ...NO_PROGRESS };\n}\n\nexport function useUpdater() {\n return {\n state: readonly(state),\n isChecking: computed(() => state.value.checking),\n isDownloading: computed(() => state.value.downloading),\n isInstalling: computed(() => state.value.installing),\n updateAvailable: computed(() => state.value.updateAvailable),\n currentUpdate: computed(() => state.value.currentUpdate),\n progress: computed(() => state.value.progress),\n cachedPath: computed(() => state.value.cachedPath),\n error: computed(() => state.value.error),\n statusMessage: computed(() => state.value.statusMessage),\n lastCheckMessage: computed(() => state.value.lastCheckMessage),\n /** True when the user may not postpone the update. */\n isRequired: computed(() => state.value.currentUpdate?.required === true),\n\n check,\n startDownload,\n installNativeUpdate,\n dismiss,\n init,\n cleanup,\n getCurrentBundle,\n };\n}\n\n/** @internal test hook - resets module state between cases. */\nexport function __resetUpdaterState(): void {\n listeners.length = 0;\n initialised = false;\n state.value = {\n checking: false,\n downloading: false,\n installing: false,\n updateAvailable: false,\n currentUpdate: null,\n progress: { ...NO_PROGRESS },\n cachedPath: null,\n error: null,\n statusMessage: \"\",\n lastCheckMessage: \"\",\n };\n}\n","import { computed } from \"vue\";\nimport { useUpdater } from \"./useUpdater.js\";\n\n/**\n * Derives everything an update prompt needs to render, so each app only writes\n * the markup for its own design system.\n *\n * The styled component itself stays in the app on purpose: this package would\n * otherwise have to ship a Framework7 dialog to one app and something else to\n * the next, and a component library is not what makes updates work. The state\n * machine is the reusable part.\n */\nexport function useUpdatePrompt() {\n const updater = useUpdater();\n\n const update = updater.currentUpdate;\n const isNativeUpdate = computed(() => update.value?.kind === \"native\");\n\n return {\n ...updater,\n\n /** Whether the prompt should be on screen at all. */\n visible: computed(() => updater.updateAvailable.value || updater.error.value !== null),\n\n title: computed(() => {\n if (updater.error.value) return \"Update problem\";\n if (!update.value) return \"\";\n return update.value.required ? \"Update required\" : \"Update available\";\n }),\n\n subtitle: computed(() => {\n if (!update.value) return \"\";\n const kind = isNativeUpdate.value ? \"app version\" : \"update\";\n return `Version ${update.value.version} - ${kind}`;\n }),\n\n body: computed(() => updater.error.value ?? update.value?.releaseNotes ?? \"\"),\n\n /**\n * Native updates need a download step and then an install step; OTA\n * updates apply themselves once downloaded.\n */\n primaryLabel: computed(() => {\n if (updater.isDownloading.value) return \"Downloading...\";\n if (updater.isInstalling.value) return \"Installing...\";\n if (isNativeUpdate.value && updater.cachedPath.value) return \"Install now\";\n if (isNativeUpdate.value) return \"Download\";\n return \"Update now\";\n }),\n\n primaryAction: () =>\n isNativeUpdate.value && updater.cachedPath.value\n ? updater.installNativeUpdate()\n : updater.startDownload(),\n\n /** Busy state - the primary button must be disabled. */\n busy: computed(() => updater.isDownloading.value || updater.isInstalling.value),\n\n /** A required update cannot be postponed, and neither can one mid-flight. */\n dismissible: computed(\n () =>\n !updater.isRequired.value && !updater.isDownloading.value && !updater.isInstalling.value,\n ),\n\n showProgress: computed(() => updater.isDownloading.value && updater.progress.value.percent > 0),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"vue.js","names":[],"sources":["../src/vue/useUpdater.ts","../src/vue/useUpdatePrompt.ts"],"sourcesContent":["import type { PluginListenerHandle } from \"@capacitor/core\";\nimport { CapacitorUpdater } from \"@capgo/capacitor-updater\";\nimport type { ResolvedUpdate } from \"@capuchoo/core\";\nimport { computed, readonly, ref } from \"vue\";\nimport {\n UpdateCheckBlockedError,\n UpdaterConfigError,\n checkForUpdate,\n logUpdateEvent,\n} from \"../api.service.js\";\nimport { getUpdaterConfig } from \"../config.js\";\nimport { isNative } from \"../device.js\";\nimport {\n downloadNativeUpdate,\n findCachedApk,\n pruneApkCache,\n type DownloadProgress,\n} from \"../download.service.js\";\nimport { openNativeInstaller } from \"../install.service.js\";\nimport { applyOtaUpdate, getCurrentBundle, notifyAppReady } from \"../ota.service.js\";\n\nexport interface UpdaterState {\n checking: boolean;\n downloading: boolean;\n installing: boolean;\n updateAvailable: boolean;\n currentUpdate: ResolvedUpdate | null;\n progress: DownloadProgress;\n /** Local path of a downloaded APK, ready to install. */\n cachedPath: string | null;\n /**\n * True once the APK has been handed to the Android package installer.\n *\n * Android shows its own confirmation dialog for a sideloaded APK and there is\n * no way around it - it is an OS security boundary, not a styling choice. The\n * handoff returns as soon as the intent is fired, long before the user has\n * decided, so `installing` flips back to false while that dialog is still on\n * screen and the prompt underneath reverted to offering an install the user\n * was already being asked about. This keeps the app honest about what it is\n * waiting for.\n */\n handedToInstaller: boolean;\n error: string | null;\n /** Transient status for the current operation. */\n statusMessage: string;\n /** Result of the last check, shown when no update is pending. */\n lastCheckMessage: string;\n}\n\nconst NO_PROGRESS: DownloadProgress = { loaded: 0, total: 0, percent: 0 };\nconst DONE_PROGRESS: DownloadProgress = { loaded: 100, total: 100, percent: 100 };\n\n/**\n * Module-level state: one updater per app, shared by every component that calls\n * `useUpdater()`. Two independent copies would race over the same download.\n */\nconst state = ref<UpdaterState>({\n checking: false,\n downloading: false,\n installing: false,\n updateAvailable: false,\n currentUpdate: null,\n progress: { ...NO_PROGRESS },\n cachedPath: null,\n handedToInstaller: false,\n error: null,\n statusMessage: \"\",\n lastCheckMessage: \"\",\n});\n\nconst listeners: PluginListenerHandle[] = [];\nlet initialised = false;\n\nfunction publish(update: ResolvedUpdate): void {\n // A pending native update outranks an OTA bundle: the bundle may well be the\n // one that needs the new binary. Do not let a plugin event downgrade it.\n if (state.value.currentUpdate?.kind === \"native\" && update.kind === \"ota\") return;\n\n state.value.currentUpdate = update;\n state.value.updateAvailable = true;\n state.value.cachedPath = null;\n state.value.handedToInstaller = false;\n state.value.progress = { ...NO_PROGRESS };\n state.value.error = null;\n state.value.lastCheckMessage = `Version ${update.version} is available`;\n}\n\nasync function attachPluginListeners(): Promise<void> {\n listeners.push(\n // Raised when the plugin's own background check finds a bundle. With\n // autoUpdate: \"onlyDownload\" it has already been fetched, so the id is\n // enough to apply it without downloading again.\n await CapacitorUpdater.addListener(\"updateAvailable\", ({ bundle }) => {\n publish({\n kind: \"ota\",\n version: bundle.version,\n bundleId: bundle.id,\n required: false,\n });\n }),\n\n await CapacitorUpdater.addListener(\"download\", ({ percent }) => {\n if (state.value.currentUpdate?.kind !== \"ota\") return;\n state.value.downloading = true;\n state.value.progress = { loaded: percent, total: 100, percent };\n }),\n\n await CapacitorUpdater.addListener(\"downloadComplete\", ({ bundle }) => {\n if (state.value.currentUpdate?.kind !== \"ota\") return;\n state.value.downloading = false;\n state.value.currentUpdate.bundleId = bundle.id;\n state.value.progress = { ...DONE_PROGRESS };\n state.value.statusMessage = \"Ready to install\";\n }),\n\n await CapacitorUpdater.addListener(\"downloadFailed\", () => {\n state.value.downloading = false;\n state.value.error = \"The update could not be downloaded\";\n }),\n\n await CapacitorUpdater.addListener(\"updateFailed\", () => {\n const { appName } = getUpdaterConfig();\n state.value.error = `The update failed, so ${appName} restored the previous version`;\n }),\n );\n}\n\n/**\n * Checks for an update.\n *\n * @param silent suppresses console noise for background checks.\n * @returns whether an update is now pending.\n */\nasync function check(silent = false): Promise<boolean> {\n if (!isNative() || state.value.checking) return false;\n\n state.value.checking = true;\n state.value.error = null;\n state.value.lastCheckMessage = \"\";\n state.value.statusMessage = \"Checking for updates...\";\n\n try {\n const update = await checkForUpdate();\n\n if (update) {\n publish(update);\n\n // A previous run may already have paid for this binary. Asking the\n // filesystem is what survives a restart; `cachedPath` alone does not, so\n // relaunching used to re-download a file that was already on disk.\n if (update.kind === \"native\") {\n state.value.cachedPath = await findCachedApk(update);\n if (state.value.cachedPath) {\n state.value.progress = { ...DONE_PROGRESS };\n state.value.statusMessage = \"Ready to install.\";\n }\n }\n\n await logUpdateEvent(\"check\", update);\n return true;\n }\n\n if (!state.value.updateAvailable) {\n const { appName } = getUpdaterConfig();\n state.value.lastCheckMessage = `${appName} is up to date`;\n }\n return false;\n } catch (error) {\n // Configuration and channel errors are the developer's problem, not the\n // user's, but reporting \"up to date\" would hide them completely.\n if (error instanceof UpdaterConfigError) {\n state.value.error = \"Updates are not configured for this build\";\n console.error(\"[capuchoo]\", error.problems.join(\"; \"));\n } else if (error instanceof UpdateCheckBlockedError) {\n state.value.error = `The update service rejected this build: ${error.message}`;\n console.error(\"[capuchoo]\", error.message, error.response);\n } else {\n state.value.error = \"Could not reach the update service\";\n if (!silent) console.error(\"[capuchoo] update check failed\", error);\n }\n return false;\n } finally {\n state.value.checking = false;\n state.value.statusMessage = \"\";\n }\n}\n\n/** Downloads the pending update. For native updates, install is a second step. */\nasync function startDownload(): Promise<void> {\n const update = state.value.currentUpdate;\n if (!update || state.value.downloading || state.value.installing) return;\n\n state.value.error = null;\n\n // Already on disk - go straight to the installer.\n if (update.kind === \"native\" && state.value.cachedPath) {\n await installNativeUpdate();\n return;\n }\n\n state.value.downloading = true;\n state.value.statusMessage =\n update.kind === \"native\" ? \"Downloading the new version...\" : \"Downloading update...\";\n\n try {\n if (update.kind === \"native\") {\n state.value.cachedPath = await downloadNativeUpdate(update, (progress) => {\n state.value.progress = progress;\n });\n state.value.progress = { ...DONE_PROGRESS };\n state.value.statusMessage = \"Download complete. Tap Install to continue.\";\n await logUpdateEvent(\"download_complete\", update);\n return;\n }\n\n // Reloads the WebView on success, so nothing below runs.\n await applyOtaUpdate(update);\n } catch (error) {\n state.value.error = error instanceof Error ? error.message : \"The update failed\";\n await logUpdateEvent(\"error\", update, { error: state.value.error });\n } finally {\n state.value.downloading = false;\n if (!state.value.cachedPath) state.value.statusMessage = \"\";\n }\n}\n\n/** Hands the downloaded APK to the Android installer. */\nasync function installNativeUpdate(): Promise<void> {\n const update = state.value.currentUpdate;\n const path = state.value.cachedPath;\n if (!update || update.kind !== \"native\" || !path) return;\n\n state.value.installing = true;\n state.value.error = null;\n\n try {\n await openNativeInstaller(path);\n\n // Fired, not finished. Android now shows its own confirmation dialog and\n // there is no callback for what the user does with it, so the prompt has to\n // say what it is waiting for rather than silently offering the install\n // again underneath.\n state.value.handedToInstaller = true;\n state.value.statusMessage = \"Confirm the installation to finish updating.\";\n\n await logUpdateEvent(\"install\", update);\n } catch (error) {\n state.value.handedToInstaller = false;\n state.value.error = error instanceof Error ? error.message : \"Installation failed\";\n await logUpdateEvent(\"error\", update, { error: state.value.error });\n } finally {\n state.value.installing = false;\n }\n}\n\n/**\n * Wires up the updater. Call once, from the app's Capacitor bootstrap.\n *\n * Also calls `notifyAppReady`, without which the OTA plugin rolls the bundle\n * back after `appReadyTimeout`.\n */\nasync function init(): Promise<void> {\n if (!isNative() || initialised) return;\n initialised = true;\n\n await notifyAppReady();\n await attachPluginListeners();\n\n // Delete APKs the device has outgrown. There is no callback from the Android\n // installer, so this is where an installed binary's 47 MB finally goes: on\n // the next launch the installed build number has passed it, which says the\n // install landed. Anything newer is left alone - it is an update already\n // downloaded and waiting, and deleting it would mean paying for it twice.\n await pruneApkCache({ installedVersionCode: await getVersionCode() });\n\n await check(true);\n}\n\nasync function cleanup(): Promise<void> {\n await Promise.all(listeners.splice(0).map((listener) => listener.remove()));\n initialised = false;\n}\n\n/** Dismisses a pending update. Refuses for required updates and mid-download. */\nasync function dismiss(): Promise<void> {\n const update = state.value.currentUpdate;\n if (!update || update.required || state.value.downloading) return;\n\n await logUpdateEvent(\"cancel\", update);\n state.value.updateAvailable = false;\n state.value.currentUpdate = null;\n state.value.cachedPath = null;\n state.value.statusMessage = \"\";\n state.value.progress = { ...NO_PROGRESS };\n}\n\nexport function useUpdater() {\n return {\n state: readonly(state),\n isChecking: computed(() => state.value.checking),\n isDownloading: computed(() => state.value.downloading),\n isInstalling: computed(() => state.value.installing),\n updateAvailable: computed(() => state.value.updateAvailable),\n currentUpdate: computed(() => state.value.currentUpdate),\n progress: computed(() => state.value.progress),\n cachedPath: computed(() => state.value.cachedPath),\n error: computed(() => state.value.error),\n statusMessage: computed(() => state.value.statusMessage),\n lastCheckMessage: computed(() => state.value.lastCheckMessage),\n /** True when the user may not postpone the update. */\n isRequired: computed(() => state.value.currentUpdate?.required === true),\n /** True while Android's own install dialog is waiting on the user. */\n handedToInstaller: computed(() => state.value.handedToInstaller),\n\n check,\n startDownload,\n installNativeUpdate,\n dismiss,\n init,\n cleanup,\n getCurrentBundle,\n };\n}\n\n/** @internal test hook - resets module state between cases. */\nexport function __resetUpdaterState(): void {\n listeners.length = 0;\n initialised = false;\n state.value = {\n checking: false,\n downloading: false,\n installing: false,\n updateAvailable: false,\n currentUpdate: null,\n progress: { ...NO_PROGRESS },\n cachedPath: null,\n handedToInstaller: false,\n error: null,\n statusMessage: \"\",\n lastCheckMessage: \"\",\n };\n}\n","import { computed } from \"vue\";\nimport { useUpdater } from \"./useUpdater.js\";\n\n/**\n * Derives everything an update prompt needs to render, so each app only writes\n * the markup for its own design system.\n *\n * The styled component itself stays in the app on purpose: this package would\n * otherwise have to ship a Framework7 dialog to one app and something else to\n * the next, and a component library is not what makes updates work. The state\n * machine is the reusable part.\n */\nexport function useUpdatePrompt() {\n const updater = useUpdater();\n\n const update = updater.currentUpdate;\n const isNativeUpdate = computed(() => update.value?.kind === \"native\");\n\n return {\n ...updater,\n\n /**\n * Whether the prompt should be on screen at all.\n *\n * True for errors as well as updates, so a failed check is not silent - but\n * only useful to a component that renders `title` and `body` from here. A\n * dialog with its own copy (\"New version available\") must bind to\n * `updateAvailable` instead, or an unreachable server is announced to users\n * as a release with a blank version number. That happened in a real app.\n */\n visible: computed(() => updater.updateAvailable.value || updater.error.value !== null),\n\n title: computed(() => {\n if (updater.error.value) return \"Update problem\";\n if (!update.value) return \"\";\n return update.value.required ? \"Update required\" : \"Update available\";\n }),\n\n subtitle: computed(() => {\n if (!update.value) return \"\";\n const kind = isNativeUpdate.value ? \"app version\" : \"update\";\n return `Version ${update.value.version} - ${kind}`;\n }),\n\n body: computed(() => updater.error.value ?? update.value?.releaseNotes ?? \"\"),\n\n /**\n * Where the update is in its lifecycle, for an app that writes its own copy.\n *\n * `primaryLabel` below bakes in English, so a localised app cannot use it\n * and writes a fixed string instead - which is how efficy's dialog ended up\n * reading \"Mettre a jour maintenant\" through a 45 MB download and then\n * again while waiting for a second tap, with nothing on screen changing.\n * Switch on this and supply your own wording.\n *\n * `downloaded` only happens for a native update: the APK is on disk and the\n * next press hands it to the system installer. An OTA bundle applies itself,\n * so it never rests here.\n */\n phase: computed<\"idle\" | \"downloading\" | \"downloaded\" | \"installing\" | \"awaiting-install\">(\n () => {\n if (updater.isInstalling.value) return \"installing\";\n if (updater.isDownloading.value) return \"downloading\";\n // Android's own dialog is up. Nothing here can dismiss it, observe it,\n // or replace it - it is an OS security boundary for a sideloaded APK -\n // so the only correct thing is to say what is being waited on.\n if (updater.handedToInstaller.value) return \"awaiting-install\";\n if (isNativeUpdate.value && updater.cachedPath.value) return \"downloaded\";\n return \"idle\";\n },\n ),\n\n /**\n * Native updates need a download step and then an install step; OTA\n * updates apply themselves once downloaded.\n */\n primaryLabel: computed(() => {\n if (updater.isDownloading.value) return `Downloading ${updater.progress.value.percent}%`;\n if (updater.isInstalling.value) return \"Installing...\";\n if (updater.handedToInstaller.value) return \"Waiting for Android...\";\n if (isNativeUpdate.value && updater.cachedPath.value) return \"Install now\";\n if (isNativeUpdate.value) return \"Download\";\n return \"Update now\";\n }),\n\n primaryAction: () =>\n isNativeUpdate.value && updater.cachedPath.value\n ? updater.installNativeUpdate()\n : updater.startDownload(),\n\n /** Busy state - the primary button must be disabled. */\n busy: computed(() => updater.isDownloading.value || updater.isInstalling.value),\n\n /** A required update cannot be postponed, and neither can one mid-flight. */\n dismissible: computed(\n () =>\n !updater.isRequired.value && !updater.isDownloading.value && !updater.isInstalling.value,\n ),\n\n showProgress: computed(() => updater.isDownloading.value && updater.progress.value.percent > 0),\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capuchoo/updater",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "App-side runtime for Capucho OTA and native updates in Capacitor apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"capacitor",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@capuchoo/core": "^0.
|
|
45
|
+
"@capuchoo/core": "^0.3.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@capacitor/app": "^8.0.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ota.service-B3LrcCPr.d.ts","names":[],"sources":["../src/download.service.ts","../src/ota.service.ts"],"mappings":";;UAKiB;EACf;EACA;EACA;;;;;;;;;iBAqBoB,qBACpB,QAAQ,gBACR,aAAa,UAAU,4BACtB;;iBAsDmB,iBAAiB;;;;;;;;;;;;;;;;;;iBClEjB,kBAAkB;;iBAWlB,oBAAgB,2CAAA;;;;;;;iBAiBhB,eAAe,QAAQ,iBAAiB;;iBA4BxC,cAAc,mBAAmB"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ota.service-SyGTRpY2.js","names":[],"sources":["../src/config.ts","../src/device.ts","../src/api.service.ts","../src/optional-plugins.ts","../src/download.service.ts","../src/install.service.ts","../src/ota.service.ts"],"sourcesContent":["/**\n * Runtime configuration for the updater.\n *\n * Values come from the build's `VITE_*` variables, which the CLI injects from\n * the flavour's env file. `configureUpdater` lets an app override any of them\n * at startup - useful for tests and for apps that resolve their endpoint from\n * a login response rather than at build time.\n */\nexport interface UpdaterConfig {\n /** Base URL of the Capuchoo backend, with no trailing slash. */\n apiUrl: string;\n /** Bundle identifier of this build. Must match what the CLI published. */\n appId: string;\n /** Human-readable name, used in prompts and in the APK cache file name. */\n appName: string;\n /** Channel to consult. Bound to an environment server-side. */\n channel: string;\n /** Free-form: an app may use flavours beyond dev/staging/prod. */\n environment: string;\n /** Milliseconds before an update check is abandoned. */\n timeoutMs: number;\n}\n\nfunction env(key: string): string | undefined {\n // `import.meta.env` is replaced at build time by Vite. Guard the access so\n // the module can also be imported from Node (tests, SSR) without throwing.\n const source = (import.meta as ImportMeta & { env?: Record<string, string | undefined> }).env;\n return source?.[key];\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\nlet overrides: Partial<UpdaterConfig> = {};\n\n/**\n * Overrides configuration resolved from the build. Call before `init`.\n * Passing `{}` clears previous overrides.\n */\nexport function configureUpdater(next: Partial<UpdaterConfig>): void {\n overrides = { ...overrides, ...next };\n}\n\n/** @internal exposed for tests. */\nexport function resetUpdaterConfig(): void {\n overrides = {};\n}\n\nexport function getUpdaterConfig(): UpdaterConfig {\n const apiUrl = overrides.apiUrl ?? env(\"VITE_UPDATE_API_URL\") ?? \"\";\n\n return {\n apiUrl: stripTrailingSlash(apiUrl),\n appId: overrides.appId ?? env(\"VITE_APP_ID\") ?? \"\",\n appName: overrides.appName ?? env(\"VITE_APP_NAME\") ?? \"the app\",\n channel: overrides.channel ?? env(\"VITE_UPDATE_CHANNEL\") ?? \"prod\",\n environment:\n overrides.environment ?? env(\"VITE_ENVIRONMENT\") ?? (env(\"PROD\") === \"true\" ? \"prod\" : \"dev\"),\n timeoutMs: overrides.timeoutMs ?? 30_000,\n };\n}\n\n/**\n * Reasons the updater cannot run, as user-facing strings.\n *\n * The previous implementation defaulted `apiUrl` to a hard-coded Render URL and\n * `appId` to a hard-coded bundle id. A build with a missing variable therefore\n * silently pointed at somebody else's backend, or asked for the wrong app, and\n * reported \"you are up to date\". Failing loudly is the whole point of this\n * function.\n */\nexport function describeConfigProblems(config: UpdaterConfig): string[] {\n const problems: string[] = [];\n if (!config.apiUrl) {\n problems.push(\"VITE_UPDATE_API_URL is not set, so updates cannot be checked\");\n }\n if (!config.appId) {\n problems.push(\"VITE_APP_ID is not set, so the server cannot identify this build\");\n }\n return problems;\n}\n","import { App } from \"@capacitor/app\";\nimport { Capacitor } from \"@capacitor/core\";\nimport { CapacitorUpdater } from \"@capgo/capacitor-updater\";\n\n/**\n * Facts about the running build that the server needs in order to decide\n * whether an update applies.\n */\n\n/**\n * Native build number of the installed binary.\n *\n * Returns 0 off-device. The old implementation returned 999999 on web, which\n * meant a browser session claimed to be newer than every published release and\n * so never saw an update - masking the very bug you would be debugging.\n */\nexport async function getVersionCode(): Promise<number> {\n if (!Capacitor.isNativePlatform()) return 0;\n\n try {\n const info = await App.getInfo();\n return Number.parseInt(info.build, 10) || 0;\n } catch {\n return 0;\n }\n}\n\n/**\n * Semantic version of the web bundle currently applied.\n *\n * `\"builtin\"` means no OTA bundle has been applied yet and the app is running\n * the assets compiled into the binary. The server treats it as 0.0.0, so it\n * must be reported honestly rather than sent as a constant.\n */\nexport async function getBundleVersion(): Promise<string> {\n if (!Capacitor.isNativePlatform()) return \"builtin\";\n\n try {\n const current = await CapacitorUpdater.current();\n return current.bundle.version || \"builtin\";\n } catch {\n return \"builtin\";\n }\n}\n\n/**\n * Stable per-install identifier, supplied by the OTA plugin.\n *\n * The plugin persists this natively. Reading `localStorage.device_id` instead -\n * as the app template did - returns null on a fresh install and is wiped\n * whenever the WebView data is cleared, so channel overrides and per-device\n * stats silently stopped working.\n */\nexport async function getDeviceId(): Promise<string> {\n if (!Capacitor.isNativePlatform()) return \"web\";\n\n try {\n const { deviceId } = await CapacitorUpdater.getDeviceId();\n return deviceId || \"unknown\";\n } catch {\n return \"unknown\";\n }\n}\n\nexport function getPlatform(): \"android\" | \"ios\" | \"web\" {\n return Capacitor.getPlatform() as \"android\" | \"ios\" | \"web\";\n}\n\nexport function isNative(): boolean {\n return Capacitor.isNativePlatform();\n}\n\n/**\n * Version of the OTA plugin the app is running.\n *\n * Useful when a device misbehaves: plugin version explains more failures than\n * app version does.\n */\nexport async function getPluginVersion(): Promise<string | undefined> {\n if (!Capacitor.isNativePlatform()) return undefined;\n\n try {\n const { version } = await CapacitorUpdater.getPluginVersion();\n return version || undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Bundle version compiled into the binary.\n *\n * Distinct from `getBundleVersion()`, which reports the OTA bundle currently\n * applied - that one says `\"builtin\"` when none has been, and this says which\n * builtin that is.\n */\nexport async function getBuiltinVersion(): Promise<string | undefined> {\n if (!Capacitor.isNativePlatform()) return undefined;\n\n try {\n const { version } = await CapacitorUpdater.getBuiltinVersion();\n return version || undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * OS version and emulator flag, from `@capacitor/device`.\n *\n * That package is an *optional* peer: it is a separate install, and an app that\n * does not have it should still be able to check for updates. So it is imported\n * dynamically and every failure - not installed, not registered, throwing on an\n * odd platform - resolves to `undefined`, which the request builder omits.\n */\nexport async function getOsFacts(): Promise<{ versionOs?: string; isEmulator?: boolean }> {\n if (!Capacitor.isNativePlatform()) return {};\n\n try {\n const { Device } = await import(\"@capacitor/device\");\n const info = await Device.getInfo();\n return {\n ...(info.osVersion ? { versionOs: info.osVersion } : {}),\n ...(typeof info.isVirtual === \"boolean\" ? { isEmulator: info.isVirtual } : {}),\n };\n } catch {\n return {};\n }\n}\n","import {\n isBlockingResponse,\n resolveUpdate,\n type ResolvedUpdate,\n type UpdateCheckRequest,\n type UpdateCheckResponse,\n type UpdateEvent,\n type UpdateEventPayload,\n} from \"@capuchoo/core\";\nimport { getUpdaterConfig, describeConfigProblems } from \"./config.js\";\nimport {\n getBuiltinVersion,\n getBundleVersion,\n getDeviceId,\n getOsFacts,\n getPlatform,\n getPluginVersion,\n getVersionCode,\n isNative,\n} from \"./device.js\";\n\n/** Raised when the updater is misconfigured, rather than reporting \"up to date\". */\nexport class UpdaterConfigError extends Error {\n readonly problems: string[];\n\n constructor(problems: string[]) {\n super(problems.join(\"; \"));\n this.name = \"UpdaterConfigError\";\n this.problems = problems;\n }\n}\n\n/** Raised when the server says the request itself cannot be served. */\nexport class UpdateCheckBlockedError extends Error {\n readonly response: UpdateCheckResponse;\n\n constructor(response: UpdateCheckResponse) {\n super(response.message ?? \"The update service rejected this request\");\n this.name = \"UpdateCheckBlockedError\";\n this.response = response;\n }\n}\n\nasync function postJson<T>(url: string, body: unknown, timeoutMs: number): Promise<T> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n throw new Error(`${url} responded ${response.status} ${response.statusText}`);\n }\n\n return (await response.json()) as T;\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Asks the server what this device should be running.\n *\n * One request, one endpoint. The app template used to call\n * `GET /api/native-updates/check` for native updates *and*\n * `POST /api/update` for OTA, which meant two sources of truth: the native\n * endpoint ignores the channel's assigned native version and the\n * `min_update_version` gate, so a device could be told to install an OTA\n * bundle its binary was too old to run.\n */\nexport interface DeviceFacts {\n appId: string;\n platform: ReturnType<typeof getPlatform>;\n channel?: string | undefined;\n isProd: boolean;\n versionCode: number;\n versionName: string;\n deviceId: string;\n pluginVersion?: string | undefined;\n versionBuiltin?: string | undefined;\n versionOs?: string | undefined;\n isEmulator?: boolean | undefined;\n customId?: string | undefined;\n}\n\n/**\n * Assemble the check payload.\n *\n * Pure, and separate from the plugin calls that gather the facts, so the shape\n * of what goes on the wire can be tested without a device. Fields the app could\n * not determine are *omitted* rather than sent empty: the server writes only the\n * keys it receives, so a placeholder would overwrite a better value that an\n * earlier, better-informed check had already stored.\n */\nexport function buildCheckRequest(facts: DeviceFacts): UpdateCheckRequest {\n const request: UpdateCheckRequest = {\n appId: facts.appId,\n platform: facts.platform,\n versionCode: String(facts.versionCode),\n // Historical alias. The server accepts either and prefers versionCode.\n versionBuild: String(facts.versionCode),\n version_name: facts.versionName,\n deviceId: facts.deviceId,\n isProd: facts.isProd,\n };\n\n if (facts.channel) {\n request.channel = facts.channel;\n request.defaultChannel = facts.channel;\n }\n if (facts.pluginVersion) request.pluginVersion = facts.pluginVersion;\n if (facts.versionBuiltin) request.versionBuiltin = facts.versionBuiltin;\n if (facts.versionOs) request.versionOs = facts.versionOs;\n if (typeof facts.isEmulator === \"boolean\") request.isEmulator = facts.isEmulator;\n if (facts.customId) request.customId = facts.customId;\n\n return request;\n}\n\nexport async function checkForUpdate(): Promise<ResolvedUpdate | null> {\n if (!isNative()) return null;\n\n const config = getUpdaterConfig();\n const problems = describeConfigProblems(config);\n if (problems.length > 0) throw new UpdaterConfigError(problems);\n\n const [versionCode, versionName, deviceId, pluginVersion, versionBuiltin, osFacts] =\n await Promise.all([\n getVersionCode(),\n getBundleVersion(),\n getDeviceId(),\n getPluginVersion(),\n getBuiltinVersion(),\n getOsFacts(),\n ]);\n\n const request = buildCheckRequest({\n appId: config.appId,\n platform: getPlatform(),\n channel: config.channel,\n isProd: config.environment === \"prod\",\n versionCode,\n versionName,\n deviceId,\n pluginVersion,\n versionBuiltin,\n ...osFacts,\n });\n\n const response = await postJson<UpdateCheckResponse>(\n `${config.apiUrl}/api/update`,\n request,\n config.timeoutMs,\n );\n\n // \"Channel not found\" and \"Environment mismatch\" are deployment mistakes.\n // Treating them as \"no update\" is how a broken channel goes unnoticed for\n // weeks.\n if (isBlockingResponse(response)) throw new UpdateCheckBlockedError(response);\n\n return resolveUpdate(response);\n}\n\n/**\n * Records a native update lifecycle event.\n *\n * Best effort: analytics must never break an update. OTA events are reported\n * by the plugin itself through its `statsUrl`, so only native ones are sent\n * from here.\n */\nexport async function logUpdateEvent(\n event: UpdateEvent,\n update: ResolvedUpdate,\n details?: { error?: string },\n): Promise<void> {\n if (update.kind !== \"native\") return;\n\n const config = getUpdaterConfig();\n if (!config.apiUrl) return;\n\n const payload: UpdateEventPayload = {\n event,\n platform: getPlatform(),\n device_id: await getDeviceId(),\n current_version_code: await getVersionCode(),\n new_version: update.version,\n new_version_code: update.versionCode,\n channel: config.channel,\n environment: String(config.environment),\n ...details,\n };\n\n try {\n await postJson(`${config.apiUrl}/api/native-updates/log`, payload, config.timeoutMs);\n } catch (error) {\n console.warn(\"[capuchoo] could not record update event\", error);\n }\n}\n","/**\n * Plugins only the native-update path needs, loaded when it runs.\n *\n * OTA updates need `@capgo/capacitor-updater` and nothing else. Downloading and\n * installing an APK additionally needs file transfer, filesystem, network and a\n * file opener - four packages an app that only ships web bundles should not have\n * to install.\n *\n * They are optional peers, imported here rather than at module load, so\n * importing this library does not require them. A missing one produces a message\n * naming what to install instead of `Cannot find module` from inside a bundler.\n *\n * They cannot be plain dependencies: `cap sync` discovers plugins by reading the\n * *application's* `dependencies` and `devDependencies` - `getDependencies()` in\n * @capacitor/cli does not recurse - so a plugin pulled in transitively would\n * have its JavaScript installed and its native half never added to the Android\n * or iOS project. `capuchoo setup --native` adds them to the app instead.\n */\n\nconst NATIVE_PACKAGES = [\n \"@capacitor/file-transfer\",\n \"@capacitor/filesystem\",\n \"@capacitor/network\",\n \"@capawesome-team/capacitor-file-opener\",\n] as const;\n\nexport class MissingNativePluginsError extends Error {\n readonly packages: readonly string[];\n\n constructor(missing: string) {\n super(\n `Native updates need ${missing}, which is not installed. ` +\n `Run: npx capuchoo setup --native (adds ${NATIVE_PACKAGES.join(\", \")} and runs cap sync). ` +\n \"OTA updates do not need any of them.\",\n );\n this.name = \"MissingNativePluginsError\";\n this.packages = NATIVE_PACKAGES;\n }\n}\n\nasync function load<T>(specifier: string, importer: () => Promise<T>): Promise<T> {\n try {\n return await importer();\n } catch (error) {\n // A genuine runtime failure inside the plugin should not be reported as a\n // missing install, so only a resolution failure is translated.\n const message = error instanceof Error ? error.message : String(error);\n if (/cannot find module|failed to resolve|module not found/i.test(message)) {\n throw new MissingNativePluginsError(specifier);\n }\n throw error;\n }\n}\n\nexport const nativePlugins = {\n fileTransfer: () => load(\"@capacitor/file-transfer\", () => import(\"@capacitor/file-transfer\")),\n filesystem: () => load(\"@capacitor/filesystem\", () => import(\"@capacitor/filesystem\")),\n network: () => load(\"@capacitor/network\", () => import(\"@capacitor/network\")),\n fileOpener: () =>\n load(\n \"@capawesome-team/capacitor-file-opener\",\n () => import(\"@capawesome-team/capacitor-file-opener\"),\n ),\n};\n","import type { PluginListenerHandle } from \"@capacitor/core\";\nimport type { ResolvedUpdate } from \"@capuchoo/core\";\nimport { getUpdaterConfig } from \"./config.js\";\nimport { nativePlugins } from \"./optional-plugins.js\";\n\nexport interface DownloadProgress {\n loaded: number;\n total: number;\n percent: number;\n}\n\n/** Cache file name prefix, derived from the app id so two flavours never collide. */\nfunction cachePrefix(): string {\n const { appId, appName } = getUpdaterConfig();\n const base = appId || appName;\n return `${base.replaceAll(/[^\\w.-]/g, \"-\")}-`;\n}\n\nfunction apkFileName(update: ResolvedUpdate): string {\n return `${cachePrefix()}${update.version}-${update.versionCode ?? 0}.apk`;\n}\n\n/**\n * Downloads a native APK into the app cache and returns its path.\n *\n * The file name embeds the app id, so a staging build and a production build\n * installed side by side cannot overwrite each other's download. The previous\n * implementation prefixed every file with a hard-coded app name.\n */\nexport async function downloadNativeUpdate(\n update: ResolvedUpdate,\n onProgress: (progress: DownloadProgress) => void,\n): Promise<string> {\n if (!update.downloadUrl) {\n throw new Error(\"This update has no download URL\");\n }\n\n const { Network } = await nativePlugins.network();\n const network = await Network.getStatus();\n if (!network.connected) {\n throw new Error(\"Connect to the internet to download this update\");\n }\n\n // Clear older APKs first: they are typically 20-60 MB and the OS can evict\n // the cache mid-download if it is already full.\n await cleanApkCache();\n\n const fileName = apkFileName(update);\n const [{ Directory, Filesystem }, { FileTransfer }] = await Promise.all([\n nativePlugins.filesystem(),\n nativePlugins.fileTransfer(),\n ]);\n\n const destination = await Filesystem.getUri({\n directory: Directory.Cache,\n path: fileName,\n });\n\n let progressListener: PluginListenerHandle | null = null;\n\n try {\n progressListener = await FileTransfer.addListener(\"progress\", (event) => {\n if (event.url !== update.downloadUrl) return;\n\n const percent =\n event.lengthComputable && event.contentLength > 0\n ? Math.round((event.bytes / event.contentLength) * 100)\n : 0;\n onProgress({ loaded: event.bytes, total: event.contentLength, percent });\n });\n\n const result = await FileTransfer.downloadFile({\n url: update.downloadUrl,\n path: destination.uri,\n progress: true,\n connectTimeout: 60_000,\n readTimeout: 300_000,\n });\n\n return result.path || destination.uri;\n } finally {\n await progressListener?.remove();\n }\n}\n\n/** Removes this app's cached APKs. Failures are non-fatal. */\nexport async function cleanApkCache(): Promise<void> {\n const prefix = cachePrefix();\n\n try {\n const { Directory, Filesystem } = await nativePlugins.filesystem();\n const { files } = await Filesystem.readdir({\n directory: Directory.Cache,\n path: \"\",\n });\n\n await Promise.all(\n files\n .filter((file) => file.name.startsWith(prefix) && file.name.endsWith(\".apk\"))\n .map((file) => Filesystem.deleteFile({ directory: Directory.Cache, path: file.name })),\n );\n } catch (error) {\n console.warn(\"[capuchoo] could not clean the APK cache\", error);\n }\n}\n","import { Capacitor } from \"@capacitor/core\";\nimport { getUpdaterConfig } from \"./config.js\";\nimport { nativePlugins } from \"./optional-plugins.js\";\n\nconst APK_MIME = \"application/vnd.android.package-archive\";\n\n/**\n * Hands a downloaded APK to the Android package installer.\n *\n * Requires `REQUEST_INSTALL_PACKAGES` in the manifest - the Trapeze config for\n * each flavour merges it in - and the user must have allowed this app to\n * install unknown apps. Both failures surface as opaque platform errors, so\n * they are translated into something a user can act on.\n */\nexport async function openNativeInstaller(path: string): Promise<void> {\n if (Capacitor.getPlatform() !== \"android\") {\n throw new Error(\n \"Installing a native update from inside the app is only possible on Android. \" +\n \"On iOS the update has to come from the App Store or TestFlight.\",\n );\n }\n\n const { FileOpener } = await nativePlugins.fileOpener();\n\n try {\n await FileOpener.openFile({ path, mimeType: APK_MIME });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const { appName } = getUpdaterConfig();\n\n if (/permission|unknown sources|REQUEST_INSTALL_PACKAGES/i.test(message)) {\n throw new Error(\n `Allow ${appName} to install unknown apps in Android settings, then try again`,\n );\n }\n if (/activity|no app/i.test(message)) {\n throw new Error(\n \"No package installer is available on this device, so the update cannot be installed here\",\n );\n }\n\n throw new Error(`Could not open the Android installer: ${message}`);\n }\n}\n","import { CapacitorUpdater } from \"@capgo/capacitor-updater\";\nimport type { ResolvedUpdate } from \"@capuchoo/core\";\nimport { isNative } from \"./device.js\";\n\n/**\n * Thin wrapper over the OTA plugin.\n *\n * Downloading and applying a web bundle stays with `@capgo/capacitor-updater`:\n * it owns the native bundle store, the atomic swap and the rollback. This\n * module only sequences those calls correctly.\n */\n\n/**\n * Confirms the current bundle booted successfully.\n *\n * **This must be called once, early, on every app start.** If the plugin does\n * not hear it within `appReadyTimeout`, it assumes the new bundle crashed and\n * rolls back to the previous one - which looks exactly like \"the update did\n * not install\".\n */\nexport async function notifyAppReady(): Promise<void> {\n if (!isNative()) return;\n\n try {\n await CapacitorUpdater.notifyAppReady();\n } catch (error) {\n console.warn(\"[capuchoo] could not mark this bundle as ready\", error);\n }\n}\n\n/** The bundle currently applied, or null off-device. */\nexport async function getCurrentBundle() {\n if (!isNative()) return null;\n\n try {\n return await CapacitorUpdater.current();\n } catch (error) {\n console.warn(\"[capuchoo] could not read the current bundle\", error);\n return null;\n }\n}\n\n/**\n * Downloads an OTA bundle and applies it.\n *\n * `set` swaps the active bundle and reloads the WebView, so nothing after it\n * runs. It is called last on purpose.\n */\nexport async function applyOtaUpdate(update: ResolvedUpdate): Promise<void> {\n if (update.kind !== \"ota\") {\n throw new Error(\"applyOtaUpdate was given a native update\");\n }\n\n // Already downloaded - either by a previous attempt in this session, or by\n // the plugin's own background download when autoUpdate is \"onlyDownload\".\n if (update.bundleId) {\n await CapacitorUpdater.set({ id: update.bundleId });\n return;\n }\n\n if (!update.downloadUrl) {\n throw new Error(\"This update has no download URL\");\n }\n\n const bundle = await CapacitorUpdater.download({\n url: update.downloadUrl,\n version: update.version,\n ...(update.checksum ? { checksum: update.checksum } : {}),\n ...(update.sessionKey ? { sessionKey: update.sessionKey } : {}),\n });\n\n update.bundleId = bundle.id;\n await CapacitorUpdater.set({ id: bundle.id });\n}\n\n/** Discards a downloaded bundle that will not be applied. */\nexport async function discardBundle(bundleId: string): Promise<void> {\n if (!isNative()) return;\n\n try {\n await CapacitorUpdater.delete({ id: bundleId });\n } catch (error) {\n console.warn(\"[capuchoo] could not delete bundle\", bundleId, error);\n }\n}\n"],"mappings":"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"}
|