-
Notifications
You must be signed in to change notification settings - Fork 6k
Support iOS universal links route deep linking #27874
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -132,22 +132,11 @@ - (void)userNotificationCenter:(UNUserNotificationCenter*)center | |
| } | ||
| } | ||
|
|
||
| static BOOL IsDeepLinkingEnabled(NSDictionary* infoDictionary) { | ||
| NSNumber* isEnabled = [infoDictionary objectForKey:@"FlutterDeepLinkingEnabled"]; | ||
| if (isEnabled) { | ||
| return [isEnabled boolValue]; | ||
| } else { | ||
| return NO; | ||
| } | ||
| } | ||
|
|
||
| - (BOOL)application:(UIApplication*)application | ||
| openURL:(NSURL*)url | ||
| options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options | ||
| infoPlistGetter:(NSDictionary* (^)())infoPlistGetter { | ||
| if ([_lifeCycleDelegate application:application openURL:url options:options]) { | ||
| return YES; | ||
| } else if (!IsDeepLinkingEnabled(infoPlistGetter())) { | ||
| - (BOOL)openURL:(NSURL*)url { | ||
| NSNumber* isDeepLinkingEnabled = | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was able to get rid of the static function here. |
||
| [[NSBundle mainBundle] objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"]; | ||
| if (!isDeepLinkingEnabled.boolValue) { | ||
| // Not set or NO. | ||
| return NO; | ||
| } else { | ||
| FlutterViewController* flutterViewController = [self rootFlutterViewController]; | ||
|
|
@@ -181,12 +170,10 @@ - (BOOL)application:(UIApplication*)application | |
| - (BOOL)application:(UIApplication*)application | ||
| openURL:(NSURL*)url | ||
| options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options { | ||
| return [self application:application | ||
| openURL:url | ||
| options:options | ||
| infoPlistGetter:^NSDictionary*() { | ||
| return [[NSBundle mainBundle] infoDictionary]; | ||
| }]; | ||
| if ([_lifeCycleDelegate application:application openURL:url options:options]) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (it was moved here) |
||
| return YES; | ||
| } | ||
| return [self openURL:url]; | ||
| } | ||
|
|
||
| - (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)url { | ||
|
|
@@ -229,9 +216,12 @@ - (BOOL)application:(UIApplication*)application | |
| continueUserActivity:(NSUserActivity*)userActivity | ||
| restorationHandler:(void (^)(NSArray* __nullable restorableObjects))restorationHandler { | ||
| #endif | ||
| return [_lifeCycleDelegate application:application | ||
| continueUserActivity:userActivity | ||
| restorationHandler:restorationHandler]; | ||
| if ([_lifeCycleDelegate application:application | ||
| continueUserActivity:userActivity | ||
| restorationHandler:restorationHandler]) { | ||
| return YES; | ||
| } | ||
| return [self openURL:userActivity.webpageURL]; | ||
| } | ||
|
|
||
| #pragma mark - FlutterPluginRegistry methods. All delegating to the rootViewController | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,84 +14,129 @@ | |
| FLUTTER_ASSERT_ARC | ||
|
|
||
| @interface FlutterAppDelegateTest : XCTestCase | ||
| @property(strong) FlutterAppDelegate* appDelegate; | ||
|
|
||
| @property(strong) id mockMainBundle; | ||
| @property(strong) id mockNavigationChannel; | ||
|
|
||
| // Retain callback until the tests are done. | ||
| // https://github.com/flutter/flutter/issues/74267 | ||
| @property(strong) id mockEngineFirstFrameCallback; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I only just landed #27854 but this is probably a better name than |
||
| @end | ||
|
|
||
| @implementation FlutterAppDelegateTest | ||
|
|
||
| - (void)testLaunchUrl { | ||
| - (void)setUp { | ||
| [super setUp]; | ||
|
|
||
| id mockMainBundle = OCMClassMock([NSBundle class]); | ||
| OCMStub([mockMainBundle mainBundle]).andReturn(mockMainBundle); | ||
| self.mockMainBundle = mockMainBundle; | ||
|
|
||
| FlutterAppDelegate* appDelegate = [[FlutterAppDelegate alloc] init]; | ||
| self.appDelegate = appDelegate; | ||
|
|
||
| FlutterViewController* viewController = OCMClassMock([FlutterViewController class]); | ||
| FlutterEngine* engine = OCMClassMock([FlutterEngine class]); | ||
| FlutterMethodChannel* navigationChannel = OCMClassMock([FlutterMethodChannel class]); | ||
| self.mockNavigationChannel = navigationChannel; | ||
|
|
||
| FlutterEngine* engine = OCMClassMock([FlutterEngine class]); | ||
| OCMStub([engine navigationChannel]).andReturn(navigationChannel); | ||
| OCMStub([viewController engine]).andReturn(engine); | ||
| // Set blockNoInvoker to a strong local to retain to end of scope. | ||
| id blockNoInvoker = [OCMArg invokeBlockWithArgs:@NO, nil]; | ||
| OCMStub([engine waitForFirstFrame:3.0 callback:blockNoInvoker]); | ||
|
|
||
| id mockEngineFirstFrameCallback = [OCMArg invokeBlockWithArgs:@NO, nil]; | ||
| self.mockEngineFirstFrameCallback = mockEngineFirstFrameCallback; | ||
| OCMStub([engine waitForFirstFrame:3.0 callback:mockEngineFirstFrameCallback]); | ||
| appDelegate.rootFlutterViewControllerGetter = ^{ | ||
| return viewController; | ||
| }; | ||
| NSURL* url = [NSURL URLWithString:@"http://myApp/custom/route?query=test"]; | ||
| NSDictionary<UIApplicationOpenURLOptionsKey, id>* options = @{}; | ||
| BOOL result = [appDelegate application:[UIApplication sharedApplication] | ||
| openURL:url | ||
| options:options | ||
| infoPlistGetter:^NSDictionary*() { | ||
| return @{@"FlutterDeepLinkingEnabled" : @(YES)}; | ||
| }]; | ||
| } | ||
|
|
||
| - (void)tearDown { | ||
| // Explicitly stop mocking the NSBundle class property. | ||
| [self.mockMainBundle stopMocking]; | ||
| [super tearDown]; | ||
| } | ||
|
|
||
| - (void)testLaunchUrl { | ||
| OCMStub([self.mockMainBundle objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"]) | ||
| .andReturn(@YES); | ||
|
|
||
| BOOL result = | ||
| [self.appDelegate application:[UIApplication sharedApplication] | ||
| openURL:[NSURL URLWithString:@"http://myApp/custom/route?query=test"] | ||
| options:@{}]; | ||
| XCTAssertTrue(result); | ||
| OCMVerify([navigationChannel invokeMethod:@"pushRoute" arguments:@"/custom/route?query=test"]); | ||
| OCMVerify([self.mockNavigationChannel invokeMethod:@"pushRoute" | ||
| arguments:@"/custom/route?query=test"]); | ||
| } | ||
|
|
||
| - (void)testLaunchUrlWithDeepLinkingNotSet { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New test. |
||
| OCMStub([self.mockMainBundle objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"]) | ||
| .andReturn(nil); | ||
|
|
||
| BOOL result = | ||
| [self.appDelegate application:[UIApplication sharedApplication] | ||
| openURL:[NSURL URLWithString:@"http://myApp/custom/route?query=test"] | ||
| options:@{}]; | ||
| XCTAssertFalse(result); | ||
| OCMReject([self.mockNavigationChannel invokeMethod:OCMOCK_ANY arguments:OCMOCK_ANY]); | ||
| } | ||
|
|
||
| - (void)testLaunchUrlWithDeepLinkingDisabled { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New test. |
||
| OCMStub([self.mockMainBundle objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"]) | ||
| .andReturn(@NO); | ||
|
|
||
| BOOL result = | ||
| [self.appDelegate application:[UIApplication sharedApplication] | ||
| openURL:[NSURL URLWithString:@"http://myApp/custom/route?query=test"] | ||
| options:@{}]; | ||
| XCTAssertFalse(result); | ||
| OCMReject([self.mockNavigationChannel invokeMethod:OCMOCK_ANY arguments:OCMOCK_ANY]); | ||
| } | ||
|
|
||
| - (void)testLaunchUrlWithQueryParameterAndFragment { | ||
| FlutterAppDelegate* appDelegate = [[FlutterAppDelegate alloc] init]; | ||
| FlutterViewController* viewController = OCMClassMock([FlutterViewController class]); | ||
| FlutterEngine* engine = OCMClassMock([FlutterEngine class]); | ||
| FlutterMethodChannel* navigationChannel = OCMClassMock([FlutterMethodChannel class]); | ||
| OCMStub([engine navigationChannel]).andReturn(navigationChannel); | ||
| OCMStub([viewController engine]).andReturn(engine); | ||
| // Set blockNoInvoker to a strong local to retain to end of scope. | ||
| id blockNoInvoker = [OCMArg invokeBlockWithArgs:@NO, nil]; | ||
| OCMStub([engine waitForFirstFrame:3.0 callback:blockNoInvoker]); | ||
| appDelegate.rootFlutterViewControllerGetter = ^{ | ||
| return viewController; | ||
| }; | ||
| NSURL* url = [NSURL URLWithString:@"http://myApp/custom/route?query=test#fragment"]; | ||
| NSDictionary<UIApplicationOpenURLOptionsKey, id>* options = @{}; | ||
| BOOL result = [appDelegate application:[UIApplication sharedApplication] | ||
| openURL:url | ||
| options:options | ||
| infoPlistGetter:^NSDictionary*() { | ||
| return @{@"FlutterDeepLinkingEnabled" : @(YES)}; | ||
| }]; | ||
| OCMStub([self.mockMainBundle objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"]) | ||
| .andReturn(@YES); | ||
|
|
||
| BOOL result = [self.appDelegate | ||
| application:[UIApplication sharedApplication] | ||
| openURL:[NSURL URLWithString:@"http://myApp/custom/route?query=test#fragment"] | ||
| options:@{}]; | ||
| XCTAssertTrue(result); | ||
| OCMVerify([navigationChannel invokeMethod:@"pushRoute" | ||
| arguments:@"/custom/route?query=test#fragment"]); | ||
| OCMVerify([self.mockNavigationChannel invokeMethod:@"pushRoute" | ||
| arguments:@"/custom/route?query=test#fragment"]); | ||
| } | ||
|
|
||
| - (void)testLaunchUrlWithFragmentNoQueryParameter { | ||
| FlutterAppDelegate* appDelegate = [[FlutterAppDelegate alloc] init]; | ||
| FlutterViewController* viewController = OCMClassMock([FlutterViewController class]); | ||
| FlutterEngine* engine = OCMClassMock([FlutterEngine class]); | ||
| FlutterMethodChannel* navigationChannel = OCMClassMock([FlutterMethodChannel class]); | ||
| OCMStub([engine navigationChannel]).andReturn(navigationChannel); | ||
| OCMStub([viewController engine]).andReturn(engine); | ||
| // Set blockNoInvoker to a strong local to retain to end of scope. | ||
| id blockNoInvoker = [OCMArg invokeBlockWithArgs:@NO, nil]; | ||
| OCMStub([engine waitForFirstFrame:3.0 callback:blockNoInvoker]); | ||
| appDelegate.rootFlutterViewControllerGetter = ^{ | ||
| return viewController; | ||
| }; | ||
| NSURL* url = [NSURL URLWithString:@"http://myApp/custom/route#fragment"]; | ||
| NSDictionary<UIApplicationOpenURLOptionsKey, id>* options = @{}; | ||
| BOOL result = [appDelegate application:[UIApplication sharedApplication] | ||
| openURL:url | ||
| options:options | ||
| infoPlistGetter:^NSDictionary*() { | ||
| return @{@"FlutterDeepLinkingEnabled" : @(YES)}; | ||
| }]; | ||
| OCMStub([self.mockMainBundle objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"]) | ||
| .andReturn(@YES); | ||
|
|
||
| BOOL result = | ||
| [self.appDelegate application:[UIApplication sharedApplication] | ||
| openURL:[NSURL URLWithString:@"http://myApp/custom/route#fragment"] | ||
| options:@{}]; | ||
| XCTAssertTrue(result); | ||
| OCMVerify([self.mockNavigationChannel invokeMethod:@"pushRoute" | ||
| arguments:@"/custom/route#fragment"]); | ||
| } | ||
|
|
||
| #pragma mark - Deep linking | ||
|
|
||
| - (void)testUniversalLinkPushRoute { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These 2 tests are very similar, maybe it's worth considering pulling out a helper function to remove some of the boilerplate so the differences are more apparent. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I pulled the boilderplate in all these tests up into |
||
| OCMStub([self.mockMainBundle objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"]) | ||
| .andReturn(@YES); | ||
|
|
||
| NSUserActivity* userActivity = [[NSUserActivity alloc] initWithActivityType:@"com.example.test"]; | ||
| userActivity.webpageURL = [NSURL URLWithString:@"http://myApp/custom/route?query=test"]; | ||
| BOOL result = [self.appDelegate | ||
| application:[UIApplication sharedApplication] | ||
| continueUserActivity:userActivity | ||
| restorationHandler:^(NSArray<id<UIUserActivityRestoring>>* __nullable restorableObjects){ | ||
| }]; | ||
| XCTAssertTrue(result); | ||
| OCMVerify([navigationChannel invokeMethod:@"pushRoute" arguments:@"/custom/route#fragment"]); | ||
| OCMVerify([self.mockNavigationChannel invokeMethod:@"pushRoute" | ||
| arguments:@"/custom/route?query=test"]); | ||
| } | ||
|
|
||
| @end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,9 +7,4 @@ | |
| @interface FlutterAppDelegate (Test) | ||
| @property(nonatomic, copy) FlutterViewController* (^rootFlutterViewControllerGetter)(void); | ||
|
|
||
| - (BOOL)application:(UIApplication*)application | ||
| openURL:(NSURL*)url | ||
| options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options | ||
| infoPlistGetter:(NSDictionary* (^)())infoPlistGetter; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wound up getting rid of this test injection helper and mocking out |
||
|
|
||
| @end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is now shared between two app delegate methods. Move the method-specific
_lifeCycleDelegatecheck up one level to-application:openURL:options:.