react-native-update 10.58.1 → 10.59.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 +7 -0
- package/README.md +8 -0
- package/android/src/main/java/cn/reactnative/modules/update/{NativeUpdateResult.java → BundlePreparationResult.java} +6 -6
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +28 -28
- package/android/src/main/java/cn/reactnative/modules/update/{NativeUpdateConfig.java → PushyConfiguration.java} +3 -3
- package/android/src/main/java/cn/reactnative/modules/update/{PushyNativeUpdate.java → PushyRuntime.java} +12 -12
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +1 -1
- package/harmony/pushy/src/main/ets/{NativeUpdateResult.ts → BundlePreparationResult.ts} +6 -6
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +39 -39
- package/harmony/pushy/src/main/ets/{NativeUpdateConfig.ts → PushyConfiguration.ts} +4 -4
- package/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets +8 -8
- package/harmony/pushy/src/main/ets/UpdateContext.ts +1 -1
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.h +8 -8
- package/ios/RCTPushy/RCTPushy.mm +7 -7
- package/ios/RCTPushy/{RCTPushyNativeConfig.h → RCTPushyConfiguration.h} +1 -1
- package/ios/RCTPushy/{RCTPushyNativeConfig.mm → RCTPushyConfiguration.mm} +2 -2
- package/package.json +1 -1
package/README-CN.md
CHANGED
|
@@ -172,3 +172,10 @@ Hermes 字节码对通用二进制 diff 极不友好,我们在两个环节同
|
|
|
172
172
|
本组件由[React Native 中文网](https://reactnative.cn/)独家发布,如有定制需求可以[联系我们](https://reactnative.cn/about.html#content)。
|
|
173
173
|
|
|
174
174
|
关于此插件发现任何问题,可以前往[Issues](https://github.com/reactnativecn/react-native-update/issues)发帖提问。
|
|
175
|
+
|
|
176
|
+
## 原生宿主接口
|
|
177
|
+
|
|
178
|
+
原生宿主入口统一为 `prepareBundle`:Android 使用 `PushyRuntime`,
|
|
179
|
+
Apple 平台使用 `RCTPushy`,Harmony 使用已有的 `PushyFileJSBundleProvider`。
|
|
180
|
+
请在正常启动 bundle 解析后调用。改名不改变 JS 桥接契约,但原生接入代码需要迁移并重新构建。
|
|
181
|
+
详见[原生接口迁移说明](NATIVE_HOST_API_MIGRATION.md)。
|
package/README.md
CHANGED
|
@@ -153,3 +153,11 @@ Since 10.52.1 every cold start runs one background update check that **does not
|
|
|
153
153
|
| **Technical Support** | ✅ Paid dedicated support | ⚠️ Community support | ❌ **Discontinued** |
|
|
154
154
|
| **Server Deployment** | ✅ Hosted service or paid private deployment | ✅ Hosted by Expo (EAS Update) | ❌ **Discontinued** |
|
|
155
155
|
| **Bandwidth Usage** | ⭐⭐⭐⭐⭐ Very low (incremental) | ⭐⭐⭐ Higher (full bundle) | ❌ **Discontinued** |
|
|
156
|
+
|
|
157
|
+
## Native host APIs
|
|
158
|
+
|
|
159
|
+
Bridge-free hosts use `PushyRuntime.prepareBundle` (Android),
|
|
160
|
+
`RCTPushy.prepareBundle` (Swift), or `PushyFileJSBundleProvider.prepareBundle`
|
|
161
|
+
(HarmonyOS), after normal launch bundle resolution. See the
|
|
162
|
+
[native host API migration guide](NATIVE_HOST_API_MIGRATION.md) for the
|
|
163
|
+
source-breaking rename, unchanged JS bridge contract, and native rebuild steps.
|
|
@@ -4,7 +4,7 @@ package cn.reactnative.modules.update;
|
|
|
4
4
|
* Snapshot of a native check-and-update round. A downloaded result does not
|
|
5
5
|
* mean that the running React Native instance has been reloaded.
|
|
6
6
|
*/
|
|
7
|
-
public final class
|
|
7
|
+
public final class BundlePreparationResult {
|
|
8
8
|
public static final String SKIPPED = "skipped";
|
|
9
9
|
public static final String NO_UPDATE = "noUpdate";
|
|
10
10
|
public static final String DOWNLOADED = "downloaded";
|
|
@@ -16,19 +16,19 @@ public final class NativeUpdateResult {
|
|
|
16
16
|
private final String hash;
|
|
17
17
|
private final boolean activated;
|
|
18
18
|
|
|
19
|
-
private
|
|
19
|
+
private BundlePreparationResult(String status, String reason, String hash, boolean activated) {
|
|
20
20
|
this.status = status;
|
|
21
21
|
this.reason = reason;
|
|
22
22
|
this.hash = hash;
|
|
23
23
|
this.activated = activated;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
static
|
|
27
|
-
return new
|
|
26
|
+
static BundlePreparationResult of(String status, String reason) {
|
|
27
|
+
return new BundlePreparationResult(status, reason, "", false);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
static
|
|
31
|
-
return new
|
|
30
|
+
static BundlePreparationResult downloaded(String hash, boolean activated) {
|
|
31
|
+
return new BundlePreparationResult(DOWNLOADED, "", hash, activated);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
public String getStatus() {
|
|
@@ -73,46 +73,46 @@ final class NativeCheckOrchestrator {
|
|
|
73
73
|
private static volatile String sJsCompletedConfig;
|
|
74
74
|
// Published after the launch rollback snapshot, before host calls are accepted.
|
|
75
75
|
private static volatile boolean nativeReady;
|
|
76
|
-
private static volatile
|
|
77
|
-
|
|
76
|
+
private static volatile BundlePreparationResult roundResult =
|
|
77
|
+
BundlePreparationResult.of(BundlePreparationResult.FAILED, "check_failed");
|
|
78
78
|
private static volatile long roundGeneration = -1;
|
|
79
79
|
private static volatile long roundConfigGeneration = -1;
|
|
80
80
|
private static volatile String roundConfigJson;
|
|
81
81
|
|
|
82
82
|
/** Blocking only on the host API's worker; never call on the UI thread. */
|
|
83
|
-
static
|
|
83
|
+
static BundlePreparationResult prepareBundle(UpdateContext context) throws InterruptedException {
|
|
84
84
|
if (UpdateContext.DEBUG) {
|
|
85
|
-
return
|
|
85
|
+
return BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "debug");
|
|
86
86
|
}
|
|
87
87
|
if (!nativeReady || sContext != context || !context.getIsUsingBundleUrl()) {
|
|
88
|
-
return
|
|
88
|
+
return BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "not_initialized");
|
|
89
89
|
}
|
|
90
90
|
String configJson = context.getKv(KEY_CONFIG);
|
|
91
91
|
if (configJson == null || configJson.isEmpty()) {
|
|
92
|
-
return
|
|
92
|
+
return BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "not_configured");
|
|
93
93
|
}
|
|
94
94
|
try {
|
|
95
95
|
JSONObject config = new JSONObject(configJson);
|
|
96
96
|
if (config.optBoolean("disabled", false)) {
|
|
97
|
-
return
|
|
97
|
+
return BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "disabled");
|
|
98
98
|
}
|
|
99
99
|
if (config.optString("appKey", "").isEmpty()) {
|
|
100
|
-
return
|
|
100
|
+
return BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_config");
|
|
101
101
|
}
|
|
102
102
|
} catch (JSONException e) {
|
|
103
|
-
return
|
|
103
|
+
return BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_config");
|
|
104
104
|
}
|
|
105
105
|
startRound(0);
|
|
106
106
|
if (!roundStarted.get()) {
|
|
107
|
-
return
|
|
107
|
+
return BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "config_changed");
|
|
108
108
|
}
|
|
109
109
|
roundDone.await();
|
|
110
110
|
if (roundConfigGeneration != UpdateContext.getNativeConfigGeneration()
|
|
111
111
|
|| !configJson.equals(roundConfigJson) || !configJson.equals(context.getKv(KEY_CONFIG))) {
|
|
112
|
-
return
|
|
112
|
+
return BundlePreparationResult.of(BundlePreparationResult.CANCELLED, "config_changed");
|
|
113
113
|
}
|
|
114
114
|
if (roundGeneration != UpdateContext.getResetGeneration()) {
|
|
115
|
-
return
|
|
115
|
+
return BundlePreparationResult.of(BundlePreparationResult.CANCELLED, "reset");
|
|
116
116
|
}
|
|
117
117
|
return roundResult;
|
|
118
118
|
}
|
|
@@ -226,7 +226,7 @@ final class NativeCheckOrchestrator {
|
|
|
226
226
|
runOnce(sContext, sLaunchRolledBackVersion, deadlineNanos);
|
|
227
227
|
} catch (Throwable e) {
|
|
228
228
|
Log.w(UpdateContext.TAG, "native check failed: " + e);
|
|
229
|
-
roundResult =
|
|
229
|
+
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "internal_error");
|
|
230
230
|
} finally {
|
|
231
231
|
roundCompleted = true;
|
|
232
232
|
roundDone.countDown();
|
|
@@ -303,27 +303,27 @@ final class NativeCheckOrchestrator {
|
|
|
303
303
|
final long resetGeneration = UpdateContext.getResetGeneration();
|
|
304
304
|
roundGeneration = resetGeneration;
|
|
305
305
|
roundConfigGeneration = UpdateContext.getNativeConfigGeneration();
|
|
306
|
-
roundResult =
|
|
306
|
+
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "check_failed");
|
|
307
307
|
String configJson = context.getKv(KEY_CONFIG);
|
|
308
308
|
roundConfigJson = configJson;
|
|
309
309
|
if (configJson == null || configJson.isEmpty()) {
|
|
310
|
-
roundResult =
|
|
310
|
+
roundResult = BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "not_configured");
|
|
311
311
|
return;
|
|
312
312
|
}
|
|
313
313
|
JSONObject config;
|
|
314
314
|
try {
|
|
315
315
|
config = new JSONObject(configJson);
|
|
316
316
|
} catch (JSONException e) {
|
|
317
|
-
roundResult =
|
|
317
|
+
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_config");
|
|
318
318
|
return;
|
|
319
319
|
}
|
|
320
320
|
if (config.optBoolean("disabled", false)) {
|
|
321
|
-
roundResult =
|
|
321
|
+
roundResult = BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "disabled");
|
|
322
322
|
return;
|
|
323
323
|
}
|
|
324
324
|
String appKey = config.optString("appKey", "");
|
|
325
325
|
if (appKey.isEmpty()) {
|
|
326
|
-
roundResult =
|
|
326
|
+
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_config");
|
|
327
327
|
return;
|
|
328
328
|
}
|
|
329
329
|
// Keep the existing interrupted-round breadcrumb and reset generation.
|
|
@@ -401,7 +401,7 @@ final class NativeCheckOrchestrator {
|
|
|
401
401
|
|
|
402
402
|
String body = NativeUpdateFlow.buildCheckRequestBody(input.toString());
|
|
403
403
|
if (body == null) {
|
|
404
|
-
roundResult =
|
|
404
|
+
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_request");
|
|
405
405
|
return;
|
|
406
406
|
}
|
|
407
407
|
|
|
@@ -418,7 +418,7 @@ final class NativeCheckOrchestrator {
|
|
|
418
418
|
String decisionJson = NativeUpdateFlow.handleCheckResponse(
|
|
419
419
|
responseText, identity.toString(), config.optString("afterDownload", ""));
|
|
420
420
|
if (decisionJson == null) {
|
|
421
|
-
roundResult =
|
|
421
|
+
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_response");
|
|
422
422
|
return;
|
|
423
423
|
}
|
|
424
424
|
JSONObject decision = new JSONObject(decisionJson);
|
|
@@ -427,15 +427,15 @@ final class NativeCheckOrchestrator {
|
|
|
427
427
|
resetGeneration, null, null, false,
|
|
428
428
|
buildResponseCacheJson(configJson, body, responseText, responseAtSeconds));
|
|
429
429
|
roundResult = committed
|
|
430
|
-
?
|
|
431
|
-
:
|
|
430
|
+
? BundlePreparationResult.of(BundlePreparationResult.NO_UPDATE, decision.optString("reason"))
|
|
431
|
+
: BundlePreparationResult.of(BundlePreparationResult.CANCELLED, "reset");
|
|
432
432
|
Log.i(UpdateContext.TAG,
|
|
433
433
|
"native check: nothing to do (" + decision.optString("reason") + ")");
|
|
434
434
|
return;
|
|
435
435
|
}
|
|
436
436
|
String hash = decision.optString("hash", "");
|
|
437
437
|
if (!UpdateFileUtils.isSafePathComponent(hash)) {
|
|
438
|
-
roundResult =
|
|
438
|
+
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_response");
|
|
439
439
|
return;
|
|
440
440
|
}
|
|
441
441
|
|
|
@@ -451,8 +451,8 @@ final class NativeCheckOrchestrator {
|
|
|
451
451
|
boolean committed = context.commitNativeCheckResult(
|
|
452
452
|
resetGeneration, null, null, false,
|
|
453
453
|
buildResponseCacheJson(configJson, body, responseText, responseAtSeconds));
|
|
454
|
-
roundResult =
|
|
455
|
-
committed ?
|
|
454
|
+
roundResult = BundlePreparationResult.of(
|
|
455
|
+
committed ? BundlePreparationResult.FAILED : BundlePreparationResult.CANCELLED,
|
|
456
456
|
committed ? "download_failed" : "reset");
|
|
457
457
|
return;
|
|
458
458
|
}
|
|
@@ -499,7 +499,7 @@ final class NativeCheckOrchestrator {
|
|
|
499
499
|
buildResponseCacheJson(configJson, body, responseText, responseAtSeconds));
|
|
500
500
|
} catch (Exception e) {
|
|
501
501
|
Log.w(UpdateContext.TAG, "native check: commit failed: " + e);
|
|
502
|
-
roundResult =
|
|
502
|
+
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "commit_failed");
|
|
503
503
|
return;
|
|
504
504
|
}
|
|
505
505
|
if (!committed) {
|
|
@@ -517,8 +517,8 @@ final class NativeCheckOrchestrator {
|
|
|
517
517
|
"native check: downloaded " + hash + ", activation left to JS");
|
|
518
518
|
}
|
|
519
519
|
roundResult = committed
|
|
520
|
-
?
|
|
521
|
-
:
|
|
520
|
+
? BundlePreparationResult.downloaded(hash, activate)
|
|
521
|
+
: BundlePreparationResult.of(BundlePreparationResult.CANCELLED, "reset");
|
|
522
522
|
}
|
|
523
523
|
|
|
524
524
|
private static String buildResponseCacheJson(
|
|
@@ -9,8 +9,8 @@ import org.json.JSONArray;
|
|
|
9
9
|
import org.json.JSONException;
|
|
10
10
|
import org.json.JSONObject;
|
|
11
11
|
|
|
12
|
-
/** Internal normalizer for the JSONObject accepted by
|
|
13
|
-
final class
|
|
12
|
+
/** Internal normalizer for the JSONObject accepted by PushyRuntime.configure. */
|
|
13
|
+
final class PushyConfiguration {
|
|
14
14
|
private static final Set<String> KEYS = new HashSet<>(Arrays.asList(
|
|
15
15
|
"appKey", "endpoints", "queryUrls", "afterDownload", "disabled",
|
|
16
16
|
"packageVersion", "rnu", "rn"));
|
|
@@ -24,7 +24,7 @@ final class NativeUpdateConfig {
|
|
|
24
24
|
"https://cdn.jsdelivr.net/gh/reactnativecn/react-native-update@master/endpoints.json"
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
private
|
|
27
|
+
private PushyConfiguration() {}
|
|
28
28
|
|
|
29
29
|
static String normalize(String json) throws JSONException {
|
|
30
30
|
JSONObject options = new JSONObject(json);
|
|
@@ -11,10 +11,10 @@ import java.util.concurrent.Executors;
|
|
|
11
11
|
import java.util.concurrent.ThreadFactory;
|
|
12
12
|
|
|
13
13
|
/** Bridge-free native configuration and update APIs. */
|
|
14
|
-
public final class
|
|
14
|
+
public final class PushyRuntime {
|
|
15
15
|
public interface Callback {
|
|
16
16
|
/** Always called on the main thread, including skipped and failed checks. */
|
|
17
|
-
void onComplete(
|
|
17
|
+
void onComplete(BundlePreparationResult result);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// A single waiting worker, not one thread per caller. The actual round is
|
|
@@ -46,7 +46,7 @@ public final class PushyNativeUpdate {
|
|
|
46
46
|
/**
|
|
47
47
|
* Validate and persist a complete configuration, even before JS or bundle
|
|
48
48
|
* resolution. This starts no network work and never resolves a bundle.
|
|
49
|
-
* Await the callback before continuing startup/
|
|
49
|
+
* Await the callback before continuing startup/prepareBundle. Unless JS
|
|
50
50
|
* uses nativeConfigSource: 'native', later JS config writes can replace it.
|
|
51
51
|
*/
|
|
52
52
|
public static void configure(Context context, JSONObject options, final ConfigurationCallback callback) {
|
|
@@ -61,7 +61,7 @@ public final class PushyNativeUpdate {
|
|
|
61
61
|
public void run() {
|
|
62
62
|
Exception failure = null;
|
|
63
63
|
try {
|
|
64
|
-
String config =
|
|
64
|
+
String config = PushyConfiguration.normalize(snapshot);
|
|
65
65
|
UpdateContext.getInstance(applicationContext).setNativeConfig(config);
|
|
66
66
|
} catch (Exception e) {
|
|
67
67
|
failure = e;
|
|
@@ -79,7 +79,7 @@ public final class PushyNativeUpdate {
|
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
private
|
|
82
|
+
private PushyRuntime() {
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/**
|
|
@@ -91,7 +91,7 @@ public final class PushyNativeUpdate {
|
|
|
91
91
|
* this method: bundle resolution consumes first-load/rollback markers.
|
|
92
92
|
* Missing persisted configuration and debug builds are reported as skipped.
|
|
93
93
|
*/
|
|
94
|
-
public static void
|
|
94
|
+
public static void prepareBundle(Context context, final Callback callback) {
|
|
95
95
|
if (context == null || callback == null) {
|
|
96
96
|
throw new IllegalArgumentException("context and callback are required");
|
|
97
97
|
}
|
|
@@ -99,22 +99,22 @@ public final class PushyNativeUpdate {
|
|
|
99
99
|
WORKER.execute(new Runnable() {
|
|
100
100
|
@Override
|
|
101
101
|
public void run() {
|
|
102
|
-
|
|
102
|
+
BundlePreparationResult outcome;
|
|
103
103
|
try {
|
|
104
104
|
if (BuildConfig.DEBUG) {
|
|
105
|
-
outcome =
|
|
105
|
+
outcome = BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "debug");
|
|
106
106
|
} else {
|
|
107
|
-
outcome = NativeCheckOrchestrator.
|
|
107
|
+
outcome = NativeCheckOrchestrator.prepareBundle(
|
|
108
108
|
UpdateContext.getInstance(applicationContext));
|
|
109
109
|
}
|
|
110
110
|
} catch (InterruptedException e) {
|
|
111
111
|
Thread.currentThread().interrupt();
|
|
112
|
-
outcome =
|
|
112
|
+
outcome = BundlePreparationResult.of(BundlePreparationResult.CANCELLED, "interrupted");
|
|
113
113
|
} catch (Exception | LinkageError e) {
|
|
114
114
|
Log.w("react-native-update", "native host check failed", e);
|
|
115
|
-
outcome =
|
|
115
|
+
outcome = BundlePreparationResult.of(BundlePreparationResult.FAILED, "internal_error");
|
|
116
116
|
}
|
|
117
|
-
final
|
|
117
|
+
final BundlePreparationResult result = outcome;
|
|
118
118
|
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
|
119
119
|
@Override
|
|
120
120
|
public void run() {
|
|
@@ -766,7 +766,7 @@ public class UpdateContext {
|
|
|
766
766
|
editor.putString(NativeCheckOrchestrator.KEY_CONFIG, config);
|
|
767
767
|
// Persist even an equal value: a previous commit may have updated
|
|
768
768
|
// SharedPreferences memory but failed to write its file.
|
|
769
|
-
persistEditorOrThrow(editor, "
|
|
769
|
+
persistEditorOrThrow(editor, "persist configuration");
|
|
770
770
|
}
|
|
771
771
|
NativeCheckOrchestrator.onConfigured(this);
|
|
772
772
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** A native round snapshot, not the current state of the running RN instance. */
|
|
2
|
-
export interface
|
|
2
|
+
export interface BundlePreparationResult {
|
|
3
3
|
/** skipped, noUpdate, downloaded, failed, or cancelled. */
|
|
4
4
|
status: string;
|
|
5
5
|
reason: string;
|
|
@@ -8,20 +8,20 @@ export interface NativeUpdateResult {
|
|
|
8
8
|
activated: boolean;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export function
|
|
11
|
+
export function bundlePreparationResult(
|
|
12
12
|
status: string,
|
|
13
13
|
reason: string = '',
|
|
14
14
|
hash: string = '',
|
|
15
15
|
activated: boolean = false,
|
|
16
|
-
):
|
|
16
|
+
): BundlePreparationResult {
|
|
17
17
|
return { status, reason, hash, activated };
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/** Internal gate shared by the delayed check and the native host API. */
|
|
21
|
-
export class
|
|
22
|
-
private task: Promise<
|
|
21
|
+
export class BundlePreparationRound {
|
|
22
|
+
private task: Promise<BundlePreparationResult> | undefined;
|
|
23
23
|
|
|
24
|
-
run(operation: () => Promise<
|
|
24
|
+
run(operation: () => Promise<BundlePreparationResult>): Promise<BundlePreparationResult> {
|
|
25
25
|
if (this.task !== undefined) {
|
|
26
26
|
return this.task;
|
|
27
27
|
}
|
|
@@ -3,8 +3,8 @@ import deviceInfo from '@ohos.deviceInfo';
|
|
|
3
3
|
import logger from './Logger';
|
|
4
4
|
import NativePatchCore from './NativePatchCore';
|
|
5
5
|
import type { UpdateContext } from './UpdateContext';
|
|
6
|
-
import {
|
|
7
|
-
import type {
|
|
6
|
+
import { BundlePreparationRound, bundlePreparationResult } from './BundlePreparationResult';
|
|
7
|
+
import type { BundlePreparationResult } from './BundlePreparationResult';
|
|
8
8
|
import { isSafePathComponent } from './PathUtils';
|
|
9
9
|
import { monotonicNowMs } from './MonotonicClock';
|
|
10
10
|
import {
|
|
@@ -109,18 +109,18 @@ interface RespCacheEntry {
|
|
|
109
109
|
|
|
110
110
|
let scheduled = false;
|
|
111
111
|
// The host and delayed check use one promise, including its settled result.
|
|
112
|
-
const hostRound = new
|
|
112
|
+
const hostRound = new BundlePreparationRound();
|
|
113
113
|
let scheduledContext: UpdateContext | undefined;
|
|
114
114
|
let scheduledRollback = '';
|
|
115
115
|
let roundGeneration = -1;
|
|
116
116
|
let roundConfigGeneration = -1;
|
|
117
117
|
let roundConfigJson: string | undefined;
|
|
118
|
-
let roundResult =
|
|
118
|
+
let roundResult = bundlePreparationResult('failed', 'check_failed');
|
|
119
119
|
|
|
120
120
|
function startNativeRound(
|
|
121
121
|
context: UpdateContext,
|
|
122
122
|
launchRolledBackVersion: string,
|
|
123
|
-
): Promise<
|
|
123
|
+
): Promise<BundlePreparationResult> {
|
|
124
124
|
const preflight = configurationError(context);
|
|
125
125
|
if (preflight !== undefined) {
|
|
126
126
|
return Promise.resolve(preflight);
|
|
@@ -130,7 +130,7 @@ function startNativeRound(
|
|
|
130
130
|
await runOnce(context, launchRolledBackVersion);
|
|
131
131
|
} catch (e) {
|
|
132
132
|
logger.error(TAG, `native check failed: ${getErrorMessage(e)}`);
|
|
133
|
-
roundResult =
|
|
133
|
+
roundResult = bundlePreparationResult('failed', 'internal_error');
|
|
134
134
|
}
|
|
135
135
|
return roundResult;
|
|
136
136
|
});
|
|
@@ -138,62 +138,62 @@ function startNativeRound(
|
|
|
138
138
|
|
|
139
139
|
// A preflight skip must not consume the process's only round: configuration
|
|
140
140
|
// may arrive after the delayed startup timer on a first-ever launch.
|
|
141
|
-
function configurationError(context: UpdateContext):
|
|
141
|
+
function configurationError(context: UpdateContext): BundlePreparationResult | undefined {
|
|
142
142
|
const json = context.getKv(KEY_CONFIG);
|
|
143
143
|
if (!json) {
|
|
144
|
-
return
|
|
144
|
+
return bundlePreparationResult('skipped', 'not_configured');
|
|
145
145
|
}
|
|
146
146
|
try {
|
|
147
147
|
const config = JSON.parse(json) as NativeConfig;
|
|
148
148
|
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
149
|
-
return
|
|
149
|
+
return bundlePreparationResult('failed', 'invalid_config');
|
|
150
150
|
}
|
|
151
151
|
if (config.disabled) {
|
|
152
|
-
return
|
|
152
|
+
return bundlePreparationResult('skipped', 'disabled');
|
|
153
153
|
}
|
|
154
154
|
if (typeof config.appKey !== 'string' || config.appKey.trim().length === 0) {
|
|
155
|
-
return
|
|
155
|
+
return bundlePreparationResult('failed', 'invalid_config');
|
|
156
156
|
}
|
|
157
157
|
} catch (e) {
|
|
158
|
-
return
|
|
158
|
+
return bundlePreparationResult('failed', 'invalid_config');
|
|
159
159
|
}
|
|
160
160
|
return undefined;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
export async function
|
|
163
|
+
export async function prepareBundleNative(
|
|
164
164
|
context: UpdateContext,
|
|
165
|
-
): Promise<
|
|
165
|
+
): Promise<BundlePreparationResult> {
|
|
166
166
|
if (scheduledContext !== context) {
|
|
167
|
-
return
|
|
167
|
+
return bundlePreparationResult('skipped', 'not_initialized');
|
|
168
168
|
}
|
|
169
169
|
const configJson = context.getKv(KEY_CONFIG);
|
|
170
170
|
if (!configJson) {
|
|
171
|
-
return
|
|
171
|
+
return bundlePreparationResult('skipped', 'not_configured');
|
|
172
172
|
}
|
|
173
173
|
try {
|
|
174
174
|
const config = JSON.parse(configJson) as NativeConfig;
|
|
175
175
|
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
176
|
-
return
|
|
176
|
+
return bundlePreparationResult('failed', 'invalid_config');
|
|
177
177
|
}
|
|
178
178
|
if (config.disabled) {
|
|
179
|
-
return
|
|
179
|
+
return bundlePreparationResult('skipped', 'disabled');
|
|
180
180
|
}
|
|
181
181
|
if (typeof config.appKey !== 'string' || !config.appKey) {
|
|
182
|
-
return
|
|
182
|
+
return bundlePreparationResult('failed', 'invalid_config');
|
|
183
183
|
}
|
|
184
184
|
} catch (e) {
|
|
185
|
-
return
|
|
185
|
+
return bundlePreparationResult('failed', 'invalid_config');
|
|
186
186
|
}
|
|
187
187
|
const result = await startNativeRound(context, scheduledRollback);
|
|
188
188
|
if (roundConfigGeneration !== context.getNativeConfigGeneration()
|
|
189
189
|
|| configJson !== roundConfigJson || configJson !== context.getKv(KEY_CONFIG)) {
|
|
190
|
-
return
|
|
190
|
+
return bundlePreparationResult('cancelled', 'config_changed');
|
|
191
191
|
}
|
|
192
192
|
if (roundGeneration !== context.getResetGeneration()) {
|
|
193
|
-
return
|
|
193
|
+
return bundlePreparationResult('cancelled', 'reset');
|
|
194
194
|
}
|
|
195
195
|
// Do not let a caller mutate the cached result observed by later callers.
|
|
196
|
-
return
|
|
196
|
+
return bundlePreparationResult(result.status, result.reason, result.hash, result.activated);
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
// JS 在本进程内已拿到有效检查响应时对应的配置 JSON(markJsCheckCompleted)。
|
|
@@ -250,32 +250,32 @@ async function runOnce(
|
|
|
250
250
|
const resetGeneration = context.getResetGeneration();
|
|
251
251
|
roundGeneration = resetGeneration;
|
|
252
252
|
roundConfigGeneration = context.getNativeConfigGeneration();
|
|
253
|
-
roundResult =
|
|
253
|
+
roundResult = bundlePreparationResult('failed', 'check_failed');
|
|
254
254
|
const configJson = context.getKv(KEY_CONFIG);
|
|
255
255
|
roundConfigJson = configJson;
|
|
256
256
|
if (!configJson) {
|
|
257
257
|
// No persisted configuration: report the rollout gate to native callers.
|
|
258
|
-
roundResult =
|
|
258
|
+
roundResult = bundlePreparationResult('skipped', 'not_configured');
|
|
259
259
|
return;
|
|
260
260
|
}
|
|
261
261
|
let config: NativeConfig;
|
|
262
262
|
try {
|
|
263
263
|
config = JSON.parse(configJson) as NativeConfig;
|
|
264
264
|
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
265
|
-
roundResult =
|
|
265
|
+
roundResult = bundlePreparationResult('failed', 'invalid_config');
|
|
266
266
|
return;
|
|
267
267
|
}
|
|
268
268
|
} catch (e) {
|
|
269
|
-
roundResult =
|
|
269
|
+
roundResult = bundlePreparationResult('failed', 'invalid_config');
|
|
270
270
|
return;
|
|
271
271
|
}
|
|
272
272
|
if (config.disabled) {
|
|
273
|
-
roundResult =
|
|
273
|
+
roundResult = bundlePreparationResult('skipped', 'disabled');
|
|
274
274
|
return;
|
|
275
275
|
}
|
|
276
276
|
const appKey = config.appKey ?? '';
|
|
277
277
|
if (!appKey) {
|
|
278
|
-
roundResult =
|
|
278
|
+
roundResult = bundlePreparationResult('failed', 'invalid_config');
|
|
279
279
|
return;
|
|
280
280
|
}
|
|
281
281
|
// 从这里起本轮开始做真实工作:留下面包屑,死于轮中时下次启动零延迟续传。
|
|
@@ -371,7 +371,7 @@ async function runConfiguredRound(
|
|
|
371
371
|
};
|
|
372
372
|
const body = NativePatchCore.buildCheckRequestBody(JSON.stringify(input));
|
|
373
373
|
if (!body) {
|
|
374
|
-
roundResult =
|
|
374
|
+
roundResult = bundlePreparationResult('failed', 'invalid_request');
|
|
375
375
|
return;
|
|
376
376
|
}
|
|
377
377
|
|
|
@@ -390,7 +390,7 @@ async function runConfiguredRound(
|
|
|
390
390
|
config.afterDownload ?? '',
|
|
391
391
|
);
|
|
392
392
|
if (!decisionJson) {
|
|
393
|
-
roundResult =
|
|
393
|
+
roundResult = bundlePreparationResult('failed', 'invalid_response');
|
|
394
394
|
return;
|
|
395
395
|
}
|
|
396
396
|
const decision = JSON.parse(decisionJson) as Decision;
|
|
@@ -403,15 +403,15 @@ async function runConfiguredRound(
|
|
|
403
403
|
buildResponseCacheJson(configJson, body, responseText, responseAtSeconds),
|
|
404
404
|
);
|
|
405
405
|
roundResult = committed
|
|
406
|
-
?
|
|
407
|
-
:
|
|
406
|
+
? bundlePreparationResult('noUpdate', decision.reason ?? '')
|
|
407
|
+
: bundlePreparationResult('cancelled', 'reset');
|
|
408
408
|
logger.info(TAG, `nothing to do (${decision.reason ?? ''})`);
|
|
409
409
|
return;
|
|
410
410
|
}
|
|
411
411
|
const hash = decision.hash ?? '';
|
|
412
412
|
if (!isSafePathComponent(hash)) {
|
|
413
413
|
logger.warn(TAG, 'decision carries an unsafe hash, ignoring');
|
|
414
|
-
roundResult =
|
|
414
|
+
roundResult = bundlePreparationResult('failed', 'invalid_response');
|
|
415
415
|
return;
|
|
416
416
|
}
|
|
417
417
|
|
|
@@ -438,8 +438,8 @@ async function runConfiguredRound(
|
|
|
438
438
|
buildResponseCacheJson(configJson, body, responseText, responseAtSeconds),
|
|
439
439
|
);
|
|
440
440
|
roundResult = committed
|
|
441
|
-
?
|
|
442
|
-
:
|
|
441
|
+
? bundlePreparationResult('failed', 'download_failed')
|
|
442
|
+
: bundlePreparationResult('cancelled', 'reset');
|
|
443
443
|
return;
|
|
444
444
|
}
|
|
445
445
|
|
|
@@ -481,7 +481,7 @@ async function runConfiguredRound(
|
|
|
481
481
|
);
|
|
482
482
|
} catch (e) {
|
|
483
483
|
logger.error(TAG, `commit failed: ${getErrorMessage(e)}`);
|
|
484
|
-
roundResult =
|
|
484
|
+
roundResult = bundlePreparationResult('failed', 'commit_failed');
|
|
485
485
|
return;
|
|
486
486
|
}
|
|
487
487
|
if (!committed) {
|
|
@@ -492,8 +492,8 @@ async function runConfiguredRound(
|
|
|
492
492
|
logger.info(TAG, `downloaded ${hash}, activation left to JS`);
|
|
493
493
|
}
|
|
494
494
|
roundResult = committed
|
|
495
|
-
?
|
|
496
|
-
:
|
|
495
|
+
? bundlePreparationResult('downloaded', '', hash, activate)
|
|
496
|
+
: bundlePreparationResult('cancelled', 'reset');
|
|
497
497
|
}
|
|
498
498
|
|
|
499
499
|
function buildResponseCacheJson(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Options accepted by PushyFileJSBundleProvider.configure(). */
|
|
2
|
-
export interface
|
|
2
|
+
export interface PushyConfiguration {
|
|
3
3
|
appKey: string;
|
|
4
4
|
/** Omitted: Pushy's built-in endpoints. Custom endpoints never inherit discovery URLs. */
|
|
5
5
|
endpoints?: string[];
|
|
@@ -14,7 +14,7 @@ export interface NativeUpdateConfig {
|
|
|
14
14
|
rn?: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
interface
|
|
17
|
+
interface PersistedPushyConfiguration {
|
|
18
18
|
appKey: string;
|
|
19
19
|
endpoints: string[];
|
|
20
20
|
queryUrls: string[];
|
|
@@ -67,7 +67,7 @@ function configUrls(values: string[], name: string, base: boolean): string[] {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/** Pure validation, before any native state is touched. Does not mutate options. */
|
|
70
|
-
export function
|
|
70
|
+
export function normalizePushyConfiguration(options: PushyConfiguration): string {
|
|
71
71
|
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
72
72
|
throw new Error('Invalid native configuration: expected an object');
|
|
73
73
|
}
|
|
@@ -90,7 +90,7 @@ export function normalizeNativeUpdateConfig(options: NativeUpdateConfig): string
|
|
|
90
90
|
if (options.disabled !== undefined && typeof options.disabled !== 'boolean') {
|
|
91
91
|
throw new Error('Invalid native configuration: disabled must be a boolean');
|
|
92
92
|
}
|
|
93
|
-
const result:
|
|
93
|
+
const result: PersistedPushyConfiguration = {
|
|
94
94
|
appKey, endpoints, queryUrls, afterDownload, disabled: options.disabled ?? false,
|
|
95
95
|
rnu: options.rnu === undefined ? '' : configString(options.rnu, 'rnu', true),
|
|
96
96
|
rn: options.rn === undefined ? '' : configString(options.rn, 'rn', true),
|
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
import common from '@ohos.app.ability.common';
|
|
7
7
|
import fs from '@ohos.file.fs';
|
|
8
8
|
import { UpdateContext } from './UpdateContext';
|
|
9
|
-
import {
|
|
10
|
-
import type {
|
|
11
|
-
import {
|
|
12
|
-
import type {
|
|
9
|
+
import { prepareBundleNative } from './NativeCheckOrchestrator';
|
|
10
|
+
import type { BundlePreparationResult } from './BundlePreparationResult';
|
|
11
|
+
import { normalizePushyConfiguration } from './PushyConfiguration';
|
|
12
|
+
import type { PushyConfiguration } from './PushyConfiguration';
|
|
13
13
|
|
|
14
14
|
export class PushyFileJSBundleProvider extends JSBundleProvider {
|
|
15
15
|
private updateContext: UpdateContext;
|
|
@@ -48,14 +48,14 @@ export class PushyFileJSBundleProvider extends JSBundleProvider {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/** Configure before normal bundle resolution; no JS or network work is required. */
|
|
51
|
-
async configure(options:
|
|
52
|
-
const config =
|
|
51
|
+
async configure(options: PushyConfiguration): Promise<void> {
|
|
52
|
+
const config = normalizePushyConfiguration(options);
|
|
53
53
|
await this.updateContext.setNativeConfig(config);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/** Call after the host's real bundle resolution; never resolves it again. */
|
|
57
|
-
|
|
58
|
-
return
|
|
57
|
+
prepareBundle(): Promise<BundlePreparationResult> {
|
|
58
|
+
return prepareBundleNative(this.updateContext);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
getAppKeys(): string[] {
|
|
@@ -480,7 +480,7 @@ export class UpdateContext {
|
|
|
480
480
|
markJsCheckCompleted('');
|
|
481
481
|
}
|
|
482
482
|
// Flush even an equal value so a retry after a storage error can succeed.
|
|
483
|
-
return this.flushPreferences('
|
|
483
|
+
return this.flushPreferences('persist configuration');
|
|
484
484
|
}
|
|
485
485
|
|
|
486
486
|
public setKv(key: string, value: string): Promise<void> {
|
package/harmony/pushy.har
CHANGED
|
Binary file
|
package/ios/RCTPushy/RCTPushy.h
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#import <React/RCTBridgeModule.h>
|
|
2
2
|
#import <React/RCTEventEmitter.h>
|
|
3
3
|
|
|
4
|
-
typedef void (^
|
|
4
|
+
typedef void (^RCTPushyConfigurationCompletion)(NSError * _Nullable error);
|
|
5
5
|
|
|
6
|
-
typedef void (^
|
|
6
|
+
typedef void (^RCTPushyBundlePreparationCompletion)(NSDictionary<NSString *, id> * _Nonnull result);
|
|
7
7
|
|
|
8
8
|
@interface RCTPushy : RCTEventEmitter<RCTBridgeModule>
|
|
9
9
|
|
|
@@ -14,20 +14,20 @@ typedef void (^RCTPushyNativeUpdateCompletion)(NSDictionary<NSString *, id> * _N
|
|
|
14
14
|
* normal launch bundle resolution for first-install native-only provisioning.
|
|
15
15
|
*/
|
|
16
16
|
+ (void)configure:(NSDictionary<NSString *, id> * _Nonnull)options
|
|
17
|
-
completion:(
|
|
17
|
+
completion:(RCTPushyConfigurationCompletion _Nullable)completion
|
|
18
18
|
NS_SWIFT_NAME(configure(_:completion:));
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* Start, join, or reuse this process's native
|
|
22
|
-
* host's real bundleURL resolution; this method never resolves the
|
|
23
|
-
* again, reloads React Native, or displays UI. Configuration is the one
|
|
21
|
+
* Start, join, or reuse this process's native bundle-preparation round. Call
|
|
22
|
+
* after the host's real bundleURL resolution; this method never resolves the
|
|
23
|
+
* bundle again, reloads React Native, or displays UI. Configuration is the one
|
|
24
24
|
* persisted by the JS SDK. The optional completion runs on the main queue.
|
|
25
25
|
*
|
|
26
26
|
* Result keys: status (skipped/noUpdate/downloaded/failed/cancelled), reason,
|
|
27
27
|
* hash (empty when nothing was installed), activated (selected for NEXT
|
|
28
28
|
* launch, not a reload of the running instance).
|
|
29
29
|
*/
|
|
30
|
-
+ (void)
|
|
31
|
-
NS_SWIFT_NAME(
|
|
30
|
+
+ (void)prepareBundleWithCompletion:(RCTPushyBundlePreparationCompletion _Nullable)completion
|
|
31
|
+
NS_SWIFT_NAME(prepareBundle(completion:));
|
|
32
32
|
|
|
33
33
|
@end
|
package/ios/RCTPushy/RCTPushy.mm
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#import "RCTPushy.h"
|
|
2
|
-
#import "
|
|
2
|
+
#import "RCTPushyConfiguration.h"
|
|
3
3
|
#import "RCTPushyDownloader.h"
|
|
4
4
|
#import "ZipArchive.h"
|
|
5
5
|
#include "../../cpp/patch_core/archive_limits.h"
|
|
@@ -738,7 +738,7 @@ static void PushySwitchVersionLocked(NSString *hash) {
|
|
|
738
738
|
@interface RCTPushyOrchestrator : NSObject
|
|
739
739
|
+ (void)persistConfiguration:(NSString *)config;
|
|
740
740
|
+ (BOOL)hasRunnableConfig;
|
|
741
|
-
+ (NSDictionary *)
|
|
741
|
+
+ (NSDictionary *)prepareBundle;
|
|
742
742
|
+ (void)scheduleFromColdStart:(NSString *)launchRolledBackVersion;
|
|
743
743
|
#if TARGET_OS_TV && !DEBUG
|
|
744
744
|
+ (BOOL)restorePurgedLaunch:(NSString *)purgedVersion
|
|
@@ -1104,11 +1104,11 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
1104
1104
|
}
|
|
1105
1105
|
|
|
1106
1106
|
+ (void)configure:(NSDictionary<NSString *, id> *)options
|
|
1107
|
-
completion:(
|
|
1107
|
+
completion:(RCTPushyConfigurationCompletion)completion
|
|
1108
1108
|
{
|
|
1109
1109
|
NSError *validationError = nil;
|
|
1110
1110
|
// Snapshot nested mutable caller values before crossing a queue boundary.
|
|
1111
|
-
NSString *config =
|
|
1111
|
+
NSString *config = RCTPushyNormalizeConfiguration(options, &validationError);
|
|
1112
1112
|
static dispatch_queue_t configQueue;
|
|
1113
1113
|
static dispatch_once_t once;
|
|
1114
1114
|
dispatch_once(&once, ^{
|
|
@@ -1132,7 +1132,7 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
1132
1132
|
});
|
|
1133
1133
|
}
|
|
1134
1134
|
|
|
1135
|
-
+ (void)
|
|
1135
|
+
+ (void)prepareBundleWithCompletion:(RCTPushyBundlePreparationCompletion)completion
|
|
1136
1136
|
{
|
|
1137
1137
|
static dispatch_queue_t hostQueue;
|
|
1138
1138
|
static dispatch_once_t once;
|
|
@@ -1142,7 +1142,7 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
1142
1142
|
dispatch_async(hostQueue, ^{
|
|
1143
1143
|
NSDictionary *result;
|
|
1144
1144
|
@try {
|
|
1145
|
-
result = [RCTPushyOrchestrator
|
|
1145
|
+
result = [RCTPushyOrchestrator prepareBundle];
|
|
1146
1146
|
} @catch (NSException *exception) {
|
|
1147
1147
|
RCTLogWarn(@"RCTPushy -- native host check failed: %@", exception.reason);
|
|
1148
1148
|
result = PushyHostResult(@"failed", @"internal_error", nil, NO);
|
|
@@ -2425,7 +2425,7 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
2425
2425
|
}
|
|
2426
2426
|
#endif
|
|
2427
2427
|
|
|
2428
|
-
+ (NSDictionary *)
|
|
2428
|
+
+ (NSDictionary *)prepareBundle {
|
|
2429
2429
|
#if DEBUG
|
|
2430
2430
|
return PushyHostResult(@"skipped", @"debug", nil, NO);
|
|
2431
2431
|
#else
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#import <Foundation/Foundation.h>
|
|
2
2
|
|
|
3
3
|
// Validates and snapshots host options without changing any update state.
|
|
4
|
-
FOUNDATION_EXPORT NSString * _Nullable
|
|
4
|
+
FOUNDATION_EXPORT NSString * _Nullable RCTPushyNormalizeConfiguration(
|
|
5
5
|
NSDictionary * _Nonnull options, NSError * _Nullable * _Nullable error);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#import "
|
|
1
|
+
#import "RCTPushyConfiguration.h"
|
|
2
2
|
|
|
3
3
|
static void PushyConfigInvalid(NSString *message) {
|
|
4
4
|
@throw [NSException exceptionWithName:NSInvalidArgumentException
|
|
@@ -48,7 +48,7 @@ static NSArray *PushyConfigUrls(id value, NSString *name, BOOL base) {
|
|
|
48
48
|
return result;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
NSString *
|
|
51
|
+
NSString *RCTPushyNormalizeConfiguration(NSDictionary *options, NSError **error) {
|
|
52
52
|
@try {
|
|
53
53
|
if (![options isKindOfClass:NSDictionary.class]) {
|
|
54
54
|
PushyConfigInvalid(@"expected an object");
|