@capgo/capacitor-updater 8.47.4 → 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.
@@ -106,6 +106,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
106
106
  private static final String PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY = "CapacitorUpdater.previewPreviousDefaultChannelWasSet";
107
107
  private static final String PREVIEW_APP_ID_PREF_KEY = "CapacitorUpdater.previewAppId";
108
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";
109
110
  private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
110
111
  private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
111
112
  private static final String LAST_REPORTED_APP_EXIT_TIMESTAMP_PREF_KEY = "CapacitorUpdater.lastReportedAppExitTimestamp";
@@ -127,7 +128,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
127
128
  static final int APPLICATION_EXIT_REASON_USER_REQUESTED = 10;
128
129
  static final int APPLICATION_EXIT_REASON_DEPENDENCY_DIED = 12;
129
130
 
130
- private final String pluginVersion = "8.47.4";
131
+ private final String pluginVersion = "8.47.5";
131
132
  private static final String DELAY_CONDITION_PREFERENCES = "";
132
133
 
133
134
  private SharedPreferences.Editor editor;
@@ -160,6 +161,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
160
161
  Boolean shakeChannelSelectorEnabled = false;
161
162
  Boolean previewSessionEnabled = false;
162
163
  private Boolean previewSessionAlertPending = false;
164
+ private Boolean isLeavingPreviewForIncomingLink = false;
163
165
  private Boolean allowManualBundleError = false;
164
166
  private Boolean allowPreview = false;
165
167
  Boolean allowSetDefaultChannel = true;
@@ -502,6 +504,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
502
504
  }
503
505
  this.implementation.previewSession = Boolean.TRUE.equals(this.previewSessionEnabled);
504
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;
505
510
  final String previewAppId = this.prefs.getString(PREVIEW_APP_ID_PREF_KEY, "");
506
511
  if (previewAppId != null && !previewAppId.isEmpty()) {
507
512
  this.setActiveAppId(previewAppId);
@@ -520,6 +525,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
520
525
  if (nativeBuildVersionChanged) {
521
526
  this.clearPreviewSessionForNativeBuildChange();
522
527
  }
528
+ this.leavePreviewSessionForLaunchIntentIfNeeded();
523
529
  this.reportPreviousAppExitReasons();
524
530
  this.reportPreviousWebViewRenderProcessGone();
525
531
  this.installWebViewStatsReporter();
@@ -534,6 +540,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
534
540
  this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.KILLED);
535
541
 
536
542
  this.checkForUpdateAfterDelay();
543
+ this.showPreviewSessionNoticeIfNeeded();
537
544
 
538
545
  // On Android 14+ (API 34+), topActivity in RecentTaskInfo returns null due to
539
546
  // security restrictions (StrandHogg task hijacking mitigations). Use ProcessLifecycleOwner
@@ -2301,6 +2308,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2301
2308
  this.shakeMenuEnabled = true;
2302
2309
  this.shakeChannelSelectorEnabled = false;
2303
2310
  this.editor.putBoolean(PREVIEW_SESSION_PREF_KEY, true);
2311
+ this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
2304
2312
  this.editor.apply();
2305
2313
  this.ensureShakeMenuStarted();
2306
2314
  call.resolve();
@@ -2322,6 +2330,58 @@ public class CapacitorUpdaterPlugin extends Plugin {
2322
2330
  final BundleInfo previewFallbackBundle = this.implementation.getPreviewFallbackBundle();
2323
2331
  this.endPreviewSession();
2324
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;
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();
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
+
2374
+ this.endPreviewSession();
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
+ ) {
2325
2385
  if (
2326
2386
  !previewBundle.isBuiltin() &&
2327
2387
  (previewFallbackBundle == null || !previewBundle.getId().equals(previewFallbackBundle.getId())) &&
@@ -2333,7 +2393,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
2333
2393
  logger.warn("Cannot delete preview bundle " + previewBundle.getId() + ": " + err.getMessage());
2334
2394
  }
2335
2395
  }
2336
- return true;
2337
2396
  }
2338
2397
 
2339
2398
  public boolean reloadPreviewSessionFromShakeMenu() {
@@ -2388,6 +2447,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2388
2447
 
2389
2448
  this.previewSessionEnabled = false;
2390
2449
  this.previewSessionAlertPending = false;
2450
+ this.isLeavingPreviewForIncomingLink = false;
2391
2451
  this.implementation.previewSession = false;
2392
2452
  this.shakeMenuEnabled = previousShakeMenuEnabled;
2393
2453
  this.shakeChannelSelectorEnabled = previousShakeChannelSelectorEnabled;
@@ -2414,6 +2474,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2414
2474
  this.restorePreviewPreviousDefaultChannel();
2415
2475
  this.previewSessionEnabled = false;
2416
2476
  this.previewSessionAlertPending = false;
2477
+ this.isLeavingPreviewForIncomingLink = false;
2417
2478
  this.implementation.previewSession = false;
2418
2479
  this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
2419
2480
  this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
@@ -2433,6 +2494,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2433
2494
  this.editor.remove(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY);
2434
2495
  this.editor.remove(PREVIEW_APP_ID_PREF_KEY);
2435
2496
  this.editor.remove(PREVIEW_PAYLOAD_URL_PREF_KEY);
2497
+ this.editor.remove(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY);
2436
2498
  this.editor.apply();
2437
2499
  }
2438
2500
 
@@ -2523,6 +2585,21 @@ public class CapacitorUpdaterPlugin extends Plugin {
2523
2585
  return this.normalizePreviewPayloadUrl(this.prefs.getString(PREVIEW_PAYLOAD_URL_PREF_KEY, null));
2524
2586
  }
2525
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
+
2526
2603
  private String readResponseBody(final InputStream stream) throws IOException {
2527
2604
  if (stream == null) {
2528
2605
  return "";
@@ -2619,6 +2696,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2619
2696
  logger.info("Native build changed; clearing preview session state");
2620
2697
  this.previewSessionEnabled = false;
2621
2698
  this.previewSessionAlertPending = false;
2699
+ this.isLeavingPreviewForIncomingLink = false;
2622
2700
  this.implementation.previewSession = false;
2623
2701
  this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
2624
2702
  this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
@@ -2657,6 +2735,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
2657
2735
  return;
2658
2736
  }
2659
2737
  this.previewSessionAlertPending = false;
2738
+ this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, false);
2739
+ this.editor.apply();
2660
2740
 
2661
2741
  new Handler(Looper.getMainLooper()).postDelayed(
2662
2742
  () -> {
@@ -2666,6 +2746,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
2666
2746
  }
2667
2747
  if (getActivity() == null || getActivity().isFinishing()) {
2668
2748
  this.previewSessionAlertPending = true;
2749
+ this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
2750
+ this.editor.apply();
2669
2751
  return;
2670
2752
  }
2671
2753
 
@@ -2676,6 +2758,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
2676
2758
  .show();
2677
2759
  } catch (final Exception e) {
2678
2760
  this.previewSessionAlertPending = true;
2761
+ this.editor.putBoolean(PREVIEW_SESSION_ALERT_PENDING_PREF_KEY, true);
2762
+ this.editor.apply();
2679
2763
  logger.warn("Could not show preview session notice: " + e.getMessage());
2680
2764
  }
2681
2765
  },
@@ -3920,6 +4004,34 @@ public class CapacitorUpdaterPlugin extends Plugin {
3920
4004
  }
3921
4005
  }
3922
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
+
3923
4035
  @Override
3924
4036
  public void handleOnStart() {
3925
4037
  try {
@@ -79,7 +79,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
79
79
  CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
80
80
  ]
81
81
  public var implementation = CapgoUpdater()
82
- private let pluginVersion: String = "8.47.4"
82
+ private let pluginVersion: String = "8.47.5"
83
83
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
84
84
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
85
85
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
@@ -105,6 +105,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
105
105
  private let previewPreviousDefaultChannelWasSetDefaultsKey = "CapacitorUpdater.previewPreviousDefaultChannelWasSet"
106
106
  private let previewAppIdDefaultsKey = "CapacitorUpdater.previewAppId"
107
107
  private let previewPayloadUrlDefaultsKey = "CapacitorUpdater.previewPayloadUrl"
108
+ private let previewSessionAlertPendingDefaultsKey = "CapacitorUpdater.previewSessionAlertPending"
109
+ private let previewDeepLinkScheme = "capgo"
110
+ private let previewDeepLinkRootComponent = "preview"
111
+ private let previewDeepLinkChannelComponent = "channel"
112
+ private let previewDeepLinkBundleComponent = "bundle"
113
+ private let previewPathSeparator = Character(UnicodeScalar(UInt8(47)))
108
114
  // Note: DELAY_CONDITION_PREFERENCES is now defined in DelayUpdateUtils.DELAY_CONDITION_PREFERENCES
109
115
  private var updateUrl = ""
110
116
  private var backgroundTaskID: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid
@@ -158,6 +164,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
158
164
  public var shakeChannelSelectorEnabled = false
159
165
  public var previewSessionEnabled = false
160
166
  private var previewSessionAlertPending = false
167
+ private var isLeavingPreviewForIncomingLink = false
161
168
  let semaphoreReady = DispatchSemaphore(value: 0)
162
169
 
163
170
  private var delayUpdateUtils: DelayUpdateUtils!
@@ -233,6 +240,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
233
240
  previewSessionEnabled = allowPreview && storedPreviewSessionEnabled
234
241
  implementation.previewSession = previewSessionEnabled
235
242
  if previewSessionEnabled {
243
+ previewSessionAlertPending = UserDefaults.standard.object(forKey: previewSessionAlertPendingDefaultsKey) as? Bool ?? true
236
244
  shakeMenuEnabled = true
237
245
  shakeChannelSelectorEnabled = false
238
246
  }
@@ -315,6 +323,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
315
323
  if nativeBuildVersionChanged {
316
324
  self.clearPreviewSessionForNativeBuildChange()
317
325
  }
326
+ self.leavePreviewSessionForLaunchURLIfNeeded()
318
327
 
319
328
  if resetWhenUpdate {
320
329
  let didResetCurrentBundle = self.resetCurrentBundleForNativeBuildChangeIfNeeded()
@@ -332,11 +341,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
332
341
  logger.error("unable to force reload, the plugin might fallback to the builtin version")
333
342
  }
334
343
 
335
- let nc = NotificationCenter.default
336
- nc.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
337
- nc.addObserver(self, selector: #selector(appMovedToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
338
- nc.addObserver(self, selector: #selector(appWillTerminate), name: UIApplication.willTerminateNotification, object: nil)
339
- nc.addObserver(self, selector: #selector(appDidReceiveMemoryWarning), name: UIApplication.didReceiveMemoryWarningNotification, object: nil)
344
+ self.registerNotificationObservers()
340
345
 
341
346
  // Check for 'kill' delay condition on app launch
342
347
  // This handles cases where the app was killed (willTerminateNotification is not reliable for system kills)
@@ -344,6 +349,47 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
344
349
 
345
350
  self.appMovedToForeground()
346
351
  self.checkForUpdateAfterDelay()
352
+ self.showPreviewSessionNoticeIfNeeded()
353
+ }
354
+
355
+ private func registerNotificationObservers() {
356
+ let notificationCenter = NotificationCenter.default
357
+ notificationCenter.addObserver(
358
+ self,
359
+ selector: #selector(appMovedToBackground),
360
+ name: UIApplication.didEnterBackgroundNotification,
361
+ object: nil
362
+ )
363
+ notificationCenter.addObserver(
364
+ self,
365
+ selector: #selector(appMovedToForeground),
366
+ name: UIApplication.willEnterForegroundNotification,
367
+ object: nil
368
+ )
369
+ notificationCenter.addObserver(
370
+ self,
371
+ selector: #selector(appWillTerminate),
372
+ name: UIApplication.willTerminateNotification,
373
+ object: nil
374
+ )
375
+ notificationCenter.addObserver(
376
+ self,
377
+ selector: #selector(appDidReceiveMemoryWarning),
378
+ name: UIApplication.didReceiveMemoryWarningNotification,
379
+ object: nil
380
+ )
381
+ notificationCenter.addObserver(
382
+ self,
383
+ selector: #selector(handleOpenURLForPreviewSession(notification:)),
384
+ name: Notification.Name.capacitorOpenURL,
385
+ object: nil
386
+ )
387
+ notificationCenter.addObserver(
388
+ self,
389
+ selector: #selector(handleOpenURLForPreviewSession(notification:)),
390
+ name: Notification.Name.capacitorOpenUniversalLink,
391
+ object: nil
392
+ )
347
393
  }
348
394
 
349
395
  private func syncKeepUrlPathFlag(enabled: Bool) {
@@ -1077,6 +1123,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1077
1123
  self.shakeMenuEnabled = true
1078
1124
  self.shakeChannelSelectorEnabled = false
1079
1125
  UserDefaults.standard.set(true, forKey: self.previewSessionDefaultsKey)
1126
+ UserDefaults.standard.set(true, forKey: self.previewSessionAlertPendingDefaultsKey)
1080
1127
  UserDefaults.standard.synchronize()
1081
1128
  call.resolve()
1082
1129
  }
@@ -1092,12 +1139,57 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1092
1139
  let previewFallbackBundle = self.implementation.getPreviewFallbackBundle()
1093
1140
  self.endPreviewSession()
1094
1141
  let restoredNextBundle = self.implementation.getNextBundle()
1142
+ self.deletePreviewBundleIfUnused(previewBundle, previewFallbackBundle: previewFallbackBundle, restoredNextBundle: restoredNextBundle)
1143
+ return true
1144
+ }
1145
+
1146
+ private func leavePreviewSessionForLaunchURLIfNeeded() {
1147
+ guard self.previewSessionEnabled,
1148
+ !self.isLeavingPreviewForIncomingLink,
1149
+ let launchUrl = ApplicationDelegateProxy.shared.lastURL,
1150
+ self.isPreviewDeepLink(launchUrl) else {
1151
+ return
1152
+ }
1153
+
1154
+ self.isLeavingPreviewForIncomingLink = true
1155
+ logger.info("Preview deeplink launch detected while preview session is active; restoring fallback before initial load")
1156
+ if !self.leavePreviewSessionWithoutReload() {
1157
+ logger.error("Could not leave preview session before initial preview deeplink routing")
1158
+ self.isLeavingPreviewForIncomingLink = false
1159
+ }
1160
+ }
1161
+
1162
+ private func leavePreviewSessionWithoutReload() -> Bool {
1163
+ let previewBundle = self.implementation.getCurrentBundle()
1164
+ guard let previewFallbackBundle = self.implementation.getPreviewFallbackBundle(), !previewFallbackBundle.isErrorStatus() else {
1165
+ logger.error("No preview fallback bundle available")
1166
+ return false
1167
+ }
1168
+ guard self.implementation.canSet(bundle: previewFallbackBundle) else {
1169
+ logger.error("Preview fallback bundle is not installable")
1170
+ return false
1171
+ }
1172
+ guard self.implementation.stagePreviewFallbackReload(bundle: previewFallbackBundle) else {
1173
+ logger.error("Could not stage preview fallback bundle")
1174
+ return false
1175
+ }
1176
+
1177
+ self.endPreviewSession()
1178
+ let restoredNextBundle = self.implementation.getNextBundle()
1179
+ self.deletePreviewBundleIfUnused(previewBundle, previewFallbackBundle: previewFallbackBundle, restoredNextBundle: restoredNextBundle)
1180
+ return true
1181
+ }
1182
+
1183
+ private func deletePreviewBundleIfUnused(
1184
+ _ previewBundle: BundleInfo,
1185
+ previewFallbackBundle: BundleInfo?,
1186
+ restoredNextBundle: BundleInfo?
1187
+ ) {
1095
1188
  if !previewBundle.isBuiltin() &&
1096
1189
  previewFallbackBundle?.getId() != previewBundle.getId() &&
1097
1190
  restoredNextBundle?.getId() != previewBundle.getId() {
1098
1191
  _ = self.implementation.delete(id: previewBundle.getId(), removeInfo: false)
1099
1192
  }
1100
- return true
1101
1193
  }
1102
1194
 
1103
1195
  func reloadPreviewSessionFromShakeMenu() -> Bool {
@@ -1147,6 +1239,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1147
1239
 
1148
1240
  self.previewSessionEnabled = false
1149
1241
  self.previewSessionAlertPending = false
1242
+ self.isLeavingPreviewForIncomingLink = false
1150
1243
  self.implementation.previewSession = false
1151
1244
  self.shakeMenuEnabled = previousShakeMenuEnabled
1152
1245
  self.shakeChannelSelectorEnabled = previousShakeChannelSelectorEnabled
@@ -1176,6 +1269,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1176
1269
  self.restorePreviewPreviousDefaultChannel()
1177
1270
  self.previewSessionEnabled = false
1178
1271
  self.previewSessionAlertPending = false
1272
+ self.isLeavingPreviewForIncomingLink = false
1179
1273
  self.implementation.previewSession = false
1180
1274
  self.shakeMenuEnabled = getConfig().getBoolean("shakeMenu", false)
1181
1275
  self.shakeChannelSelectorEnabled = getConfig().getBoolean("allowShakeChannelSelector", false)
@@ -1193,6 +1287,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1193
1287
  UserDefaults.standard.removeObject(forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey)
1194
1288
  UserDefaults.standard.removeObject(forKey: self.previewAppIdDefaultsKey)
1195
1289
  UserDefaults.standard.removeObject(forKey: self.previewPayloadUrlDefaultsKey)
1290
+ UserDefaults.standard.removeObject(forKey: self.previewSessionAlertPendingDefaultsKey)
1196
1291
  UserDefaults.standard.synchronize()
1197
1292
  }
1198
1293
 
@@ -1259,6 +1354,55 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1259
1354
  normalizedPreviewPayloadUrl(UserDefaults.standard.string(forKey: self.previewPayloadUrlDefaultsKey))
1260
1355
  }
1261
1356
 
1357
+ private func previewPath(from url: URL) -> String {
1358
+ if url.scheme == self.previewDeepLinkScheme {
1359
+ var components: [String] = []
1360
+ if let host = url.host, !host.isEmpty {
1361
+ components.append(host)
1362
+ }
1363
+ components.append(contentsOf: url.path.split(separator: self.previewPathSeparator).map(String.init))
1364
+ return self.normalizedPreviewPath(components)
1365
+ }
1366
+
1367
+ return url.path
1368
+ }
1369
+
1370
+ private func normalizedPreviewPath(_ components: [String]) -> String {
1371
+ let separator = String(self.previewPathSeparator)
1372
+ return separator + components.filter { !$0.isEmpty }.joined(separator: separator)
1373
+ }
1374
+
1375
+ private func previewDeepLinkPath(_ leafComponent: String) -> String {
1376
+ self.normalizedPreviewPath([self.previewDeepLinkRootComponent, leafComponent])
1377
+ }
1378
+
1379
+ private func isPreviewDeepLink(_ url: URL) -> Bool {
1380
+ let path = self.previewPath(from: url)
1381
+ return path == self.previewDeepLinkPath(self.previewDeepLinkChannelComponent) ||
1382
+ path == self.previewDeepLinkPath(self.previewDeepLinkBundleComponent)
1383
+ }
1384
+
1385
+ @objc private func handleOpenURLForPreviewSession(notification: NSNotification) {
1386
+ let rawUrl = (notification.object as? [String: Any])?["url"]
1387
+ let url = rawUrl as? URL ?? (rawUrl as? NSURL).map { $0 as URL }
1388
+ guard self.previewSessionEnabled,
1389
+ !self.isLeavingPreviewForIncomingLink,
1390
+ let url,
1391
+ self.isPreviewDeepLink(url) else {
1392
+ return
1393
+ }
1394
+
1395
+ self.isLeavingPreviewForIncomingLink = true
1396
+ logger.info("Preview deeplink received while preview session is active; restoring fallback before routing")
1397
+ DispatchQueue.global(qos: .userInitiated).async {
1398
+ let didLeave = self.leavePreviewSessionFromShakeMenu()
1399
+ if !didLeave {
1400
+ self.logger.error("Could not leave preview session before routing incoming preview deeplink")
1401
+ self.isLeavingPreviewForIncomingLink = false
1402
+ }
1403
+ }
1404
+ }
1405
+
1262
1406
  private func fetchPreviewPayload(_ payloadUrl: URL) throws -> PreviewPayload {
1263
1407
  var request = URLRequest(url: payloadUrl)
1264
1408
  request.setValue("application/json", forHTTPHeaderField: "Accept")
@@ -1340,6 +1484,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1340
1484
  logger.info("Native build changed; clearing preview session state")
1341
1485
  self.previewSessionEnabled = false
1342
1486
  self.previewSessionAlertPending = false
1487
+ self.isLeavingPreviewForIncomingLink = false
1343
1488
  self.implementation.previewSession = false
1344
1489
  self.shakeMenuEnabled = getConfig().getBoolean("shakeMenu", false)
1345
1490
  self.shakeChannelSelectorEnabled = getConfig().getBoolean("allowShakeChannelSelector", false)
@@ -1367,6 +1512,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1367
1512
  return
1368
1513
  }
1369
1514
  self.previewSessionAlertPending = false
1515
+ UserDefaults.standard.set(false, forKey: self.previewSessionAlertPendingDefaultsKey)
1516
+ UserDefaults.standard.synchronize()
1370
1517
 
1371
1518
  DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(600)) {
1372
1519
  guard self.previewSessionEnabled else {
@@ -1375,6 +1522,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1375
1522
  if let topVC = UIApplication.topViewController(),
1376
1523
  topVC.isKind(of: UIAlertController.self) {
1377
1524
  self.previewSessionAlertPending = true
1525
+ UserDefaults.standard.set(true, forKey: self.previewSessionAlertPendingDefaultsKey)
1526
+ UserDefaults.standard.synchronize()
1378
1527
  return
1379
1528
  }
1380
1529
 
@@ -1386,6 +1535,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1386
1535
  alert.addAction(UIAlertAction(title: "Got it", style: .default))
1387
1536
  if let topVC = UIApplication.topViewController() {
1388
1537
  topVC.present(alert, animated: true)
1538
+ } else {
1539
+ self.previewSessionAlertPending = true
1540
+ UserDefaults.standard.set(true, forKey: self.previewSessionAlertPendingDefaultsKey)
1541
+ UserDefaults.standard.synchronize()
1389
1542
  }
1390
1543
  }
1391
1544
  }
@@ -29,7 +29,7 @@ extension UIWindow {
29
29
  // Find the CapacitorUpdaterPlugin instance
30
30
  guard let bridgeViewController = rootViewController as? CAPBridgeViewController,
31
31
  let bridge = bridgeViewController.bridge,
32
- let plugin = bridge.plugin(withName: "CapacitorUpdaterPlugin") as? CapacitorUpdaterPlugin else {
32
+ let plugin = bridge.plugin(withName: "CapacitorUpdater") as? CapacitorUpdaterPlugin else {
33
33
  return
34
34
  }
35
35
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "8.47.4",
3
+ "version": "8.47.5",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",