@viettelpost/react-native-ota 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 +38 -0
- package/android/build.gradle +48 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAHashUtils.kt +21 -0
- package/android/src/main/java/com/viettelpost/otakit/OTATestReceiver.kt +51 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateBundleResolver.kt +405 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateCleanup.kt +186 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateDownloader.kt +649 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateMetadata.kt +72 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateModule.kt +140 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdatePackage.kt +30 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateSignatureVerifier.kt +63 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateStorage.kt +62 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAZipUtils.kt +100 -0
- package/android/src/main/res/raw/ota_public_key.pem +9 -0
- package/bin/cli/assets-zip.js +77 -0
- package/bin/cli/bundle.js +72 -0
- package/bin/cli/deploy.js +224 -0
- package/bin/cli/sign.js +97 -0
- package/bin/cli/upload.js +109 -0
- package/bin/ota.js +200 -0
- package/docs/BACKEND_CONTRACT.md +93 -0
- package/docs/DEPLOY_CLI.md +39 -0
- package/docs/INTEGRATION_ANDROID.md +20 -0
- package/docs/INTEGRATION_IOS.md +21 -0
- package/docs/RELEASE_WORKFLOW.md +14 -0
- package/ios/OTAHashUtils.swift +22 -0
- package/ios/OTAUpdateBundleResolver.swift +359 -0
- package/ios/OTAUpdateCleanup.swift +269 -0
- package/ios/OTAUpdateDownloader.swift +709 -0
- package/ios/OTAUpdateMetadata.swift +47 -0
- package/ios/OTAUpdateModule.mm +190 -0
- package/ios/OTAUpdateSignatureVerifier.swift +81 -0
- package/ios/OTAUpdateStorage.swift +83 -0
- package/ios/OTAZipUtils.swift +103 -0
- package/ios/ota_public_key.pem +9 -0
- package/lib/NativeOTAUpdate.d.ts +77 -0
- package/lib/NativeOTAUpdate.js +59 -0
- package/lib/OTAClient.d.ts +27 -0
- package/lib/OTAClient.js +101 -0
- package/lib/config.d.ts +14 -0
- package/lib/config.js +29 -0
- package/lib/devtools.d.ts +10 -0
- package/lib/devtools.js +54 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.js +32 -0
- package/lib/spec/NativeOTAUpdate.d.ts +16 -0
- package/lib/spec/NativeOTAUpdate.js +4 -0
- package/package.json +82 -0
- package/react-native-ota.podspec +21 -0
- package/scripts/run-bin.js +67 -0
- package/src/NativeOTAUpdate.ts +144 -0
- package/src/OTAClient.ts +151 -0
- package/src/config.ts +41 -0
- package/src/devtools.ts +64 -0
- package/src/index.ts +69 -0
- package/src/spec/NativeOTAUpdate.ts +21 -0
package/src/OTAClient.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
downloadAndInstallBundle,
|
|
5
|
+
getCurrentBundleInfo,
|
|
6
|
+
getOTAMetadata,
|
|
7
|
+
type OTABundleInfo,
|
|
8
|
+
type OTAMetadata,
|
|
9
|
+
type OTAUpdatePayload,
|
|
10
|
+
} from './NativeOTAUpdate';
|
|
11
|
+
import { getConfig, resolveHeaders, type OTAConfig } from './config';
|
|
12
|
+
|
|
13
|
+
export type OTACheckUpdateResponse = {
|
|
14
|
+
version: string;
|
|
15
|
+
bundleUrl: string;
|
|
16
|
+
fileName: string;
|
|
17
|
+
platform: 'android' | 'ios';
|
|
18
|
+
sha256: string;
|
|
19
|
+
signature: string;
|
|
20
|
+
assetsUrl?: string;
|
|
21
|
+
assetsSha256?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type OTAUpdateResult =
|
|
25
|
+
| { success: true; version: string; alreadyUpToDate?: boolean }
|
|
26
|
+
| { success: false; code: string; message: string };
|
|
27
|
+
|
|
28
|
+
function buildCheckUpdateURL(
|
|
29
|
+
baseURL: string,
|
|
30
|
+
currentVersion: string,
|
|
31
|
+
checkUpdatePath = '/api/ota/check-update',
|
|
32
|
+
): string {
|
|
33
|
+
const platform = Platform.OS === 'ios' ? 'ios' : 'android';
|
|
34
|
+
const normalizedBaseURL = baseURL.replace(/\/+$/, '');
|
|
35
|
+
const normalizedPath = checkUpdatePath.startsWith('/')
|
|
36
|
+
? checkUpdatePath
|
|
37
|
+
: `/${checkUpdatePath}`;
|
|
38
|
+
return `${normalizedBaseURL}${normalizedPath}?platform=${platform}¤tVersion=${encodeURIComponent(currentVersion)}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function fetchUpdateMetadata(
|
|
42
|
+
url: string,
|
|
43
|
+
headers: Record<string, string>,
|
|
44
|
+
): Promise<OTACheckUpdateResponse | null> {
|
|
45
|
+
const response = await fetch(url, {
|
|
46
|
+
method: 'GET',
|
|
47
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (response.status === 204) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
throw new Error(`OTA check-update failed: HTTP ${response.status}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const data = (await response.json()) as Record<string, unknown>;
|
|
58
|
+
const required = ['version', 'bundleUrl', 'fileName', 'platform', 'sha256', 'signature'];
|
|
59
|
+
for (const field of required) {
|
|
60
|
+
if (typeof data[field] !== 'string' || !(data[field] as string).trim()) {
|
|
61
|
+
throw new Error(`OTA check-update response missing required field: ${field}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return data as unknown as OTACheckUpdateResponse;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function toPayload(r: OTACheckUpdateResponse): OTAUpdatePayload {
|
|
68
|
+
return {
|
|
69
|
+
version: r.version,
|
|
70
|
+
bundleUrl: r.bundleUrl,
|
|
71
|
+
platform: r.platform,
|
|
72
|
+
fileName: r.fileName,
|
|
73
|
+
sha256: r.sha256,
|
|
74
|
+
signature: r.signature,
|
|
75
|
+
assetsUrl: r.assetsUrl,
|
|
76
|
+
assetsSha256: r.assetsSha256,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function checkAndInstallOTA(
|
|
81
|
+
otaBaseURL: string,
|
|
82
|
+
options: Pick<OTAConfig, 'headers' | 'checkUpdatePath'> = {},
|
|
83
|
+
): Promise<OTAUpdateResult> {
|
|
84
|
+
try {
|
|
85
|
+
const metadata = await getOTAMetadata();
|
|
86
|
+
const currentVersion =
|
|
87
|
+
metadata.activeBundleVersion || metadata.embeddedBundleVersion;
|
|
88
|
+
|
|
89
|
+
const url = buildCheckUpdateURL(
|
|
90
|
+
otaBaseURL,
|
|
91
|
+
currentVersion,
|
|
92
|
+
options.checkUpdatePath,
|
|
93
|
+
);
|
|
94
|
+
const update = await fetchUpdateMetadata(
|
|
95
|
+
url,
|
|
96
|
+
await resolveHeaders(options.headers),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (!update) {
|
|
100
|
+
return { success: true, version: currentVersion, alreadyUpToDate: true };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const expectedPlatform = Platform.OS === 'ios' ? 'ios' : 'android';
|
|
104
|
+
if (update.platform !== expectedPlatform) {
|
|
105
|
+
return {
|
|
106
|
+
success: false,
|
|
107
|
+
code: 'OTA_PLATFORM_MISMATCH',
|
|
108
|
+
message: `Server returned platform=${update.platform}, expected ${expectedPlatform}`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (metadata.activeBundleVersion === update.version) {
|
|
113
|
+
return { success: true, version: update.version, alreadyUpToDate: true };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const didInstall = await downloadAndInstallBundle(toPayload(update));
|
|
117
|
+
if (!didInstall) {
|
|
118
|
+
return {
|
|
119
|
+
success: false,
|
|
120
|
+
code: 'OTA_INSTALL_FAILED',
|
|
121
|
+
message: `Failed to install OTA update version ${update.version}`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return { success: true, version: update.version };
|
|
125
|
+
} catch (error) {
|
|
126
|
+
return {
|
|
127
|
+
success: false,
|
|
128
|
+
code: 'OTA_SERVICE_ERROR',
|
|
129
|
+
message: error instanceof Error ? error.message : String(error),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function sync(): Promise<OTAUpdateResult> {
|
|
135
|
+
const config = getConfig();
|
|
136
|
+
return checkAndInstallOTA(config.baseURL, {
|
|
137
|
+
headers: config.headers,
|
|
138
|
+
checkUpdatePath: config.checkUpdatePath,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function getCurrentOTAStatus(): Promise<{
|
|
143
|
+
metadata: OTAMetadata;
|
|
144
|
+
bundleInfo: OTABundleInfo;
|
|
145
|
+
}> {
|
|
146
|
+
const [metadata, bundleInfo] = await Promise.all([
|
|
147
|
+
getOTAMetadata(),
|
|
148
|
+
getCurrentBundleInfo(),
|
|
149
|
+
]);
|
|
150
|
+
return { metadata, bundleInfo };
|
|
151
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type OTAHeaders =
|
|
2
|
+
| Record<string, string>
|
|
3
|
+
| (() => Record<string, string> | Promise<Record<string, string>>);
|
|
4
|
+
|
|
5
|
+
export type OTAConfig = {
|
|
6
|
+
baseURL: string;
|
|
7
|
+
headers?: OTAHeaders;
|
|
8
|
+
checkUpdatePath?: string;
|
|
9
|
+
publicKey?: string;
|
|
10
|
+
policy?: {
|
|
11
|
+
autoInstall?: boolean;
|
|
12
|
+
restart?: 'manual' | 'immediate';
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let currentConfig: OTAConfig | null = null;
|
|
17
|
+
|
|
18
|
+
export function configure(config: OTAConfig): void {
|
|
19
|
+
if (!config.baseURL || !config.baseURL.trim()) {
|
|
20
|
+
throw new Error('OTA.configure requires a non-empty baseURL');
|
|
21
|
+
}
|
|
22
|
+
currentConfig = {
|
|
23
|
+
...config,
|
|
24
|
+
baseURL: config.baseURL.replace(/\/+$/, ''),
|
|
25
|
+
checkUpdatePath: config.checkUpdatePath || '/api/ota/check-update',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getConfig(): OTAConfig {
|
|
30
|
+
if (!currentConfig) {
|
|
31
|
+
throw new Error('OTA is not configured. Call OTA.configure({ baseURL }) before syncing.');
|
|
32
|
+
}
|
|
33
|
+
return currentConfig;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function resolveHeaders(headers?: OTAHeaders): Promise<Record<string, string>> {
|
|
37
|
+
if (!headers) {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
return typeof headers === 'function' ? headers() : headers;
|
|
41
|
+
}
|
package/src/devtools.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cleanupOTAStorage,
|
|
3
|
+
downloadAndInstallBundle,
|
|
4
|
+
getCurrentBundleInfo,
|
|
5
|
+
getDownloadInfo,
|
|
6
|
+
getOTADiskUsage,
|
|
7
|
+
getOTADirectory,
|
|
8
|
+
getOTAMetadata,
|
|
9
|
+
markOTASuccess,
|
|
10
|
+
type OTAUpdatePayload,
|
|
11
|
+
} from './NativeOTAUpdate';
|
|
12
|
+
|
|
13
|
+
function warnIfNotDev() {
|
|
14
|
+
if (!__DEV__) {
|
|
15
|
+
console.warn('OTADevTools is intended only for manual OTA testing.');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function logOTAInfo() {
|
|
20
|
+
warnIfNotDev();
|
|
21
|
+
const [directory, metadata, bundleInfo] = await Promise.all([
|
|
22
|
+
getOTADirectory(),
|
|
23
|
+
getOTAMetadata(),
|
|
24
|
+
getCurrentBundleInfo(),
|
|
25
|
+
]);
|
|
26
|
+
console.log('OTA directory:', directory);
|
|
27
|
+
console.log('OTA metadata:', metadata);
|
|
28
|
+
console.log('OTA bundle info:', bundleInfo);
|
|
29
|
+
return { directory, metadata, bundleInfo };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function confirmOTASuccess(): Promise<boolean> {
|
|
33
|
+
warnIfNotDev();
|
|
34
|
+
const didMarkSuccess = await markOTASuccess();
|
|
35
|
+
console.log('OTA success marked:', didMarkSuccess);
|
|
36
|
+
return didMarkSuccess;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function downloadAndInstallOTA(
|
|
40
|
+
update: OTAUpdatePayload,
|
|
41
|
+
): Promise<boolean> {
|
|
42
|
+
warnIfNotDev();
|
|
43
|
+
const didInstall = await downloadAndInstallBundle(update);
|
|
44
|
+
const downloadInfo = await getDownloadInfo(update.version);
|
|
45
|
+
console.log('OTA downloaded install prepared:', didInstall, update.version);
|
|
46
|
+
console.log('OTA download info:', downloadInfo);
|
|
47
|
+
return didInstall;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function logOTADiskUsage() {
|
|
51
|
+
warnIfNotDev();
|
|
52
|
+
const diskUsage = await getOTADiskUsage();
|
|
53
|
+
console.log('OTA disk usage:', diskUsage);
|
|
54
|
+
return diskUsage;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function cleanupOTADevStorage(): Promise<boolean> {
|
|
58
|
+
warnIfNotDev();
|
|
59
|
+
const didCleanup = await cleanupOTAStorage();
|
|
60
|
+
const diskUsage = await getOTADiskUsage();
|
|
61
|
+
console.log('OTA cleanup completed:', didCleanup);
|
|
62
|
+
console.log('OTA disk usage after cleanup:', diskUsage);
|
|
63
|
+
return didCleanup;
|
|
64
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { configure, getConfig, type OTAConfig } from './config';
|
|
2
|
+
import {
|
|
3
|
+
checkAndInstallOTA,
|
|
4
|
+
getCurrentOTAStatus,
|
|
5
|
+
sync,
|
|
6
|
+
type OTACheckUpdateResponse,
|
|
7
|
+
type OTAUpdateResult,
|
|
8
|
+
} from './OTAClient';
|
|
9
|
+
import {
|
|
10
|
+
cleanupOTAStorage,
|
|
11
|
+
copyBundleFromDocuments,
|
|
12
|
+
downloadAndInstallBundle,
|
|
13
|
+
getCurrentBundleInfo,
|
|
14
|
+
getDownloadInfo,
|
|
15
|
+
getOTADirectory,
|
|
16
|
+
getOTADiskUsage,
|
|
17
|
+
getOTAMetadata,
|
|
18
|
+
markOTASuccess,
|
|
19
|
+
prepareManualInstall,
|
|
20
|
+
resetOTAMetadata,
|
|
21
|
+
type OTABundleInfo,
|
|
22
|
+
type OTADiskUsage,
|
|
23
|
+
type OTADownloadInfo,
|
|
24
|
+
type OTAMetadata,
|
|
25
|
+
type OTAStatus,
|
|
26
|
+
type OTAUpdatePayload,
|
|
27
|
+
} from './NativeOTAUpdate';
|
|
28
|
+
|
|
29
|
+
export type {
|
|
30
|
+
OTAConfig,
|
|
31
|
+
OTACheckUpdateResponse,
|
|
32
|
+
OTAUpdateResult,
|
|
33
|
+
OTABundleInfo,
|
|
34
|
+
OTADiskUsage,
|
|
35
|
+
OTADownloadInfo,
|
|
36
|
+
OTAMetadata,
|
|
37
|
+
OTAStatus,
|
|
38
|
+
OTAUpdatePayload,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
checkAndInstallOTA,
|
|
43
|
+
cleanupOTAStorage,
|
|
44
|
+
configure,
|
|
45
|
+
copyBundleFromDocuments,
|
|
46
|
+
downloadAndInstallBundle,
|
|
47
|
+
getConfig,
|
|
48
|
+
getCurrentBundleInfo,
|
|
49
|
+
getCurrentOTAStatus,
|
|
50
|
+
getDownloadInfo,
|
|
51
|
+
getOTADirectory,
|
|
52
|
+
getOTADiskUsage,
|
|
53
|
+
getOTAMetadata,
|
|
54
|
+
markOTASuccess,
|
|
55
|
+
prepareManualInstall,
|
|
56
|
+
resetOTAMetadata,
|
|
57
|
+
sync,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const OTA = {
|
|
61
|
+
configure,
|
|
62
|
+
sync,
|
|
63
|
+
checkAndInstall: checkAndInstallOTA,
|
|
64
|
+
markSuccess: markOTASuccess,
|
|
65
|
+
getMetadata: getOTAMetadata,
|
|
66
|
+
getCurrentBundleInfo,
|
|
67
|
+
getCurrentStatus: getCurrentOTAStatus,
|
|
68
|
+
cleanupStorage: cleanupOTAStorage,
|
|
69
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { TurboModule } from 'react-native';
|
|
2
|
+
import { TurboModuleRegistry } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface Spec extends TurboModule {
|
|
5
|
+
getMetadata(): Promise<string>;
|
|
6
|
+
resetMetadata(): Promise<boolean>;
|
|
7
|
+
getOTADirectory(): Promise<string>;
|
|
8
|
+
prepareManualInstall(bundleVersion: string): Promise<boolean>;
|
|
9
|
+
markSuccess(): Promise<boolean>;
|
|
10
|
+
copyBundleFromDocuments(
|
|
11
|
+
bundleVersion: string,
|
|
12
|
+
fileName: string,
|
|
13
|
+
): Promise<boolean>;
|
|
14
|
+
getCurrentBundleInfo(): Promise<string>;
|
|
15
|
+
downloadAndInstallBundle(updateJson: string): Promise<boolean>;
|
|
16
|
+
getDownloadInfo(version: string): Promise<string>;
|
|
17
|
+
getOTADiskUsage(): Promise<string>;
|
|
18
|
+
cleanupOTAStorage(): Promise<boolean>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default TurboModuleRegistry.getEnforcing<Spec>('OTAUpdate');
|