@vanikya/ota-react-native 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +223 -0
- package/android/build.gradle +58 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/com/otaupdate/OTAUpdateModule.kt +185 -0
- package/android/src/main/java/com/otaupdate/OTAUpdatePackage.kt +16 -0
- package/ios/OTAUpdate.m +61 -0
- package/ios/OTAUpdate.swift +194 -0
- package/lib/commonjs/OTAProvider.js +113 -0
- package/lib/commonjs/OTAProvider.js.map +1 -0
- package/lib/commonjs/hooks/useOTAUpdate.js +272 -0
- package/lib/commonjs/hooks/useOTAUpdate.js.map +1 -0
- package/lib/commonjs/index.js +98 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/utils/api.js +60 -0
- package/lib/commonjs/utils/api.js.map +1 -0
- package/lib/commonjs/utils/storage.js +209 -0
- package/lib/commonjs/utils/storage.js.map +1 -0
- package/lib/commonjs/utils/verification.js +145 -0
- package/lib/commonjs/utils/verification.js.map +1 -0
- package/lib/module/OTAProvider.js +104 -0
- package/lib/module/OTAProvider.js.map +1 -0
- package/lib/module/hooks/useOTAUpdate.js +266 -0
- package/lib/module/hooks/useOTAUpdate.js.map +1 -0
- package/lib/module/index.js +11 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/utils/api.js +52 -0
- package/lib/module/utils/api.js.map +1 -0
- package/lib/module/utils/storage.js +202 -0
- package/lib/module/utils/storage.js.map +1 -0
- package/lib/module/utils/verification.js +137 -0
- package/lib/module/utils/verification.js.map +1 -0
- package/lib/typescript/OTAProvider.d.ts +28 -0
- package/lib/typescript/OTAProvider.d.ts.map +1 -0
- package/lib/typescript/hooks/useOTAUpdate.d.ts +35 -0
- package/lib/typescript/hooks/useOTAUpdate.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +12 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/utils/api.d.ts +47 -0
- package/lib/typescript/utils/api.d.ts.map +1 -0
- package/lib/typescript/utils/storage.d.ts +32 -0
- package/lib/typescript/utils/storage.d.ts.map +1 -0
- package/lib/typescript/utils/verification.d.ts +11 -0
- package/lib/typescript/utils/verification.d.ts.map +1 -0
- package/ota-update.podspec +21 -0
- package/package.json +83 -0
- package/src/OTAProvider.tsx +160 -0
- package/src/hooks/useOTAUpdate.ts +344 -0
- package/src/index.ts +36 -0
- package/src/utils/api.ts +99 -0
- package/src/utils/storage.ts +249 -0
- package/src/utils/verification.ts +167 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { AppState, AppStateStatus, NativeModules, Platform } from 'react-native';
|
|
3
|
+
import { OTAApiClient, ReleaseInfo, getDeviceInfo } from '../utils/api';
|
|
4
|
+
import { UpdateStorage, StoredUpdate } from '../utils/storage';
|
|
5
|
+
import { verifyBundle, VerificationResult } from '../utils/verification';
|
|
6
|
+
|
|
7
|
+
const OTAUpdateNative = NativeModules.OTAUpdate;
|
|
8
|
+
|
|
9
|
+
export interface OTAUpdateConfig {
|
|
10
|
+
serverUrl: string;
|
|
11
|
+
appSlug: string;
|
|
12
|
+
channel?: string;
|
|
13
|
+
appVersion: string;
|
|
14
|
+
publicKey?: string;
|
|
15
|
+
checkOnMount?: boolean;
|
|
16
|
+
checkOnForeground?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UpdateInfo {
|
|
20
|
+
version: string;
|
|
21
|
+
releaseId: string;
|
|
22
|
+
bundleSize: number;
|
|
23
|
+
isMandatory: boolean;
|
|
24
|
+
releaseNotes: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DownloadProgress {
|
|
28
|
+
downloadedBytes: number;
|
|
29
|
+
totalBytes: number;
|
|
30
|
+
percentage: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type UpdateStatus =
|
|
34
|
+
| 'idle'
|
|
35
|
+
| 'checking'
|
|
36
|
+
| 'available'
|
|
37
|
+
| 'downloading'
|
|
38
|
+
| 'verifying'
|
|
39
|
+
| 'ready'
|
|
40
|
+
| 'applying'
|
|
41
|
+
| 'error'
|
|
42
|
+
| 'up-to-date';
|
|
43
|
+
|
|
44
|
+
export interface UseOTAUpdateResult {
|
|
45
|
+
status: UpdateStatus;
|
|
46
|
+
updateInfo: UpdateInfo | null;
|
|
47
|
+
downloadProgress: DownloadProgress | null;
|
|
48
|
+
error: Error | null;
|
|
49
|
+
currentVersion: string | null;
|
|
50
|
+
checkForUpdate: () => Promise<UpdateInfo | null>;
|
|
51
|
+
downloadUpdate: () => Promise<void>;
|
|
52
|
+
applyUpdate: (restartApp?: boolean) => Promise<void>;
|
|
53
|
+
clearPendingUpdate: () => Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Generate or get device ID
|
|
57
|
+
async function getDeviceId(): Promise<string> {
|
|
58
|
+
// Try to get from native module or AsyncStorage
|
|
59
|
+
// For simplicity, we generate a random one and ideally persist it
|
|
60
|
+
// In production, use a proper device ID solution
|
|
61
|
+
const id = `device_${Math.random().toString(36).substring(2, 15)}`;
|
|
62
|
+
return id;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function useOTAUpdate(config: OTAUpdateConfig): UseOTAUpdateResult {
|
|
66
|
+
const {
|
|
67
|
+
serverUrl,
|
|
68
|
+
appSlug,
|
|
69
|
+
channel = 'production',
|
|
70
|
+
appVersion,
|
|
71
|
+
publicKey,
|
|
72
|
+
checkOnMount = true,
|
|
73
|
+
checkOnForeground = true,
|
|
74
|
+
} = config;
|
|
75
|
+
|
|
76
|
+
const [status, setStatus] = useState<UpdateStatus>('idle');
|
|
77
|
+
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
|
|
78
|
+
const [downloadProgress, setDownloadProgress] = useState<DownloadProgress | null>(null);
|
|
79
|
+
const [error, setError] = useState<Error | null>(null);
|
|
80
|
+
const [currentVersion, setCurrentVersion] = useState<string | null>(null);
|
|
81
|
+
|
|
82
|
+
const apiClient = useRef(new OTAApiClient(serverUrl));
|
|
83
|
+
const storage = useRef(new UpdateStorage());
|
|
84
|
+
const deviceIdRef = useRef<string | null>(null);
|
|
85
|
+
const releaseRef = useRef<ReleaseInfo | null>(null);
|
|
86
|
+
|
|
87
|
+
// Load current version on mount
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const loadCurrentVersion = async () => {
|
|
90
|
+
const metadata = await storage.current.getMetadata();
|
|
91
|
+
if (metadata) {
|
|
92
|
+
setCurrentVersion(metadata.version);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
loadCurrentVersion();
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
// Get device ID
|
|
99
|
+
const getDeviceIdCached = useCallback(async () => {
|
|
100
|
+
if (!deviceIdRef.current) {
|
|
101
|
+
deviceIdRef.current = await getDeviceId();
|
|
102
|
+
}
|
|
103
|
+
return deviceIdRef.current;
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
// Check for updates
|
|
107
|
+
const checkForUpdate = useCallback(async (): Promise<UpdateInfo | null> => {
|
|
108
|
+
try {
|
|
109
|
+
setStatus('checking');
|
|
110
|
+
setError(null);
|
|
111
|
+
|
|
112
|
+
const deviceId = await getDeviceIdCached();
|
|
113
|
+
const currentMetadata = await storage.current.getMetadata();
|
|
114
|
+
|
|
115
|
+
const response = await apiClient.current.checkUpdate({
|
|
116
|
+
appSlug,
|
|
117
|
+
channel,
|
|
118
|
+
platform: Platform.OS as 'ios' | 'android',
|
|
119
|
+
currentVersion: currentMetadata?.version || null,
|
|
120
|
+
appVersion,
|
|
121
|
+
deviceId,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (!response.updateAvailable || !response.release) {
|
|
125
|
+
setStatus('up-to-date');
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
releaseRef.current = response.release;
|
|
130
|
+
|
|
131
|
+
const info: UpdateInfo = {
|
|
132
|
+
version: response.release.version,
|
|
133
|
+
releaseId: response.release.id,
|
|
134
|
+
bundleSize: response.release.bundleSize,
|
|
135
|
+
isMandatory: response.release.isMandatory,
|
|
136
|
+
releaseNotes: response.release.releaseNotes,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
setUpdateInfo(info);
|
|
140
|
+
setStatus('available');
|
|
141
|
+
|
|
142
|
+
return info;
|
|
143
|
+
} catch (err) {
|
|
144
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
145
|
+
setError(error);
|
|
146
|
+
setStatus('error');
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}, [appSlug, channel, appVersion, getDeviceIdCached]);
|
|
150
|
+
|
|
151
|
+
// Download update
|
|
152
|
+
const downloadUpdate = useCallback(async (): Promise<void> => {
|
|
153
|
+
if (!releaseRef.current) {
|
|
154
|
+
throw new Error('No update available to download');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const release = releaseRef.current;
|
|
158
|
+
const deviceId = await getDeviceIdCached();
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
setStatus('downloading');
|
|
162
|
+
setDownloadProgress({ downloadedBytes: 0, totalBytes: release.bundleSize, percentage: 0 });
|
|
163
|
+
|
|
164
|
+
// Report download start
|
|
165
|
+
apiClient.current.reportEvent({
|
|
166
|
+
appSlug,
|
|
167
|
+
releaseId: release.id,
|
|
168
|
+
deviceId,
|
|
169
|
+
eventType: 'download',
|
|
170
|
+
appVersion,
|
|
171
|
+
deviceInfo: getDeviceInfo(),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Download bundle
|
|
175
|
+
const bundleData = await apiClient.current.downloadBundle(release.bundleUrl);
|
|
176
|
+
|
|
177
|
+
setDownloadProgress({
|
|
178
|
+
downloadedBytes: bundleData.byteLength,
|
|
179
|
+
totalBytes: release.bundleSize,
|
|
180
|
+
percentage: 100,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Verify bundle
|
|
184
|
+
setStatus('verifying');
|
|
185
|
+
|
|
186
|
+
const verification = await verifyBundle(
|
|
187
|
+
bundleData,
|
|
188
|
+
release.bundleHash,
|
|
189
|
+
release.bundleSignature,
|
|
190
|
+
publicKey || null
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
if (!verification.valid) {
|
|
194
|
+
throw new Error(verification.error || 'Bundle verification failed');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Save bundle
|
|
198
|
+
const bundlePath = await storage.current.saveBundle(release.id, bundleData);
|
|
199
|
+
|
|
200
|
+
// Save metadata
|
|
201
|
+
await storage.current.saveMetadata({
|
|
202
|
+
releaseId: release.id,
|
|
203
|
+
version: release.version,
|
|
204
|
+
bundlePath,
|
|
205
|
+
bundleHash: release.bundleHash,
|
|
206
|
+
downloadedAt: Date.now(),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
setStatus('ready');
|
|
210
|
+
} catch (err) {
|
|
211
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
212
|
+
|
|
213
|
+
// Report failure
|
|
214
|
+
apiClient.current.reportEvent({
|
|
215
|
+
appSlug,
|
|
216
|
+
releaseId: release.id,
|
|
217
|
+
deviceId,
|
|
218
|
+
eventType: 'failure',
|
|
219
|
+
errorMessage: error.message,
|
|
220
|
+
appVersion,
|
|
221
|
+
deviceInfo: getDeviceInfo(),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
setError(error);
|
|
225
|
+
setStatus('error');
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
}, [appSlug, appVersion, publicKey, getDeviceIdCached]);
|
|
229
|
+
|
|
230
|
+
// Apply update
|
|
231
|
+
const applyUpdate = useCallback(async (restartApp: boolean = true): Promise<void> => {
|
|
232
|
+
const metadata = await storage.current.getMetadata();
|
|
233
|
+
|
|
234
|
+
if (!metadata) {
|
|
235
|
+
throw new Error('No update available to apply');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const deviceId = await getDeviceIdCached();
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
setStatus('applying');
|
|
242
|
+
|
|
243
|
+
// Report apply
|
|
244
|
+
apiClient.current.reportEvent({
|
|
245
|
+
appSlug,
|
|
246
|
+
releaseId: metadata.releaseId,
|
|
247
|
+
deviceId,
|
|
248
|
+
eventType: 'apply',
|
|
249
|
+
appVersion,
|
|
250
|
+
deviceInfo: getDeviceInfo(),
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Apply bundle using native module
|
|
254
|
+
if (OTAUpdateNative?.applyBundle) {
|
|
255
|
+
await OTAUpdateNative.applyBundle(metadata.bundlePath, restartApp);
|
|
256
|
+
} else if (restartApp) {
|
|
257
|
+
// If no native module, we need to restart manually
|
|
258
|
+
// The bundle will be loaded on next app start
|
|
259
|
+
if (__DEV__) {
|
|
260
|
+
console.log('[OTAUpdate] Update ready. Restart the app to apply.');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Report success (this might not run if app restarts)
|
|
265
|
+
apiClient.current.reportEvent({
|
|
266
|
+
appSlug,
|
|
267
|
+
releaseId: metadata.releaseId,
|
|
268
|
+
deviceId,
|
|
269
|
+
eventType: 'success',
|
|
270
|
+
appVersion,
|
|
271
|
+
deviceInfo: getDeviceInfo(),
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
setCurrentVersion(metadata.version);
|
|
275
|
+
setStatus('idle');
|
|
276
|
+
} catch (err) {
|
|
277
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
278
|
+
|
|
279
|
+
apiClient.current.reportEvent({
|
|
280
|
+
appSlug,
|
|
281
|
+
releaseId: metadata.releaseId,
|
|
282
|
+
deviceId,
|
|
283
|
+
eventType: 'failure',
|
|
284
|
+
errorMessage: error.message,
|
|
285
|
+
appVersion,
|
|
286
|
+
deviceInfo: getDeviceInfo(),
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
setError(error);
|
|
290
|
+
setStatus('error');
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}, [appSlug, appVersion, getDeviceIdCached]);
|
|
294
|
+
|
|
295
|
+
// Clear pending update
|
|
296
|
+
const clearPendingUpdate = useCallback(async (): Promise<void> => {
|
|
297
|
+
const metadata = await storage.current.getMetadata();
|
|
298
|
+
|
|
299
|
+
if (metadata) {
|
|
300
|
+
await storage.current.deleteBundle(metadata.releaseId);
|
|
301
|
+
await storage.current.clearMetadata();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
setUpdateInfo(null);
|
|
305
|
+
releaseRef.current = null;
|
|
306
|
+
setStatus('idle');
|
|
307
|
+
}, []);
|
|
308
|
+
|
|
309
|
+
// Check on mount
|
|
310
|
+
useEffect(() => {
|
|
311
|
+
if (checkOnMount) {
|
|
312
|
+
checkForUpdate();
|
|
313
|
+
}
|
|
314
|
+
}, [checkOnMount, checkForUpdate]);
|
|
315
|
+
|
|
316
|
+
// Check on foreground
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
if (!checkOnForeground) return;
|
|
319
|
+
|
|
320
|
+
const handleAppStateChange = (nextAppState: AppStateStatus) => {
|
|
321
|
+
if (nextAppState === 'active' && status === 'idle') {
|
|
322
|
+
checkForUpdate();
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const subscription = AppState.addEventListener('change', handleAppStateChange);
|
|
327
|
+
|
|
328
|
+
return () => {
|
|
329
|
+
subscription.remove();
|
|
330
|
+
};
|
|
331
|
+
}, [checkOnForeground, checkForUpdate, status]);
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
status,
|
|
335
|
+
updateInfo,
|
|
336
|
+
downloadProgress,
|
|
337
|
+
error,
|
|
338
|
+
currentVersion,
|
|
339
|
+
checkForUpdate,
|
|
340
|
+
downloadUpdate,
|
|
341
|
+
applyUpdate,
|
|
342
|
+
clearPendingUpdate,
|
|
343
|
+
};
|
|
344
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Main exports
|
|
2
|
+
export { OTAProvider, useOTA, withOTA, UpdateBanner } from './OTAProvider';
|
|
3
|
+
export type { OTAProviderProps, UpdateBannerProps } from './OTAProvider';
|
|
4
|
+
|
|
5
|
+
// Hook export
|
|
6
|
+
export { useOTAUpdate } from './hooks/useOTAUpdate';
|
|
7
|
+
export type {
|
|
8
|
+
OTAUpdateConfig,
|
|
9
|
+
UpdateInfo,
|
|
10
|
+
DownloadProgress,
|
|
11
|
+
UpdateStatus,
|
|
12
|
+
UseOTAUpdateResult,
|
|
13
|
+
} from './hooks/useOTAUpdate';
|
|
14
|
+
|
|
15
|
+
// Utilities
|
|
16
|
+
export { OTAApiClient, getDeviceInfo } from './utils/api';
|
|
17
|
+
export type {
|
|
18
|
+
CheckUpdateRequest,
|
|
19
|
+
CheckUpdateResponse,
|
|
20
|
+
ReleaseInfo,
|
|
21
|
+
ReportEventRequest,
|
|
22
|
+
} from './utils/api';
|
|
23
|
+
|
|
24
|
+
export { UpdateStorage, getStorageAdapter } from './utils/storage';
|
|
25
|
+
export type { StoredUpdate, StorageAdapter } from './utils/storage';
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
calculateHash,
|
|
29
|
+
verifyBundleHash,
|
|
30
|
+
verifySignature,
|
|
31
|
+
verifyBundle,
|
|
32
|
+
} from './utils/verification';
|
|
33
|
+
export type { VerificationResult } from './utils/verification';
|
|
34
|
+
|
|
35
|
+
// Version info
|
|
36
|
+
export const VERSION = '0.1.0';
|
package/src/utils/api.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export interface CheckUpdateRequest {
|
|
4
|
+
appSlug: string;
|
|
5
|
+
channel: string;
|
|
6
|
+
platform: 'ios' | 'android';
|
|
7
|
+
currentVersion: string | null;
|
|
8
|
+
appVersion: string;
|
|
9
|
+
deviceId: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ReleaseInfo {
|
|
13
|
+
id: string;
|
|
14
|
+
version: string;
|
|
15
|
+
bundleUrl: string;
|
|
16
|
+
bundleHash: string;
|
|
17
|
+
bundleSignature: string | null;
|
|
18
|
+
bundleSize: number;
|
|
19
|
+
isMandatory: boolean;
|
|
20
|
+
releaseNotes: string | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CheckUpdateResponse {
|
|
24
|
+
updateAvailable: boolean;
|
|
25
|
+
release?: ReleaseInfo;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ReportEventRequest {
|
|
29
|
+
appSlug: string;
|
|
30
|
+
releaseId: string | null;
|
|
31
|
+
deviceId: string;
|
|
32
|
+
eventType: 'download' | 'apply' | 'success' | 'failure' | 'rollback';
|
|
33
|
+
errorMessage?: string;
|
|
34
|
+
appVersion?: string;
|
|
35
|
+
deviceInfo?: {
|
|
36
|
+
os: string;
|
|
37
|
+
osVersion: string;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class OTAApiClient {
|
|
43
|
+
private serverUrl: string;
|
|
44
|
+
|
|
45
|
+
constructor(serverUrl: string) {
|
|
46
|
+
this.serverUrl = serverUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async checkUpdate(request: CheckUpdateRequest): Promise<CheckUpdateResponse> {
|
|
50
|
+
const response = await fetch(`${this.serverUrl}/api/v1/check-update`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify(request),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
60
|
+
throw new Error((error as { error: string }).error || `HTTP ${response.status}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return response.json();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async reportEvent(request: ReportEventRequest): Promise<void> {
|
|
67
|
+
try {
|
|
68
|
+
await fetch(`${this.serverUrl}/api/v1/report-event`, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify(request),
|
|
74
|
+
});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
// Silently fail analytics - don't block the app
|
|
77
|
+
if (__DEV__) {
|
|
78
|
+
console.warn('[OTAUpdate] Failed to report event:', error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async downloadBundle(bundleUrl: string): Promise<ArrayBuffer> {
|
|
84
|
+
const response = await fetch(bundleUrl);
|
|
85
|
+
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
throw new Error(`Failed to download bundle: HTTP ${response.status}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return response.arrayBuffer();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getDeviceInfo(): { os: string; osVersion: string } {
|
|
95
|
+
return {
|
|
96
|
+
os: Platform.OS,
|
|
97
|
+
osVersion: Platform.Version.toString(),
|
|
98
|
+
};
|
|
99
|
+
}
|