Skip to content

Commit c8ea66e

Browse files
authored
Implements web view for presenting Auth web content on iOS 7 and 8. (#253)
Also (hopefully) fixes thread safety issues in presenting Auth web content.
1 parent e8cf906 commit c8ea66e

12 files changed

+466
-84
lines changed

Example/Auth/Tests/FIRAuthURLPresenterTests.m

Lines changed: 44 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
#import "FIRAuthUIDelegate.h"
2323
#import "FIRAuthURLPresenter.h"
24+
#import "FIRAuthWebViewController.h"
2425

2526
/** @var kExpectationTimeout
2627
@brief The maximum time waiting for expectations to fulfill.
@@ -61,7 +62,6 @@ - (void)testFIRAuthURLPresenterNilUIDelegate {
6162
*/
6263
- (void)testFIRAuthURLPresenterUsingDefaultUIDelegate:(BOOL)usesDefaultUIDelegate {
6364
id mockUIDelegate = OCMProtocolMock(@protocol(FIRAuthUIDelegate));
64-
id mockUIApplication = OCMPartialMock([UIApplication sharedApplication]);
6565
NSURL *presenterURL = [NSURL URLWithString:@"https://presenter.url"];
6666
FIRAuthURLPresenter *presenter = [[FIRAuthURLPresenter alloc] init];
6767

@@ -70,68 +70,70 @@ - (void)testFIRAuthURLPresenterUsingDefaultUIDelegate:(BOOL)usesDefaultUIDelegat
7070
OCMStub(ClassMethod([mockDefaultUIDelegateClass defaultUIDelegate])).andReturn(mockUIDelegate);
7171
}
7272

73-
XCTestExpectation *callbackMatcherExpectation =
74-
[self expectationWithDescription:@"callbackMatcher callback"];
73+
__block XCTestExpectation *callbackMatcherExpectation;
7574
FIRAuthURLCallbackMatcher callbackMatcher = ^BOOL(NSURL *_Nonnull callbackURL) {
75+
XCTAssertNotNil(callbackMatcherExpectation);
7676
XCTAssertEqualObjects(callbackURL, presenterURL);
7777
[callbackMatcherExpectation fulfill];
7878
return YES;
7979
};
8080

81-
XCTestExpectation *completionBlockExpectation =
82-
[self expectationWithDescription:@"completion callback"];
81+
__block XCTestExpectation *completionBlockExpectation;
8382
FIRAuthURLPresentationCompletion completionBlock = ^(NSURL *_Nullable callbackURL,
8483
NSError *_Nullable error) {
84+
XCTAssertNotNil(completionBlockExpectation);
8585
XCTAssertEqualObjects(callbackURL, presenterURL);
8686
XCTAssertNil(error);
8787
[completionBlockExpectation fulfill];
8888
};
8989

90-
if ([SFSafariViewController class]) {
91-
id presenterArg = [OCMArg isKindOfClass:[SFSafariViewController class]];
92-
OCMExpect([mockUIDelegate presentViewController:presenterArg
93-
animated:YES
94-
completion:nil]).andDo(^(NSInvocation *invocation) {
95-
__unsafe_unretained id unretainedArgument;
96-
// Indices 0 and 1 indicate the hidden arguments self and _cmd.
97-
// `presentViewController` is at index 2.
98-
[invocation getArgument:&unretainedArgument atIndex:2];
99-
100-
SFSafariViewController *viewController = unretainedArgument;
101-
XCTAssertEqual(viewController.delegate, presenter);
90+
XCTestExpectation *UIPresentationExpectation = [self expectationWithDescription:@"present UI"];
91+
OCMExpect([mockUIDelegate presentViewController:[OCMArg any]
92+
animated:YES
93+
completion:nil]).andDo(^(NSInvocation *invocation) {
94+
XCTAssertTrue([NSThread isMainThread]);
95+
__unsafe_unretained id unretainedArgument;
96+
// Indices 0 and 1 indicate the hidden arguments self and _cmd.
97+
// `presentViewController` is at index 2.
98+
[invocation getArgument:&unretainedArgument atIndex:2];
99+
100+
id presentViewController = unretainedArgument;
101+
if ([SFSafariViewController class]) {
102+
SFSafariViewController *viewController = presentViewController;
102103
XCTAssertTrue([viewController isKindOfClass:[SFSafariViewController class]]);
103-
});
104-
} else {
105-
id mockUIApplicationClass = OCMClassMock([UIApplication class]);
106-
OCMStub(ClassMethod([mockUIApplicationClass sharedApplication])).andReturn(mockUIApplication);
107-
OCMExpect([mockUIApplication openURL:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) {
108-
__unsafe_unretained id unretainedArgument;
109-
// Indices 0 and 1 indicate the hidden arguments self and _cmd.
110-
// `openURL` is at index 2.
111-
[invocation getArgument:&unretainedArgument atIndex:2];
112-
XCTAssertEqualObjects(presenterURL, unretainedArgument);
113-
});
114-
}
104+
XCTAssertEqual(viewController.delegate, presenter);
105+
} else {
106+
UINavigationController *navigationController = presentViewController;
107+
XCTAssertTrue([navigationController isKindOfClass:[UINavigationController class]]);
108+
FIRAuthWebViewController *webViewController =
109+
navigationController.viewControllers.firstObject;
110+
XCTAssertTrue([webViewController isKindOfClass:[FIRAuthWebViewController class]]);
111+
}
112+
[UIPresentationExpectation fulfill];
113+
});
115114

116115
// Present the content.
117116
[presenter presentURL:presenterURL
118117
UIDelegate:usesDefaultUIDelegate ? nil : mockUIDelegate
119118
callbackMatcher:callbackMatcher
120119
completion:completionBlock];
120+
[self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
121121
OCMVerifyAll(mockUIDelegate);
122-
OCMVerifyAll(mockUIApplication);
123-
if ([SFSafariViewController class]) {
124-
OCMExpect([mockUIDelegate dismissViewControllerAnimated:OCMOCK_ANY
125-
completion:OCMOCK_ANY])
126-
.andDo(^(NSInvocation *invocation) {
127-
__unsafe_unretained id unretainedArgument;
128-
// Indices 0 and 1 indicate the hidden arguments self and _cmd.
129-
// `completion` is at index 3.
130-
[invocation getArgument:&unretainedArgument atIndex:3];
131-
void (^finishBlock)() = unretainedArgument;
132-
finishBlock();
133-
});
134-
}
122+
123+
// Pretend dismissing view controller.
124+
OCMExpect([mockUIDelegate dismissViewControllerAnimated:OCMOCK_ANY
125+
completion:OCMOCK_ANY])
126+
.andDo(^(NSInvocation *invocation) {
127+
XCTAssertTrue([NSThread isMainThread]);
128+
__unsafe_unretained id unretainedArgument;
129+
// Indices 0 and 1 indicate the hidden arguments self and _cmd.
130+
// `completion` is at index 3.
131+
[invocation getArgument:&unretainedArgument atIndex:3];
132+
void (^completion)() = unretainedArgument;
133+
dispatch_async(dispatch_get_main_queue(), completion);
134+
});
135+
completionBlockExpectation = [self expectationWithDescription:@"completion callback"];
136+
callbackMatcherExpectation = [self expectationWithDescription:@"callbackMatcher callback"];
135137

136138
// Close the presented content.
137139
XCTAssertTrue([presenter canHandleURL:presenterURL]);

Example/Auth/Tests/FIRUserTests.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,6 +1302,7 @@ - (void)testlinkEmailAndRetrieveDataError {
13021302
completion:^(FIRAuthDataResult *_Nullable
13031303
linkAuthResult,
13041304
NSError *_Nullable error) {
1305+
XCTAssertTrue([NSThread isMainThread]);
13051306
XCTAssertNil(linkAuthResult);
13061307
XCTAssertEqual(error.code, FIRAuthErrorCodeTooManyRequests);
13071308
[expectation fulfill];

Firebase/Auth/FirebaseAuth.podspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ Simplify your iOS development, grow your user base, and monetize more effectivel
3333
'Source/**/FIRAuthDefaultUIDelegate.[mh]',
3434
'Source/**/FIRAuthUIDelegate.h',
3535
'Source/**/FIRAuthURLPresenter.[mh]',
36+
'Source/**/FIRAuthWebView.[mh]',
37+
'Source/**/FIRAuthWebViewController.[mh]',
3638
'Source/**/FIRPhoneAuthCredential.[mh]',
3739
'Source/**/FIRPhoneAuthProvider.[mh]'
3840
s.public_header_files = 'Source/Public/*.h'

Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,15 +146,16 @@ - (void)verifyPhoneNumber:(NSString *)phoneNumber
146146
callBackOnMainThread(nil, error);
147147
return;
148148
}
149+
FIRAuthURLCallbackMatcher callbackMatcher = ^BOOL(NSURL *_Nullable callbackURL) {
150+
return [self isVerifyAppURL:callbackURL];
151+
};
149152
[_auth.authURLPresenter presentURL:reCAPTCHAURL
150153
UIDelegate:UIDelegate
151-
callbackMatcher:^BOOL(NSURL * _Nullable callbackURL) {
152-
return [self isVerifyAppURL:callbackURL];
153-
}
154+
callbackMatcher:callbackMatcher
154155
completion:^(NSURL *_Nullable callbackURL,
155156
NSError *_Nullable error) {
156157
if (error) {
157-
completion(nil, error);
158+
callBackOnMainThread(nil, error);
158159
return;
159160
}
160161
NSError *reCAPTCHAError;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2017 Google
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#import <Foundation/Foundation.h>
18+
19+
#import "FIRAuthUIDelegate.h"
20+
21+
NS_ASSUME_NONNULL_BEGIN
22+
23+
@interface FIRAuthDefaultUIDelegate : NSObject <FIRAuthUIDelegate>
24+
25+
/** @fn defaultUIDelegate
26+
@brief Unavailable. Please use @c +defaultUIDelegate:
27+
*/
28+
- (instancetype)init NS_UNAVAILABLE;
29+
30+
/** @fn defaultUIDelegate
31+
@brief Returns a default FIRAuthUIDelegate object.
32+
@return The default FIRAuthUIDelegate object.
33+
*/
34+
+ (id<FIRAuthUIDelegate>)defaultUIDelegate;
35+
36+
@end
37+
38+
NS_ASSUME_NONNULL_END

Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,20 @@
1414
* limitations under the License.
1515
*/
1616

17-
#import <Foundation/Foundation.h>
18-
19-
#import "FIRAuthUIDelegate.h"
17+
#import "FIRAuthDefaultUIDelegate.h"
2018

2119
NS_ASSUME_NONNULL_BEGIN
2220

23-
@interface FIRAuthDefaultUIDelegate : NSObject <FIRAuthUIDelegate>
24-
/** @fn defaultUIDelegate
25-
@brief Unavailable. Please use initWithViewController:
26-
*/
27-
- (instancetype)init NS_UNAVAILABLE;
21+
@interface FIRAuthDefaultUIDelegate ()
2822

2923
/** @fn initWithViewController:
3024
@brief Initializes the instance with a view controller.
31-
@param viewController The view controller as the presenting view controller in @c GOIUIDelegate.
25+
@param viewController The view controller as the presenting view controller in @c
26+
FIRAuthUIDelegate.
3227
@return The initialized instance.
3328
*/
3429
- (instancetype)initWithViewController:(UIViewController *)viewController NS_DESIGNATED_INITIALIZER;
30+
3531
@end
3632

3733
@implementation FIRAuthDefaultUIDelegate {

Firebase/Auth/Source/FIRAuthURLPresenter.m

Lines changed: 70 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,15 @@
1818

1919
#import <SafariServices/SafariServices.h>
2020

21+
#import "FIRAuthDefaultUIDelegate.h"
2122
#import "FIRAuthErrorUtils.h"
23+
#import "FIRAuthGlobalWorkQueue.h"
2224
#import "FIRAuthUIDelegate.h"
25+
#import "FIRAuthWebViewController.h"
2326

2427
NS_ASSUME_NONNULL_BEGIN
2528

26-
@interface FIRAuthDefaultUIDelegate : NSObject <FIRAuthUIDelegate>
27-
/** @fn defaultUIDelegate
28-
@brief Returns a default FIRAuthUIDelegate object.
29-
@return The default FIRAuthUIDelegate object.
30-
*/
31-
+ (id<FIRAuthUIDelegate>)defaultUIDelegate;
32-
@end
33-
34-
@interface FIRAuthURLPresenter () <SFSafariViewControllerDelegate>
29+
@interface FIRAuthURLPresenter () <SFSafariViewControllerDelegate, FIRAuthWebViewDelegate>
3530
@end
3631

3732
@implementation FIRAuthURLPresenter {
@@ -50,6 +45,11 @@ @implementation FIRAuthURLPresenter {
5045
*/
5146
SFSafariViewController *_Nullable _safariViewController;
5247

48+
/** @var _webViewController
49+
@brief The FIRAuthWebViewController used for the current presentation, if any.
50+
*/
51+
FIRAuthWebViewController *_Nullable _webViewController;
52+
5353
/** @var _UIDelegate
5454
@brief The UIDelegate used to present the SFSafariViewController.
5555
*/
@@ -74,17 +74,20 @@ - (void)presentURL:(NSURL *)URL
7474
_isPresenting = YES;
7575
_callbackMatcher = callbackMatcher;
7676
_completion = completion;
77-
_UIDelegate = UIDelegate ?: [FIRAuthDefaultUIDelegate defaultUIDelegate];
78-
if ([SFSafariViewController class]) {
79-
SFSafariViewController *safariViewController = [[SFSafariViewController alloc] initWithURL:URL];
80-
_safariViewController = safariViewController;
81-
_safariViewController.delegate = self;
82-
[_UIDelegate presentViewController:safariViewController animated:YES completion:nil];
83-
return;
84-
} else {
85-
// TODO: Use web view instead.
86-
[[UIApplication sharedApplication] openURL:URL];
87-
}
77+
dispatch_async(dispatch_get_main_queue(), ^() {
78+
_UIDelegate = UIDelegate ?: [FIRAuthDefaultUIDelegate defaultUIDelegate];
79+
if ([SFSafariViewController class]) {
80+
_safariViewController = [[SFSafariViewController alloc] initWithURL:URL];
81+
_safariViewController.delegate = self;
82+
[_UIDelegate presentViewController:_safariViewController animated:YES completion:nil];
83+
return;
84+
} else {
85+
_webViewController = [[FIRAuthWebViewController alloc] initWithURL:URL delegate:self];
86+
UINavigationController *navController =
87+
[[UINavigationController alloc] initWithRootViewController:_webViewController];
88+
[_UIDelegate presentViewController:navController animated:YES completion:nil];
89+
}
90+
});
8891
}
8992

9093
- (BOOL)canHandleURL:(NSURL *)URL {
@@ -98,13 +101,45 @@ - (BOOL)canHandleURL:(NSURL *)URL {
98101
#pragma mark - SFSafariViewControllerDelegate
99102

100103
- (void)safariViewControllerDidFinish:(SFSafariViewController *)controller {
101-
if (controller == _safariViewController) {
102-
_safariViewController = nil;
103-
//TODO:Ensure that the SFSafariViewController is actually removed from the screen before
104-
//invoking finishPresentationWithURL:error:
105-
[self finishPresentationWithURL:nil
106-
error:[FIRAuthErrorUtils webContextCancelledErrorWithMessage:nil]];
107-
}
104+
dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
105+
if (controller == _safariViewController) {
106+
_safariViewController = nil;
107+
//TODO:Ensure that the SFSafariViewController is actually removed from the screen before
108+
//invoking finishPresentationWithURL:error:
109+
[self finishPresentationWithURL:nil
110+
error:[FIRAuthErrorUtils webContextCancelledErrorWithMessage:nil]];
111+
}
112+
});
113+
}
114+
115+
#pragma mark - FIRAuthwebViewControllerDelegate
116+
117+
- (BOOL)webViewController:(FIRAuthWebViewController *)webViewController canHandleURL:(NSURL *)URL {
118+
__block BOOL result = NO;
119+
dispatch_sync(FIRAuthGlobalWorkQueue(), ^() {
120+
if (webViewController == _webViewController) {
121+
result = [self canHandleURL:URL];
122+
}
123+
});
124+
return result;
125+
}
126+
127+
- (void)webViewControllerDidCancel:(FIRAuthWebViewController *)webViewController {
128+
dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
129+
if (webViewController == _webViewController) {
130+
[self finishPresentationWithURL:nil
131+
error:[FIRAuthErrorUtils webContextCancelledErrorWithMessage:nil]];
132+
}
133+
});
134+
}
135+
136+
- (void)webViewController:(FIRAuthWebViewController *)webViewController
137+
didFailWithError:(NSError *)error {
138+
dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
139+
if (webViewController == _webViewController) {
140+
[self finishPresentationWithURL:nil error:error];
141+
}
142+
});
108143
}
109144

110145
#pragma mark - Private methods
@@ -127,8 +162,14 @@ - (void)finishPresentationWithURL:(nullable NSURL *)URL
127162
};
128163
SFSafariViewController *safariViewController = _safariViewController;
129164
_safariViewController = nil;
130-
if (safariViewController) {
131-
[UIDelegate dismissViewControllerAnimated:YES completion:finishBlock];
165+
FIRAuthWebViewController *webViewController = _webViewController;
166+
_webViewController = nil;
167+
if (safariViewController || webViewController) {
168+
dispatch_async(dispatch_get_main_queue(), ^() {
169+
[UIDelegate dismissViewControllerAnimated:YES completion:^() {
170+
dispatch_async(FIRAuthGlobalWorkQueue(), finishBlock);
171+
}];
172+
});
132173
} else {
133174
finishBlock();
134175
}

0 commit comments

Comments
 (0)