@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.
Files changed (51) hide show
  1. package/README.md +223 -0
  2. package/android/build.gradle +58 -0
  3. package/android/src/main/AndroidManifest.xml +4 -0
  4. package/android/src/main/java/com/otaupdate/OTAUpdateModule.kt +185 -0
  5. package/android/src/main/java/com/otaupdate/OTAUpdatePackage.kt +16 -0
  6. package/ios/OTAUpdate.m +61 -0
  7. package/ios/OTAUpdate.swift +194 -0
  8. package/lib/commonjs/OTAProvider.js +113 -0
  9. package/lib/commonjs/OTAProvider.js.map +1 -0
  10. package/lib/commonjs/hooks/useOTAUpdate.js +272 -0
  11. package/lib/commonjs/hooks/useOTAUpdate.js.map +1 -0
  12. package/lib/commonjs/index.js +98 -0
  13. package/lib/commonjs/index.js.map +1 -0
  14. package/lib/commonjs/utils/api.js +60 -0
  15. package/lib/commonjs/utils/api.js.map +1 -0
  16. package/lib/commonjs/utils/storage.js +209 -0
  17. package/lib/commonjs/utils/storage.js.map +1 -0
  18. package/lib/commonjs/utils/verification.js +145 -0
  19. package/lib/commonjs/utils/verification.js.map +1 -0
  20. package/lib/module/OTAProvider.js +104 -0
  21. package/lib/module/OTAProvider.js.map +1 -0
  22. package/lib/module/hooks/useOTAUpdate.js +266 -0
  23. package/lib/module/hooks/useOTAUpdate.js.map +1 -0
  24. package/lib/module/index.js +11 -0
  25. package/lib/module/index.js.map +1 -0
  26. package/lib/module/utils/api.js +52 -0
  27. package/lib/module/utils/api.js.map +1 -0
  28. package/lib/module/utils/storage.js +202 -0
  29. package/lib/module/utils/storage.js.map +1 -0
  30. package/lib/module/utils/verification.js +137 -0
  31. package/lib/module/utils/verification.js.map +1 -0
  32. package/lib/typescript/OTAProvider.d.ts +28 -0
  33. package/lib/typescript/OTAProvider.d.ts.map +1 -0
  34. package/lib/typescript/hooks/useOTAUpdate.d.ts +35 -0
  35. package/lib/typescript/hooks/useOTAUpdate.d.ts.map +1 -0
  36. package/lib/typescript/index.d.ts +12 -0
  37. package/lib/typescript/index.d.ts.map +1 -0
  38. package/lib/typescript/utils/api.d.ts +47 -0
  39. package/lib/typescript/utils/api.d.ts.map +1 -0
  40. package/lib/typescript/utils/storage.d.ts +32 -0
  41. package/lib/typescript/utils/storage.d.ts.map +1 -0
  42. package/lib/typescript/utils/verification.d.ts +11 -0
  43. package/lib/typescript/utils/verification.d.ts.map +1 -0
  44. package/ota-update.podspec +21 -0
  45. package/package.json +83 -0
  46. package/src/OTAProvider.tsx +160 -0
  47. package/src/hooks/useOTAUpdate.ts +344 -0
  48. package/src/index.ts +36 -0
  49. package/src/utils/api.ts +99 -0
  50. package/src/utils/storage.ts +249 -0
  51. 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';
@@ -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
+ }