@capuchoo/updater 0.4.1 → 0.6.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.
@@ -19,16 +19,27 @@
19
19
  */
20
20
  type UpdaterMode = "onlyDownload" | "manual";
21
21
  interface UpdaterPluginOptions {
22
- /** Base URL of the Capuchoo backend. No trailing slash needed. */
23
- apiUrl: string;
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 the built-in bundle. Pass the app's
28
- * package.json version - if this is stale, the server compares against the
29
- * wrong baseline and re-serves bundles the device already has.
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: string;
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
- version: string;
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;;EAEf;;EAEA;;;;;;EAMA;EACA,OAAO;;EAEP;EACA;;;;;;EAMA;;UAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;iBAGc,sBAAsB,SAAS,uBAAuB"}
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"}
@@ -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("capuchooUpdaterConfig: apiUrl is empty. Set VITE_UPDATE_API_URL for this flavour before building, otherwise the app ships with updates disabled.");
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 /** Base URL of the Capuchoo backend. No trailing slash needed. */\n apiUrl: string;\n /** Channel this build defaults to. */\n channel: string;\n /**\n * Version the plugin reports as the built-in bundle. Pass the app's\n * package.json version - if this is stale, the server compares against the\n * wrong baseline and re-serves bundles the device already has.\n */\n version: string;\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 autoUpdate: boolean | \"onlyDownload\";\n updateUrl: string;\n statsUrl: string;\n channelUrl: string;\n defaultChannel: string;\n version: string;\n directUpdate: boolean;\n appReadyTimeout: number;\n responseTimeout: number;\n allowModifyUrl: boolean;\n}\n\nexport function capuchooUpdaterConfig(options: UpdaterPluginOptions): CapacitorUpdaterPluginConfig {\n const apiUrl = options.apiUrl.replace(/\\/+$/, \"\");\n\n if (!apiUrl) {\n // The plugin accepts an empty updateUrl and then silently never checks for\n // updates, which is the worst possible outcome. Fail the build instead.\n throw new Error(\n \"capuchooUpdaterConfig: apiUrl is empty. Set VITE_UPDATE_API_URL for this \" +\n \"flavour before building, otherwise the app ships with 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 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":";AAyDA,SAAgB,sBAAsB,SAA6D;CACjG,MAAM,SAAS,QAAQ,OAAO,QAAQ,QAAQ,EAAE;CAEhD,IAAI,CAAC,QAGH,MAAM,IAAI,MACR,kJAEF;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,SAAS,QAAQ;EAGjB,cAAc;EACd,iBAAiB,QAAQ,mBAAmB;EAC5C,iBAAiB,QAAQ,mBAAmB;EAC5C,gBAAgB,QAAQ,kBAAkB;CAC5C;AACF"}
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 cleanApkCache, r as getCurrentBundle, s as downloadNativeUpdate, t as applyOtaUpdate } from "./ota.service-B3LrcCPr.js";
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,50 @@ 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
+ * Pure over file names and sizes; `download.service.ts` supplies the filesystem.
165
+ */
166
+ /** Cache file name prefix, derived from the app id so two flavours never collide. */
167
+ declare function cachePrefix(appId: string): string;
168
+ interface ApkIdentity {
169
+ version: string;
170
+ versionCode: number;
171
+ }
172
+ /** `com.efficy.app-1.0.56-67.apk` */
173
+ declare function apkFileName(appId: string, update: ApkIdentity): string;
174
+ /**
175
+ * Reads a name produced by `apkFileName`, or null for anything else - the
176
+ * WebView's own files share this directory and the caller deletes what we claim.
177
+ */
178
+ declare function parseApkFileName(appId: string, fileName: string): ApkIdentity | null;
179
+ /**
180
+ * Whether a cached file is the offered update, complete.
181
+ *
182
+ * Size is the check: an interrupted download leaves a partial file at the right
183
+ * path. Without an expected size the file is not trusted.
184
+ */
185
+ declare function isCompleteDownload(cached: {
186
+ size: number;
187
+ } | null, expectedSize: number | undefined): boolean;
188
+ interface CachedApk extends ApkIdentity {
189
+ fileName: string;
190
+ }
191
+ /**
192
+ * Which cached APKs to delete.
193
+ *
194
+ * Without `keep`: only what the installed build number has caught up with, so a
195
+ * newer downloaded-but-uninstalled APK survives. With `keep`: also other pending
196
+ * downloads, to make room. `keep` itself is never deleted.
197
+ */
198
+ declare function apksToDelete(input: {
199
+ cached: CachedApk[];
200
+ installedVersionCode: number;
201
+ keep?: string | undefined;
202
+ }): string[];
203
+ //#endregion
160
204
  //#region src/install.service.d.ts
161
205
  /**
162
206
  * Hands a downloaded APK to the Android package installer.
@@ -168,5 +212,5 @@ declare function describeConfigProblems(config: UpdaterConfig): string[];
168
212
  */
169
213
  declare function openNativeInstaller(path: string): Promise<void>;
170
214
  //#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, checkForUpdate, cleanApkCache, configureUpdater, describeConfigProblems, discardBundle, downloadNativeUpdate, getBuiltinVersion, getBundleVersion, getCurrentBundle, getDeviceId, getOsFacts, getPlatform, getPluginVersion, getUpdaterConfig, getVersionCode, isNative, logUpdateEvent, notifyAppReady, openNativeInstaller };
215
+ 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
216
  //# sourceMappingURL=index.d.ts.map
@@ -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;;;;;;;;;;;iBC1DzB,oBAAoB,eAAe"}
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;;;;;;;;;iBCjE/B,YAAY;UAIX;EACf;EACA;;;iBAIc,YAAY,eAAe,QAAQ;;;;;iBAQnC,iBAAiB,eAAe,mBAAmB;;;;;;;iBAiBnD,mBACd;EAAU;UACV;UAMe,kBAAkB;EACjC;;;;;;;;;iBAUc,aAAa;EAC3B,QAAQ;EACR;EACA;;;;;;;;;;;;iBClDoB,oBAAoB,eAAe"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { C as getUpdaterConfig, S as describeConfigProblems, _ as getPlatform, a as openNativeInstaller, b as isNative, c as UpdateCheckBlockedError, d as checkForUpdate, f as logUpdateEvent, g as getOsFacts, h as getDeviceId, i as notifyAppReady, l as UpdaterConfigError, m as getBundleVersion, n as discardBundle, o as cleanApkCache, p as getBuiltinVersion, r as getCurrentBundle, s as downloadNativeUpdate, t as applyOtaUpdate, u as buildCheckRequest, v as getPluginVersion, x as configureUpdater, y as getVersionCode } from "./ota.service-SyGTRpY2.js";
2
- export { UpdateCheckBlockedError, UpdaterConfigError, applyOtaUpdate, buildCheckRequest, checkForUpdate, cleanApkCache, configureUpdater, describeConfigProblems, discardBundle, downloadNativeUpdate, getBuiltinVersion, getBundleVersion, getCurrentBundle, getDeviceId, getOsFacts, getPlatform, getPluginVersion, getUpdaterConfig, getVersionCode, isNative, logUpdateEvent, notifyAppReady, openNativeInstaller };
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-DmSAtu9q.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
- * The file name embeds the app id, so a staging build and a production build
12
- * installed side by side cannot overwrite each other's download. The previous
13
- * implementation prefixed every file with a hard-coded app name.
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
- /** Removes this app's cached APKs. Failures are non-fatal. */
17
- declare function cleanApkCache(): Promise<void>;
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, cleanApkCache as o, getCurrentBundle as r, downloadNativeUpdate as s, applyOtaUpdate as t };
49
- //# sourceMappingURL=ota.service-B3LrcCPr.d.ts.map
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,62 @@ 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
+ * Pure over file names and sizes; `download.service.ts` supplies the filesystem.
290
+ */
291
+ /** Cache file name prefix, derived from the app id so two flavours never collide. */
292
+ function cachePrefix(appId) {
293
+ return `${appId.replaceAll(/[^\w.-]/g, "-")}-`;
294
+ }
295
+ /** `com.efficy.app-1.0.56-67.apk` */
296
+ function apkFileName(appId, update) {
297
+ return `${cachePrefix(appId)}${update.version}-${update.versionCode}.apk`;
298
+ }
299
+ /**
300
+ * Reads a name produced by `apkFileName`, or null for anything else - the
301
+ * WebView's own files share this directory and the caller deletes what we claim.
302
+ */
303
+ function parseApkFileName(appId, fileName) {
304
+ const prefix = cachePrefix(appId);
305
+ if (!fileName.startsWith(prefix) || !fileName.endsWith(".apk")) return null;
306
+ const middle = fileName.slice(prefix.length, -4);
307
+ const match = /^(.+)-(\d+)$/.exec(middle);
308
+ if (!match) return null;
309
+ return {
310
+ version: match[1],
311
+ versionCode: Number(match[2])
312
+ };
313
+ }
314
+ /**
315
+ * Whether a cached file is the offered update, complete.
316
+ *
317
+ * Size is the check: an interrupted download leaves a partial file at the right
318
+ * path. Without an expected size the file is not trusted.
319
+ */
320
+ function isCompleteDownload(cached, expectedSize) {
321
+ if (!cached || !expectedSize) return false;
322
+ return cached.size === expectedSize;
323
+ }
324
+ /**
325
+ * Which cached APKs to delete.
326
+ *
327
+ * Without `keep`: only what the installed build number has caught up with, so a
328
+ * newer downloaded-but-uninstalled APK survives. With `keep`: also other pending
329
+ * downloads, to make room. `keep` itself is never deleted.
330
+ */
331
+ function apksToDelete(input) {
332
+ const { cached, installedVersionCode, keep } = input;
333
+ const makingRoom = keep !== void 0;
334
+ return cached.filter((apk) => {
335
+ if (apk.fileName === keep) return false;
336
+ if (apk.versionCode <= installedVersionCode) return true;
337
+ return makingRoom;
338
+ }).map((apk) => apk.fileName);
339
+ }
340
+ //#endregion
284
341
  //#region src/optional-plugins.ts
285
342
  /**
286
343
  * Plugins only the native-update path needs, loaded when it runs.
@@ -331,27 +388,86 @@ const nativePlugins = {
331
388
  };
332
389
  //#endregion
333
390
  //#region src/download.service.ts
334
- /** Cache file name prefix, derived from the app id so two flavours never collide. */
335
- function cachePrefix() {
391
+ const DONE = {
392
+ loaded: 0,
393
+ total: 0,
394
+ percent: 100
395
+ };
396
+ function fileNameFor(update) {
336
397
  const { appId, appName } = getUpdaterConfig();
337
- return `${(appId || appName).replaceAll(/[^\w.-]/g, "-")}-`;
398
+ return apkFileName(appId || appName, {
399
+ version: update.version,
400
+ versionCode: update.versionCode ?? 0
401
+ });
338
402
  }
339
- function apkFileName(update) {
340
- return `${cachePrefix()}${update.version}-${update.versionCode ?? 0}.apk`;
403
+ /** Every APK in the cache that belongs to this app. */
404
+ async function listCachedApks() {
405
+ const { appId, appName } = getUpdaterConfig();
406
+ const owner = appId || appName;
407
+ try {
408
+ const { Directory, Filesystem } = await nativePlugins.filesystem();
409
+ const { files } = await Filesystem.readdir({
410
+ directory: Directory.Cache,
411
+ path: ""
412
+ });
413
+ return files.flatMap((file) => {
414
+ const identity = parseApkFileName(owner, file.name);
415
+ return identity ? [{
416
+ ...identity,
417
+ fileName: file.name
418
+ }] : [];
419
+ });
420
+ } catch {
421
+ return [];
422
+ }
423
+ }
424
+ /**
425
+ * The path of an already-downloaded, complete copy of this update.
426
+ *
427
+ * The only record that a download had finished used to be an in-memory
428
+ * `cachedPath`, so closing the app threw it away and the next launch downloaded
429
+ * the same 45 MB again while the file sat on disk. Asking the filesystem
430
+ * survives a restart.
431
+ */
432
+ async function findCachedApk(update) {
433
+ const fileName = fileNameFor(update);
434
+ try {
435
+ const { Directory, Filesystem } = await nativePlugins.filesystem();
436
+ if (!isCompleteDownload({ size: (await Filesystem.stat({
437
+ directory: Directory.Cache,
438
+ path: fileName
439
+ })).size }, update.fileSize)) return null;
440
+ const { uri } = await Filesystem.getUri({
441
+ directory: Directory.Cache,
442
+ path: fileName
443
+ });
444
+ return uri;
445
+ } catch {
446
+ return null;
447
+ }
341
448
  }
342
449
  /**
343
450
  * Downloads a native APK into the app cache and returns its path.
344
451
  *
345
- * The file name embeds the app id, so a staging build and a production build
346
- * installed side by side cannot overwrite each other's download. The previous
347
- * implementation prefixed every file with a hard-coded app name.
452
+ * Returns immediately when a complete copy is already there. The previous
453
+ * implementation could not: its first step was `cleanApkCache()`, which deleted
454
+ * every APK for this app - including the exact file it was about to fetch - so
455
+ * pressing update twice always paid for the binary twice.
348
456
  */
349
457
  async function downloadNativeUpdate(update, onProgress) {
350
458
  if (!update.downloadUrl) throw new Error("This update has no download URL");
459
+ const existing = await findCachedApk(update);
460
+ if (existing) {
461
+ onProgress({ ...DONE });
462
+ return existing;
463
+ }
351
464
  const { Network } = await nativePlugins.network();
352
465
  if (!(await Network.getStatus()).connected) throw new Error("Connect to the internet to download this update");
353
- await cleanApkCache();
354
- const fileName = apkFileName(update);
466
+ const fileName = fileNameFor(update);
467
+ await pruneApkCache({
468
+ installedVersionCode: 0,
469
+ keep: fileName
470
+ });
355
471
  const [{ Directory, Filesystem }, { FileTransfer }] = await Promise.all([nativePlugins.filesystem(), nativePlugins.fileTransfer()]);
356
472
  const destination = await Filesystem.getUri({
357
473
  directory: Directory.Cache,
@@ -379,21 +495,33 @@ async function downloadNativeUpdate(update, onProgress) {
379
495
  await progressListener?.remove();
380
496
  }
381
497
  }
382
- /** Removes this app's cached APKs. Failures are non-fatal. */
383
- async function cleanApkCache() {
384
- const prefix = cachePrefix();
498
+ /**
499
+ * Deletes cached APKs the device has outgrown.
500
+ *
501
+ * Called on start-up with the installed build number, which is how an installed
502
+ * APK is finally cleaned up: the Android installer never calls back, but the
503
+ * next launch reports a higher build number and that says the same thing. A
504
+ * *newer* APK is kept - it is an update already paid for and waiting to be
505
+ * installed.
506
+ *
507
+ * Failures are non-fatal. Reclaiming disk space must never break an update.
508
+ */
509
+ async function pruneApkCache(options) {
385
510
  try {
386
- const { Directory, Filesystem } = await nativePlugins.filesystem();
387
- const { files } = await Filesystem.readdir({
388
- directory: Directory.Cache,
389
- path: ""
511
+ const doomed = apksToDelete({
512
+ cached: await listCachedApks(),
513
+ ...options
390
514
  });
391
- await Promise.all(files.filter((file) => file.name.startsWith(prefix) && file.name.endsWith(".apk")).map((file) => Filesystem.deleteFile({
515
+ if (doomed.length === 0) return [];
516
+ const { Directory, Filesystem } = await nativePlugins.filesystem();
517
+ await Promise.all(doomed.map((fileName) => Filesystem.deleteFile({
392
518
  directory: Directory.Cache,
393
- path: file.name
519
+ path: fileName
394
520
  })));
521
+ return doomed;
395
522
  } catch (error) {
396
- console.warn("[capuchoo] could not clean the APK cache", error);
523
+ console.warn("[capuchoo] could not prune the APK cache", error);
524
+ return [];
397
525
  }
398
526
  }
399
527
  //#endregion
@@ -490,6 +618,6 @@ async function discardBundle(bundleId) {
490
618
  }
491
619
  }
492
620
  //#endregion
493
- export { getUpdaterConfig as C, describeConfigProblems as S, getPlatform as _, openNativeInstaller as a, isNative as b, UpdateCheckBlockedError as c, checkForUpdate as d, logUpdateEvent as f, getOsFacts as g, getDeviceId as h, notifyAppReady as i, UpdaterConfigError as l, getBundleVersion as m, discardBundle as n, cleanApkCache as o, getBuiltinVersion as p, getCurrentBundle as r, downloadNativeUpdate as s, applyOtaUpdate as t, buildCheckRequest as u, getPluginVersion as v, configureUpdater as x, getVersionCode as y };
621
+ 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
622
 
495
- //# sourceMappingURL=ota.service-SyGTRpY2.js.map
623
+ //# sourceMappingURL=ota.service-DmSAtu9q.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ota.service-DmSAtu9q.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 * Pure over file names and sizes; `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`, or null for anything else - the\n * WebView's own files share this directory and the caller deletes what we claim.\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 offered update, complete.\n *\n * Size is the check: an interrupted download leaves a partial file at the right\n * path. Without an expected size the file is not trusted.\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.\n *\n * Without `keep`: only what the installed build number has caught up with, so a\n * newer downloaded-but-uninstalled APK survives. With `keep`: also other pending\n * downloads, to make room. `keep` itself is never deleted.\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-B3LrcCPr.js";
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
- /** Whether the prompt should be on screen at all. */
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<"Downloading..." | "Installing..." | "Install now" | "Download" | "Update now">;
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":";;;UAgBiB;EACf;EACA;EACA;EACA;EACA,eAAe;EACf,UAAU;;EAEV;EACA;;EAEA;;EAEA;;;;;;;;iBAqFa,MAAM,mBAAiB;;iBA2CvB,iBAAiB;;iBAuCjB,uBAAuB;;;;;;;iBAyBvB,QAAQ;iBAUR,WAAW;;iBAMX,WAAW;iBAYV;;aAxOJ;aACG;aACD;aACK;;;;;;;;;;;;;;;;;;aAIL;aACL;aAEQ;aAEG;;aAZR;aACG;aACD;aACK;;;;;;;;;;;;;;;;;;aAIL;aACL;aAEQ;aAEG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBCjBJ"}
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 { C as getUpdaterConfig, a as openNativeInstaller, b as isNative, c as UpdateCheckBlockedError, d as checkForUpdate, f as logUpdateEvent, i as notifyAppReady, l as UpdaterConfigError, o as cleanApkCache, r as getCurrentBundle, s as downloadNativeUpdate, t as applyOtaUpdate } from "./ota.service-SyGTRpY2.js";
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-DmSAtu9q.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 cleanApkCache();
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
- /** Whether the prompt should be on screen at all. */
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 "Downloading...";
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":";;;;AAgCA,MAAM,cAAgC;CAAE,QAAQ;CAAG,OAAO;CAAG,SAAS;AAAE;AACxE,MAAM,gBAAkC;CAAE,QAAQ;CAAK,OAAO;CAAK,SAAS;AAAI;;;;;AAMhF,MAAM,QAAQ,IAAkB;CAC9B,UAAU;CACV,aAAa;CACb,YAAY;CACZ,iBAAiB;CACjB,eAAe;CACf,UAAU,EAAE,GAAG,YAAY;CAC3B,YAAY;CACZ,OAAO;CACP,eAAe;CACf,kBAAkB;AACpB,CAAC;AAED,MAAM,YAAoC,CAAC;AAC3C,IAAI,cAAc;AAElB,SAAS,QAAQ,QAA8B;CAG7C,IAAI,MAAM,MAAM,eAAe,SAAS,YAAY,OAAO,SAAS,OAAO;CAE3E,MAAM,MAAM,gBAAgB;CAC5B,MAAM,MAAM,kBAAkB;CAC9B,MAAM,MAAM,aAAa;CACzB,MAAM,MAAM,WAAW,EAAE,GAAG,YAAY;CACxC,MAAM,MAAM,QAAQ;CACpB,MAAM,MAAM,mBAAmB,WAAW,OAAO,QAAQ;AAC3D;AAEA,eAAe,wBAAuC;CACpD,UAAU,KAIR,MAAM,iBAAiB,YAAY,oBAAoB,EAAE,aAAa;EACpE,QAAQ;GACN,MAAM;GACN,SAAS,OAAO;GAChB,UAAU,OAAO;GACjB,UAAU;EACZ,CAAC;CACH,CAAC,GAED,MAAM,iBAAiB,YAAY,aAAa,EAAE,cAAc;EAC9D,IAAI,MAAM,MAAM,eAAe,SAAS,OAAO;EAC/C,MAAM,MAAM,cAAc;EAC1B,MAAM,MAAM,WAAW;GAAE,QAAQ;GAAS,OAAO;GAAK;EAAQ;CAChE,CAAC,GAED,MAAM,iBAAiB,YAAY,qBAAqB,EAAE,aAAa;EACrE,IAAI,MAAM,MAAM,eAAe,SAAS,OAAO;EAC/C,MAAM,MAAM,cAAc;EAC1B,MAAM,MAAM,cAAc,WAAW,OAAO;EAC5C,MAAM,MAAM,WAAW,EAAE,GAAG,cAAc;EAC1C,MAAM,MAAM,gBAAgB;CAC9B,CAAC,GAED,MAAM,iBAAiB,YAAY,wBAAwB;EACzD,MAAM,MAAM,cAAc;EAC1B,MAAM,MAAM,QAAQ;CACtB,CAAC,GAED,MAAM,iBAAiB,YAAY,sBAAsB;EACvD,MAAM,EAAE,YAAY,iBAAiB;EACrC,MAAM,MAAM,QAAQ,yBAAyB,QAAQ;CACvD,CAAC,CACH;AACF;;;;;;;AAQA,eAAe,MAAM,SAAS,OAAyB;CACrD,IAAI,CAAC,SAAS,KAAK,MAAM,MAAM,UAAU,OAAO;CAEhD,MAAM,MAAM,WAAW;CACvB,MAAM,MAAM,QAAQ;CACpB,MAAM,MAAM,mBAAmB;CAC/B,MAAM,MAAM,gBAAgB;CAE5B,IAAI;EACF,MAAM,SAAS,MAAM,eAAe;EAEpC,IAAI,QAAQ;GACV,QAAQ,MAAM;GACd,MAAM,eAAe,SAAS,MAAM;GACpC,OAAO;EACT;EAEA,IAAI,CAAC,MAAM,MAAM,iBAAiB;GAChC,MAAM,EAAE,YAAY,iBAAiB;GACrC,MAAM,MAAM,mBAAmB,GAAG,QAAQ;EAC5C;EACA,OAAO;CACT,SAAS,OAAO;EAGd,IAAI,iBAAiB,oBAAoB;GACvC,MAAM,MAAM,QAAQ;GACpB,QAAQ,MAAM,cAAc,MAAM,SAAS,KAAK,IAAI,CAAC;EACvD,OAAO,IAAI,iBAAiB,yBAAyB;GACnD,MAAM,MAAM,QAAQ,2CAA2C,MAAM;GACrE,QAAQ,MAAM,cAAc,MAAM,SAAS,MAAM,QAAQ;EAC3D,OAAO;GACL,MAAM,MAAM,QAAQ;GACpB,IAAI,CAAC,QAAQ,QAAQ,MAAM,kCAAkC,KAAK;EACpE;EACA,OAAO;CACT,UAAU;EACR,MAAM,MAAM,WAAW;EACvB,MAAM,MAAM,gBAAgB;CAC9B;AACF;;AAGA,eAAe,gBAA+B;CAC5C,MAAM,SAAS,MAAM,MAAM;CAC3B,IAAI,CAAC,UAAU,MAAM,MAAM,eAAe,MAAM,MAAM,YAAY;CAElE,MAAM,MAAM,QAAQ;CAGpB,IAAI,OAAO,SAAS,YAAY,MAAM,MAAM,YAAY;EACtD,MAAM,oBAAoB;EAC1B;CACF;CAEA,MAAM,MAAM,cAAc;CAC1B,MAAM,MAAM,gBACV,OAAO,SAAS,WAAW,mCAAmC;CAEhE,IAAI;EACF,IAAI,OAAO,SAAS,UAAU;GAC5B,MAAM,MAAM,aAAa,MAAM,qBAAqB,SAAS,aAAa;IACxE,MAAM,MAAM,WAAW;GACzB,CAAC;GACD,MAAM,MAAM,WAAW,EAAE,GAAG,cAAc;GAC1C,MAAM,MAAM,gBAAgB;GAC5B,MAAM,eAAe,qBAAqB,MAAM;GAChD;EACF;EAGA,MAAM,eAAe,MAAM;CAC7B,SAAS,OAAO;EACd,MAAM,MAAM,QAAQ,iBAAiB,QAAQ,MAAM,UAAU;EAC7D,MAAM,eAAe,SAAS,QAAQ,EAAE,OAAO,MAAM,MAAM,MAAM,CAAC;CACpE,UAAU;EACR,MAAM,MAAM,cAAc;EAC1B,IAAI,CAAC,MAAM,MAAM,YAAY,MAAM,MAAM,gBAAgB;CAC3D;AACF;;AAGA,eAAe,sBAAqC;CAClD,MAAM,SAAS,MAAM,MAAM;CAC3B,MAAM,OAAO,MAAM,MAAM;CACzB,IAAI,CAAC,UAAU,OAAO,SAAS,YAAY,CAAC,MAAM;CAElD,MAAM,MAAM,aAAa;CACzB,MAAM,MAAM,QAAQ;CAEpB,IAAI;EACF,MAAM,oBAAoB,IAAI;EAC9B,MAAM,eAAe,WAAW,MAAM;CACxC,SAAS,OAAO;EACd,MAAM,MAAM,QAAQ,iBAAiB,QAAQ,MAAM,UAAU;EAC7D,MAAM,eAAe,SAAS,QAAQ,EAAE,OAAO,MAAM,MAAM,MAAM,CAAC;CACpE,UAAU;EACR,MAAM,MAAM,aAAa;CAC3B;AACF;;;;;;;AAQA,eAAe,OAAsB;CACnC,IAAI,CAAC,SAAS,KAAK,aAAa;CAChC,cAAc;CAEd,MAAM,eAAe;CACrB,MAAM,sBAAsB;CAC5B,MAAM,cAAc;CACpB,MAAM,MAAM,IAAI;AAClB;AAEA,eAAe,UAAyB;CACtC,MAAM,QAAQ,IAAI,UAAU,OAAO,CAAC,CAAC,CAAC,KAAK,aAAa,SAAS,OAAO,CAAC,CAAC;CAC1E,cAAc;AAChB;;AAGA,eAAe,UAAyB;CACtC,MAAM,SAAS,MAAM,MAAM;CAC3B,IAAI,CAAC,UAAU,OAAO,YAAY,MAAM,MAAM,aAAa;CAE3D,MAAM,eAAe,UAAU,MAAM;CACrC,MAAM,MAAM,kBAAkB;CAC9B,MAAM,MAAM,gBAAgB;CAC5B,MAAM,MAAM,aAAa;CACzB,MAAM,MAAM,gBAAgB;CAC5B,MAAM,MAAM,WAAW,EAAE,GAAG,YAAY;AAC1C;AAEA,SAAgB,aAAa;CAC3B,OAAO;EACL,OAAO,SAAS,KAAK;EACrB,YAAY,eAAe,MAAM,MAAM,QAAQ;EAC/C,eAAe,eAAe,MAAM,MAAM,WAAW;EACrD,cAAc,eAAe,MAAM,MAAM,UAAU;EACnD,iBAAiB,eAAe,MAAM,MAAM,eAAe;EAC3D,eAAe,eAAe,MAAM,MAAM,aAAa;EACvD,UAAU,eAAe,MAAM,MAAM,QAAQ;EAC7C,YAAY,eAAe,MAAM,MAAM,UAAU;EACjD,OAAO,eAAe,MAAM,MAAM,KAAK;EACvC,eAAe,eAAe,MAAM,MAAM,aAAa;EACvD,kBAAkB,eAAe,MAAM,MAAM,gBAAgB;;EAE7D,YAAY,eAAe,MAAM,MAAM,eAAe,aAAa,IAAI;EAEvE;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF;;;;;;;;;;;;ACrQA,SAAgB,kBAAkB;CAChC,MAAM,UAAU,WAAW;CAE3B,MAAM,SAAS,QAAQ;CACvB,MAAM,iBAAiB,eAAe,OAAO,OAAO,SAAS,QAAQ;CAErE,OAAO;EACL,GAAG;;EAGH,SAAS,eAAe,QAAQ,gBAAgB,SAAS,QAAQ,MAAM,UAAU,IAAI;EAErF,OAAO,eAAe;GACpB,IAAI,QAAQ,MAAM,OAAO,OAAO;GAChC,IAAI,CAAC,OAAO,OAAO,OAAO;GAC1B,OAAO,OAAO,MAAM,WAAW,oBAAoB;EACrD,CAAC;EAED,UAAU,eAAe;GACvB,IAAI,CAAC,OAAO,OAAO,OAAO;GAC1B,MAAM,OAAO,eAAe,QAAQ,gBAAgB;GACpD,OAAO,WAAW,OAAO,MAAM,QAAQ,KAAK;EAC9C,CAAC;EAED,MAAM,eAAe,QAAQ,MAAM,SAAS,OAAO,OAAO,gBAAgB,EAAE;;;;;EAM5E,cAAc,eAAe;GAC3B,IAAI,QAAQ,cAAc,OAAO,OAAO;GACxC,IAAI,QAAQ,aAAa,OAAO,OAAO;GACvC,IAAI,eAAe,SAAS,QAAQ,WAAW,OAAO,OAAO;GAC7D,IAAI,eAAe,OAAO,OAAO;GACjC,OAAO;EACT,CAAC;EAED,qBACE,eAAe,SAAS,QAAQ,WAAW,QACvC,QAAQ,oBAAoB,IAC5B,QAAQ,cAAc;;EAG5B,MAAM,eAAe,QAAQ,cAAc,SAAS,QAAQ,aAAa,KAAK;;EAG9E,aAAa,eAET,CAAC,QAAQ,WAAW,SAAS,CAAC,QAAQ,cAAc,SAAS,CAAC,QAAQ,aAAa,KACvF;EAEA,cAAc,eAAe,QAAQ,cAAc,SAAS,QAAQ,SAAS,MAAM,UAAU,CAAC;CAChG;AACF"}
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.4.1",
3
+ "version": "0.6.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.2.0"
45
+ "@capuchoo/core": "^0.4.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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"}