@exodus/react-native-bundle-loader 0.2.0-exodus.2 → 0.2.0-exodus.3

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.
@@ -7,6 +7,7 @@ import android.util.Log;
7
7
 
8
8
  import androidx.annotation.NonNull;
9
9
 
10
+ import com.facebook.react.ReactApplication;
10
11
  import com.facebook.react.bridge.Promise;
11
12
  import com.facebook.react.bridge.ReactApplicationContext;
12
13
  import com.facebook.react.bridge.ReactContextBaseJavaModule;
@@ -24,12 +25,13 @@ import java.security.NoSuchAlgorithmException;
24
25
  public class BundleLoaderModule extends ReactContextBaseJavaModule {
25
26
 
26
27
  private static final String TAG = "BundleLoader";
27
- // These values are referenced by string literals in the host app's MainApplication.
28
- // Do not rename without updating the host app integration accordingly.
28
+ // Host app references these as string literals (library is debugImplementation only).
29
29
  static final String BUNDLE_FILENAME = "verified-bundle.jsbundle";
30
30
  static final String PREFS_NAME = "BundleLoader";
31
31
  static final String PREFS_PENDING_KEY = "pending_remote_bundle";
32
32
  static final String PREFS_ACTIVE_KEY = "active_remote_bundle";
33
+ // Metro/APK source URL captured at load time for correct asset resolution after restart.
34
+ static final String PREFS_METRO_SOURCE_URL_KEY = "metro_source_url";
33
35
 
34
36
  private static final int CONNECT_TIMEOUT_MS = 30_000;
35
37
  private static final int READ_TIMEOUT_MS = 30_000;
@@ -128,21 +130,35 @@ public class BundleLoaderModule extends ReactContextBaseJavaModule {
128
130
  }
129
131
 
130
132
  private void setPendingFlag() {
131
- // commit() not apply() — apply() is async and the write may not reach disk
132
- // before killProcess() terminates the process.
133
- getReactApplicationContext()
133
+ SharedPreferences.Editor editor = getReactApplicationContext()
134
134
  .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
135
135
  .edit()
136
- .putBoolean(PREFS_PENDING_KEY, true)
137
- .commit();
136
+ .putBoolean(PREFS_PENDING_KEY, true);
137
+
138
+ // Store scriptURL for asset resolution after restart: Metro URL if available,
139
+ // asset:/// otherwise (genesis/release — assets served from APK).
140
+ String sourceUrl = getMetroSourceUrl();
141
+ editor.putString(PREFS_METRO_SOURCE_URL_KEY,
142
+ sourceUrl != null ? sourceUrl : "asset:///index.android.bundle");
143
+
144
+ // commit() not apply() — write must reach disk before killProcess().
145
+ editor.commit();
138
146
  }
139
147
 
140
- /**
141
- * Restarts the app process. On next launch, MainApplication reads the pending
142
- * flag from SharedPreferences and loads the cached bundle instead of Metro.
143
- * A process restart avoids running both the old and new Hermes runtimes
144
- * simultaneously, which would exceed the device heap limit.
145
- */
148
+ private String getMetroSourceUrl() {
149
+ try {
150
+ ReactApplication app = (ReactApplication) getReactApplicationContext().getApplicationContext();
151
+ String url = app.getReactNativeHost()
152
+ .getReactInstanceManager()
153
+ .getDevSupportManager()
154
+ .getSourceUrl();
155
+ return (url != null && !url.isEmpty()) ? url : null;
156
+ } catch (Exception e) {
157
+ return null;
158
+ }
159
+ }
160
+
161
+ /** Kills and relaunches the process. Avoids running two Hermes runtimes simultaneously. */
146
162
  private void restartApp() {
147
163
  Context context = getReactApplicationContext();
148
164
  Intent intent = context.getPackageManager()
@@ -154,20 +170,12 @@ public class BundleLoaderModule extends ReactContextBaseJavaModule {
154
170
  android.os.Process.killProcess(android.os.Process.myPid());
155
171
  }
156
172
 
157
- /**
158
- * Returns true iff the URL is non-null and uses the https scheme.
159
- * Extracted as a static helper so it can be unit-tested without the threading
160
- * machinery in {@link #load(String)}.
161
- */
173
+ /** Package-private for testing. */
162
174
  static boolean isHttps(String url) {
163
175
  return url != null && url.startsWith("https://");
164
176
  }
165
177
 
166
- /**
167
- * Parses a 64-character lowercase hex string into a 32-byte SHA-256 digest.
168
- * Throws {@link IllegalArgumentException} on invalid input so callers can
169
- * reject the promise before touching the network.
170
- */
178
+ /** Parses a 64-char hex string into a 32-byte digest. Package-private for testing. */
171
179
  static byte[] parseHexSha256(String hex) {
172
180
  if (hex == null || hex.length() != 64) {
173
181
  throw new IllegalArgumentException("Expected SHA-256 must be a 64-character hex string");
@@ -184,11 +192,7 @@ public class BundleLoaderModule extends ReactContextBaseJavaModule {
184
192
  return out;
185
193
  }
186
194
 
187
- /**
188
- * Constant-time byte array comparison: XORs all pairs into an accumulator
189
- * and returns true iff the accumulator is zero. Both arrays must be the
190
- * same length; returns false immediately if they differ.
191
- */
195
+ /** Constant-time equality check to prevent timing attacks. Package-private for testing. */
192
196
  static boolean timingSafeEquals(byte[] a, byte[] b) {
193
197
  if (a.length != b.length) return false;
194
198
  int diff = 0;
@@ -199,14 +203,8 @@ public class BundleLoaderModule extends ReactContextBaseJavaModule {
199
203
  }
200
204
 
201
205
  /**
202
- * Downloads {@code urlString} into {@code targetFile}, computing SHA-256 of
203
- * the body in the same streaming pass. Returns the 32-byte digest.
204
- * <p>
205
- * Mirrors the security properties of {@link #downloadToCache}: redirects are
206
- * disabled, non-200 responses throw, and the body is capped at {@code maxBytes}.
207
- * <p>
208
- * Package-private and static so the JVM unit tests can drive it against a
209
- * MockWebServer without spinning up a ReactApplicationContext.
206
+ * Downloads into {@code targetFile} and returns its SHA-256 digest.
207
+ * No redirects; non-200 throws; body capped at {@code maxBytes}. Package-private for testing.
210
208
  */
211
209
  static byte[] downloadAndHashToCache(
212
210
  String urlString,
@@ -253,13 +251,8 @@ public class BundleLoaderModule extends ReactContextBaseJavaModule {
253
251
  }
254
252
 
255
253
  /**
256
- * Downloads {@code urlString} into {@code targetFile} using HttpURLConnection.
257
- * Redirects are not followed and non-200 responses throw IOException with the
258
- * status code in the message. The download is hard-capped at {@code maxBytes};
259
- * once the cap is exceeded the read loop aborts with IOException.
260
- * <p>
261
- * Package-private and static so the JVM unit tests can drive it against a
262
- * MockWebServer without spinning up a ReactApplicationContext.
254
+ * Downloads into {@code targetFile}. No redirects; non-200 throws; body capped at
255
+ * {@code maxBytes}. Package-private for testing.
263
256
  */
264
257
  static File downloadToCache(
265
258
  String urlString,
@@ -1,6 +1,9 @@
1
1
  #import <React/RCTBridgeModule.h>
2
2
  #import <React/RCTRootView.h>
3
3
 
4
+ // NSUserDefaults key storing the pending remote bundle URL for loadSourceForBridge:.
5
+ extern NSString * const RNBundleLoaderPendingURLKey;
6
+
4
7
  @interface BundleLoader : NSObject <RCTBridgeModule>
5
8
 
6
9
  @end
@@ -1,6 +1,8 @@
1
1
  #import "BundleLoader.h"
2
2
  #import <CommonCrypto/CommonDigest.h>
3
3
 
4
+ NSString * const RNBundleLoaderPendingURLKey = @"RNBundleLoaderPendingURL";
5
+
4
6
  @implementation BundleLoader
5
7
 
6
8
  @synthesize bridge = _bridge;
@@ -9,16 +11,16 @@ RCT_EXPORT_MODULE()
9
11
 
10
12
  - (void)setBundleURLAndReload:(NSURL *)url
11
13
  {
12
- [_bridge setValue:url forKey:@"bundleURL"];
14
+ [[NSUserDefaults standardUserDefaults] setURL:url forKey:RNBundleLoaderPendingURLKey];
15
+ [[NSUserDefaults standardUserDefaults] synchronize];
13
16
  [_bridge reload];
14
17
  }
15
18
 
16
19
  RCT_EXPORT_METHOD(runningMode:(RCTPromiseResolveBlock)resolve
17
20
  rejecter:(RCTPromiseRejectBlock)reject)
18
21
  {
19
- NSString *scheme = [[_bridge bundleURL] scheme];
20
- BOOL isRemote = [scheme isEqualToString:@"https"];
21
- resolve(isRemote ? @"REMOTE" : @"LOCAL");
22
+ NSURL *pending = [[NSUserDefaults standardUserDefaults] URLForKey:RNBundleLoaderPendingURLKey];
23
+ resolve(pending ? @"REMOTE" : @"LOCAL");
22
24
  }
23
25
 
24
26
  RCT_EXPORT_METHOD(load:(NSURL *)url)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/react-native-bundle-loader",
3
- "version": "0.2.0-exodus.2",
3
+ "version": "0.2.0-exodus.3",
4
4
  "description": "Loads a remote React Native JS bundle.",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",