@forthtilliath/expo-release-updates-ui 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/dist/UpdateInstallCard.d.ts +32 -0
- package/dist/UpdateInstallCard.d.ts.map +1 -0
- package/dist/UpdateInstallCard.js +62 -0
- package/dist/UpdateNotifier.d.ts +27 -0
- package/dist/UpdateNotifier.d.ts.map +1 -0
- package/dist/UpdateNotifier.js +45 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/useApkInstall.d.ts +20 -0
- package/dist/useApkInstall.d.ts.map +1 -0
- package/dist/useApkInstall.js +27 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -131,6 +131,42 @@ import { UpdateAvailableBanner } from "@forthtilliath/expo-release-updates-ui";
|
|
|
131
131
|
|
|
132
132
|
Built-in labels are French by default; pass `locale="en"` for English (e.g. from the app's language on a bilingual app). `labels` still overrides individual strings — `title`, `action`, `dismiss`, `dismissAccessibilityLabel` — whatever the locale.
|
|
133
133
|
|
|
134
|
+
### `<UpdateNotifier ... />` — the whole flow, install included
|
|
135
|
+
|
|
136
|
+
Mount it once near the root: it checks on launch (`useUpdateCheck`), shows the banner with **Install / Ignore**, and "Install" downloads the APK right away with a progress card — Android then asks for confirmation — instead of sending the user to an update screen. A failed download offers Retry / Close. "Ignore" calls `onDismissVersion` so that exact version is never announced again (a newer one will be).
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
import { UpdateNotifier } from "@forthtilliath/expo-release-updates-ui/UpdateNotifier";
|
|
140
|
+
|
|
141
|
+
<View
|
|
142
|
+
style={{ position: "absolute", top: 56, left: 16, right: 16, zIndex: 10 }}
|
|
143
|
+
>
|
|
144
|
+
<UpdateNotifier
|
|
145
|
+
enabled={settings !== undefined}
|
|
146
|
+
currentVersion={Constants.expoConfig?.version ?? "0.0.0"}
|
|
147
|
+
checkForUpdate={fetchLatestRelease}
|
|
148
|
+
getLastCheck={() => ({
|
|
149
|
+
lastCheckedAt: settings?.lastUpdateCheckAt ?? null,
|
|
150
|
+
dismissedVersion: settings?.dismissedUpdateVersion ?? null,
|
|
151
|
+
})}
|
|
152
|
+
onChecked={(at) => recordUpdateCheck(at)}
|
|
153
|
+
onDismissVersion={(version) => dismissUpdateVersion(version)}
|
|
154
|
+
downloadAndInstallApk={(apkUrl, onProgress) =>
|
|
155
|
+
downloadAndInstallApk({
|
|
156
|
+
apkUrl,
|
|
157
|
+
fileName: "myapp-update.apk",
|
|
158
|
+
onProgress,
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
styles={{ banner: { container: { backgroundColor: colors.surface } } }}
|
|
162
|
+
/>
|
|
163
|
+
</View>;
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Pass **`enabled={false}` until the settings `getLastCheck` reads have loaded** (async database, live query…): the check runs once, on mount, so starting earlier would ignore both the 12h throttle and the dismissed version. Positioning is left to the parent. `labels` / `styles` take `{ banner, install }`, same keys as `UpdateAvailableBanner` and `UpdateInstallCard`.
|
|
167
|
+
|
|
168
|
+
The building blocks are exported too, to compose a different flow: `useApkInstall(downloadAndInstallApk)` (`{ state, start(version, apkUrl), retry, reset }`) and `<UpdateInstallCard state onRetry onClose />` (renders nothing while idle).
|
|
169
|
+
|
|
134
170
|
## Scripts
|
|
135
171
|
|
|
136
172
|
```bash
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { StyleProp, TextStyle, ViewStyle } from "react-native";
|
|
2
|
+
import type { UpdateBannerLocale } from "./UpdateAvailableBanner.js";
|
|
3
|
+
import type { ApkInstallState } from "./useApkInstall.js";
|
|
4
|
+
export interface UpdateInstallCardStyles {
|
|
5
|
+
container?: StyleProp<ViewStyle>;
|
|
6
|
+
title?: StyleProp<TextStyle>;
|
|
7
|
+
hint?: StyleProp<TextStyle>;
|
|
8
|
+
error?: StyleProp<TextStyle>;
|
|
9
|
+
actions?: StyleProp<ViewStyle>;
|
|
10
|
+
retryButton?: StyleProp<ViewStyle>;
|
|
11
|
+
retryButtonText?: StyleProp<TextStyle>;
|
|
12
|
+
closeButton?: StyleProp<ViewStyle>;
|
|
13
|
+
closeButtonText?: StyleProp<TextStyle>;
|
|
14
|
+
}
|
|
15
|
+
export interface UpdateInstallCardLabels {
|
|
16
|
+
downloading?: (version: string, percent: number) => string;
|
|
17
|
+
hint?: string;
|
|
18
|
+
error?: (version: string) => string;
|
|
19
|
+
retry?: string;
|
|
20
|
+
close?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface UpdateInstallCardProps {
|
|
23
|
+
state: ApkInstallState;
|
|
24
|
+
onRetry: () => void;
|
|
25
|
+
onClose: () => void;
|
|
26
|
+
labels?: UpdateInstallCardLabels;
|
|
27
|
+
/** @defaultValue "fr" */
|
|
28
|
+
locale?: UpdateBannerLocale;
|
|
29
|
+
styles?: UpdateInstallCardStyles;
|
|
30
|
+
}
|
|
31
|
+
export declare function UpdateInstallCard({ state, onRetry, onClose, labels, locale, styles, }: UpdateInstallCardProps): import("react").JSX.Element | null;
|
|
32
|
+
//# sourceMappingURL=UpdateInstallCard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UpdateInstallCard.d.ts","sourceRoot":"","sources":["../src/UpdateInstallCard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAGpE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAE1D,MAAM,WAAW,uBAAuB;IACtC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,IAAI,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC5B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,OAAO,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC/B,WAAW,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACnC,eAAe,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACvC,WAAW,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACnC,eAAe,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,uBAAuB;IACtC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,eAAe,CAAC;IACvB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,yBAAyB;IACzB,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,MAAM,CAAC,EAAE,uBAAuB,CAAC;CAClC;AAwDD,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,OAAO,EACP,OAAO,EACP,MAAM,EACN,MAAa,EACb,MAAM,GACP,EAAE,sBAAsB,sCAwCxB"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Pressable, Text, View } from "react-native";
|
|
3
|
+
const defaultStyles = {
|
|
4
|
+
container: {
|
|
5
|
+
backgroundColor: "#ffffff",
|
|
6
|
+
borderWidth: 1,
|
|
7
|
+
borderColor: "#e5e7eb",
|
|
8
|
+
borderRadius: 10,
|
|
9
|
+
padding: 14,
|
|
10
|
+
gap: 6,
|
|
11
|
+
},
|
|
12
|
+
title: { fontSize: 15, fontWeight: "700", color: "#111827" },
|
|
13
|
+
hint: { fontSize: 12, color: "#6b7280" },
|
|
14
|
+
error: { fontSize: 14, color: "#dc2626" },
|
|
15
|
+
actions: {
|
|
16
|
+
flexDirection: "row",
|
|
17
|
+
justifyContent: "flex-end",
|
|
18
|
+
gap: 8,
|
|
19
|
+
marginTop: 4,
|
|
20
|
+
},
|
|
21
|
+
retryButton: {
|
|
22
|
+
backgroundColor: "#2563eb",
|
|
23
|
+
borderRadius: 8,
|
|
24
|
+
paddingVertical: 8,
|
|
25
|
+
paddingHorizontal: 14,
|
|
26
|
+
},
|
|
27
|
+
retryButtonText: { color: "#ffffff", fontWeight: "700" },
|
|
28
|
+
closeButton: { paddingVertical: 8, paddingHorizontal: 14 },
|
|
29
|
+
closeButtonText: { color: "#6b7280" },
|
|
30
|
+
};
|
|
31
|
+
const defaultLabels = {
|
|
32
|
+
fr: {
|
|
33
|
+
downloading: (version, percent) => `Téléchargement de la version ${version}… ${percent}%`,
|
|
34
|
+
hint: "Le téléphone va ensuite demander confirmation pour l'installer.",
|
|
35
|
+
error: (version) => `Le téléchargement de la version ${version} a échoué.`,
|
|
36
|
+
retry: "Réessayer",
|
|
37
|
+
close: "Fermer",
|
|
38
|
+
},
|
|
39
|
+
en: {
|
|
40
|
+
downloading: (version, percent) => `Downloading version ${version}… ${percent}%`,
|
|
41
|
+
hint: "Your phone will then ask you to confirm the installation.",
|
|
42
|
+
error: (version) => `Downloading version ${version} failed.`,
|
|
43
|
+
retry: "Retry",
|
|
44
|
+
close: "Close",
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
// Progress of an update started from the UI (see `useApkInstall`): a
|
|
48
|
+
// non-interactive card while the APK downloads, then — only on failure — an
|
|
49
|
+
// error with Retry/Close. Renders nothing while idle.
|
|
50
|
+
export function UpdateInstallCard({ state, onRetry, onClose, labels, locale = "fr", styles, }) {
|
|
51
|
+
if (state.status === "idle")
|
|
52
|
+
return null;
|
|
53
|
+
const t = { ...defaultLabels[locale], ...labels };
|
|
54
|
+
const s = (key) => [
|
|
55
|
+
defaultStyles[key],
|
|
56
|
+
styles?.[key],
|
|
57
|
+
];
|
|
58
|
+
if (state.status === "downloading") {
|
|
59
|
+
return (_jsxs(View, { style: s("container"), pointerEvents: "none", children: [_jsx(Text, { style: s("title"), children: t.downloading(state.version, Math.round(state.progress * 100)) }), _jsx(Text, { style: s("hint"), children: t.hint })] }));
|
|
60
|
+
}
|
|
61
|
+
return (_jsxs(View, { style: s("container"), children: [_jsx(Text, { style: s("error"), children: t.error(state.version) }), _jsxs(View, { style: s("actions"), children: [_jsx(Pressable, { style: s("closeButton"), onPress: onClose, accessibilityRole: "button", children: _jsx(Text, { style: s("closeButtonText"), children: t.close }) }), _jsx(Pressable, { style: s("retryButton"), onPress: onRetry, accessibilityRole: "button", children: _jsx(Text, { style: s("retryButtonText"), children: t.retry }) })] })] }));
|
|
62
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { UpdateAvailableBannerLabels, UpdateAvailableBannerStyles, UpdateBannerLocale } from "./UpdateAvailableBanner.js";
|
|
2
|
+
import type { UpdateInstallCardLabels, UpdateInstallCardStyles } from "./UpdateInstallCard.js";
|
|
3
|
+
import type { DownloadAndInstallApk } from "./useApkInstall.js";
|
|
4
|
+
import type { UseUpdateCheckOptions } from "./useUpdateCheck.js";
|
|
5
|
+
export interface UpdateNotifierProps extends UseUpdateCheckOptions {
|
|
6
|
+
/**
|
|
7
|
+
* `false` until what `getLastCheck` reads has loaded (settings from an async
|
|
8
|
+
* database...): the check runs once, on mount, so starting it earlier would
|
|
9
|
+
* ignore both the throttle and the dismissed version. Default: `true`.
|
|
10
|
+
*/
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
downloadAndInstallApk: DownloadAndInstallApk;
|
|
13
|
+
/** "Ignore": persist it so this exact version is never announced again. */
|
|
14
|
+
onDismissVersion: (version: string) => void;
|
|
15
|
+
/** @defaultValue "fr" */
|
|
16
|
+
locale?: UpdateBannerLocale;
|
|
17
|
+
labels?: {
|
|
18
|
+
banner?: UpdateAvailableBannerLabels;
|
|
19
|
+
install?: UpdateInstallCardLabels;
|
|
20
|
+
};
|
|
21
|
+
styles?: {
|
|
22
|
+
banner?: UpdateAvailableBannerStyles;
|
|
23
|
+
install?: UpdateInstallCardStyles;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export declare function UpdateNotifier({ enabled, ...props }: UpdateNotifierProps): import("react").JSX.Element | null;
|
|
27
|
+
//# sourceMappingURL=UpdateNotifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UpdateNotifier.d.ts","sourceRoot":"","sources":["../src/UpdateNotifier.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,2BAA2B,EAC3B,2BAA2B,EAC3B,kBAAkB,EACnB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EACV,uBAAuB,EACvB,uBAAuB,EACxB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAEhE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAGjE,MAAM,WAAW,mBAAoB,SAAQ,qBAAqB;IAChE;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qBAAqB,EAAE,qBAAqB,CAAC;IAC7C,2EAA2E;IAC3E,gBAAgB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,yBAAyB;IACzB,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,MAAM,CAAC,EAAE;QACP,MAAM,CAAC,EAAE,2BAA2B,CAAC;QACrC,OAAO,CAAC,EAAE,uBAAuB,CAAC;KACnC,CAAC;IACF,MAAM,CAAC,EAAE;QACP,MAAM,CAAC,EAAE,2BAA2B,CAAC;QACrC,OAAO,CAAC,EAAE,uBAAuB,CAAC;KACnC,CAAC;CACH;AAqBD,wBAAgB,cAAc,CAAC,EAC7B,OAAc,EACd,GAAG,KAAK,EACT,EAAE,mBAAmB,sCAErB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { UpdateAvailableBanner } from "./UpdateAvailableBanner.js";
|
|
3
|
+
import { UpdateInstallCard } from "./UpdateInstallCard.js";
|
|
4
|
+
import { useApkInstall } from "./useApkInstall.js";
|
|
5
|
+
import { useUpdateCheck } from "./useUpdateCheck.js";
|
|
6
|
+
const bannerLabels = {
|
|
7
|
+
fr: { action: "Installer", dismiss: "Ignorer" },
|
|
8
|
+
en: { action: "Install", dismiss: "Ignore" },
|
|
9
|
+
};
|
|
10
|
+
const dismissAccessibilityLabel = {
|
|
11
|
+
fr: (version) => `Ignorer la version ${version}`,
|
|
12
|
+
en: (version) => `Ignore version ${version}`,
|
|
13
|
+
};
|
|
14
|
+
// Whole update flow for a sideloaded app, to mount once near the root: checks
|
|
15
|
+
// for a new release on launch (`useUpdateCheck`), announces it, and
|
|
16
|
+
// "Install" downloads the APK right away with a progress card (Android then
|
|
17
|
+
// asks for confirmation) — no detour through an update screen. "Ignore" never
|
|
18
|
+
// announces that version again, but a newer one will be. Position it
|
|
19
|
+
// (absolute, safe area...) from the parent.
|
|
20
|
+
export function UpdateNotifier({ enabled = true, ...props }) {
|
|
21
|
+
return enabled ? _jsx(ActiveUpdateNotifier, { ...props }) : null;
|
|
22
|
+
}
|
|
23
|
+
function ActiveUpdateNotifier({ downloadAndInstallApk, onDismissVersion, locale = "fr", labels, styles, ...checkOptions }) {
|
|
24
|
+
const update = useUpdateCheck(checkOptions);
|
|
25
|
+
const install = useApkInstall(downloadAndInstallApk);
|
|
26
|
+
if (install.state.status !== "idle") {
|
|
27
|
+
return (_jsx(UpdateInstallCard, { state: install.state, onRetry: install.retry, onClose: install.reset, locale: locale, labels: labels?.install, styles: styles?.install }));
|
|
28
|
+
}
|
|
29
|
+
if (update.status !== "available")
|
|
30
|
+
return null;
|
|
31
|
+
const { release } = update;
|
|
32
|
+
return (_jsx(UpdateAvailableBanner, { version: release.version, notes: release.notes, locale: locale, labels: {
|
|
33
|
+
...bannerLabels[locale],
|
|
34
|
+
dismissAccessibilityLabel: dismissAccessibilityLabel[locale](release.version),
|
|
35
|
+
...labels?.banner,
|
|
36
|
+
}, styles: styles?.banner, onPress: () => {
|
|
37
|
+
// Not persisted as dismissed: if the install doesn't go through, the
|
|
38
|
+
// banner may come back on a later launch (unlike "Ignore").
|
|
39
|
+
update.dismiss();
|
|
40
|
+
install.start(release.version, release.apkUrl);
|
|
41
|
+
}, onDismiss: () => {
|
|
42
|
+
onDismissVersion(release.version);
|
|
43
|
+
update.dismiss();
|
|
44
|
+
} }));
|
|
45
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export * from "./ChangelogNotes.js";
|
|
2
2
|
export * from "./UpdateAvailableBanner.js";
|
|
3
|
+
export * from "./UpdateInstallCard.js";
|
|
4
|
+
export * from "./UpdateNotifier.js";
|
|
5
|
+
export * from "./useApkInstall.js";
|
|
3
6
|
export * from "./useUpdateCheck.js";
|
|
4
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Downloads an APK then hands it to Android's installer, reporting progress from 0 to 1. */
|
|
2
|
+
export type DownloadAndInstallApk = (apkUrl: string, onProgress?: (fraction: number) => void) => Promise<void>;
|
|
3
|
+
export type ApkInstallState = {
|
|
4
|
+
status: "idle";
|
|
5
|
+
} | {
|
|
6
|
+
status: "downloading";
|
|
7
|
+
version: string;
|
|
8
|
+
progress: number;
|
|
9
|
+
} | {
|
|
10
|
+
status: "error";
|
|
11
|
+
version: string;
|
|
12
|
+
apkUrl: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function useApkInstall(downloadAndInstallApk: DownloadAndInstallApk): {
|
|
15
|
+
state: ApkInstallState;
|
|
16
|
+
start: (version: string, apkUrl: string) => void;
|
|
17
|
+
retry: () => void;
|
|
18
|
+
reset: () => void;
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=useApkInstall.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useApkInstall.d.ts","sourceRoot":"","sources":["../src/useApkInstall.ts"],"names":[],"mappings":"AAEA,6FAA6F;AAC7F,MAAM,MAAM,qBAAqB,GAAG,CAClC,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,KACpC,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB,MAAM,MAAM,eAAe,GACvB;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC5D;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAKzD,wBAAgB,aAAa,CAAC,qBAAqB,EAAE,qBAAqB;;qBAGhD,MAAM,UAAU,MAAM;;;EAsB/C"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
// Tracks one APK download/install started from the UI: progress while it
|
|
3
|
+
// downloads, back to idle once Android's installer has taken over, an error
|
|
4
|
+
// state (keeping the URL) so the user can retry.
|
|
5
|
+
export function useApkInstall(downloadAndInstallApk) {
|
|
6
|
+
const [state, setState] = useState({ status: "idle" });
|
|
7
|
+
function start(version, apkUrl) {
|
|
8
|
+
setState({ status: "downloading", version, progress: 0 });
|
|
9
|
+
downloadAndInstallApk(apkUrl, (progress) => {
|
|
10
|
+
setState({ status: "downloading", version, progress });
|
|
11
|
+
})
|
|
12
|
+
.then(() => {
|
|
13
|
+
setState({ status: "idle" });
|
|
14
|
+
})
|
|
15
|
+
.catch(() => {
|
|
16
|
+
setState({ status: "error", version, apkUrl });
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function retry() {
|
|
20
|
+
if (state.status === "error")
|
|
21
|
+
start(state.version, state.apkUrl);
|
|
22
|
+
}
|
|
23
|
+
function reset() {
|
|
24
|
+
setState({ status: "idle" });
|
|
25
|
+
}
|
|
26
|
+
return { state, start, retry, reset };
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forthtilliath/expo-release-updates-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"./*": "./dist/*.js"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@forthtilliath/expo-release-updates": "^0.
|
|
25
|
+
"@forthtilliath/expo-release-updates": "^0.7.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"react": ">=19",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"react-test-renderer": "19.2.3",
|
|
38
38
|
"typescript": "^5.9.3",
|
|
39
39
|
"vitest": "^5.0.2",
|
|
40
|
-
"@forthtilliath/
|
|
41
|
-
"@forthtilliath/
|
|
40
|
+
"@forthtilliath/eslint-config": "0.9.0",
|
|
41
|
+
"@forthtilliath/typescript-config": "0.1.1"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"lint": "eslint . --max-warnings 0",
|