@rific/updater 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +21 -10
- package/dist/index.mjs +21 -10
- package/package.json +2 -1
- package/src/getUpdateConfirmation.ts +1 -3
- package/src/types.ts +0 -1
- package/src/useUpdater.ts +30 -6
package/README.md
CHANGED
|
@@ -55,6 +55,16 @@ const { check, checking } = useUpdater({
|
|
|
55
55
|
})
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
### With custom info alerts
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
const { check, checking } = useUpdater({
|
|
62
|
+
onInfo: (title, message) => myInfoDialog(title, message),
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Covers `check()`'s three purely-informational cases — dev-mode disabled, web unsupported, and "you're already up to date" — none of which need a confirm/cancel choice, just something to acknowledge. Defaults to `Alert.alert` like `onError`.
|
|
67
|
+
|
|
58
68
|
### Disable automatic mount/foreground checks entirely
|
|
59
69
|
|
|
60
70
|
```tsx
|
|
@@ -83,6 +93,7 @@ interface UseUpdaterOptions {
|
|
|
83
93
|
autoPrompt?: boolean // default: true
|
|
84
94
|
onConfirm?: (manifest: UpdateManifest) => Promise<boolean>
|
|
85
95
|
onError?: (message: string) => void
|
|
96
|
+
onInfo?: (title: string, message: string) => void
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
interface UseUpdaterReturn {
|
|
@@ -96,8 +107,9 @@ interface UseUpdaterReturn {
|
|
|
96
107
|
|--------|---------|-------------|
|
|
97
108
|
| `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
109
|
| `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
|
-
| `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
|
|
110
|
+
| `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. |
|
|
100
111
|
| `onError` | — | Called with an error message string if `check()` throws. Defaults to `Alert.alert`. |
|
|
112
|
+
| `onInfo` | — | Called with `(title, message)` for `check()`'s purely-informational cases (dev-mode disabled, web unsupported, no update found) — no confirm/cancel choice, just an acknowledgeable message. Defaults to `Alert.alert`. |
|
|
101
113
|
|
|
102
114
|
| Return | Description |
|
|
103
115
|
|--------|-------------|
|
|
@@ -158,6 +170,7 @@ The path argument defaults to `src/constants/release.ts` if omitted.
|
|
|
158
170
|
- `autoPrompt`'s foreground flow shares that same `checkingRef` guard with `check()`, so a manual check and an auto-prompt can't both be mid-confirm at once
|
|
159
171
|
- `updateReady` and the staged manifest ref are cleared in `finally` so they reset on both confirm and cancel
|
|
160
172
|
- `onConfirm` replaces the default `Alert` entirely — useful in apps that have their own dialog primitive (e.g. a `select()` utility or bottom sheet)
|
|
173
|
+
- `onInfo` is the same idea for `check()`'s three non-choice messages (dev-mode disabled, web unsupported, no update found) — separate from `onConfirm` because there's nothing to confirm, just something to show
|
|
161
174
|
- No Provider or context required — the hook is self-contained
|
|
162
175
|
|
|
163
176
|
---
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
interface UpdateManifest {
|
|
2
2
|
createdAt: string;
|
|
3
|
-
metadata?: Record<string, string | undefined>;
|
|
4
3
|
}
|
|
5
4
|
|
|
6
5
|
interface UseUpdaterOptions {
|
|
@@ -8,6 +7,7 @@ interface UseUpdaterOptions {
|
|
|
8
7
|
autoPrompt?: boolean;
|
|
9
8
|
onConfirm?: (manifest: UpdateManifest) => Promise<boolean>;
|
|
10
9
|
onError?: (message: string) => void;
|
|
10
|
+
onInfo?: (title: string, message: string) => void;
|
|
11
11
|
}
|
|
12
12
|
interface UseUpdaterReturn {
|
|
13
13
|
check: () => Promise<void>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
interface UpdateManifest {
|
|
2
2
|
createdAt: string;
|
|
3
|
-
metadata?: Record<string, string | undefined>;
|
|
4
3
|
}
|
|
5
4
|
|
|
6
5
|
interface UseUpdaterOptions {
|
|
@@ -8,6 +7,7 @@ interface UseUpdaterOptions {
|
|
|
8
7
|
autoPrompt?: boolean;
|
|
9
8
|
onConfirm?: (manifest: UpdateManifest) => Promise<boolean>;
|
|
10
9
|
onError?: (message: string) => void;
|
|
10
|
+
onInfo?: (title: string, message: string) => void;
|
|
11
11
|
}
|
|
12
12
|
interface UseUpdaterReturn {
|
|
13
13
|
check: () => Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -42,11 +42,9 @@ var checkForUpdate = async () => {
|
|
|
42
42
|
var import_react_native = require("react-native");
|
|
43
43
|
var getUpdateConfirmation = (manifest) => {
|
|
44
44
|
const date = new Date(manifest.createdAt);
|
|
45
|
-
|
|
46
|
-
if (manifest.metadata?.message) info += `
|
|
45
|
+
const info = `A new update was released on ${date.toLocaleDateString()} at ${date.toLocaleTimeString()}.
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
info += "\n\nRestart app to update.";
|
|
47
|
+
Restart app to update.`;
|
|
50
48
|
return new Promise((resolve) => {
|
|
51
49
|
import_react_native.Alert.alert("Update available", info, [
|
|
52
50
|
{ text: "Cancel", style: "cancel", onPress: () => resolve(false) },
|
|
@@ -58,19 +56,24 @@ Message: ${manifest.metadata.message}.`;
|
|
|
58
56
|
// src/useUpdater.ts
|
|
59
57
|
var isUnsupported = () => __DEV__ || import_react_native2.Platform.OS === "web";
|
|
60
58
|
var useUpdater = (options = {}) => {
|
|
61
|
-
const { autoCheck = true, autoPrompt = true, onConfirm, onError } = options;
|
|
59
|
+
const { autoCheck = true, autoPrompt = true, onConfirm, onError, onInfo } = options;
|
|
62
60
|
const [checking, setChecking] = (0, import_react.useState)(false);
|
|
63
61
|
const [updateReady, setUpdateReady] = (0, import_react.useState)(false);
|
|
64
62
|
const checkingRef = (0, import_react.useRef)(false);
|
|
63
|
+
const autoCheckPendingRef = (0, import_react.useRef)(false);
|
|
65
64
|
const appState = (0, import_react.useRef)(import_react_native2.AppState.currentState);
|
|
66
65
|
const stagedManifest = (0, import_react.useRef)(null);
|
|
67
66
|
const onConfirmRef = (0, import_react.useRef)(onConfirm);
|
|
68
67
|
const onErrorRef = (0, import_react.useRef)(onError);
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
(0, import_react.useEffect)(() => {
|
|
69
|
+
onConfirmRef.current = onConfirm;
|
|
70
|
+
onErrorRef.current = onError;
|
|
71
|
+
});
|
|
71
72
|
(0, import_react.useEffect)(() => {
|
|
72
73
|
if (!autoCheck || isUnsupported()) return;
|
|
73
74
|
const runAutoCheck = () => {
|
|
75
|
+
if (autoCheckPendingRef.current) return;
|
|
76
|
+
autoCheckPendingRef.current = true;
|
|
74
77
|
checkForUpdate().then(async (manifest) => {
|
|
75
78
|
if (!manifest) return;
|
|
76
79
|
stagedManifest.current = manifest;
|
|
@@ -93,6 +96,8 @@ var useUpdater = (options = {}) => {
|
|
|
93
96
|
setChecking(false);
|
|
94
97
|
}
|
|
95
98
|
}).catch(() => {
|
|
99
|
+
}).finally(() => {
|
|
100
|
+
autoCheckPendingRef.current = false;
|
|
96
101
|
});
|
|
97
102
|
};
|
|
98
103
|
runAutoCheck();
|
|
@@ -106,11 +111,15 @@ var useUpdater = (options = {}) => {
|
|
|
106
111
|
}, [autoCheck, autoPrompt]);
|
|
107
112
|
const check = async () => {
|
|
108
113
|
if (__DEV__) {
|
|
109
|
-
|
|
114
|
+
const message = "Update checks are disabled in development mode.";
|
|
115
|
+
if (onInfo) onInfo("Updates unavailable", message);
|
|
116
|
+
else import_react_native2.Alert.alert("Updates unavailable", message);
|
|
110
117
|
return;
|
|
111
118
|
}
|
|
112
119
|
if (import_react_native2.Platform.OS === "web") {
|
|
113
|
-
|
|
120
|
+
const message = "Update checks are not supported on web.";
|
|
121
|
+
if (onInfo) onInfo("Updates unavailable", message);
|
|
122
|
+
else import_react_native2.Alert.alert("Updates unavailable", message);
|
|
114
123
|
return;
|
|
115
124
|
}
|
|
116
125
|
if (checkingRef.current) return;
|
|
@@ -119,7 +128,9 @@ var useUpdater = (options = {}) => {
|
|
|
119
128
|
try {
|
|
120
129
|
const manifest = stagedManifest.current ?? await checkForUpdate();
|
|
121
130
|
if (!manifest) {
|
|
122
|
-
|
|
131
|
+
const message = "You are on the most recent version.";
|
|
132
|
+
if (onInfo) onInfo("No update", message);
|
|
133
|
+
else import_react_native2.Alert.alert("No update", message);
|
|
123
134
|
return;
|
|
124
135
|
}
|
|
125
136
|
const confirmFn = onConfirm ?? getUpdateConfirmation;
|
package/dist/index.mjs
CHANGED
|
@@ -16,11 +16,9 @@ var checkForUpdate = async () => {
|
|
|
16
16
|
import { Alert } from "react-native";
|
|
17
17
|
var getUpdateConfirmation = (manifest) => {
|
|
18
18
|
const date = new Date(manifest.createdAt);
|
|
19
|
-
|
|
20
|
-
if (manifest.metadata?.message) info += `
|
|
19
|
+
const info = `A new update was released on ${date.toLocaleDateString()} at ${date.toLocaleTimeString()}.
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
info += "\n\nRestart app to update.";
|
|
21
|
+
Restart app to update.`;
|
|
24
22
|
return new Promise((resolve) => {
|
|
25
23
|
Alert.alert("Update available", info, [
|
|
26
24
|
{ text: "Cancel", style: "cancel", onPress: () => resolve(false) },
|
|
@@ -32,19 +30,24 @@ Message: ${manifest.metadata.message}.`;
|
|
|
32
30
|
// src/useUpdater.ts
|
|
33
31
|
var isUnsupported = () => __DEV__ || Platform.OS === "web";
|
|
34
32
|
var useUpdater = (options = {}) => {
|
|
35
|
-
const { autoCheck = true, autoPrompt = true, onConfirm, onError } = options;
|
|
33
|
+
const { autoCheck = true, autoPrompt = true, onConfirm, onError, onInfo } = options;
|
|
36
34
|
const [checking, setChecking] = useState(false);
|
|
37
35
|
const [updateReady, setUpdateReady] = useState(false);
|
|
38
36
|
const checkingRef = useRef(false);
|
|
37
|
+
const autoCheckPendingRef = useRef(false);
|
|
39
38
|
const appState = useRef(AppState.currentState);
|
|
40
39
|
const stagedManifest = useRef(null);
|
|
41
40
|
const onConfirmRef = useRef(onConfirm);
|
|
42
41
|
const onErrorRef = useRef(onError);
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
onConfirmRef.current = onConfirm;
|
|
44
|
+
onErrorRef.current = onError;
|
|
45
|
+
});
|
|
45
46
|
useEffect(() => {
|
|
46
47
|
if (!autoCheck || isUnsupported()) return;
|
|
47
48
|
const runAutoCheck = () => {
|
|
49
|
+
if (autoCheckPendingRef.current) return;
|
|
50
|
+
autoCheckPendingRef.current = true;
|
|
48
51
|
checkForUpdate().then(async (manifest) => {
|
|
49
52
|
if (!manifest) return;
|
|
50
53
|
stagedManifest.current = manifest;
|
|
@@ -67,6 +70,8 @@ var useUpdater = (options = {}) => {
|
|
|
67
70
|
setChecking(false);
|
|
68
71
|
}
|
|
69
72
|
}).catch(() => {
|
|
73
|
+
}).finally(() => {
|
|
74
|
+
autoCheckPendingRef.current = false;
|
|
70
75
|
});
|
|
71
76
|
};
|
|
72
77
|
runAutoCheck();
|
|
@@ -80,11 +85,15 @@ var useUpdater = (options = {}) => {
|
|
|
80
85
|
}, [autoCheck, autoPrompt]);
|
|
81
86
|
const check = async () => {
|
|
82
87
|
if (__DEV__) {
|
|
83
|
-
|
|
88
|
+
const message = "Update checks are disabled in development mode.";
|
|
89
|
+
if (onInfo) onInfo("Updates unavailable", message);
|
|
90
|
+
else Alert2.alert("Updates unavailable", message);
|
|
84
91
|
return;
|
|
85
92
|
}
|
|
86
93
|
if (Platform.OS === "web") {
|
|
87
|
-
|
|
94
|
+
const message = "Update checks are not supported on web.";
|
|
95
|
+
if (onInfo) onInfo("Updates unavailable", message);
|
|
96
|
+
else Alert2.alert("Updates unavailable", message);
|
|
88
97
|
return;
|
|
89
98
|
}
|
|
90
99
|
if (checkingRef.current) return;
|
|
@@ -93,7 +102,9 @@ var useUpdater = (options = {}) => {
|
|
|
93
102
|
try {
|
|
94
103
|
const manifest = stagedManifest.current ?? await checkForUpdate();
|
|
95
104
|
if (!manifest) {
|
|
96
|
-
|
|
105
|
+
const message = "You are on the most recent version.";
|
|
106
|
+
if (onInfo) onInfo("No update", message);
|
|
107
|
+
else Alert2.alert("No update", message);
|
|
97
108
|
return;
|
|
98
109
|
}
|
|
99
110
|
const confirmFn = onConfirm ?? getUpdateConfirmation;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rific/updater",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "OTA update hook for Expo apps. Silent background fetch on foreground, manual check with confirmation dialog",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"expo",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"eslint-config-prettier": "^10.1.8",
|
|
68
68
|
"eslint-plugin-package-json": "^1.0.0",
|
|
69
69
|
"eslint-plugin-prettier": "^5.5.5",
|
|
70
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
70
71
|
"eslint-plugin-react-native": "^5.0.0",
|
|
71
72
|
"eslint-plugin-simple-import-sort": "^13.0.0",
|
|
72
73
|
"expo-updates": ">=0.25.0",
|
|
@@ -4,9 +4,7 @@ import { UpdateManifest } from './types'
|
|
|
4
4
|
|
|
5
5
|
export const getUpdateConfirmation = (manifest: UpdateManifest): Promise<boolean> => {
|
|
6
6
|
const date = new Date(manifest.createdAt)
|
|
7
|
-
|
|
8
|
-
if (manifest.metadata?.message) info += `\n\nMessage: ${manifest.metadata.message}.`
|
|
9
|
-
info += '\n\nRestart app to update.'
|
|
7
|
+
const info = `A new update was released on ${date.toLocaleDateString()} at ${date.toLocaleTimeString()}.\n\nRestart app to update.`
|
|
10
8
|
|
|
11
9
|
return new Promise((resolve) => {
|
|
12
10
|
Alert.alert('Update available', info, [
|
package/src/types.ts
CHANGED
package/src/useUpdater.ts
CHANGED
|
@@ -11,6 +11,11 @@ export interface UseUpdaterOptions {
|
|
|
11
11
|
autoPrompt?: boolean
|
|
12
12
|
onConfirm?: (manifest: UpdateManifest) => Promise<boolean>
|
|
13
13
|
onError?: (message: string) => void
|
|
14
|
+
// Purely informational — no confirm/cancel choice, just something to acknowledge. Covers the
|
|
15
|
+
// three plain Alert.alert calls inside check() below (dev-mode disabled, web unsupported, and
|
|
16
|
+
// "you're already up to date") — none of them go through onConfirm, since there's no decision
|
|
17
|
+
// to make.
|
|
18
|
+
onInfo?: (title: string, message: string) => void
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
export interface UseUpdaterReturn {
|
|
@@ -22,23 +27,33 @@ export interface UseUpdaterReturn {
|
|
|
22
27
|
const isUnsupported = () => __DEV__ || Platform.OS === 'web'
|
|
23
28
|
|
|
24
29
|
export const useUpdater = (options: UseUpdaterOptions = {}): UseUpdaterReturn => {
|
|
25
|
-
const { autoCheck = true, autoPrompt = true, onConfirm, onError } = options
|
|
30
|
+
const { autoCheck = true, autoPrompt = true, onConfirm, onError, onInfo } = options
|
|
26
31
|
const [checking, setChecking] = useState(false)
|
|
27
32
|
const [updateReady, setUpdateReady] = useState(false)
|
|
28
33
|
const checkingRef = useRef(false)
|
|
34
|
+
const autoCheckPendingRef = useRef(false)
|
|
29
35
|
const appState = useRef(AppState.currentState)
|
|
30
36
|
const stagedManifest = useRef<UpdateManifest | null>(null)
|
|
31
37
|
// Read via refs inside the AppState listener so onConfirm/onError identity changes (e.g. an
|
|
32
38
|
// inline arrow function) don't tear down and re-subscribe the listener on every render.
|
|
33
39
|
const onConfirmRef = useRef(onConfirm)
|
|
34
40
|
const onErrorRef = useRef(onError)
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
onConfirmRef.current = onConfirm
|
|
44
|
+
onErrorRef.current = onError
|
|
45
|
+
})
|
|
37
46
|
|
|
38
47
|
useEffect(() => {
|
|
39
48
|
if (!autoCheck || isUnsupported()) return
|
|
40
49
|
|
|
41
50
|
const runAutoCheck = () => {
|
|
51
|
+
// Guards against overlapping fetches if AppState fires again (e.g. rapid app-switcher
|
|
52
|
+
// transitions) before a prior auto-check has resolved. Independent of checkingRef, which
|
|
53
|
+
// only guards the confirmation prompt and covers manual check() calls too.
|
|
54
|
+
if (autoCheckPendingRef.current) return
|
|
55
|
+
autoCheckPendingRef.current = true
|
|
56
|
+
|
|
42
57
|
checkForUpdate()
|
|
43
58
|
.then(async (manifest) => {
|
|
44
59
|
if (!manifest) return
|
|
@@ -64,6 +79,9 @@ export const useUpdater = (options: UseUpdaterOptions = {}): UseUpdaterReturn =>
|
|
|
64
79
|
}
|
|
65
80
|
})
|
|
66
81
|
.catch(() => {})
|
|
82
|
+
.finally(() => {
|
|
83
|
+
autoCheckPendingRef.current = false
|
|
84
|
+
})
|
|
67
85
|
}
|
|
68
86
|
|
|
69
87
|
// Cold launch: run the same check+prompt flow as a foreground resume so update
|
|
@@ -82,11 +100,15 @@ export const useUpdater = (options: UseUpdaterOptions = {}): UseUpdaterReturn =>
|
|
|
82
100
|
|
|
83
101
|
const check = async (): Promise<void> => {
|
|
84
102
|
if (__DEV__) {
|
|
85
|
-
|
|
103
|
+
const message = 'Update checks are disabled in development mode.'
|
|
104
|
+
if (onInfo) onInfo('Updates unavailable', message)
|
|
105
|
+
else Alert.alert('Updates unavailable', message)
|
|
86
106
|
return
|
|
87
107
|
}
|
|
88
108
|
if (Platform.OS === 'web') {
|
|
89
|
-
|
|
109
|
+
const message = 'Update checks are not supported on web.'
|
|
110
|
+
if (onInfo) onInfo('Updates unavailable', message)
|
|
111
|
+
else Alert.alert('Updates unavailable', message)
|
|
90
112
|
return
|
|
91
113
|
}
|
|
92
114
|
if (checkingRef.current) return
|
|
@@ -95,7 +117,9 @@ export const useUpdater = (options: UseUpdaterOptions = {}): UseUpdaterReturn =>
|
|
|
95
117
|
try {
|
|
96
118
|
const manifest = stagedManifest.current ?? (await checkForUpdate())
|
|
97
119
|
if (!manifest) {
|
|
98
|
-
|
|
120
|
+
const message = 'You are on the most recent version.'
|
|
121
|
+
if (onInfo) onInfo('No update', message)
|
|
122
|
+
else Alert.alert('No update', message)
|
|
99
123
|
return
|
|
100
124
|
}
|
|
101
125
|
const confirmFn = onConfirm ?? getUpdateConfirmation
|