@hot-updater/react-native 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,96 +1,3 @@
1
- # hot-updater (WIP)
2
- React Native OTA solution for internal infrastructure
1
+ # Hot Updater
3
2
 
4
- ## IOS Usage
5
- * as-is
6
- ```objective-c
7
- // filename: ios/MyApp/AppDelegate.mm
8
- // ...
9
- #import <HotUpdater/HotUpdater.h>
10
-
11
- // ...
12
-
13
- - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
14
- {
15
- #if DEBUG
16
- return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
17
- #else
18
- return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
19
- #endif
20
- }
21
-
22
- // ...
23
- ```
24
-
25
- * to-be
26
- ```objective-c
27
- // filename: ios/MyApp/AppDelegate.mm
28
- // ...
29
-
30
- - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
31
- {
32
- return [HotUpdater bundleURL];
33
- }
34
-
35
- // ...
36
- ```
37
-
38
- ## Android Usage
39
- ```kotlin
40
- package com.hotupdaterexample
41
-
42
- import android.app.Application
43
- import com.facebook.react.PackageList
44
- import com.facebook.react.ReactApplication
45
- import com.facebook.react.ReactHost
46
- import com.facebook.react.ReactNativeHost
47
- import com.facebook.react.ReactPackage
48
- import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
49
- import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
50
- import com.facebook.react.defaults.DefaultReactNativeHost
51
- import com.facebook.soloader.SoLoader
52
- import com.hotupdater.HotUpdater
53
-
54
- class MainApplication : Application(), ReactApplication {
55
-
56
- override val reactNativeHost: ReactNativeHost =
57
- object : DefaultReactNativeHost(this) {
58
- override fun getPackages(): List<ReactPackage> =
59
- PackageList(this).packages.apply {
60
- // Packages that cannot be autolinked yet can be added manually here, for example:
61
- // add(MyReactNativePackage())
62
- }
63
-
64
- override fun getJSMainModuleName(): String = "index"
65
-
66
- override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
67
-
68
- override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
69
- override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
70
-
71
- override fun getJSBundleFile(): String? {
72
- // This field
73
- return HotUpdater.getJSBundleFile() ?: super.getJSBundleFile()
74
- }
75
- }
76
-
77
- override val reactHost: ReactHost
78
- get() = getDefaultReactHost(applicationContext, reactNativeHost)
79
-
80
- override fun onCreate() {
81
- super.onCreate()
82
- SoLoader.init(this, false)
83
- // This field
84
- HotUpdater.init(applicationContext, reactNativeHost)
85
- if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
86
- // If you opted-in for the New Architecture, we load the native entry point for this app.
87
- load()
88
- }
89
- }
90
- }
91
- ```
92
-
93
- ## Android Debug
94
- ```sh
95
- > adb logcat -s HotUpdater
96
- ```
3
+ A self-hostable OTA (Over-The-Air) update solution for React Native.
@@ -0,0 +1,2 @@
1
+ import type { Bundle, BundleArg, GetBundlesArgs, UpdateInfo } from "@hot-updater/core";
2
+ export declare const ensureUpdateInfo: (source: BundleArg, { appVersion, bundleId, platform }: GetBundlesArgs, requestHeaders?: Record<string, string>) => Promise<Bundle[] | UpdateInfo>;
package/dist/index.d.ts CHANGED
@@ -1,17 +1,14 @@
1
- export type * from "./init";
1
+ import { wrap } from "./wrap";
2
+ export type * from "./wrap";
2
3
  export type * from "./native";
3
4
  export * from "./store";
4
5
  export declare const HotUpdater: {
5
- init: (config: import("./init").HotUpdaterInitConfig) => Promise<void>;
6
+ wrap: typeof wrap;
6
7
  reload: () => void;
7
8
  getAppVersion: () => Promise<string | null>;
8
9
  getBundleId: () => string;
9
10
  addListener: <T extends keyof import("./native").HotUpdaterEvent>(eventName: T, listener: (event: import("./native").HotUpdaterEvent[T]) => void) => void;
10
- ensureBundles: (bundle: 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>;
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>;
11
12
  updateBundle: (bundleId: string, zipUrl: string | null) => Promise<boolean>;
12
13
  getUpdateInfo: (bundles: import("@hot-updater/core").Bundle[], { platform, bundleId, appVersion }: import("@hot-updater/core").GetBundlesArgs) => Promise<import("@hot-updater/core").UpdateInfo | null>;
13
- /**
14
- * In production environment, this value will be replaced with a uuidv7.
15
- */
16
- HOT_UPDATER_BUNDLE_ID: string;
17
14
  };
package/dist/index.js CHANGED
@@ -1582,6 +1582,12 @@ var __webpack_modules__ = {
1582
1582
  }, V = {
1583
1583
  transition: null
1584
1584
  };
1585
+ exports1.useEffect = function(a, b) {
1586
+ return U.current.useEffect(a, b);
1587
+ };
1588
+ exports1.useState = function(a) {
1589
+ return U.current.useState(a);
1590
+ };
1585
1591
  exports1.useSyncExternalStore = function(a, b, e) {
1586
1592
  return U.current.useSyncExternalStore(a, b, e);
1587
1593
  };
@@ -3078,11 +3084,11 @@ const getUpdateInfo = async (bundles, { platform, bundleId, appVersion })=>{
3078
3084
  };
3079
3085
  return null;
3080
3086
  };
3081
- const ensureBundles = async (bundle, { appVersion, bundleId, platform }, requestHeaders)=>{
3087
+ const ensureUpdateInfo = async (source, { appVersion, bundleId, platform }, requestHeaders)=>{
3082
3088
  try {
3083
3089
  let bundles = null;
3084
- if ("string" == typeof bundle) {
3085
- if (bundle.startsWith("http")) return await fetch(bundle, {
3090
+ if ("string" == typeof source) {
3091
+ if (source.startsWith("http")) return await fetch(source, {
3086
3092
  headers: {
3087
3093
  "x-app-platform": platform,
3088
3094
  "x-app-version": appVersion,
@@ -3090,19 +3096,16 @@ const ensureBundles = async (bundle, { appVersion, bundleId, platform }, request
3090
3096
  ...requestHeaders
3091
3097
  }
3092
3098
  }).then((res)=>res.json());
3093
- } else bundles = "function" == typeof bundle ? await bundle() : bundle;
3099
+ } else bundles = "function" == typeof source ? await source() : source;
3094
3100
  return bundles ?? [];
3095
3101
  } catch {
3096
3102
  return [];
3097
3103
  }
3098
3104
  };
3099
3105
  var external_react_native_ = __webpack_require__("react-native");
3100
- class HotUpdaterError extends Error {
3101
- constructor(message){
3102
- super(message);
3103
- this.name = "HotUpdaterError";
3104
- }
3105
- }
3106
+ const HotUpdater = {
3107
+ HOT_UPDATER_BUNDLE_ID: core_namespaceObject.NIL_UUID
3108
+ };
3106
3109
  const LINKING_ERROR = `The package '@hot-updater/react-native' doesn't seem to be linked. Make sure: \n\n` + external_react_native_.Platform.select({
3107
3110
  ios: "- You have run 'pod install'\n",
3108
3111
  default: ""
@@ -3118,59 +3121,12 @@ const addListener = (eventName, listener)=>{
3118
3121
  const eventEmitter = new external_react_native_.NativeEventEmitter(HotUpdaterNative);
3119
3122
  eventEmitter?.addListener(eventName, listener);
3120
3123
  };
3121
- const getBundleId = ()=>HotUpdater.HOT_UPDATER_BUNDLE_ID ?? core_namespaceObject.NIL_UUID;
3122
3124
  const updateBundle = (bundleId, zipUrl)=>HotUpdaterNative.updateBundle(bundleId, zipUrl);
3123
3125
  const getAppVersion = ()=>HotUpdaterNative.getAppVersion();
3124
3126
  const reload = ()=>{
3125
3127
  HotUpdaterNative.reload();
3126
3128
  };
3127
- const init = async (config)=>{
3128
- if (__DEV__) {
3129
- console.warn("[HotUpdater] __DEV__ is true, HotUpdater is only supported in production");
3130
- return;
3131
- }
3132
- if (![
3133
- "ios",
3134
- "android"
3135
- ].includes(external_react_native_.Platform.OS)) {
3136
- const error = new HotUpdaterError("HotUpdater is only supported on iOS and Android");
3137
- config?.onError?.(error);
3138
- throw error;
3139
- }
3140
- const currentAppVersion = await getAppVersion();
3141
- const platform = external_react_native_.Platform.OS;
3142
- const currentBundleId = await getBundleId();
3143
- if (!currentAppVersion) {
3144
- const error = new HotUpdaterError("Failed to get app version");
3145
- config?.onError?.(error);
3146
- throw error;
3147
- }
3148
- const bundles = await ensureBundles(config.source, {
3149
- appVersion: currentAppVersion,
3150
- bundleId: currentBundleId,
3151
- platform
3152
- }, config.requestHeaders);
3153
- let updateInfo = null;
3154
- updateInfo = Array.isArray(bundles) ? await getUpdateInfo(bundles, {
3155
- appVersion: currentAppVersion,
3156
- bundleId: currentBundleId,
3157
- platform
3158
- }) : bundles;
3159
- if (!updateInfo) {
3160
- config?.onSuccess?.("UP_TO_DATE");
3161
- return;
3162
- }
3163
- try {
3164
- const isSuccess = await updateBundle(updateInfo.id, updateInfo.fileUrl || "");
3165
- if (isSuccess && updateInfo.forceUpdate) {
3166
- reload();
3167
- config?.onSuccess?.("INSTALLING_UPDATE");
3168
- }
3169
- } catch (error) {
3170
- if (error instanceof HotUpdaterError) config?.onError?.(error);
3171
- throw error;
3172
- }
3173
- };
3129
+ const getBundleId = ()=>HotUpdater.HOT_UPDATER_BUNDLE_ID;
3174
3130
  var react = __webpack_require__("../../node_modules/.pnpm/react@18.3.1/node_modules/react/index.js");
3175
3131
  const createHotUpdaterStore = ()=>{
3176
3132
  let state = {
@@ -3200,21 +3156,105 @@ const createHotUpdaterStore = ()=>{
3200
3156
  };
3201
3157
  const hotUpdaterStore = createHotUpdaterStore();
3202
3158
  const useHotUpdaterStore = ()=>(0, react.useSyncExternalStore)(hotUpdaterStore.subscribe, hotUpdaterStore.getState, hotUpdaterStore.getState);
3159
+ class HotUpdaterError extends Error {
3160
+ constructor(message){
3161
+ super(message);
3162
+ this.name = "HotUpdaterError";
3163
+ }
3164
+ }
3165
+ async function checkUpdate(config) {
3166
+ if (__DEV__) {
3167
+ console.warn("[HotUpdater] __DEV__ is true, HotUpdater is only supported in production");
3168
+ return null;
3169
+ }
3170
+ if (![
3171
+ "ios",
3172
+ "android"
3173
+ ].includes(external_react_native_.Platform.OS)) throw new HotUpdaterError("HotUpdater is only supported on iOS and Android");
3174
+ const currentAppVersion = await getAppVersion();
3175
+ const platform = external_react_native_.Platform.OS;
3176
+ const currentBundleId = await getBundleId();
3177
+ if (!currentAppVersion) throw new HotUpdaterError("Failed to get app version");
3178
+ const ensuredUpdateInfo = await ensureUpdateInfo(config.source, {
3179
+ appVersion: currentAppVersion,
3180
+ bundleId: currentBundleId,
3181
+ platform
3182
+ }, config.requestHeaders);
3183
+ let updateInfo = null;
3184
+ if (Array.isArray(ensuredUpdateInfo)) {
3185
+ const bundles = ensuredUpdateInfo;
3186
+ updateInfo = await getUpdateInfo(bundles, {
3187
+ appVersion: currentAppVersion,
3188
+ bundleId: currentBundleId,
3189
+ platform
3190
+ });
3191
+ } else updateInfo = ensuredUpdateInfo;
3192
+ return updateInfo;
3193
+ }
3194
+ async function installUpdate(updateInfo) {
3195
+ const isSuccess = await updateBundle(updateInfo.id, updateInfo.fileUrl || "");
3196
+ if (isSuccess && updateInfo.forceUpdate) {
3197
+ reload();
3198
+ return true;
3199
+ }
3200
+ return isSuccess;
3201
+ }
3202
+ function wrap(config) {
3203
+ return (WrappedComponent)=>{
3204
+ const HotUpdaterHOC = (props)=>{
3205
+ const [updateStatus, setUpdateStatus] = (0, react.useState)(null);
3206
+ const [updateError, setUpdateError] = (0, react.useState)(null);
3207
+ const { progress } = useHotUpdaterStore();
3208
+ (0, react.useEffect)(()=>{
3209
+ const initHotUpdater = async ()=>{
3210
+ try {
3211
+ const updateInfo = await checkUpdate(config);
3212
+ if (!updateInfo) {
3213
+ setUpdateStatus("UP_TO_DATE");
3214
+ return;
3215
+ }
3216
+ setUpdateStatus("UPDATING");
3217
+ const isSuccess = await installUpdate(updateInfo);
3218
+ if (isSuccess) setUpdateStatus("INSTALLING_UPDATE");
3219
+ } catch (error) {
3220
+ if (error instanceof HotUpdaterError) setUpdateError(error);
3221
+ throw error;
3222
+ }
3223
+ };
3224
+ initHotUpdater();
3225
+ }, [
3226
+ config.source,
3227
+ config.requestHeaders
3228
+ ]);
3229
+ if ("UPDATING" === updateStatus && config.fallbackComponent) {
3230
+ const Fallback = config.fallbackComponent;
3231
+ return /*#__PURE__*/ React.createElement(Fallback, {
3232
+ progress: progress
3233
+ });
3234
+ }
3235
+ return /*#__PURE__*/ React.createElement(WrappedComponent, {
3236
+ ...props,
3237
+ updateStatus: updateStatus,
3238
+ updateError: updateError
3239
+ });
3240
+ };
3241
+ return HotUpdaterHOC;
3242
+ };
3243
+ }
3203
3244
  addListener("onProgress", ({ progress })=>{
3204
3245
  hotUpdaterStore.setState({
3205
3246
  progress
3206
3247
  });
3207
3248
  });
3208
3249
  const src_HotUpdater = {
3209
- init: init,
3250
+ wrap: wrap,
3210
3251
  reload: reload,
3211
3252
  getAppVersion: getAppVersion,
3212
3253
  getBundleId: getBundleId,
3213
3254
  addListener: addListener,
3214
- ensureBundles: ensureBundles,
3255
+ ensureUpdateInfo: ensureUpdateInfo,
3215
3256
  updateBundle: updateBundle,
3216
- getUpdateInfo: getUpdateInfo,
3217
- HOT_UPDATER_BUNDLE_ID: core_namespaceObject.NIL_UUID
3257
+ getUpdateInfo: getUpdateInfo
3218
3258
  };
3219
3259
  var __webpack_export_target__ = exports;
3220
3260
  for(var __webpack_i__ in __webpack_exports__)__webpack_export_target__[__webpack_i__] = __webpack_exports__[__webpack_i__];
package/dist/index.mjs CHANGED
@@ -1583,6 +1583,12 @@ var __webpack_modules__ = {
1583
1583
  }, V = {
1584
1584
  transition: null
1585
1585
  };
1586
+ exports.useEffect = function(a, b) {
1587
+ return U.current.useEffect(a, b);
1588
+ };
1589
+ exports.useState = function(a) {
1590
+ return U.current.useState(a);
1591
+ };
1586
1592
  exports.useSyncExternalStore = function(a, b, e) {
1587
1593
  return U.current.useSyncExternalStore(a, b, e);
1588
1594
  };
@@ -3046,11 +3052,11 @@ const getUpdateInfo = async (bundles, { platform, bundleId, appVersion })=>{
3046
3052
  };
3047
3053
  return null;
3048
3054
  };
3049
- const ensureBundles = async (bundle, { appVersion, bundleId, platform }, requestHeaders)=>{
3055
+ const ensureUpdateInfo = async (source, { appVersion, bundleId, platform }, requestHeaders)=>{
3050
3056
  try {
3051
3057
  let bundles = null;
3052
- if ("string" == typeof bundle) {
3053
- if (bundle.startsWith("http")) return await fetch(bundle, {
3058
+ if ("string" == typeof source) {
3059
+ if (source.startsWith("http")) return await fetch(source, {
3054
3060
  headers: {
3055
3061
  "x-app-platform": platform,
3056
3062
  "x-app-version": appVersion,
@@ -3058,19 +3064,16 @@ const ensureBundles = async (bundle, { appVersion, bundleId, platform }, request
3058
3064
  ...requestHeaders
3059
3065
  }
3060
3066
  }).then((res)=>res.json());
3061
- } else bundles = "function" == typeof bundle ? await bundle() : bundle;
3067
+ } else bundles = "function" == typeof source ? await source() : source;
3062
3068
  return bundles ?? [];
3063
3069
  } catch {
3064
3070
  return [];
3065
3071
  }
3066
3072
  };
3067
3073
  var external_react_native_ = __webpack_require__("react-native");
3068
- class HotUpdaterError extends Error {
3069
- constructor(message){
3070
- super(message);
3071
- this.name = "HotUpdaterError";
3072
- }
3073
- }
3074
+ const HotUpdater = {
3075
+ HOT_UPDATER_BUNDLE_ID: __WEBPACK_EXTERNAL_MODULE__hot_updater_core__.NIL_UUID
3076
+ };
3074
3077
  const LINKING_ERROR = `The package '@hot-updater/react-native' doesn't seem to be linked. Make sure: \n\n` + external_react_native_.Platform.select({
3075
3078
  ios: "- You have run 'pod install'\n",
3076
3079
  default: ""
@@ -3086,59 +3089,12 @@ const addListener = (eventName, listener)=>{
3086
3089
  const eventEmitter = new external_react_native_.NativeEventEmitter(HotUpdaterNative);
3087
3090
  eventEmitter?.addListener(eventName, listener);
3088
3091
  };
3089
- const getBundleId = ()=>HotUpdater.HOT_UPDATER_BUNDLE_ID ?? __WEBPACK_EXTERNAL_MODULE__hot_updater_core__.NIL_UUID;
3090
3092
  const updateBundle = (bundleId, zipUrl)=>HotUpdaterNative.updateBundle(bundleId, zipUrl);
3091
3093
  const getAppVersion = ()=>HotUpdaterNative.getAppVersion();
3092
3094
  const reload = ()=>{
3093
3095
  HotUpdaterNative.reload();
3094
3096
  };
3095
- const init = async (config)=>{
3096
- if (__DEV__) {
3097
- console.warn("[HotUpdater] __DEV__ is true, HotUpdater is only supported in production");
3098
- return;
3099
- }
3100
- if (![
3101
- "ios",
3102
- "android"
3103
- ].includes(external_react_native_.Platform.OS)) {
3104
- const error = new HotUpdaterError("HotUpdater is only supported on iOS and Android");
3105
- config?.onError?.(error);
3106
- throw error;
3107
- }
3108
- const currentAppVersion = await getAppVersion();
3109
- const platform = external_react_native_.Platform.OS;
3110
- const currentBundleId = await getBundleId();
3111
- if (!currentAppVersion) {
3112
- const error = new HotUpdaterError("Failed to get app version");
3113
- config?.onError?.(error);
3114
- throw error;
3115
- }
3116
- const bundles = await ensureBundles(config.source, {
3117
- appVersion: currentAppVersion,
3118
- bundleId: currentBundleId,
3119
- platform
3120
- }, config.requestHeaders);
3121
- let updateInfo = null;
3122
- updateInfo = Array.isArray(bundles) ? await getUpdateInfo(bundles, {
3123
- appVersion: currentAppVersion,
3124
- bundleId: currentBundleId,
3125
- platform
3126
- }) : bundles;
3127
- if (!updateInfo) {
3128
- config?.onSuccess?.("UP_TO_DATE");
3129
- return;
3130
- }
3131
- try {
3132
- const isSuccess = await updateBundle(updateInfo.id, updateInfo.fileUrl || "");
3133
- if (isSuccess && updateInfo.forceUpdate) {
3134
- reload();
3135
- config?.onSuccess?.("INSTALLING_UPDATE");
3136
- }
3137
- } catch (error) {
3138
- if (error instanceof HotUpdaterError) config?.onError?.(error);
3139
- throw error;
3140
- }
3141
- };
3097
+ const getBundleId = ()=>HotUpdater.HOT_UPDATER_BUNDLE_ID;
3142
3098
  var react = __webpack_require__("../../node_modules/.pnpm/react@18.3.1/node_modules/react/index.js");
3143
3099
  const createHotUpdaterStore = ()=>{
3144
3100
  let state = {
@@ -3168,20 +3124,104 @@ const createHotUpdaterStore = ()=>{
3168
3124
  };
3169
3125
  const hotUpdaterStore = createHotUpdaterStore();
3170
3126
  const useHotUpdaterStore = ()=>(0, react.useSyncExternalStore)(hotUpdaterStore.subscribe, hotUpdaterStore.getState, hotUpdaterStore.getState);
3127
+ class HotUpdaterError extends Error {
3128
+ constructor(message){
3129
+ super(message);
3130
+ this.name = "HotUpdaterError";
3131
+ }
3132
+ }
3133
+ async function checkUpdate(config) {
3134
+ if (__DEV__) {
3135
+ console.warn("[HotUpdater] __DEV__ is true, HotUpdater is only supported in production");
3136
+ return null;
3137
+ }
3138
+ if (![
3139
+ "ios",
3140
+ "android"
3141
+ ].includes(external_react_native_.Platform.OS)) throw new HotUpdaterError("HotUpdater is only supported on iOS and Android");
3142
+ const currentAppVersion = await getAppVersion();
3143
+ const platform = external_react_native_.Platform.OS;
3144
+ const currentBundleId = await getBundleId();
3145
+ if (!currentAppVersion) throw new HotUpdaterError("Failed to get app version");
3146
+ const ensuredUpdateInfo = await ensureUpdateInfo(config.source, {
3147
+ appVersion: currentAppVersion,
3148
+ bundleId: currentBundleId,
3149
+ platform
3150
+ }, config.requestHeaders);
3151
+ let updateInfo = null;
3152
+ if (Array.isArray(ensuredUpdateInfo)) {
3153
+ const bundles = ensuredUpdateInfo;
3154
+ updateInfo = await getUpdateInfo(bundles, {
3155
+ appVersion: currentAppVersion,
3156
+ bundleId: currentBundleId,
3157
+ platform
3158
+ });
3159
+ } else updateInfo = ensuredUpdateInfo;
3160
+ return updateInfo;
3161
+ }
3162
+ async function installUpdate(updateInfo) {
3163
+ const isSuccess = await updateBundle(updateInfo.id, updateInfo.fileUrl || "");
3164
+ if (isSuccess && updateInfo.forceUpdate) {
3165
+ reload();
3166
+ return true;
3167
+ }
3168
+ return isSuccess;
3169
+ }
3170
+ function wrap(config) {
3171
+ return (WrappedComponent)=>{
3172
+ const HotUpdaterHOC = (props)=>{
3173
+ const [updateStatus, setUpdateStatus] = (0, react.useState)(null);
3174
+ const [updateError, setUpdateError] = (0, react.useState)(null);
3175
+ const { progress } = useHotUpdaterStore();
3176
+ (0, react.useEffect)(()=>{
3177
+ const initHotUpdater = async ()=>{
3178
+ try {
3179
+ const updateInfo = await checkUpdate(config);
3180
+ if (!updateInfo) {
3181
+ setUpdateStatus("UP_TO_DATE");
3182
+ return;
3183
+ }
3184
+ setUpdateStatus("UPDATING");
3185
+ const isSuccess = await installUpdate(updateInfo);
3186
+ if (isSuccess) setUpdateStatus("INSTALLING_UPDATE");
3187
+ } catch (error) {
3188
+ if (error instanceof HotUpdaterError) setUpdateError(error);
3189
+ throw error;
3190
+ }
3191
+ };
3192
+ initHotUpdater();
3193
+ }, [
3194
+ config.source,
3195
+ config.requestHeaders
3196
+ ]);
3197
+ if ("UPDATING" === updateStatus && config.fallbackComponent) {
3198
+ const Fallback = config.fallbackComponent;
3199
+ return /*#__PURE__*/ React.createElement(Fallback, {
3200
+ progress: progress
3201
+ });
3202
+ }
3203
+ return /*#__PURE__*/ React.createElement(WrappedComponent, {
3204
+ ...props,
3205
+ updateStatus: updateStatus,
3206
+ updateError: updateError
3207
+ });
3208
+ };
3209
+ return HotUpdaterHOC;
3210
+ };
3211
+ }
3171
3212
  addListener("onProgress", ({ progress })=>{
3172
3213
  hotUpdaterStore.setState({
3173
3214
  progress
3174
3215
  });
3175
3216
  });
3176
3217
  const src_HotUpdater = {
3177
- init: init,
3218
+ wrap: wrap,
3178
3219
  reload: reload,
3179
3220
  getAppVersion: getAppVersion,
3180
3221
  getBundleId: getBundleId,
3181
3222
  addListener: addListener,
3182
- ensureBundles: ensureBundles,
3223
+ ensureUpdateInfo: ensureUpdateInfo,
3183
3224
  updateBundle: updateBundle,
3184
- getUpdateInfo: getUpdateInfo,
3185
- HOT_UPDATER_BUNDLE_ID: __WEBPACK_EXTERNAL_MODULE__hot_updater_core__.NIL_UUID
3225
+ getUpdateInfo: getUpdateInfo
3186
3226
  };
3187
3227
  export { src_HotUpdater as HotUpdater, hotUpdaterStore, useHotUpdaterStore };
package/dist/native.d.ts CHANGED
@@ -4,13 +4,6 @@ export type HotUpdaterEvent = {
4
4
  };
5
5
  };
6
6
  export declare const addListener: <T extends keyof HotUpdaterEvent>(eventName: T, listener: (event: HotUpdaterEvent[T]) => void) => void;
7
- /**
8
- * Fetches the current bundle version id.
9
- *
10
- * @async
11
- * @returns {Promise<string>} Resolves with the current version id or null if not available.
12
- */
13
- export declare const getBundleId: () => string;
14
7
  /**
15
8
  * Downloads files from given URLs.
16
9
  *
@@ -27,3 +20,10 @@ export declare const getAppVersion: () => Promise<string | null>;
27
20
  * Reloads the app.
28
21
  */
29
22
  export declare const reload: () => void;
23
+ /**
24
+ * Fetches the current bundle version id.
25
+ *
26
+ * @async
27
+ * @returns {Promise<string>} Resolves with the current version id or null if not available.
28
+ */
29
+ export declare const getBundleId: () => string;
package/dist/wrap.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { BundleArg, UpdateInfo } from "@hot-updater/core";
2
+ import type React from "react";
3
+ import { HotUpdaterError } from "./error";
4
+ export type HotUpdaterStatus = "INSTALLING_UPDATE" | "UP_TO_DATE" | "UPDATING";
5
+ export interface CheckUpdateConfig {
6
+ source: BundleArg;
7
+ requestHeaders?: Record<string, string>;
8
+ }
9
+ export interface HotUpdaterConfig extends CheckUpdateConfig {
10
+ fallbackComponent?: React.FC<HotUpdaterFallbackProps>;
11
+ }
12
+ export interface HotUpdaterFallbackProps {
13
+ progress: number;
14
+ }
15
+ export interface WithHotUpdaterProps {
16
+ updateStatus: HotUpdaterStatus | null;
17
+ updateError: HotUpdaterError | null;
18
+ }
19
+ export declare function checkUpdate(config: CheckUpdateConfig): Promise<UpdateInfo | null>;
20
+ export declare function wrap<P>(config: HotUpdaterConfig): (WrappedComponent: React.ComponentType<P & WithHotUpdaterProps>) => React.ComponentType<P>;
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@hot-updater/react-native",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "React Native OTA solution for self-hosted",
5
- "main": "dist/index.cjs",
6
- "module": "dist/index.js",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
7
  "source": "src/index.ts",
8
8
  "react-native": "src/index.ts",
9
9
  "types": "dist/index.d.ts",
@@ -73,10 +73,10 @@
73
73
  "react": "18.3.1",
74
74
  "react-native": "0.76.2",
75
75
  "react-native-builder-bob": "^0.33.1",
76
- "@hot-updater/js": "0.2.0"
76
+ "@hot-updater/js": "0.3.1"
77
77
  },
78
78
  "dependencies": {
79
- "@hot-updater/core": "0.2.0"
79
+ "@hot-updater/core": "0.3.1"
80
80
  },
81
81
  "scripts": {
82
82
  "build": "rslib build",
@@ -5,16 +5,16 @@ import type {
5
5
  UpdateInfo,
6
6
  } from "@hot-updater/core";
7
7
 
8
- export const ensureBundles = async (
9
- bundle: BundleArg,
8
+ export const ensureUpdateInfo = async (
9
+ source: BundleArg,
10
10
  { appVersion, bundleId, platform }: GetBundlesArgs,
11
11
  requestHeaders?: Record<string, string>,
12
12
  ): Promise<Bundle[] | UpdateInfo> => {
13
13
  try {
14
14
  let bundles: Bundle[] | null = null;
15
- if (typeof bundle === "string") {
16
- if (bundle.startsWith("http")) {
17
- return await fetch(bundle, {
15
+ if (typeof source === "string") {
16
+ if (source.startsWith("http")) {
17
+ return await fetch(source, {
18
18
  headers: {
19
19
  "x-app-platform": platform,
20
20
  "x-app-version": appVersion,
@@ -23,10 +23,10 @@ export const ensureBundles = async (
23
23
  },
24
24
  }).then((res) => res.json());
25
25
  }
26
- } else if (typeof bundle === "function") {
27
- bundles = await bundle();
26
+ } else if (typeof source === "function") {
27
+ bundles = await source();
28
28
  } else {
29
- bundles = bundle;
29
+ bundles = source;
30
30
  }
31
31
 
32
32
  return bundles ?? [];
package/src/index.ts CHANGED
@@ -1,7 +1,5 @@
1
- import { NIL_UUID } from "@hot-updater/core";
2
1
  import { getUpdateInfo } from "@hot-updater/js";
3
- import { ensureBundles } from "./ensureBundles";
4
- import { init } from "./init";
2
+ import { ensureUpdateInfo } from "./ensureUpdateInfo";
5
3
  import {
6
4
  addListener,
7
5
  getAppVersion,
@@ -10,8 +8,9 @@ import {
10
8
  updateBundle,
11
9
  } from "./native";
12
10
  import { hotUpdaterStore } from "./store";
11
+ import { wrap } from "./wrap";
13
12
 
14
- export type * from "./init";
13
+ export type * from "./wrap";
15
14
  export type * from "./native";
16
15
 
17
16
  export * from "./store";
@@ -21,17 +20,14 @@ addListener("onProgress", ({ progress }) => {
21
20
  });
22
21
 
23
22
  export const HotUpdater = {
24
- init,
23
+ wrap,
24
+
25
25
  reload,
26
26
  getAppVersion,
27
27
  getBundleId,
28
28
  addListener,
29
29
 
30
- ensureBundles,
30
+ ensureUpdateInfo,
31
31
  updateBundle,
32
32
  getUpdateInfo,
33
- /**
34
- * In production environment, this value will be replaced with a uuidv7.
35
- */
36
- HOT_UPDATER_BUNDLE_ID: NIL_UUID,
37
33
  };
package/src/native.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import { NIL_UUID } from "@hot-updater/core";
2
2
  import { NativeEventEmitter, NativeModules, Platform } from "react-native";
3
3
 
4
+ const HotUpdater = {
5
+ HOT_UPDATER_BUNDLE_ID: NIL_UUID,
6
+ };
7
+
4
8
  const LINKING_ERROR =
5
9
  // biome-ignore lint/style/useTemplate: <explanation>
6
10
  `The package '@hot-updater/react-native' doesn't seem to be linked. Make sure: \n\n` +
@@ -41,16 +45,6 @@ export const addListener = <T extends keyof HotUpdaterEvent>(
41
45
  eventEmitter?.addListener(eventName, listener);
42
46
  };
43
47
 
44
- /**
45
- * Fetches the current bundle version id.
46
- *
47
- * @async
48
- * @returns {Promise<string>} Resolves with the current version id or null if not available.
49
- */
50
- export const getBundleId = (): string => {
51
- return HotUpdater.HOT_UPDATER_BUNDLE_ID ?? NIL_UUID;
52
- };
53
-
54
48
  /**
55
49
  * Downloads files from given URLs.
56
50
  *
@@ -78,3 +72,13 @@ export const getAppVersion = (): Promise<string | null> => {
78
72
  export const reload = () => {
79
73
  HotUpdaterNative.reload();
80
74
  };
75
+
76
+ /**
77
+ * Fetches the current bundle version id.
78
+ *
79
+ * @async
80
+ * @returns {Promise<string>} Resolves with the current version id or null if not available.
81
+ */
82
+ export const getBundleId = (): string => {
83
+ return HotUpdater.HOT_UPDATER_BUNDLE_ID;
84
+ };
package/src/wrap.tsx ADDED
@@ -0,0 +1,148 @@
1
+ import type { Bundle, BundleArg, UpdateInfo } from "@hot-updater/core";
2
+ import { getUpdateInfo } from "@hot-updater/js";
3
+ import type React from "react";
4
+ import { useEffect, useState } from "react";
5
+ import { Platform } from "react-native";
6
+ import { ensureUpdateInfo } from "./ensureUpdateInfo";
7
+ import { HotUpdaterError } from "./error";
8
+ import { getAppVersion, getBundleId, reload, updateBundle } from "./native";
9
+ import { useHotUpdaterStore } from "./store";
10
+
11
+ export type HotUpdaterStatus = "INSTALLING_UPDATE" | "UP_TO_DATE" | "UPDATING";
12
+
13
+ export interface CheckUpdateConfig {
14
+ source: BundleArg;
15
+ requestHeaders?: Record<string, string>;
16
+ }
17
+
18
+ export interface HotUpdaterConfig extends CheckUpdateConfig {
19
+ fallbackComponent?: React.FC<HotUpdaterFallbackProps>;
20
+ }
21
+
22
+ export interface HotUpdaterFallbackProps {
23
+ progress: number;
24
+ }
25
+
26
+ export interface WithHotUpdaterProps {
27
+ updateStatus: HotUpdaterStatus | null;
28
+ updateError: HotUpdaterError | null;
29
+ }
30
+
31
+ export async function checkUpdate(config: CheckUpdateConfig) {
32
+ if (__DEV__) {
33
+ console.warn(
34
+ "[HotUpdater] __DEV__ is true, HotUpdater is only supported in production",
35
+ );
36
+ return null;
37
+ }
38
+
39
+ if (!["ios", "android"].includes(Platform.OS)) {
40
+ throw new HotUpdaterError(
41
+ "HotUpdater is only supported on iOS and Android",
42
+ );
43
+ }
44
+
45
+ const currentAppVersion = await getAppVersion();
46
+ const platform = Platform.OS as "ios" | "android";
47
+ const currentBundleId = await getBundleId();
48
+
49
+ if (!currentAppVersion) {
50
+ throw new HotUpdaterError("Failed to get app version");
51
+ }
52
+
53
+ const ensuredUpdateInfo = await ensureUpdateInfo(
54
+ config.source,
55
+ {
56
+ appVersion: currentAppVersion,
57
+ bundleId: currentBundleId,
58
+ platform,
59
+ },
60
+ config.requestHeaders,
61
+ );
62
+
63
+ let updateInfo: UpdateInfo | null = null;
64
+ if (Array.isArray(ensuredUpdateInfo)) {
65
+ const bundles: Bundle[] = ensuredUpdateInfo;
66
+
67
+ updateInfo = await getUpdateInfo(bundles, {
68
+ appVersion: currentAppVersion,
69
+ bundleId: currentBundleId,
70
+ platform,
71
+ });
72
+ } else {
73
+ updateInfo = ensuredUpdateInfo;
74
+ }
75
+
76
+ return updateInfo;
77
+ }
78
+
79
+ async function installUpdate(updateInfo: UpdateInfo) {
80
+ const isSuccess = await updateBundle(updateInfo.id, updateInfo.fileUrl || "");
81
+
82
+ if (isSuccess && updateInfo.forceUpdate) {
83
+ reload();
84
+ return true;
85
+ }
86
+
87
+ return isSuccess;
88
+ }
89
+
90
+ export function wrap<P>(
91
+ config: HotUpdaterConfig,
92
+ ): (
93
+ WrappedComponent: React.ComponentType<P & WithHotUpdaterProps>,
94
+ ) => React.ComponentType<P> {
95
+ return (WrappedComponent) => {
96
+ const HotUpdaterHOC: React.FC<P> = (props) => {
97
+ const [updateStatus, setUpdateStatus] = useState<HotUpdaterStatus | null>(
98
+ null,
99
+ );
100
+ const [updateError, setUpdateError] = useState<HotUpdaterError | null>(
101
+ null,
102
+ );
103
+
104
+ const { progress } = useHotUpdaterStore();
105
+
106
+ useEffect(() => {
107
+ const initHotUpdater = async () => {
108
+ try {
109
+ const updateInfo = await checkUpdate(config);
110
+
111
+ if (!updateInfo) {
112
+ setUpdateStatus("UP_TO_DATE");
113
+ return;
114
+ }
115
+
116
+ setUpdateStatus("UPDATING");
117
+ const isSuccess = await installUpdate(updateInfo);
118
+ if (isSuccess) {
119
+ setUpdateStatus("INSTALLING_UPDATE");
120
+ }
121
+ } catch (error) {
122
+ if (error instanceof HotUpdaterError) {
123
+ setUpdateError(error);
124
+ }
125
+ throw error;
126
+ }
127
+ };
128
+
129
+ initHotUpdater();
130
+ }, [config.source, config.requestHeaders]);
131
+
132
+ if (updateStatus === "UPDATING" && config.fallbackComponent) {
133
+ const Fallback = config.fallbackComponent;
134
+ return <Fallback progress={progress} />;
135
+ }
136
+
137
+ return (
138
+ <WrappedComponent
139
+ {...props}
140
+ updateStatus={updateStatus}
141
+ updateError={updateError}
142
+ />
143
+ );
144
+ };
145
+
146
+ return HotUpdaterHOC;
147
+ };
148
+ }
@@ -1,2 +0,0 @@
1
- import type { Bundle, BundleArg, GetBundlesArgs, UpdateInfo } from "@hot-updater/core";
2
- export declare const ensureBundles: (bundle: BundleArg, { appVersion, bundleId, platform }: GetBundlesArgs, requestHeaders?: Record<string, string>) => Promise<Bundle[] | UpdateInfo>;
package/dist/init.d.ts DELETED
@@ -1,10 +0,0 @@
1
- import type { BundleArg } from "@hot-updater/core";
2
- import { HotUpdaterError } from "./error";
3
- export type HotUpdaterStatus = "INSTALLING_UPDATE" | "UP_TO_DATE";
4
- export interface HotUpdaterInitConfig {
5
- source: BundleArg;
6
- requestHeaders?: Record<string, string>;
7
- onSuccess?: (status: HotUpdaterStatus) => void;
8
- onError?: (error: HotUpdaterError) => void;
9
- }
10
- export declare const init: (config: HotUpdaterInitConfig) => Promise<void>;
package/src/init.tsx DELETED
@@ -1,88 +0,0 @@
1
- import type { BundleArg, UpdateInfo } from "@hot-updater/core";
2
- import { getUpdateInfo } from "@hot-updater/js";
3
- import { Platform } from "react-native";
4
- import { ensureBundles } from "./ensureBundles";
5
- import { HotUpdaterError } from "./error";
6
- import { getAppVersion, getBundleId, reload, updateBundle } from "./native";
7
-
8
- export type HotUpdaterStatus = "INSTALLING_UPDATE" | "UP_TO_DATE";
9
-
10
- export interface HotUpdaterInitConfig {
11
- source: BundleArg;
12
- requestHeaders?: Record<string, string>;
13
- onSuccess?: (status: HotUpdaterStatus) => void;
14
- onError?: (error: HotUpdaterError) => void;
15
- }
16
-
17
- export const init = async (config: HotUpdaterInitConfig) => {
18
- if (__DEV__) {
19
- console.warn(
20
- "[HotUpdater] __DEV__ is true, HotUpdater is only supported in production",
21
- );
22
- return;
23
- }
24
-
25
- if (!["ios", "android"].includes(Platform.OS)) {
26
- const error = new HotUpdaterError(
27
- "HotUpdater is only supported on iOS and Android",
28
- );
29
-
30
- config?.onError?.(error);
31
- throw error;
32
- }
33
-
34
- const currentAppVersion = await getAppVersion();
35
- const platform = Platform.OS as "ios" | "android";
36
- const currentBundleId = await getBundleId();
37
-
38
- if (!currentAppVersion) {
39
- const error = new HotUpdaterError("Failed to get app version");
40
- config?.onError?.(error);
41
- throw error;
42
- }
43
-
44
- const bundles = await ensureBundles(
45
- config.source,
46
- {
47
- appVersion: currentAppVersion,
48
- bundleId: currentBundleId,
49
- platform,
50
- },
51
- config.requestHeaders,
52
- );
53
-
54
- let updateInfo: UpdateInfo | null = null;
55
- if (Array.isArray(bundles)) {
56
- // Direct comparison
57
- updateInfo = await getUpdateInfo(bundles, {
58
- appVersion: currentAppVersion,
59
- bundleId: currentBundleId,
60
- platform,
61
- });
62
- } else {
63
- // Already verified from server
64
- updateInfo = bundles;
65
- }
66
-
67
- if (!updateInfo) {
68
- config?.onSuccess?.("UP_TO_DATE");
69
- return;
70
- }
71
-
72
- try {
73
- const isSuccess = await updateBundle(
74
- updateInfo.id,
75
- updateInfo.fileUrl || "",
76
- );
77
- if (isSuccess && updateInfo.forceUpdate) {
78
- reload();
79
-
80
- config?.onSuccess?.("INSTALLING_UPDATE");
81
- }
82
- } catch (error) {
83
- if (error instanceof HotUpdaterError) {
84
- config?.onError?.(error);
85
- }
86
- throw error;
87
- }
88
- };