react-native-bspatch 1.0.0

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.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +471 -0
  3. package/android/build.gradle +100 -0
  4. package/android/src/main/AndroidManifest.xml +3 -0
  5. package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +139 -0
  6. package/android/src/main/java/vn/reactnativebspatch/BsPatchPackage.kt +16 -0
  7. package/android/src/main/java/vn/reactnativebspatch/OtaBundleResolver.kt +227 -0
  8. package/cpp/CMakeLists.txt +43 -0
  9. package/cpp/bsdiff/bspatch.c +194 -0
  10. package/cpp/bsdiff/bspatch.h +41 -0
  11. package/cpp/bsdiff/bspatch_wrapper.c +150 -0
  12. package/cpp/bsdiff/bspatch_wrapper.h +6 -0
  13. package/cpp/bspatch_jni.cpp +41 -0
  14. package/cpp/bzip2/LICENSE +42 -0
  15. package/cpp/bzip2/blocksort.c +1094 -0
  16. package/cpp/bzip2/bzlib.c +1572 -0
  17. package/cpp/bzip2/bzlib.h +282 -0
  18. package/cpp/bzip2/bzlib_private.h +509 -0
  19. package/cpp/bzip2/compress.c +672 -0
  20. package/cpp/bzip2/crctable.c +104 -0
  21. package/cpp/bzip2/decompress.c +652 -0
  22. package/cpp/bzip2/huffman.c +205 -0
  23. package/cpp/bzip2/randtable.c +84 -0
  24. package/ios/OtaBundleResolver.swift +241 -0
  25. package/ios/RNBsPatch.h +8 -0
  26. package/ios/RNBsPatch.mm +198 -0
  27. package/lib/commonjs/NativeBsPatch.js +9 -0
  28. package/lib/commonjs/NativeBsPatch.js.map +1 -0
  29. package/lib/commonjs/crypto-js.d.js +2 -0
  30. package/lib/commonjs/crypto-js.d.js.map +1 -0
  31. package/lib/commonjs/index.js +90 -0
  32. package/lib/commonjs/index.js.map +1 -0
  33. package/lib/commonjs/ota/OtaProvider.js +306 -0
  34. package/lib/commonjs/ota/OtaProvider.js.map +1 -0
  35. package/lib/commonjs/ota/otaAssetResolver.js +145 -0
  36. package/lib/commonjs/ota/otaAssetResolver.js.map +1 -0
  37. package/lib/commonjs/ota/otaCore.js +736 -0
  38. package/lib/commonjs/ota/otaCore.js.map +1 -0
  39. package/lib/commonjs/ota/otaLogger.js +64 -0
  40. package/lib/commonjs/ota/otaLogger.js.map +1 -0
  41. package/lib/commonjs/ota/otaMetadata.js +138 -0
  42. package/lib/commonjs/ota/otaMetadata.js.map +1 -0
  43. package/lib/commonjs/ota/storage/CustomStorage.js +140 -0
  44. package/lib/commonjs/ota/storage/CustomStorage.js.map +1 -0
  45. package/lib/commonjs/ota/storage/GitHubReleaseStorage.js +96 -0
  46. package/lib/commonjs/ota/storage/GitHubReleaseStorage.js.map +1 -0
  47. package/lib/commonjs/ota/types.js +39 -0
  48. package/lib/commonjs/ota/types.js.map +1 -0
  49. package/lib/commonjs/package.json +1 -0
  50. package/lib/module/NativeBsPatch.js +5 -0
  51. package/lib/module/NativeBsPatch.js.map +1 -0
  52. package/lib/module/crypto-js.d.js +2 -0
  53. package/lib/module/crypto-js.d.js.map +1 -0
  54. package/lib/module/index.js +23 -0
  55. package/lib/module/index.js.map +1 -0
  56. package/lib/module/ota/OtaProvider.js +299 -0
  57. package/lib/module/ota/OtaProvider.js.map +1 -0
  58. package/lib/module/ota/otaAssetResolver.js +138 -0
  59. package/lib/module/ota/otaAssetResolver.js.map +1 -0
  60. package/lib/module/ota/otaCore.js +725 -0
  61. package/lib/module/ota/otaCore.js.map +1 -0
  62. package/lib/module/ota/otaLogger.js +59 -0
  63. package/lib/module/ota/otaLogger.js.map +1 -0
  64. package/lib/module/ota/otaMetadata.js +127 -0
  65. package/lib/module/ota/otaMetadata.js.map +1 -0
  66. package/lib/module/ota/storage/CustomStorage.js +134 -0
  67. package/lib/module/ota/storage/CustomStorage.js.map +1 -0
  68. package/lib/module/ota/storage/GitHubReleaseStorage.js +90 -0
  69. package/lib/module/ota/storage/GitHubReleaseStorage.js.map +1 -0
  70. package/lib/module/ota/types.js +38 -0
  71. package/lib/module/ota/types.js.map +1 -0
  72. package/lib/typescript/NativeBsPatch.d.ts +39 -0
  73. package/lib/typescript/NativeBsPatch.d.ts.map +1 -0
  74. package/lib/typescript/index.d.ts +15 -0
  75. package/lib/typescript/index.d.ts.map +1 -0
  76. package/lib/typescript/ota/OtaProvider.d.ts +10 -0
  77. package/lib/typescript/ota/OtaProvider.d.ts.map +1 -0
  78. package/lib/typescript/ota/otaAssetResolver.d.ts +15 -0
  79. package/lib/typescript/ota/otaAssetResolver.d.ts.map +1 -0
  80. package/lib/typescript/ota/otaCore.d.ts +109 -0
  81. package/lib/typescript/ota/otaCore.d.ts.map +1 -0
  82. package/lib/typescript/ota/otaLogger.d.ts +44 -0
  83. package/lib/typescript/ota/otaLogger.d.ts.map +1 -0
  84. package/lib/typescript/ota/otaMetadata.d.ts +46 -0
  85. package/lib/typescript/ota/otaMetadata.d.ts.map +1 -0
  86. package/lib/typescript/ota/storage/CustomStorage.d.ts +12 -0
  87. package/lib/typescript/ota/storage/CustomStorage.d.ts.map +1 -0
  88. package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts +20 -0
  89. package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts.map +1 -0
  90. package/lib/typescript/ota/types.d.ts +191 -0
  91. package/lib/typescript/ota/types.d.ts.map +1 -0
  92. package/package.json +89 -0
  93. package/react-native-bspatch.podspec +46 -0
  94. package/src/NativeBsPatch.ts +52 -0
  95. package/src/crypto-js.d.ts +1 -0
  96. package/src/index.ts +29 -0
  97. package/src/ota/OtaProvider.tsx +399 -0
  98. package/src/ota/otaAssetResolver.ts +182 -0
  99. package/src/ota/otaCore.ts +963 -0
  100. package/src/ota/otaLogger.ts +72 -0
  101. package/src/ota/otaMetadata.ts +149 -0
  102. package/src/ota/storage/CustomStorage.ts +143 -0
  103. package/src/ota/storage/GitHubReleaseStorage.ts +98 -0
  104. package/src/ota/types.ts +211 -0
  105. package/tools/bsdiff +0 -0
  106. package/tools/bsdiff.c +445 -0
  107. package/tools/bsdiff.h +45 -0
  108. package/tools/build_ota.sh +457 -0
  109. package/tools/setup_project.js +273 -0
  110. package/tools/update_registry.js +145 -0
  111. package/tools/upload_ota_custom.sh +138 -0
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env node
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+ // react-native-bspatch Setup Script
4
+ //
5
+ // Automatically injects OTA integration into a React Native project:
6
+ // - iOS: Patches project.pbxproj (Xcode build phase bundle sync)
7
+ // Patches AppDelegate.swift (import + bundleURL override)
8
+ // - Android: Patches MainApplication.kt (import + jsBundleFilePath)
9
+ //
10
+ // Idempotent: safe to run multiple times; skips already-patched files.
11
+ // Usage: node setup_project.js [--root <path>]
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ // ── Helpers ──────────────────────────────────────────────────────────────────
18
+
19
+ function findFile(baseDir, predicate, maxDepth = 10) {
20
+ const results = [];
21
+ function walk(dir, depth) {
22
+ if (depth > maxDepth) return;
23
+ let entries;
24
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
25
+ for (const e of entries) {
26
+ const full = path.join(dir, e.name);
27
+ if (e.isDirectory()) { walk(full, depth + 1); }
28
+ else if (predicate(e.name, full)) { results.push(full); }
29
+ }
30
+ }
31
+ walk(baseDir, 0);
32
+ return results;
33
+ }
34
+
35
+ function patchFile(filePath, label, patchFn) {
36
+ if (!fs.existsSync(filePath)) {
37
+ console.log(` ⚠ ${label}: file not found at ${filePath}`);
38
+ return false;
39
+ }
40
+ const original = fs.readFileSync(filePath, 'utf8');
41
+ const patched = patchFn(original);
42
+ if (patched === original) {
43
+ console.log(` ✓ ${label}: already set up (skipped)`);
44
+ return false;
45
+ }
46
+ fs.writeFileSync(filePath, patched, 'utf8');
47
+ console.log(` ✓ ${label}: patched`);
48
+ return true;
49
+ }
50
+
51
+ // ── Constants ─────────────────────────────────────────────────────────────────
52
+
53
+ const XCODE_BUNDLE_SYNC_SNIPPET = [
54
+ '',
55
+ '# [react-native-bspatch] Sync compiled bundle to output/ for OTA builds',
56
+ 'DEST_FILE="$PROJECT_DIR/output/main.jsbundle"',
57
+ 'BUNDLE_RESULT="$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/main.jsbundle"',
58
+ '',
59
+ 'if [ -f "$BUNDLE_RESULT" ]; then',
60
+ ' mkdir -p "$(dirname \\"$DEST_FILE\\")"',
61
+ ' cp "$BUNDLE_RESULT" "$DEST_FILE"',
62
+ ' echo "Bundle synced to $DEST_FILE"',
63
+ 'else',
64
+ ' echo "Bundle not found at $BUNDLE_RESULT (skipping sync)"',
65
+ 'fi',
66
+ ].join('\\n');
67
+
68
+ // The escaped version as it appears in pbxproj shellScript value
69
+ const XCODE_BUNDLE_SYNC_MARKER = 'react-native-bspatch] Sync compiled bundle to output/';
70
+
71
+ const IOS_IMPORT = 'import react_native_bspatch';
72
+ const IOS_BUNDLE_URL_OVERRIDE = `
73
+ override func bundleURL() -> URL? {
74
+ #if DEBUG
75
+ return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
76
+ #else
77
+ if let otaBundle = OtaBundleResolver.resolve() {
78
+ return otaBundle
79
+ }
80
+ return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
81
+ #endif
82
+ }
83
+ `;
84
+
85
+ const ANDROID_IMPORT = 'import vn.reactnativebspatch.OtaBundleResolver';
86
+ const ANDROID_JS_BUNDLE = 'jsBundleFilePath = OtaBundleResolver.resolve(applicationContext)';
87
+
88
+ // ── iOS: project.pbxproj ─────────────────────────────────────────────────────
89
+
90
+ function patchPbxproj(content) {
91
+ // Already patched?
92
+ if (content.includes(XCODE_BUNDLE_SYNC_MARKER)) return content;
93
+
94
+ // Find the Bundle React Native code and images shellScript value
95
+ // Pattern: name = "Bundle React Native code and images"; ... shellScript = "...";
96
+ // The shellScript ends with \n\";\n — we inject before the last closing \n\";
97
+ const shellScriptRegex = /(00DD1BFF1BD5951E006B06BC[^;]*?shellScript = ")([\s\S]*?)(\s*";)/m;
98
+ const match = content.match(shellScriptRegex);
99
+ if (!match) {
100
+ // Fallback: find by name
101
+ console.log(' ⚠ Could not find "Bundle React Native code and images" build phase by UUID, trying by name...');
102
+ // Simpler approach: find shellScript belonging to the bundle phase by name proximity
103
+ const nameIdx = content.indexOf('name = "Bundle React Native code and images"');
104
+ if (nameIdx === -1) {
105
+ console.log(' ⚠ Could not find build phase. Please add the bundle sync snippet manually.');
106
+ return content;
107
+ }
108
+ // Find the shellScript = "..." after this name line
109
+ const afterName = content.indexOf('shellScript = "', nameIdx - 500);
110
+ if (afterName === -1) return content;
111
+
112
+ // Find the end of this shellScript: look for \n\"; after it
113
+ const scriptEnd = content.indexOf('\\n";', afterName);
114
+ if (scriptEnd === -1) return content;
115
+
116
+ return (
117
+ content.slice(0, scriptEnd) +
118
+ XCODE_BUNDLE_SYNC_SNIPPET +
119
+ content.slice(scriptEnd)
120
+ );
121
+ }
122
+
123
+ // Insert before the closing \n\";
124
+ const scriptBody = match[2];
125
+ // Remove trailing \n if present before closing
126
+ const insertPoint = match.index + match[1].length + scriptBody.length;
127
+ return (
128
+ content.slice(0, insertPoint) +
129
+ XCODE_BUNDLE_SYNC_SNIPPET +
130
+ content.slice(insertPoint)
131
+ );
132
+ }
133
+
134
+ // ── iOS: AppDelegate.swift ────────────────────────────────────────────────────
135
+
136
+ function patchAppDelegate(content) {
137
+ let result = content;
138
+ let changed = false;
139
+
140
+ // 1. Add import if missing
141
+ if (!result.includes(IOS_IMPORT)) {
142
+ // Insert after the last import line
143
+ const lastImportMatch = [...result.matchAll(/^import .+$/gm)].pop();
144
+ if (lastImportMatch) {
145
+ const idx = lastImportMatch.index + lastImportMatch[0].length;
146
+ result = result.slice(0, idx) + '\n' + IOS_IMPORT + result.slice(idx);
147
+ changed = true;
148
+ }
149
+ }
150
+
151
+ // 2. Add bundleURL override if missing
152
+ if (!result.includes('OtaBundleResolver.resolve()')) {
153
+ // Look for an existing bundleURL() function
154
+ const bundleURLFnRegex = /override func bundleURL\(\)[^{]*\{[\s\S]*?\}/m;
155
+ if (bundleURLFnRegex.test(result)) {
156
+ // Replace existing bundleURL() entirely
157
+ result = result.replace(bundleURLFnRegex, IOS_BUNDLE_URL_OVERRIDE.trim());
158
+ } else {
159
+ // Look for sourceURL override and insert bundleURL after it, or at end of class
160
+ const sourceURLIdx = result.lastIndexOf('override func sourceURL');
161
+ if (sourceURLIdx !== -1) {
162
+ // Find end of that function
163
+ const endBrace = result.indexOf('\n }', sourceURLIdx) + 4;
164
+ result = result.slice(0, endBrace) + '\n' + IOS_BUNDLE_URL_OVERRIDE + result.slice(endBrace);
165
+ } else {
166
+ // Insert before last closing brace of the file
167
+ const lastBrace = result.lastIndexOf('}');
168
+ result = result.slice(0, lastBrace) + IOS_BUNDLE_URL_OVERRIDE + '\n' + result.slice(lastBrace);
169
+ }
170
+ }
171
+ changed = true;
172
+ }
173
+
174
+ return changed ? result : content;
175
+ }
176
+
177
+ // ── Android: MainApplication.kt ──────────────────────────────────────────────
178
+
179
+ function patchMainApplication(content) {
180
+ let result = content;
181
+ let changed = false;
182
+
183
+ // 1. Add import if missing
184
+ if (!result.includes(ANDROID_IMPORT)) {
185
+ const lastImportMatch = [...result.matchAll(/^import .+$/gm)].pop();
186
+ if (lastImportMatch) {
187
+ const idx = lastImportMatch.index + lastImportMatch[0].length;
188
+ result = result.slice(0, idx) + '\n' + ANDROID_IMPORT + result.slice(idx);
189
+ changed = true;
190
+ }
191
+ }
192
+
193
+ // 2. Add jsBundleFilePath if missing
194
+ if (!result.includes('OtaBundleResolver.resolve')) {
195
+ // Find getDefaultReactHost(... call and inject jsBundleFilePath param
196
+ // Look for closing paren of getDefaultReactHost block
197
+ const hostCallRegex = /getDefaultReactHost\s*\(\s*([\s\S]*?)\)/m;
198
+ const hostMatch = result.match(hostCallRegex);
199
+ if (hostMatch) {
200
+ // Insert before the closing paren
201
+ const closeIdx = hostMatch.index + hostMatch[0].length - 1;
202
+ result =
203
+ result.slice(0, closeIdx) +
204
+ ',\n ' + ANDROID_JS_BUNDLE + '\n ' +
205
+ result.slice(closeIdx);
206
+ changed = true;
207
+ } else {
208
+ console.log(' ⚠ Could not find getDefaultReactHost(...) call. Please add jsBundleFilePath manually.');
209
+ }
210
+ }
211
+
212
+ return changed ? result : content;
213
+ }
214
+
215
+ // ── Main ──────────────────────────────────────────────────────────────────────
216
+
217
+ function main() {
218
+ // Determine project root
219
+ const args = process.argv.slice(2);
220
+ const rootIdx = args.indexOf('--root');
221
+ const ROOT = rootIdx !== -1 ? path.resolve(args[rootIdx + 1]) : process.cwd();
222
+
223
+ console.log('\n=== react-native-bspatch Setup ===');
224
+ console.log(` Project root: ${ROOT}`);
225
+
226
+ const IOS_DIR = path.join(ROOT, 'ios');
227
+ const ANDROID_DIR = path.join(ROOT, 'android');
228
+
229
+ // ── iOS ──
230
+ console.log('\n[iOS]');
231
+
232
+ // project.pbxproj
233
+ const pbxprojFiles = findFile(IOS_DIR, (name) => name === 'project.pbxproj');
234
+ if (pbxprojFiles.length === 0) {
235
+ console.log(' ⚠ No project.pbxproj found. Is this an iOS project?');
236
+ } else {
237
+ const pbxproj = pbxprojFiles[0];
238
+ patchFile(pbxproj, 'project.pbxproj (Xcode build phase)', patchPbxproj);
239
+ }
240
+
241
+ // AppDelegate.swift
242
+ const appDelegateFiles = findFile(IOS_DIR, (name) => name === 'AppDelegate.swift');
243
+ if (appDelegateFiles.length === 0) {
244
+ console.log(' ⚠ No AppDelegate.swift found.');
245
+ } else {
246
+ patchFile(appDelegateFiles[0], 'AppDelegate.swift', patchAppDelegate);
247
+ }
248
+
249
+ // Ensure ios/output/ exists
250
+ const outputDir = path.join(IOS_DIR, 'output');
251
+ if (!fs.existsSync(outputDir)) {
252
+ fs.mkdirSync(outputDir, { recursive: true });
253
+ console.log(' ✓ Created ios/output/ directory');
254
+ }
255
+
256
+ // ── Android ──
257
+ console.log('\n[Android]');
258
+ const mainAppFiles = findFile(ANDROID_DIR, (name) => name === 'MainApplication.kt');
259
+ if (mainAppFiles.length === 0) {
260
+ console.log(' ⚠ No MainApplication.kt found. Is this an Android project?');
261
+ } else {
262
+ patchFile(mainAppFiles[0], 'MainApplication.kt', patchMainApplication);
263
+ }
264
+
265
+ console.log('\n=== Setup Complete ===');
266
+ console.log('Next steps:');
267
+ console.log(' 1. iOS: Run Xcode build once to populate ios/output/main.jsbundle');
268
+ console.log(' (or run: yarn ota:init --platform ios)');
269
+ console.log(' 2. Android: Run: yarn ota:init --platform android');
270
+ console.log(' 3. Then: yarn ota:build to start publishing OTA updates\n');
271
+ }
272
+
273
+ main();
@@ -0,0 +1,145 @@
1
+ #!/bin/env node
2
+ /**
3
+ * update_registry.js
4
+ *
5
+ * Called by build_ota.sh after generating patches.
6
+ * 1. Reads : android/output/ota_registry.json (or ios/output/...)
7
+ * 2. Scopes to : appVersion (e.g. "1.0.0")
8
+ * 3. Scopes to : flavor (e.g. "dev", "staging", "production")
9
+ * 4. Updates patches and latestOtaVersion for that specific app+flavor combination.
10
+ * 5. Writes : otaPatches.generated.ts (flattened across all app versions/flavors)
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ const [
17
+ projectRoot,
18
+ appVersion,
19
+ toOtaVersionStr,
20
+ patchesJson,
21
+ platform = 'android',
22
+ _assetManifestFile = '',
23
+ flavorInput = 'dev',
24
+ ] = process.argv.slice(2);
25
+
26
+ const toOtaVersion = parseInt(toOtaVersionStr, 10);
27
+ const newPatches = JSON.parse(patchesJson || '[]');
28
+
29
+ const flavor = flavorInput.toLowerCase();
30
+
31
+ const registryPath = projectRoot.endsWith('.json')
32
+ ? projectRoot
33
+ : path.join(projectRoot, 'ota_registry.json');
34
+
35
+ const generatedPath = projectRoot.endsWith('.json')
36
+ ? path.join(path.dirname(projectRoot), 'src/ota/otaPatches.generated.ts')
37
+ : path.join(projectRoot, 'src/ota/otaPatches.generated.ts');
38
+
39
+ // ── Load or init registry ───────────────────────────────────────────────────
40
+ let registry = { apps: {} };
41
+ if (fs.existsSync(registryPath)) {
42
+ try {
43
+ registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
44
+ } catch (e) {
45
+ console.warn(' ⚠ Failed to parse existing registry, starting fresh');
46
+ }
47
+ }
48
+
49
+ // Ensure nested structure: apps -> appVersion -> flavors -> flavor
50
+ if (!registry.apps) registry.apps = {};
51
+ if (!registry.apps[appVersion]) {
52
+ registry.apps[appVersion] = { flavors: {} };
53
+ }
54
+ if (!registry.apps[appVersion].flavors) {
55
+ registry.apps[appVersion].flavors = {};
56
+ }
57
+ if (!registry.apps[appVersion].flavors[flavor]) {
58
+ registry.apps[appVersion].flavors[flavor] = { latestOtaVersion: 0, patches: [] };
59
+ }
60
+
61
+ const flavorGroup = registry.apps[appVersion].flavors[flavor];
62
+ flavorGroup.latestOtaVersion = toOtaVersion;
63
+
64
+ // ── Update patches ──────────────────────────────────────────────────────────
65
+ if (newPatches.length > 0) {
66
+ // We merge patches by platform to avoid overwriting between Android and iOS
67
+ const otherPlatformPatches = flavorGroup.patches.filter(p => p.platform !== platform);
68
+
69
+ const mappedNewPatches = newPatches.map(p => {
70
+ // Naming convention from build_ota.sh
71
+ const fullBundleName = `bundle_${appVersion}_v${toOtaVersion}_${platform}_${flavor}.hbc`;
72
+ const manifestName = path.basename(_assetManifestFile);
73
+
74
+ return {
75
+ id: `v${p.from}-to-v${p.to}-${flavor}-${platform}`,
76
+ label: `V${p.from} → V${p.to} (${flavor} ${platform})`,
77
+ appVersion,
78
+ filename: p.file,
79
+ fromVersion: p.from,
80
+ toVersion: p.to,
81
+ platform,
82
+ baseHash: p.baseHash || '',
83
+ bundleHash: p.bundleHash || '',
84
+ fullBundleUrl: fullBundleName,
85
+ assetManifestUrl: manifestName,
86
+ flavor: flavor,
87
+ };
88
+ });
89
+
90
+ flavorGroup.patches = [...otherPlatformPatches, ...mappedNewPatches];
91
+ }
92
+
93
+ // ── Write updated registry ───────────────────────────────────────────────────
94
+ fs.mkdirSync(path.dirname(registryPath), { recursive: true });
95
+ fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2) + '\n');
96
+ console.log(
97
+ ` ✓ Registry updated | App: ${appVersion} | Flavor: ${flavor} | latestOta: ${toOtaVersion}`,
98
+ );
99
+
100
+ // ── Regenerate otaPatches.generated.ts ──────────────────────────────────────
101
+ // Flatten all patches from all apps and flavors into the TS file
102
+ const allPatches = [];
103
+ for (const ver in registry.apps) {
104
+ const app = registry.apps[ver];
105
+ if (app.flavors) {
106
+ for (const f in app.flavors) {
107
+ allPatches.push(...app.flavors[f].patches);
108
+ }
109
+ }
110
+ }
111
+
112
+ const patchLines = allPatches
113
+ .map(p => {
114
+ const fields = [
115
+ `id: '${p.id}'`,
116
+ `label: '${p.label}'`,
117
+ `appVersion: '${p.appVersion}'`,
118
+ `filename: '${p.filename}'`,
119
+ `fromVersion: ${p.fromVersion}`,
120
+ `toVersion: ${p.toVersion}`,
121
+ `platform: '${p.platform || 'android'}'`,
122
+ ];
123
+ if (p.baseHash) fields.push(`baseHash: '${p.baseHash}'`);
124
+ if (p.bundleHash) fields.push(`bundleHash: '${p.bundleHash}'`);
125
+ if (p.fullBundleUrl) fields.push(`fullBundleUrl: '${p.fullBundleUrl}'`);
126
+ if (p.assetManifestUrl) fields.push(`assetManifestUrl: '${p.assetManifestUrl}'`);
127
+ // Add flavor for runtime filtering if needed
128
+ fields.push(`flavor: '${p.flavor || ''}'`);
129
+
130
+ return ` { ${fields.join(', ')} },`;
131
+ })
132
+ .join('\n');
133
+
134
+ const tsContent = `// AUTO-GENERATED by build_ota.sh — do not edit manually
135
+ import type { OtaPatch } from 'react-native-bspatch';
136
+
137
+ export const OTA_PATCHES: OtaPatch[] = [
138
+ ${patchLines}
139
+ ];
140
+ `;
141
+
142
+ fs.writeFileSync(generatedPath, tsContent);
143
+ console.log(
144
+ ` ✓ otaPatches.generated.ts updated (${allPatches.length} total patches across all flavors)`,
145
+ );
@@ -0,0 +1,138 @@
1
+ #!/bin/bash
2
+
3
+ # ─────────────────────────────────────────────────────────────────────────────
4
+ # Custom OTA Upload Script for Sync Server
5
+ # Custom server is a drop-in replacement for GitHub releases.
6
+ # Uploads: 1) All .patch files → {baseFolder}/{appVersion}/
7
+ # 2) ota_registry.json → {baseFolder}/ota_registry.json
8
+ # ─────────────────────────────────────────────────────────────────────────────
9
+
10
+ set -e
11
+
12
+ BASE_URL="https://sync-server.osp.com.vn"
13
+ UPCODE_USER="callapi_up"
14
+ UPCODE_PASS="41221312fsdfdsf2fsdgsdf1AA1"
15
+
16
+ APP_VERSION="$1"
17
+ CHANNEL="$2"
18
+ OTA_VERSION="$3"
19
+ BUNDLE_PATH="$4" # unused — we upload .patch files, not the bundle
20
+ PLATFORM="$5"
21
+ BASE_FOLDER="${6:-congplqg}"
22
+
23
+ ROOT_DIR=$(pwd)
24
+ REGISTRY_FILE="$ROOT_DIR/ota_registry.json"
25
+ OUTPUT_DIR="$ROOT_DIR/android/output"
26
+ if [[ "$PLATFORM" == "ios" ]]; then
27
+ OUTPUT_DIR="$ROOT_DIR/ios/output"
28
+ fi
29
+
30
+ echo ""
31
+ echo "=== Custom OTA Upload | App: $APP_VERSION | Channel: $CHANNEL | V$OTA_VERSION ==="
32
+ echo " Output dir: $OUTPUT_DIR"
33
+
34
+ # Validate
35
+ if [[ -z "$APP_VERSION" || -z "$OTA_VERSION" ]]; then
36
+ echo "Usage: $0 <app_version> <channel> <ota_version> <bundle_path> [platform] [base_folder]"
37
+ exit 1
38
+ fi
39
+ if [[ ! -f "$REGISTRY_FILE" ]]; then
40
+ echo "ERROR: ota_registry.json not found: $REGISTRY_FILE"
41
+ exit 1
42
+ fi
43
+
44
+ # 1. Login
45
+ echo "Logging in..."
46
+ LOGIN_RES=$(curl -s -X "POST" "$BASE_URL/public/authen/login" \
47
+ -H 'Content-Type: application/json; charset=utf-8' \
48
+ -d "{\"username\": \"$UPCODE_USER\", \"password\": \"$UPCODE_PASS\"}")
49
+
50
+ TOKEN=$(echo "$LOGIN_RES" | node -e "
51
+ try {
52
+ const r = JSON.parse(require('fs').readFileSync(0, 'utf8'));
53
+ console.log(r.data?.accessTokenInfo?.accessToken || '');
54
+ } catch(e) { console.log(''); }
55
+ ")
56
+ if [[ -z "$TOKEN" ]]; then
57
+ echo "ERROR: Login failed. Response: $LOGIN_RES"
58
+ exit 1
59
+ fi
60
+ echo " ✓ Login successful"
61
+
62
+ # ── Helpers ─────────────────────────────────────────────────────────────────
63
+
64
+ remote_delete() {
65
+ local path=$1
66
+ local encoded
67
+ encoded=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$path'))")
68
+ curl -s -X "DELETE" "$BASE_URL/api/minio/delete?path=$encoded" \
69
+ -H "Authorization: Bearer $TOKEN" > /dev/null
70
+ }
71
+
72
+ remote_upload() {
73
+ local local_file=$1
74
+ local remote_path=$2
75
+ echo " uploading: $remote_path"
76
+ local RES
77
+ RES=$(curl -s -X "POST" "$BASE_URL/api/minio/upload" \
78
+ -H "Authorization: Bearer $TOKEN" \
79
+ -H 'Content-Type: multipart/form-data' \
80
+ -F "file=@$local_file" \
81
+ -F "path=$remote_path")
82
+ if [[ "$RES" != *"\"success\":true"* ]] && [[ "$RES" != *"\"code\":1"* ]] && [[ "$RES" != *"\"status\":200"* ]] && [[ "$RES" != *"\"code\":200"* ]]; then
83
+ echo " ⚠ Upload potential issue: $RES"
84
+ else
85
+ echo " ✓ Upload success"
86
+ fi
87
+ }
88
+
89
+ # 2. Upload all patch files, full bundles, and asset manifests
90
+ echo "Uploading files for channel: $CHANNEL..."
91
+ FILE_COUNT=0
92
+ for FILE in "$OUTPUT_DIR"/*.{patch,hbc,json}; do
93
+ [[ -f "$FILE" ]] || continue
94
+ FNAME=$(basename "$FILE")
95
+ # Only upload files belonging to the current APP_VERSION
96
+ if [[ "$FNAME" == "patch_"* ]] || [[ "$FNAME" == "bundle_"* ]]; then
97
+ if [[ "$FNAME" != *"_${APP_VERSION}_"* ]]; then
98
+ continue
99
+ fi
100
+ fi
101
+
102
+ # Further general filtering
103
+ if [[ "$FNAME" != *"_${APP_VERSION}_"* ]] && [[ "$FNAME" != *"_${CHANNEL}_"* ]] && [[ "$FNAME" != *"_${PLATFORM}_"* ]] && [[ "$FNAME" != "asset-manifest"* ]] && [[ "$FNAME" != "base-assets"* ]]; then
104
+ continue
105
+ fi
106
+
107
+ REMOTE_PATH="$BASE_FOLDER/$APP_VERSION/$CHANNEL/$FNAME"
108
+ remote_delete "$REMOTE_PATH"
109
+ remote_upload "$FILE" "$REMOTE_PATH"
110
+ FILE_COUNT=$((FILE_COUNT + 1))
111
+ done
112
+
113
+ # 3. Upload hashed asset store
114
+ if [[ -d "$OUTPUT_DIR/asset-store" ]]; then
115
+ echo "Uploading hashed assets..."
116
+ for FILE in "$OUTPUT_DIR/asset-store"/*; do
117
+ [[ -f "$FILE" ]] || continue
118
+ FNAME=$(basename "$FILE")
119
+ # Assets are global to the base folder (content-addressed)
120
+ REMOTE_PATH="$BASE_FOLDER/assets/$FNAME"
121
+ # Optional: skip if already exists to save time, but for now we re-upload for safety
122
+ remote_upload "$FILE" "$REMOTE_PATH"
123
+ FILE_COUNT=$((FILE_COUNT + 1))
124
+ done
125
+ fi
126
+ echo " ✓ $FILE_COUNT file(s) uploaded (patches + bundles + assets)"
127
+
128
+ # 3. Upload ota_registry.json
129
+ echo "Uploading ota_registry.json..."
130
+ REMOTE_REGISTRY="$BASE_FOLDER/ota_registry.json"
131
+ remote_delete "$REMOTE_REGISTRY"
132
+ remote_upload "$REGISTRY_FILE" "$REMOTE_REGISTRY"
133
+ echo " ✓ Registry uploaded"
134
+
135
+ echo ""
136
+ echo "=== Upload Complete ==="
137
+ echo " Registry : $REMOTE_REGISTRY"
138
+ echo " Patches : $BASE_FOLDER/$APP_VERSION/patch_*.patch ($PATCH_COUNT files)"