@vanikya/ota-react-native 0.2.0 → 0.2.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.
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/otaupdate/OTAUpdateModule.kt +116 -25
- package/app.plugin.js +53 -12
- package/ios/OTAUpdate.swift +29 -0
- package/lib/commonjs/OTAProvider.js +37 -0
- package/lib/commonjs/OTAProvider.js.map +1 -1
- package/lib/commonjs/components/OTADebugPanel.js +426 -0
- package/lib/commonjs/components/OTADebugPanel.js.map +1 -0
- package/lib/commonjs/hooks/useOTAUpdate.js +38 -2
- package/lib/commonjs/hooks/useOTAUpdate.js.map +1 -1
- package/lib/commonjs/index.js +10 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/utils/storage.js +79 -4
- package/lib/commonjs/utils/storage.js.map +1 -1
- package/lib/module/OTAProvider.js +38 -1
- package/lib/module/OTAProvider.js.map +1 -1
- package/lib/module/components/OTADebugPanel.js +418 -0
- package/lib/module/components/OTADebugPanel.js.map +1 -0
- package/lib/module/hooks/useOTAUpdate.js +38 -2
- package/lib/module/hooks/useOTAUpdate.js.map +1 -1
- package/lib/module/index.js +4 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/storage.js +79 -4
- package/lib/module/utils/storage.js.map +1 -1
- package/lib/typescript/OTAProvider.d.ts.map +1 -1
- package/lib/typescript/components/OTADebugPanel.d.ts +18 -0
- package/lib/typescript/components/OTADebugPanel.d.ts.map +1 -0
- package/lib/typescript/hooks/useOTAUpdate.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/utils/storage.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/OTAProvider.tsx +40 -0
- package/src/components/OTADebugPanel.tsx +447 -0
- package/src/hooks/useOTAUpdate.ts +49 -2
- package/src/index.ts +4 -1
- package/src/utils/storage.ts +105 -4
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
TouchableOpacity,
|
|
6
|
+
ScrollView,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
NativeModules,
|
|
9
|
+
Platform,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
|
|
12
|
+
const { OTAUpdate } = NativeModules;
|
|
13
|
+
|
|
14
|
+
interface LogEntry {
|
|
15
|
+
time: string;
|
|
16
|
+
message: string;
|
|
17
|
+
type: 'info' | 'success' | 'error' | 'warn';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface OTADebugPanelProps {
|
|
21
|
+
testBundleUrl?: string;
|
|
22
|
+
serverUrl?: string;
|
|
23
|
+
appSlug?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Debug panel for testing OTA update functionality.
|
|
28
|
+
* Use this component during development to verify:
|
|
29
|
+
* - Native module is linked correctly
|
|
30
|
+
* - Downloads work properly
|
|
31
|
+
* - Hash calculation works
|
|
32
|
+
* - SharedPreferences/UserDefaults are saved
|
|
33
|
+
* - Bundle loading on restart
|
|
34
|
+
*/
|
|
35
|
+
export function OTADebugPanel({ testBundleUrl, serverUrl, appSlug }: OTADebugPanelProps) {
|
|
36
|
+
const [logs, setLogs] = useState<LogEntry[]>([]);
|
|
37
|
+
const [isExpanded, setIsExpanded] = useState(true);
|
|
38
|
+
|
|
39
|
+
const addLog = (message: string, type: LogEntry['type'] = 'info') => {
|
|
40
|
+
const time = new Date().toLocaleTimeString();
|
|
41
|
+
setLogs(prev => [...prev, { time, message, type }]);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const clearLogs = () => setLogs([]);
|
|
45
|
+
|
|
46
|
+
// Check native module on mount
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (OTAUpdate) {
|
|
49
|
+
addLog('Native module found', 'success');
|
|
50
|
+
const docDir = OTAUpdate.getDocumentDirectory?.();
|
|
51
|
+
if (docDir) {
|
|
52
|
+
addLog(`Document directory: ${docDir}`, 'info');
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
addLog('Native module NOT found - OTA will not work!', 'error');
|
|
56
|
+
}
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
const testNativeModule = async () => {
|
|
60
|
+
addLog('Testing native module...');
|
|
61
|
+
|
|
62
|
+
if (!OTAUpdate) {
|
|
63
|
+
addLog('Native module not available', 'error');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
// Test getDocumentDirectory
|
|
69
|
+
const docDir = OTAUpdate.getDocumentDirectory();
|
|
70
|
+
addLog(`getDocumentDirectory: ${docDir}`, 'success');
|
|
71
|
+
|
|
72
|
+
// Test file operations
|
|
73
|
+
const testPath = `${docDir}ota-test.txt`;
|
|
74
|
+
await OTAUpdate.writeFile(testPath, 'Hello OTA!');
|
|
75
|
+
addLog(`writeFile: ${testPath}`, 'success');
|
|
76
|
+
|
|
77
|
+
const content = await OTAUpdate.readFile(testPath);
|
|
78
|
+
addLog(`readFile: "${content}"`, 'success');
|
|
79
|
+
|
|
80
|
+
const exists = await OTAUpdate.exists(testPath);
|
|
81
|
+
addLog(`exists: ${exists}`, 'success');
|
|
82
|
+
|
|
83
|
+
await OTAUpdate.deleteFile(testPath);
|
|
84
|
+
addLog('deleteFile: success', 'success');
|
|
85
|
+
|
|
86
|
+
const existsAfter = await OTAUpdate.exists(testPath);
|
|
87
|
+
addLog(`exists after delete: ${existsAfter}`, 'success');
|
|
88
|
+
|
|
89
|
+
} catch (error: any) {
|
|
90
|
+
addLog(`Error: ${error.message}`, 'error');
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const testDownload = async () => {
|
|
95
|
+
if (!testBundleUrl) {
|
|
96
|
+
addLog('No testBundleUrl provided', 'warn');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!OTAUpdate?.downloadFile) {
|
|
101
|
+
addLog('downloadFile not available', 'error');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const docDir = OTAUpdate.getDocumentDirectory();
|
|
107
|
+
const destPath = `${docDir}ota-update/test-bundle.js`;
|
|
108
|
+
|
|
109
|
+
addLog(`Downloading from: ${testBundleUrl}`);
|
|
110
|
+
addLog(`Destination: ${destPath}`);
|
|
111
|
+
|
|
112
|
+
const startTime = Date.now();
|
|
113
|
+
const result = await OTAUpdate.downloadFile(testBundleUrl, destPath);
|
|
114
|
+
const duration = Date.now() - startTime;
|
|
115
|
+
|
|
116
|
+
addLog(`Download complete in ${duration}ms`, 'success');
|
|
117
|
+
addLog(`File size: ${result.fileSize} bytes`, 'success');
|
|
118
|
+
|
|
119
|
+
// Verify file exists
|
|
120
|
+
const exists = await OTAUpdate.exists(destPath);
|
|
121
|
+
addLog(`File exists: ${exists}`, exists ? 'success' : 'error');
|
|
122
|
+
|
|
123
|
+
} catch (error: any) {
|
|
124
|
+
addLog(`Download failed: ${error.message}`, 'error');
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const testHashCalculation = async () => {
|
|
129
|
+
if (!OTAUpdate?.calculateSHA256FromFile) {
|
|
130
|
+
addLog('calculateSHA256FromFile not available', 'error');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const docDir = OTAUpdate.getDocumentDirectory();
|
|
136
|
+
const testPath = `${docDir}ota-update/test-bundle.js`;
|
|
137
|
+
|
|
138
|
+
const exists = await OTAUpdate.exists(testPath);
|
|
139
|
+
if (!exists) {
|
|
140
|
+
addLog('Test bundle not found. Run "Test Download" first.', 'warn');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
addLog('Calculating hash...');
|
|
145
|
+
const startTime = Date.now();
|
|
146
|
+
const hash = await OTAUpdate.calculateSHA256FromFile(testPath);
|
|
147
|
+
const duration = Date.now() - startTime;
|
|
148
|
+
|
|
149
|
+
addLog(`Hash: ${hash}`, 'success');
|
|
150
|
+
addLog(`Calculated in ${duration}ms`, 'success');
|
|
151
|
+
|
|
152
|
+
} catch (error: any) {
|
|
153
|
+
addLog(`Hash calculation failed: ${error.message}`, 'error');
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const testApplyBundle = async () => {
|
|
158
|
+
if (!OTAUpdate?.applyBundle) {
|
|
159
|
+
addLog('applyBundle not available', 'error');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const docDir = OTAUpdate.getDocumentDirectory();
|
|
165
|
+
const testPath = `${docDir}ota-update/test-bundle.js`;
|
|
166
|
+
|
|
167
|
+
const exists = await OTAUpdate.exists(testPath);
|
|
168
|
+
if (!exists) {
|
|
169
|
+
addLog('Test bundle not found. Run "Test Download" first.', 'warn');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
addLog(`Registering bundle: ${testPath}`);
|
|
174
|
+
await OTAUpdate.applyBundle(testPath, false); // Don't restart
|
|
175
|
+
addLog('Bundle registered (no restart)', 'success');
|
|
176
|
+
|
|
177
|
+
// Verify it was saved
|
|
178
|
+
const savedPath = await OTAUpdate.getPendingBundlePath();
|
|
179
|
+
addLog(`Saved path: ${savedPath}`, savedPath === testPath ? 'success' : 'error');
|
|
180
|
+
|
|
181
|
+
} catch (error: any) {
|
|
182
|
+
addLog(`Apply failed: ${error.message}`, 'error');
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const testGetPendingBundle = async () => {
|
|
187
|
+
if (!OTAUpdate?.getPendingBundlePath) {
|
|
188
|
+
addLog('getPendingBundlePath not available', 'error');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const path = await OTAUpdate.getPendingBundlePath();
|
|
194
|
+
if (path) {
|
|
195
|
+
addLog(`Pending bundle: ${path}`, 'success');
|
|
196
|
+
const exists = await OTAUpdate.exists(path);
|
|
197
|
+
addLog(`File exists: ${exists}`, exists ? 'success' : 'error');
|
|
198
|
+
} else {
|
|
199
|
+
addLog('No pending bundle', 'info');
|
|
200
|
+
}
|
|
201
|
+
} catch (error: any) {
|
|
202
|
+
addLog(`Error: ${error.message}`, 'error');
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const testClearPendingBundle = async () => {
|
|
207
|
+
if (!OTAUpdate?.clearPendingBundle) {
|
|
208
|
+
addLog('clearPendingBundle not available', 'error');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
await OTAUpdate.clearPendingBundle();
|
|
214
|
+
addLog('Pending bundle cleared', 'success');
|
|
215
|
+
} catch (error: any) {
|
|
216
|
+
addLog(`Error: ${error.message}`, 'error');
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const testApplyAndRestart = async () => {
|
|
221
|
+
if (!OTAUpdate?.applyBundle) {
|
|
222
|
+
addLog('applyBundle not available', 'error');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const docDir = OTAUpdate.getDocumentDirectory();
|
|
228
|
+
const testPath = `${docDir}ota-update/test-bundle.js`;
|
|
229
|
+
|
|
230
|
+
const exists = await OTAUpdate.exists(testPath);
|
|
231
|
+
if (!exists) {
|
|
232
|
+
addLog('Test bundle not found. Run "Test Download" first.', 'warn');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
addLog('Applying bundle and restarting...');
|
|
237
|
+
addLog('App will restart in ~200ms');
|
|
238
|
+
await OTAUpdate.applyBundle(testPath, true); // Restart
|
|
239
|
+
|
|
240
|
+
} catch (error: any) {
|
|
241
|
+
addLog(`Error: ${error.message}`, 'error');
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const getLogColor = (type: LogEntry['type']) => {
|
|
246
|
+
switch (type) {
|
|
247
|
+
case 'success': return '#4CAF50';
|
|
248
|
+
case 'error': return '#F44336';
|
|
249
|
+
case 'warn': return '#FF9800';
|
|
250
|
+
default: return '#2196F3';
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
if (!isExpanded) {
|
|
255
|
+
return (
|
|
256
|
+
<TouchableOpacity
|
|
257
|
+
style={styles.collapsedButton}
|
|
258
|
+
onPress={() => setIsExpanded(true)}
|
|
259
|
+
>
|
|
260
|
+
<Text style={styles.collapsedButtonText}>OTA Debug</Text>
|
|
261
|
+
</TouchableOpacity>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<View style={styles.container}>
|
|
267
|
+
<View style={styles.header}>
|
|
268
|
+
<Text style={styles.title}>OTA Debug Panel</Text>
|
|
269
|
+
<View style={styles.headerButtons}>
|
|
270
|
+
<TouchableOpacity onPress={clearLogs} style={styles.headerButton}>
|
|
271
|
+
<Text style={styles.headerButtonText}>Clear</Text>
|
|
272
|
+
</TouchableOpacity>
|
|
273
|
+
<TouchableOpacity onPress={() => setIsExpanded(false)} style={styles.headerButton}>
|
|
274
|
+
<Text style={styles.headerButtonText}>Hide</Text>
|
|
275
|
+
</TouchableOpacity>
|
|
276
|
+
</View>
|
|
277
|
+
</View>
|
|
278
|
+
|
|
279
|
+
<View style={styles.info}>
|
|
280
|
+
<Text style={styles.infoText}>Platform: {Platform.OS}</Text>
|
|
281
|
+
<Text style={styles.infoText}>
|
|
282
|
+
Native Module: {OTAUpdate ? 'Available' : 'NOT FOUND'}
|
|
283
|
+
</Text>
|
|
284
|
+
</View>
|
|
285
|
+
|
|
286
|
+
<ScrollView style={styles.buttonContainer} horizontal showsHorizontalScrollIndicator={false}>
|
|
287
|
+
<TouchableOpacity style={styles.button} onPress={testNativeModule}>
|
|
288
|
+
<Text style={styles.buttonText}>Test Module</Text>
|
|
289
|
+
</TouchableOpacity>
|
|
290
|
+
<TouchableOpacity style={styles.button} onPress={testDownload}>
|
|
291
|
+
<Text style={styles.buttonText}>Test Download</Text>
|
|
292
|
+
</TouchableOpacity>
|
|
293
|
+
<TouchableOpacity style={styles.button} onPress={testHashCalculation}>
|
|
294
|
+
<Text style={styles.buttonText}>Test Hash</Text>
|
|
295
|
+
</TouchableOpacity>
|
|
296
|
+
<TouchableOpacity style={styles.button} onPress={testApplyBundle}>
|
|
297
|
+
<Text style={styles.buttonText}>Register Bundle</Text>
|
|
298
|
+
</TouchableOpacity>
|
|
299
|
+
<TouchableOpacity style={styles.button} onPress={testGetPendingBundle}>
|
|
300
|
+
<Text style={styles.buttonText}>Get Pending</Text>
|
|
301
|
+
</TouchableOpacity>
|
|
302
|
+
<TouchableOpacity style={styles.button} onPress={testClearPendingBundle}>
|
|
303
|
+
<Text style={styles.buttonText}>Clear Pending</Text>
|
|
304
|
+
</TouchableOpacity>
|
|
305
|
+
<TouchableOpacity style={[styles.button, styles.dangerButton]} onPress={testApplyAndRestart}>
|
|
306
|
+
<Text style={styles.buttonText}>Apply + Restart</Text>
|
|
307
|
+
</TouchableOpacity>
|
|
308
|
+
</ScrollView>
|
|
309
|
+
|
|
310
|
+
<ScrollView style={styles.logContainer}>
|
|
311
|
+
{logs.length === 0 ? (
|
|
312
|
+
<Text style={styles.emptyLog}>Tap a button to start testing...</Text>
|
|
313
|
+
) : (
|
|
314
|
+
logs.map((log, index) => (
|
|
315
|
+
<View key={index} style={styles.logEntry}>
|
|
316
|
+
<Text style={[styles.logTime]}>{log.time}</Text>
|
|
317
|
+
<Text style={[styles.logMessage, { color: getLogColor(log.type) }]}>
|
|
318
|
+
{log.message}
|
|
319
|
+
</Text>
|
|
320
|
+
</View>
|
|
321
|
+
))
|
|
322
|
+
)}
|
|
323
|
+
</ScrollView>
|
|
324
|
+
</View>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const styles = StyleSheet.create({
|
|
329
|
+
container: {
|
|
330
|
+
position: 'absolute',
|
|
331
|
+
bottom: 0,
|
|
332
|
+
left: 0,
|
|
333
|
+
right: 0,
|
|
334
|
+
backgroundColor: '#1a1a2e',
|
|
335
|
+
borderTopLeftRadius: 16,
|
|
336
|
+
borderTopRightRadius: 16,
|
|
337
|
+
maxHeight: '60%',
|
|
338
|
+
shadowColor: '#000',
|
|
339
|
+
shadowOffset: { width: 0, height: -2 },
|
|
340
|
+
shadowOpacity: 0.25,
|
|
341
|
+
shadowRadius: 4,
|
|
342
|
+
elevation: 5,
|
|
343
|
+
},
|
|
344
|
+
collapsedButton: {
|
|
345
|
+
position: 'absolute',
|
|
346
|
+
bottom: 20,
|
|
347
|
+
right: 20,
|
|
348
|
+
backgroundColor: '#6C63FF',
|
|
349
|
+
paddingHorizontal: 16,
|
|
350
|
+
paddingVertical: 10,
|
|
351
|
+
borderRadius: 20,
|
|
352
|
+
shadowColor: '#000',
|
|
353
|
+
shadowOffset: { width: 0, height: 2 },
|
|
354
|
+
shadowOpacity: 0.25,
|
|
355
|
+
shadowRadius: 4,
|
|
356
|
+
elevation: 5,
|
|
357
|
+
},
|
|
358
|
+
collapsedButtonText: {
|
|
359
|
+
color: '#fff',
|
|
360
|
+
fontWeight: 'bold',
|
|
361
|
+
fontSize: 12,
|
|
362
|
+
},
|
|
363
|
+
header: {
|
|
364
|
+
flexDirection: 'row',
|
|
365
|
+
justifyContent: 'space-between',
|
|
366
|
+
alignItems: 'center',
|
|
367
|
+
padding: 12,
|
|
368
|
+
borderBottomWidth: 1,
|
|
369
|
+
borderBottomColor: '#2a2a4e',
|
|
370
|
+
},
|
|
371
|
+
title: {
|
|
372
|
+
color: '#fff',
|
|
373
|
+
fontSize: 16,
|
|
374
|
+
fontWeight: 'bold',
|
|
375
|
+
},
|
|
376
|
+
headerButtons: {
|
|
377
|
+
flexDirection: 'row',
|
|
378
|
+
gap: 8,
|
|
379
|
+
},
|
|
380
|
+
headerButton: {
|
|
381
|
+
paddingHorizontal: 12,
|
|
382
|
+
paddingVertical: 4,
|
|
383
|
+
backgroundColor: '#2a2a4e',
|
|
384
|
+
borderRadius: 4,
|
|
385
|
+
},
|
|
386
|
+
headerButtonText: {
|
|
387
|
+
color: '#aaa',
|
|
388
|
+
fontSize: 12,
|
|
389
|
+
},
|
|
390
|
+
info: {
|
|
391
|
+
padding: 8,
|
|
392
|
+
backgroundColor: '#2a2a4e',
|
|
393
|
+
flexDirection: 'row',
|
|
394
|
+
justifyContent: 'space-around',
|
|
395
|
+
},
|
|
396
|
+
infoText: {
|
|
397
|
+
color: '#888',
|
|
398
|
+
fontSize: 11,
|
|
399
|
+
},
|
|
400
|
+
buttonContainer: {
|
|
401
|
+
padding: 8,
|
|
402
|
+
flexGrow: 0,
|
|
403
|
+
},
|
|
404
|
+
button: {
|
|
405
|
+
backgroundColor: '#6C63FF',
|
|
406
|
+
paddingHorizontal: 12,
|
|
407
|
+
paddingVertical: 8,
|
|
408
|
+
borderRadius: 6,
|
|
409
|
+
marginRight: 8,
|
|
410
|
+
},
|
|
411
|
+
dangerButton: {
|
|
412
|
+
backgroundColor: '#F44336',
|
|
413
|
+
},
|
|
414
|
+
buttonText: {
|
|
415
|
+
color: '#fff',
|
|
416
|
+
fontSize: 12,
|
|
417
|
+
fontWeight: '600',
|
|
418
|
+
},
|
|
419
|
+
logContainer: {
|
|
420
|
+
flex: 1,
|
|
421
|
+
padding: 8,
|
|
422
|
+
},
|
|
423
|
+
emptyLog: {
|
|
424
|
+
color: '#666',
|
|
425
|
+
textAlign: 'center',
|
|
426
|
+
marginTop: 20,
|
|
427
|
+
},
|
|
428
|
+
logEntry: {
|
|
429
|
+
flexDirection: 'row',
|
|
430
|
+
paddingVertical: 4,
|
|
431
|
+
borderBottomWidth: 1,
|
|
432
|
+
borderBottomColor: '#2a2a4e',
|
|
433
|
+
},
|
|
434
|
+
logTime: {
|
|
435
|
+
color: '#666',
|
|
436
|
+
fontSize: 10,
|
|
437
|
+
width: 70,
|
|
438
|
+
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
|
|
439
|
+
},
|
|
440
|
+
logMessage: {
|
|
441
|
+
flex: 1,
|
|
442
|
+
fontSize: 11,
|
|
443
|
+
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
export default OTADebugPanel;
|
|
@@ -62,6 +62,25 @@ async function getDeviceId(): Promise<string> {
|
|
|
62
62
|
return id;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// Timeout wrapper for async operations
|
|
66
|
+
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, operation: string): Promise<T> {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const timer = setTimeout(() => {
|
|
69
|
+
reject(new Error(`${operation} timed out after ${timeoutMs}ms`));
|
|
70
|
+
}, timeoutMs);
|
|
71
|
+
|
|
72
|
+
promise
|
|
73
|
+
.then((result) => {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
resolve(result);
|
|
76
|
+
})
|
|
77
|
+
.catch((error) => {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
reject(error);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
65
84
|
export function useOTAUpdate(config: OTAUpdateConfig): UseOTAUpdateResult {
|
|
66
85
|
const {
|
|
67
86
|
serverUrl,
|
|
@@ -178,7 +197,21 @@ export function useOTAUpdate(config: OTAUpdateConfig): UseOTAUpdateResult {
|
|
|
178
197
|
});
|
|
179
198
|
|
|
180
199
|
// Download bundle directly to file (bypasses JS memory - critical for large bundles)
|
|
181
|
-
|
|
200
|
+
// Use a 5 minute timeout for large bundles on slow networks
|
|
201
|
+
if (__DEV__) {
|
|
202
|
+
console.log(`[OTAUpdate] Starting download from: ${release.bundleUrl}`);
|
|
203
|
+
console.log(`[OTAUpdate] Expected size: ${release.bundleSize} bytes`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const downloadResult = await withTimeout(
|
|
207
|
+
storage.current.downloadBundleToFile(release.bundleUrl, release.id),
|
|
208
|
+
5 * 60 * 1000, // 5 minute timeout
|
|
209
|
+
'Bundle download'
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
if (__DEV__) {
|
|
213
|
+
console.log(`[OTAUpdate] Download complete: ${downloadResult.fileSize} bytes`);
|
|
214
|
+
}
|
|
182
215
|
|
|
183
216
|
setDownloadProgress({
|
|
184
217
|
downloadedBytes: downloadResult.fileSize,
|
|
@@ -189,10 +222,24 @@ export function useOTAUpdate(config: OTAUpdateConfig): UseOTAUpdateResult {
|
|
|
189
222
|
// Verify bundle hash
|
|
190
223
|
setStatus('verifying');
|
|
191
224
|
|
|
225
|
+
if (__DEV__) {
|
|
226
|
+
console.log('[OTAUpdate] Calculating bundle hash...');
|
|
227
|
+
}
|
|
228
|
+
|
|
192
229
|
// Calculate hash from file (streaming to avoid memory issues)
|
|
193
|
-
|
|
230
|
+
// Use a 2 minute timeout for hash calculation
|
|
231
|
+
const actualHash = await withTimeout(
|
|
232
|
+
storage.current.calculateBundleHash(release.id),
|
|
233
|
+
2 * 60 * 1000, // 2 minute timeout
|
|
234
|
+
'Hash calculation'
|
|
235
|
+
);
|
|
194
236
|
const expectedHashWithoutPrefix = release.bundleHash.replace(/^sha256:/, '');
|
|
195
237
|
|
|
238
|
+
if (__DEV__) {
|
|
239
|
+
console.log(`[OTAUpdate] Expected hash: ${expectedHashWithoutPrefix}`);
|
|
240
|
+
console.log(`[OTAUpdate] Actual hash: ${actualHash}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
196
243
|
if (actualHash !== expectedHashWithoutPrefix) {
|
|
197
244
|
// Delete corrupted bundle
|
|
198
245
|
await storage.current.deleteBundle(release.id);
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
export { OTAProvider, useOTA, withOTA, UpdateBanner } from './OTAProvider';
|
|
3
3
|
export type { OTAProviderProps, UpdateBannerProps } from './OTAProvider';
|
|
4
4
|
|
|
5
|
+
// Debug component (for development/testing)
|
|
6
|
+
export { OTADebugPanel } from './components/OTADebugPanel';
|
|
7
|
+
|
|
5
8
|
// Hook export
|
|
6
9
|
export { useOTAUpdate } from './hooks/useOTAUpdate';
|
|
7
10
|
export type {
|
|
@@ -33,4 +36,4 @@ export {
|
|
|
33
36
|
export type { VerificationResult } from './utils/verification';
|
|
34
37
|
|
|
35
38
|
// Version info
|
|
36
|
-
export const VERSION = '0.
|
|
39
|
+
export const VERSION = '0.2.2';
|
package/src/utils/storage.ts
CHANGED
|
@@ -130,15 +130,30 @@ class ExpoStorageAdapter implements StorageAdapter {
|
|
|
130
130
|
async downloadToFile(url: string, destPath: string): Promise<{ fileSize: number }> {
|
|
131
131
|
// Use Expo's downloadAsync which downloads directly to file
|
|
132
132
|
// This bypasses JS memory entirely - critical for large bundles
|
|
133
|
+
if (__DEV__) {
|
|
134
|
+
console.log('[OTAUpdate] Expo: Starting download from:', url);
|
|
135
|
+
console.log('[OTAUpdate] Expo: Destination:', destPath);
|
|
136
|
+
}
|
|
137
|
+
|
|
133
138
|
const result = await ExpoFileSystem.downloadAsync(url, destPath);
|
|
134
139
|
|
|
140
|
+
if (__DEV__) {
|
|
141
|
+
console.log('[OTAUpdate] Expo: Download status:', result.status);
|
|
142
|
+
}
|
|
143
|
+
|
|
135
144
|
if (result.status !== 200) {
|
|
136
145
|
throw new Error(`Download failed with status ${result.status}`);
|
|
137
146
|
}
|
|
138
147
|
|
|
139
148
|
// Get file size
|
|
140
149
|
const info = await ExpoFileSystem.getInfoAsync(destPath);
|
|
141
|
-
|
|
150
|
+
const fileSize = (info as any).size || 0;
|
|
151
|
+
|
|
152
|
+
if (__DEV__) {
|
|
153
|
+
console.log('[OTAUpdate] Expo: Downloaded file size:', fileSize, 'bytes');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { fileSize };
|
|
142
157
|
}
|
|
143
158
|
|
|
144
159
|
async calculateHashFromFile(path: string): Promise<string> {
|
|
@@ -151,11 +166,58 @@ class ExpoStorageAdapter implements StorageAdapter {
|
|
|
151
166
|
// expo-crypto not available
|
|
152
167
|
}
|
|
153
168
|
|
|
169
|
+
if (__DEV__) {
|
|
170
|
+
console.log('[OTAUpdate] Expo: Calculating hash for:', path);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Get file info first to log size
|
|
174
|
+
const fileInfo = await ExpoFileSystem.getInfoAsync(path);
|
|
175
|
+
if (__DEV__ && fileInfo.exists) {
|
|
176
|
+
console.log('[OTAUpdate] File size:', (fileInfo as any).size, 'bytes');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (ExpoCrypto?.digestStringAsync) {
|
|
180
|
+
// Use digestStringAsync which is more efficient for large files
|
|
181
|
+
// Read file as base64
|
|
182
|
+
const base64 = await ExpoFileSystem.readAsStringAsync(path, {
|
|
183
|
+
encoding: ExpoFileSystem.EncodingType.Base64,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (__DEV__) {
|
|
187
|
+
console.log('[OTAUpdate] File read as base64, length:', base64.length);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Use digestStringAsync with base64 encoding
|
|
191
|
+
const hash = await ExpoCrypto.digestStringAsync(
|
|
192
|
+
ExpoCrypto.CryptoDigestAlgorithm.SHA256,
|
|
193
|
+
base64,
|
|
194
|
+
{ encoding: ExpoCrypto.CryptoEncoding.BASE64 }
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// digestStringAsync with BASE64 encoding returns base64-encoded hash
|
|
198
|
+
// We need to convert it to hex
|
|
199
|
+
const hashBytes = Uint8Array.from(atob(hash), c => c.charCodeAt(0));
|
|
200
|
+
const hexHash = Array.from(hashBytes)
|
|
201
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
202
|
+
.join('');
|
|
203
|
+
|
|
204
|
+
if (__DEV__) {
|
|
205
|
+
console.log('[OTAUpdate] Hash calculated:', hexHash);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return hexHash;
|
|
209
|
+
}
|
|
210
|
+
|
|
154
211
|
if (ExpoCrypto?.digest) {
|
|
155
212
|
// Read file as base64 and convert to Uint8Array
|
|
156
213
|
const base64 = await ExpoFileSystem.readAsStringAsync(path, {
|
|
157
214
|
encoding: ExpoFileSystem.EncodingType.Base64,
|
|
158
215
|
});
|
|
216
|
+
|
|
217
|
+
if (__DEV__) {
|
|
218
|
+
console.log('[OTAUpdate] File read as base64, length:', base64.length);
|
|
219
|
+
}
|
|
220
|
+
|
|
159
221
|
const binary = atob(base64);
|
|
160
222
|
const bytes = new Uint8Array(binary.length);
|
|
161
223
|
for (let i = 0; i < binary.length; i++) {
|
|
@@ -167,9 +229,15 @@ class ExpoStorageAdapter implements StorageAdapter {
|
|
|
167
229
|
);
|
|
168
230
|
// Convert ArrayBuffer to hex
|
|
169
231
|
const hashBytes = new Uint8Array(hashBuffer);
|
|
170
|
-
|
|
232
|
+
const hexHash = Array.from(hashBytes)
|
|
171
233
|
.map(b => b.toString(16).padStart(2, '0'))
|
|
172
234
|
.join('');
|
|
235
|
+
|
|
236
|
+
if (__DEV__) {
|
|
237
|
+
console.log('[OTAUpdate] Hash calculated:', hexHash);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return hexHash;
|
|
173
241
|
}
|
|
174
242
|
|
|
175
243
|
// Fallback to SubtleCrypto if available
|
|
@@ -264,12 +332,27 @@ class NativeStorageAdapter implements StorageAdapter {
|
|
|
264
332
|
|
|
265
333
|
// Use native module's downloadFile method if available (preferred)
|
|
266
334
|
if (OTAUpdateNative.downloadFile) {
|
|
335
|
+
if (__DEV__) {
|
|
336
|
+
console.log('[OTAUpdate] Native: Starting download from:', url);
|
|
337
|
+
console.log('[OTAUpdate] Native: Destination:', destPath);
|
|
338
|
+
}
|
|
339
|
+
|
|
267
340
|
const result = await OTAUpdateNative.downloadFile(url, destPath);
|
|
268
|
-
|
|
341
|
+
const fileSize = result.fileSize || 0;
|
|
342
|
+
|
|
343
|
+
if (__DEV__) {
|
|
344
|
+
console.log('[OTAUpdate] Native: Downloaded file size:', fileSize, 'bytes');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return { fileSize };
|
|
269
348
|
}
|
|
270
349
|
|
|
271
350
|
// Fallback: download via fetch and write in chunks
|
|
272
351
|
// This is less efficient but works without native download support
|
|
352
|
+
if (__DEV__) {
|
|
353
|
+
console.log('[OTAUpdate] Native: Using fetch fallback for download');
|
|
354
|
+
}
|
|
355
|
+
|
|
273
356
|
const response = await fetch(url);
|
|
274
357
|
if (!response.ok) {
|
|
275
358
|
throw new Error(`Download failed with status ${response.status}`);
|
|
@@ -279,6 +362,10 @@ class NativeStorageAdapter implements StorageAdapter {
|
|
|
279
362
|
const base64 = arrayBufferToBase64(data);
|
|
280
363
|
await OTAUpdateNative.writeFileBase64(destPath, base64);
|
|
281
364
|
|
|
365
|
+
if (__DEV__) {
|
|
366
|
+
console.log('[OTAUpdate] Native: Fallback download complete, size:', data.byteLength);
|
|
367
|
+
}
|
|
368
|
+
|
|
282
369
|
return { fileSize: data.byteLength };
|
|
283
370
|
}
|
|
284
371
|
|
|
@@ -289,12 +376,26 @@ class NativeStorageAdapter implements StorageAdapter {
|
|
|
289
376
|
|
|
290
377
|
// Use native module's calculateSHA256FromFile if available (preferred - streams file)
|
|
291
378
|
if (OTAUpdateNative.calculateSHA256FromFile) {
|
|
292
|
-
|
|
379
|
+
if (__DEV__) {
|
|
380
|
+
console.log('[OTAUpdate] Native: Calculating hash for:', path);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const hash = await OTAUpdateNative.calculateSHA256FromFile(path);
|
|
384
|
+
|
|
385
|
+
if (__DEV__) {
|
|
386
|
+
console.log('[OTAUpdate] Native: Hash calculated:', hash);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return hash;
|
|
293
390
|
}
|
|
294
391
|
|
|
295
392
|
// Fallback: read file as base64 and use the base64 hash method
|
|
296
393
|
// This loads the file into memory, but is better than nothing
|
|
297
394
|
if (OTAUpdateNative.calculateSHA256 && OTAUpdateNative.readFileBase64) {
|
|
395
|
+
if (__DEV__) {
|
|
396
|
+
console.log('[OTAUpdate] Native: Using fallback hash calculation (loads file into memory)');
|
|
397
|
+
}
|
|
398
|
+
|
|
298
399
|
const base64 = await OTAUpdateNative.readFileBase64(path);
|
|
299
400
|
return OTAUpdateNative.calculateSHA256(base64);
|
|
300
401
|
}
|