@vanikya/ota-react-native 0.2.0 → 0.2.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.
- 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 +64 -5
- 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 +64 -5
- 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 +82 -5
|
@@ -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.3';
|
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,25 +166,54 @@ 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
|
+
|
|
154
179
|
if (ExpoCrypto?.digest) {
|
|
155
|
-
// Read file as base64 and convert to Uint8Array
|
|
180
|
+
// Read file as base64 and convert to Uint8Array for hashing
|
|
156
181
|
const base64 = await ExpoFileSystem.readAsStringAsync(path, {
|
|
157
182
|
encoding: ExpoFileSystem.EncodingType.Base64,
|
|
158
183
|
});
|
|
184
|
+
|
|
185
|
+
if (__DEV__) {
|
|
186
|
+
console.log('[OTAUpdate] File read as base64, length:', base64.length);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Decode base64 to binary - this is what the server hashes
|
|
159
190
|
const binary = atob(base64);
|
|
160
191
|
const bytes = new Uint8Array(binary.length);
|
|
161
192
|
for (let i = 0; i < binary.length; i++) {
|
|
162
193
|
bytes[i] = binary.charCodeAt(i);
|
|
163
194
|
}
|
|
195
|
+
|
|
196
|
+
if (__DEV__) {
|
|
197
|
+
console.log('[OTAUpdate] Decoded to binary, length:', bytes.length);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Hash the binary data
|
|
164
201
|
const hashBuffer = await ExpoCrypto.digest(
|
|
165
202
|
ExpoCrypto.CryptoDigestAlgorithm.SHA256,
|
|
166
203
|
bytes
|
|
167
204
|
);
|
|
205
|
+
|
|
168
206
|
// Convert ArrayBuffer to hex
|
|
169
207
|
const hashBytes = new Uint8Array(hashBuffer);
|
|
170
|
-
|
|
208
|
+
const hexHash = Array.from(hashBytes)
|
|
171
209
|
.map(b => b.toString(16).padStart(2, '0'))
|
|
172
210
|
.join('');
|
|
211
|
+
|
|
212
|
+
if (__DEV__) {
|
|
213
|
+
console.log('[OTAUpdate] Hash calculated:', hexHash);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return hexHash;
|
|
173
217
|
}
|
|
174
218
|
|
|
175
219
|
// Fallback to SubtleCrypto if available
|
|
@@ -264,12 +308,27 @@ class NativeStorageAdapter implements StorageAdapter {
|
|
|
264
308
|
|
|
265
309
|
// Use native module's downloadFile method if available (preferred)
|
|
266
310
|
if (OTAUpdateNative.downloadFile) {
|
|
311
|
+
if (__DEV__) {
|
|
312
|
+
console.log('[OTAUpdate] Native: Starting download from:', url);
|
|
313
|
+
console.log('[OTAUpdate] Native: Destination:', destPath);
|
|
314
|
+
}
|
|
315
|
+
|
|
267
316
|
const result = await OTAUpdateNative.downloadFile(url, destPath);
|
|
268
|
-
|
|
317
|
+
const fileSize = result.fileSize || 0;
|
|
318
|
+
|
|
319
|
+
if (__DEV__) {
|
|
320
|
+
console.log('[OTAUpdate] Native: Downloaded file size:', fileSize, 'bytes');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return { fileSize };
|
|
269
324
|
}
|
|
270
325
|
|
|
271
326
|
// Fallback: download via fetch and write in chunks
|
|
272
327
|
// This is less efficient but works without native download support
|
|
328
|
+
if (__DEV__) {
|
|
329
|
+
console.log('[OTAUpdate] Native: Using fetch fallback for download');
|
|
330
|
+
}
|
|
331
|
+
|
|
273
332
|
const response = await fetch(url);
|
|
274
333
|
if (!response.ok) {
|
|
275
334
|
throw new Error(`Download failed with status ${response.status}`);
|
|
@@ -279,6 +338,10 @@ class NativeStorageAdapter implements StorageAdapter {
|
|
|
279
338
|
const base64 = arrayBufferToBase64(data);
|
|
280
339
|
await OTAUpdateNative.writeFileBase64(destPath, base64);
|
|
281
340
|
|
|
341
|
+
if (__DEV__) {
|
|
342
|
+
console.log('[OTAUpdate] Native: Fallback download complete, size:', data.byteLength);
|
|
343
|
+
}
|
|
344
|
+
|
|
282
345
|
return { fileSize: data.byteLength };
|
|
283
346
|
}
|
|
284
347
|
|
|
@@ -289,12 +352,26 @@ class NativeStorageAdapter implements StorageAdapter {
|
|
|
289
352
|
|
|
290
353
|
// Use native module's calculateSHA256FromFile if available (preferred - streams file)
|
|
291
354
|
if (OTAUpdateNative.calculateSHA256FromFile) {
|
|
292
|
-
|
|
355
|
+
if (__DEV__) {
|
|
356
|
+
console.log('[OTAUpdate] Native: Calculating hash for:', path);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const hash = await OTAUpdateNative.calculateSHA256FromFile(path);
|
|
360
|
+
|
|
361
|
+
if (__DEV__) {
|
|
362
|
+
console.log('[OTAUpdate] Native: Hash calculated:', hash);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return hash;
|
|
293
366
|
}
|
|
294
367
|
|
|
295
368
|
// Fallback: read file as base64 and use the base64 hash method
|
|
296
369
|
// This loads the file into memory, but is better than nothing
|
|
297
370
|
if (OTAUpdateNative.calculateSHA256 && OTAUpdateNative.readFileBase64) {
|
|
371
|
+
if (__DEV__) {
|
|
372
|
+
console.log('[OTAUpdate] Native: Using fallback hash calculation (loads file into memory)');
|
|
373
|
+
}
|
|
374
|
+
|
|
298
375
|
const base64 = await OTAUpdateNative.readFileBase64(path);
|
|
299
376
|
return OTAUpdateNative.calculateSHA256(base64);
|
|
300
377
|
}
|