Skip to content

Commit

Permalink
Support iOS universal links route deep linking (#27874)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmagman committed Aug 9, 2021
1 parent 6f8134e commit 266435d
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 87 deletions.
40 changes: 15 additions & 25 deletions shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"];
if (!isDeepLinkingEnabled.boolValue) {
// Not set or NO.
return NO;
} else {
FlutterViewController* flutterViewController = [self rootFlutterViewController];
Expand Down Expand Up @@ -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]) {
return YES;
}
return [self openURL:url];
}

- (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)url {
Expand Down Expand Up @@ -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
Expand Down
159 changes: 102 additions & 57 deletions shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
@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 {
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 {
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 {
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
Expand Up @@ -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;

@end
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
0D6AB73E22BD8F0200EEE540 /* FlutterEngineConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = FlutterEngineConfig.xcconfig; sourceTree = "<group>"; };
F7521D7226BB671E005F15C5 /* libios_test_flutter.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libios_test_flutter.dylib; path = "../../../../out/$(FLUTTER_ENGINE)/libios_test_flutter.dylib"; sourceTree = "<group>"; };
F7521D7526BB673E005F15C5 /* libocmock_shared.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libocmock_shared.dylib; path = "../../../../out/$(FLUTTER_ENGINE)/libocmock_shared.dylib"; sourceTree = "<group>"; };
F7A3FDE026B9E0A300EADD61 /* FlutterAppDelegateTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterAppDelegateTest.mm; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand All @@ -93,6 +94,7 @@
0AC232E924BA71D300A85907 /* Source */ = {
isa = PBXGroup;
children = (
F7A3FDE026B9E0A300EADD61 /* FlutterAppDelegateTest.mm */,
0AC232F424BA71D300A85907 /* SemanticsObjectTest.mm */,
0AC232F724BA71D300A85907 /* FlutterEngineTest.mm */,
0AC2330324BA71D300A85907 /* accessibility_bridge_test.mm */,
Expand Down

0 comments on commit 266435d

Please sign in to comment.