This is an automated email from the ASF dual-hosted git repository.

GitToTheHub pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cordova-ios.git


The following commit(s) were added to refs/heads/master by this push:
     new 15d090dc fix: deliver cold-launch deep link URLs after page load 
(#1681)
15d090dc is described below

commit 15d090dc250d06d628b34b3a5b26508c2877adc6
Author: Rohit <[email protected]>
AuthorDate: Fri Aug 7 15:31:56 2026 +0530

    fix: deliver cold-launch deep link URLs after page load (#1681)
    
    - On a cold launch from a custom-scheme deep link under the UIScene 
lifecycle, `CDVSceneDelegate` posts `CDVPluginHandleOpenURLNotification` 
synchronously during scene connection. At that point `CDVHandleOpenURL` has not 
yet registered its observer (it does so later in `pluginInitialize`), so 
`NSNotificationCenter` has no observer to deliver to and the launch URL is 
dropped. Warm launches were unaffected.
    - Buffer the launch URL contexts captured in `scene:willConnectToSession:` 
and replay them on `CDVPageDidLoadNotification`, by which point the plugin 
observer exists.
    - A `sceneDidDisconnect:` teardown removes the pending observer if the 
scene disconnects before the page loads. The runtime `openURLContexts` path is 
unchanged.
    
    Generated-by: Opus 4.8
---
 CordovaLib/Classes/Public/CDVSceneDelegate.m       |  52 +++++-
 tests/CordovaLibTests/CDVSceneDelegateTests.m      | 193 +++++++++++++++++++++
 .../CordovaTests.xcodeproj/project.pbxproj         |   4 +
 3 files changed, 247 insertions(+), 2 deletions(-)

diff --git a/CordovaLib/Classes/Public/CDVSceneDelegate.m 
b/CordovaLib/Classes/Public/CDVSceneDelegate.m
index da3944bb..9df26697 100644
--- a/CordovaLib/Classes/Public/CDVSceneDelegate.m
+++ b/CordovaLib/Classes/Public/CDVSceneDelegate.m
@@ -20,12 +20,60 @@
 #import <Cordova/CDVSceneDelegate.h>
 #import <Cordova/CDVPluginNotifications.h>
 
+@interface CDVSceneDelegate ()
+
+// URL contexts captured on a cold launch, held until the page loads so that
+// CDVHandleOpenURL has registered its observer before we post the 
notification.
+@property (nonatomic, strong, nullable) NSSet<UIOpenURLContext *> 
*launchURLContexts;
+@property (nonatomic, weak, nullable) UIScene *launchScene;
+
+@end
+
 @implementation CDVSceneDelegate
 
 - (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session 
options:(UISceneConnectionOptions *)connectionOptions
 {
-    // If the app was launched from a URL, that should also fire the 
CDVPluginOpenURLNotification
-    [self scene:scene openURLContexts:connectionOptions.URLContexts];
+    // If the app was launched from a URL, that should also fire the 
CDVPluginHandleOpenURLNotification.
+    //
+    // On a cold launch this runs during scene connection, which is *before*
+    // CDVHandleOpenURL registers its observer in -pluginInitialize. Posting 
the
+    // notification now would deliver it to no observer and the URL would be 
lost
+    // (see https://github.com/apache/cordova-ios/issues/1671). Buffer the URL
+    // contexts and replay them once the page has loaded, by which time the
+    // plugin observer exists.
+    if (connectionOptions.URLContexts.count > 0) {
+        self.launchScene = scene;
+        self.launchURLContexts = connectionOptions.URLContexts;
+        [[NSNotificationCenter defaultCenter] addObserver:self
+                                                 
selector:@selector(pageDidLoad:)
+                                                     
name:CDVPageDidLoadNotification
+                                                   object:nil];
+    }
+}
+
+- (void)pageDidLoad:(NSNotification *)notification
+{
+    [[NSNotificationCenter defaultCenter] removeObserver:self 
name:CDVPageDidLoadNotification object:nil];
+
+    UIScene *scene = self.launchScene;
+    NSSet<UIOpenURLContext *> *contexts = self.launchURLContexts;
+    self.launchScene = nil;
+    self.launchURLContexts = nil;
+
+    if (scene != nil && contexts != nil) {
+        [self scene:scene openURLContexts:contexts];
+    }
+}
+
+- (void)sceneDidDisconnect:(UIScene *)scene
+{
+    // If the scene disconnects before the page loaded, tear down the pending
+    // launch-URL observer so it cannot fire for a defunct scene.
+    if (self.launchURLContexts != nil) {
+        [[NSNotificationCenter defaultCenter] removeObserver:self 
name:CDVPageDidLoadNotification object:nil];
+        self.launchScene = nil;
+        self.launchURLContexts = nil;
+    }
 }
 
 - (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> 
*)URLContexts
diff --git a/tests/CordovaLibTests/CDVSceneDelegateTests.m 
b/tests/CordovaLibTests/CDVSceneDelegateTests.m
new file mode 100644
index 00000000..e925426f
--- /dev/null
+++ b/tests/CordovaLibTests/CDVSceneDelegateTests.m
@@ -0,0 +1,193 @@
+/*
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+*/
+
+#import <XCTest/XCTest.h>
+#import <Cordova/Cordova.h>
+#import <objc/runtime.h>
+
+// UIOpenURLContext and UISceneConnectionOptions cannot be instantiated 
normally
+// (their initializers are NS_UNAVAILABLE), but neither class is subclassing-
+// restricted. We subclass each, allocate a bare instance with the Objective-C
+// runtime, and override only the readonly getters CDVSceneDelegate actually
+// reads — the objects are used purely as method-override shells.
+
+@interface CDVFakeOpenURLContext : UIOpenURLContext
+@property (nonatomic, strong) NSURL *fakeURL;
++ (instancetype)contextWithURL:(NSURL *)url;
+@end
+
+@implementation CDVFakeOpenURLContext
++ (instancetype)contextWithURL:(NSURL *)url
+{
+    CDVFakeOpenURLContext *context = class_createInstance(self, 0);
+    context.fakeURL = url;
+    return context;
+}
+- (NSURL *)URL { return self.fakeURL; }
+- (UISceneOpenURLOptions *)options { return nil; }
+@end
+
+@interface CDVFakeSceneConnectionOptions : UISceneConnectionOptions
+@property (nonatomic, strong) NSSet<UIOpenURLContext *> *fakeURLContexts;
++ (instancetype)optionsWithURLContexts:(NSSet<UIOpenURLContext *> *)contexts;
+@end
+
+@implementation CDVFakeSceneConnectionOptions
++ (instancetype)optionsWithURLContexts:(NSSet<UIOpenURLContext *> *)contexts
+{
+    CDVFakeSceneConnectionOptions *options = class_createInstance(self, 0);
+    options.fakeURLContexts = contexts;
+    return options;
+}
+- (NSSet<UIOpenURLContext *> *)URLContexts { return self.fakeURLContexts; }
+@end
+
+#pragma mark -
+
+@interface CDVSceneDelegateTests : XCTestCase
+@property (nonatomic, strong) CDVSceneDelegate *sceneDelegate;
+@property (nonatomic, strong) UIScene *placeholderScene;
+@end
+
+@implementation CDVSceneDelegateTests
+
+- (void)setUp
+{
+    [super setUp];
+    self.sceneDelegate = [[CDVSceneDelegate alloc] init];
+    // The scene object is only stored and passed back through; it is never
+    // messaged, so any non-nil placeholder cast to UIScene * is sufficient.
+    self.placeholderScene = (UIScene *)[NSObject new];
+}
+
+- (void)tearDown
+{
+    // Ensure no pending launch-URL observer leaks into the next test.
+    [self.sceneDelegate sceneDidDisconnect:self.placeholderScene];
+    self.sceneDelegate = nil;
+    self.placeholderScene = nil;
+    [super tearDown];
+}
+
+- (CDVFakeSceneConnectionOptions *)connectionOptionsForURL:(NSURL *)url
+{
+    CDVFakeOpenURLContext *context = [CDVFakeOpenURLContext 
contextWithURL:url];
+    return [CDVFakeSceneConnectionOptions optionsWithURLContexts:[NSSet 
setWithObject:context]];
+}
+
+// A cold-launch URL must NOT be posted during scene connection, because
+// CDVHandleOpenURL has not registered its observer yet (issue #1671). It must 
be
+// buffered and only replayed once the page has loaded.
+- (void)testColdLaunchURLIsBufferedUntilPageDidLoad
+{
+    NSURL *launchURL = [NSURL URLWithString:@"testscheme://path?foo=bar"];
+
+    XCTNSNotificationExpectation *notFiredYet = [[XCTNSNotificationExpectation 
alloc]
+        initWithName:CDVPluginHandleOpenURLNotification];
+    notFiredYet.inverted = YES;
+
+    [self.sceneDelegate scene:self.placeholderScene
+          willConnectToSession:nil
+                       options:[self connectionOptionsForURL:launchURL]];
+
+    // Before the page loads, the open-URL notification must not have fired.
+    [self waitForExpectations:@[notFiredYet] timeout:0.3];
+}
+
+// Once CDVPageDidLoadNotification fires (the plugin observer now exists), the
+// buffered launch URL must be replayed as CDVPluginHandleOpenURLNotification.
+- (void)testColdLaunchURLIsReplayedAfterPageDidLoad
+{
+    NSURL *launchURL = [NSURL URLWithString:@"testscheme://path?foo=bar"];
+
+    [self.sceneDelegate scene:self.placeholderScene
+          willConnectToSession:nil
+                       options:[self connectionOptionsForURL:launchURL]];
+
+    XCTNSNotificationExpectation *openURLFired = 
[[XCTNSNotificationExpectation alloc]
+        initWithName:CDVPluginHandleOpenURLNotification];
+    openURLFired.handler = ^BOOL(NSNotification *notification) {
+        return [notification.object isEqual:launchURL];
+    };
+
+    [[NSNotificationCenter defaultCenter] 
postNotificationName:CDVPageDidLoadNotification object:nil];
+
+    [self waitForExpectations:@[openURLFired] timeout:1.0];
+}
+
+// A launch with no URL contexts must never post an open-URL notification, even
+// after a page load (nothing to deliver).
+- (void)testLaunchWithoutURLDoesNotPostOpenURL
+{
+    CDVFakeSceneConnectionOptions *options = [CDVFakeSceneConnectionOptions 
optionsWithURLContexts:[NSSet set]];
+
+    [self.sceneDelegate scene:self.placeholderScene
+          willConnectToSession:nil
+                       options:options];
+
+    XCTNSNotificationExpectation *notFired = [[XCTNSNotificationExpectation 
alloc]
+        initWithName:CDVPluginHandleOpenURLNotification];
+    notFired.inverted = YES;
+
+    [[NSNotificationCenter defaultCenter] 
postNotificationName:CDVPageDidLoadNotification object:nil];
+
+    [self waitForExpectations:@[notFired] timeout:0.3];
+}
+
+// If the scene disconnects before the page loads, the pending observer must be
+// torn down so a later page-load cannot replay a URL for a defunct scene.
+- (void)testSceneDisconnectBeforePageLoadCancelsPendingURL
+{
+    NSURL *launchURL = [NSURL URLWithString:@"testscheme://path?foo=bar"];
+
+    [self.sceneDelegate scene:self.placeholderScene
+          willConnectToSession:nil
+                       options:[self connectionOptionsForURL:launchURL]];
+
+    [self.sceneDelegate sceneDidDisconnect:self.placeholderScene];
+
+    XCTNSNotificationExpectation *notFired = [[XCTNSNotificationExpectation 
alloc]
+        initWithName:CDVPluginHandleOpenURLNotification];
+    notFired.inverted = YES;
+
+    [[NSNotificationCenter defaultCenter] 
postNotificationName:CDVPageDidLoadNotification object:nil];
+
+    [self waitForExpectations:@[notFired] timeout:0.3];
+}
+
+// The warm path (app already running) posts immediately and is unaffected by 
the
+// cold-launch buffering.
+- (void)testWarmOpenURLContextsPostsImmediately
+{
+    NSURL *warmURL = [NSURL URLWithString:@"testscheme://warm"];
+
+    CDVFakeOpenURLContext *context = [CDVFakeOpenURLContext 
contextWithURL:warmURL];
+
+    XCTNSNotificationExpectation *openURLFired = 
[[XCTNSNotificationExpectation alloc]
+        initWithName:CDVPluginHandleOpenURLNotification];
+    openURLFired.handler = ^BOOL(NSNotification *notification) {
+        return [notification.object isEqual:warmURL];
+    };
+
+    [self.sceneDelegate scene:self.placeholderScene openURLContexts:[NSSet 
setWithObject:context]];
+
+    [self waitForExpectations:@[openURLFired] timeout:1.0];
+}
+
+@end
diff --git a/tests/CordovaLibTests/CordovaTests.xcodeproj/project.pbxproj 
b/tests/CordovaLibTests/CordovaTests.xcodeproj/project.pbxproj
index b237508d..8468dd5d 100644
--- a/tests/CordovaLibTests/CordovaTests.xcodeproj/project.pbxproj
+++ b/tests/CordovaLibTests/CordovaTests.xcodeproj/project.pbxproj
@@ -30,6 +30,7 @@
                9017CA502FD8206D00F540FC /* www in Resources */ = {isa = 
PBXBuildFile; fileRef = 9017CA4F2FD8206D00F540FC /* www */; };
                9017CA532FD820B800F540FC /* cordova.js in CopyFiles */ = {isa = 
PBXBuildFile; fileRef = 9017CA522FD820B800F540FC /* cordova.js */; };
                902530FC29A6DC53004AF1CF /* CDVPluginInitTests.m in Sources */ 
= {isa = PBXBuildFile; fileRef = 902530FA29A6DC53004AF1CF /* 
CDVPluginInitTests.m */; };
+               AB1671FE01D0FACE00170002 /* CDVSceneDelegateTests.m in Sources 
*/ = {isa = PBXBuildFile; fileRef = AB1671FE01D0FACE00170001 /* 
CDVSceneDelegateTests.m */; };
                9021AA402FD8E41C00A950DD /* CDVWebViewEngineTests.swift in 
Sources */ = {isa = PBXBuildFile; fileRef = 9021AA3F2FD8E41500A950DD /* 
CDVWebViewEngineTests.swift */; };
                905C8BA82CF0756E007EECEF /* AppDelegate.swift in Sources */ = 
{isa = PBXBuildFile; fileRef = 905C8BA72CF0756E007EECEF /* AppDelegate.swift 
*/; };
                9085EE302CF0617800603B73 /* Cordova.framework in Frameworks */ 
= {isa = PBXBuildFile; fileRef = C0FA7CAC1E4BB6B30077B045 /* Cordova.framework 
*/; };
@@ -105,6 +106,7 @@
                9017CA4F2FD8206D00F540FC /* www */ = {isa = PBXFileReference; 
lastKnownFileType = folder; path = www; sourceTree = "<group>"; };
                9017CA522FD820B800F540FC /* cordova.js */ = {isa = 
PBXFileReference; lastKnownFileType = sourcecode.javascript; name = cordova.js; 
path = ../../templates/project/www/cordova.js; sourceTree = "<group>"; };
                902530FA29A6DC53004AF1CF /* CDVPluginInitTests.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path 
= CDVPluginInitTests.m; sourceTree = "<group>"; };
+               AB1671FE01D0FACE00170001 /* CDVSceneDelegateTests.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path 
= CDVSceneDelegateTests.m; sourceTree = "<group>"; };
                9021AA3F2FD8E41500A950DD /* CDVWebViewEngineTests.swift */ = 
{isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = 
CDVWebViewEngineTests.swift; sourceTree = "<group>"; };
                905C8BA72CF0756E007EECEF /* AppDelegate.swift */ = {isa = 
PBXFileReference; lastKnownFileType = sourcecode.swift; path = 
AppDelegate.swift; sourceTree = "<group>"; };
                9085EE2F2CF05B5900603B73 /* CordovaTests.xctestplan */ = {isa = 
PBXFileReference; lastKnownFileType = text; path = CordovaTests.xctestplan; 
sourceTree = "<group>"; };
@@ -217,6 +219,7 @@
                                90D08FEA2F4E63CA00A9838F /* 
CDVURLSchemeHandlerTest.m */,
                                90A775DC2C6F256D00FC5BB0 /* 
CDVSettingsDictionaryTests.m */,
                                902530FA29A6DC53004AF1CF /* 
CDVPluginInitTests.m */,
+                               AB1671FE01D0FACE00170001 /* 
CDVSceneDelegateTests.m */,
                                4E23F90123E175AD006CD852 /* 
CDVWebViewEngineTest.m */,
                                30D1B08B15A2B36D0060C291 /* CDVBase64Tests.m */,
                                30610C9119AD9B95000B3781 /* 
CDVCommandDelegateTests.m */,
@@ -349,6 +352,7 @@
                                90AE8F9C2EBF03F600FF0979 /* 
CDVWebViewEngineTest.m in Sources */,
                                C0FA7CB61E4BBBBE0077B045 /* 
CDVPluginResultJSONSerializationTests.m in Sources */,
                                902530FC29A6DC53004AF1CF /* 
CDVPluginInitTests.m in Sources */,
+                               AB1671FE01D0FACE00170002 /* 
CDVSceneDelegateTests.m in Sources */,
                                90D08FED2F4E690700A9838F /* 
CDVSettingsDictionarySwiftTests.swift in Sources */,
                                C0FA7CB91E4BBBBE0077B045 /* CDVBase64Tests.m in 
Sources */,
                                90A775DD2C6F257500FC5BB0 /* 
CDVSettingsDictionaryTests.m in Sources */,


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to