@vanikya/ota-react-native 0.2.3 → 0.2.4

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,90 @@
1
+ package com.otaupdate
2
+
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+ import android.util.Log
6
+ import java.io.File
7
+
8
+ /**
9
+ * Helper object for OTA Update bundle loading.
10
+ * This is called from MainApplication.getJSBundleFile() to get the OTA bundle path.
11
+ */
12
+ object OTAUpdateHelper {
13
+ private const val TAG = "OTAUpdate"
14
+ private const val PREFS_NAME = "OTAUpdate"
15
+ private const val KEY_BUNDLE_PATH = "BundlePath"
16
+
17
+ /**
18
+ * Get the JS bundle file path for React Native to load.
19
+ * Returns the OTA bundle path if available and valid, otherwise null (loads default bundle).
20
+ *
21
+ * @param context Application context
22
+ * @return Bundle file path or null
23
+ */
24
+ @JvmStatic
25
+ fun getJSBundleFile(context: Context): String? {
26
+ return try {
27
+ val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
28
+ val bundlePath = prefs.getString(KEY_BUNDLE_PATH, null)
29
+
30
+ Log.d(TAG, "getJSBundleFile called, stored path: $bundlePath")
31
+
32
+ if (bundlePath.isNullOrEmpty()) {
33
+ Log.d(TAG, "No OTA bundle path stored, loading default bundle")
34
+ return null
35
+ }
36
+
37
+ val file = File(bundlePath)
38
+ if (!file.exists()) {
39
+ Log.w(TAG, "OTA bundle file does not exist: $bundlePath")
40
+ // Clear invalid path
41
+ prefs.edit().remove(KEY_BUNDLE_PATH).commit()
42
+ return null
43
+ }
44
+
45
+ if (!file.canRead()) {
46
+ Log.w(TAG, "OTA bundle file is not readable: $bundlePath")
47
+ return null
48
+ }
49
+
50
+ val fileSize = file.length()
51
+ if (fileSize < 100) {
52
+ Log.w(TAG, "OTA bundle file is too small (${fileSize} bytes), likely corrupted")
53
+ prefs.edit().remove(KEY_BUNDLE_PATH).commit()
54
+ return null
55
+ }
56
+
57
+ Log.d(TAG, "Loading OTA bundle: $bundlePath ($fileSize bytes)")
58
+ bundlePath
59
+ } catch (e: Exception) {
60
+ Log.e(TAG, "Error getting JS bundle file: ${e.message}", e)
61
+ null
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Check if an OTA bundle is pending to be loaded.
67
+ *
68
+ * @param context Application context
69
+ * @return true if OTA bundle is available
70
+ */
71
+ @JvmStatic
72
+ fun hasPendingBundle(context: Context): Boolean {
73
+ val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
74
+ val bundlePath = prefs.getString(KEY_BUNDLE_PATH, null)
75
+ if (bundlePath.isNullOrEmpty()) return false
76
+ return File(bundlePath).exists()
77
+ }
78
+
79
+ /**
80
+ * Clear the pending OTA bundle.
81
+ *
82
+ * @param context Application context
83
+ */
84
+ @JvmStatic
85
+ fun clearPendingBundle(context: Context) {
86
+ val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
87
+ prefs.edit().remove(KEY_BUNDLE_PATH).commit()
88
+ Log.d(TAG, "Pending bundle cleared")
89
+ }
90
+ }
package/app.plugin.js CHANGED
@@ -4,7 +4,7 @@ const { withMainApplication, withAppDelegate } = require('@expo/config-plugins')
4
4
  * OTA Update Expo Config Plugin
5
5
  *
6
6
  * This plugin modifies the native code to enable OTA bundle loading:
7
- * - Android: Overrides getJSBundleFile() in MainApplication.kt
7
+ * - Android: Overrides getJSBundleFile() in MainApplication.kt using OTAUpdateHelper
8
8
  * - iOS: Modifies bundleURL() in AppDelegate.swift
9
9
  *
10
10
  * The modifications check SharedPreferences (Android) / UserDefaults (iOS)
@@ -16,153 +16,123 @@ function withOTAUpdateAndroid(config) {
16
16
  let contents = config.modResults.contents;
17
17
 
18
18
  // Check if already modified
19
- if (contents.includes('getJSBundleFile')) {
20
- console.log('[OTAUpdate] Android: getJSBundleFile already present, skipping');
19
+ if (contents.includes('OTAUpdateHelper')) {
20
+ console.log('[OTAUpdate] Android: OTAUpdateHelper already present, skipping');
21
21
  return config;
22
22
  }
23
23
 
24
- // Add imports if not present
25
- if (!contents.includes('import android.content.SharedPreferences')) {
26
- // Find the package declaration and add imports after it
27
- const packageMatch = contents.match(/^package\s+[\w.]+\s*\n/m);
28
- if (packageMatch) {
29
- const insertPos = packageMatch.index + packageMatch[0].length;
30
- const imports = `\nimport android.content.SharedPreferences\nimport java.io.File\n`;
31
- contents = contents.slice(0, insertPos) + imports + contents.slice(insertPos);
24
+ // Add import for OTAUpdateHelper
25
+ const packageMatch = contents.match(/^package\s+[\w.]+\s*\n/m);
26
+ if (packageMatch) {
27
+ const insertPos = packageMatch.index + packageMatch[0].length;
28
+ // Check if import section exists
29
+ if (!contents.includes('import com.otaupdate.OTAUpdateHelper')) {
30
+ const importStatement = `\nimport com.otaupdate.OTAUpdateHelper\n`;
31
+ contents = contents.slice(0, insertPos) + importStatement + contents.slice(insertPos);
32
32
  }
33
33
  }
34
34
 
35
- // Strategy 1: Look for "override val reactNativeHost" pattern (Expo SDK 50+)
36
- // This is more reliable than matching getUseDeveloperSupport
37
- const reactNativeHostPattern = /(override\s+val\s+reactNativeHost\s*:\s*ReactNativeHost\s*=\s*object\s*:\s*DefaultReactNativeHost\s*\(\s*this\s*\)\s*\{)/;
38
-
39
- if (reactNativeHostPattern.test(contents)) {
40
- const getJSBundleFileOverride = `
35
+ // The getJSBundleFile override that uses our helper
36
+ const getJSBundleFileOverride = `
41
37
  override fun getJSBundleFile(): String? {
42
- val prefs: SharedPreferences = applicationContext.getSharedPreferences("OTAUpdate", android.content.Context.MODE_PRIVATE)
43
- val bundlePath = prefs.getString("BundlePath", null)
44
- android.util.Log.d("OTAUpdate", "getJSBundleFile called, stored path: $bundlePath")
45
- if (bundlePath != null) {
46
- val file = File(bundlePath)
47
- if (file.exists() && file.canRead()) {
48
- android.util.Log.d("OTAUpdate", "Loading OTA bundle: $bundlePath (${file.length()} bytes)")
49
- return bundlePath
50
- } else {
51
- android.util.Log.w("OTAUpdate", "OTA bundle not found or not readable: $bundlePath, exists=${file.exists()}")
52
- }
53
- }
54
- android.util.Log.d("OTAUpdate", "Loading default bundle")
55
- return null
38
+ return OTAUpdateHelper.getJSBundleFile(applicationContext)
56
39
  }
57
40
  `;
41
+
42
+ // Strategy 1: Look for "object : DefaultReactNativeHost" pattern
43
+ // This handles most Expo SDK 50+ apps
44
+ const defaultHostPattern = /(object\s*:\s*DefaultReactNativeHost\s*\([^)]*\)\s*\{)/;
45
+
46
+ if (defaultHostPattern.test(contents)) {
47
+ contents = contents.replace(
48
+ defaultHostPattern,
49
+ `$1${getJSBundleFileOverride}`
50
+ );
51
+ console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (DefaultReactNativeHost pattern)');
52
+ config.modResults.contents = contents;
53
+ return config;
54
+ }
55
+
56
+ // Strategy 2: Look for "override val reactNativeHost" with object block
57
+ const reactNativeHostPattern = /(override\s+val\s+reactNativeHost[^=]*=\s*object[^{]*\{)/;
58
+
59
+ if (reactNativeHostPattern.test(contents)) {
58
60
  contents = contents.replace(
59
61
  reactNativeHostPattern,
60
62
  `$1${getJSBundleFileOverride}`
61
63
  );
62
- console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (pattern 1)');
64
+ console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (reactNativeHost pattern)');
63
65
  config.modResults.contents = contents;
64
66
  return config;
65
67
  }
66
68
 
67
- // Strategy 2: Look for DefaultReactNativeHost with different formatting
68
- const altPattern = /(object\s*:\s*DefaultReactNativeHost\s*\(\s*this\s*\)\s*\{)/;
69
+ // Strategy 3: Look for any ReactNativeHost object block
70
+ const anyHostPattern = /(ReactNativeHost\s*\([^)]*\)\s*\{)/;
69
71
 
70
- if (altPattern.test(contents)) {
71
- const getJSBundleFileOverride = `
72
- override fun getJSBundleFile(): String? {
73
- val prefs: SharedPreferences = applicationContext.getSharedPreferences("OTAUpdate", android.content.Context.MODE_PRIVATE)
74
- val bundlePath = prefs.getString("BundlePath", null)
75
- android.util.Log.d("OTAUpdate", "getJSBundleFile called, stored path: $bundlePath")
76
- if (bundlePath != null) {
77
- val file = File(bundlePath)
78
- if (file.exists() && file.canRead()) {
79
- android.util.Log.d("OTAUpdate", "Loading OTA bundle: $bundlePath (${file.length()} bytes)")
80
- return bundlePath
81
- } else {
82
- android.util.Log.w("OTAUpdate", "OTA bundle not found or not readable: $bundlePath, exists=${file.exists()}")
83
- }
84
- }
85
- android.util.Log.d("OTAUpdate", "Loading default bundle")
86
- return null
87
- }
88
- `;
72
+ if (anyHostPattern.test(contents)) {
89
73
  contents = contents.replace(
90
- altPattern,
74
+ anyHostPattern,
91
75
  `$1${getJSBundleFileOverride}`
92
76
  );
93
- console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (pattern 2)');
77
+ console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (generic ReactNativeHost pattern)');
94
78
  config.modResults.contents = contents;
95
79
  return config;
96
80
  }
97
81
 
98
- // Strategy 3: Look for getUseDeveloperSupport with flexible whitespace
99
- const devSupportPattern = /(override\s+fun\s+getUseDeveloperSupport\s*\(\s*\)\s*[:\s]*Boolean\s*[=\{])/;
82
+ // Strategy 4: Find the class and look for the host definition more flexibly
83
+ // Look for "override fun getUseDeveloperSupport" and insert before it
84
+ const devSupportPattern = /([\t ]*)(override\s+fun\s+getUseDeveloperSupport)/;
100
85
 
101
86
  if (devSupportPattern.test(contents)) {
102
- const getJSBundleFileOverride = `override fun getJSBundleFile(): String? {
103
- val prefs: SharedPreferences = applicationContext.getSharedPreferences("OTAUpdate", android.content.Context.MODE_PRIVATE)
104
- val bundlePath = prefs.getString("BundlePath", null)
105
- android.util.Log.d("OTAUpdate", "getJSBundleFile called, stored path: $bundlePath")
106
- if (bundlePath != null) {
107
- val file = File(bundlePath)
108
- if (file.exists() && file.canRead()) {
109
- android.util.Log.d("OTAUpdate", "Loading OTA bundle: $bundlePath (${file.length()} bytes)")
110
- return bundlePath
111
- } else {
112
- android.util.Log.w("OTAUpdate", "OTA bundle not found or not readable: $bundlePath, exists=${file.exists()}")
113
- }
114
- }
115
- android.util.Log.d("OTAUpdate", "Loading default bundle")
116
- return null
117
- }
118
-
119
- `;
87
+ const indentMatch = contents.match(devSupportPattern);
88
+ const indent = indentMatch ? indentMatch[1] : ' ';
89
+ const overrideCode = `${indent}override fun getJSBundleFile(): String? {\n${indent} return OTAUpdateHelper.getJSBundleFile(applicationContext)\n${indent}}\n\n${indent}`;
120
90
  contents = contents.replace(
121
91
  devSupportPattern,
122
- `${getJSBundleFileOverride}$1`
92
+ `${overrideCode}$2`
123
93
  );
124
- console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (pattern 3)');
94
+ console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (before getUseDeveloperSupport)');
125
95
  config.modResults.contents = contents;
126
96
  return config;
127
97
  }
128
98
 
129
- // Strategy 4: Look for ReactNativeHost in any form
130
- const genericPattern = /(ReactNativeHost\s*[\(\{])/;
99
+ // Strategy 5: Last resort - find any class with Application and inject
100
+ // Look for the ReactApplication interface implementation
101
+ const reactAppPattern = /(class\s+\w+\s*:\s*Application\s*\(\s*\)\s*,\s*ReactApplication\s*\{)/;
131
102
 
132
- if (genericPattern.test(contents)) {
133
- // Find the opening brace after ReactNativeHost and insert after it
134
- const match = contents.match(/ReactNativeHost[^{]*\{/);
135
- if (match) {
136
- const insertPos = match.index + match[0].length;
137
- const getJSBundleFileOverride = `
138
- override fun getJSBundleFile(): String? {
139
- val prefs: SharedPreferences = applicationContext.getSharedPreferences("OTAUpdate", android.content.Context.MODE_PRIVATE)
140
- val bundlePath = prefs.getString("BundlePath", null)
141
- android.util.Log.d("OTAUpdate", "getJSBundleFile called, stored path: $bundlePath")
142
- if (bundlePath != null) {
143
- val file = File(bundlePath)
144
- if (file.exists() && file.canRead()) {
145
- android.util.Log.d("OTAUpdate", "Loading OTA bundle: $bundlePath (${file.length()} bytes)")
146
- return bundlePath
147
- } else {
148
- android.util.Log.w("OTAUpdate", "OTA bundle not found or not readable: $bundlePath, exists=${file.exists()}")
149
- }
150
- }
151
- android.util.Log.d("OTAUpdate", "Loading default bundle")
152
- return null
153
- }
103
+ if (reactAppPattern.test(contents)) {
104
+ // Find where to inject - after the class opening
105
+ const classMatch = contents.match(reactAppPattern);
106
+ if (classMatch) {
107
+ const insertPos = classMatch.index + classMatch[0].length;
108
+ const helperComment = `
109
+ // OTA Update: Override to load OTA bundle if available
110
+ private fun getOTABundleFile(): String? {
111
+ return OTAUpdateHelper.getJSBundleFile(applicationContext)
112
+ }
154
113
  `;
155
- contents = contents.slice(0, insertPos) + getJSBundleFileOverride + contents.slice(insertPos);
156
- console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (pattern 4)');
157
- config.modResults.contents = contents;
158
- return config;
114
+ contents = contents.slice(0, insertPos) + helperComment + contents.slice(insertPos);
115
+ console.log('[OTAUpdate] Android: Added OTA helper method to Application class');
116
+ console.log('[OTAUpdate] Android: WARNING - You may need to manually wire getJSBundleFile() override');
159
117
  }
160
118
  }
161
119
 
162
- console.warn('[OTAUpdate] Android: Could not find suitable injection point for getJSBundleFile');
163
- console.warn('[OTAUpdate] Android: Please manually add getJSBundleFile override to MainApplication.kt');
120
+ // Log the current MainApplication structure for debugging
121
+ console.warn('[OTAUpdate] Android: Could not find standard injection point');
122
+ console.warn('[OTAUpdate] Android: Please ensure your MainApplication.kt has a ReactNativeHost definition');
123
+ console.warn('[OTAUpdate] Android: You may need to manually add the getJSBundleFile override');
164
124
  console.warn('[OTAUpdate] Android: See https://vanikya.github.io/ota-update/ for manual setup instructions');
165
125
 
126
+ // Log first 100 lines for debugging
127
+ const lines = contents.split('\n').slice(0, 100);
128
+ console.log('[OTAUpdate] Android: First 100 lines of MainApplication.kt:');
129
+ lines.forEach((line, i) => {
130
+ if (line.includes('ReactNativeHost') || line.includes('DefaultReactNativeHost') ||
131
+ line.includes('getJSBundleFile') || line.includes('override')) {
132
+ console.log(` ${i + 1}: ${line}`);
133
+ }
134
+ });
135
+
166
136
  config.modResults.contents = contents;
167
137
  return config;
168
138
  });
@@ -196,6 +166,8 @@ function withOTAUpdateIOS(config) {
196
166
  return URL(fileURLWithPath: path)
197
167
  } else {
198
168
  NSLog("[OTAUpdate] OTA bundle not found at path: %@", path)
169
+ // Clear invalid path
170
+ UserDefaults.standard.removeObject(forKey: "OTAUpdateBundlePath")
199
171
  }
200
172
  }
201
173
  NSLog("[OTAUpdate] Loading default bundle")
@@ -204,14 +176,12 @@ function withOTAUpdateIOS(config) {
204
176
  `;
205
177
 
206
178
  // Strategy 1: Look for bundleURL() with flexible pattern
207
- // Match: func bundleURL() -> URL? { ... } with any content inside
208
179
  const bundleURLPattern1 = /(func\s+bundleURL\s*\(\s*\)\s*->\s*URL\?\s*\{)([\s\S]*?)(\n\s*\})/;
209
180
 
210
181
  if (bundleURLPattern1.test(contents)) {
211
182
  contents = contents.replace(
212
183
  bundleURLPattern1,
213
184
  (match, funcStart, funcBody, funcEnd) => {
214
- // Check if it already has OTA check
215
185
  if (funcBody.includes('getOTABundleURL')) {
216
186
  return match;
217
187
  }
@@ -291,8 +261,10 @@ ${helperFunction}
291
261
  // Check for OTA bundle
292
262
  NSString *bundlePath = [[NSUserDefaults standardUserDefaults] stringForKey:@"OTAUpdateBundlePath"];
293
263
  if (bundlePath && [[NSFileManager defaultManager] fileExistsAtPath:bundlePath]) {
264
+ NSLog(@"[OTAUpdate] Loading OTA bundle: %@", bundlePath);
294
265
  return [NSURL fileURLWithPath:bundlePath];
295
266
  }
267
+ NSLog(@"[OTAUpdate] Loading default bundle");
296
268
  `;
297
269
  contents = contents.replace(
298
270
  sourceURLPattern,
@@ -103,5 +103,5 @@ var _verification = require("./utils/verification");
103
103
  // Utilities
104
104
 
105
105
  // Version info
106
- const VERSION = exports.VERSION = '0.2.3';
106
+ const VERSION = exports.VERSION = '0.2.4';
107
107
  //# sourceMappingURL=index.js.map
@@ -10,5 +10,5 @@ export { OTAApiClient, getDeviceInfo } from './utils/api';
10
10
  export { UpdateStorage, getStorageAdapter } from './utils/storage';
11
11
  export { calculateHash, verifyBundleHash, verifySignature, verifyBundle } from './utils/verification';
12
12
  // Version info
13
- export const VERSION = '0.2.3';
13
+ export const VERSION = '0.2.4';
14
14
  //# sourceMappingURL=index.js.map
@@ -9,5 +9,5 @@ export { UpdateStorage, getStorageAdapter } from './utils/storage';
9
9
  export type { StoredUpdate, StorageAdapter } from './utils/storage';
10
10
  export { calculateHash, verifyBundleHash, verifySignature, verifyBundle, } from './utils/verification';
11
11
  export type { VerificationResult } from './utils/verification';
12
- export declare const VERSION = "0.2.3";
12
+ export declare const VERSION = "0.2.4";
13
13
  //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanikya/ota-react-native",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "OTA Update SDK for React Native apps - self-hosted CodePush/EAS Updates alternative",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
package/src/index.ts CHANGED
@@ -36,4 +36,4 @@ export {
36
36
  export type { VerificationResult } from './utils/verification';
37
37
 
38
38
  // Version info
39
- export const VERSION = '0.2.3';
39
+ export const VERSION = '0.2.4';