@capgo/capacitor-webview-version-checker 8.0.0 → 8.1.1

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.
@@ -25,6 +25,7 @@ import java.net.URL;
25
25
  import java.nio.charset.StandardCharsets;
26
26
  import java.text.SimpleDateFormat;
27
27
  import java.util.Date;
28
+ import java.util.Iterator;
28
29
  import java.util.Locale;
29
30
  import java.util.TimeZone;
30
31
  import org.json.JSONArray;
@@ -66,6 +67,14 @@ public class WebviewVersionCheckerPlugin extends Plugin {
66
67
 
67
68
  private long cachedLatestVersionTimestamp;
68
69
 
70
+ @Nullable
71
+ private JSONObject cachedVersionShareByMajor;
72
+
73
+ @Nullable
74
+ private String cachedVersionShareApiUrl;
75
+
76
+ private long cachedVersionShareTimestamp;
77
+
69
78
  @Override
70
79
  public void load() {
71
80
  super.load();
@@ -167,7 +176,6 @@ public class WebviewVersionCheckerPlugin extends Plugin {
167
176
 
168
177
  private JSObject evaluateStatus(String source, RuntimeOptions options) {
169
178
  CurrentWebViewInfo current = resolveCurrentWebViewInfo();
170
- LatestVersionResolution latestResolution = resolveLatestVersion(options);
171
179
 
172
180
  JSObject status = new JSObject();
173
181
  status.put("platform", "android");
@@ -181,15 +189,6 @@ public class WebviewVersionCheckerPlugin extends Plugin {
181
189
  status.put("currentMajorVersion", currentMajorVersion);
182
190
  }
183
191
 
184
- String latestVersion = latestResolution.version;
185
- if (!isBlank(latestVersion)) {
186
- status.put("latestVersion", latestVersion);
187
- Integer latestMajorVersion = versionChecker.parseMajorVersion(latestVersion);
188
- if (latestMajorVersion != null) {
189
- status.put("latestMajorVersion", latestMajorVersion);
190
- }
191
- }
192
-
193
192
  String updateUrl = resolveUpdateUrl(options.updateUrl, status);
194
193
  if (!isBlank(updateUrl)) {
195
194
  status.put("updateUrl", updateUrl);
@@ -202,6 +201,55 @@ public class WebviewVersionCheckerPlugin extends Plugin {
202
201
  return status;
203
202
  }
204
203
 
204
+ String deviceShareErrorMessage = null;
205
+ if (options.minimumDeviceSharePercent != null) {
206
+ status.put("minimumDeviceSharePercent", options.minimumDeviceSharePercent);
207
+ if (currentMajorVersion == null) {
208
+ deviceShareErrorMessage =
209
+ "Could not evaluate device-share threshold because the installed WebView major version could not be parsed.";
210
+ } else {
211
+ VersionShareResolution versionShareResolution = resolveVersionShare(options);
212
+ if (!isBlank(versionShareResolution.source)) {
213
+ status.put("versionShareSource", versionShareResolution.source);
214
+ }
215
+
216
+ Double currentVersionSharePercent = readShareForMajor(versionShareResolution.shareByMajor, currentMajorVersion);
217
+ if (currentVersionSharePercent != null) {
218
+ status.put("currentVersionSharePercent", currentVersionSharePercent);
219
+ boolean isLatest = currentVersionSharePercent >= options.minimumDeviceSharePercent;
220
+ status.put("state", isLatest ? "latest" : "outdated");
221
+ status.put("isLatest", isLatest);
222
+ status.put(
223
+ "reason",
224
+ isLatest
225
+ ? "Installed WebView major version satisfies the configured minimum device-share threshold."
226
+ : "Installed WebView major version is below the configured minimum device-share threshold."
227
+ );
228
+ return status;
229
+ }
230
+
231
+ if (!isBlank(versionShareResolution.errorMessage)) {
232
+ deviceShareErrorMessage = versionShareResolution.errorMessage;
233
+ } else {
234
+ deviceShareErrorMessage =
235
+ "No version-share data was found for installed WebView major version " + currentMajorVersion + ".";
236
+ }
237
+ }
238
+ if (!isBlank(deviceShareErrorMessage)) {
239
+ status.put("deviceShareError", deviceShareErrorMessage);
240
+ }
241
+ }
242
+
243
+ LatestVersionResolution latestResolution = resolveLatestVersion(options);
244
+ String latestVersion = latestResolution.version;
245
+ if (!isBlank(latestVersion)) {
246
+ status.put("latestVersion", latestVersion);
247
+ Integer latestMajorVersion = versionChecker.parseMajorVersion(latestVersion);
248
+ if (latestMajorVersion != null) {
249
+ status.put("latestMajorVersion", latestMajorVersion);
250
+ }
251
+ }
252
+
205
253
  if (!isBlank(latestVersion)) {
206
254
  int cmp = versionChecker.compareVersions(current.versionName, latestVersion);
207
255
  boolean isLatest = cmp >= 0;
@@ -234,8 +282,13 @@ public class WebviewVersionCheckerPlugin extends Plugin {
234
282
  status.put("isLatest", false);
235
283
  if (!isBlank(latestResolution.errorMessage)) {
236
284
  status.put("reason", latestResolution.errorMessage);
285
+ } else if (!isBlank(deviceShareErrorMessage)) {
286
+ status.put("reason", deviceShareErrorMessage);
237
287
  } else {
238
- status.put("reason", "Unable to resolve latest WebView version. Configure latestVersion or minimumMajorVersion.");
288
+ status.put(
289
+ "reason",
290
+ "Unable to resolve compatibility status. Configure latestVersion, minimumMajorVersion, or minimumDeviceSharePercent."
291
+ );
239
292
  }
240
293
 
241
294
  return status;
@@ -316,7 +369,7 @@ public class WebviewVersionCheckerPlugin extends Plugin {
316
369
  callback.onResult(true, false);
317
370
  }
318
371
  })
319
- .setOnCancelListener(dialogInterface -> {
372
+ .setOnCancelListener((dialogInterface) -> {
320
373
  if (callback != null && !resolved[0]) {
321
374
  resolved[0] = true;
322
375
  callback.onResult(true, false);
@@ -392,6 +445,82 @@ public class WebviewVersionCheckerPlugin extends Plugin {
392
445
  }
393
446
  }
394
447
 
448
+ private VersionShareResolution resolveVersionShare(RuntimeOptions options) {
449
+ if (options.versionShareByMajor != null && options.versionShareByMajor.length() > 0) {
450
+ return new VersionShareResolution(copyJsonObject(options.versionShareByMajor), "versionShareByMajor", null);
451
+ }
452
+
453
+ if (!isBlank(options.versionShareApiUrl)) {
454
+ String apiUrl = options.versionShareApiUrl;
455
+
456
+ if (
457
+ cachedVersionShareByMajor != null &&
458
+ !isBlank(cachedVersionShareApiUrl) &&
459
+ apiUrl.equals(cachedVersionShareApiUrl) &&
460
+ (System.currentTimeMillis() - cachedVersionShareTimestamp) < LATEST_VERSION_CACHE_TTL_MS
461
+ ) {
462
+ return new VersionShareResolution(copyJsonObject(cachedVersionShareByMajor), "versionShareApiUrl", null);
463
+ }
464
+
465
+ try {
466
+ String payload = readUrl(apiUrl);
467
+ JSONObject shareByMajor = extractVersionShareByMajor(payload);
468
+ if (shareByMajor == null || shareByMajor.length() == 0) {
469
+ return new VersionShareResolution(null, "versionShareApiUrl", "Version-share API returned no compatible dataset.");
470
+ }
471
+
472
+ cachedVersionShareByMajor = copyJsonObject(shareByMajor);
473
+ cachedVersionShareApiUrl = apiUrl;
474
+ cachedVersionShareTimestamp = System.currentTimeMillis();
475
+
476
+ return new VersionShareResolution(shareByMajor, "versionShareApiUrl", null);
477
+ } catch (Exception error) {
478
+ return new VersionShareResolution(
479
+ null,
480
+ "versionShareApiUrl",
481
+ "Could not resolve version-share dataset: " + error.getMessage()
482
+ );
483
+ }
484
+ }
485
+
486
+ JSONObject defaultDataset = GeneratedVersionShareData.getDefaultVersionShareByMajor();
487
+ if (defaultDataset != null && defaultDataset.length() > 0) {
488
+ return new VersionShareResolution(defaultDataset, GeneratedVersionShareData.getDataSource(), null);
489
+ }
490
+
491
+ return new VersionShareResolution(
492
+ null,
493
+ GeneratedVersionShareData.getDataSource(),
494
+ "Built-in version-share dataset is unavailable."
495
+ );
496
+ }
497
+
498
+ @Nullable
499
+ private Double readShareForMajor(@Nullable JSONObject shareByMajor, int majorVersion) {
500
+ if (shareByMajor == null) {
501
+ return null;
502
+ }
503
+
504
+ String directKey = String.valueOf(majorVersion);
505
+ if (shareByMajor.has(directKey) && !shareByMajor.isNull(directKey)) {
506
+ return parsePercentFromAny(shareByMajor.opt(directKey));
507
+ }
508
+
509
+ Iterator<String> keys = shareByMajor.keys();
510
+ while (keys.hasNext()) {
511
+ String key = keys.next();
512
+ Integer mappedMajor = parseMajorFromShareKey(key);
513
+ if (mappedMajor != null && mappedMajor == majorVersion) {
514
+ Double share = parsePercentFromAny(shareByMajor.opt(key));
515
+ if (share != null) {
516
+ return share;
517
+ }
518
+ }
519
+ }
520
+
521
+ return null;
522
+ }
523
+
395
524
  private String readUrl(String urlValue) throws IOException {
396
525
  HttpURLConnection connection = (HttpURLConnection) new URL(urlValue).openConnection();
397
526
  connection.setRequestMethod("GET");
@@ -417,6 +546,223 @@ public class WebviewVersionCheckerPlugin extends Plugin {
417
546
  }
418
547
  }
419
548
 
549
+ @Nullable
550
+ private JSONObject extractVersionShareByMajor(String payload) throws Exception {
551
+ JSONObject json = new JSONObject(payload);
552
+ return extractVersionShareByMajor(json);
553
+ }
554
+
555
+ @Nullable
556
+ private JSONObject extractVersionShareByMajor(JSONObject json) {
557
+ JSONObject out = new JSONObject();
558
+
559
+ JSONObject explicitMap = json.optJSONObject("versionShareByMajor");
560
+ if (explicitMap != null) {
561
+ mergeVersionShareMap(out, explicitMap);
562
+ }
563
+
564
+ if (out.length() == 0) {
565
+ JSONObject secondaryMap = json.optJSONObject("shareByMajor");
566
+ if (secondaryMap != null) {
567
+ mergeVersionShareMap(out, secondaryMap);
568
+ }
569
+ }
570
+
571
+ if (out.length() == 0) {
572
+ JSONArray versionsArray = json.optJSONArray("versions");
573
+ if (versionsArray != null) {
574
+ mergeVersionShareArray(out, versionsArray);
575
+ }
576
+ }
577
+
578
+ if (out.length() == 0) {
579
+ JSONArray dataArray = json.optJSONArray("data");
580
+ if (dataArray != null) {
581
+ mergeVersionShareArray(out, dataArray);
582
+ }
583
+ }
584
+
585
+ if (out.length() == 0) {
586
+ JSONObject caniuseShareByMajor = extractCaniuseVersionShareByMajor(json);
587
+ if (caniuseShareByMajor != null) {
588
+ mergeVersionShareMap(out, caniuseShareByMajor);
589
+ }
590
+ }
591
+
592
+ if (out.length() == 0) {
593
+ mergeVersionShareMap(out, json);
594
+ }
595
+
596
+ if (out.length() == 0) {
597
+ return null;
598
+ }
599
+ return out;
600
+ }
601
+
602
+ @Nullable
603
+ private JSONObject extractCaniuseVersionShareByMajor(JSONObject json) {
604
+ JSONObject agents = json.optJSONObject("agents");
605
+ if (agents == null) {
606
+ return null;
607
+ }
608
+
609
+ JSONObject out = new JSONObject();
610
+
611
+ // Prefer Android-specific buckets first.
612
+ mergeVersionShareMap(out, getUsageGlobalMap(agents, "and_chr"));
613
+ mergeVersionShareMap(out, getUsageGlobalMap(agents, "android"));
614
+
615
+ // If Android-specific buckets are sparse, complete with Chrome global data.
616
+ if (out.length() < 5) {
617
+ mergeVersionShareMap(out, getUsageGlobalMap(agents, "chrome"));
618
+ }
619
+
620
+ return out.length() > 0 ? out : null;
621
+ }
622
+
623
+ @Nullable
624
+ private JSONObject getUsageGlobalMap(JSONObject agents, String agentKey) {
625
+ JSONObject agent = agents.optJSONObject(agentKey);
626
+ if (agent == null) {
627
+ return null;
628
+ }
629
+ return agent.optJSONObject("usage_global");
630
+ }
631
+
632
+ private void mergeVersionShareMap(JSONObject target, JSONObject source) {
633
+ if (source == null) {
634
+ return;
635
+ }
636
+ Iterator<String> keys = source.keys();
637
+ while (keys.hasNext()) {
638
+ String key = keys.next();
639
+ Integer majorVersion = parseMajorFromShareKey(key);
640
+ Double sharePercent = parsePercentFromAny(source.opt(key));
641
+ putShareEntry(target, majorVersion, sharePercent);
642
+ }
643
+ }
644
+
645
+ private void mergeVersionShareArray(JSONObject target, JSONArray source) {
646
+ for (int index = 0; index < source.length(); index++) {
647
+ JSONObject item = source.optJSONObject(index);
648
+ if (item == null) {
649
+ continue;
650
+ }
651
+
652
+ Integer majorVersion = parseMajorFromAny(item.opt("major"));
653
+ if (majorVersion == null) {
654
+ majorVersion = parseMajorFromAny(item.opt("version"));
655
+ }
656
+ if (majorVersion == null) {
657
+ majorVersion = parseMajorFromAny(item.opt("name"));
658
+ }
659
+
660
+ Double sharePercent = parsePercentFromAny(item.opt("share"));
661
+ if (sharePercent == null) {
662
+ sharePercent = parsePercentFromAny(item.opt("sharePercent"));
663
+ }
664
+ if (sharePercent == null) {
665
+ sharePercent = parsePercentFromAny(item.opt("percent"));
666
+ }
667
+ if (sharePercent == null) {
668
+ sharePercent = parsePercentFromAny(item.opt("usage"));
669
+ }
670
+ if (sharePercent == null) {
671
+ sharePercent = parsePercentFromAny(item.opt("value"));
672
+ }
673
+
674
+ putShareEntry(target, majorVersion, sharePercent);
675
+ }
676
+ }
677
+
678
+ private void putShareEntry(JSONObject target, @Nullable Integer majorVersion, @Nullable Double sharePercent) {
679
+ if (majorVersion == null || sharePercent == null) {
680
+ return;
681
+ }
682
+ try {
683
+ target.put(String.valueOf(majorVersion), sharePercent);
684
+ } catch (Exception ignored) {
685
+ // Ignore invalid JSON insertions for malformed input.
686
+ }
687
+ }
688
+
689
+ @Nullable
690
+ private Integer parseMajorFromShareKey(@Nullable String key) {
691
+ if (isBlank(key)) {
692
+ return null;
693
+ }
694
+
695
+ String normalized = key.trim();
696
+ char first = normalized.charAt(0);
697
+ if (Character.isDigit(first)) {
698
+ return versionChecker.parseMajorVersion(normalized);
699
+ }
700
+
701
+ if ((first == 'v' || first == 'V') && normalized.length() > 1 && Character.isDigit(normalized.charAt(1))) {
702
+ return versionChecker.parseMajorVersion(normalized.substring(1));
703
+ }
704
+
705
+ return null;
706
+ }
707
+
708
+ @Nullable
709
+ private Integer parseMajorFromAny(@Nullable Object value) {
710
+ if (value == null) {
711
+ return null;
712
+ }
713
+
714
+ if (value instanceof Number) {
715
+ int major = ((Number) value).intValue();
716
+ return major >= 0 ? major : null;
717
+ }
718
+
719
+ if (value instanceof String) {
720
+ return versionChecker.parseMajorVersion((String) value);
721
+ }
722
+
723
+ return null;
724
+ }
725
+
726
+ @Nullable
727
+ private Double parsePercentFromAny(@Nullable Object value) {
728
+ if (value == null) {
729
+ return null;
730
+ }
731
+
732
+ Double percent = null;
733
+ if (value instanceof Number) {
734
+ percent = ((Number) value).doubleValue();
735
+ } else if (value instanceof String) {
736
+ try {
737
+ percent = Double.parseDouble(((String) value).trim());
738
+ } catch (NumberFormatException ignored) {
739
+ return null;
740
+ }
741
+ }
742
+
743
+ return normalizePercentValue(percent);
744
+ }
745
+
746
+ @Nullable
747
+ private Double normalizePercentValue(@Nullable Double value) {
748
+ if (value == null || value.isNaN() || value.isInfinite()) {
749
+ return null;
750
+ }
751
+ return Math.max(0.0, Math.min(100.0, value));
752
+ }
753
+
754
+ @Nullable
755
+ private JSONObject copyJsonObject(@Nullable JSONObject value) {
756
+ if (value == null) {
757
+ return null;
758
+ }
759
+ try {
760
+ return new JSONObject(value.toString());
761
+ } catch (Exception error) {
762
+ return null;
763
+ }
764
+ }
765
+
420
766
  @Nullable
421
767
  private String extractLatestVersion(String payload) throws Exception {
422
768
  JSONObject json = new JSONObject(payload);
@@ -584,6 +930,24 @@ public class WebviewVersionCheckerPlugin extends Plugin {
584
930
  }
585
931
  }
586
932
 
933
+ private static final class VersionShareResolution {
934
+
935
+ @Nullable
936
+ private final JSONObject shareByMajor;
937
+
938
+ @Nullable
939
+ private final String source;
940
+
941
+ @Nullable
942
+ private final String errorMessage;
943
+
944
+ private VersionShareResolution(@Nullable JSONObject shareByMajor, @Nullable String source, @Nullable String errorMessage) {
945
+ this.shareByMajor = shareByMajor;
946
+ this.source = source;
947
+ this.errorMessage = errorMessage;
948
+ }
949
+ }
950
+
587
951
  private static final class PromptOptions {
588
952
 
589
953
  @Nullable
@@ -643,6 +1007,15 @@ public class WebviewVersionCheckerPlugin extends Plugin {
643
1007
  @Nullable
644
1008
  private Integer minimumMajorVersion;
645
1009
 
1010
+ @Nullable
1011
+ private Double minimumDeviceSharePercent = 3.0;
1012
+
1013
+ @Nullable
1014
+ private JSONObject versionShareByMajor;
1015
+
1016
+ @Nullable
1017
+ private String versionShareApiUrl;
1018
+
646
1019
  @Nullable
647
1020
  private String latestVersionApiUrl;
648
1021
 
@@ -667,10 +1040,13 @@ public class WebviewVersionCheckerPlugin extends Plugin {
667
1040
  out.autoCheckOnResume = config.getBoolean("autoCheckOnResume", true);
668
1041
  out.autoPromptOnOutdated = config.getBoolean("autoPromptOnOutdated", false);
669
1042
  out.latestVersion = emptyToNull(config.getString("latestVersion"));
670
- Integer minimum = config.getConfigJSON().has("minimumMajorVersion")
671
- ? config.getInt("minimumMajorVersion", 0)
672
- : null;
1043
+ Integer minimum = config.getConfigJSON().has("minimumMajorVersion") ? config.getInt("minimumMajorVersion", 0) : null;
673
1044
  out.minimumMajorVersion = minimum;
1045
+ if (config.getConfigJSON().has("minimumDeviceSharePercent")) {
1046
+ out.minimumDeviceSharePercent = normalizePercent(readOptionalDouble(config.getConfigJSON(), "minimumDeviceSharePercent"));
1047
+ }
1048
+ out.versionShareByMajor = copyJsonObject(config.getObject("versionShareByMajor"));
1049
+ out.versionShareApiUrl = emptyToNull(config.getString("versionShareApiUrl"));
674
1050
  out.latestVersionApiUrl = emptyToNull(config.getString("latestVersionApiUrl"));
675
1051
  out.updateUrl = emptyToNull(config.getString("updateUrl"));
676
1052
  out.promptTitle = emptyToNull(config.getString("promptTitle"));
@@ -701,6 +1077,15 @@ public class WebviewVersionCheckerPlugin extends Plugin {
701
1077
  if (call.hasOption("minimumMajorVersion")) {
702
1078
  out.minimumMajorVersion = call.getInt("minimumMajorVersion");
703
1079
  }
1080
+ if (call.hasOption("minimumDeviceSharePercent")) {
1081
+ out.minimumDeviceSharePercent = normalizePercent(call.getDouble("minimumDeviceSharePercent"));
1082
+ }
1083
+ if (call.hasOption("versionShareByMajor")) {
1084
+ out.versionShareByMajor = copyJsonObject(call.getObject("versionShareByMajor"));
1085
+ }
1086
+ if (call.hasOption("versionShareApiUrl")) {
1087
+ out.versionShareApiUrl = emptyToNull(call.getString("versionShareApiUrl"));
1088
+ }
704
1089
  if (call.hasOption("latestVersionApiUrl")) {
705
1090
  out.latestVersionApiUrl = emptyToNull(call.getString("latestVersionApiUrl"));
706
1091
  }
@@ -731,6 +1116,9 @@ public class WebviewVersionCheckerPlugin extends Plugin {
731
1116
  out.showPromptOnOutdated = showPromptOnOutdated;
732
1117
  out.latestVersion = latestVersion;
733
1118
  out.minimumMajorVersion = minimumMajorVersion;
1119
+ out.minimumDeviceSharePercent = minimumDeviceSharePercent;
1120
+ out.versionShareByMajor = copyJsonObject(versionShareByMajor);
1121
+ out.versionShareApiUrl = versionShareApiUrl;
734
1122
  out.latestVersionApiUrl = latestVersionApiUrl;
735
1123
  out.updateUrl = updateUrl;
736
1124
  out.promptTitle = promptTitle;
@@ -747,5 +1135,47 @@ public class WebviewVersionCheckerPlugin extends Plugin {
747
1135
  }
748
1136
  return value;
749
1137
  }
1138
+
1139
+ @Nullable
1140
+ private static Double readOptionalDouble(JSONObject source, String key) {
1141
+ if (!source.has(key) || source.isNull(key)) {
1142
+ return null;
1143
+ }
1144
+
1145
+ Object value = source.opt(key);
1146
+ if (value instanceof Number) {
1147
+ return ((Number) value).doubleValue();
1148
+ }
1149
+
1150
+ if (value instanceof String) {
1151
+ try {
1152
+ return Double.parseDouble(((String) value).trim());
1153
+ } catch (NumberFormatException ignored) {
1154
+ return null;
1155
+ }
1156
+ }
1157
+
1158
+ return null;
1159
+ }
1160
+
1161
+ @Nullable
1162
+ private static Double normalizePercent(@Nullable Double value) {
1163
+ if (value == null || value.isNaN() || value.isInfinite()) {
1164
+ return null;
1165
+ }
1166
+ return Math.max(0.0, Math.min(100.0, value));
1167
+ }
1168
+
1169
+ @Nullable
1170
+ private static JSONObject copyJsonObject(@Nullable JSONObject value) {
1171
+ if (value == null || value.length() == 0) {
1172
+ return null;
1173
+ }
1174
+ try {
1175
+ return new JSONObject(value.toString());
1176
+ } catch (Exception error) {
1177
+ return null;
1178
+ }
1179
+ }
750
1180
  }
751
1181
  }
package/dist/docs.json CHANGED
@@ -255,6 +255,34 @@
255
255
  "complexTypes": [],
256
256
  "type": "number | undefined"
257
257
  },
258
+ {
259
+ "name": "currentVersionSharePercent",
260
+ "tags": [],
261
+ "docs": "Device-share percentage for the installed WebView major version.\n\nPresent only when a version-share dataset is available and includes the current major version.",
262
+ "complexTypes": [],
263
+ "type": "number | undefined"
264
+ },
265
+ {
266
+ "name": "minimumDeviceSharePercent",
267
+ "tags": [],
268
+ "docs": "Configured threshold used by compatibility-threshold mode.\n\nPresent only when `minimumDeviceSharePercent` was requested for that check.",
269
+ "complexTypes": [],
270
+ "type": "number | undefined"
271
+ },
272
+ {
273
+ "name": "versionShareSource",
274
+ "tags": [],
275
+ "docs": "Source used for version-share data.\n\nValues:\n- `versionShareByMajor` (inline dataset)\n- `versionShareApiUrl` (remote dataset)\n- `generatedVersionShareByMajor` (built-in dataset generated at build time)",
276
+ "complexTypes": [],
277
+ "type": "string | undefined"
278
+ },
279
+ {
280
+ "name": "deviceShareError",
281
+ "tags": [],
282
+ "docs": "Diagnostic message for compatibility-threshold mode.\n\nPresent when `minimumDeviceSharePercent` was requested but the threshold check\ncould not be fully evaluated.",
283
+ "complexTypes": [],
284
+ "type": "string | undefined"
285
+ },
258
286
  {
259
287
  "name": "providerPackage",
260
288
  "tags": [],