@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.
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 +79 -4
  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 +79 -4
  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 +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
- 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.2';
@@ -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,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
- return Array.from(hashBytes)
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
- return { fileSize: result.fileSize || 0 };
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
- return OTAUpdateNative.calculateSHA256FromFile(path);
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
  }