@rific/updater 0.3.0 → 0.3.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @rific/updater
2
2
 
3
- OTA update hook for Expo apps. Checks for updates in the background when the app is foregrounded and prompts to restart as soon as one's found, and exposes a manual `check()` for settings screens. No surprise restarts — the user always confirms before the app reloads.
3
+ OTA update hook for Expo apps. Checks for updates on launch and again whenever the app is foregrounded, prompting to restart as soon as one's found, and exposes a manual `check()` for settings screens. No surprise restarts — the user always confirms before the app reloads.
4
4
 
5
5
  ---
6
6
 
@@ -55,13 +55,13 @@ const { check, checking } = useUpdater({
55
55
  })
56
56
  ```
57
57
 
58
- ### Disable automatic foreground check entirely
58
+ ### Disable automatic mount/foreground checks entirely
59
59
 
60
60
  ```tsx
61
61
  const { check, checking } = useUpdater({ autoCheck: false })
62
62
  ```
63
63
 
64
- Fully manual — no `AppState` listener at all, `check()` always fetches fresh. `autoPrompt` is irrelevant here.
64
+ Fully manual — no mount-time fetch, no `AppState` listener, `check()` always fetches fresh. `autoPrompt` is irrelevant here.
65
65
 
66
66
  ### Silent background staging only (no auto-prompt)
67
67
 
@@ -69,7 +69,7 @@ Fully manual — no `AppState` listener at all, `check()` always fetches fresh.
69
69
  const { check, checking, updateReady } = useUpdater({ autoPrompt: false })
70
70
  ```
71
71
 
72
- Foreground fetches still run and stage the update (`updateReady` flips `true`), but the confirm dialog only shows up via a manual `check()` — same as the settings-screen example above. Good for games or anything where you don't want a dialog interrupting the user; the staged bundle still applies on the next cold launch even if `check()` is never called.
72
+ Mount and foreground fetches still run and stage the update (`updateReady` flips `true`), but the confirm dialog only shows up via a manual `check()` — same as the settings-screen example above. Good for games or anything where you don't want a dialog interrupting the user.
73
73
 
74
74
  ---
75
75
 
@@ -94,8 +94,8 @@ interface UseUpdaterReturn {
94
94
 
95
95
  | Option | Default | Description |
96
96
  |--------|---------|-------------|
97
- | `autoCheck` | `true` | Registers an `AppState` listener that fetches available updates whenever the app comes to the foreground. Disable for apps that want full manual control. |
98
- | `autoPrompt` | `true` | When a foreground `autoCheck` fetch finds an update, run the confirmation dialog (and reload on confirm) immediately. Set `false` to fall back to the old behavior — silently stage it for a manual `check()` or the next cold launch instead. Ignored if `autoCheck` is `false`. A manual `check()` call and an auto-prompt won't run concurrently — whichever is in flight blocks the other. |
97
+ | `autoCheck` | `true` | Fetches available updates once on mount and again via an `AppState` listener whenever the app comes to the foreground. Disable for apps that want full manual control. |
98
+ | `autoPrompt` | `true` | When a mount or foreground `autoCheck` fetch finds an update, run the confirmation dialog (and reload on confirm) immediately. Set `false` to fall back to the old behavior — silently stage it for a manual `check()` or the next cold launch instead. Ignored if `autoCheck` is `false`. A manual `check()` call and an auto-prompt won't run concurrently — whichever is in flight blocks the other. |
99
99
  | `onConfirm` | — | Custom confirmation dialog. Receives the update manifest, must return `Promise<boolean>` — `true` to reload, `false` to cancel. Defaults to a native `Alert` showing the release date and metadata message. |
100
100
  | `onError` | — | Called with an error message string if `check()` throws. Defaults to `Alert.alert`. |
101
101
 
@@ -109,7 +109,7 @@ interface UseUpdaterReturn {
109
109
 
110
110
  ## How updates work
111
111
 
112
- **Automatic (foreground):** When `autoCheck` is `true`, the hook registers an `AppState` listener. Each time the app returns from background/inactive to active, it calls `checkForUpdateAsync()` + `fetchUpdateAsync()`. By default (`autoPrompt: true`) a found update goes straight into the confirmation dialog and `reloadAsync()` on confirm — no tap required. With `autoPrompt: false`, the downloaded bundle just sits on disk instead — no prompt, no restart — until a manual `check()` or the **next cold launch**, which automatically runs it.
112
+ **Automatic (mount + foreground):** When `autoCheck` is `true`, the hook fetches once on mount (covering cold launch) and also registers an `AppState` listener that re-fetches each time the app returns from background/inactive to active. Both paths call `checkForUpdateAsync()` + `fetchUpdateAsync()` and share the same confirm/reload flow. By default (`autoPrompt: true`) a found update goes straight into the confirmation dialog and `reloadAsync()` on confirm — no tap required. With `autoPrompt: false`, the downloaded bundle just sits on disk instead — no prompt, no restart — until a manual `check()` is called.
113
113
 
114
114
  **Manual (`check()`):** Runs the full flow — check (or reuse staged manifest) → confirmation dialog → `reloadAsync()`. The user sees what was released and chooses whether to restart now.
115
115
 
@@ -165,11 +165,13 @@ The path argument defaults to `src/constants/release.ts` if omitted.
165
165
  ## Consuming apps
166
166
 
167
167
  > **0.3.0 changed the default:** `autoPrompt` now defaults to `true`, so a bare `useUpdater()` prompts on its own the moment a foreground fetch finds something — it no longer just stages silently for next launch. Every app below was written against the old silent-by-default behavior; pass `autoPrompt: false` explicitly if that's still what you want (this is what Lumber's and CashierFu-Utility's manual-check hooks already do via `autoCheck: false`, so they're unaffected — it's the bare root-layout `useUpdater()` calls and the games that actually change behavior on upgrade).
168
+ >
169
+ > **Next release adds a mount-time check:** `autoCheck` now also fetches once on mount, in addition to the existing foreground `AppState` listener — covering cold launch, which previously only got an update via native `expo-updates` (`checkAutomatically`), silently and outside this hook's confirm/reload flow. Any app with a bare root-layout `useUpdater()` (default `autoPrompt: true`) will now show the confirm dialog on cold launch too, not just on foreground return.
168
170
 
169
171
  - **Lumber** (`../Lumber`) — account screen, shows version + update badge. Root layout's bare `useUpdater()` will start auto-prompting on upgrade unless changed.
170
172
  - **CashierFu-Utility** (`../CashierFu-Utility`) — settings modal, uses `@rific/toaster` for `onError`. Same root-layout caveat as Lumber.
171
173
  - **Swirlio** (`../Swirlio`) — top sheet; now just relies on the `autoPrompt` default rather than passing it explicitly.
172
- - Games (Setter, Hangman, Crumby, HexFleet, etc.) — call `useUpdater()` with no options, relying on the old silent-only default. Will start prompting on foreground return (not during active play — the listener only fires on a background→active transition) unless given `autoPrompt: false`.
174
+ - Games (Setter, Hangman, Crumby, HexFleet, etc.) — call `useUpdater()` with no options, relying on the old silent-only default. Will start prompting on cold launch and on foreground return (not during active play — the listener only fires on a background→active transition) unless given `autoPrompt: false`.
173
175
 
174
176
  ### Local development (yalc)
175
177
 
package/dist/index.js CHANGED
@@ -70,31 +70,35 @@ var useUpdater = (options = {}) => {
70
70
  onErrorRef.current = onError;
71
71
  (0, import_react.useEffect)(() => {
72
72
  if (!autoCheck || isUnsupported()) return;
73
+ const runAutoCheck = () => {
74
+ checkForUpdate().then(async (manifest) => {
75
+ if (!manifest) return;
76
+ stagedManifest.current = manifest;
77
+ setUpdateReady(true);
78
+ if (!autoPrompt || checkingRef.current) return;
79
+ checkingRef.current = true;
80
+ setChecking(true);
81
+ try {
82
+ const confirmFn = onConfirmRef.current ?? getUpdateConfirmation;
83
+ const confirmed = await confirmFn(manifest);
84
+ if (confirmed) await (0, import_expo_updates2.reloadAsync)();
85
+ } catch (err) {
86
+ const message = err instanceof Error ? err.message : "Could not check for updates.";
87
+ if (onErrorRef.current) onErrorRef.current(message);
88
+ else import_react_native2.Alert.alert("Update error", message);
89
+ } finally {
90
+ stagedManifest.current = null;
91
+ setUpdateReady(false);
92
+ checkingRef.current = false;
93
+ setChecking(false);
94
+ }
95
+ }).catch(() => {
96
+ });
97
+ };
98
+ runAutoCheck();
73
99
  const subscription = import_react_native2.AppState.addEventListener("change", (nextState) => {
74
100
  if (/inactive|background/.test(appState.current) && nextState === "active") {
75
- checkForUpdate().then(async (manifest) => {
76
- if (!manifest) return;
77
- stagedManifest.current = manifest;
78
- setUpdateReady(true);
79
- if (!autoPrompt || checkingRef.current) return;
80
- checkingRef.current = true;
81
- setChecking(true);
82
- try {
83
- const confirmFn = onConfirmRef.current ?? getUpdateConfirmation;
84
- const confirmed = await confirmFn(manifest);
85
- if (confirmed) await (0, import_expo_updates2.reloadAsync)();
86
- } catch (err) {
87
- const message = err instanceof Error ? err.message : "Could not check for updates.";
88
- if (onErrorRef.current) onErrorRef.current(message);
89
- else import_react_native2.Alert.alert("Update error", message);
90
- } finally {
91
- stagedManifest.current = null;
92
- setUpdateReady(false);
93
- checkingRef.current = false;
94
- setChecking(false);
95
- }
96
- }).catch(() => {
97
- });
101
+ runAutoCheck();
98
102
  }
99
103
  appState.current = nextState;
100
104
  });
package/dist/index.mjs CHANGED
@@ -44,31 +44,35 @@ var useUpdater = (options = {}) => {
44
44
  onErrorRef.current = onError;
45
45
  useEffect(() => {
46
46
  if (!autoCheck || isUnsupported()) return;
47
+ const runAutoCheck = () => {
48
+ checkForUpdate().then(async (manifest) => {
49
+ if (!manifest) return;
50
+ stagedManifest.current = manifest;
51
+ setUpdateReady(true);
52
+ if (!autoPrompt || checkingRef.current) return;
53
+ checkingRef.current = true;
54
+ setChecking(true);
55
+ try {
56
+ const confirmFn = onConfirmRef.current ?? getUpdateConfirmation;
57
+ const confirmed = await confirmFn(manifest);
58
+ if (confirmed) await reloadAsync();
59
+ } catch (err) {
60
+ const message = err instanceof Error ? err.message : "Could not check for updates.";
61
+ if (onErrorRef.current) onErrorRef.current(message);
62
+ else Alert2.alert("Update error", message);
63
+ } finally {
64
+ stagedManifest.current = null;
65
+ setUpdateReady(false);
66
+ checkingRef.current = false;
67
+ setChecking(false);
68
+ }
69
+ }).catch(() => {
70
+ });
71
+ };
72
+ runAutoCheck();
47
73
  const subscription = AppState.addEventListener("change", (nextState) => {
48
74
  if (/inactive|background/.test(appState.current) && nextState === "active") {
49
- checkForUpdate().then(async (manifest) => {
50
- if (!manifest) return;
51
- stagedManifest.current = manifest;
52
- setUpdateReady(true);
53
- if (!autoPrompt || checkingRef.current) return;
54
- checkingRef.current = true;
55
- setChecking(true);
56
- try {
57
- const confirmFn = onConfirmRef.current ?? getUpdateConfirmation;
58
- const confirmed = await confirmFn(manifest);
59
- if (confirmed) await reloadAsync();
60
- } catch (err) {
61
- const message = err instanceof Error ? err.message : "Could not check for updates.";
62
- if (onErrorRef.current) onErrorRef.current(message);
63
- else Alert2.alert("Update error", message);
64
- } finally {
65
- stagedManifest.current = null;
66
- setUpdateReady(false);
67
- checkingRef.current = false;
68
- setChecking(false);
69
- }
70
- }).catch(() => {
71
- });
75
+ runAutoCheck();
72
76
  }
73
77
  appState.current = nextState;
74
78
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rific/updater",
3
- "version": "0.3.0",
4
- "description": "OTA update hook for Expo apps silent background fetch on foreground, manual check with confirmation dialog",
3
+ "version": "0.3.2",
4
+ "description": "OTA update hook for Expo apps. Silent background fetch on foreground, manual check with confirmation dialog",
5
5
  "keywords": [
6
6
  "expo",
7
7
  "expo-updates",
File without changes
package/src/useUpdater.ts CHANGED
@@ -38,33 +38,41 @@ export const useUpdater = (options: UseUpdaterOptions = {}): UseUpdaterReturn =>
38
38
  useEffect(() => {
39
39
  if (!autoCheck || isUnsupported()) return
40
40
 
41
+ const runAutoCheck = () => {
42
+ checkForUpdate()
43
+ .then(async (manifest) => {
44
+ if (!manifest) return
45
+ stagedManifest.current = manifest
46
+ setUpdateReady(true)
47
+ if (!autoPrompt || checkingRef.current) return
48
+
49
+ checkingRef.current = true
50
+ setChecking(true)
51
+ try {
52
+ const confirmFn = onConfirmRef.current ?? getUpdateConfirmation
53
+ const confirmed = await confirmFn(manifest)
54
+ if (confirmed) await reloadAsync()
55
+ } catch (err) {
56
+ const message = err instanceof Error ? err.message : 'Could not check for updates.'
57
+ if (onErrorRef.current) onErrorRef.current(message)
58
+ else Alert.alert('Update error', message)
59
+ } finally {
60
+ stagedManifest.current = null
61
+ setUpdateReady(false)
62
+ checkingRef.current = false
63
+ setChecking(false)
64
+ }
65
+ })
66
+ .catch(() => {})
67
+ }
68
+
69
+ // Cold launch: run the same check+prompt flow as a foreground resume so update
70
+ // discovery is consistent regardless of how the app was started.
71
+ runAutoCheck()
72
+
41
73
  const subscription = AppState.addEventListener('change', (nextState: AppStateStatus) => {
42
74
  if (/inactive|background/.test(appState.current) && nextState === 'active') {
43
- checkForUpdate()
44
- .then(async (manifest) => {
45
- if (!manifest) return
46
- stagedManifest.current = manifest
47
- setUpdateReady(true)
48
- if (!autoPrompt || checkingRef.current) return
49
-
50
- checkingRef.current = true
51
- setChecking(true)
52
- try {
53
- const confirmFn = onConfirmRef.current ?? getUpdateConfirmation
54
- const confirmed = await confirmFn(manifest)
55
- if (confirmed) await reloadAsync()
56
- } catch (err) {
57
- const message = err instanceof Error ? err.message : 'Could not check for updates.'
58
- if (onErrorRef.current) onErrorRef.current(message)
59
- else Alert.alert('Update error', message)
60
- } finally {
61
- stagedManifest.current = null
62
- setUpdateReady(false)
63
- checkingRef.current = false
64
- setChecking(false)
65
- }
66
- })
67
- .catch(() => {})
75
+ runAutoCheck()
68
76
  }
69
77
  appState.current = nextState
70
78
  })