react-native-update 10.55.1 → 10.56.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-CN.md +35 -0
- package/README.md +37 -0
- package/android/build.gradle +53 -27
- package/android/jni/Application.mk +6 -1
- package/android/jni/hpatch.c +59 -4
- package/android/lib/arm64-v8a/librnupdate.so +0 -0
- package/android/lib/armeabi-v7a/librnupdate.so +0 -0
- package/android/lib/x86/librnupdate.so +0 -0
- package/android/lib/x86_64/librnupdate.so +0 -0
- package/android/proguard.pro +70 -14
- package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +15 -8
- package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +1 -0
- package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +1 -2
- package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +25 -0
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -34
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +29 -0
- package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +15 -0
- package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +59 -0
- package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +27 -1
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +57 -16
- package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +38 -46
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +279 -183
- package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +14 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +222 -215
- package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +5 -0
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +23 -25
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +29 -29
- package/cpp/patch_core/archive_limits.h +6 -0
- package/cpp/patch_core/jni_util.h +6 -2
- package/cpp/patch_core/patch_core.cpp +237 -84
- package/cpp/patch_core/patch_core.h +11 -0
- package/cpp/patch_core/patch_core_android.cpp +49 -33
- package/cpp/patch_core/update_core_android.cpp +134 -103
- package/cpp/update_flow_core/flow_json.cpp +150 -17
- package/cpp/update_flow_core/flow_json.h +13 -1
- package/cpp/update_flow_core/update_flow_core.cpp +78 -8
- package/cpp/update_flow_core/update_flow_core.h +9 -0
- package/cpp/update_flow_core/update_flow_jni.cpp +44 -25
- package/harmony/hvigor-plugin.ts +168 -21
- package/harmony/pushy/src/README.md +74 -0
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +13 -3
- package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +11 -5
- package/harmony/pushy/src/main/cpp/pushy.cpp +103 -18
- package/harmony/pushy/src/main/ets/ArchiveLimits.ts +61 -5
- package/harmony/pushy/src/main/ets/DownloadTask.ts +255 -166
- package/harmony/pushy/src/main/ets/ErrorCodes.ts +75 -0
- package/harmony/pushy/src/main/ets/EventHub.ts +22 -20
- package/harmony/pushy/src/main/ets/InstallRecord.ts +79 -21
- package/harmony/pushy/src/main/ets/Logger.ts +42 -16
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +98 -17
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +10 -3
- package/harmony/pushy/src/main/ets/PathUtils.ts +9 -3
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +139 -53
- package/harmony/pushy/src/main/ets/UpdateContext.ts +269 -159
- package/harmony/pushy/src/main/module.json5 +5 -1
- package/harmony/pushy.har +0 -0
- package/ios/PrivacyInfo.xcprivacy +79 -0
- package/ios/RCTPushy/RCTPushy.mm +399 -170
- package/ios/RCTPushy/RCTPushyDownloader.h +14 -0
- package/ios/RCTPushy/RCTPushyDownloader.mm +136 -11
- package/package.json +14 -24
- package/react-native-update.podspec +36 -3
- package/src/client.ts +236 -82
- package/src/context.ts +40 -27
- package/src/core.ts +41 -16
- package/src/endpoint.ts +2 -1
- package/src/error.ts +49 -11
- package/src/index.ts +14 -0
- package/src/locales/en.ts +1 -23
- package/src/locales/zh.ts +1 -22
- package/src/provider.tsx +159 -111
- package/src/type.ts +0 -2
- package/src/updateFlowCore.ts +10 -2
- package/src/utils.ts +61 -4
- package/android/src/main/java/cn/reactnative/modules/update/ReactNativeHostHandler.java +0 -13
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +0 -45
package/src/provider.tsx
CHANGED
|
@@ -7,14 +7,7 @@ import React, {
|
|
|
7
7
|
useRef,
|
|
8
8
|
useState,
|
|
9
9
|
} from 'react';
|
|
10
|
-
import {
|
|
11
|
-
Alert,
|
|
12
|
-
AppState,
|
|
13
|
-
Linking,
|
|
14
|
-
type NativeEventSubscription,
|
|
15
|
-
Platform,
|
|
16
|
-
} from 'react-native';
|
|
17
|
-
import { URL } from 'react-native-url-polyfill';
|
|
10
|
+
import { Alert, AppState, Linking, Platform } from 'react-native';
|
|
18
11
|
import { type Cresc, type Pushy, sharedState } from './client';
|
|
19
12
|
import { ProgressContext, UpdateContext } from './context';
|
|
20
13
|
import {
|
|
@@ -27,7 +20,7 @@ import {
|
|
|
27
20
|
} from './core';
|
|
28
21
|
import type { CheckResult, ProgressData, UpdateTestPayload } from './type';
|
|
29
22
|
import { decideDownload, resolveCheckResult } from './updateFlowCore';
|
|
30
|
-
import { assertWeb, log, noop } from './utils';
|
|
23
|
+
import { assertWeb, isWeb, log, noop, parseQueryParams } from './utils';
|
|
31
24
|
|
|
32
25
|
export const UpdateProvider = ({
|
|
33
26
|
client,
|
|
@@ -42,24 +35,22 @@ export const UpdateProvider = ({
|
|
|
42
35
|
// A second concurrently mounted provider is a hard integration error (the
|
|
43
36
|
// client is a process-level singleton); the client throws on the second
|
|
44
37
|
// claim. Also releases the claim on unmount.
|
|
45
|
-
useEffect(() => client.claimProviderMount
|
|
38
|
+
useEffect(() => client.claimProviderMount(), [client]);
|
|
46
39
|
|
|
47
40
|
// options is mutated in place by client.setOptions (its identity never
|
|
48
41
|
// changes), so effects keyed on it would never re-run. The client bumps a
|
|
49
42
|
// version on every setOptions; mirroring it into state re-renders this
|
|
50
|
-
// provider
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
);
|
|
43
|
+
// provider, which re-reads the option fields its effects key on (see the
|
|
44
|
+
// lifecycle effects below).
|
|
45
|
+
const [, setOptionsVersion] = useState(client.optionsVersion);
|
|
54
46
|
useEffect(
|
|
55
47
|
() =>
|
|
56
|
-
client.onOptionsChange
|
|
57
|
-
setOptionsVersion(
|
|
48
|
+
client.onOptionsChange(() => {
|
|
49
|
+
setOptionsVersion(client.optionsVersion);
|
|
58
50
|
}),
|
|
59
51
|
[client]
|
|
60
52
|
);
|
|
61
53
|
|
|
62
|
-
const stateListener = useRef<NativeEventSubscription>(undefined);
|
|
63
54
|
const [updateInfo, setUpdateInfo] = useState<CheckResult>();
|
|
64
55
|
const updateInfoRef = useRef(updateInfo);
|
|
65
56
|
const [progress, setProgress] = useState<ProgressData>();
|
|
@@ -198,6 +189,27 @@ export const UpdateProvider = ({
|
|
|
198
189
|
[client]
|
|
199
190
|
);
|
|
200
191
|
|
|
192
|
+
// The action behind an expired package: install the APK in place on
|
|
193
|
+
// Android, hand every other URL to the system. The URL is server-controlled,
|
|
194
|
+
// so a scheme without a handler (or a malformed value) rejects — from an
|
|
195
|
+
// alert button or the silent branch that must not become an unhandled
|
|
196
|
+
// rejection.
|
|
197
|
+
const openExpiredDownload = useCallback(
|
|
198
|
+
(downloadUrl: string) => {
|
|
199
|
+
if (Platform.OS === 'android' && downloadUrl.endsWith('.apk')) {
|
|
200
|
+
downloadAndInstallApk(downloadUrl).catch(noop);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
Promise.resolve()
|
|
204
|
+
.then(() => Linking.openURL(downloadUrl))
|
|
205
|
+
.catch((e: any) => {
|
|
206
|
+
log('openURL failed:', downloadUrl, e?.message || e);
|
|
207
|
+
setLastError(e instanceof Error ? e : new Error(String(e)));
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
[downloadAndInstallApk]
|
|
211
|
+
);
|
|
212
|
+
|
|
201
213
|
const checkUpdate = useCallback(
|
|
202
214
|
async ({ extra }: { extra?: Partial<{ toHash: string }> } = {}) => {
|
|
203
215
|
// No throttle here: the client already dedupes checks via its 5s
|
|
@@ -234,16 +246,27 @@ export const UpdateProvider = ({
|
|
|
234
246
|
log
|
|
235
247
|
);
|
|
236
248
|
if (
|
|
237
|
-
!info.expired &&
|
|
238
249
|
info.update &&
|
|
239
250
|
(typeof info.hash !== 'string' || info.hash.length === 0)
|
|
240
251
|
) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
252
|
+
if (info.expired) {
|
|
253
|
+
// An expired package carries its own action (downloadUrl); a stray
|
|
254
|
+
// `update: true` without a hash is nothing the app could act on, so
|
|
255
|
+
// present the response as expired-only instead of also advertising
|
|
256
|
+
// an update.
|
|
257
|
+
info = { ...info, update: false };
|
|
258
|
+
} else {
|
|
259
|
+
// A malformed rollout/root entry must not produce an alert whose
|
|
260
|
+
// confirm button can never download anything. Surface it to the
|
|
261
|
+
// developer telemetry/logger and present it to the app as no update.
|
|
262
|
+
client.reportInvalidUpdateOnce('missingHash');
|
|
263
|
+
info = { upToDate: true };
|
|
264
|
+
}
|
|
246
265
|
}
|
|
266
|
+
const silentStrategy =
|
|
267
|
+
options.updateStrategy === 'silentAndNow' ||
|
|
268
|
+
options.updateStrategy === 'silentAndLater';
|
|
269
|
+
let unusableRelease: CheckResult | undefined;
|
|
247
270
|
if (info.update && !info.expired) {
|
|
248
271
|
const decision = decideDownload(
|
|
249
272
|
info,
|
|
@@ -252,9 +275,17 @@ export const UpdateProvider = ({
|
|
|
252
275
|
);
|
|
253
276
|
if (decision.action === 'none') {
|
|
254
277
|
if (decision.reason === 'noArtifact') {
|
|
255
|
-
// Invalid server data is worth reporting
|
|
256
|
-
// expected no-ops and stay silent.
|
|
257
|
-
client
|
|
278
|
+
// Invalid server data is worth reporting (local rollout guards are
|
|
279
|
+
// expected no-ops and stay silent) — once per bad release. The
|
|
280
|
+
// client reports it from the download attempt, which the silent
|
|
281
|
+
// strategies make below; the alert strategies never get there
|
|
282
|
+
// (no confirm button is shown for an update that cannot
|
|
283
|
+
// download), so only they report from here.
|
|
284
|
+
if (silentStrategy) {
|
|
285
|
+
unusableRelease = info;
|
|
286
|
+
} else {
|
|
287
|
+
client.reportInvalidUpdateOnce('noArtifact', info.hash || '');
|
|
288
|
+
}
|
|
258
289
|
}
|
|
259
290
|
info = { upToDate: true };
|
|
260
291
|
}
|
|
@@ -264,6 +295,11 @@ export const UpdateProvider = ({
|
|
|
264
295
|
}
|
|
265
296
|
updateInfoRef.current = info;
|
|
266
297
|
setUpdateInfo(info);
|
|
298
|
+
if (unusableRelease) {
|
|
299
|
+
// Resolves false once the client has reported the bad release.
|
|
300
|
+
downloadUpdate(unusableRelease).catch(noop);
|
|
301
|
+
return info;
|
|
302
|
+
}
|
|
267
303
|
if (info.expired) {
|
|
268
304
|
if (
|
|
269
305
|
options.onPackageExpired &&
|
|
@@ -275,39 +311,30 @@ export const UpdateProvider = ({
|
|
|
275
311
|
const { downloadUrl } = info;
|
|
276
312
|
if (downloadUrl && sharedState.apkStatus === null) {
|
|
277
313
|
if (options.updateStrategy === 'silentAndNow') {
|
|
278
|
-
|
|
279
|
-
downloadAndInstallApk(downloadUrl).catch(noop);
|
|
280
|
-
} else {
|
|
281
|
-
Linking.openURL(downloadUrl);
|
|
282
|
-
}
|
|
314
|
+
openExpiredDownload(downloadUrl);
|
|
283
315
|
return info;
|
|
284
316
|
}
|
|
285
317
|
alertUpdate(client.t('alert_title'), client.t('alert_app_updated'), [
|
|
286
318
|
{
|
|
287
319
|
text: client.t('alert_update_button'),
|
|
288
320
|
onPress: () => {
|
|
289
|
-
|
|
290
|
-
downloadAndInstallApk(downloadUrl).catch(noop);
|
|
291
|
-
} else {
|
|
292
|
-
Linking.openURL(downloadUrl);
|
|
293
|
-
}
|
|
321
|
+
openExpiredDownload(downloadUrl);
|
|
294
322
|
},
|
|
295
323
|
},
|
|
296
324
|
]);
|
|
297
325
|
}
|
|
298
326
|
} else if (info.update) {
|
|
299
|
-
if (
|
|
300
|
-
options.updateStrategy === 'silentAndNow' ||
|
|
301
|
-
options.updateStrategy === 'silentAndLater'
|
|
302
|
-
) {
|
|
327
|
+
if (silentStrategy) {
|
|
303
328
|
downloadUpdate(info).catch(noop);
|
|
304
329
|
return info;
|
|
305
330
|
}
|
|
306
331
|
alertUpdate(
|
|
307
332
|
client.t('alert_title'),
|
|
308
333
|
client.t('alert_new_version_found', {
|
|
309
|
-
name
|
|
310
|
-
|
|
334
|
+
// A server that omits the name must not leave a literal
|
|
335
|
+
// `{{name}}` in the alert.
|
|
336
|
+
name: info.name || '',
|
|
337
|
+
description: info.description || '',
|
|
311
338
|
}),
|
|
312
339
|
[
|
|
313
340
|
{ text: client.t('alert_cancel'), style: 'cancel' },
|
|
@@ -328,78 +355,105 @@ export const UpdateProvider = ({
|
|
|
328
355
|
options,
|
|
329
356
|
alertUpdate,
|
|
330
357
|
alertError,
|
|
331
|
-
|
|
358
|
+
openExpiredDownload,
|
|
332
359
|
downloadUpdate,
|
|
333
360
|
]
|
|
334
361
|
);
|
|
335
362
|
|
|
336
363
|
const markSuccess = client.markSuccess;
|
|
337
364
|
|
|
338
|
-
//
|
|
365
|
+
// The lifecycle effects below key on the primitive option values they read
|
|
366
|
+
// (re-read on every render, i.e. after every setOptions), so a setOptions
|
|
367
|
+
// that touches something else — a logger, disableErrorReporting — no longer
|
|
368
|
+
// re-runs a start check, resets the auto-mark timer or re-subscribes
|
|
369
|
+
// AppState. checkUpdate's identity follows the strategy options; the
|
|
370
|
+
// listener and the start check reach it through this ref instead of
|
|
371
|
+
// depending on it.
|
|
372
|
+
const { checkStrategy, autoMarkSuccess, autoMarkSuccessDelayMs } = options;
|
|
373
|
+
const latestCheckUpdate = useRef(checkUpdate);
|
|
339
374
|
useEffect(() => {
|
|
340
|
-
|
|
375
|
+
latestCheckUpdate.current = checkUpdate;
|
|
376
|
+
}, [checkUpdate]);
|
|
377
|
+
|
|
378
|
+
// (a) Foreground checks: one AppState subscription for as long as the
|
|
379
|
+
// strategy asks for it.
|
|
380
|
+
useEffect(() => {
|
|
381
|
+
if (
|
|
382
|
+
isWeb ||
|
|
383
|
+
(checkStrategy !== 'both' && checkStrategy !== 'onAppResume') ||
|
|
384
|
+
!client.assertDebug('checkUpdate()')
|
|
385
|
+
) {
|
|
341
386
|
return;
|
|
342
387
|
}
|
|
343
|
-
|
|
388
|
+
const subscription = AppState.addEventListener('change', (nextAppState) => {
|
|
389
|
+
if (nextAppState === 'active') {
|
|
390
|
+
latestCheckUpdate.current().catch(noop);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
return () => {
|
|
394
|
+
subscription.remove();
|
|
395
|
+
};
|
|
396
|
+
}, [client, checkStrategy]);
|
|
397
|
+
|
|
398
|
+
// (b) Automatic markSuccess: armed once per (autoMarkSuccess, delay); an
|
|
399
|
+
// unrelated setOptions must not push the health confirmation out again.
|
|
400
|
+
useEffect(() => {
|
|
401
|
+
if (isWeb || !autoMarkSuccess || !client.assertDebug('markSuccess()')) {
|
|
344
402
|
return;
|
|
345
403
|
}
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
healthy = (await healthCheck()) !== false;
|
|
363
|
-
} catch (e: any) {
|
|
364
|
-
log('healthCheck threw, not marking success:', e?.message || e);
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
if (!healthy) {
|
|
368
|
-
log('healthCheck returned false, not marking success');
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
404
|
+
const delay =
|
|
405
|
+
typeof autoMarkSuccessDelayMs === 'number' && autoMarkSuccessDelayMs >= 0
|
|
406
|
+
? autoMarkSuccessDelayMs
|
|
407
|
+
: 1000;
|
|
408
|
+
const markSuccessTimer = setTimeout(() => {
|
|
409
|
+
// The health check is read when the timer fires, not when it was
|
|
410
|
+
// armed, so a late setOptions still applies.
|
|
411
|
+
const { healthCheck } = client.options;
|
|
412
|
+
(async () => {
|
|
413
|
+
if (healthCheck) {
|
|
414
|
+
let healthy = false;
|
|
415
|
+
try {
|
|
416
|
+
healthy = (await healthCheck()) !== false;
|
|
417
|
+
} catch (e: any) {
|
|
418
|
+
log('healthCheck threw, not marking success:', e?.message || e);
|
|
419
|
+
return;
|
|
371
420
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}, delay);
|
|
376
|
-
}
|
|
377
|
-
if (checkStrategy === 'both' || checkStrategy === 'onAppResume') {
|
|
378
|
-
stateListener.current = AppState.addEventListener(
|
|
379
|
-
'change',
|
|
380
|
-
(nextAppState) => {
|
|
381
|
-
if (nextAppState === 'active') {
|
|
382
|
-
checkUpdate().catch(noop);
|
|
421
|
+
if (!healthy) {
|
|
422
|
+
log('healthCheck returned false, not marking success');
|
|
423
|
+
return;
|
|
383
424
|
}
|
|
384
425
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
426
|
+
// Failures are reported and surfaced via the onError subscription.
|
|
427
|
+
await client.markSuccess();
|
|
428
|
+
})().catch(noop);
|
|
429
|
+
}, delay);
|
|
390
430
|
return () => {
|
|
391
|
-
|
|
392
|
-
clearTimeout(markSuccessTimer);
|
|
393
|
-
}
|
|
394
|
-
stateListener.current?.remove();
|
|
431
|
+
clearTimeout(markSuccessTimer);
|
|
395
432
|
};
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
433
|
+
}, [client, autoMarkSuccess, autoMarkSuccessDelayMs]);
|
|
434
|
+
|
|
435
|
+
// (c) The start-of-life check runs once per mount, never again because of a
|
|
436
|
+
// setOptions. It still respects the strategy in force at mount: an app that
|
|
437
|
+
// mounts without automatic checks and enables them later gets its one
|
|
438
|
+
// start check at that point.
|
|
439
|
+
const startCheckDone = useRef(false);
|
|
440
|
+
useEffect(() => {
|
|
441
|
+
if (
|
|
442
|
+
startCheckDone.current ||
|
|
443
|
+
(checkStrategy !== 'both' && checkStrategy !== 'onAppStart') ||
|
|
444
|
+
!client.assertDebug('checkUpdate()') ||
|
|
445
|
+
!assertWeb()
|
|
446
|
+
) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
startCheckDone.current = true;
|
|
450
|
+
latestCheckUpdate.current().catch(noop);
|
|
451
|
+
}, [client, checkStrategy]);
|
|
399
452
|
|
|
400
|
-
//
|
|
453
|
+
// A dismissErrorAfter change reschedules a running timer, not only the next
|
|
454
|
+
// error's.
|
|
455
|
+
const { dismissErrorAfter } = options;
|
|
401
456
|
useEffect(() => {
|
|
402
|
-
const { dismissErrorAfter } = options;
|
|
403
457
|
if (
|
|
404
458
|
lastError &&
|
|
405
459
|
typeof dismissErrorAfter === 'number' &&
|
|
@@ -412,9 +466,7 @@ export const UpdateProvider = ({
|
|
|
412
466
|
clearTimeout(dismissErrorTimer);
|
|
413
467
|
};
|
|
414
468
|
}
|
|
415
|
-
|
|
416
|
-
// timer, not only apply to the next error.
|
|
417
|
-
}, [lastError, options, dismissError, optionsVersion]);
|
|
469
|
+
}, [lastError, dismissErrorAfter, dismissError]);
|
|
418
470
|
|
|
419
471
|
const parseTestPayload = useCallback(
|
|
420
472
|
(payload: UpdateTestPayload) => {
|
|
@@ -496,18 +548,14 @@ export const UpdateProvider = ({
|
|
|
496
548
|
if (!url) {
|
|
497
549
|
return;
|
|
498
550
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
}
|
|
507
|
-
// A malformed deep link (new URL throws) must not become an
|
|
508
|
-
// unhandled rejection / a throw inside the 'url' event handler.
|
|
509
|
-
log('parseLinking: invalid url', e?.message || e);
|
|
510
|
-
}
|
|
551
|
+
// A plain query-string parse (never throws, see parseQueryParams): a
|
|
552
|
+
// malformed deep link must not become an unhandled rejection / a throw
|
|
553
|
+
// inside the 'url' event handler.
|
|
554
|
+
const params = parseQueryParams(url);
|
|
555
|
+
parseTestPayload({
|
|
556
|
+
type: params.type ?? null,
|
|
557
|
+
data: params.data ?? null,
|
|
558
|
+
});
|
|
511
559
|
};
|
|
512
560
|
|
|
513
561
|
Linking.getInitialURL().then(parseLinking).catch(noop);
|
package/src/type.ts
CHANGED
package/src/updateFlowCore.ts
CHANGED
|
@@ -355,7 +355,11 @@ export interface DownloadPlan {
|
|
|
355
355
|
hash: string;
|
|
356
356
|
/** Ordered attempts: incremental first, full as the last resort. */
|
|
357
357
|
attempts: DownloadAttempt[];
|
|
358
|
-
/**
|
|
358
|
+
/**
|
|
359
|
+
* Dev-only: nothing to fetch. The executor logs why and delivers nothing
|
|
360
|
+
* (resolves undefined) — not a fake success whose hash could never be
|
|
361
|
+
* switched to.
|
|
362
|
+
*/
|
|
359
363
|
devNoop: boolean;
|
|
360
364
|
}
|
|
361
365
|
|
|
@@ -371,7 +375,11 @@ export function decideDownload(
|
|
|
371
375
|
identity: { currentVersion?: string; rolledBackVersion?: string },
|
|
372
376
|
isDev = false
|
|
373
377
|
): DownloadDecision {
|
|
374
|
-
const { hash, diff, pdiff, full
|
|
378
|
+
const { hash, diff, pdiff, full } = info;
|
|
379
|
+
// A server `paths: null` behaves like an absent field (a destructuring
|
|
380
|
+
// default only covers undefined, and `null.map` would throw). The C++ port
|
|
381
|
+
// treats null and undefined alike here.
|
|
382
|
+
const paths = info.paths ?? [];
|
|
375
383
|
if (!info.update || !hash) {
|
|
376
384
|
return { action: 'none', reason: 'noUpdate' };
|
|
377
385
|
}
|
package/src/utils.ts
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import { Platform } from 'react-native';
|
|
2
2
|
import i18n from './i18n';
|
|
3
3
|
|
|
4
|
+
// log/info are developer diagnostics (they include full check request bodies
|
|
5
|
+
// and responses); release builds only print them when the client's `debug`
|
|
6
|
+
// option is on. warn/error always print.
|
|
7
|
+
let debugLogging = false;
|
|
8
|
+
export const setDebugLogging = (enabled: boolean) => {
|
|
9
|
+
debugLogging = !!enabled;
|
|
10
|
+
};
|
|
11
|
+
const isVerbose = () =>
|
|
12
|
+
// Metro always defines __DEV__; guard anyway so a bundler that does not
|
|
13
|
+
// cannot break the whole module at import time.
|
|
14
|
+
(typeof __DEV__ === 'boolean' && __DEV__) || debugLogging;
|
|
15
|
+
|
|
4
16
|
export function log(...args: any[]) {
|
|
5
|
-
|
|
17
|
+
if (isVerbose()) {
|
|
18
|
+
console.log(i18n.t('dev_log_prefix'), ...args);
|
|
19
|
+
}
|
|
6
20
|
}
|
|
7
21
|
|
|
8
22
|
export function info(...args: any[]) {
|
|
9
|
-
|
|
23
|
+
if (isVerbose()) {
|
|
24
|
+
console.info(i18n.t('dev_log_prefix'), ...args);
|
|
25
|
+
}
|
|
10
26
|
}
|
|
11
27
|
|
|
12
28
|
export function warn(...args: any[]) {
|
|
@@ -41,7 +57,6 @@ export function promiseAny<T>(promises: Promise<T>[]) {
|
|
|
41
57
|
});
|
|
42
58
|
}
|
|
43
59
|
|
|
44
|
-
export const emptyObj = {};
|
|
45
60
|
export const noop = () => {};
|
|
46
61
|
const emptyModuleTarget: Record<string, typeof noop> = {};
|
|
47
62
|
export const emptyModule = new Proxy(emptyModuleTarget, {
|
|
@@ -147,10 +162,11 @@ export const fetchWithTimeout = (
|
|
|
147
162
|
// caller-provided signal (e.g. the hedged endpoint race cancelling losers)
|
|
148
163
|
// must be chained onto it manually.
|
|
149
164
|
const externalSignal = (params as any)?.signal as AbortSignal | undefined;
|
|
165
|
+
const onExternalAbort = () => controller.abort();
|
|
150
166
|
if (externalSignal?.aborted) {
|
|
151
167
|
controller.abort();
|
|
152
168
|
} else if (externalSignal) {
|
|
153
|
-
externalSignal.addEventListener('abort',
|
|
169
|
+
externalSignal.addEventListener('abort', onExternalAbort);
|
|
154
170
|
}
|
|
155
171
|
const timeoutId = setTimeout(() => {
|
|
156
172
|
log('fetch timeout', url);
|
|
@@ -170,9 +186,50 @@ export const fetchWithTimeout = (
|
|
|
170
186
|
})
|
|
171
187
|
.finally(() => {
|
|
172
188
|
clearTimeout(timeoutId);
|
|
189
|
+
// A long-lived caller signal (one per endpoint race) would otherwise
|
|
190
|
+
// keep every finished request's listener — and its controller — alive.
|
|
191
|
+
externalSignal?.removeEventListener('abort', onExternalAbort);
|
|
173
192
|
});
|
|
174
193
|
};
|
|
175
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Query parameters of a URL as a plain object, without a URL polyfill: the
|
|
197
|
+
* only consumer is the test-channel deep link (`?type=...&data=...`). Handles
|
|
198
|
+
* a missing `?`, a `#fragment`, `+` as space and percent-encoding; a value
|
|
199
|
+
* that fails to decode is kept verbatim rather than throwing. Later
|
|
200
|
+
* duplicates win.
|
|
201
|
+
*/
|
|
202
|
+
export const parseQueryParams = (url: string): Record<string, string> => {
|
|
203
|
+
const params: Record<string, string> = {};
|
|
204
|
+
const hashStart = url.indexOf('#');
|
|
205
|
+
const withoutFragment = hashStart < 0 ? url : url.slice(0, hashStart);
|
|
206
|
+
const queryStart = withoutFragment.indexOf('?');
|
|
207
|
+
if (queryStart < 0) {
|
|
208
|
+
return params;
|
|
209
|
+
}
|
|
210
|
+
const query = withoutFragment.slice(queryStart + 1);
|
|
211
|
+
const decode = (value: string) => {
|
|
212
|
+
const spaced = value.replace(/\+/g, ' ');
|
|
213
|
+
try {
|
|
214
|
+
return decodeURIComponent(spaced);
|
|
215
|
+
} catch {
|
|
216
|
+
return spaced;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
for (const pair of query.split('&')) {
|
|
220
|
+
if (!pair) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const separator = pair.indexOf('=');
|
|
224
|
+
const key = decode(separator < 0 ? pair : pair.slice(0, separator));
|
|
225
|
+
if (!key) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
params[key] = separator < 0 ? '' : decode(pair.slice(separator + 1));
|
|
229
|
+
}
|
|
230
|
+
return params;
|
|
231
|
+
};
|
|
232
|
+
|
|
176
233
|
/**
|
|
177
234
|
* True when `finalUrl` (after redirects) is plaintext http while the request
|
|
178
235
|
* started on https. An https endpoint or artifact URL must never be served
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
package cn.reactnative.modules.update;
|
|
2
|
-
|
|
3
|
-
import androidx.annotation.Nullable;
|
|
4
|
-
|
|
5
|
-
public interface ReactNativeHostHandler {
|
|
6
|
-
@Nullable
|
|
7
|
-
String getJSBundleFile(boolean useDeveloperSupport);
|
|
8
|
-
|
|
9
|
-
@Nullable
|
|
10
|
-
String getBundleAssetName(boolean useDeveloperSupport);
|
|
11
|
-
|
|
12
|
-
void onWillCreateReactInstance(boolean useDeveloperSupport);
|
|
13
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
package cn.reactnative.modules.update;
|
|
2
|
-
|
|
3
|
-
import android.util.Log;
|
|
4
|
-
import java.util.HashMap;
|
|
5
|
-
import java.util.Map;
|
|
6
|
-
|
|
7
|
-
final class UpdateModuleSupport {
|
|
8
|
-
private UpdateModuleSupport() {
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
static Map<String, Object> getConstants(UpdateContext updateContext) {
|
|
12
|
-
final Map<String, Object> constants = new HashMap<String, Object>();
|
|
13
|
-
constants.put("downloadRootDir", updateContext.getRootDir());
|
|
14
|
-
constants.put("packageVersion", updateContext.getPackageVersion());
|
|
15
|
-
|
|
16
|
-
String currentVersion = updateContext.getCurrentVersion();
|
|
17
|
-
constants.put("currentVersion", currentVersion);
|
|
18
|
-
constants.put("currentVersionInfo", updateContext.getKv("hash_" + currentVersion));
|
|
19
|
-
constants.put("currentBundleSha256", updateContext.currentBundleSha256(currentVersion));
|
|
20
|
-
constants.put("buildTime", updateContext.getBuildTime());
|
|
21
|
-
constants.put("isUsingBundleUrl", updateContext.getIsUsingBundleUrl());
|
|
22
|
-
|
|
23
|
-
boolean isFirstTime = updateContext.consumeFirstLoadMarker();
|
|
24
|
-
constants.put("isFirstTime", isFirstTime);
|
|
25
|
-
|
|
26
|
-
String rolledBackVersion = updateContext.rolledBackVersion();
|
|
27
|
-
constants.put("rolledBackVersion", rolledBackVersion);
|
|
28
|
-
if (rolledBackVersion != null) {
|
|
29
|
-
updateContext.clearRollbackMark();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
constants.put("uuid", updateContext.getKv("uuid"));
|
|
33
|
-
int supportedDiffVersion = 0;
|
|
34
|
-
try {
|
|
35
|
-
supportedDiffVersion = NativeUpdateCore.supportedDiffVersion();
|
|
36
|
-
} catch (UnsatisfiedLinkError e) {
|
|
37
|
-
// A mismatched librnupdate.so (stale manual copy / build cache)
|
|
38
|
-
// must not crash startup via getConstants; 0 simply means "no v2
|
|
39
|
-
// diff track" and the server degrades gracefully.
|
|
40
|
-
Log.e("pushy", "supportedDiffVersion missing from librnupdate.so", e);
|
|
41
|
-
}
|
|
42
|
-
constants.put("supportedDiffVersion", supportedDiffVersion);
|
|
43
|
-
return constants;
|
|
44
|
-
}
|
|
45
|
-
}
|