react-native-instantpay-code-push 1.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/InstantpayCodePush.podspec +20 -0
- package/LICENSE +20 -0
- package/README.md +158 -0
- package/android/build.gradle +91 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/instantpaycodepush/BundleFileStorageService.kt +835 -0
- package/android/src/main/java/com/instantpaycodepush/BundleMetadata.kt +249 -0
- package/android/src/main/java/com/instantpaycodepush/CommonHelper.kt +39 -0
- package/android/src/main/java/com/instantpaycodepush/DecompressService.kt +85 -0
- package/android/src/main/java/com/instantpaycodepush/DecompressionStrategy.kt +24 -0
- package/android/src/main/java/com/instantpaycodepush/FileManagerService.kt +105 -0
- package/android/src/main/java/com/instantpaycodepush/HashUtils.kt +50 -0
- package/android/src/main/java/com/instantpaycodepush/InstantpayCodePushModule.kt +182 -0
- package/android/src/main/java/com/instantpaycodepush/InstantpayCodePushPackage.kt +33 -0
- package/android/src/main/java/com/instantpaycodepush/IpayCodePush.kt +101 -0
- package/android/src/main/java/com/instantpaycodepush/IpayCodePushException.kt +135 -0
- package/android/src/main/java/com/instantpaycodepush/IpayCodePushImpl.kt +329 -0
- package/android/src/main/java/com/instantpaycodepush/OkHttpDownloadService.kt +283 -0
- package/android/src/main/java/com/instantpaycodepush/ReactIntegrationManager.kt +141 -0
- package/android/src/main/java/com/instantpaycodepush/ReactIntegrationManagerBase.kt +35 -0
- package/android/src/main/java/com/instantpaycodepush/SignatureVerifier.kt +354 -0
- package/android/src/main/java/com/instantpaycodepush/VersionedPreferencesService.kt +70 -0
- package/android/src/main/java/com/instantpaycodepush/ZipDecompressionStrategy.kt +198 -0
- package/ios/InstantpayCodePush.h +5 -0
- package/ios/InstantpayCodePush.mm +21 -0
- package/lib/module/DefaultResolver.js +34 -0
- package/lib/module/DefaultResolver.js.map +1 -0
- package/lib/module/NativeInstantpayCodePush.js +5 -0
- package/lib/module/NativeInstantpayCodePush.js.map +1 -0
- package/lib/module/checkForUpdate.js +68 -0
- package/lib/module/checkForUpdate.js.map +1 -0
- package/lib/module/error.js +137 -0
- package/lib/module/error.js.map +1 -0
- package/lib/module/fetchUpdateInfo.js +36 -0
- package/lib/module/fetchUpdateInfo.js.map +1 -0
- package/lib/module/global.d.js +8 -0
- package/lib/module/global.d.js.map +1 -0
- package/lib/module/hooks/useEventCallback.js +13 -0
- package/lib/module/hooks/useEventCallback.js.map +1 -0
- package/lib/module/index.js +291 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/native.js +233 -0
- package/lib/module/native.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/store.js +53 -0
- package/lib/module/store.js.map +1 -0
- package/lib/module/types.js +62 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/wrap.js +171 -0
- package/lib/module/wrap.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/DefaultResolver.d.ts +10 -0
- package/lib/typescript/src/DefaultResolver.d.ts.map +1 -0
- package/lib/typescript/src/NativeInstantpayCodePush.d.ts +100 -0
- package/lib/typescript/src/NativeInstantpayCodePush.d.ts.map +1 -0
- package/lib/typescript/src/checkForUpdate.d.ts +29 -0
- package/lib/typescript/src/checkForUpdate.d.ts.map +1 -0
- package/lib/typescript/src/error.d.ts +124 -0
- package/lib/typescript/src/error.d.ts.map +1 -0
- package/lib/typescript/src/fetchUpdateInfo.d.ts +8 -0
- package/lib/typescript/src/fetchUpdateInfo.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useEventCallback.d.ts +5 -0
- package/lib/typescript/src/hooks/useEventCallback.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +203 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/native.d.ts +128 -0
- package/lib/typescript/src/native.d.ts.map +1 -0
- package/lib/typescript/src/store.d.ts +11 -0
- package/lib/typescript/src/store.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +174 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/wrap.d.ts +179 -0
- package/lib/typescript/src/wrap.d.ts.map +1 -0
- package/package.json +174 -0
- package/src/DefaultResolver.ts +36 -0
- package/src/NativeInstantpayCodePush.ts +111 -0
- package/src/checkForUpdate.ts +122 -0
- package/src/error.ts +159 -0
- package/src/fetchUpdateInfo.ts +47 -0
- package/src/global.d.ts +23 -0
- package/src/hooks/useEventCallback.ts +30 -0
- package/src/index.tsx +379 -0
- package/src/native.ts +280 -0
- package/src/store.ts +69 -0
- package/src/types.ts +227 -0
- package/src/wrap.tsx +384 -0
package/src/native.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { NativeEventEmitter } from "react-native";
|
|
2
|
+
import { IpayCodePushErrorCode, isIpayCodePushError } from "./error";
|
|
3
|
+
import type { UpdateStatus } from "./types"
|
|
4
|
+
|
|
5
|
+
import IpayCodePushNative, {
|
|
6
|
+
type UpdateBundleParams
|
|
7
|
+
} from './NativeInstantpayCodePush';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export { IpayCodePushErrorCode, isIpayCodePushError };
|
|
11
|
+
|
|
12
|
+
const NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
13
|
+
|
|
14
|
+
const __IPAY_CODE_PUSH_BUNDLE_ID=undefined;
|
|
15
|
+
|
|
16
|
+
export const IpayCodePushConstants = {
|
|
17
|
+
IPAY_CODE_PUSH_BUNDLE_ID: __IPAY_CODE_PUSH_BUNDLE_ID || NIL_UUID,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type IpayCodePushEvent = {
|
|
21
|
+
onProgress: {
|
|
22
|
+
progress: number;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
export const addListener = <T extends keyof IpayCodePushEvent>(
|
|
28
|
+
eventName: T,
|
|
29
|
+
listener: (event: any) => void, //(event: IpayCodePushEvent[T])
|
|
30
|
+
) => {
|
|
31
|
+
const eventEmitter = new NativeEventEmitter(IpayCodePushNative);
|
|
32
|
+
const subscription = eventEmitter.addListener(eventName, listener);
|
|
33
|
+
|
|
34
|
+
return () => {
|
|
35
|
+
subscription.remove();
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type UpdateParams = UpdateBundleParams & {
|
|
40
|
+
status: UpdateStatus;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// In-flight update deduplication by bundleId (session-scoped).
|
|
44
|
+
const inflightUpdates = new Map<string, Promise<boolean>>();
|
|
45
|
+
|
|
46
|
+
// Tracks the last successfully installed bundleId for this session.
|
|
47
|
+
let lastInstalledBundleId: string | null = null;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Downloads files and applies them to the app.
|
|
51
|
+
*
|
|
52
|
+
* @param {UpdateParams} params - Parameters object required for bundle update
|
|
53
|
+
* @returns {Promise<boolean>} Resolves with true if download was successful
|
|
54
|
+
* @throws {Error} Rejects with error.code from HotUpdaterErrorCode enum and error.message
|
|
55
|
+
*/
|
|
56
|
+
export async function updateBundle(params: UpdateParams): Promise<boolean>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @deprecated Use updateBundle(params: UpdateBundleParamsWithStatus) instead
|
|
60
|
+
*/
|
|
61
|
+
export async function updateBundle(
|
|
62
|
+
bundleId: string,
|
|
63
|
+
fileUrl: string | null,
|
|
64
|
+
): Promise<boolean>;
|
|
65
|
+
|
|
66
|
+
export async function updateBundle(
|
|
67
|
+
paramsOrBundleId: UpdateParams | string,
|
|
68
|
+
fileUrl?: string | null,
|
|
69
|
+
): Promise<boolean> {
|
|
70
|
+
|
|
71
|
+
const updateBundleId = typeof paramsOrBundleId === "string" ? paramsOrBundleId : paramsOrBundleId.bundleId;
|
|
72
|
+
|
|
73
|
+
const status = typeof paramsOrBundleId === "string" ? "UPDATE" : paramsOrBundleId.status;
|
|
74
|
+
|
|
75
|
+
// If we have already installed this bundle in this session, skip re-download.
|
|
76
|
+
if (status === "UPDATE" && lastInstalledBundleId === updateBundleId) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const currentBundleId = getBundleId();
|
|
81
|
+
|
|
82
|
+
// updateBundleId <= currentBundleId
|
|
83
|
+
if (
|
|
84
|
+
status === "UPDATE" &&
|
|
85
|
+
updateBundleId.localeCompare(currentBundleId) <= 0
|
|
86
|
+
) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
"Update bundle id is the same as the current bundle id. Preventing infinite update loop.",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// In-flight guard: return the same promise if the same bundle is already updating.
|
|
93
|
+
const existing = inflightUpdates.get(updateBundleId);
|
|
94
|
+
if (existing) return existing;
|
|
95
|
+
|
|
96
|
+
const targetFileUrl = typeof paramsOrBundleId === "string" ? (fileUrl ?? null) : paramsOrBundleId.fileUrl;
|
|
97
|
+
|
|
98
|
+
const targetFileHash = typeof paramsOrBundleId === "string" ? undefined : paramsOrBundleId.fileHash;
|
|
99
|
+
|
|
100
|
+
const promise = (async () => {
|
|
101
|
+
try {
|
|
102
|
+
const ok = await IpayCodePushNative.updateBundle({
|
|
103
|
+
bundleId: updateBundleId,
|
|
104
|
+
fileUrl: targetFileUrl,
|
|
105
|
+
fileHash: targetFileHash ?? null,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (ok) {
|
|
109
|
+
lastInstalledBundleId = updateBundleId;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return ok;
|
|
113
|
+
|
|
114
|
+
} finally {
|
|
115
|
+
inflightUpdates.delete(updateBundleId);
|
|
116
|
+
}
|
|
117
|
+
})();
|
|
118
|
+
|
|
119
|
+
inflightUpdates.set(updateBundleId, promise);
|
|
120
|
+
return promise;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Fetches the current app version.
|
|
125
|
+
*/
|
|
126
|
+
export const getAppVersion = (): string | null => {
|
|
127
|
+
const constants = IpayCodePushNative.getConstants();
|
|
128
|
+
return constants?.APP_VERSION ?? null;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Reloads the app.
|
|
133
|
+
*/
|
|
134
|
+
export const reload = async () => {
|
|
135
|
+
await IpayCodePushNative.reload();
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Fetches the minimum bundle id, which represents the initial bundle of the app
|
|
140
|
+
* since it is created at build time.
|
|
141
|
+
*
|
|
142
|
+
* @returns {string} Resolves with the minimum bundle id or null if not available.
|
|
143
|
+
*/
|
|
144
|
+
export const getMinBundleId = (): string => {
|
|
145
|
+
const constants = IpayCodePushNative.getConstants();
|
|
146
|
+
return constants.MIN_BUNDLE_ID;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Fetches the current bundle version id.
|
|
151
|
+
*
|
|
152
|
+
* @async
|
|
153
|
+
* @returns {string} Resolves with the current version id or null if not available.
|
|
154
|
+
*/
|
|
155
|
+
export const getBundleId = (): string => {
|
|
156
|
+
return IpayCodePushConstants.IPAY_CODE_PUSH_BUNDLE_ID === NIL_UUID
|
|
157
|
+
? getMinBundleId()
|
|
158
|
+
: IpayCodePushConstants.IPAY_CODE_PUSH_BUNDLE_ID;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Fetches the channel for the app.
|
|
163
|
+
*
|
|
164
|
+
* @returns {string} Resolves with the channel or null if not available.
|
|
165
|
+
*/
|
|
166
|
+
export const getChannel = (): string => {
|
|
167
|
+
const constants = IpayCodePushNative.getConstants();
|
|
168
|
+
return constants.CHANNEL;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Fetches the fingerprint for the app.
|
|
173
|
+
*
|
|
174
|
+
* @returns {string | null} Resolves with the fingerprint hash
|
|
175
|
+
*/
|
|
176
|
+
export const getFingerprintHash = (): string | null => {
|
|
177
|
+
const constants = IpayCodePushNative.getConstants();
|
|
178
|
+
return constants.FINGERPRINT_HASH;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Result returned by notifyAppReady()
|
|
183
|
+
* - `status: "PROMOTED"` - Staging bundle was promoted to stable (ACTIVE event)
|
|
184
|
+
* - `status: "RECOVERED"` - App recovered from crash, rollback occurred (ROLLBACK event)
|
|
185
|
+
* - `status: "STABLE"` - No changes, already stable
|
|
186
|
+
* - `crashedBundleId` - Present only when status is "RECOVERED"
|
|
187
|
+
*/
|
|
188
|
+
export type NotifyAppReadyResult = {
|
|
189
|
+
status: "PROMOTED" | "RECOVERED" | "STABLE";
|
|
190
|
+
crashedBundleId?: string;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Notifies the native side that the app has successfully started with the current bundle.
|
|
195
|
+
* If the bundle matches the staging bundle, it promotes to stable.
|
|
196
|
+
*
|
|
197
|
+
* This function is called automatically when the module loads.
|
|
198
|
+
*
|
|
199
|
+
* @returns {NotifyAppReadyResult} Bundle state information
|
|
200
|
+
* - `status: "PROMOTED"` - Staging bundle was promoted to stable (ACTIVE event)
|
|
201
|
+
* - `status: "RECOVERED"` - App recovered from crash, rollback occurred (ROLLBACK event)
|
|
202
|
+
* - `status: "STABLE"` - No changes, already stable
|
|
203
|
+
* - `crashedBundleId` - Present only when status is "RECOVERED"
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```ts
|
|
207
|
+
* const result = IpayCodePush.notifyAppReady();
|
|
208
|
+
*
|
|
209
|
+
* switch (result.status) {
|
|
210
|
+
* case "PROMOTED":
|
|
211
|
+
* // Send ACTIVE analytics event
|
|
212
|
+
* analytics.track('bundle_active', { bundleId: IpayCodePush.getBundleId() });
|
|
213
|
+
* break;
|
|
214
|
+
* case "RECOVERED":
|
|
215
|
+
* // Send ROLLBACK analytics event
|
|
216
|
+
* analytics.track('bundle_rollback', { crashedBundleId: result.crashedBundleId });
|
|
217
|
+
* break;
|
|
218
|
+
* case "STABLE":
|
|
219
|
+
* // No special action needed
|
|
220
|
+
* break;
|
|
221
|
+
* }
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
export const notifyAppReady = (): NotifyAppReadyResult => {
|
|
225
|
+
const bundleId = getBundleId();
|
|
226
|
+
const result = IpayCodePushNative.notifyAppReady({ bundleId });
|
|
227
|
+
// Oldarch returns JSON string, newarch returns array
|
|
228
|
+
if (typeof result === "string") {
|
|
229
|
+
try {
|
|
230
|
+
return JSON.parse(result);
|
|
231
|
+
} catch {
|
|
232
|
+
return { status: "STABLE" };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Gets the list of bundle IDs that have been marked as crashed.
|
|
240
|
+
* These bundles will be rejected if attempted to install again.
|
|
241
|
+
*
|
|
242
|
+
* @returns {string[]} Array of crashed bundle IDs
|
|
243
|
+
*/
|
|
244
|
+
export const getCrashHistory = (): string[] => {
|
|
245
|
+
const result = IpayCodePushNative.getCrashHistory();
|
|
246
|
+
// Oldarch returns JSON string, newarch returns array
|
|
247
|
+
if (typeof result === "string") {
|
|
248
|
+
try {
|
|
249
|
+
return JSON.parse(result);
|
|
250
|
+
} catch {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Clears the crashed bundle history, allowing previously crashed bundles
|
|
259
|
+
* to be installed again.
|
|
260
|
+
*
|
|
261
|
+
* @returns {boolean} true if clearing was successful
|
|
262
|
+
*/
|
|
263
|
+
export const clearCrashHistory = (): boolean => {
|
|
264
|
+
return IpayCodePushNative.clearCrashHistory();
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Gets the base URL for the current active bundle directory.
|
|
269
|
+
* Returns the file:// URL to the bundle directory without trailing slash.
|
|
270
|
+
* This is used for Expo DOM components to construct full asset paths.
|
|
271
|
+
*
|
|
272
|
+
* @returns {string | null} Base URL string (e.g., "file:///data/.../bundle-store/abc123") or null if not available
|
|
273
|
+
*/
|
|
274
|
+
export const getBaseURL = (): string | null => {
|
|
275
|
+
const result = IpayCodePushNative.getBaseURL();
|
|
276
|
+
if (typeof result === "string" && result !== "") {
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
};
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import useSyncExternalStoreExports from "use-sync-external-store/shim/with-selector";
|
|
2
|
+
|
|
3
|
+
export type IpayCodePushState = {
|
|
4
|
+
progress: number;
|
|
5
|
+
isUpdateDownloaded: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports;
|
|
9
|
+
|
|
10
|
+
const createIpayCodePushStore = () => {
|
|
11
|
+
|
|
12
|
+
let state: IpayCodePushState = {
|
|
13
|
+
progress: 0,
|
|
14
|
+
isUpdateDownloaded: false,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const getSnapshot = () => {
|
|
18
|
+
return state;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const listeners = new Set<() => void>();
|
|
22
|
+
|
|
23
|
+
const emitChange = () => {
|
|
24
|
+
for (const listener of listeners) {
|
|
25
|
+
listener();
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const setState = (newState: Partial<IpayCodePushState>) => {
|
|
30
|
+
// Merge first, then normalize derived fields
|
|
31
|
+
const nextState: IpayCodePushState = {
|
|
32
|
+
...state,
|
|
33
|
+
...newState,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Derive `isUpdateDownloaded` from `progress` if provided.
|
|
37
|
+
// If `progress` is not provided but `isUpdateDownloaded` is,
|
|
38
|
+
// honor the explicit value.
|
|
39
|
+
if ("progress" in newState && typeof newState.progress === "number") {
|
|
40
|
+
nextState.isUpdateDownloaded = newState.progress >= 1;
|
|
41
|
+
} else if ("isUpdateDownloaded" in newState && typeof newState.isUpdateDownloaded === "boolean") {
|
|
42
|
+
nextState.isUpdateDownloaded = newState.isUpdateDownloaded;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
state = nextState;
|
|
46
|
+
emitChange();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const subscribe = (listener: () => void) => {
|
|
50
|
+
listeners.add(listener);
|
|
51
|
+
return () => listeners.delete(listener);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return { getSnapshot, setState, subscribe };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const ipayCodePushStore = createIpayCodePushStore();
|
|
58
|
+
|
|
59
|
+
export const useIpayCodePushStore = <T = IpayCodePushState>(
|
|
60
|
+
selector: (snapshot: IpayCodePushState) => T = (snapshot) => snapshot as T,
|
|
61
|
+
) => {
|
|
62
|
+
return useSyncExternalStoreWithSelector(
|
|
63
|
+
ipayCodePushStore.subscribe,
|
|
64
|
+
ipayCodePushStore.getSnapshot,
|
|
65
|
+
ipayCodePushStore.getSnapshot,
|
|
66
|
+
selector,
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Parameters passed to resolver.checkUpdate method
|
|
4
|
+
*/
|
|
5
|
+
export interface ResolverCheckUpdateParams {
|
|
6
|
+
/**
|
|
7
|
+
* The platform the app is running on
|
|
8
|
+
*/
|
|
9
|
+
platform: "ios" | "android";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The current app version
|
|
13
|
+
*/
|
|
14
|
+
appVersion: string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The current bundle ID
|
|
18
|
+
*/
|
|
19
|
+
bundleId: string;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Minimum bundle ID from build time
|
|
23
|
+
*/
|
|
24
|
+
minBundleId: string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The channel name (e.g., "production", "staging")
|
|
28
|
+
*/
|
|
29
|
+
channel: string;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Update strategy being used
|
|
33
|
+
*/
|
|
34
|
+
updateStrategy: "fingerprint" | "appVersion";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The fingerprint hash (only present when using fingerprint strategy)
|
|
38
|
+
*/
|
|
39
|
+
fingerprintHash: string | null;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Request headers from global config (for optional use)
|
|
43
|
+
*/
|
|
44
|
+
requestHeaders?: Record<string, string>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Request timeout from global config (for optional use)
|
|
48
|
+
*/
|
|
49
|
+
requestTimeout?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parameters passed to resolver.notifyAppReady method
|
|
54
|
+
*/
|
|
55
|
+
export interface ResolverNotifyAppReadyParams {
|
|
56
|
+
/**
|
|
57
|
+
* The bundle state from native notifyAppReady
|
|
58
|
+
* - "PROMOTED": Staging bundle was promoted to stable
|
|
59
|
+
* - "RECOVERED": App recovered from crash, rollback occurred
|
|
60
|
+
* - "STABLE": No changes, bundle is stable
|
|
61
|
+
*/
|
|
62
|
+
status: "PROMOTED" | "RECOVERED" | "STABLE";
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Present only when status is "RECOVERED"
|
|
66
|
+
*/
|
|
67
|
+
crashedBundleId?: string;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Request headers from global config (for optional use)
|
|
71
|
+
*/
|
|
72
|
+
requestHeaders?: Record<string, string>;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Request timeout from global config (for optional use)
|
|
76
|
+
*/
|
|
77
|
+
requestTimeout?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Resolver interface for custom network operations
|
|
82
|
+
*/
|
|
83
|
+
export interface IpayCodePushResolver {
|
|
84
|
+
/**
|
|
85
|
+
* Custom implementation for checking updates.
|
|
86
|
+
* When provided, this completely replaces the default fetchUpdateInfo flow.
|
|
87
|
+
*
|
|
88
|
+
* @param params - All parameters needed to check for updates
|
|
89
|
+
* @returns Update information or null if up to date
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* checkUpdate: async (params) => {
|
|
94
|
+
* const response = await fetch(`https://api.custom.com/check`, {
|
|
95
|
+
* method: 'POST',
|
|
96
|
+
* body: JSON.stringify(params),
|
|
97
|
+
* headers: params.requestHeaders,
|
|
98
|
+
* });
|
|
99
|
+
*
|
|
100
|
+
* if (!response.ok) return null;
|
|
101
|
+
* return response.json();
|
|
102
|
+
* }
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
checkUpdate?: (
|
|
106
|
+
params: ResolverCheckUpdateParams,
|
|
107
|
+
) => Promise<AppUpdateInfo | null>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Custom implementation for notifying app ready.
|
|
111
|
+
* When provided, this completely replaces the default notifyAppReady network flow.
|
|
112
|
+
* Note: The native notifyAppReady for bundle promotion still happens automatically.
|
|
113
|
+
*
|
|
114
|
+
* @param params - All parameters about the current app state
|
|
115
|
+
* @returns Notification result
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* notifyAppReady: async (params) => {
|
|
120
|
+
* await fetch(`https://api.custom.com/notify`, {
|
|
121
|
+
* method: 'POST',
|
|
122
|
+
* body: JSON.stringify(params),
|
|
123
|
+
* });
|
|
124
|
+
*
|
|
125
|
+
* return { status: "STABLE" };
|
|
126
|
+
* }
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
notifyAppReady?: (
|
|
130
|
+
params: ResolverNotifyAppReadyParams,
|
|
131
|
+
) => Promise<undefined>; //Promise<NotifyAppReadyResult | undefined>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Information about a signature verification failure.
|
|
136
|
+
* This is a security-critical event that indicates the bundle
|
|
137
|
+
* may have been tampered with or the public key is misconfigured.
|
|
138
|
+
*/
|
|
139
|
+
export interface SignatureVerificationFailure {
|
|
140
|
+
/**
|
|
141
|
+
* The bundle ID that failed verification.
|
|
142
|
+
*/
|
|
143
|
+
bundleId: string;
|
|
144
|
+
/**
|
|
145
|
+
* Human-readable error message from the native layer.
|
|
146
|
+
*/
|
|
147
|
+
message: string;
|
|
148
|
+
/**
|
|
149
|
+
* The underlying error object.
|
|
150
|
+
*/
|
|
151
|
+
error: Error;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Checks if an error is a signature verification failure.
|
|
156
|
+
* Matches error messages from both iOS and Android native implementations.
|
|
157
|
+
*
|
|
158
|
+
* **IMPORTANT**: This function relies on specific error message patterns from native code.
|
|
159
|
+
* If you change the error messages in the native implementations, update these patterns:
|
|
160
|
+
* - iOS: `ios/ipaycodepush/Internal/SignatureVerifier.swift` (SignatureVerificationError)
|
|
161
|
+
* - Android: `android/src/main/java/com/ipaycodepush/SignatureVerifier.kt` (SignatureVerificationException)
|
|
162
|
+
*/
|
|
163
|
+
export function isSignatureVerificationError(error: unknown): boolean {
|
|
164
|
+
if (!(error instanceof Error)) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const message = error.message.toLowerCase();
|
|
169
|
+
|
|
170
|
+
// Match iOS SignatureVerificationError messages
|
|
171
|
+
// Match Android SignatureVerificationException messages
|
|
172
|
+
return (
|
|
173
|
+
message.includes("signature verification") ||
|
|
174
|
+
message.includes("public key not configured") ||
|
|
175
|
+
message.includes("public key format is invalid") ||
|
|
176
|
+
message.includes("signature format is invalid") ||
|
|
177
|
+
message.includes("bundle may be corrupted or tampered")
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Extracts signature verification failure details from an error.
|
|
183
|
+
*/
|
|
184
|
+
export function extractSignatureFailure(
|
|
185
|
+
error: unknown,
|
|
186
|
+
bundleId: string,
|
|
187
|
+
): SignatureVerificationFailure {
|
|
188
|
+
const normalizedError =
|
|
189
|
+
error instanceof Error ? error : new Error(String(error));
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
bundleId,
|
|
193
|
+
message: normalizedError.message,
|
|
194
|
+
error: normalizedError,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
export type UpdateStatus = "ROLLBACK" | "UPDATE";
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The update info for the database layer.
|
|
203
|
+
* This is the update info that is used by the database.
|
|
204
|
+
*/
|
|
205
|
+
export interface UpdateInfo {
|
|
206
|
+
id: string;
|
|
207
|
+
shouldForceUpdate: boolean;
|
|
208
|
+
message: string | null;
|
|
209
|
+
status: UpdateStatus;
|
|
210
|
+
storageUri: string | null;
|
|
211
|
+
fileHash: string | null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The update info for the app layer.
|
|
216
|
+
* This is the update info that is used by the app.
|
|
217
|
+
*/
|
|
218
|
+
export interface AppUpdateInfo extends Omit<UpdateInfo, "storageUri"> {
|
|
219
|
+
fileUrl: string | null;
|
|
220
|
+
/**
|
|
221
|
+
* SHA256 hash of the bundle file, optionally with embedded signature.
|
|
222
|
+
* Format when signed: "sig:<base64_signature>"
|
|
223
|
+
* Format when unsigned: "<hex_hash>" (64-character lowercase hex)
|
|
224
|
+
* The client parses this to extract signature for native verification.
|
|
225
|
+
*/
|
|
226
|
+
fileHash: string | null;
|
|
227
|
+
}
|