@capuchoo/updater 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,370 @@
1
+ import { isBlockingResponse, resolveUpdate } from "@capuchoo/core";
2
+ import { App } from "@capacitor/app";
3
+ import { Capacitor } from "@capacitor/core";
4
+ import { CapacitorUpdater } from "@capgo/capacitor-updater";
5
+ import { FileTransfer } from "@capacitor/file-transfer";
6
+ import { Directory, Filesystem } from "@capacitor/filesystem";
7
+ import { Network } from "@capacitor/network";
8
+ import { FileOpener } from "@capawesome-team/capacitor-file-opener";
9
+ //#region src/config.ts
10
+ function env(key) {
11
+ return import.meta.env?.[key];
12
+ }
13
+ function stripTrailingSlash(url) {
14
+ return url.replace(/\/+$/, "");
15
+ }
16
+ let overrides = {};
17
+ /**
18
+ * Overrides configuration resolved from the build. Call before `init`.
19
+ * Passing `{}` clears previous overrides.
20
+ */
21
+ function configureUpdater(next) {
22
+ overrides = {
23
+ ...overrides,
24
+ ...next
25
+ };
26
+ }
27
+ function getUpdaterConfig() {
28
+ return {
29
+ apiUrl: stripTrailingSlash(overrides.apiUrl ?? env("VITE_UPDATE_API_URL") ?? ""),
30
+ appId: overrides.appId ?? env("VITE_APP_ID") ?? "",
31
+ appName: overrides.appName ?? env("VITE_APP_NAME") ?? "the app",
32
+ channel: overrides.channel ?? env("VITE_UPDATE_CHANNEL") ?? "prod",
33
+ environment: overrides.environment ?? env("VITE_ENVIRONMENT") ?? (env("PROD") === "true" ? "prod" : "dev"),
34
+ timeoutMs: overrides.timeoutMs ?? 3e4
35
+ };
36
+ }
37
+ /**
38
+ * Reasons the updater cannot run, as user-facing strings.
39
+ *
40
+ * The previous implementation defaulted `apiUrl` to a hard-coded Render URL and
41
+ * `appId` to a hard-coded bundle id. A build with a missing variable therefore
42
+ * silently pointed at somebody else's backend, or asked for the wrong app, and
43
+ * reported "you are up to date". Failing loudly is the whole point of this
44
+ * function.
45
+ */
46
+ function describeConfigProblems(config) {
47
+ const problems = [];
48
+ if (!config.apiUrl) problems.push("VITE_UPDATE_API_URL is not set, so updates cannot be checked");
49
+ if (!config.appId) problems.push("VITE_APP_ID is not set, so the server cannot identify this build");
50
+ return problems;
51
+ }
52
+ //#endregion
53
+ //#region src/device.ts
54
+ /**
55
+ * Facts about the running build that the server needs in order to decide
56
+ * whether an update applies.
57
+ */
58
+ /**
59
+ * Native build number of the installed binary.
60
+ *
61
+ * Returns 0 off-device. The old implementation returned 999999 on web, which
62
+ * meant a browser session claimed to be newer than every published release and
63
+ * so never saw an update - masking the very bug you would be debugging.
64
+ */
65
+ async function getVersionCode() {
66
+ if (!Capacitor.isNativePlatform()) return 0;
67
+ try {
68
+ const info = await App.getInfo();
69
+ return Number.parseInt(info.build, 10) || 0;
70
+ } catch {
71
+ return 0;
72
+ }
73
+ }
74
+ /**
75
+ * Semantic version of the web bundle currently applied.
76
+ *
77
+ * `"builtin"` means no OTA bundle has been applied yet and the app is running
78
+ * the assets compiled into the binary. The server treats it as 0.0.0, so it
79
+ * must be reported honestly rather than sent as a constant.
80
+ */
81
+ async function getBundleVersion() {
82
+ if (!Capacitor.isNativePlatform()) return "builtin";
83
+ try {
84
+ return (await CapacitorUpdater.current()).bundle.version || "builtin";
85
+ } catch {
86
+ return "builtin";
87
+ }
88
+ }
89
+ /**
90
+ * Stable per-install identifier, supplied by the OTA plugin.
91
+ *
92
+ * The plugin persists this natively. Reading `localStorage.device_id` instead -
93
+ * as the app template did - returns null on a fresh install and is wiped
94
+ * whenever the WebView data is cleared, so channel overrides and per-device
95
+ * stats silently stopped working.
96
+ */
97
+ async function getDeviceId() {
98
+ if (!Capacitor.isNativePlatform()) return "web";
99
+ try {
100
+ const { deviceId } = await CapacitorUpdater.getDeviceId();
101
+ return deviceId || "unknown";
102
+ } catch {
103
+ return "unknown";
104
+ }
105
+ }
106
+ function getPlatform() {
107
+ return Capacitor.getPlatform();
108
+ }
109
+ function isNative() {
110
+ return Capacitor.isNativePlatform();
111
+ }
112
+ //#endregion
113
+ //#region src/api.service.ts
114
+ /** Raised when the updater is misconfigured, rather than reporting "up to date". */
115
+ var UpdaterConfigError = class extends Error {
116
+ problems;
117
+ constructor(problems) {
118
+ super(problems.join("; "));
119
+ this.name = "UpdaterConfigError";
120
+ this.problems = problems;
121
+ }
122
+ };
123
+ /** Raised when the server says the request itself cannot be served. */
124
+ var UpdateCheckBlockedError = class extends Error {
125
+ response;
126
+ constructor(response) {
127
+ super(response.message ?? "The update service rejected this request");
128
+ this.name = "UpdateCheckBlockedError";
129
+ this.response = response;
130
+ }
131
+ };
132
+ async function postJson(url, body, timeoutMs) {
133
+ const controller = new AbortController();
134
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
135
+ try {
136
+ const response = await fetch(url, {
137
+ method: "POST",
138
+ headers: { "Content-Type": "application/json" },
139
+ body: JSON.stringify(body),
140
+ signal: controller.signal
141
+ });
142
+ if (!response.ok) throw new Error(`${url} responded ${response.status} ${response.statusText}`);
143
+ return await response.json();
144
+ } finally {
145
+ clearTimeout(timer);
146
+ }
147
+ }
148
+ /**
149
+ * Asks the server what this device should be running.
150
+ *
151
+ * One request, one endpoint. The app template used to call
152
+ * `GET /api/native-updates/check` for native updates *and*
153
+ * `POST /api/update` for OTA, which meant two sources of truth: the native
154
+ * endpoint ignores the channel's assigned native version and the
155
+ * `min_update_version` gate, so a device could be told to install an OTA
156
+ * bundle its binary was too old to run.
157
+ */
158
+ async function checkForUpdate() {
159
+ if (!isNative()) return null;
160
+ const config = getUpdaterConfig();
161
+ const problems = describeConfigProblems(config);
162
+ if (problems.length > 0) throw new UpdaterConfigError(problems);
163
+ const [versionCode, versionName, deviceId] = await Promise.all([
164
+ getVersionCode(),
165
+ getBundleVersion(),
166
+ getDeviceId()
167
+ ]);
168
+ const request = {
169
+ appId: config.appId,
170
+ platform: getPlatform(),
171
+ channel: config.channel,
172
+ defaultChannel: config.channel,
173
+ versionCode: String(versionCode),
174
+ versionBuild: String(versionCode),
175
+ version_name: versionName,
176
+ deviceId,
177
+ isProd: config.environment === "prod"
178
+ };
179
+ const response = await postJson(`${config.apiUrl}/api/update`, request, config.timeoutMs);
180
+ if (isBlockingResponse(response)) throw new UpdateCheckBlockedError(response);
181
+ return resolveUpdate(response);
182
+ }
183
+ /**
184
+ * Records a native update lifecycle event.
185
+ *
186
+ * Best effort: analytics must never break an update. OTA events are reported
187
+ * by the plugin itself through its `statsUrl`, so only native ones are sent
188
+ * from here.
189
+ */
190
+ async function logUpdateEvent(event, update, details) {
191
+ if (update.kind !== "native") return;
192
+ const config = getUpdaterConfig();
193
+ if (!config.apiUrl) return;
194
+ const payload = {
195
+ event,
196
+ platform: getPlatform(),
197
+ device_id: await getDeviceId(),
198
+ current_version_code: await getVersionCode(),
199
+ new_version: update.version,
200
+ new_version_code: update.versionCode,
201
+ channel: config.channel,
202
+ environment: String(config.environment),
203
+ ...details
204
+ };
205
+ try {
206
+ await postJson(`${config.apiUrl}/api/native-updates/log`, payload, config.timeoutMs);
207
+ } catch (error) {
208
+ console.warn("[capuchoo] could not record update event", error);
209
+ }
210
+ }
211
+ //#endregion
212
+ //#region src/download.service.ts
213
+ /** Cache file name prefix, derived from the app id so two flavours never collide. */
214
+ function cachePrefix() {
215
+ const { appId, appName } = getUpdaterConfig();
216
+ return `${(appId || appName).replaceAll(/[^\w.-]/g, "-")}-`;
217
+ }
218
+ function apkFileName(update) {
219
+ return `${cachePrefix()}${update.version}-${update.versionCode ?? 0}.apk`;
220
+ }
221
+ /**
222
+ * Downloads a native APK into the app cache and returns its path.
223
+ *
224
+ * The file name embeds the app id, so a staging build and a production build
225
+ * installed side by side cannot overwrite each other's download. The previous
226
+ * implementation prefixed every file with a hard-coded app name.
227
+ */
228
+ async function downloadNativeUpdate(update, onProgress) {
229
+ if (!update.downloadUrl) throw new Error("This update has no download URL");
230
+ if (!(await Network.getStatus()).connected) throw new Error("Connect to the internet to download this update");
231
+ await cleanApkCache();
232
+ const fileName = apkFileName(update);
233
+ const destination = await Filesystem.getUri({
234
+ directory: Directory.Cache,
235
+ path: fileName
236
+ });
237
+ let progressListener = null;
238
+ try {
239
+ progressListener = await FileTransfer.addListener("progress", (event) => {
240
+ if (event.url !== update.downloadUrl) return;
241
+ const percent = event.lengthComputable && event.contentLength > 0 ? Math.round(event.bytes / event.contentLength * 100) : 0;
242
+ onProgress({
243
+ loaded: event.bytes,
244
+ total: event.contentLength,
245
+ percent
246
+ });
247
+ });
248
+ return (await FileTransfer.downloadFile({
249
+ url: update.downloadUrl,
250
+ path: destination.uri,
251
+ progress: true,
252
+ connectTimeout: 6e4,
253
+ readTimeout: 3e5
254
+ })).path || destination.uri;
255
+ } finally {
256
+ await progressListener?.remove();
257
+ }
258
+ }
259
+ /** Removes this app's cached APKs. Failures are non-fatal. */
260
+ async function cleanApkCache() {
261
+ const prefix = cachePrefix();
262
+ try {
263
+ const { files } = await Filesystem.readdir({
264
+ directory: Directory.Cache,
265
+ path: ""
266
+ });
267
+ await Promise.all(files.filter((file) => file.name.startsWith(prefix) && file.name.endsWith(".apk")).map((file) => Filesystem.deleteFile({
268
+ directory: Directory.Cache,
269
+ path: file.name
270
+ })));
271
+ } catch (error) {
272
+ console.warn("[capuchoo] could not clean the APK cache", error);
273
+ }
274
+ }
275
+ //#endregion
276
+ //#region src/install.service.ts
277
+ const APK_MIME = "application/vnd.android.package-archive";
278
+ /**
279
+ * Hands a downloaded APK to the Android package installer.
280
+ *
281
+ * Requires `REQUEST_INSTALL_PACKAGES` in the manifest - the Trapeze config for
282
+ * each flavour merges it in - and the user must have allowed this app to
283
+ * install unknown apps. Both failures surface as opaque platform errors, so
284
+ * they are translated into something a user can act on.
285
+ */
286
+ async function openNativeInstaller(path) {
287
+ if (Capacitor.getPlatform() !== "android") throw new Error("Installing a native update from inside the app is only possible on Android. On iOS the update has to come from the App Store or TestFlight.");
288
+ try {
289
+ await FileOpener.openFile({
290
+ path,
291
+ mimeType: APK_MIME
292
+ });
293
+ } catch (error) {
294
+ const message = error instanceof Error ? error.message : String(error);
295
+ const { appName } = getUpdaterConfig();
296
+ if (/permission|unknown sources|REQUEST_INSTALL_PACKAGES/i.test(message)) throw new Error(`Allow ${appName} to install unknown apps in Android settings, then try again`);
297
+ if (/activity|no app/i.test(message)) throw new Error("No package installer is available on this device, so the update cannot be installed here");
298
+ throw new Error(`Could not open the Android installer: ${message}`);
299
+ }
300
+ }
301
+ //#endregion
302
+ //#region src/ota.service.ts
303
+ /**
304
+ * Thin wrapper over the OTA plugin.
305
+ *
306
+ * Downloading and applying a web bundle stays with `@capgo/capacitor-updater`:
307
+ * it owns the native bundle store, the atomic swap and the rollback. This
308
+ * module only sequences those calls correctly.
309
+ */
310
+ /**
311
+ * Confirms the current bundle booted successfully.
312
+ *
313
+ * **This must be called once, early, on every app start.** If the plugin does
314
+ * not hear it within `appReadyTimeout`, it assumes the new bundle crashed and
315
+ * rolls back to the previous one - which looks exactly like "the update did
316
+ * not install".
317
+ */
318
+ async function notifyAppReady() {
319
+ if (!isNative()) return;
320
+ try {
321
+ await CapacitorUpdater.notifyAppReady();
322
+ } catch (error) {
323
+ console.warn("[capuchoo] could not mark this bundle as ready", error);
324
+ }
325
+ }
326
+ /** The bundle currently applied, or null off-device. */
327
+ async function getCurrentBundle() {
328
+ if (!isNative()) return null;
329
+ try {
330
+ return await CapacitorUpdater.current();
331
+ } catch (error) {
332
+ console.warn("[capuchoo] could not read the current bundle", error);
333
+ return null;
334
+ }
335
+ }
336
+ /**
337
+ * Downloads an OTA bundle and applies it.
338
+ *
339
+ * `set` swaps the active bundle and reloads the WebView, so nothing after it
340
+ * runs. It is called last on purpose.
341
+ */
342
+ async function applyOtaUpdate(update) {
343
+ if (update.kind !== "ota") throw new Error("applyOtaUpdate was given a native update");
344
+ if (update.bundleId) {
345
+ await CapacitorUpdater.set({ id: update.bundleId });
346
+ return;
347
+ }
348
+ if (!update.downloadUrl) throw new Error("This update has no download URL");
349
+ const bundle = await CapacitorUpdater.download({
350
+ url: update.downloadUrl,
351
+ version: update.version,
352
+ ...update.checksum ? { checksum: update.checksum } : {},
353
+ ...update.sessionKey ? { sessionKey: update.sessionKey } : {}
354
+ });
355
+ update.bundleId = bundle.id;
356
+ await CapacitorUpdater.set({ id: bundle.id });
357
+ }
358
+ /** Discards a downloaded bundle that will not be applied. */
359
+ async function discardBundle(bundleId) {
360
+ if (!isNative()) return;
361
+ try {
362
+ await CapacitorUpdater.delete({ id: bundleId });
363
+ } catch (error) {
364
+ console.warn("[capuchoo] could not delete bundle", bundleId, error);
365
+ }
366
+ }
367
+ //#endregion
368
+ export { configureUpdater as _, openNativeInstaller as a, UpdateCheckBlockedError as c, logUpdateEvent as d, getBundleVersion as f, isNative as g, getVersionCode as h, notifyAppReady as i, UpdaterConfigError as l, getPlatform as m, discardBundle as n, cleanApkCache as o, getDeviceId as p, getCurrentBundle as r, downloadNativeUpdate as s, applyOtaUpdate as t, checkForUpdate as u, describeConfigProblems as v, getUpdaterConfig as y };
369
+
370
+ //# sourceMappingURL=ota.service-t2_hIxcy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ota.service-t2_hIxcy.js","names":[],"sources":["../src/config.ts","../src/device.ts","../src/api.service.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 Capucho 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","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 { getBundleVersion, getDeviceId, getPlatform, getVersionCode, isNative } 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 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] = await Promise.all([\n getVersionCode(),\n getBundleVersion(),\n getDeviceId(),\n ]);\n\n const request: UpdateCheckRequest = {\n appId: config.appId,\n platform: getPlatform(),\n channel: config.channel,\n defaultChannel: config.channel,\n versionCode: String(versionCode),\n versionBuild: String(versionCode),\n version_name: versionName,\n deviceId,\n isProd: config.environment === \"prod\",\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","import type { PluginListenerHandle } from \"@capacitor/core\";\nimport { FileTransfer } from \"@capacitor/file-transfer\";\nimport { Directory, Filesystem } from \"@capacitor/filesystem\";\nimport { Network } from \"@capacitor/network\";\nimport type { ResolvedUpdate } from \"@capuchoo/core\";\nimport { getUpdaterConfig } from \"./config.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 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 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 { 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 { FileOpener } from \"@capawesome-team/capacitor-file-opener\";\nimport { getUpdaterConfig } from \"./config.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 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 ADDED
@@ -0,0 +1,246 @@
1
+ import { a as DownloadProgress, r as getCurrentBundle } from "./ota.service-B3LrcCPr.js";
2
+ import { ResolvedUpdate } from "@capuchoo/core";
3
+ //#region src/vue/useUpdater.d.ts
4
+ interface UpdaterState {
5
+ checking: boolean;
6
+ downloading: boolean;
7
+ installing: boolean;
8
+ updateAvailable: boolean;
9
+ currentUpdate: ResolvedUpdate | null;
10
+ progress: DownloadProgress;
11
+ /** Local path of a downloaded APK, ready to install. */
12
+ cachedPath: string | null;
13
+ error: string | null;
14
+ /** Transient status for the current operation. */
15
+ statusMessage: string;
16
+ /** Result of the last check, shown when no update is pending. */
17
+ lastCheckMessage: string;
18
+ }
19
+ /**
20
+ * Checks for an update.
21
+ *
22
+ * @param silent suppresses console noise for background checks.
23
+ * @returns whether an update is now pending.
24
+ */
25
+ declare function check(silent?: boolean): Promise<boolean>;
26
+ /** Downloads the pending update. For native updates, install is a second step. */
27
+ declare function startDownload(): Promise<void>;
28
+ /** Hands the downloaded APK to the Android installer. */
29
+ declare function installNativeUpdate(): Promise<void>;
30
+ /**
31
+ * Wires up the updater. Call once, from the app's Capacitor bootstrap.
32
+ *
33
+ * Also calls `notifyAppReady`, without which the OTA plugin rolls the bundle
34
+ * back after `appReadyTimeout`.
35
+ */
36
+ declare function init(): Promise<void>;
37
+ declare function cleanup(): Promise<void>;
38
+ /** Dismisses a pending update. Refuses for required updates and mid-download. */
39
+ declare function dismiss(): Promise<void>;
40
+ declare function useUpdater(): {
41
+ state: Readonly<import("vue").Ref<{
42
+ readonly checking: boolean;
43
+ readonly downloading: boolean;
44
+ readonly installing: boolean;
45
+ readonly updateAvailable: boolean;
46
+ readonly currentUpdate: {
47
+ readonly kind: import("@capuchoo/core").UpdateKind;
48
+ readonly version: string;
49
+ readonly versionCode?: number | undefined;
50
+ readonly downloadUrl?: string | undefined;
51
+ readonly releaseNotes?: string | undefined;
52
+ readonly required: boolean;
53
+ readonly platform?: import("@capuchoo/core").Platform | undefined;
54
+ readonly checksum?: string | undefined;
55
+ readonly sessionKey?: string | undefined;
56
+ readonly bundleId?: string | undefined;
57
+ } | null;
58
+ readonly progress: {
59
+ readonly loaded: number;
60
+ readonly total: number;
61
+ readonly percent: number;
62
+ };
63
+ readonly cachedPath: string | null;
64
+ readonly error: string | null;
65
+ readonly statusMessage: string;
66
+ readonly lastCheckMessage: string;
67
+ }, {
68
+ readonly checking: boolean;
69
+ readonly downloading: boolean;
70
+ readonly installing: boolean;
71
+ readonly updateAvailable: boolean;
72
+ readonly currentUpdate: {
73
+ readonly kind: import("@capuchoo/core").UpdateKind;
74
+ readonly version: string;
75
+ readonly versionCode?: number | undefined;
76
+ readonly downloadUrl?: string | undefined;
77
+ readonly releaseNotes?: string | undefined;
78
+ readonly required: boolean;
79
+ readonly platform?: import("@capuchoo/core").Platform | undefined;
80
+ readonly checksum?: string | undefined;
81
+ readonly sessionKey?: string | undefined;
82
+ readonly bundleId?: string | undefined;
83
+ } | null;
84
+ readonly progress: {
85
+ readonly loaded: number;
86
+ readonly total: number;
87
+ readonly percent: number;
88
+ };
89
+ readonly cachedPath: string | null;
90
+ readonly error: string | null;
91
+ readonly statusMessage: string;
92
+ readonly lastCheckMessage: string;
93
+ }>>;
94
+ isChecking: import("vue").ComputedRef<boolean>;
95
+ isDownloading: import("vue").ComputedRef<boolean>;
96
+ isInstalling: import("vue").ComputedRef<boolean>;
97
+ updateAvailable: import("vue").ComputedRef<boolean>;
98
+ currentUpdate: import("vue").ComputedRef<{
99
+ kind: import("@capuchoo/core").UpdateKind;
100
+ version: string;
101
+ versionCode?: number | undefined;
102
+ downloadUrl?: string | undefined;
103
+ releaseNotes?: string | undefined;
104
+ required: boolean;
105
+ platform?: import("@capuchoo/core").Platform | undefined;
106
+ checksum?: string | undefined;
107
+ sessionKey?: string | undefined;
108
+ bundleId?: string | undefined;
109
+ } | null>;
110
+ progress: import("vue").ComputedRef<{
111
+ loaded: number;
112
+ total: number;
113
+ percent: number;
114
+ }>;
115
+ cachedPath: import("vue").ComputedRef<string | null>;
116
+ error: import("vue").ComputedRef<string | null>;
117
+ statusMessage: import("vue").ComputedRef<string>;
118
+ lastCheckMessage: import("vue").ComputedRef<string>;
119
+ /** True when the user may not postpone the update. */
120
+ isRequired: import("vue").ComputedRef<boolean>;
121
+ check: typeof check;
122
+ startDownload: typeof startDownload;
123
+ installNativeUpdate: typeof installNativeUpdate;
124
+ dismiss: typeof dismiss;
125
+ init: typeof init;
126
+ cleanup: typeof cleanup;
127
+ getCurrentBundle: typeof getCurrentBundle;
128
+ };
129
+ //#endregion
130
+ //#region src/vue/useUpdatePrompt.d.ts
131
+ /**
132
+ * Derives everything an update prompt needs to render, so each app only writes
133
+ * the markup for its own design system.
134
+ *
135
+ * The styled component itself stays in the app on purpose: this package would
136
+ * otherwise have to ship a Framework7 dialog to one app and something else to
137
+ * the next, and a component library is not what makes updates work. The state
138
+ * machine is the reusable part.
139
+ */
140
+ declare function useUpdatePrompt(): {
141
+ /** Whether the prompt should be on screen at all. */
142
+ visible: import("vue").ComputedRef<boolean>;
143
+ title: import("vue").ComputedRef<"" | "Update problem" | "Update required" | "Update available">;
144
+ subtitle: import("vue").ComputedRef<string>;
145
+ body: import("vue").ComputedRef<string>;
146
+ /**
147
+ * Native updates need a download step and then an install step; OTA
148
+ * updates apply themselves once downloaded.
149
+ */
150
+ primaryLabel: import("vue").ComputedRef<"Downloading..." | "Installing..." | "Install now" | "Download" | "Update now">;
151
+ primaryAction: () => Promise<void>;
152
+ /** Busy state - the primary button must be disabled. */
153
+ busy: import("vue").ComputedRef<boolean>;
154
+ /** A required update cannot be postponed, and neither can one mid-flight. */
155
+ dismissible: import("vue").ComputedRef<boolean>;
156
+ showProgress: import("vue").ComputedRef<boolean>;
157
+ state: Readonly<import("vue").Ref<{
158
+ readonly checking: boolean;
159
+ readonly downloading: boolean;
160
+ readonly installing: boolean;
161
+ readonly updateAvailable: boolean;
162
+ readonly currentUpdate: {
163
+ readonly kind: import("@capuchoo/core").UpdateKind;
164
+ readonly version: string;
165
+ readonly versionCode?: number | undefined;
166
+ readonly downloadUrl?: string | undefined;
167
+ readonly releaseNotes?: string | undefined;
168
+ readonly required: boolean;
169
+ readonly platform?: import("@capuchoo/core").Platform | undefined;
170
+ readonly checksum?: string | undefined;
171
+ readonly sessionKey?: string | undefined;
172
+ readonly bundleId?: string | undefined;
173
+ } | null;
174
+ readonly progress: {
175
+ readonly loaded: number;
176
+ readonly total: number;
177
+ readonly percent: number;
178
+ };
179
+ readonly cachedPath: string | null;
180
+ readonly error: string | null;
181
+ readonly statusMessage: string;
182
+ readonly lastCheckMessage: string;
183
+ }, {
184
+ readonly checking: boolean;
185
+ readonly downloading: boolean;
186
+ readonly installing: boolean;
187
+ readonly updateAvailable: boolean;
188
+ readonly currentUpdate: {
189
+ readonly kind: import("@capuchoo/core").UpdateKind;
190
+ readonly version: string;
191
+ readonly versionCode?: number | undefined;
192
+ readonly downloadUrl?: string | undefined;
193
+ readonly releaseNotes?: string | undefined;
194
+ readonly required: boolean;
195
+ readonly platform?: import("@capuchoo/core").Platform | undefined;
196
+ readonly checksum?: string | undefined;
197
+ readonly sessionKey?: string | undefined;
198
+ readonly bundleId?: string | undefined;
199
+ } | null;
200
+ readonly progress: {
201
+ readonly loaded: number;
202
+ readonly total: number;
203
+ readonly percent: number;
204
+ };
205
+ readonly cachedPath: string | null;
206
+ readonly error: string | null;
207
+ readonly statusMessage: string;
208
+ readonly lastCheckMessage: string;
209
+ }>>;
210
+ isChecking: import("vue").ComputedRef<boolean>;
211
+ isDownloading: import("vue").ComputedRef<boolean>;
212
+ isInstalling: import("vue").ComputedRef<boolean>;
213
+ updateAvailable: import("vue").ComputedRef<boolean>;
214
+ currentUpdate: import("vue").ComputedRef<{
215
+ kind: import("@capuchoo/core").UpdateKind;
216
+ version: string;
217
+ versionCode?: number | undefined;
218
+ downloadUrl?: string | undefined;
219
+ releaseNotes?: string | undefined;
220
+ required: boolean;
221
+ platform?: import("@capuchoo/core").Platform | undefined;
222
+ checksum?: string | undefined;
223
+ sessionKey?: string | undefined;
224
+ bundleId?: string | undefined;
225
+ } | null>;
226
+ progress: import("vue").ComputedRef<{
227
+ loaded: number;
228
+ total: number;
229
+ percent: number;
230
+ }>;
231
+ cachedPath: import("vue").ComputedRef<string | null>;
232
+ error: import("vue").ComputedRef<string | null>;
233
+ statusMessage: import("vue").ComputedRef<string>;
234
+ lastCheckMessage: import("vue").ComputedRef<string>;
235
+ isRequired: import("vue").ComputedRef<boolean>;
236
+ check: (silent?: boolean) => Promise<boolean>;
237
+ startDownload: () => Promise<void>;
238
+ installNativeUpdate: () => Promise<void>;
239
+ dismiss: () => Promise<void>;
240
+ init: () => Promise<void>;
241
+ cleanup: () => Promise<void>;
242
+ getCurrentBundle: typeof getCurrentBundle;
243
+ };
244
+ //#endregion
245
+ export { type UpdaterState, useUpdatePrompt, useUpdater };
246
+ //# sourceMappingURL=vue.d.ts.map
@@ -0,0 +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"}