@capgo/capacitor-updater 8.47.3 → 8.47.5

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.md CHANGED
@@ -2200,9 +2200,10 @@ If you don't use backend, you need to provide the URL and version of the bundle.
2200
2200
 
2201
2201
  ##### StartPreviewSessionOptions
2202
2202
 
2203
- | Prop | Type | Description | Default | Since |
2204
- | ----------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ------ |
2205
- | **`appId`** | <code>string</code> | App id to use while the preview session is active. The previous app id is restored when leaving the preview session. Requires {@link PluginsConfig.CapacitorUpdater.allowPreview} to be `true`. | <code>undefined</code> | 8.47.0 |
2203
+ | Prop | Type | Description | Default | Since |
2204
+ | ---------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ------ |
2205
+ | **`appId`** | <code>string</code> | App id to use while the preview session is active. The previous app id is restored when leaving the preview session. Requires {@link PluginsConfig.CapacitorUpdater.allowPreview} to be `true`. | <code>undefined</code> | 8.47.0 |
2206
+ | **`payloadUrl`** | <code>string</code> | HTTP(S) URL returning a preview download payload. When provided, the native shake reload action fetches this payload again before reloading so channel previews can move to the latest bundle. Requires {@link PluginsConfig.CapacitorUpdater.allowPreview} to be `true`. | <code>undefined</code> | 8.48.0 |
2206
2207
 
2207
2208
 
2208
2209
  ##### BundleListResult
@@ -49,9 +49,13 @@ import com.google.android.play.core.install.model.AppUpdateType;
49
49
  import com.google.android.play.core.install.model.InstallStatus;
50
50
  import com.google.android.play.core.install.model.UpdateAvailability;
51
51
  import io.github.g00fy2.versioncompare.Version;
52
+ import java.io.ByteArrayOutputStream;
52
53
  import java.io.IOException;
54
+ import java.io.InputStream;
55
+ import java.net.HttpURLConnection;
53
56
  import java.net.MalformedURLException;
54
57
  import java.net.URL;
58
+ import java.nio.charset.StandardCharsets;
55
59
  import java.util.ArrayList;
56
60
  import java.util.Date;
57
61
  import java.util.HashMap;
@@ -98,7 +102,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
98
102
  private static final String PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY = "CapacitorUpdater.previewPreviousShakeChannelSelector";
99
103
  private static final String PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY = "CapacitorUpdater.previewPreviousNextBundle";
100
104
  private static final String PREVIEW_PREVIOUS_APP_ID_PREF_KEY = "CapacitorUpdater.previewPreviousAppId";
105
+ private static final String PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY = "CapacitorUpdater.previewPreviousDefaultChannel";
106
+ private static final String PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY = "CapacitorUpdater.previewPreviousDefaultChannelWasSet";
101
107
  private static final String PREVIEW_APP_ID_PREF_KEY = "CapacitorUpdater.previewAppId";
108
+ private static final String PREVIEW_PAYLOAD_URL_PREF_KEY = "CapacitorUpdater.previewPayloadUrl";
109
+ private static final String PREVIEW_SESSION_ALERT_PENDING_PREF_KEY = "CapacitorUpdater.previewSessionAlertPending";
102
110
  private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
103
111
  private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
104
112
  private static final String LAST_REPORTED_APP_EXIT_TIMESTAMP_PREF_KEY = "CapacitorUpdater.lastReportedAppExitTimestamp";
@@ -120,7 +128,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
120
128
  static final int APPLICATION_EXIT_REASON_USER_REQUESTED = 10;
121
129
  static final int APPLICATION_EXIT_REASON_DEPENDENCY_DIED = 12;
122
130
 
123
- private final String pluginVersion = "8.47.3";
131
+ private final String pluginVersion = "8.47.5";
124
132
  private static final String DELAY_CONDITION_PREFERENCES = "";
125
133
 
126
134
  private SharedPreferences.Editor editor;
@@ -153,6 +161,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
153
161
  Boolean shakeChannelSelectorEnabled = false;
154
162
  Boolean previewSessionEnabled = false;
155
163
  private Boolean previewSessionAlertPending = false;
164
+ private Boolean isLeavingPreviewForIncomingLink = false;
156
165
  private Boolean allowManualBundleError = false;
157
166
  private Boolean allowPreview = false;
158
167
  Boolean allowSetDefaultChannel = true;
@@ -495,6 +504,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
495
504
  }
496
505
  this.implementation.previewSession = Boolean.TRUE.equals(this.previewSessionEnabled);
497
506
  if (Boolean.TRUE.equals(this.previewSessionEnabled)) {
507
+ this.previewSessionAlertPending = this.prefs.contains(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY)
508
+ ? this.prefs.getBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, false)
509
+ : true;
498
510
  final String previewAppId = this.prefs.getString(PREVIEW_APP_ID_PREF_KEY, "");
499
511
  if (previewAppId != null && !previewAppId.isEmpty()) {
500
512
  this.setActiveAppId(previewAppId);
@@ -513,6 +525,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
513
525
  if (nativeBuildVersionChanged) {
514
526
  this.clearPreviewSessionForNativeBuildChange();
515
527
  }
528
+ this.leavePreviewSessionForLaunchIntentIfNeeded();
516
529
  this.reportPreviousAppExitReasons();
517
530
  this.reportPreviousWebViewRenderProcessGone();
518
531
  this.installWebViewStatsReporter();
@@ -527,6 +540,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
527
540
  this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.KILLED);
528
541
 
529
542
  this.checkForUpdateAfterDelay();
543
+ this.showPreviewSessionNoticeIfNeeded();
530
544
 
531
545
  // On Android 14+ (API 34+), topActivity in RecentTaskInfo returns null due to
532
546
  // security restrictions (StrandHogg task hijacking mitigations). Use ProcessLifecycleOwner
@@ -1930,6 +1944,20 @@ public class CapacitorUpdaterPlugin extends Plugin {
1930
1944
  }
1931
1945
  }
1932
1946
 
1947
+ private BundleInfo downloadBundle(
1948
+ final String url,
1949
+ final String version,
1950
+ final String sessionKey,
1951
+ final String checksum,
1952
+ final JSONArray manifest
1953
+ ) throws IOException {
1954
+ if (manifest != null) {
1955
+ return this.implementation.downloadManifest(url, version, sessionKey, checksum, manifest);
1956
+ }
1957
+
1958
+ return this.implementation.download(url, version, sessionKey, checksum);
1959
+ }
1960
+
1933
1961
  @PluginMethod
1934
1962
  public void download(final PluginCall call) {
1935
1963
  final String url = call.getString("url");
@@ -1951,20 +1979,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1951
1979
  logger.info("Downloading " + url);
1952
1980
  startNewThread(() -> {
1953
1981
  try {
1954
- final BundleInfo downloaded;
1955
- if (manifest != null) {
1956
- // For manifest downloads, we need to handle this asynchronously
1957
- // to avoid automatically scheduling/applying the downloaded bundle.
1958
- // Manual download must not schedule/apply the bundle automatically.
1959
- CapacitorUpdaterPlugin.this.implementation.downloadBackground(url, version, sessionKey, checksum, manifest, false);
1960
- // Return immediately with a pending status - the actual result will come via listeners
1961
- final String id = CapacitorUpdaterPlugin.this.implementation.randomString();
1962
- downloaded = new BundleInfo(id, version, BundleStatus.DOWNLOADING, new Date(System.currentTimeMillis()), "");
1963
- call.resolve(InternalUtils.mapToJSObject(downloaded.toJSONMap()));
1964
- return;
1965
- } else {
1966
- downloaded = CapacitorUpdaterPlugin.this.implementation.download(url, version, sessionKey, checksum);
1967
- }
1982
+ final BundleInfo downloaded = this.downloadBundle(url, version, sessionKey, checksum, manifest);
1968
1983
  if (downloaded.isErrorStatus()) {
1969
1984
  throw new RuntimeException("Download failed: " + downloaded.getStatus());
1970
1985
  } else {
@@ -2231,6 +2246,13 @@ public class CapacitorUpdaterPlugin extends Plugin {
2231
2246
  return;
2232
2247
  }
2233
2248
  final String previewAppId = this.normalizePreviewAppId(call.getString("appId"));
2249
+ final String rawPayloadUrl = call.getString("payloadUrl");
2250
+ final String previewPayloadUrl = this.normalizePreviewPayloadUrl(rawPayloadUrl);
2251
+ if (this.hasPreviewPayloadUrl(rawPayloadUrl) && previewPayloadUrl == null) {
2252
+ logger.error("startPreviewSession called with invalid payloadUrl");
2253
+ call.reject("Invalid preview payloadUrl");
2254
+ return;
2255
+ }
2234
2256
  startNewThread(() -> {
2235
2257
  try {
2236
2258
  if (!Boolean.TRUE.equals(this.previewSessionEnabled)) {
@@ -2249,6 +2271,16 @@ public class CapacitorUpdaterPlugin extends Plugin {
2249
2271
  }
2250
2272
 
2251
2273
  this.editor.putString(PREVIEW_PREVIOUS_APP_ID_PREF_KEY, this.implementation.appId);
2274
+ if (this.prefs.contains(DEFAULT_CHANNEL_PREF_KEY)) {
2275
+ this.editor.putString(
2276
+ PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY,
2277
+ this.prefs.getString(DEFAULT_CHANNEL_PREF_KEY, "")
2278
+ );
2279
+ this.editor.putBoolean(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY, true);
2280
+ } else {
2281
+ this.editor.remove(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY);
2282
+ this.editor.putBoolean(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY, false);
2283
+ }
2252
2284
  this.editor.putBoolean(PREVIEW_PREVIOUS_SHAKE_MENU_PREF_KEY, Boolean.TRUE.equals(this.shakeMenuEnabled));
2253
2285
  this.editor.putBoolean(
2254
2286
  PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY,
@@ -2263,12 +2295,20 @@ public class CapacitorUpdaterPlugin extends Plugin {
2263
2295
  logger.info("Preview session using appId: " + previewAppId);
2264
2296
  }
2265
2297
 
2298
+ if (previewPayloadUrl != null) {
2299
+ this.editor.putString(PREVIEW_PAYLOAD_URL_PREF_KEY, previewPayloadUrl);
2300
+ logger.info("Preview session using payload URL");
2301
+ } else {
2302
+ this.editor.remove(PREVIEW_PAYLOAD_URL_PREF_KEY);
2303
+ }
2304
+
2266
2305
  this.previewSessionEnabled = true;
2267
2306
  this.previewSessionAlertPending = true;
2268
2307
  this.implementation.previewSession = true;
2269
2308
  this.shakeMenuEnabled = true;
2270
2309
  this.shakeChannelSelectorEnabled = false;
2271
2310
  this.editor.putBoolean(PREVIEW_SESSION_PREF_KEY, true);
2311
+ this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
2272
2312
  this.editor.apply();
2273
2313
  this.ensureShakeMenuStarted();
2274
2314
  call.resolve();
@@ -2287,12 +2327,61 @@ public class CapacitorUpdaterPlugin extends Plugin {
2287
2327
  return false;
2288
2328
  }
2289
2329
 
2290
- if (!this.clearPreviewChannelOverride()) {
2291
- return false;
2330
+ final BundleInfo previewFallbackBundle = this.implementation.getPreviewFallbackBundle();
2331
+ this.endPreviewSession();
2332
+ final BundleInfo restoredNextBundle = this.implementation.getNextBundle();
2333
+ this.deletePreviewBundleIfUnused(previewBundle, previewFallbackBundle, restoredNextBundle);
2334
+ return true;
2335
+ }
2336
+
2337
+ private void leavePreviewSessionForLaunchIntentIfNeeded() {
2338
+ final Intent intent = getActivity() == null ? null : getActivity().getIntent();
2339
+ if (
2340
+ intent == null ||
2341
+ !Intent.ACTION_VIEW.equals(intent.getAction()) ||
2342
+ intent.getData() == null ||
2343
+ !Boolean.TRUE.equals(this.previewSessionEnabled) ||
2344
+ !isPreviewDeepLink(intent.getData()) ||
2345
+ Boolean.TRUE.equals(this.isLeavingPreviewForIncomingLink)
2346
+ ) {
2347
+ return;
2292
2348
  }
2349
+
2350
+ this.isLeavingPreviewForIncomingLink = true;
2351
+ logger.info("Preview deeplink launch detected while preview session is active; restoring fallback before initial load");
2352
+ if (!this.leavePreviewSessionWithoutReload()) {
2353
+ logger.error("Could not leave preview session before initial preview deeplink routing");
2354
+ this.isLeavingPreviewForIncomingLink = false;
2355
+ }
2356
+ }
2357
+
2358
+ private boolean leavePreviewSessionWithoutReload() {
2359
+ final BundleInfo previewBundle = this.implementation.getCurrentBundle();
2293
2360
  final BundleInfo previewFallbackBundle = this.implementation.getPreviewFallbackBundle();
2361
+ if (previewFallbackBundle == null || previewFallbackBundle.isErrorStatus()) {
2362
+ logger.error("No preview fallback bundle available");
2363
+ return false;
2364
+ }
2365
+ if (!this.implementation.canSet(previewFallbackBundle)) {
2366
+ logger.error("Preview fallback bundle is not installable");
2367
+ return false;
2368
+ }
2369
+ if (!this.implementation.stagePreviewFallbackReload(previewFallbackBundle)) {
2370
+ logger.error("Could not stage preview fallback bundle");
2371
+ return false;
2372
+ }
2373
+
2294
2374
  this.endPreviewSession();
2295
2375
  final BundleInfo restoredNextBundle = this.implementation.getNextBundle();
2376
+ this.deletePreviewBundleIfUnused(previewBundle, previewFallbackBundle, restoredNextBundle);
2377
+ return true;
2378
+ }
2379
+
2380
+ private void deletePreviewBundleIfUnused(
2381
+ final BundleInfo previewBundle,
2382
+ final BundleInfo previewFallbackBundle,
2383
+ final BundleInfo restoredNextBundle
2384
+ ) {
2296
2385
  if (
2297
2386
  !previewBundle.isBuiltin() &&
2298
2387
  (previewFallbackBundle == null || !previewBundle.getId().equals(previewFallbackBundle.getId())) &&
@@ -2304,10 +2393,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
2304
2393
  logger.warn("Cannot delete preview bundle " + previewBundle.getId() + ": " + err.getMessage());
2305
2394
  }
2306
2395
  }
2307
- return true;
2308
2396
  }
2309
2397
 
2310
2398
  public boolean reloadPreviewSessionFromShakeMenu() {
2399
+ final String payloadUrl = this.storedPreviewPayloadUrl();
2400
+ if (payloadUrl != null) {
2401
+ return this.refreshPreviewSessionFromPayloadUrl(payloadUrl);
2402
+ }
2403
+
2311
2404
  return this._reload();
2312
2405
  }
2313
2406
 
@@ -2350,9 +2443,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
2350
2443
  );
2351
2444
  this.restorePreviewPreviousNextBundle();
2352
2445
  this.restorePreviewPreviousAppId();
2446
+ this.restorePreviewPreviousDefaultChannel();
2353
2447
 
2354
2448
  this.previewSessionEnabled = false;
2355
2449
  this.previewSessionAlertPending = false;
2450
+ this.isLeavingPreviewForIncomingLink = false;
2356
2451
  this.implementation.previewSession = false;
2357
2452
  this.shakeMenuEnabled = previousShakeMenuEnabled;
2358
2453
  this.shakeChannelSelectorEnabled = previousShakeChannelSelectorEnabled;
@@ -2376,8 +2471,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
2376
2471
 
2377
2472
  this.restorePreviewPreviousNextBundle();
2378
2473
  this.restorePreviewPreviousAppId();
2474
+ this.restorePreviewPreviousDefaultChannel();
2379
2475
  this.previewSessionEnabled = false;
2380
2476
  this.previewSessionAlertPending = false;
2477
+ this.isLeavingPreviewForIncomingLink = false;
2381
2478
  this.implementation.previewSession = false;
2382
2479
  this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
2383
2480
  this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
@@ -2393,7 +2490,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
2393
2490
  this.editor.remove(PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY);
2394
2491
  this.editor.remove(PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY);
2395
2492
  this.editor.remove(PREVIEW_PREVIOUS_APP_ID_PREF_KEY);
2493
+ this.editor.remove(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY);
2494
+ this.editor.remove(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY);
2396
2495
  this.editor.remove(PREVIEW_APP_ID_PREF_KEY);
2496
+ this.editor.remove(PREVIEW_PAYLOAD_URL_PREF_KEY);
2497
+ this.editor.remove(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY);
2397
2498
  this.editor.apply();
2398
2499
  }
2399
2500
 
@@ -2413,6 +2514,23 @@ public class CapacitorUpdaterPlugin extends Plugin {
2413
2514
  logger.info("Restored appId after preview: " + previousAppId);
2414
2515
  }
2415
2516
 
2517
+ private void restorePreviewPreviousDefaultChannel() {
2518
+ final String configDefaultChannel = this.getConfig().getString("defaultChannel", "");
2519
+ if (this.prefs.getBoolean(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY, false)) {
2520
+ final String previousDefaultChannel = this.prefs.getString(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY, "");
2521
+ this.editor.putString(DEFAULT_CHANNEL_PREF_KEY, previousDefaultChannel);
2522
+ this.implementation.defaultChannel = previousDefaultChannel;
2523
+ this.editor.apply();
2524
+ logger.info("Restored defaultChannel after preview");
2525
+ return;
2526
+ }
2527
+
2528
+ this.editor.remove(DEFAULT_CHANNEL_PREF_KEY);
2529
+ this.implementation.defaultChannel = configDefaultChannel;
2530
+ this.editor.apply();
2531
+ logger.info("Restored defaultChannel after preview to config value");
2532
+ }
2533
+
2416
2534
  private String normalizePreviewAppId(final String rawAppId) {
2417
2535
  if (rawAppId == null) {
2418
2536
  return null;
@@ -2431,6 +2549,146 @@ public class CapacitorUpdaterPlugin extends Plugin {
2431
2549
  return appId;
2432
2550
  }
2433
2551
 
2552
+ private boolean hasPreviewPayloadUrl(final String rawPayloadUrl) {
2553
+ if (rawPayloadUrl == null) {
2554
+ return false;
2555
+ }
2556
+
2557
+ final String payloadUrl = rawPayloadUrl.trim();
2558
+ if (payloadUrl.isEmpty()) {
2559
+ return false;
2560
+ }
2561
+
2562
+ final String lowercasedPayloadUrl = payloadUrl.toLowerCase(java.util.Locale.ROOT);
2563
+ return !"undefined".equals(lowercasedPayloadUrl) && !"null".equals(lowercasedPayloadUrl);
2564
+ }
2565
+
2566
+ private String normalizePreviewPayloadUrl(final String rawPayloadUrl) {
2567
+ if (!this.hasPreviewPayloadUrl(rawPayloadUrl)) {
2568
+ return null;
2569
+ }
2570
+
2571
+ final String payloadUrl = rawPayloadUrl.trim();
2572
+ try {
2573
+ final URL parsedUrl = new URL(payloadUrl);
2574
+ final String protocol = parsedUrl.getProtocol();
2575
+ if (!"https".equals(protocol) && !"http".equals(protocol)) {
2576
+ return null;
2577
+ }
2578
+ return parsedUrl.toString();
2579
+ } catch (final MalformedURLException ignored) {
2580
+ return null;
2581
+ }
2582
+ }
2583
+
2584
+ private String storedPreviewPayloadUrl() {
2585
+ return this.normalizePreviewPayloadUrl(this.prefs.getString(PREVIEW_PAYLOAD_URL_PREF_KEY, null));
2586
+ }
2587
+
2588
+ private String previewPathFromUri(final Uri uri) {
2589
+ if ("capgo".equals(uri.getScheme())) {
2590
+ final String host = uri.getHost();
2591
+ final String path = uri.getPath();
2592
+ return ("/" + (host == null ? "" : host) + (path == null ? "" : path)).replaceAll("/+", "/");
2593
+ }
2594
+
2595
+ return uri.getPath();
2596
+ }
2597
+
2598
+ private boolean isPreviewDeepLink(final Uri uri) {
2599
+ final String path = this.previewPathFromUri(uri);
2600
+ return "/preview/channel".equals(path) || "/preview/bundle".equals(path);
2601
+ }
2602
+
2603
+ private String readResponseBody(final InputStream stream) throws IOException {
2604
+ if (stream == null) {
2605
+ return "";
2606
+ }
2607
+
2608
+ try (InputStream input = stream; ByteArrayOutputStream output = new ByteArrayOutputStream()) {
2609
+ final byte[] buffer = new byte[8192];
2610
+ int read;
2611
+ while ((read = input.read(buffer)) != -1) {
2612
+ output.write(buffer, 0, read);
2613
+ }
2614
+ return output.toString(StandardCharsets.UTF_8.name());
2615
+ }
2616
+ }
2617
+
2618
+ private JSONObject fetchPreviewPayload(final String payloadUrl) throws IOException, JSONException {
2619
+ final HttpURLConnection connection = (HttpURLConnection) new URL(payloadUrl).openConnection();
2620
+ connection.setRequestMethod("GET");
2621
+ connection.setRequestProperty("Accept", "application/json");
2622
+ connection.setConnectTimeout(30000);
2623
+ connection.setReadTimeout(60000);
2624
+
2625
+ try {
2626
+ final int statusCode = connection.getResponseCode();
2627
+ final String body = this.readResponseBody(
2628
+ statusCode >= 200 && statusCode < 300 ? connection.getInputStream() : connection.getErrorStream()
2629
+ );
2630
+ final JSONObject payload = new JSONObject(body);
2631
+ if (statusCode < 200 || statusCode >= 300) {
2632
+ throw new IOException(
2633
+ payload.optString("message", payload.optString("error", "Preview payload request failed with HTTP " + statusCode))
2634
+ );
2635
+ }
2636
+ return payload;
2637
+ } finally {
2638
+ connection.disconnect();
2639
+ }
2640
+ }
2641
+
2642
+ private BundleInfo downloadPreviewPayloadBundle(final JSONObject payload) throws IOException, JSONException {
2643
+ final String version = payload.optString("version", "").trim();
2644
+ if (version.isEmpty()) {
2645
+ throw new IOException("Preview payload is missing a version");
2646
+ }
2647
+
2648
+ final JSONArray manifest = payload.optJSONArray("manifest");
2649
+ final String url = payload.optString("url", "");
2650
+ if ((url == null || url.isEmpty()) && (manifest == null || manifest.length() == 0)) {
2651
+ throw new IOException("Preview payload is missing download information");
2652
+ }
2653
+
2654
+ return this.downloadBundle(
2655
+ url == null || url.isEmpty() ? "https://404.capgo.app/no.zip" : url,
2656
+ version,
2657
+ payload.optString("sessionKey", ""),
2658
+ payload.optString("checksum", ""),
2659
+ manifest
2660
+ );
2661
+ }
2662
+
2663
+ private boolean refreshPreviewSessionFromPayloadUrl(final String payloadUrl) {
2664
+ try {
2665
+ final JSONObject payload = this.fetchPreviewPayload(payloadUrl);
2666
+ final String version = payload.optString("version", "").trim();
2667
+ if (version.isEmpty()) {
2668
+ throw new IOException("Preview payload is missing a version");
2669
+ }
2670
+
2671
+ if (version.equals(this.implementation.getCurrentBundle().getVersionName())) {
2672
+ logger.info("Preview payload unchanged, reloading current bundle");
2673
+ return this._reload();
2674
+ }
2675
+
2676
+ final BundleInfo next = this.downloadPreviewPayloadBundle(payload);
2677
+ if (next.isErrorStatus()) {
2678
+ throw new IOException("Download failed: " + next.getStatus());
2679
+ }
2680
+ if (!this.implementation.set(next.getId())) {
2681
+ throw new IOException("Downloaded preview bundle cannot be applied");
2682
+ }
2683
+
2684
+ this.notifyBundleSet(next);
2685
+ return this._reload();
2686
+ } catch (final Exception err) {
2687
+ logger.error("Could not refresh preview session: " + err.getMessage());
2688
+ return false;
2689
+ }
2690
+ }
2691
+
2434
2692
  private void clearPreviewSessionForNativeBuildChange() {
2435
2693
  if (!Boolean.TRUE.equals(this.previewSessionEnabled) && this.implementation.getPreviewFallbackBundle() == null) {
2436
2694
  return;
@@ -2438,39 +2696,17 @@ public class CapacitorUpdaterPlugin extends Plugin {
2438
2696
  logger.info("Native build changed; clearing preview session state");
2439
2697
  this.previewSessionEnabled = false;
2440
2698
  this.previewSessionAlertPending = false;
2699
+ this.isLeavingPreviewForIncomingLink = false;
2441
2700
  this.implementation.previewSession = false;
2442
2701
  this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
2443
2702
  this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
2444
2703
  this.restorePreviewPreviousAppId();
2704
+ this.restorePreviewPreviousDefaultChannel();
2445
2705
  this.implementation.setPreviewFallbackBundle(null);
2446
2706
  this.implementation.setNextBundle(null);
2447
- this.clearPreviewChannelOverride();
2448
2707
  this.clearPreviewSessionPreferences();
2449
2708
  }
2450
2709
 
2451
- private boolean clearPreviewChannelOverride() {
2452
- final String configDefaultChannel = this.getConfig().getString("defaultChannel", "");
2453
- final AtomicReference<Map<String, Object>> unsetChannelResult = new AtomicReference<>();
2454
- try {
2455
- this.implementation.unsetChannel(this.editor, DEFAULT_CHANNEL_PREF_KEY, configDefaultChannel, unsetChannelResult::set);
2456
- } catch (final Exception err) {
2457
- logger.error("Could not clear preview channel override: " + err.getMessage());
2458
- return false;
2459
- }
2460
-
2461
- final Map<String, Object> result = unsetChannelResult.get();
2462
- if (result == null) {
2463
- logger.error("Could not clear preview channel override: no result");
2464
- return false;
2465
- }
2466
- if (result.containsKey("error")) {
2467
- final Object message = result.getOrDefault("message", result.get("error"));
2468
- logger.error("Could not clear preview channel override: " + message);
2469
- return false;
2470
- }
2471
- return true;
2472
- }
2473
-
2474
2710
  private void restorePreviewPreviousNextBundle() {
2475
2711
  final String previousNextBundleId = this.prefs.getString(PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY, null);
2476
2712
  if (previousNextBundleId == null || previousNextBundleId.isEmpty()) {
@@ -2499,6 +2735,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
2499
2735
  return;
2500
2736
  }
2501
2737
  this.previewSessionAlertPending = false;
2738
+ this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, false);
2739
+ this.editor.apply();
2502
2740
 
2503
2741
  new Handler(Looper.getMainLooper()).postDelayed(
2504
2742
  () -> {
@@ -2508,6 +2746,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
2508
2746
  }
2509
2747
  if (getActivity() == null || getActivity().isFinishing()) {
2510
2748
  this.previewSessionAlertPending = true;
2749
+ this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
2750
+ this.editor.apply();
2511
2751
  return;
2512
2752
  }
2513
2753
 
@@ -2518,6 +2758,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
2518
2758
  .show();
2519
2759
  } catch (final Exception e) {
2520
2760
  this.previewSessionAlertPending = true;
2761
+ this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
2762
+ this.editor.apply();
2521
2763
  logger.warn("Could not show preview session notice: " + e.getMessage());
2522
2764
  }
2523
2765
  },
@@ -3762,6 +4004,34 @@ public class CapacitorUpdaterPlugin extends Plugin {
3762
4004
  }
3763
4005
  }
3764
4006
 
4007
+ @Override
4008
+ protected void handleOnNewIntent(Intent intent) {
4009
+ super.handleOnNewIntent(intent);
4010
+ if (
4011
+ intent == null ||
4012
+ !Intent.ACTION_VIEW.equals(intent.getAction()) ||
4013
+ intent.getData() == null ||
4014
+ !Boolean.TRUE.equals(this.previewSessionEnabled) ||
4015
+ !isPreviewDeepLink(intent.getData()) ||
4016
+ Boolean.TRUE.equals(this.isLeavingPreviewForIncomingLink)
4017
+ ) {
4018
+ return;
4019
+ }
4020
+
4021
+ this.isLeavingPreviewForIncomingLink = true;
4022
+ if (getActivity() != null) {
4023
+ getActivity().setIntent(intent);
4024
+ }
4025
+ logger.info("Preview deeplink received while preview session is active; restoring fallback before routing");
4026
+ startNewThread(() -> {
4027
+ final boolean didLeave = this.leavePreviewSessionFromShakeMenu();
4028
+ if (!didLeave) {
4029
+ logger.error("Could not leave preview session before routing incoming preview deeplink");
4030
+ this.isLeavingPreviewForIncomingLink = false;
4031
+ }
4032
+ });
4033
+ }
4034
+
3765
4035
  @Override
3766
4036
  public void handleOnStart() {
3767
4037
  try {
package/dist/docs.json CHANGED
@@ -1991,6 +1991,22 @@
1991
1991
  "docs": "App id to use while the preview session is active.\nThe previous app id is restored when leaving the preview session.\nRequires {@link PluginsConfig.CapacitorUpdater.allowPreview} to be `true`.",
1992
1992
  "complexTypes": [],
1993
1993
  "type": "string | undefined"
1994
+ },
1995
+ {
1996
+ "name": "payloadUrl",
1997
+ "tags": [
1998
+ {
1999
+ "text": "8.48.0",
2000
+ "name": "since"
2001
+ },
2002
+ {
2003
+ "text": "undefined",
2004
+ "name": "default"
2005
+ }
2006
+ ],
2007
+ "docs": "HTTP(S) URL returning a preview download payload.\nWhen provided, the native shake reload action fetches this payload again\nbefore reloading so channel previews can move to the latest bundle.\nRequires {@link PluginsConfig.CapacitorUpdater.allowPreview} to be `true`.",
2008
+ "complexTypes": [],
2009
+ "type": "string | undefined"
1994
2010
  }
1995
2011
  ]
1996
2012
  },
@@ -1973,6 +1973,15 @@ export interface StartPreviewSessionOptions {
1973
1973
  * @default undefined
1974
1974
  */
1975
1975
  appId?: string;
1976
+ /**
1977
+ * HTTP(S) URL returning a preview download payload.
1978
+ * When provided, the native shake reload action fetches this payload again
1979
+ * before reloading so channel previews can move to the latest bundle.
1980
+ * Requires {@link PluginsConfig.CapacitorUpdater.allowPreview} to be `true`.
1981
+ * @since 8.48.0
1982
+ * @default undefined
1983
+ */
1984
+ payloadUrl?: string;
1976
1985
  }
1977
1986
  export interface AppReadyResult {
1978
1987
  bundle: BundleInfo;