@hot-updater/react-native 0.5.0 → 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.
@@ -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
- ensureUpdateInfo: (source: import("@hot-updater/core").BundleArg, { appVersion, bundleId, platform }: import("@hot-updater/core").GetBundlesArgs, requestHeaders?: Record<string, string>) => Promise<import("@hot-updater/core").Bundle[] | import("@hot-updater/core").UpdateInfo>;
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
- class HotUpdaterError extends Error {
1755
- constructor(message){
1756
- super(message);
1757
- this.name = "HotUpdaterError";
1758
- }
1759
- }
1760
- async function checkUpdate(config) {
1761
- if (__DEV__) {
1762
- console.warn("[HotUpdater] __DEV__ is true, HotUpdater is only supported in production");
1763
- return null;
1764
- }
1765
- if (![
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 [isCheckUpdateCompleted, setIsCheckUpdateCompleted] = (0, react.useState)(false);
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
- config.onProgress?.(progress);
1862
+ restConfig.onProgress?.(progress);
1804
1863
  }, [
1805
1864
  progress
1806
1865
  ]);
1807
- (0, react.useEffect)(()=>{
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
- config.source,
1832
- config.requestHeaders
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
- ensureUpdateInfo: ensureUpdateInfo,
1855
- updateBundle: updateBundle,
1856
- getUpdateInfo: js_namespaceObject.getUpdateInfo
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
- var external_react_native_ = __webpack_require__("react-native");
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
- class HotUpdaterError extends Error {
1723
- constructor(message){
1724
- super(message);
1725
- this.name = "HotUpdaterError";
1726
- }
1727
- }
1728
- async function checkUpdate(config) {
1729
- if (__DEV__) {
1730
- console.warn("[HotUpdater] __DEV__ is true, HotUpdater is only supported in production");
1731
- return null;
1732
- }
1733
- if (![
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 [isCheckUpdateCompleted, setIsCheckUpdateCompleted] = (0, react.useState)(false);
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
- config.onProgress?.(progress);
1830
+ restConfig.onProgress?.(progress);
1772
1831
  }, [
1773
1832
  progress
1774
1833
  ]);
1775
- (0, react.useEffect)(()=>{
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
- config.source,
1800
- config.requestHeaders
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
- ensureUpdateInfo: ensureUpdateInfo,
1823
- updateBundle: updateBundle,
1824
- getUpdateInfo: __WEBPACK_EXTERNAL_MODULE__hot_updater_js__.getUpdateInfo
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 CheckUpdateConfig {
6
- source: BundleArg;
7
- requestHeaders?: Record<string, string>;
8
- }
9
- export interface HotUpdaterConfig extends CheckUpdateConfig {
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
- onCheckUpdateCompleted?: ({ isBundleUpdated, }: {
14
- isBundleUpdated: boolean;
15
- }) => void;
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.0",
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.0",
79
- "@hot-updater/core": "0.5.0"
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 { getUpdateInfo } from "@hot-updater/js";
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
- ensureUpdateInfo,
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 { Platform } from "react-native";
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 { getAppVersion, getBundleId, reload, updateBundle } from "./native";
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 CheckUpdateConfig {
12
- source: BundleArg;
13
- requestHeaders?: Record<string, string>;
14
- }
15
-
16
- export interface HotUpdaterConfig extends CheckUpdateConfig {
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
- onCheckUpdateCompleted?: ({
21
- isBundleUpdated,
22
- }: { isBundleUpdated: boolean }) => void;
23
- }
24
-
25
- export async function checkUpdate(config: CheckUpdateConfig) {
26
- if (__DEV__) {
27
- console.warn(
28
- "[HotUpdater] __DEV__ is true, HotUpdater is only supported in production",
29
- );
30
- return null;
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 [isCheckUpdateCompleted, setIsCheckUpdateCompleted] =
91
- useState(false);
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
- useEffect(() => {
94
- config.onProgress?.(progress);
95
- }, [progress]);
76
+ setStatus("UPDATING");
96
77
 
97
- useEffect(() => {
98
- const initHotUpdater = async () => {
99
- try {
100
- const updateInfo = await checkUpdate(config);
101
- if (!updateInfo) {
102
- config.onCheckUpdateCompleted?.({ isBundleUpdated: false });
103
- setIsCheckUpdateCompleted(true);
104
- return;
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
- const isSuccess = await installUpdate(updateInfo);
108
- config.onCheckUpdateCompleted?.({ isBundleUpdated: isSuccess });
109
- setIsCheckUpdateCompleted(true);
110
- } catch (error) {
111
- if (error instanceof HotUpdaterError) {
112
- config.onError?.(error);
113
- }
114
- setIsCheckUpdateCompleted(true);
115
- throw error;
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
- }, [config.source, config.requestHeaders]);
113
+ }, []);
121
114
 
122
115
  if (
123
- config.fallbackComponent &&
124
- (!isCheckUpdateCompleted || (progress > 0 && progress < 1))
116
+ restConfig.fallbackComponent &&
117
+ status !== "UPDATE_PROCESS_COMPLETED"
125
118
  ) {
126
- const Fallback = config.fallbackComponent;
119
+ const Fallback = restConfig.fallbackComponent;
127
120
  return <Fallback progress={progress} />;
128
121
  }
129
122