diff --git a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm index 67d2ee3791243..4c2d0cecc2554 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm @@ -134,44 +134,47 @@ - (void)userNotificationCenter:(UNUserNotificationCenter*)center } } -- (BOOL)openURL:(NSURL*)url { +- (BOOL)isFlutterDeepLinkingEnabled { NSNumber* isDeepLinkingEnabled = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"]; - if (!isDeepLinkingEnabled.boolValue) { - // Not set or NO. - return NO; - } else { - FlutterViewController* flutterViewController = [self rootFlutterViewController]; - if (flutterViewController) { - [flutterViewController.engine - waitForFirstFrame:3.0 - callback:^(BOOL didTimeout) { - if (didTimeout) { - FML_LOG(ERROR) - << "Timeout waiting for the first frame when launching an URL."; - } else { - [flutterViewController.engine.navigationChannel - invokeMethod:@"pushRouteInformation" - arguments:@{ - @"location" : url.absoluteString ?: [NSNull null], - }]; - } - }]; - return YES; - } else { - FML_LOG(ERROR) << "Attempting to open an URL without a Flutter RootViewController."; - return NO; - } - } + // if not set, return NO + return isDeepLinkingEnabled ? [isDeepLinkingEnabled boolValue] : NO; } +// This method is called when opening an URL with custom schemes. - (BOOL)application:(UIApplication*)application openURL:(NSURL*)url options:(NSDictionary*)options { if ([_lifeCycleDelegate application:application openURL:url options:options]) { return YES; } - return [self openURL:url]; + + // Relaying to the system here will case an infinite loop, so we don't do it here. + return [self handleOpenURL:url options:options relayToSystemIfUnhandled:NO]; +} + +// Helper function for opening an URL, either with a custom scheme or a http/https scheme. +- (BOOL)handleOpenURL:(NSURL*)url + options:(NSDictionary*)options + relayToSystemIfUnhandled:(BOOL)throwBack { + if (![self isFlutterDeepLinkingEnabled]) { + return NO; + } + + FlutterViewController* flutterViewController = [self rootFlutterViewController]; + if (flutterViewController) { + [flutterViewController sendDeepLinkToFramework:url + completionHandler:^(BOOL success) { + if (!success && throwBack) { + // throw it back to iOS + [UIApplication.sharedApplication openURL:url]; + } + }]; + } else { + FML_LOG(ERROR) << "Attempting to open an URL without a Flutter RootViewController."; + return NO; + } + return YES; } - (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)url { @@ -204,6 +207,7 @@ - (void)application:(UIApplication*)application completionHandler:completionHandler]; } +// This method is called when opening an URL with a http/https scheme. - (BOOL)application:(UIApplication*)application continueUserActivity:(NSUserActivity*)userActivity restorationHandler: @@ -214,7 +218,8 @@ - (BOOL)application:(UIApplication*)application restorationHandler:restorationHandler]) { return YES; } - return [self openURL:userActivity.webpageURL]; + + return [self handleOpenURL:userActivity.webpageURL options:@{} relayToSystemIfUnhandled:YES]; } #pragma mark - FlutterPluginRegistry methods. All delegating to the rootViewController diff --git a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm index 81b93f0a8ff13..3caddbc53254a 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm @@ -62,14 +62,18 @@ - (void)testLaunchUrl { OCMStub([self.mockMainBundle objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"]) .andReturn(@YES); + OCMStub([self.mockNavigationChannel + invokeMethod:@"pushRouteInformation" + arguments:@{@"location" : @"http://myApp/custom/route?query=test"}]) + .andReturn(@YES); + BOOL result = [self.appDelegate application:[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://myApp/custom/route?query=test"] options:@{}]; + XCTAssertTrue(result); - OCMVerify([self.mockNavigationChannel - invokeMethod:@"pushRouteInformation" - arguments:@{@"location" : @"http://myApp/custom/route?query=test"}]); + OCMVerifyAll(self.mockNavigationChannel); } - (void)testLaunchUrlWithDeepLinkingNotSet { @@ -99,29 +103,31 @@ - (void)testLaunchUrlWithDeepLinkingDisabled { - (void)testLaunchUrlWithQueryParameterAndFragment { OCMStub([self.mockMainBundle objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"]) .andReturn(@YES); - + OCMStub([self.mockNavigationChannel + invokeMethod:@"pushRouteInformation" + arguments:@{@"location" : @"http://myApp/custom/route?query=test#fragment"}]) + .andReturn(@YES); BOOL result = [self.appDelegate application:[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://myApp/custom/route?query=test#fragment"] options:@{}]; XCTAssertTrue(result); - OCMVerify([self.mockNavigationChannel - invokeMethod:@"pushRouteInformation" - arguments:@{@"location" : @"http://myApp/custom/route?query=test#fragment"}]); + OCMVerifyAll(self.mockNavigationChannel); } - (void)testLaunchUrlWithFragmentNoQueryParameter { OCMStub([self.mockMainBundle objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"]) .andReturn(@YES); - + OCMStub([self.mockNavigationChannel + invokeMethod:@"pushRouteInformation" + arguments:@{@"location" : @"http://myApp/custom/route#fragment"}]) + .andReturn(@YES); BOOL result = [self.appDelegate application:[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://myApp/custom/route#fragment"] options:@{}]; XCTAssertTrue(result); - OCMVerify([self.mockNavigationChannel - invokeMethod:@"pushRouteInformation" - arguments:@{@"location" : @"http://myApp/custom/route#fragment"}]); + OCMVerifyAll(self.mockNavigationChannel); } - (void)testReleasesWindowOnDealloc { @@ -145,7 +151,10 @@ - (void)testReleasesWindowOnDealloc { - (void)testUniversalLinkPushRouteInformation { OCMStub([self.mockMainBundle objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"]) .andReturn(@YES); - + OCMStub([self.mockNavigationChannel + invokeMethod:@"pushRouteInformation" + arguments:@{@"location" : @"http://myApp/custom/route?query=test"}]) + .andReturn(@YES); NSUserActivity* userActivity = [[NSUserActivity alloc] initWithActivityType:@"com.example.test"]; userActivity.webpageURL = [NSURL URLWithString:@"http://myApp/custom/route?query=test"]; BOOL result = [self.appDelegate @@ -154,9 +163,7 @@ - (void)testUniversalLinkPushRouteInformation { restorationHandler:^(NSArray>* __nullable restorableObjects){ }]; XCTAssertTrue(result); - OCMVerify([self.mockNavigationChannel - invokeMethod:@"pushRouteInformation" - arguments:@{@"location" : @"http://myApp/custom/route?query=test"}]); + OCMVerifyAll(self.mockNavigationChannel); } @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index b8911bed34e77..096c5a244ec78 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -1863,6 +1863,33 @@ - (void)handlePressEvent:(FlutterUIPressProxy*)press [self.keyboardManager handlePress:press nextAction:next]; } +- (void)sendDeepLinkToFramework:(NSURL*)url completionHandler:(void (^)(BOOL success))completion { + [_engine.get() + waitForFirstFrame:3.0 + callback:^(BOOL didTimeout) { + if (didTimeout) { + FML_LOG(ERROR) << "Timeout waiting for the first frame when launching an URL."; + completion(NO); + } else { + // invove the method and get the result + [[_engine.get() navigationChannel] + invokeMethod:@"pushRouteInformation" + arguments:@{ + @"location" : url.absoluteString ?: [NSNull null], + } + result:^(id _Nullable result) { + BOOL success = + [result isKindOfClass:[NSNumber class]] && [result boolValue]; + if (!success) { + // Logging the error if the result is not successful + FML_LOG(ERROR) << "Failed to handle route information in Flutter."; + } + completion(success); + }]; + } + }]; +} + // The documentation for presses* handlers (implemented below) is entirely // unclear about how to handle the case where some, but not all, of the presses // are handled here. I've elected to call super separately for each of the diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h index fea1a9d50f7f6..f86cb98bd7578 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h @@ -65,12 +65,12 @@ typedef void (^FlutterKeyboardAnimationCallback)(fml::TimePoint); // handled. - (void)handlePressEvent:(FlutterUIPressProxy*)press nextAction:(void (^)())nextAction API_AVAILABLE(ios(13.4)); +- (void)sendDeepLinkToFramework:(NSURL*)url completionHandler:(void (^)(BOOL success))completion; - (void)addInternalPlugins; - (void)deregisterNotifications; - (int32_t)accessibilityFlags; - (BOOL)supportsShowingSystemContextMenu; - @end #endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERVIEWCONTROLLER_INTERNAL_H_