@hot-updater/react-native 0.5.1 → 0.5.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/dist/checkUpdate.d.ts +6 -0
- package/dist/hooks/useEventCallback.d.ts +4 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +111 -76
- package/dist/index.mjs +111 -76
- package/dist/native.d.ts +1 -1
- package/dist/runUpdateProcess.d.ts +17 -0
- package/dist/wrap.d.ts +37 -10
- package/package.json +3 -3
- package/src/checkUpdate.ts +59 -0
- package/src/hooks/useEventCallback.ts +28 -0
- package/src/index.ts +4 -4
- package/src/native.ts +1 -1
- package/src/runUpdateProcess.ts +47 -0
- package/src/wrap.tsx +95 -102
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BundleArg, UpdateInfo } from "@hot-updater/core";
|
|
2
|
+
export interface CheckForUpdateConfig {
|
|
3
|
+
source: BundleArg;
|
|
4
|
+
requestHeaders?: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
export declare function checkForUpdate(config: CheckForUpdateConfig): Promise<UpdateInfo | null>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
type EventCallback<Args extends unknown[], R> = ((...args: Args) => R) | undefined;
|
|
2
|
+
export declare function useEventCallback<Args extends unknown[], R>(fn: (...args: Args) => R): (...args: Args) => R;
|
|
3
|
+
export declare function useEventCallback<Args extends unknown[], R>(fn: EventCallback<Args, R>): EventCallback<Args, R>;
|
|
4
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { checkForUpdate } from "./checkUpdate";
|
|
1
2
|
import { wrap } from "./wrap";
|
|
2
3
|
export type * from "./wrap";
|
|
3
4
|
export type * from "./native";
|
|
@@ -8,7 +9,7 @@ export declare const HotUpdater: {
|
|
|
8
9
|
getAppVersion: () => Promise<string | null>;
|
|
9
10
|
getBundleId: () => string;
|
|
10
11
|
addListener: <T extends keyof import("./native").HotUpdaterEvent>(eventName: T, listener: (event: import("./native").HotUpdaterEvent[T]) => void) => () => void;
|
|
11
|
-
|
|
12
|
+
checkForUpdate: typeof checkForUpdate;
|
|
13
|
+
runUpdateProcess: ({ reloadOnForceUpdate, ...checkForUpdateConfig }: import("./runUpdateProcess").RunUpdateProcessConfig) => Promise<import("./runUpdateProcess").RunUpdateProcessResponse>;
|
|
12
14
|
updateBundle: (bundleId: string, zipUrl: string | null) => Promise<boolean>;
|
|
13
|
-
getUpdateInfo: (bundles: import("@hot-updater/core").Bundle[], { platform, bundleId, appVersion }: import("@hot-updater/core").GetBundlesArgs) => Promise<import("@hot-updater/core").UpdateInfo | null>;
|
|
14
15
|
};
|
package/dist/index.js
CHANGED
|
@@ -1582,9 +1582,18 @@ var __webpack_modules__ = {
|
|
|
1582
1582
|
}, V = {
|
|
1583
1583
|
transition: null
|
|
1584
1584
|
};
|
|
1585
|
+
exports1.useCallback = function(a, b) {
|
|
1586
|
+
return U.current.useCallback(a, b);
|
|
1587
|
+
};
|
|
1585
1588
|
exports1.useEffect = function(a, b) {
|
|
1586
1589
|
return U.current.useEffect(a, b);
|
|
1587
1590
|
};
|
|
1591
|
+
exports1.useLayoutEffect = function(a, b) {
|
|
1592
|
+
return U.current.useLayoutEffect(a, b);
|
|
1593
|
+
};
|
|
1594
|
+
exports1.useRef = function(a) {
|
|
1595
|
+
return U.current.useRef(a);
|
|
1596
|
+
};
|
|
1588
1597
|
exports1.useState = function(a) {
|
|
1589
1598
|
return U.current.useState(a);
|
|
1590
1599
|
};
|
|
@@ -1674,6 +1683,7 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
1674
1683
|
HotUpdater: ()=>src_HotUpdater
|
|
1675
1684
|
});
|
|
1676
1685
|
const js_namespaceObject = require("@hot-updater/js");
|
|
1686
|
+
var external_react_native_ = __webpack_require__("react-native");
|
|
1677
1687
|
const ensureUpdateInfo = async (source, { appVersion, bundleId, platform }, requestHeaders)=>{
|
|
1678
1688
|
try {
|
|
1679
1689
|
let bundles = null;
|
|
@@ -1692,8 +1702,13 @@ const ensureUpdateInfo = async (source, { appVersion, bundleId, platform }, requ
|
|
|
1692
1702
|
return [];
|
|
1693
1703
|
}
|
|
1694
1704
|
};
|
|
1705
|
+
class HotUpdaterError extends Error {
|
|
1706
|
+
constructor(message){
|
|
1707
|
+
super(message);
|
|
1708
|
+
this.name = "HotUpdaterError";
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1695
1711
|
const core_namespaceObject = require("@hot-updater/core");
|
|
1696
|
-
var external_react_native_ = __webpack_require__("react-native");
|
|
1697
1712
|
const HotUpdater = {
|
|
1698
1713
|
HOT_UPDATER_BUNDLE_ID: core_namespaceObject.NIL_UUID
|
|
1699
1714
|
};
|
|
@@ -1721,6 +1736,49 @@ const reload = ()=>{
|
|
|
1721
1736
|
HotUpdaterNative.reload();
|
|
1722
1737
|
};
|
|
1723
1738
|
const getBundleId = ()=>HotUpdater.HOT_UPDATER_BUNDLE_ID;
|
|
1739
|
+
async function checkForUpdate(config) {
|
|
1740
|
+
if (__DEV__) {
|
|
1741
|
+
console.warn("[HotUpdater] __DEV__ is true, HotUpdater is only supported in production");
|
|
1742
|
+
return null;
|
|
1743
|
+
}
|
|
1744
|
+
if (![
|
|
1745
|
+
"ios",
|
|
1746
|
+
"android"
|
|
1747
|
+
].includes(external_react_native_.Platform.OS)) throw new HotUpdaterError("HotUpdater is only supported on iOS and Android");
|
|
1748
|
+
const currentAppVersion = await getAppVersion();
|
|
1749
|
+
const platform = external_react_native_.Platform.OS;
|
|
1750
|
+
const currentBundleId = await getBundleId();
|
|
1751
|
+
if (!currentAppVersion) throw new HotUpdaterError("Failed to get app version");
|
|
1752
|
+
const ensuredUpdateInfo = await ensureUpdateInfo(config.source, {
|
|
1753
|
+
appVersion: currentAppVersion,
|
|
1754
|
+
bundleId: currentBundleId,
|
|
1755
|
+
platform
|
|
1756
|
+
}, config.requestHeaders);
|
|
1757
|
+
let updateInfo = null;
|
|
1758
|
+
if (Array.isArray(ensuredUpdateInfo)) {
|
|
1759
|
+
const bundles = ensuredUpdateInfo;
|
|
1760
|
+
updateInfo = await (0, js_namespaceObject.getUpdateInfo)(bundles, {
|
|
1761
|
+
appVersion: currentAppVersion,
|
|
1762
|
+
bundleId: currentBundleId,
|
|
1763
|
+
platform
|
|
1764
|
+
});
|
|
1765
|
+
} else updateInfo = ensuredUpdateInfo;
|
|
1766
|
+
return updateInfo;
|
|
1767
|
+
}
|
|
1768
|
+
const runUpdateProcess = async ({ reloadOnForceUpdate = false, ...checkForUpdateConfig })=>{
|
|
1769
|
+
const updateInfo = await checkForUpdate(checkForUpdateConfig);
|
|
1770
|
+
if (!updateInfo) return {
|
|
1771
|
+
status: "UP_TO_DATE"
|
|
1772
|
+
};
|
|
1773
|
+
const isUpdated = await updateBundle(updateInfo.id, updateInfo.fileUrl);
|
|
1774
|
+
if (isUpdated && updateInfo.forceUpdate && reloadOnForceUpdate) reload();
|
|
1775
|
+
if (!isUpdated) throw new Error("New update was found but failed to download the bundle.");
|
|
1776
|
+
return {
|
|
1777
|
+
status: updateInfo.status,
|
|
1778
|
+
isForceUpdate: updateInfo.forceUpdate,
|
|
1779
|
+
id: updateInfo.id
|
|
1780
|
+
};
|
|
1781
|
+
};
|
|
1724
1782
|
var react = __webpack_require__("../../node_modules/.pnpm/react@18.3.1/node_modules/react/index.js");
|
|
1725
1783
|
const createHotUpdaterStore = ()=>{
|
|
1726
1784
|
let state = {
|
|
@@ -1751,88 +1809,65 @@ const createHotUpdaterStore = ()=>{
|
|
|
1751
1809
|
};
|
|
1752
1810
|
const hotUpdaterStore = createHotUpdaterStore();
|
|
1753
1811
|
const useHotUpdaterStore = ()=>(0, react.useSyncExternalStore)(hotUpdaterStore.subscribe, hotUpdaterStore.getSnapshot, hotUpdaterStore.getSnapshot);
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
"ios",
|
|
1767
|
-
"android"
|
|
1768
|
-
].includes(external_react_native_.Platform.OS)) throw new HotUpdaterError("HotUpdater is only supported on iOS and Android");
|
|
1769
|
-
const currentAppVersion = await getAppVersion();
|
|
1770
|
-
const platform = external_react_native_.Platform.OS;
|
|
1771
|
-
const currentBundleId = await getBundleId();
|
|
1772
|
-
if (!currentAppVersion) throw new HotUpdaterError("Failed to get app version");
|
|
1773
|
-
const ensuredUpdateInfo = await ensureUpdateInfo(config.source, {
|
|
1774
|
-
appVersion: currentAppVersion,
|
|
1775
|
-
bundleId: currentBundleId,
|
|
1776
|
-
platform
|
|
1777
|
-
}, config.requestHeaders);
|
|
1778
|
-
let updateInfo = null;
|
|
1779
|
-
if (Array.isArray(ensuredUpdateInfo)) {
|
|
1780
|
-
const bundles = ensuredUpdateInfo;
|
|
1781
|
-
updateInfo = await (0, js_namespaceObject.getUpdateInfo)(bundles, {
|
|
1782
|
-
appVersion: currentAppVersion,
|
|
1783
|
-
bundleId: currentBundleId,
|
|
1784
|
-
platform
|
|
1785
|
-
});
|
|
1786
|
-
} else updateInfo = ensuredUpdateInfo;
|
|
1787
|
-
return updateInfo;
|
|
1788
|
-
}
|
|
1789
|
-
async function installUpdate(updateInfo) {
|
|
1790
|
-
const isSuccess = await updateBundle(updateInfo.id, updateInfo.fileUrl || "");
|
|
1791
|
-
if (isSuccess && updateInfo.forceUpdate) {
|
|
1792
|
-
reload();
|
|
1793
|
-
return true;
|
|
1794
|
-
}
|
|
1795
|
-
return isSuccess;
|
|
1812
|
+
function useEventCallback(fn) {
|
|
1813
|
+
const callbackRef = (0, react.useRef)(()=>{
|
|
1814
|
+
throw new Error("Cannot call an event handler while rendering.");
|
|
1815
|
+
});
|
|
1816
|
+
(0, react.useLayoutEffect)(()=>{
|
|
1817
|
+
callbackRef.current = fn;
|
|
1818
|
+
}, [
|
|
1819
|
+
fn
|
|
1820
|
+
]);
|
|
1821
|
+
return (0, react.useCallback)((...args)=>callbackRef.current?.(...args), [
|
|
1822
|
+
callbackRef
|
|
1823
|
+
]);
|
|
1796
1824
|
}
|
|
1797
1825
|
function wrap(config) {
|
|
1826
|
+
const { reloadOnForceUpdate = true, ...restConfig } = config;
|
|
1798
1827
|
return (WrappedComponent)=>{
|
|
1799
1828
|
const HotUpdaterHOC = ()=>{
|
|
1800
1829
|
const { progress } = useHotUpdaterStore();
|
|
1801
|
-
const [
|
|
1830
|
+
const [status, setStatus] = (0, react.useState)("IDLE");
|
|
1831
|
+
const initHotUpdater = useEventCallback(async ()=>{
|
|
1832
|
+
try {
|
|
1833
|
+
setStatus("CHECK_FOR_UPDATE");
|
|
1834
|
+
const updateInfo = await checkForUpdate({
|
|
1835
|
+
source: restConfig.source,
|
|
1836
|
+
requestHeaders: restConfig.requestHeaders
|
|
1837
|
+
});
|
|
1838
|
+
if (!updateInfo) {
|
|
1839
|
+
restConfig.onUpdateProcessCompleted?.({
|
|
1840
|
+
status: "UP_TO_DATE"
|
|
1841
|
+
});
|
|
1842
|
+
setStatus("UPDATE_PROCESS_COMPLETED");
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
setStatus("UPDATING");
|
|
1846
|
+
const isSuccess = await updateBundle(updateInfo.id, updateInfo.fileUrl);
|
|
1847
|
+
if (!isSuccess) throw new Error("New update was found but failed to download the bundle.");
|
|
1848
|
+
if (updateInfo.forceUpdate && reloadOnForceUpdate) reload();
|
|
1849
|
+
restConfig.onUpdateProcessCompleted?.({
|
|
1850
|
+
id: updateInfo.id,
|
|
1851
|
+
status: updateInfo.status,
|
|
1852
|
+
isForceUpdate: updateInfo.forceUpdate
|
|
1853
|
+
});
|
|
1854
|
+
setStatus("UPDATE_PROCESS_COMPLETED");
|
|
1855
|
+
} catch (error) {
|
|
1856
|
+
if (error instanceof HotUpdaterError) restConfig.onError?.(error);
|
|
1857
|
+
setStatus("UPDATE_PROCESS_COMPLETED");
|
|
1858
|
+
throw error;
|
|
1859
|
+
}
|
|
1860
|
+
});
|
|
1802
1861
|
(0, react.useEffect)(()=>{
|
|
1803
|
-
|
|
1862
|
+
restConfig.onProgress?.(progress);
|
|
1804
1863
|
}, [
|
|
1805
1864
|
progress
|
|
1806
1865
|
]);
|
|
1807
|
-
(0, react.
|
|
1808
|
-
const initHotUpdater = async ()=>{
|
|
1809
|
-
try {
|
|
1810
|
-
const updateInfo = await checkUpdate(config);
|
|
1811
|
-
if (!updateInfo) {
|
|
1812
|
-
config.onCheckUpdateCompleted?.({
|
|
1813
|
-
isBundleUpdated: false
|
|
1814
|
-
});
|
|
1815
|
-
setIsCheckUpdateCompleted(true);
|
|
1816
|
-
return;
|
|
1817
|
-
}
|
|
1818
|
-
const isSuccess = await installUpdate(updateInfo);
|
|
1819
|
-
config.onCheckUpdateCompleted?.({
|
|
1820
|
-
isBundleUpdated: isSuccess
|
|
1821
|
-
});
|
|
1822
|
-
setIsCheckUpdateCompleted(true);
|
|
1823
|
-
} catch (error) {
|
|
1824
|
-
if (error instanceof HotUpdaterError) config.onError?.(error);
|
|
1825
|
-
setIsCheckUpdateCompleted(true);
|
|
1826
|
-
throw error;
|
|
1827
|
-
}
|
|
1828
|
-
};
|
|
1866
|
+
(0, react.useLayoutEffect)(()=>{
|
|
1829
1867
|
initHotUpdater();
|
|
1830
|
-
}, [
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
]);
|
|
1834
|
-
if (config.fallbackComponent && (!isCheckUpdateCompleted || progress > 0 && progress < 1)) {
|
|
1835
|
-
const Fallback = config.fallbackComponent;
|
|
1868
|
+
}, []);
|
|
1869
|
+
if (restConfig.fallbackComponent && "UPDATE_PROCESS_COMPLETED" !== status) {
|
|
1870
|
+
const Fallback = restConfig.fallbackComponent;
|
|
1836
1871
|
return /*#__PURE__*/ React.createElement(Fallback, {
|
|
1837
1872
|
progress: progress
|
|
1838
1873
|
});
|
|
@@ -1851,9 +1886,9 @@ const src_HotUpdater = {
|
|
|
1851
1886
|
getAppVersion: getAppVersion,
|
|
1852
1887
|
getBundleId: getBundleId,
|
|
1853
1888
|
addListener: addListener,
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1889
|
+
checkForUpdate: checkForUpdate,
|
|
1890
|
+
runUpdateProcess: runUpdateProcess,
|
|
1891
|
+
updateBundle: updateBundle
|
|
1857
1892
|
};
|
|
1858
1893
|
var __webpack_export_target__ = exports;
|
|
1859
1894
|
for(var __webpack_i__ in __webpack_exports__)__webpack_export_target__[__webpack_i__] = __webpack_exports__[__webpack_i__];
|
package/dist/index.mjs
CHANGED
|
@@ -1584,9 +1584,18 @@ var __webpack_modules__ = {
|
|
|
1584
1584
|
}, V = {
|
|
1585
1585
|
transition: null
|
|
1586
1586
|
};
|
|
1587
|
+
exports.useCallback = function(a, b) {
|
|
1588
|
+
return U.current.useCallback(a, b);
|
|
1589
|
+
};
|
|
1587
1590
|
exports.useEffect = function(a, b) {
|
|
1588
1591
|
return U.current.useEffect(a, b);
|
|
1589
1592
|
};
|
|
1593
|
+
exports.useLayoutEffect = function(a, b) {
|
|
1594
|
+
return U.current.useLayoutEffect(a, b);
|
|
1595
|
+
};
|
|
1596
|
+
exports.useRef = function(a) {
|
|
1597
|
+
return U.current.useRef(a);
|
|
1598
|
+
};
|
|
1590
1599
|
exports.useState = function(a) {
|
|
1591
1600
|
return U.current.useState(a);
|
|
1592
1601
|
};
|
|
@@ -1643,6 +1652,7 @@ function __webpack_require__(moduleId) {
|
|
|
1643
1652
|
return module;
|
|
1644
1653
|
};
|
|
1645
1654
|
})();
|
|
1655
|
+
var external_react_native_ = __webpack_require__("react-native");
|
|
1646
1656
|
const ensureUpdateInfo = async (source, { appVersion, bundleId, platform }, requestHeaders)=>{
|
|
1647
1657
|
try {
|
|
1648
1658
|
let bundles = null;
|
|
@@ -1661,7 +1671,12 @@ const ensureUpdateInfo = async (source, { appVersion, bundleId, platform }, requ
|
|
|
1661
1671
|
return [];
|
|
1662
1672
|
}
|
|
1663
1673
|
};
|
|
1664
|
-
|
|
1674
|
+
class HotUpdaterError extends Error {
|
|
1675
|
+
constructor(message){
|
|
1676
|
+
super(message);
|
|
1677
|
+
this.name = "HotUpdaterError";
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1665
1680
|
const HotUpdater = {
|
|
1666
1681
|
HOT_UPDATER_BUNDLE_ID: __WEBPACK_EXTERNAL_MODULE__hot_updater_core__.NIL_UUID
|
|
1667
1682
|
};
|
|
@@ -1689,6 +1704,49 @@ const reload = ()=>{
|
|
|
1689
1704
|
HotUpdaterNative.reload();
|
|
1690
1705
|
};
|
|
1691
1706
|
const getBundleId = ()=>HotUpdater.HOT_UPDATER_BUNDLE_ID;
|
|
1707
|
+
async function checkForUpdate(config) {
|
|
1708
|
+
if (__DEV__) {
|
|
1709
|
+
console.warn("[HotUpdater] __DEV__ is true, HotUpdater is only supported in production");
|
|
1710
|
+
return null;
|
|
1711
|
+
}
|
|
1712
|
+
if (![
|
|
1713
|
+
"ios",
|
|
1714
|
+
"android"
|
|
1715
|
+
].includes(external_react_native_.Platform.OS)) throw new HotUpdaterError("HotUpdater is only supported on iOS and Android");
|
|
1716
|
+
const currentAppVersion = await getAppVersion();
|
|
1717
|
+
const platform = external_react_native_.Platform.OS;
|
|
1718
|
+
const currentBundleId = await getBundleId();
|
|
1719
|
+
if (!currentAppVersion) throw new HotUpdaterError("Failed to get app version");
|
|
1720
|
+
const ensuredUpdateInfo = await ensureUpdateInfo(config.source, {
|
|
1721
|
+
appVersion: currentAppVersion,
|
|
1722
|
+
bundleId: currentBundleId,
|
|
1723
|
+
platform
|
|
1724
|
+
}, config.requestHeaders);
|
|
1725
|
+
let updateInfo = null;
|
|
1726
|
+
if (Array.isArray(ensuredUpdateInfo)) {
|
|
1727
|
+
const bundles = ensuredUpdateInfo;
|
|
1728
|
+
updateInfo = await (0, __WEBPACK_EXTERNAL_MODULE__hot_updater_js__.getUpdateInfo)(bundles, {
|
|
1729
|
+
appVersion: currentAppVersion,
|
|
1730
|
+
bundleId: currentBundleId,
|
|
1731
|
+
platform
|
|
1732
|
+
});
|
|
1733
|
+
} else updateInfo = ensuredUpdateInfo;
|
|
1734
|
+
return updateInfo;
|
|
1735
|
+
}
|
|
1736
|
+
const runUpdateProcess = async ({ reloadOnForceUpdate = false, ...checkForUpdateConfig })=>{
|
|
1737
|
+
const updateInfo = await checkForUpdate(checkForUpdateConfig);
|
|
1738
|
+
if (!updateInfo) return {
|
|
1739
|
+
status: "UP_TO_DATE"
|
|
1740
|
+
};
|
|
1741
|
+
const isUpdated = await updateBundle(updateInfo.id, updateInfo.fileUrl);
|
|
1742
|
+
if (isUpdated && updateInfo.forceUpdate && reloadOnForceUpdate) reload();
|
|
1743
|
+
if (!isUpdated) throw new Error("New update was found but failed to download the bundle.");
|
|
1744
|
+
return {
|
|
1745
|
+
status: updateInfo.status,
|
|
1746
|
+
isForceUpdate: updateInfo.forceUpdate,
|
|
1747
|
+
id: updateInfo.id
|
|
1748
|
+
};
|
|
1749
|
+
};
|
|
1692
1750
|
var react = __webpack_require__("../../node_modules/.pnpm/react@18.3.1/node_modules/react/index.js");
|
|
1693
1751
|
const createHotUpdaterStore = ()=>{
|
|
1694
1752
|
let state = {
|
|
@@ -1719,88 +1777,65 @@ const createHotUpdaterStore = ()=>{
|
|
|
1719
1777
|
};
|
|
1720
1778
|
const hotUpdaterStore = createHotUpdaterStore();
|
|
1721
1779
|
const useHotUpdaterStore = ()=>(0, react.useSyncExternalStore)(hotUpdaterStore.subscribe, hotUpdaterStore.getSnapshot, hotUpdaterStore.getSnapshot);
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
"ios",
|
|
1735
|
-
"android"
|
|
1736
|
-
].includes(external_react_native_.Platform.OS)) throw new HotUpdaterError("HotUpdater is only supported on iOS and Android");
|
|
1737
|
-
const currentAppVersion = await getAppVersion();
|
|
1738
|
-
const platform = external_react_native_.Platform.OS;
|
|
1739
|
-
const currentBundleId = await getBundleId();
|
|
1740
|
-
if (!currentAppVersion) throw new HotUpdaterError("Failed to get app version");
|
|
1741
|
-
const ensuredUpdateInfo = await ensureUpdateInfo(config.source, {
|
|
1742
|
-
appVersion: currentAppVersion,
|
|
1743
|
-
bundleId: currentBundleId,
|
|
1744
|
-
platform
|
|
1745
|
-
}, config.requestHeaders);
|
|
1746
|
-
let updateInfo = null;
|
|
1747
|
-
if (Array.isArray(ensuredUpdateInfo)) {
|
|
1748
|
-
const bundles = ensuredUpdateInfo;
|
|
1749
|
-
updateInfo = await (0, __WEBPACK_EXTERNAL_MODULE__hot_updater_js__.getUpdateInfo)(bundles, {
|
|
1750
|
-
appVersion: currentAppVersion,
|
|
1751
|
-
bundleId: currentBundleId,
|
|
1752
|
-
platform
|
|
1753
|
-
});
|
|
1754
|
-
} else updateInfo = ensuredUpdateInfo;
|
|
1755
|
-
return updateInfo;
|
|
1756
|
-
}
|
|
1757
|
-
async function installUpdate(updateInfo) {
|
|
1758
|
-
const isSuccess = await updateBundle(updateInfo.id, updateInfo.fileUrl || "");
|
|
1759
|
-
if (isSuccess && updateInfo.forceUpdate) {
|
|
1760
|
-
reload();
|
|
1761
|
-
return true;
|
|
1762
|
-
}
|
|
1763
|
-
return isSuccess;
|
|
1780
|
+
function useEventCallback(fn) {
|
|
1781
|
+
const callbackRef = (0, react.useRef)(()=>{
|
|
1782
|
+
throw new Error("Cannot call an event handler while rendering.");
|
|
1783
|
+
});
|
|
1784
|
+
(0, react.useLayoutEffect)(()=>{
|
|
1785
|
+
callbackRef.current = fn;
|
|
1786
|
+
}, [
|
|
1787
|
+
fn
|
|
1788
|
+
]);
|
|
1789
|
+
return (0, react.useCallback)((...args)=>callbackRef.current?.(...args), [
|
|
1790
|
+
callbackRef
|
|
1791
|
+
]);
|
|
1764
1792
|
}
|
|
1765
1793
|
function wrap(config) {
|
|
1794
|
+
const { reloadOnForceUpdate = true, ...restConfig } = config;
|
|
1766
1795
|
return (WrappedComponent)=>{
|
|
1767
1796
|
const HotUpdaterHOC = ()=>{
|
|
1768
1797
|
const { progress } = useHotUpdaterStore();
|
|
1769
|
-
const [
|
|
1798
|
+
const [status, setStatus] = (0, react.useState)("IDLE");
|
|
1799
|
+
const initHotUpdater = useEventCallback(async ()=>{
|
|
1800
|
+
try {
|
|
1801
|
+
setStatus("CHECK_FOR_UPDATE");
|
|
1802
|
+
const updateInfo = await checkForUpdate({
|
|
1803
|
+
source: restConfig.source,
|
|
1804
|
+
requestHeaders: restConfig.requestHeaders
|
|
1805
|
+
});
|
|
1806
|
+
if (!updateInfo) {
|
|
1807
|
+
restConfig.onUpdateProcessCompleted?.({
|
|
1808
|
+
status: "UP_TO_DATE"
|
|
1809
|
+
});
|
|
1810
|
+
setStatus("UPDATE_PROCESS_COMPLETED");
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
setStatus("UPDATING");
|
|
1814
|
+
const isSuccess = await updateBundle(updateInfo.id, updateInfo.fileUrl);
|
|
1815
|
+
if (!isSuccess) throw new Error("New update was found but failed to download the bundle.");
|
|
1816
|
+
if (updateInfo.forceUpdate && reloadOnForceUpdate) reload();
|
|
1817
|
+
restConfig.onUpdateProcessCompleted?.({
|
|
1818
|
+
id: updateInfo.id,
|
|
1819
|
+
status: updateInfo.status,
|
|
1820
|
+
isForceUpdate: updateInfo.forceUpdate
|
|
1821
|
+
});
|
|
1822
|
+
setStatus("UPDATE_PROCESS_COMPLETED");
|
|
1823
|
+
} catch (error) {
|
|
1824
|
+
if (error instanceof HotUpdaterError) restConfig.onError?.(error);
|
|
1825
|
+
setStatus("UPDATE_PROCESS_COMPLETED");
|
|
1826
|
+
throw error;
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1770
1829
|
(0, react.useEffect)(()=>{
|
|
1771
|
-
|
|
1830
|
+
restConfig.onProgress?.(progress);
|
|
1772
1831
|
}, [
|
|
1773
1832
|
progress
|
|
1774
1833
|
]);
|
|
1775
|
-
(0, react.
|
|
1776
|
-
const initHotUpdater = async ()=>{
|
|
1777
|
-
try {
|
|
1778
|
-
const updateInfo = await checkUpdate(config);
|
|
1779
|
-
if (!updateInfo) {
|
|
1780
|
-
config.onCheckUpdateCompleted?.({
|
|
1781
|
-
isBundleUpdated: false
|
|
1782
|
-
});
|
|
1783
|
-
setIsCheckUpdateCompleted(true);
|
|
1784
|
-
return;
|
|
1785
|
-
}
|
|
1786
|
-
const isSuccess = await installUpdate(updateInfo);
|
|
1787
|
-
config.onCheckUpdateCompleted?.({
|
|
1788
|
-
isBundleUpdated: isSuccess
|
|
1789
|
-
});
|
|
1790
|
-
setIsCheckUpdateCompleted(true);
|
|
1791
|
-
} catch (error) {
|
|
1792
|
-
if (error instanceof HotUpdaterError) config.onError?.(error);
|
|
1793
|
-
setIsCheckUpdateCompleted(true);
|
|
1794
|
-
throw error;
|
|
1795
|
-
}
|
|
1796
|
-
};
|
|
1834
|
+
(0, react.useLayoutEffect)(()=>{
|
|
1797
1835
|
initHotUpdater();
|
|
1798
|
-
}, [
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
]);
|
|
1802
|
-
if (config.fallbackComponent && (!isCheckUpdateCompleted || progress > 0 && progress < 1)) {
|
|
1803
|
-
const Fallback = config.fallbackComponent;
|
|
1836
|
+
}, []);
|
|
1837
|
+
if (restConfig.fallbackComponent && "UPDATE_PROCESS_COMPLETED" !== status) {
|
|
1838
|
+
const Fallback = restConfig.fallbackComponent;
|
|
1804
1839
|
return /*#__PURE__*/ React.createElement(Fallback, {
|
|
1805
1840
|
progress: progress
|
|
1806
1841
|
});
|
|
@@ -1819,8 +1854,8 @@ const src_HotUpdater = {
|
|
|
1819
1854
|
getAppVersion: getAppVersion,
|
|
1820
1855
|
getBundleId: getBundleId,
|
|
1821
1856
|
addListener: addListener,
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1857
|
+
checkForUpdate: checkForUpdate,
|
|
1858
|
+
runUpdateProcess: runUpdateProcess,
|
|
1859
|
+
updateBundle: updateBundle
|
|
1825
1860
|
};
|
|
1826
1861
|
export { src_HotUpdater as HotUpdater, hotUpdaterStore, useHotUpdaterStore };
|
package/dist/native.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare const addListener: <T extends keyof HotUpdaterEvent>(eventName: T
|
|
|
8
8
|
* Downloads files from given URLs.
|
|
9
9
|
*
|
|
10
10
|
* @param {string} bundleId - identifier for the bundle version.
|
|
11
|
-
* @param {string | null} zipUrl - zip file URL.
|
|
11
|
+
* @param {string | null} zipUrl - zip file URL. If null, it means rolling back to the built-in bundle
|
|
12
12
|
* @returns {Promise<boolean>} Resolves with true if download was successful, otherwise rejects with an error.
|
|
13
13
|
*/
|
|
14
14
|
export declare const updateBundle: (bundleId: string, zipUrl: string | null) => Promise<boolean>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type CheckForUpdateConfig } from "./checkUpdate";
|
|
2
|
+
export type RunUpdateProcessResponse = {
|
|
3
|
+
status: "ROLLBACK" | "UPDATE";
|
|
4
|
+
isForceUpdate: boolean;
|
|
5
|
+
id: string;
|
|
6
|
+
} | {
|
|
7
|
+
status: "UP_TO_DATE";
|
|
8
|
+
};
|
|
9
|
+
export interface RunUpdateProcessConfig extends CheckForUpdateConfig {
|
|
10
|
+
/**
|
|
11
|
+
* If `true`, the app will be reloaded when the downloaded bundle is a force update.
|
|
12
|
+
* If `false`, isForceUpdate will be returned as true but the app won't reload.
|
|
13
|
+
* @default false
|
|
14
|
+
*/
|
|
15
|
+
reloadOnForceUpdate?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare const runUpdateProcess: ({ reloadOnForceUpdate, ...checkForUpdateConfig }: RunUpdateProcessConfig) => Promise<RunUpdateProcessResponse>;
|
package/dist/wrap.d.ts
CHANGED
|
@@ -1,18 +1,45 @@
|
|
|
1
|
-
import type { BundleArg, UpdateInfo } from "@hot-updater/core";
|
|
2
1
|
import type React from "react";
|
|
2
|
+
import { type CheckForUpdateConfig } from "./checkUpdate";
|
|
3
3
|
import { HotUpdaterError } from "./error";
|
|
4
|
+
import type { RunUpdateProcessResponse } from "./runUpdateProcess";
|
|
4
5
|
import { type HotUpdaterState } from "./store";
|
|
5
|
-
export interface
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
export interface HotUpdaterConfig extends CheckForUpdateConfig {
|
|
7
|
+
/**
|
|
8
|
+
* Component to show while downloading a new bundle update.
|
|
9
|
+
*
|
|
10
|
+
* When an update exists and the bundle is being downloaded, this component will block access
|
|
11
|
+
* to the entry point and show download progress.
|
|
12
|
+
*
|
|
13
|
+
* @see {@link https://gronxb.github.io/hot-updater/guide/hot-updater/wrap.html#fallback-component}
|
|
14
|
+
*
|
|
15
|
+
* ```tsx
|
|
16
|
+
* HotUpdater.wrap({
|
|
17
|
+
* source: "<update-server-url>",
|
|
18
|
+
* fallbackComponent: ({ progress = 0 }) => (
|
|
19
|
+
* <View style={styles.container}>
|
|
20
|
+
* <Text style={styles.text}>Updating... {progress}%</Text>
|
|
21
|
+
* </View>
|
|
22
|
+
* )
|
|
23
|
+
* })(App)
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* If not defined, the bundle will download in the background without blocking the screen.
|
|
27
|
+
*/
|
|
10
28
|
fallbackComponent?: React.FC<Pick<HotUpdaterState, "progress">>;
|
|
11
29
|
onError?: (error: HotUpdaterError) => void;
|
|
12
30
|
onProgress?: (progress: number) => void;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
31
|
+
/**
|
|
32
|
+
* When a force update exists, the app will automatically reload.
|
|
33
|
+
* If `false`, When a force update exists, the app will not reload. `isForceUpdate` will be returned as `true` in `onUpdateProcessCompleted`.
|
|
34
|
+
* If `true`, When a force update exists, the app will automatically reload.
|
|
35
|
+
* @default true
|
|
36
|
+
*/
|
|
37
|
+
reloadOnForceUpdate?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Callback function that is called when the update process is completed.
|
|
40
|
+
*
|
|
41
|
+
* @see {@link https://gronxb.github.io/hot-updater/guide/hot-updater/wrap.html#onupdateprocesscompleted}
|
|
42
|
+
*/
|
|
43
|
+
onUpdateProcessCompleted?: (response: RunUpdateProcessResponse) => void;
|
|
16
44
|
}
|
|
17
|
-
export declare function checkUpdate(config: CheckUpdateConfig): Promise<UpdateInfo | null>;
|
|
18
45
|
export declare function wrap<P>(config: HotUpdaterConfig): (WrappedComponent: React.ComponentType) => React.ComponentType<P>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/react-native",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "React Native OTA solution for self-hosted",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -75,8 +75,8 @@
|
|
|
75
75
|
"react-native-builder-bob": "^0.33.1"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@hot-updater/js": "0.5.
|
|
79
|
-
"@hot-updater/core": "0.5.
|
|
78
|
+
"@hot-updater/js": "0.5.2",
|
|
79
|
+
"@hot-updater/core": "0.5.2"
|
|
80
80
|
},
|
|
81
81
|
"scripts": {
|
|
82
82
|
"build": "rslib build",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Bundle, BundleArg, UpdateInfo } from "@hot-updater/core";
|
|
2
|
+
import { getUpdateInfo } from "@hot-updater/js";
|
|
3
|
+
import { Platform } from "react-native";
|
|
4
|
+
import { ensureUpdateInfo } from "./ensureUpdateInfo";
|
|
5
|
+
import { HotUpdaterError } from "./error";
|
|
6
|
+
import { getAppVersion, getBundleId } from "./native";
|
|
7
|
+
|
|
8
|
+
export interface CheckForUpdateConfig {
|
|
9
|
+
source: BundleArg;
|
|
10
|
+
requestHeaders?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function checkForUpdate(config: CheckForUpdateConfig) {
|
|
14
|
+
if (__DEV__) {
|
|
15
|
+
console.warn(
|
|
16
|
+
"[HotUpdater] __DEV__ is true, HotUpdater is only supported in production",
|
|
17
|
+
);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!["ios", "android"].includes(Platform.OS)) {
|
|
22
|
+
throw new HotUpdaterError(
|
|
23
|
+
"HotUpdater is only supported on iOS and Android",
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const currentAppVersion = await getAppVersion();
|
|
28
|
+
const platform = Platform.OS as "ios" | "android";
|
|
29
|
+
const currentBundleId = await getBundleId();
|
|
30
|
+
|
|
31
|
+
if (!currentAppVersion) {
|
|
32
|
+
throw new HotUpdaterError("Failed to get app version");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ensuredUpdateInfo = await ensureUpdateInfo(
|
|
36
|
+
config.source,
|
|
37
|
+
{
|
|
38
|
+
appVersion: currentAppVersion,
|
|
39
|
+
bundleId: currentBundleId,
|
|
40
|
+
platform,
|
|
41
|
+
},
|
|
42
|
+
config.requestHeaders,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
let updateInfo: UpdateInfo | null = null;
|
|
46
|
+
if (Array.isArray(ensuredUpdateInfo)) {
|
|
47
|
+
const bundles: Bundle[] = ensuredUpdateInfo;
|
|
48
|
+
|
|
49
|
+
updateInfo = await getUpdateInfo(bundles, {
|
|
50
|
+
appVersion: currentAppVersion,
|
|
51
|
+
bundleId: currentBundleId,
|
|
52
|
+
platform,
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
updateInfo = ensuredUpdateInfo;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return updateInfo;
|
|
59
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
type EventCallback<Args extends unknown[], R> =
|
|
4
|
+
| ((...args: Args) => R)
|
|
5
|
+
| undefined;
|
|
6
|
+
|
|
7
|
+
export function useEventCallback<Args extends unknown[], R>(
|
|
8
|
+
fn: (...args: Args) => R,
|
|
9
|
+
): (...args: Args) => R;
|
|
10
|
+
export function useEventCallback<Args extends unknown[], R>(
|
|
11
|
+
fn: EventCallback<Args, R>,
|
|
12
|
+
): EventCallback<Args, R>;
|
|
13
|
+
export function useEventCallback<Args extends unknown[], R>(
|
|
14
|
+
fn: EventCallback<Args, R>,
|
|
15
|
+
): EventCallback<Args, R> {
|
|
16
|
+
const callbackRef = useRef<EventCallback<Args, R>>(() => {
|
|
17
|
+
throw new Error("Cannot call an event handler while rendering.");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
useLayoutEffect(() => {
|
|
21
|
+
callbackRef.current = fn;
|
|
22
|
+
}, [fn]);
|
|
23
|
+
|
|
24
|
+
return useCallback(
|
|
25
|
+
(...args: Args) => callbackRef.current?.(...args),
|
|
26
|
+
[callbackRef],
|
|
27
|
+
) as (...args: Args) => R;
|
|
28
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ensureUpdateInfo } from "./ensureUpdateInfo";
|
|
1
|
+
import { checkForUpdate } from "./checkUpdate";
|
|
3
2
|
import {
|
|
4
3
|
addListener,
|
|
5
4
|
getAppVersion,
|
|
@@ -7,6 +6,7 @@ import {
|
|
|
7
6
|
reload,
|
|
8
7
|
updateBundle,
|
|
9
8
|
} from "./native";
|
|
9
|
+
import { runUpdateProcess } from "./runUpdateProcess";
|
|
10
10
|
import { hotUpdaterStore } from "./store";
|
|
11
11
|
import { wrap } from "./wrap";
|
|
12
12
|
|
|
@@ -27,7 +27,7 @@ export const HotUpdater = {
|
|
|
27
27
|
getBundleId,
|
|
28
28
|
addListener,
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
checkForUpdate,
|
|
31
|
+
runUpdateProcess,
|
|
31
32
|
updateBundle,
|
|
32
|
-
getUpdateInfo,
|
|
33
33
|
};
|
package/src/native.ts
CHANGED
|
@@ -52,7 +52,7 @@ export const addListener = <T extends keyof HotUpdaterEvent>(
|
|
|
52
52
|
* Downloads files from given URLs.
|
|
53
53
|
*
|
|
54
54
|
* @param {string} bundleId - identifier for the bundle version.
|
|
55
|
-
* @param {string | null} zipUrl - zip file URL.
|
|
55
|
+
* @param {string | null} zipUrl - zip file URL. If null, it means rolling back to the built-in bundle
|
|
56
56
|
* @returns {Promise<boolean>} Resolves with true if download was successful, otherwise rejects with an error.
|
|
57
57
|
*/
|
|
58
58
|
export const updateBundle = (
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type CheckForUpdateConfig, checkForUpdate } from "./checkUpdate";
|
|
2
|
+
import { reload, updateBundle } from "./native";
|
|
3
|
+
|
|
4
|
+
export type RunUpdateProcessResponse =
|
|
5
|
+
| {
|
|
6
|
+
status: "ROLLBACK" | "UPDATE";
|
|
7
|
+
isForceUpdate: boolean;
|
|
8
|
+
id: string;
|
|
9
|
+
}
|
|
10
|
+
| {
|
|
11
|
+
status: "UP_TO_DATE";
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export interface RunUpdateProcessConfig extends CheckForUpdateConfig {
|
|
15
|
+
/**
|
|
16
|
+
* If `true`, the app will be reloaded when the downloaded bundle is a force update.
|
|
17
|
+
* If `false`, isForceUpdate will be returned as true but the app won't reload.
|
|
18
|
+
* @default false
|
|
19
|
+
*/
|
|
20
|
+
reloadOnForceUpdate?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const runUpdateProcess = async ({
|
|
24
|
+
reloadOnForceUpdate = false,
|
|
25
|
+
...checkForUpdateConfig
|
|
26
|
+
}: RunUpdateProcessConfig): Promise<RunUpdateProcessResponse> => {
|
|
27
|
+
const updateInfo = await checkForUpdate(checkForUpdateConfig);
|
|
28
|
+
if (!updateInfo) {
|
|
29
|
+
return {
|
|
30
|
+
status: "UP_TO_DATE",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const isUpdated = await updateBundle(updateInfo.id, updateInfo.fileUrl);
|
|
35
|
+
if (isUpdated && updateInfo.forceUpdate && reloadOnForceUpdate) {
|
|
36
|
+
reload();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!isUpdated) {
|
|
40
|
+
throw new Error("New update was found but failed to download the bundle.");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
status: updateInfo.status,
|
|
44
|
+
isForceUpdate: updateInfo.forceUpdate,
|
|
45
|
+
id: updateInfo.id,
|
|
46
|
+
};
|
|
47
|
+
};
|
package/src/wrap.tsx
CHANGED
|
@@ -1,129 +1,122 @@
|
|
|
1
|
-
import type { Bundle, BundleArg, UpdateInfo } from "@hot-updater/core";
|
|
2
|
-
import { getUpdateInfo } from "@hot-updater/js";
|
|
3
1
|
import type React from "react";
|
|
4
|
-
import { useEffect, useState } from "react";
|
|
5
|
-
import {
|
|
6
|
-
import { ensureUpdateInfo } from "./ensureUpdateInfo";
|
|
2
|
+
import { useEffect, useLayoutEffect, useState } from "react";
|
|
3
|
+
import { type CheckForUpdateConfig, checkForUpdate } from "./checkUpdate";
|
|
7
4
|
import { HotUpdaterError } from "./error";
|
|
8
|
-
import {
|
|
5
|
+
import { useEventCallback } from "./hooks/useEventCallback";
|
|
6
|
+
import { reload, updateBundle } from "./native";
|
|
7
|
+
import type { RunUpdateProcessResponse } from "./runUpdateProcess";
|
|
9
8
|
import { type HotUpdaterState, useHotUpdaterStore } from "./store";
|
|
10
9
|
|
|
11
|
-
export interface
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
export interface HotUpdaterConfig extends CheckForUpdateConfig {
|
|
11
|
+
/**
|
|
12
|
+
* Component to show while downloading a new bundle update.
|
|
13
|
+
*
|
|
14
|
+
* When an update exists and the bundle is being downloaded, this component will block access
|
|
15
|
+
* to the entry point and show download progress.
|
|
16
|
+
*
|
|
17
|
+
* @see {@link https://gronxb.github.io/hot-updater/guide/hot-updater/wrap.html#fallback-component}
|
|
18
|
+
*
|
|
19
|
+
* ```tsx
|
|
20
|
+
* HotUpdater.wrap({
|
|
21
|
+
* source: "<update-server-url>",
|
|
22
|
+
* fallbackComponent: ({ progress = 0 }) => (
|
|
23
|
+
* <View style={styles.container}>
|
|
24
|
+
* <Text style={styles.text}>Updating... {progress}%</Text>
|
|
25
|
+
* </View>
|
|
26
|
+
* )
|
|
27
|
+
* })(App)
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* If not defined, the bundle will download in the background without blocking the screen.
|
|
31
|
+
*/
|
|
17
32
|
fallbackComponent?: React.FC<Pick<HotUpdaterState, "progress">>;
|
|
18
33
|
onError?: (error: HotUpdaterError) => void;
|
|
19
34
|
onProgress?: (progress: number) => void;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (!["ios", "android"].includes(Platform.OS)) {
|
|
34
|
-
throw new HotUpdaterError(
|
|
35
|
-
"HotUpdater is only supported on iOS and Android",
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const currentAppVersion = await getAppVersion();
|
|
40
|
-
const platform = Platform.OS as "ios" | "android";
|
|
41
|
-
const currentBundleId = await getBundleId();
|
|
42
|
-
|
|
43
|
-
if (!currentAppVersion) {
|
|
44
|
-
throw new HotUpdaterError("Failed to get app version");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const ensuredUpdateInfo = await ensureUpdateInfo(
|
|
48
|
-
config.source,
|
|
49
|
-
{
|
|
50
|
-
appVersion: currentAppVersion,
|
|
51
|
-
bundleId: currentBundleId,
|
|
52
|
-
platform,
|
|
53
|
-
},
|
|
54
|
-
config.requestHeaders,
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
let updateInfo: UpdateInfo | null = null;
|
|
58
|
-
if (Array.isArray(ensuredUpdateInfo)) {
|
|
59
|
-
const bundles: Bundle[] = ensuredUpdateInfo;
|
|
60
|
-
|
|
61
|
-
updateInfo = await getUpdateInfo(bundles, {
|
|
62
|
-
appVersion: currentAppVersion,
|
|
63
|
-
bundleId: currentBundleId,
|
|
64
|
-
platform,
|
|
65
|
-
});
|
|
66
|
-
} else {
|
|
67
|
-
updateInfo = ensuredUpdateInfo;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return updateInfo;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function installUpdate(updateInfo: UpdateInfo) {
|
|
74
|
-
const isSuccess = await updateBundle(updateInfo.id, updateInfo.fileUrl || "");
|
|
75
|
-
|
|
76
|
-
if (isSuccess && updateInfo.forceUpdate) {
|
|
77
|
-
reload();
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return isSuccess;
|
|
35
|
+
/**
|
|
36
|
+
* When a force update exists, the app will automatically reload.
|
|
37
|
+
* If `false`, When a force update exists, the app will not reload. `isForceUpdate` will be returned as `true` in `onUpdateProcessCompleted`.
|
|
38
|
+
* If `true`, When a force update exists, the app will automatically reload.
|
|
39
|
+
* @default true
|
|
40
|
+
*/
|
|
41
|
+
reloadOnForceUpdate?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Callback function that is called when the update process is completed.
|
|
44
|
+
*
|
|
45
|
+
* @see {@link https://gronxb.github.io/hot-updater/guide/hot-updater/wrap.html#onupdateprocesscompleted}
|
|
46
|
+
*/
|
|
47
|
+
onUpdateProcessCompleted?: (response: RunUpdateProcessResponse) => void;
|
|
82
48
|
}
|
|
83
49
|
|
|
84
50
|
export function wrap<P>(
|
|
85
51
|
config: HotUpdaterConfig,
|
|
86
52
|
): (WrappedComponent: React.ComponentType) => React.ComponentType<P> {
|
|
53
|
+
const { reloadOnForceUpdate = true, ...restConfig } = config;
|
|
87
54
|
return (WrappedComponent) => {
|
|
88
55
|
const HotUpdaterHOC: React.FC<P> = () => {
|
|
89
56
|
const { progress } = useHotUpdaterStore();
|
|
90
|
-
const [
|
|
91
|
-
|
|
57
|
+
const [status, setStatus] = useState<
|
|
58
|
+
"IDLE" | "CHECK_FOR_UPDATE" | "UPDATING" | "UPDATE_PROCESS_COMPLETED"
|
|
59
|
+
>("IDLE");
|
|
60
|
+
|
|
61
|
+
const initHotUpdater = useEventCallback(async () => {
|
|
62
|
+
try {
|
|
63
|
+
setStatus("CHECK_FOR_UPDATE");
|
|
64
|
+
const updateInfo = await checkForUpdate({
|
|
65
|
+
source: restConfig.source,
|
|
66
|
+
requestHeaders: restConfig.requestHeaders,
|
|
67
|
+
});
|
|
68
|
+
if (!updateInfo) {
|
|
69
|
+
restConfig.onUpdateProcessCompleted?.({
|
|
70
|
+
status: "UP_TO_DATE",
|
|
71
|
+
});
|
|
72
|
+
setStatus("UPDATE_PROCESS_COMPLETED");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
92
75
|
|
|
93
|
-
|
|
94
|
-
config.onProgress?.(progress);
|
|
95
|
-
}, [progress]);
|
|
76
|
+
setStatus("UPDATING");
|
|
96
77
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
78
|
+
const isSuccess = await updateBundle(
|
|
79
|
+
updateInfo.id,
|
|
80
|
+
updateInfo.fileUrl,
|
|
81
|
+
);
|
|
82
|
+
if (!isSuccess) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
"New update was found but failed to download the bundle.",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (updateInfo.forceUpdate && reloadOnForceUpdate) {
|
|
89
|
+
reload();
|
|
90
|
+
}
|
|
106
91
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
92
|
+
restConfig.onUpdateProcessCompleted?.({
|
|
93
|
+
id: updateInfo.id,
|
|
94
|
+
status: updateInfo.status,
|
|
95
|
+
isForceUpdate: updateInfo.forceUpdate,
|
|
96
|
+
});
|
|
97
|
+
setStatus("UPDATE_PROCESS_COMPLETED");
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error instanceof HotUpdaterError) {
|
|
100
|
+
restConfig.onError?.(error);
|
|
116
101
|
}
|
|
117
|
-
|
|
102
|
+
setStatus("UPDATE_PROCESS_COMPLETED");
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
restConfig.onProgress?.(progress);
|
|
109
|
+
}, [progress]);
|
|
118
110
|
|
|
111
|
+
useLayoutEffect(() => {
|
|
119
112
|
initHotUpdater();
|
|
120
|
-
}, [
|
|
113
|
+
}, []);
|
|
121
114
|
|
|
122
115
|
if (
|
|
123
|
-
|
|
124
|
-
|
|
116
|
+
restConfig.fallbackComponent &&
|
|
117
|
+
status !== "UPDATE_PROCESS_COMPLETED"
|
|
125
118
|
) {
|
|
126
|
-
const Fallback =
|
|
119
|
+
const Fallback = restConfig.fallbackComponent;
|
|
127
120
|
return <Fallback progress={progress} />;
|
|
128
121
|
}
|
|
129
122
|
|