From 9f374dd4d96acc16a3f8ee540f924001549748f8 Mon Sep 17 00:00:00 2001 From: Xiangtian Dai Date: Mon, 11 Sep 2017 18:13:16 -0700 Subject: [PATCH 1/4] Implements web view for presenting Auth web content on iOS 7 and 8. Also (hopefully) fixes thread safety issues in presenting Auth web content. --- Example/Auth/Tests/FIRAuthURLPresenterTests.m | 86 ++++++------- Example/Auth/Tests/FIRUserTests.m | 1 + Firebase/Auth/FirebaseAuth.podspec | 2 + .../Phone/FIRPhoneAuthProvider.m | 9 +- .../Auth/Source/FIRAuthDefaultUIDelegate.h | 39 ++++++ .../Auth/Source/FIRAuthDefaultUIDelegate.m | 18 +-- Firebase/Auth/Source/FIRAuthURLPresenter.m | 88 +++++++++++--- Firebase/Auth/Source/FIRAuthWebView.h | 35 ++++++ Firebase/Auth/Source/FIRAuthWebView.m | 78 ++++++++++++ .../Auth/Source/FIRAuthWebViewController.h | 63 ++++++++++ .../Auth/Source/FIRAuthWebViewController.m | 115 ++++++++++++++++++ FirebaseCommunity.podspec | 2 + 12 files changed, 453 insertions(+), 83 deletions(-) create mode 100644 Firebase/Auth/Source/FIRAuthDefaultUIDelegate.h create mode 100644 Firebase/Auth/Source/FIRAuthWebView.h create mode 100644 Firebase/Auth/Source/FIRAuthWebView.m create mode 100644 Firebase/Auth/Source/FIRAuthWebViewController.h create mode 100644 Firebase/Auth/Source/FIRAuthWebViewController.m diff --git a/Example/Auth/Tests/FIRAuthURLPresenterTests.m b/Example/Auth/Tests/FIRAuthURLPresenterTests.m index 5d42e5f931e..4ae737b7dd5 100644 --- a/Example/Auth/Tests/FIRAuthURLPresenterTests.m +++ b/Example/Auth/Tests/FIRAuthURLPresenterTests.m @@ -21,6 +21,7 @@ #import "FIRAuthUIDelegate.h" #import "FIRAuthURLPresenter.h" +#import "FIRAuthWebViewController.h" /** @var kExpectationTimeout @brief The maximum time waiting for expectations to fulfill. @@ -61,7 +62,6 @@ - (void)testFIRAuthURLPresenterNilUIDelegate { */ - (void)testFIRAuthURLPresenterUsingDefaultUIDelegate:(BOOL)usesDefaultUIDelegate { id mockUIDelegate = OCMProtocolMock(@protocol(FIRAuthUIDelegate)); - id mockUIApplication = OCMPartialMock([UIApplication sharedApplication]); NSURL *presenterURL = [NSURL URLWithString:@"https://presenter.url"]; FIRAuthURLPresenter *presenter = [[FIRAuthURLPresenter alloc] init]; @@ -70,70 +70,72 @@ - (void)testFIRAuthURLPresenterUsingDefaultUIDelegate:(BOOL)usesDefaultUIDelegat OCMStub(ClassMethod([mockDefaultUIDelegateClass defaultUIDelegate])).andReturn(mockUIDelegate); } - XCTestExpectation *callbackMatcherExpectation = - [self expectationWithDescription:@"callbackMatcher callback"]; + __block XCTestExpectation *callbackMatcherExpectation; FIRAuthURLCallbackMatcher callbackMatcher = ^BOOL(NSURL *_Nonnull callbackURL) { + XCTAssertNotNil(callbackMatcherExpectation); XCTAssertEqualObjects(callbackURL, presenterURL); [callbackMatcherExpectation fulfill]; return YES; }; - XCTestExpectation *completionBlockExpectation = - [self expectationWithDescription:@"completion callback"]; + __block XCTestExpectation *completionBlockExpectation; FIRAuthURLPresentationCompletion completionBlock = ^(NSURL *_Nullable callbackURL, NSError *_Nullable error) { + XCTAssertNotNil(completionBlockExpectation); XCTAssertEqualObjects(callbackURL, presenterURL); XCTAssertNil(error); [completionBlockExpectation fulfill]; }; - if ([SFSafariViewController class]) { - id presenterArg = [OCMArg isKindOfClass:[SFSafariViewController class]]; - OCMExpect([mockUIDelegate presentViewController:presenterArg - animated:YES - completion:nil]).andDo(^(NSInvocation *invocation) { - __unsafe_unretained id unretainedArgument; - // Indices 0 and 1 indicate the hidden arguments self and _cmd. - // `presentViewController` is at index 2. - [invocation getArgument:&unretainedArgument atIndex:2]; - - SFSafariViewController *viewController = unretainedArgument; - XCTAssertEqual(viewController.delegate, presenter); + XCTestExpectation *UIPresentationExpectation = [self expectationWithDescription:@"present UI"]; + OCMExpect([mockUIDelegate presentViewController:[OCMArg any] + animated:YES + completion:nil]).andDo(^(NSInvocation *invocation) { + XCTAssertTrue([NSThread isMainThread]); + __unsafe_unretained id unretainedArgument; + // Indices 0 and 1 indicate the hidden arguments self and _cmd. + // `presentViewController` is at index 2. + [invocation getArgument:&unretainedArgument atIndex:2]; + + id presentViewController = unretainedArgument; + if ([SFSafariViewController class]) { + SFSafariViewController *viewController = presentViewController; XCTAssertTrue([viewController isKindOfClass:[SFSafariViewController class]]); - }); - } else { - id mockUIApplicationClass = OCMClassMock([UIApplication class]); - OCMStub(ClassMethod([mockUIApplicationClass sharedApplication])).andReturn(mockUIApplication); - OCMExpect([mockUIApplication openURL:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { - __unsafe_unretained id unretainedArgument; - // Indices 0 and 1 indicate the hidden arguments self and _cmd. - // `openURL` is at index 2. - [invocation getArgument:&unretainedArgument atIndex:2]; - XCTAssertEqualObjects(presenterURL, unretainedArgument); - }); - } + XCTAssertEqual(viewController.delegate, presenter); + } else { + UINavigationController *navigationController = presentViewController; + XCTAssertTrue([navigationController isKindOfClass:[UINavigationController class]]); + FIRAuthWebViewController *webViewController = + navigationController.viewControllers.firstObject; + XCTAssertTrue([webViewController isKindOfClass:[FIRAuthWebViewController class]]); + } + [UIPresentationExpectation fulfill]; + }); // Present the content. [presenter presentURL:presenterURL UIDelegate:usesDefaultUIDelegate ? nil : mockUIDelegate callbackMatcher:callbackMatcher completion:completionBlock]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; OCMVerifyAll(mockUIDelegate); - OCMVerifyAll(mockUIApplication); - if ([SFSafariViewController class]) { - OCMExpect([mockUIDelegate dismissViewControllerAnimated:OCMOCK_ANY - completion:OCMOCK_ANY]) - .andDo(^(NSInvocation *invocation) { - __unsafe_unretained id unretainedArgument; - // Indices 0 and 1 indicate the hidden arguments self and _cmd. - // `completion` is at index 3. - [invocation getArgument:&unretainedArgument atIndex:3]; - void (^finishBlock)() = unretainedArgument; - finishBlock(); - }); - } + + // Pretend dismissing view controller. + OCMExpect([mockUIDelegate dismissViewControllerAnimated:OCMOCK_ANY + completion:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + XCTAssertTrue([NSThread isMainThread]); + __unsafe_unretained id unretainedArgument; + // Indices 0 and 1 indicate the hidden arguments self and _cmd. + // `completion` is at index 3. + [invocation getArgument:&unretainedArgument atIndex:3]; + void (^finishBlock)() = unretainedArgument; + finishBlock(); + }); // Close the presented content. + completionBlockExpectation = [self expectationWithDescription:@"completion callback"]; + callbackMatcherExpectation = [self expectationWithDescription:@"callbackMatcher callback"]; XCTAssertTrue([presenter canHandleURL:presenterURL]); [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; OCMVerifyAll(mockUIDelegate); diff --git a/Example/Auth/Tests/FIRUserTests.m b/Example/Auth/Tests/FIRUserTests.m index 64051a35e4b..a0a26445a31 100644 --- a/Example/Auth/Tests/FIRUserTests.m +++ b/Example/Auth/Tests/FIRUserTests.m @@ -1302,6 +1302,7 @@ - (void)testlinkEmailAndRetrieveDataError { completion:^(FIRAuthDataResult *_Nullable linkAuthResult, NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); XCTAssertNil(linkAuthResult); XCTAssertEqual(error.code, FIRAuthErrorCodeTooManyRequests); [expectation fulfill]; diff --git a/Firebase/Auth/FirebaseAuth.podspec b/Firebase/Auth/FirebaseAuth.podspec index ad4c54f5c16..57b8a733a36 100644 --- a/Firebase/Auth/FirebaseAuth.podspec +++ b/Firebase/Auth/FirebaseAuth.podspec @@ -33,6 +33,8 @@ Simplify your iOS development, grow your user base, and monetize more effectivel 'Source/**/FIRAuthDefaultUIDelegate.[mh]', 'Source/**/FIRAuthUIDelegate.h', 'Source/**/FIRAuthURLPresenter.[mh]', + 'Source/**/FIRAuthWebView.[mh]', + 'Source/**/FIRAuthWebViewController.[mh]', 'Source/**/FIRPhoneAuthCredential.[mh]', 'Source/**/FIRPhoneAuthProvider.[mh]' s.public_header_files = 'Source/Public/*.h' diff --git a/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m index 32b5da0d8a3..7587f1b0c80 100644 --- a/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m +++ b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m @@ -146,15 +146,16 @@ - (void)verifyPhoneNumber:(NSString *)phoneNumber callBackOnMainThread(nil, error); return; } + FIRAuthURLCallbackMatcher callbackMatcher = ^BOOL(NSURL *_Nullable callbackURL) { + return [self isVerifyAppURL:callbackURL]; + }; [_auth.authURLPresenter presentURL:reCAPTCHAURL UIDelegate:UIDelegate - callbackMatcher:^BOOL(NSURL * _Nullable callbackURL) { - return [self isVerifyAppURL:callbackURL]; - } + callbackMatcher:callbackMatcher completion:^(NSURL *_Nullable callbackURL, NSError *_Nullable error) { if (error) { - completion(nil, error); + callBackOnMainThread(nil, error); return; } NSError *reCAPTCHAError; diff --git a/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.h b/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.h new file mode 100644 index 00000000000..071a92a9205 --- /dev/null +++ b/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.h @@ -0,0 +1,39 @@ +/* + * Copyright 2017 Google + * + * Licensed 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 + +#import "FIRAuthUIDelegate.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRAuthDefaultUIDelegate : NSObject + +/** @fn defaultUIDelegate + @brief Unavailable. Please use initWithViewController: + */ +- (instancetype)init NS_UNAVAILABLE; + +/** @fn initWithViewController: + @brief Initializes the instance with a view controller. + @param viewController The view controller as the presenting view controller in @c GOIUIDelegate. + @return The initialized instance. + */ +- (instancetype)initWithViewController:(UIViewController *)viewController NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m b/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m index 118b73c3606..8b8f6cf5290 100644 --- a/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m +++ b/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m @@ -14,26 +14,10 @@ * limitations under the License. */ -#import - -#import "FIRAuthUIDelegate.h" +#import "FIRAuthDefaultUIDelegate.h" NS_ASSUME_NONNULL_BEGIN -@interface FIRAuthDefaultUIDelegate : NSObject -/** @fn defaultUIDelegate - @brief Unavailable. Please use initWithViewController: - */ -- (instancetype)init NS_UNAVAILABLE; - -/** @fn initWithViewController: - @brief Initializes the instance with a view controller. - @param viewController The view controller as the presenting view controller in @c GOIUIDelegate. - @return The initialized instance. - */ -- (instancetype)initWithViewController:(UIViewController *)viewController NS_DESIGNATED_INITIALIZER; -@end - @implementation FIRAuthDefaultUIDelegate { /** @var _viewController @brief The presenting view controller. diff --git a/Firebase/Auth/Source/FIRAuthURLPresenter.m b/Firebase/Auth/Source/FIRAuthURLPresenter.m index 722326c6332..3a679aa1c73 100644 --- a/Firebase/Auth/Source/FIRAuthURLPresenter.m +++ b/Firebase/Auth/Source/FIRAuthURLPresenter.m @@ -19,7 +19,9 @@ #import #import "FIRAuthErrorUtils.h" +#import "FIRAuthGlobalWorkQueue.h" #import "FIRAuthUIDelegate.h" +#import "FIRAuthWebViewController.h" NS_ASSUME_NONNULL_BEGIN @@ -31,7 +33,7 @@ @interface FIRAuthDefaultUIDelegate : NSObject + (id)defaultUIDelegate; @end -@interface FIRAuthURLPresenter () +@interface FIRAuthURLPresenter () @end @implementation FIRAuthURLPresenter { @@ -50,6 +52,11 @@ @implementation FIRAuthURLPresenter { */ SFSafariViewController *_Nullable _safariViewController; + /** @var _webViewController + @brief The FIRAuthWebViewController used for the current presentation, if any. + */ + FIRAuthWebViewController *_Nullable _webViewController; + /** @var _UIDelegate @brief The UIDelegate used to present the SFSafariViewController. */ @@ -75,16 +82,19 @@ - (void)presentURL:(NSURL *)URL _callbackMatcher = callbackMatcher; _completion = completion; _UIDelegate = UIDelegate ?: [FIRAuthDefaultUIDelegate defaultUIDelegate]; - if ([SFSafariViewController class]) { - SFSafariViewController *safariViewController = [[SFSafariViewController alloc] initWithURL:URL]; - _safariViewController = safariViewController; - _safariViewController.delegate = self; - [_UIDelegate presentViewController:safariViewController animated:YES completion:nil]; - return; - } else { - // TODO: Use web view instead. - [[UIApplication sharedApplication] openURL:URL]; - } + dispatch_async(dispatch_get_main_queue(), ^() { + if ([SFSafariViewController class]) { + _safariViewController = [[SFSafariViewController alloc] initWithURL:URL]; + _safariViewController.delegate = self; + [_UIDelegate presentViewController:_safariViewController animated:YES completion:nil]; + return; + } else { + _webViewController = [[FIRAuthWebViewController alloc] initWithURL:URL delegate:self]; + UINavigationController *navController = + [[UINavigationController alloc] initWithRootViewController:_webViewController]; + [_UIDelegate presentViewController:navController animated:YES completion:nil]; + } + }); } - (BOOL)canHandleURL:(NSURL *)URL { @@ -98,13 +108,45 @@ - (BOOL)canHandleURL:(NSURL *)URL { #pragma mark - SFSafariViewControllerDelegate - (void)safariViewControllerDidFinish:(SFSafariViewController *)controller { - if (controller == _safariViewController) { - _safariViewController = nil; - //TODO:Ensure that the SFSafariViewController is actually removed from the screen before - //invoking finishPresentationWithURL:error: - [self finishPresentationWithURL:nil - error:[FIRAuthErrorUtils webContextCancelledErrorWithMessage:nil]]; - } + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + if (controller == _safariViewController) { + _safariViewController = nil; + //TODO:Ensure that the SFSafariViewController is actually removed from the screen before + //invoking finishPresentationWithURL:error: + [self finishPresentationWithURL:nil + error:[FIRAuthErrorUtils webContextCancelledErrorWithMessage:nil]]; + } + }); +} + +#pragma mark - FIRAuthwebViewControllerDelegate + +- (BOOL)webViewController:(FIRAuthWebViewController *)webViewController canHandleURL:(NSURL *)URL { + __block BOOL result = NO; + dispatch_sync(FIRAuthGlobalWorkQueue(), ^() { + if (webViewController == _webViewController) { + result = [self canHandleURL:URL]; + } + }); + return result; +} + +- (void)webViewControllerDidCancel:(FIRAuthWebViewController *)webViewController { + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + if (webViewController == _webViewController) { + [self finishPresentationWithURL:nil + error:[FIRAuthErrorUtils webContextCancelledErrorWithMessage:nil]]; + } + }); +} + +- (void)webViewController:(FIRAuthWebViewController *)webViewController + didFailWithError:(NSError *)error { + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + if (webViewController == _webViewController) { + [self finishPresentationWithURL:nil error:error]; + } + }); } #pragma mark - Private methods @@ -127,8 +169,14 @@ - (void)finishPresentationWithURL:(nullable NSURL *)URL }; SFSafariViewController *safariViewController = _safariViewController; _safariViewController = nil; - if (safariViewController) { - [UIDelegate dismissViewControllerAnimated:YES completion:finishBlock]; + FIRAuthWebViewController *webViewController = _webViewController; + _webViewController = nil; + if (safariViewController || webViewController) { + dispatch_async(dispatch_get_main_queue(), ^() { + [UIDelegate dismissViewControllerAnimated:YES completion:^() { + dispatch_async(FIRAuthGlobalWorkQueue(), finishBlock); + }]; + }); } else { finishBlock(); } diff --git a/Firebase/Auth/Source/FIRAuthWebView.h b/Firebase/Auth/Source/FIRAuthWebView.h new file mode 100644 index 00000000000..f4a8d1b6781 --- /dev/null +++ b/Firebase/Auth/Source/FIRAuthWebView.h @@ -0,0 +1,35 @@ +/* + * Copyright 2017 Google + * + * Licensed 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRAuthWebView : UIView + +/** @property webView + * @brief The web view. + */ +@property(nonatomic, weak) UIWebView *webView; + +/** @property spinner + * @brief The spinner that indicates web view loading. + */ +@property(nonatomic, weak) UIActivityIndicatorView *spinner; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/FIRAuthWebView.m b/Firebase/Auth/Source/FIRAuthWebView.m new file mode 100644 index 00000000000..44f908283f3 --- /dev/null +++ b/Firebase/Auth/Source/FIRAuthWebView.m @@ -0,0 +1,78 @@ +/* + * Copyright 2017 Google + * + * Licensed 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 "FIRAuthWebView.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRAuthWebView + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + self.backgroundColor = [UIColor whiteColor]; + [self initializeSubviews]; + } + return self; +} + +- (void)initializeSubviews { + UIWebView *webView = [self createWebView]; + UIActivityIndicatorView *spinner = [self createSpinner]; + + // The order of the following controls z-order. + [self addSubview:webView]; + [self addSubview:spinner]; + + [self layoutSubviews]; + _webView = webView; + _spinner = spinner; +} + +// Calculate subview layouts. +- (void)layoutSubviews { + CGFloat height = self.bounds.size.height; + CGFloat width = self.bounds.size.width; + _webView.frame = CGRectMake(0, 0, width, height); + _spinner.center = _webView.center; +} + +// Initialize the web view. +- (UIWebView *)createWebView { + UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectZero]; + // Trickery to make the web view not do weird things (like showing a black background when + // the prompt in the navigation bar animates changes.) + webView.opaque = NO; + webView.backgroundColor = [UIColor clearColor]; + webView.scrollView.opaque = NO; + webView.scrollView.backgroundColor = [UIColor clearColor]; + webView.scrollView.bounces = NO; + webView.scrollView.alwaysBounceVertical = NO; + webView.scrollView.alwaysBounceHorizontal = NO; + return webView; +} + +// Initialize the spinner. +- (UIActivityIndicatorView *)createSpinner { + UIActivityIndicatorViewStyle spinnerStyle = UIActivityIndicatorViewStyleGray; + UIActivityIndicatorView *spinner = + [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:spinnerStyle]; + return spinner; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/FIRAuthWebViewController.h b/Firebase/Auth/Source/FIRAuthWebViewController.h new file mode 100644 index 00000000000..f442dc9950c --- /dev/null +++ b/Firebase/Auth/Source/FIRAuthWebViewController.h @@ -0,0 +1,63 @@ +/* + * Copyright 2017 Google + * + * Licensed 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 + +@class FIRAuthWebViewController; + +NS_ASSUME_NONNULL_BEGIN + +@protocol FIRAuthWebViewDelegate + +/** @fn webViewController:canHandleURL: + @brief Determines if a URL should be handled by the delegate. + @param URL The URL to handle. + @return Whether the URL could be handled or not. + */ +- (BOOL)webViewController:(FIRAuthWebViewController *)webViewController canHandleURL:(NSURL *)URL; + +/** @fn webViewControllerDidCancel: + @brief Notifies the delegate that the web view controller is being cancelled by the user. + @param webViewController The web view controller in question. + */ +- (void)webViewControllerDidCancel:(FIRAuthWebViewController *)webViewController; + +/** @fn webViewController:didFailWithError: + @brief Notifies the delegate that the web view controller failed to load a page. + @param webViewController The web view controller in question. + @param error The error that has occurred. + */ +- (void)webViewController:(FIRAuthWebViewController *)webViewController + didFailWithError:(NSError *)error; + +@end + +@interface FIRAuthWebViewController : UIViewController + +// Please call initWithURL:delegate: +- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil + bundle:(nullable NSBundle *)nibBundleOrNil NS_UNAVAILABLE; + +// Please call initWithURL:delegate: +- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE; + +- (instancetype)initWithURL:(NSURL *)URL + delegate:(__weak id)delegate + NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/FIRAuthWebViewController.m b/Firebase/Auth/Source/FIRAuthWebViewController.m new file mode 100644 index 00000000000..313b473f6e4 --- /dev/null +++ b/Firebase/Auth/Source/FIRAuthWebViewController.m @@ -0,0 +1,115 @@ +/* + * Copyright 2017 Google + * + * Licensed 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 "FIRAuthWebViewController.h" + +#import "FIRAuthWebView.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRAuthWebViewController () +@end + +@implementation FIRAuthWebViewController { + /** @var _URL + @brief The initial URL to display. + */ + NSURL *_URL; + + /** @var _delegate + @brief The delegate to call. + */ + __weak id _delegate; + + /** @var _webView; + @brief The web view instance for easier access. + */ + __weak FIRAuthWebView *_webView; +} + +- (instancetype)initWithURL:(NSURL *)URL + delegate:(__weak id)delegate { + self = [super initWithNibName:nil bundle:nil]; + if (self) { + _URL = URL; + _delegate = delegate; + } + return self; +} + +#pragma mark - Lifecycle + +- (void)loadView { + FIRAuthWebView *webView = [[FIRAuthWebView alloc] initWithFrame:[UIScreen mainScreen].bounds]; + webView.webView.delegate = self; + self.view = webView; + _webView = webView; + + // Set an initial prompt so that there is no animation on first webview navigation. + //self.navigationItem.prompt = @" "; + //self.navigationItem.title = @"asdf"; + self.navigationItem.leftBarButtonItem = + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel + target:self + action:@selector(cancel)]; +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + + // Loads the requested URL in the web view. + [_webView.webView loadRequest:[NSURLRequest requestWithURL:_URL]]; +} + +#pragma mark - UI Targets + +- (void)cancel { + [_delegate webViewControllerDidCancel:self]; +} + +#pragma mark - UIWebViewDelegate + +- (BOOL)webView:(UIWebView *)webView + shouldStartLoadWithRequest:(NSURLRequest *)request + navigationType:(UIWebViewNavigationType)navigationType { + return ![_delegate webViewController:self canHandleURL:request.URL]; +} + +- (void)webViewDidStartLoad:(UIWebView *)webView { + // Show & animate the activity indicator. + _webView.spinner.hidden = NO; + [_webView.spinner startAnimating]; +} + +- (void)webViewDidFinishLoad:(UIWebView *)webView { + // Hide & stop the activity indicator. + _webView.spinner.hidden = YES; + [_webView.spinner stopAnimating]; +} + +- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error { + if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) { + // It's okay for the page to be redirected before it is completely loaded. See b/32028062 . + return; + } + // Forward notification to our delegate. + [self webViewDidFinishLoad:webView]; + [_delegate webViewController:self didFailWithError:error]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseCommunity.podspec b/FirebaseCommunity.podspec index 307a93dbf52..735dac6bc85 100644 --- a/FirebaseCommunity.podspec +++ b/FirebaseCommunity.podspec @@ -45,6 +45,8 @@ Firebase Development CocoaPod including experimental and community supported fea 'Firebase/Auth/Source/**/FIRAuthDefaultUIDelegate.[mh]', 'Firebase/Auth/Source/**/FIRAuthUIDelegate.h', 'Firebase/Auth/Source/**/FIRAuthURLPresenter.[mh]', + 'Firebase/Auth/Source/**/FIRAuthWebView.[mh]', + 'Firebase/Auth/Source/**/FIRAuthWebViewController.[mh]', 'Firebase/Auth/Source/**/FIRPhoneAuthCredential.[mh]', 'Firebase/Auth/Source/**/FIRPhoneAuthProvider.[mh]' sp.public_header_files = 'Firebase/Auth/Source/Public/*.h' From b9145bbaecd639e9dbcbd6b747d559c8b9d1908e Mon Sep 17 00:00:00 2001 From: Xiangtian Dai Date: Mon, 11 Sep 2017 19:33:17 -0700 Subject: [PATCH 2/4] - Minor style and test fixes. --- Example/Auth/Tests/FIRAuthURLPresenterTests.m | 4 ++-- Firebase/Auth/Source/FIRAuthWebViewController.m | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Example/Auth/Tests/FIRAuthURLPresenterTests.m b/Example/Auth/Tests/FIRAuthURLPresenterTests.m index 4ae737b7dd5..3b9ec678e69 100644 --- a/Example/Auth/Tests/FIRAuthURLPresenterTests.m +++ b/Example/Auth/Tests/FIRAuthURLPresenterTests.m @@ -129,8 +129,8 @@ - (void)testFIRAuthURLPresenterUsingDefaultUIDelegate:(BOOL)usesDefaultUIDelegat // Indices 0 and 1 indicate the hidden arguments self and _cmd. // `completion` is at index 3. [invocation getArgument:&unretainedArgument atIndex:3]; - void (^finishBlock)() = unretainedArgument; - finishBlock(); + void (^completion)() = unretainedArgument; + dispatch_async(dispatch_get_main_queue(), completion); }); // Close the presented content. diff --git a/Firebase/Auth/Source/FIRAuthWebViewController.m b/Firebase/Auth/Source/FIRAuthWebViewController.m index 313b473f6e4..b9a2473690f 100644 --- a/Firebase/Auth/Source/FIRAuthWebViewController.m +++ b/Firebase/Auth/Source/FIRAuthWebViewController.m @@ -57,10 +57,6 @@ - (void)loadView { webView.webView.delegate = self; self.view = webView; _webView = webView; - - // Set an initial prompt so that there is no animation on first webview navigation. - //self.navigationItem.prompt = @" "; - //self.navigationItem.title = @"asdf"; self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self From f43d87c6c85b98213a75cc836ec032a498a7f88a Mon Sep 17 00:00:00 2001 From: Xiangtian Dai Date: Mon, 11 Sep 2017 20:03:19 -0700 Subject: [PATCH 3/4] Fixes yet another threading issue. --- Firebase/Auth/Source/FIRAuthURLPresenter.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firebase/Auth/Source/FIRAuthURLPresenter.m b/Firebase/Auth/Source/FIRAuthURLPresenter.m index 3a679aa1c73..848f010072a 100644 --- a/Firebase/Auth/Source/FIRAuthURLPresenter.m +++ b/Firebase/Auth/Source/FIRAuthURLPresenter.m @@ -81,8 +81,8 @@ - (void)presentURL:(NSURL *)URL _isPresenting = YES; _callbackMatcher = callbackMatcher; _completion = completion; - _UIDelegate = UIDelegate ?: [FIRAuthDefaultUIDelegate defaultUIDelegate]; dispatch_async(dispatch_get_main_queue(), ^() { + _UIDelegate = UIDelegate ?: [FIRAuthDefaultUIDelegate defaultUIDelegate]; if ([SFSafariViewController class]) { _safariViewController = [[SFSafariViewController alloc] initWithURL:URL]; _safariViewController.delegate = self; From 52ccef8355e9568e7fcaaa781216cfff11f4e428 Mon Sep 17 00:00:00 2001 From: Xiangtian Dai Date: Tue, 12 Sep 2017 09:55:32 -0700 Subject: [PATCH 4/4] Addresses review comments. --- Example/Auth/Tests/FIRAuthURLPresenterTests.m | 4 ++-- Firebase/Auth/Source/FIRAuthDefaultUIDelegate.h | 11 +++++------ Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m | 12 ++++++++++++ Firebase/Auth/Source/FIRAuthURLPresenter.m | 9 +-------- Firebase/Auth/Source/FIRAuthWebView.m | 14 +++++++++++--- Firebase/Auth/Source/FIRAuthWebViewController.h | 8 ++++++-- 6 files changed, 37 insertions(+), 21 deletions(-) diff --git a/Example/Auth/Tests/FIRAuthURLPresenterTests.m b/Example/Auth/Tests/FIRAuthURLPresenterTests.m index 3b9ec678e69..fcc64e9e9ff 100644 --- a/Example/Auth/Tests/FIRAuthURLPresenterTests.m +++ b/Example/Auth/Tests/FIRAuthURLPresenterTests.m @@ -132,10 +132,10 @@ - (void)testFIRAuthURLPresenterUsingDefaultUIDelegate:(BOOL)usesDefaultUIDelegat void (^completion)() = unretainedArgument; dispatch_async(dispatch_get_main_queue(), completion); }); - - // Close the presented content. completionBlockExpectation = [self expectationWithDescription:@"completion callback"]; callbackMatcherExpectation = [self expectationWithDescription:@"callbackMatcher callback"]; + + // Close the presented content. XCTAssertTrue([presenter canHandleURL:presenterURL]); [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; OCMVerifyAll(mockUIDelegate); diff --git a/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.h b/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.h index 071a92a9205..f0e5d805847 100644 --- a/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.h +++ b/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.h @@ -23,16 +23,15 @@ NS_ASSUME_NONNULL_BEGIN @interface FIRAuthDefaultUIDelegate : NSObject /** @fn defaultUIDelegate - @brief Unavailable. Please use initWithViewController: + @brief Unavailable. Please use @c +defaultUIDelegate: */ - (instancetype)init NS_UNAVAILABLE; -/** @fn initWithViewController: - @brief Initializes the instance with a view controller. - @param viewController The view controller as the presenting view controller in @c GOIUIDelegate. - @return The initialized instance. +/** @fn defaultUIDelegate + @brief Returns a default FIRAuthUIDelegate object. + @return The default FIRAuthUIDelegate object. */ -- (instancetype)initWithViewController:(UIViewController *)viewController NS_DESIGNATED_INITIALIZER; ++ (id)defaultUIDelegate; @end diff --git a/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m b/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m index 8b8f6cf5290..a00d0e91e6f 100644 --- a/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m +++ b/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m @@ -18,6 +18,18 @@ NS_ASSUME_NONNULL_BEGIN +@interface FIRAuthDefaultUIDelegate () + +/** @fn initWithViewController: + @brief Initializes the instance with a view controller. + @param viewController The view controller as the presenting view controller in @c + FIRAuthUIDelegate. + @return The initialized instance. + */ +- (instancetype)initWithViewController:(UIViewController *)viewController NS_DESIGNATED_INITIALIZER; + +@end + @implementation FIRAuthDefaultUIDelegate { /** @var _viewController @brief The presenting view controller. diff --git a/Firebase/Auth/Source/FIRAuthURLPresenter.m b/Firebase/Auth/Source/FIRAuthURLPresenter.m index 848f010072a..d923c8a7c8c 100644 --- a/Firebase/Auth/Source/FIRAuthURLPresenter.m +++ b/Firebase/Auth/Source/FIRAuthURLPresenter.m @@ -18,6 +18,7 @@ #import +#import "FIRAuthDefaultUIDelegate.h" #import "FIRAuthErrorUtils.h" #import "FIRAuthGlobalWorkQueue.h" #import "FIRAuthUIDelegate.h" @@ -25,14 +26,6 @@ NS_ASSUME_NONNULL_BEGIN -@interface FIRAuthDefaultUIDelegate : NSObject -/** @fn defaultUIDelegate - @brief Returns a default FIRAuthUIDelegate object. - @return The default FIRAuthUIDelegate object. - */ -+ (id)defaultUIDelegate; -@end - @interface FIRAuthURLPresenter () @end diff --git a/Firebase/Auth/Source/FIRAuthWebView.m b/Firebase/Auth/Source/FIRAuthWebView.m index 44f908283f3..80b90f0fbe2 100644 --- a/Firebase/Auth/Source/FIRAuthWebView.m +++ b/Firebase/Auth/Source/FIRAuthWebView.m @@ -29,6 +29,9 @@ - (instancetype)initWithFrame:(CGRect)frame { return self; } +/** @fn initializeSubviews + @brief Initializes the subviews of this view. + */ - (void)initializeSubviews { UIWebView *webView = [self createWebView]; UIActivityIndicatorView *spinner = [self createSpinner]; @@ -42,7 +45,6 @@ - (void)initializeSubviews { _spinner = spinner; } -// Calculate subview layouts. - (void)layoutSubviews { CGFloat height = self.bounds.size.height; CGFloat width = self.bounds.size.width; @@ -50,7 +52,10 @@ - (void)layoutSubviews { _spinner.center = _webView.center; } -// Initialize the web view. +/** @fn createWebView + @brief Creates a web view to be used by this view. + @return The newly created web view. + */ - (UIWebView *)createWebView { UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectZero]; // Trickery to make the web view not do weird things (like showing a black background when @@ -65,7 +70,10 @@ - (UIWebView *)createWebView { return webView; } -// Initialize the spinner. +/** @fn createSpinner + @brief Creates a spinner to be used by this view. + @return The newly created spinner. + */ - (UIActivityIndicatorView *)createSpinner { UIActivityIndicatorViewStyle spinnerStyle = UIActivityIndicatorViewStyleGray; UIActivityIndicatorView *spinner = diff --git a/Firebase/Auth/Source/FIRAuthWebViewController.h b/Firebase/Auth/Source/FIRAuthWebViewController.h index f442dc9950c..5c2c0423f16 100644 --- a/Firebase/Auth/Source/FIRAuthWebViewController.h +++ b/Firebase/Auth/Source/FIRAuthWebViewController.h @@ -47,11 +47,15 @@ NS_ASSUME_NONNULL_BEGIN @interface FIRAuthWebViewController : UIViewController -// Please call initWithURL:delegate: +/** @fn initWithNibName:bundle: + * @brief Please call initWithURL:delegate: + */ - (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil NS_UNAVAILABLE; -// Please call initWithURL:delegate: +/** @fn initWithCoder: + * @brief Please call initWithURL:delegate: + */ - (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE; - (instancetype)initWithURL:(NSURL *)URL