@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.
Files changed (37) hide show
  1. package/android/src/main/AndroidManifest.xml +2 -0
  2. package/android/src/main/java/com/otaupdate/OTAUpdateModule.kt +116 -25
  3. package/app.plugin.js +53 -12
  4. package/ios/OTAUpdate.swift +29 -0
  5. package/lib/commonjs/OTAProvider.js +37 -0
  6. package/lib/commonjs/OTAProvider.js.map +1 -1
  7. package/lib/commonjs/components/OTADebugPanel.js +426 -0
  8. package/lib/commonjs/components/OTADebugPanel.js.map +1 -0
  9. package/lib/commonjs/hooks/useOTAUpdate.js +38 -2
  10. package/lib/commonjs/hooks/useOTAUpdate.js.map +1 -1
  11. package/lib/commonjs/index.js +10 -1
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/utils/storage.js +64 -5
  14. package/lib/commonjs/utils/storage.js.map +1 -1
  15. package/lib/module/OTAProvider.js +38 -1
  16. package/lib/module/OTAProvider.js.map +1 -1
  17. package/lib/module/components/OTADebugPanel.js +418 -0
  18. package/lib/module/components/OTADebugPanel.js.map +1 -0
  19. package/lib/module/hooks/useOTAUpdate.js +38 -2
  20. package/lib/module/hooks/useOTAUpdate.js.map +1 -1
  21. package/lib/module/index.js +4 -1
  22. package/lib/module/index.js.map +1 -1
  23. package/lib/module/utils/storage.js +64 -5
  24. package/lib/module/utils/storage.js.map +1 -1
  25. package/lib/typescript/OTAProvider.d.ts.map +1 -1
  26. package/lib/typescript/components/OTADebugPanel.d.ts +18 -0
  27. package/lib/typescript/components/OTADebugPanel.d.ts.map +1 -0
  28. package/lib/typescript/hooks/useOTAUpdate.d.ts.map +1 -1
  29. package/lib/typescript/index.d.ts +2 -1
  30. package/lib/typescript/index.d.ts.map +1 -1
  31. package/lib/typescript/utils/storage.d.ts.map +1 -1
  32. package/package.json +1 -1
  33. package/src/OTAProvider.tsx +40 -0
  34. package/src/components/OTADebugPanel.tsx +447 -0
  35. package/src/hooks/useOTAUpdate.ts +49 -2
  36. package/src/index.ts +4 -1
  37. 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
- const downloadResult = await storage.current.downloadBundleToFile(release.bundleUrl, release.id);
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
- const actualHash = await storage.current.calculateBundleHash(release.id);
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.1.8';
39
+ export const VERSION = '0.2.3';
@@ -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
- return { fileSize: info.size || 0 };
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
- return Array.from(hashBytes)
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
- return { fileSize: result.fileSize || 0 };
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
- return OTAUpdateNative.calculateSHA256FromFile(path);
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
  }