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.
Files changed (87) hide show
  1. package/InstantpayCodePush.podspec +20 -0
  2. package/LICENSE +20 -0
  3. package/README.md +158 -0
  4. package/android/build.gradle +91 -0
  5. package/android/gradle.properties +5 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/java/com/instantpaycodepush/BundleFileStorageService.kt +835 -0
  8. package/android/src/main/java/com/instantpaycodepush/BundleMetadata.kt +249 -0
  9. package/android/src/main/java/com/instantpaycodepush/CommonHelper.kt +39 -0
  10. package/android/src/main/java/com/instantpaycodepush/DecompressService.kt +85 -0
  11. package/android/src/main/java/com/instantpaycodepush/DecompressionStrategy.kt +24 -0
  12. package/android/src/main/java/com/instantpaycodepush/FileManagerService.kt +105 -0
  13. package/android/src/main/java/com/instantpaycodepush/HashUtils.kt +50 -0
  14. package/android/src/main/java/com/instantpaycodepush/InstantpayCodePushModule.kt +182 -0
  15. package/android/src/main/java/com/instantpaycodepush/InstantpayCodePushPackage.kt +33 -0
  16. package/android/src/main/java/com/instantpaycodepush/IpayCodePush.kt +101 -0
  17. package/android/src/main/java/com/instantpaycodepush/IpayCodePushException.kt +135 -0
  18. package/android/src/main/java/com/instantpaycodepush/IpayCodePushImpl.kt +329 -0
  19. package/android/src/main/java/com/instantpaycodepush/OkHttpDownloadService.kt +283 -0
  20. package/android/src/main/java/com/instantpaycodepush/ReactIntegrationManager.kt +141 -0
  21. package/android/src/main/java/com/instantpaycodepush/ReactIntegrationManagerBase.kt +35 -0
  22. package/android/src/main/java/com/instantpaycodepush/SignatureVerifier.kt +354 -0
  23. package/android/src/main/java/com/instantpaycodepush/VersionedPreferencesService.kt +70 -0
  24. package/android/src/main/java/com/instantpaycodepush/ZipDecompressionStrategy.kt +198 -0
  25. package/ios/InstantpayCodePush.h +5 -0
  26. package/ios/InstantpayCodePush.mm +21 -0
  27. package/lib/module/DefaultResolver.js +34 -0
  28. package/lib/module/DefaultResolver.js.map +1 -0
  29. package/lib/module/NativeInstantpayCodePush.js +5 -0
  30. package/lib/module/NativeInstantpayCodePush.js.map +1 -0
  31. package/lib/module/checkForUpdate.js +68 -0
  32. package/lib/module/checkForUpdate.js.map +1 -0
  33. package/lib/module/error.js +137 -0
  34. package/lib/module/error.js.map +1 -0
  35. package/lib/module/fetchUpdateInfo.js +36 -0
  36. package/lib/module/fetchUpdateInfo.js.map +1 -0
  37. package/lib/module/global.d.js +8 -0
  38. package/lib/module/global.d.js.map +1 -0
  39. package/lib/module/hooks/useEventCallback.js +13 -0
  40. package/lib/module/hooks/useEventCallback.js.map +1 -0
  41. package/lib/module/index.js +291 -0
  42. package/lib/module/index.js.map +1 -0
  43. package/lib/module/native.js +233 -0
  44. package/lib/module/native.js.map +1 -0
  45. package/lib/module/package.json +1 -0
  46. package/lib/module/store.js +53 -0
  47. package/lib/module/store.js.map +1 -0
  48. package/lib/module/types.js +62 -0
  49. package/lib/module/types.js.map +1 -0
  50. package/lib/module/wrap.js +171 -0
  51. package/lib/module/wrap.js.map +1 -0
  52. package/lib/typescript/package.json +1 -0
  53. package/lib/typescript/src/DefaultResolver.d.ts +10 -0
  54. package/lib/typescript/src/DefaultResolver.d.ts.map +1 -0
  55. package/lib/typescript/src/NativeInstantpayCodePush.d.ts +100 -0
  56. package/lib/typescript/src/NativeInstantpayCodePush.d.ts.map +1 -0
  57. package/lib/typescript/src/checkForUpdate.d.ts +29 -0
  58. package/lib/typescript/src/checkForUpdate.d.ts.map +1 -0
  59. package/lib/typescript/src/error.d.ts +124 -0
  60. package/lib/typescript/src/error.d.ts.map +1 -0
  61. package/lib/typescript/src/fetchUpdateInfo.d.ts +8 -0
  62. package/lib/typescript/src/fetchUpdateInfo.d.ts.map +1 -0
  63. package/lib/typescript/src/hooks/useEventCallback.d.ts +5 -0
  64. package/lib/typescript/src/hooks/useEventCallback.d.ts.map +1 -0
  65. package/lib/typescript/src/index.d.ts +203 -0
  66. package/lib/typescript/src/index.d.ts.map +1 -0
  67. package/lib/typescript/src/native.d.ts +128 -0
  68. package/lib/typescript/src/native.d.ts.map +1 -0
  69. package/lib/typescript/src/store.d.ts +11 -0
  70. package/lib/typescript/src/store.d.ts.map +1 -0
  71. package/lib/typescript/src/types.d.ts +174 -0
  72. package/lib/typescript/src/types.d.ts.map +1 -0
  73. package/lib/typescript/src/wrap.d.ts +179 -0
  74. package/lib/typescript/src/wrap.d.ts.map +1 -0
  75. package/package.json +174 -0
  76. package/src/DefaultResolver.ts +36 -0
  77. package/src/NativeInstantpayCodePush.ts +111 -0
  78. package/src/checkForUpdate.ts +122 -0
  79. package/src/error.ts +159 -0
  80. package/src/fetchUpdateInfo.ts +47 -0
  81. package/src/global.d.ts +23 -0
  82. package/src/hooks/useEventCallback.ts +30 -0
  83. package/src/index.tsx +379 -0
  84. package/src/native.ts +280 -0
  85. package/src/store.ts +69 -0
  86. package/src/types.ts +227 -0
  87. 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
+ }