@capgo/capacitor-updater 8.47.6 → 8.47.8

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
@@ -323,8 +323,8 @@ CapacitorUpdater can be configured with these options:
323
323
  | **`keepUrlPathAfterReload`** | <code>boolean</code> | Configure the plugin to keep the URL path after a reload. WARNING: When a reload is triggered, 'window.history' will be cleared. | <code>false</code> | 6.8.0 |
324
324
  | **`disableJSLogging`** | <code>boolean</code> | Disable the JavaScript logging of the plugin. if true, the plugin will not log to the JavaScript console. only the native log will be done | <code>false</code> | 7.3.0 |
325
325
  | **`osLogging`** | <code>boolean</code> | Enable OS-level logging. When enabled, logs are written to the system log which can be inspected in production builds. - **iOS**: Uses os_log instead of Swift.print, logs accessible via Console.app or Instruments - **Android**: Logs to Logcat (android.util.Log) When set to false, system logging is disabled on both platforms (only JavaScript console logging will occur if enabled). This is useful for debugging production apps (App Store/TestFlight builds on iOS, or production APKs on Android). | <code>true</code> | 8.42.0 |
326
- | **`shakeMenu`** | <code>boolean</code> | Enable shake gesture to show update menu for debugging/testing purposes | <code>false</code> | 7.5.0 |
327
- | **`allowShakeChannelSelector`** | <code>boolean</code> | Enable the shake gesture to show a channel selector menu for switching between update channels. When enabled AND `shakeMenu` is true, the shake gesture shows a channel selector instead of the default debug menu (Go Home/Reload/Close). After selecting a channel, the app automatically checks for updates and downloads if available. Only works if channels have `allow_self_set` enabled on the backend. Only available for Android and iOS. | <code>false</code> | 8.43.0 |
326
+ | **`shakeMenu`** | <code>boolean</code> | Enable the shake gesture while a preview session is active. Outside preview sessions this preview menu is ignored, unless {@link PluginsConfig.CapacitorUpdater.allowShakeChannelSelector} is enabled. | <code>false</code> | 7.5.0 |
327
+ | **`allowShakeChannelSelector`** | <code>boolean</code> | Enable the shake gesture to show a channel selector menu for switching between update channels. If {@link PluginsConfig.CapacitorUpdater.shakeMenu} is also enabled while a preview session is active, the shake menu includes both preview actions and channel switching. Only available for Android and iOS. | <code>false</code> | 8.43.0 |
328
328
 
329
329
  ### Examples
330
330
 
@@ -1818,19 +1818,15 @@ Use this to:
1818
1818
  setShakeMenu(options: SetShakeMenuOptions) => Promise<void>
1819
1819
  ```
1820
1820
 
1821
- Enable or disable the shake gesture menu for debugging and testing.
1821
+ Enable or disable the shake gesture menu.
1822
1822
 
1823
- When enabled, users can shake their device to open a debug menu that shows:
1824
- - Current bundle information
1825
- - Available bundles
1826
- - Options to switch bundles manually
1827
- - Update status
1823
+ During preview sessions, users can shake their device to:
1824
+ - Reload the current preview
1825
+ - Leave the test app and return to the fallback bundle
1826
+ - Switch update channel, when {@link PluginsConfig.CapacitorUpdater.allowShakeChannelSelector} is also enabled
1828
1827
 
1829
- This is useful during development and testing to:
1830
- - Quickly test different bundle versions
1831
- - Debug update flows
1832
- - Switch between production and test bundles
1833
- - Verify bundle installations
1828
+ Outside preview sessions, this preview menu is ignored. The channel selector can still be
1829
+ shown outside preview sessions when {@link PluginsConfig.CapacitorUpdater.allowShakeChannelSelector} is enabled.
1834
1830
 
1835
1831
  **Important:** Disable this in production builds or only enable for internal testers.
1836
1832
 
@@ -1876,12 +1872,9 @@ setShakeChannelSelector(options: SetShakeChannelSelectorOptions) => Promise<void
1876
1872
 
1877
1873
  Enable or disable the shake channel selector at runtime.
1878
1874
 
1879
- When enabled AND shakeMenu is true, shaking the device shows a channel
1880
- selector instead of the debug menu. This allows users to switch between
1881
- update channels by shaking their device.
1882
-
1883
- After selecting a channel, the app automatically checks for updates
1884
- and downloads if available.
1875
+ When enabled, shaking the device can show a channel selector, including outside preview sessions.
1876
+ If {@link setShakeMenu} is also enabled while a preview session is active, the shake menu includes
1877
+ both preview actions and channel switching.
1885
1878
 
1886
1879
  Can also be configured via {@link PluginsConfig.CapacitorUpdater.allowShakeChannelSelector}.
1887
1880
 
@@ -2386,7 +2379,7 @@ Result returned after requesting an immediate native auto-update check.
2386
2379
 
2387
2380
  | Prop | Type | Description | Since |
2388
2381
  | -------------------- | -------------------- | ----------------------------------------------- | ----- |
2389
- | **`id`** | <code>string</code> | The channel ID | 7.5.0 |
2382
+ | **`id`** | <code>number</code> | The channel ID | 7.5.0 |
2390
2383
  | **`name`** | <code>string</code> | The channel name | 7.5.0 |
2391
2384
  | **`public`** | <code>boolean</code> | Whether this is a public channel | 7.5.0 |
2392
2385
  | **`allow_self_set`** | <code>boolean</code> | Whether devices can self-assign to this channel | 7.5.0 |
@@ -115,6 +115,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
115
115
  private static final int SPLASH_SCREEN_RETRY_DELAY_MS = 100;
116
116
  private static final int SPLASH_SCREEN_MAX_RETRIES = 20;
117
117
  private static final long PENDING_BUNDLE_APP_READY_MIN_TIMEOUT_MS = 30000L;
118
+ private static final long PREVIEW_TRANSITION_LOADER_TIMEOUT_MS = 60000L;
118
119
  static final int APPLICATION_EXIT_REASON_UNKNOWN = 0;
119
120
  static final int APPLICATION_EXIT_REASON_EXIT_SELF = 1;
120
121
  static final int APPLICATION_EXIT_REASON_SIGNALED = 2;
@@ -128,7 +129,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
128
129
  static final int APPLICATION_EXIT_REASON_USER_REQUESTED = 10;
129
130
  static final int APPLICATION_EXIT_REASON_DEPENDENCY_DIED = 12;
130
131
 
131
- private final String pluginVersion = "8.47.6";
132
+ private final String pluginVersion = "8.47.8";
132
133
  private static final String DELAY_CONDITION_PREFERENCES = "";
133
134
 
134
135
  private SharedPreferences.Editor editor;
@@ -222,6 +223,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
222
223
  private final Handler mainHandler = new Handler(Looper.getMainLooper());
223
224
  private FrameLayout splashscreenLoaderOverlay;
224
225
  private Runnable splashscreenTimeoutRunnable;
226
+ private FrameLayout previewTransitionLoaderOverlay;
227
+ private Runnable previewTransitionLoaderTimeoutRunnable;
228
+ private boolean previewTransitionLoaderRequested = false;
225
229
  private WebViewListener webViewStatsListener;
226
230
 
227
231
  private static final class FireAndForgetPluginCall extends PluginCall {
@@ -537,7 +541,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
537
541
  logger.info("Using preview appId " + previewAppId);
538
542
  }
539
543
  this.shakeMenuEnabled = true;
540
- this.shakeChannelSelectorEnabled = false;
544
+ this.shakeChannelSelectorEnabled = this.prefs.contains(PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY)
545
+ ? this.prefs.getBoolean(PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY, false)
546
+ : this.shakeChannelSelectorEnabled;
541
547
  }
542
548
  boolean resetWhenUpdate = this.getConfig().getBoolean("resetWhenUpdate", true);
543
549
 
@@ -682,6 +688,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
682
688
  if (this.autoSplashscreen) {
683
689
  this.hideSplashscreen();
684
690
  }
691
+ this.hidePreviewTransitionLoader("app-ready");
685
692
  }
686
693
 
687
694
  private void hideSplashscreen() {
@@ -793,6 +800,38 @@ public class CapacitorUpdaterPlugin extends Plugin {
793
800
  return requestToken == this.splashscreenInvocationToken;
794
801
  }
795
802
 
803
+ private FrameLayout createLoaderOverlay(final Activity activity, final boolean blocksTouches, final int backgroundColor) {
804
+ final ProgressBar progressBar = new ProgressBar(activity);
805
+ progressBar.setIndeterminate(true);
806
+
807
+ final FrameLayout overlay = new FrameLayout(activity);
808
+ overlay.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
809
+ overlay.setClickable(blocksTouches);
810
+ overlay.setFocusable(blocksTouches);
811
+ overlay.setBackgroundColor(backgroundColor);
812
+ overlay.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
813
+
814
+ final FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
815
+ ViewGroup.LayoutParams.WRAP_CONTENT,
816
+ ViewGroup.LayoutParams.WRAP_CONTENT
817
+ );
818
+ params.gravity = Gravity.CENTER;
819
+ overlay.addView(progressBar, params);
820
+ return overlay;
821
+ }
822
+
823
+ private void attachLoaderOverlay(final Activity activity, final FrameLayout overlay) {
824
+ final ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
825
+ decorView.addView(overlay);
826
+ }
827
+
828
+ private void removeLoaderOverlay(final FrameLayout overlay) {
829
+ final ViewGroup parent = (ViewGroup) overlay.getParent();
830
+ if (parent != null) {
831
+ parent.removeView(overlay);
832
+ }
833
+ }
834
+
796
835
  private void addSplashscreenLoaderIfNeeded() {
797
836
  if (!Boolean.TRUE.equals(this.autoSplashscreenLoader)) {
798
837
  return;
@@ -809,26 +848,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
809
848
  return;
810
849
  }
811
850
 
812
- ProgressBar progressBar = new ProgressBar(activity);
813
- progressBar.setIndeterminate(true);
814
-
815
- FrameLayout overlay = new FrameLayout(activity);
816
- overlay.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
817
- overlay.setClickable(false);
818
- overlay.setFocusable(false);
819
- overlay.setBackgroundColor(Color.TRANSPARENT);
820
- overlay.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
821
-
822
- FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
823
- ViewGroup.LayoutParams.WRAP_CONTENT,
824
- ViewGroup.LayoutParams.WRAP_CONTENT
825
- );
826
- params.gravity = Gravity.CENTER;
827
- overlay.addView(progressBar, params);
828
-
829
- ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
830
- decorView.addView(overlay);
831
-
851
+ FrameLayout overlay = createLoaderOverlay(activity, false, Color.TRANSPARENT);
852
+ attachLoaderOverlay(activity, overlay);
832
853
  this.splashscreenLoaderOverlay = overlay;
833
854
  };
834
855
 
@@ -842,10 +863,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
842
863
  private void removeSplashscreenLoader() {
843
864
  Runnable removeLoader = () -> {
844
865
  if (this.splashscreenLoaderOverlay != null) {
845
- ViewGroup parent = (ViewGroup) this.splashscreenLoaderOverlay.getParent();
846
- if (parent != null) {
847
- parent.removeView(this.splashscreenLoaderOverlay);
848
- }
866
+ removeLoaderOverlay(this.splashscreenLoaderOverlay);
849
867
  this.splashscreenLoaderOverlay = null;
850
868
  }
851
869
  };
@@ -857,6 +875,84 @@ public class CapacitorUpdaterPlugin extends Plugin {
857
875
  }
858
876
  }
859
877
 
878
+ private void showPreviewTransitionLoader(final String reason) {
879
+ this.previewTransitionLoaderRequested = true;
880
+ final Runnable showLoader = () -> {
881
+ if (!this.previewTransitionLoaderRequested) {
882
+ return;
883
+ }
884
+
885
+ if (this.previewTransitionLoaderOverlay != null) {
886
+ cancelPreviewTransitionLoaderTimeout();
887
+ schedulePreviewTransitionLoaderTimeout();
888
+ this.previewTransitionLoaderOverlay.bringToFront();
889
+ return;
890
+ }
891
+
892
+ final Activity activity = getActivity();
893
+ if (activity == null) {
894
+ logger.warn("Preview transition loader unavailable: activity missing for " + reason);
895
+ this.previewTransitionLoaderRequested = false;
896
+ return;
897
+ }
898
+
899
+ cancelPreviewTransitionLoaderTimeout();
900
+ schedulePreviewTransitionLoaderTimeout();
901
+
902
+ final FrameLayout overlay = createLoaderOverlay(activity, true, Color.argb(46, 0, 0, 0));
903
+ attachLoaderOverlay(activity, overlay);
904
+ this.previewTransitionLoaderOverlay = overlay;
905
+ logger.info("Preview transition loader shown: " + reason);
906
+ };
907
+
908
+ if (Looper.myLooper() == Looper.getMainLooper()) {
909
+ showLoader.run();
910
+ } else {
911
+ this.mainHandler.post(showLoader);
912
+ }
913
+ }
914
+
915
+ private void hidePreviewTransitionLoader(final String reason) {
916
+ if (
917
+ !this.previewTransitionLoaderRequested &&
918
+ this.previewTransitionLoaderOverlay == null &&
919
+ this.previewTransitionLoaderTimeoutRunnable == null
920
+ ) {
921
+ return;
922
+ }
923
+
924
+ final Runnable hideLoader = () -> {
925
+ this.previewTransitionLoaderRequested = false;
926
+ cancelPreviewTransitionLoaderTimeout();
927
+ if (this.previewTransitionLoaderOverlay == null) {
928
+ return;
929
+ }
930
+
931
+ removeLoaderOverlay(this.previewTransitionLoaderOverlay);
932
+ this.previewTransitionLoaderOverlay = null;
933
+ logger.info("Preview transition loader hidden: " + reason);
934
+ };
935
+
936
+ if (Looper.myLooper() == Looper.getMainLooper()) {
937
+ hideLoader.run();
938
+ } else {
939
+ this.mainHandler.post(hideLoader);
940
+ }
941
+ }
942
+
943
+ private void schedulePreviewTransitionLoaderTimeout() {
944
+ cancelPreviewTransitionLoaderTimeout();
945
+ this.previewTransitionLoaderTimeoutRunnable = () -> hidePreviewTransitionLoader("preview-transition-timeout");
946
+ this.mainHandler.postDelayed(this.previewTransitionLoaderTimeoutRunnable, PREVIEW_TRANSITION_LOADER_TIMEOUT_MS);
947
+ }
948
+
949
+ private void cancelPreviewTransitionLoaderTimeout() {
950
+ if (this.previewTransitionLoaderTimeoutRunnable != null) {
951
+ this.mainHandler.removeCallbacks(this.previewTransitionLoaderTimeoutRunnable);
952
+ this.previewTransitionLoaderTimeoutRunnable = null;
953
+ }
954
+ }
955
+
860
956
  private void scheduleSplashscreenTimeout() {
861
957
  if (this.autoSplashscreenTimeout == null || this.autoSplashscreenTimeout <= 0) {
862
958
  return;
@@ -2325,6 +2421,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2325
2421
  @PluginMethod
2326
2422
  public void startPreviewSession(final PluginCall call) {
2327
2423
  if (!Boolean.TRUE.equals(this.allowPreview)) {
2424
+ this.hidePreviewTransitionLoader("preview-session-not-allowed");
2328
2425
  logger.error("startPreviewSession not allowed set allowPreview in your config to true to enable it");
2329
2426
  call.reject("startPreviewSession not allowed");
2330
2427
  return;
@@ -2333,6 +2430,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2333
2430
  final String rawPayloadUrl = call.getString("payloadUrl");
2334
2431
  final String previewPayloadUrl = this.normalizePreviewPayloadUrl(rawPayloadUrl);
2335
2432
  if (this.hasPreviewPayloadUrl(rawPayloadUrl) && previewPayloadUrl == null) {
2433
+ this.hidePreviewTransitionLoader("preview-session-invalid-payload");
2336
2434
  logger.error("startPreviewSession called with invalid payloadUrl");
2337
2435
  call.reject("Invalid preview payloadUrl");
2338
2436
  return;
@@ -2342,6 +2440,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2342
2440
  if (!Boolean.TRUE.equals(this.previewSessionEnabled)) {
2343
2441
  final BundleInfo current = this.implementation.getCurrentBundle();
2344
2442
  if (!this.implementation.setPreviewFallbackBundle(current.getId())) {
2443
+ this.hidePreviewTransitionLoader("preview-session-fallback-failed");
2345
2444
  logger.error("Could not save current bundle as preview fallback");
2346
2445
  call.reject("Could not save current bundle as preview fallback");
2347
2446
  return;
@@ -2386,17 +2485,18 @@ public class CapacitorUpdaterPlugin extends Plugin {
2386
2485
  this.editor.remove(PREVIEW_PAYLOAD_URL_PREF_KEY);
2387
2486
  }
2388
2487
 
2488
+ this.hidePreviewTransitionLoader("preview-session-started");
2389
2489
  this.previewSessionEnabled = true;
2390
2490
  this.previewSessionAlertPending = true;
2391
2491
  this.implementation.previewSession = true;
2392
2492
  this.shakeMenuEnabled = true;
2393
- this.shakeChannelSelectorEnabled = false;
2394
2493
  this.editor.putBoolean(PREVIEW_SESSION_PREF_KEY, true);
2395
2494
  this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
2396
2495
  this.editor.apply();
2397
2496
  this.ensureShakeMenuStarted();
2398
2497
  call.resolve();
2399
2498
  } catch (final Exception e) {
2499
+ this.hidePreviewTransitionLoader("preview-session-failed");
2400
2500
  logger.error("Could not start preview session " + e.getMessage());
2401
2501
  call.reject("Could not start preview session", e);
2402
2502
  }
@@ -2406,8 +2506,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
2406
2506
  public boolean leavePreviewSessionFromShakeMenu() {
2407
2507
  final BundleInfo previewBundle = this.implementation.getCurrentBundle();
2408
2508
 
2509
+ this.showPreviewTransitionLoader("leave-preview-session");
2409
2510
  final boolean didReset = this.resetToPreviewFallbackBundle();
2410
2511
  if (!didReset) {
2512
+ this.hidePreviewTransitionLoader("leave-preview-session-failed");
2411
2513
  return false;
2412
2514
  }
2413
2515
 
@@ -2419,8 +2521,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
2419
2521
  }
2420
2522
 
2421
2523
  private boolean leavePreviewSessionForIncomingPreviewLink() {
2524
+ this.showPreviewTransitionLoader("incoming-preview-deeplink");
2422
2525
  final BundleInfo previewBundle = this.implementation.getCurrentBundle();
2423
2526
  final BundleInfo previewFallbackBundle = this.implementation.getPreviewFallbackBundle();
2527
+ boolean didReload = false;
2424
2528
 
2425
2529
  try {
2426
2530
  if (previewFallbackBundle == null || previewFallbackBundle.isErrorStatus()) {
@@ -2443,6 +2547,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2443
2547
  this.restoreLiveBundleStateAfterFailedReload();
2444
2548
  return false;
2445
2549
  }
2550
+ didReload = true;
2446
2551
 
2447
2552
  this.endPreviewSession(true);
2448
2553
  final BundleInfo restoredNextBundle = this.implementation.getNextBundle();
@@ -2450,6 +2555,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
2450
2555
  return true;
2451
2556
  } finally {
2452
2557
  this.clearIncomingPreviewTransition();
2558
+ if (!didReload) {
2559
+ this.hidePreviewTransitionLoader("incoming-preview-deeplink-failed");
2560
+ }
2453
2561
  }
2454
2562
  }
2455
2563
 
@@ -2467,10 +2575,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
2467
2575
  }
2468
2576
 
2469
2577
  this.isLeavingPreviewForIncomingLink = true;
2578
+ this.showPreviewTransitionLoader("preview-launch-deeplink");
2470
2579
  logger.info("Preview deeplink launch detected while preview session is active; restoring fallback before initial load");
2471
2580
  if (!this.leavePreviewSessionWithoutReload()) {
2472
2581
  logger.error("Could not leave preview session before initial preview deeplink routing");
2473
2582
  this.isLeavingPreviewForIncomingLink = false;
2583
+ this.hidePreviewTransitionLoader("preview-launch-deeplink-failed");
2474
2584
  }
2475
2585
  }
2476
2586
 
@@ -2519,12 +2629,19 @@ public class CapacitorUpdaterPlugin extends Plugin {
2519
2629
  }
2520
2630
 
2521
2631
  public boolean reloadPreviewSessionFromShakeMenu() {
2632
+ this.showPreviewTransitionLoader("reload-preview-session");
2633
+ final boolean didReload;
2522
2634
  final String payloadUrl = this.storedPreviewPayloadUrl();
2523
2635
  if (payloadUrl != null) {
2524
- return this.refreshPreviewSessionFromPayloadUrl(payloadUrl);
2636
+ didReload = this.refreshPreviewSessionFromPayloadUrl(payloadUrl);
2637
+ } else {
2638
+ didReload = this.reloadWithoutWaitingForAppReady();
2525
2639
  }
2526
2640
 
2527
- return this.reloadWithoutWaitingForAppReady();
2641
+ if (!didReload) {
2642
+ this.hidePreviewTransitionLoader("reload-preview-session-failed");
2643
+ }
2644
+ return didReload;
2528
2645
  }
2529
2646
 
2530
2647
  public boolean hasActivePreviewSession() {
@@ -2581,6 +2698,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2581
2698
  }
2582
2699
  this.shakeMenuEnabled = previousShakeMenuEnabled;
2583
2700
  this.shakeChannelSelectorEnabled = previousShakeChannelSelectorEnabled;
2701
+ this.syncShakeMenuLifecycle();
2584
2702
  this.implementation.setPreviewFallbackBundle(null);
2585
2703
  this.clearPreviewSessionPreferences();
2586
2704
  logger.info("Preview session ended");
@@ -2605,8 +2723,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
2605
2723
  this.previewSessionAlertPending = false;
2606
2724
  this.isLeavingPreviewForIncomingLink = false;
2607
2725
  this.implementation.previewSession = false;
2726
+ this.hidePreviewTransitionLoader("preview-session-disabled");
2608
2727
  this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
2609
2728
  this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
2729
+ this.syncShakeMenuLifecycle();
2610
2730
  this.clearPreviewSessionPreferences();
2611
2731
  }
2612
2732
 
@@ -2829,6 +2949,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2829
2949
  this.implementation.previewSession = false;
2830
2950
  this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
2831
2951
  this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
2952
+ this.syncShakeMenuLifecycle();
2832
2953
  this.restorePreviewPreviousAppId();
2833
2954
  this.restorePreviewPreviousDefaultChannel();
2834
2955
  this.implementation.setPreviewFallbackBundle(null);
@@ -2859,6 +2980,24 @@ public class CapacitorUpdaterPlugin extends Plugin {
2859
2980
  }
2860
2981
  }
2861
2982
 
2983
+ private void syncShakeMenuLifecycle() {
2984
+ if (this.shouldListenForShake()) {
2985
+ this.ensureShakeMenuStarted();
2986
+ } else if (shakeMenu != null) {
2987
+ try {
2988
+ shakeMenu.stop();
2989
+ shakeMenu = null;
2990
+ logger.info("Shake menu stopped");
2991
+ } catch (Exception e) {
2992
+ logger.error("Failed to stop shake menu: " + e.getMessage());
2993
+ }
2994
+ }
2995
+ }
2996
+
2997
+ private boolean shouldListenForShake() {
2998
+ return Boolean.TRUE.equals(this.shakeMenuEnabled) || Boolean.TRUE.equals(this.shakeChannelSelectorEnabled);
2999
+ }
3000
+
2862
3001
  private void showPreviewSessionNoticeIfNeeded() {
2863
3002
  if (!Boolean.TRUE.equals(this.previewSessionEnabled) || !Boolean.TRUE.equals(this.previewSessionAlertPending)) {
2864
3003
  return;
@@ -3337,6 +3476,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
3337
3476
  this.semaphoreDown();
3338
3477
  logger.info("semaphoreReady countDown done");
3339
3478
  this.clearIncomingPreviewTransition();
3479
+ this.hidePreviewTransitionLoader("notify-app-ready");
3340
3480
  final JSObject ret = new JSObject();
3341
3481
  ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
3342
3482
  call.resolve(ret);
@@ -4185,6 +4325,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
4185
4325
  }
4186
4326
 
4187
4327
  this.isLeavingPreviewForIncomingLink = true;
4328
+ this.showPreviewTransitionLoader("incoming-preview-deeplink");
4188
4329
  if (getActivity() != null) {
4189
4330
  getActivity().setIntent(intent);
4190
4331
  }
@@ -4194,6 +4335,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
4194
4335
  if (!didLeave) {
4195
4336
  logger.error("Could not leave preview session before routing incoming preview deeplink");
4196
4337
  this.isLeavingPreviewForIncomingLink = false;
4338
+ this.hidePreviewTransitionLoader("incoming-preview-deeplink-failed");
4197
4339
  }
4198
4340
  });
4199
4341
  }
@@ -4214,13 +4356,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
4214
4356
  }
4215
4357
 
4216
4358
  // Initialize shake menu if enabled and activity is BridgeActivity
4217
- if (shakeMenuEnabled && getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
4218
- try {
4219
- shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger);
4220
- logger.info("Shake menu initialized");
4221
- } catch (Exception e) {
4222
- logger.error("Failed to initialize shake menu: " + e.getMessage());
4223
- }
4359
+ if (this.shouldListenForShake()) {
4360
+ this.ensureShakeMenuStarted();
4224
4361
  }
4225
4362
  } catch (Exception e) {
4226
4363
  logger.error("Failed to run handleOnStart: " + e.getMessage());
@@ -4279,23 +4416,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
4279
4416
  this.shakeMenuEnabled = enabled;
4280
4417
  logger.info("Shake menu " + (enabled ? "enabled" : "disabled"));
4281
4418
 
4282
- // Manage shake menu instance based on enabled state
4283
- if (enabled && getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
4284
- try {
4285
- shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger);
4286
- logger.info("Shake menu initialized");
4287
- } catch (Exception e) {
4288
- logger.error("Failed to initialize shake menu: " + e.getMessage());
4289
- }
4290
- } else if (!enabled && shakeMenu != null) {
4291
- try {
4292
- shakeMenu.stop();
4293
- shakeMenu = null;
4294
- logger.info("Shake menu stopped");
4295
- } catch (Exception e) {
4296
- logger.error("Failed to stop shake menu: " + e.getMessage());
4297
- }
4298
- }
4419
+ this.syncShakeMenuLifecycle();
4299
4420
 
4300
4421
  call.resolve();
4301
4422
  }
@@ -4323,6 +4444,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
4323
4444
 
4324
4445
  this.shakeChannelSelectorEnabled = enabled;
4325
4446
  logger.info("Shake channel selector " + (enabled ? "enabled" : "disabled"));
4447
+ this.syncShakeMenuLifecycle();
4326
4448
  call.resolve();
4327
4449
  }
4328
4450
 
@@ -1903,59 +1903,38 @@ public class CapgoUpdater {
1903
1903
  String data = responseBody.string();
1904
1904
 
1905
1905
  try {
1906
- Map<String, Object> ret = new HashMap<>();
1906
+ Map<String, Object> ret = parseListChannelsResponse(data);
1907
1907
 
1908
+ logger.info("Channels listed successfully");
1909
+ callback.callback(ret);
1910
+ } catch (JSONException arrayException) {
1911
+ // If not an array, try to parse as error object
1908
1912
  try {
1909
- // Try to parse as direct array first
1910
- JSONArray channelsJson = new JSONArray(data);
1911
- List<Map<String, Object>> channelsList = new ArrayList<>();
1912
-
1913
- for (int i = 0; i < channelsJson.length(); i++) {
1914
- JSONObject channelJson = channelsJson.getJSONObject(i);
1915
- Map<String, Object> channel = new HashMap<>();
1916
- channel.put("id", channelJson.optString("id", ""));
1917
- channel.put("name", channelJson.optString("name", ""));
1918
- channel.put("public", channelJson.optBoolean("public", false));
1919
- channel.put("allow_self_set", channelJson.optBoolean("allow_self_set", false));
1920
- channelsList.add(channel);
1921
- }
1922
-
1923
- // Wrap in channels object for JS API
1924
- ret.put("channels", channelsList);
1925
-
1926
- logger.info("Channels listed successfully");
1927
- callback.callback(ret);
1928
- } catch (JSONException arrayException) {
1929
- // If not an array, try to parse as error object
1930
- try {
1931
- JSONObject json = new JSONObject(data);
1932
- if (json.has("error")) {
1933
- Map<String, Object> retError = new HashMap<>();
1934
- retError.put("error", json.getString("error"));
1935
- if (json.has("message")) {
1936
- retError.put("message", json.getString("message"));
1937
- } else {
1938
- retError.put("message", "server did not provide a message");
1939
- }
1940
- callback.callback(retError);
1941
- return;
1942
- }
1913
+ JSONObject json = new JSONObject(data);
1914
+ if (json.has("error")) {
1943
1915
  Map<String, Object> retError = new HashMap<>();
1944
- retError.put("message", "Unexpected channels response format");
1945
- retError.put("error", "parse_error");
1916
+ retError.put("error", json.getString("error"));
1917
+ if (json.has("message")) {
1918
+ retError.put("message", json.getString("message"));
1919
+ } else {
1920
+ retError.put("message", "server did not provide a message");
1921
+ }
1946
1922
  callback.callback(retError);
1947
1923
  return;
1948
- } catch (JSONException objException) {
1949
- // If neither array nor object, throw parse error
1950
- arrayException.addSuppressed(objException);
1951
- throw arrayException;
1952
1924
  }
1925
+ Map<String, Object> retError = new HashMap<>();
1926
+ retError.put("message", "Unexpected channels response format");
1927
+ retError.put("error", "parse_error");
1928
+ callback.callback(retError);
1929
+ return;
1930
+ } catch (JSONException objException) {
1931
+ // If neither array nor object, throw parse error
1932
+ arrayException.addSuppressed(objException);
1933
+ Map<String, Object> retError = new HashMap<>();
1934
+ retError.put("message", "JSON parse error: " + arrayException.getMessage());
1935
+ retError.put("error", "parse_error");
1936
+ callback.callback(retError);
1953
1937
  }
1954
- } catch (JSONException e) {
1955
- Map<String, Object> retError = new HashMap<>();
1956
- retError.put("message", "JSON parse error: " + e.getMessage());
1957
- retError.put("error", "parse_error");
1958
- callback.callback(retError);
1959
1938
  }
1960
1939
  }
1961
1940
  }
@@ -1963,6 +1942,29 @@ public class CapgoUpdater {
1963
1942
  );
1964
1943
  }
1965
1944
 
1945
+ static Map<String, Object> parseListChannelsResponse(final String data) throws JSONException {
1946
+ JSONArray channelsJson = new JSONArray(data);
1947
+ List<Map<String, Object>> channelsList = new ArrayList<>();
1948
+
1949
+ for (int i = 0; i < channelsJson.length(); i++) {
1950
+ JSONObject channelJson = channelsJson.getJSONObject(i);
1951
+ Object channelId = channelJson.get("id");
1952
+ if (!(channelId instanceof Number)) {
1953
+ throw new JSONException("Channel id must be a number");
1954
+ }
1955
+ Map<String, Object> channel = new HashMap<>();
1956
+ channel.put("id", channelId);
1957
+ channel.put("name", channelJson.optString("name", ""));
1958
+ channel.put("public", channelJson.optBoolean("public", false));
1959
+ channel.put("allow_self_set", channelJson.optBoolean("allow_self_set", false));
1960
+ channelsList.add(channel);
1961
+ }
1962
+
1963
+ Map<String, Object> ret = new HashMap<>();
1964
+ ret.put("channels", channelsList);
1965
+ return ret;
1966
+ }
1967
+
1966
1968
  public void sendStats(final String action) {
1967
1969
  this.sendStats(action, this.getCurrentBundle().getVersionName());
1968
1970
  }