diff options
author | Xiangtian Dai <xiangtian@google.com> | 2017-09-12 13:49:05 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-09-12 13:49:05 -0700 |
commit | c8ea66eecfb256925e457d14b64bc08984da4387 (patch) | |
tree | a5c01d7f36c447111092ec80cbde68cfdb7fca07 | |
parent | e8cf906d21501513f53666a34d4c4eb3e95bef6e (diff) |
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.
-rw-r--r-- | Example/Auth/Tests/FIRAuthURLPresenterTests.m | 86 | ||||
-rw-r--r-- | Example/Auth/Tests/FIRUserTests.m | 1 | ||||
-rw-r--r-- | Firebase/Auth/FirebaseAuth.podspec | 2 | ||||
-rw-r--r-- | Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m | 9 | ||||
-rw-r--r-- | Firebase/Auth/Source/FIRAuthDefaultUIDelegate.h | 38 | ||||
-rw-r--r-- | Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m | 14 | ||||
-rw-r--r-- | Firebase/Auth/Source/FIRAuthURLPresenter.m | 99 | ||||
-rw-r--r-- | Firebase/Auth/Source/FIRAuthWebView.h | 35 | ||||
-rw-r--r-- | Firebase/Auth/Source/FIRAuthWebView.m | 86 | ||||
-rw-r--r-- | Firebase/Auth/Source/FIRAuthWebViewController.h | 67 | ||||
-rw-r--r-- | Firebase/Auth/Source/FIRAuthWebViewController.m | 111 | ||||
-rw-r--r-- | FirebaseCommunity.podspec | 2 |
12 files changed, 466 insertions, 84 deletions
diff --git a/Example/Auth/Tests/FIRAuthURLPresenterTests.m b/Example/Auth/Tests/FIRAuthURLPresenterTests.m index 5d42e5f..fcc64e9 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 @@ static NSTimeInterval kExpectationTimeout = 1; */ - (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,68 +70,70 @@ static NSTimeInterval kExpectationTimeout = 1; 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 (^completion)() = unretainedArgument; + dispatch_async(dispatch_get_main_queue(), completion); + }); + completionBlockExpectation = [self expectationWithDescription:@"completion callback"]; + callbackMatcherExpectation = [self expectationWithDescription:@"callbackMatcher callback"]; // Close the presented content. XCTAssertTrue([presenter canHandleURL:presenterURL]); diff --git a/Example/Auth/Tests/FIRUserTests.m b/Example/Auth/Tests/FIRUserTests.m index 64051a3..a0a2644 100644 --- a/Example/Auth/Tests/FIRUserTests.m +++ b/Example/Auth/Tests/FIRUserTests.m @@ -1302,6 +1302,7 @@ static const NSTimeInterval kExpectationTimeout = 1; 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 ad4c54f..57b8a73 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 32b5da0..7587f1b 100644 --- a/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m +++ b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m @@ -146,15 +146,16 @@ NSString *const kReCAPTCHAURLStringFormat = @"https://%@/__/auth/handler?%@"; 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 0000000..f0e5d80 --- /dev/null +++ b/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.h @@ -0,0 +1,38 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FIRAuthUIDelegate.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRAuthDefaultUIDelegate : NSObject <FIRAuthUIDelegate> + +/** @fn defaultUIDelegate + @brief Unavailable. Please use @c +defaultUIDelegate: + */ +- (instancetype)init NS_UNAVAILABLE; + +/** @fn defaultUIDelegate + @brief Returns a default FIRAuthUIDelegate object. + @return The default FIRAuthUIDelegate object. + */ ++ (id<FIRAuthUIDelegate>)defaultUIDelegate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m b/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m index 118b73c..a00d0e9 100644 --- a/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m +++ b/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m @@ -14,24 +14,20 @@ * limitations under the License. */ -#import <Foundation/Foundation.h> - -#import "FIRAuthUIDelegate.h" +#import "FIRAuthDefaultUIDelegate.h" NS_ASSUME_NONNULL_BEGIN -@interface FIRAuthDefaultUIDelegate : NSObject <FIRAuthUIDelegate> -/** @fn defaultUIDelegate - @brief Unavailable. Please use initWithViewController: - */ -- (instancetype)init NS_UNAVAILABLE; +@interface FIRAuthDefaultUIDelegate () /** @fn initWithViewController: @brief Initializes the instance with a view controller. - @param viewController The view controller as the presenting view controller in @c GOIUIDelegate. + @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 { diff --git a/Firebase/Auth/Source/FIRAuthURLPresenter.m b/Firebase/Auth/Source/FIRAuthURLPresenter.m index 722326c..d923c8a 100644 --- a/Firebase/Auth/Source/FIRAuthURLPresenter.m +++ b/Firebase/Auth/Source/FIRAuthURLPresenter.m @@ -18,20 +18,15 @@ #import <SafariServices/SafariServices.h> +#import "FIRAuthDefaultUIDelegate.h" #import "FIRAuthErrorUtils.h" +#import "FIRAuthGlobalWorkQueue.h" #import "FIRAuthUIDelegate.h" +#import "FIRAuthWebViewController.h" NS_ASSUME_NONNULL_BEGIN -@interface FIRAuthDefaultUIDelegate : NSObject <FIRAuthUIDelegate> -/** @fn defaultUIDelegate - @brief Returns a default FIRAuthUIDelegate object. - @return The default FIRAuthUIDelegate object. - */ -+ (id<FIRAuthUIDelegate>)defaultUIDelegate; -@end - -@interface FIRAuthURLPresenter () <SFSafariViewControllerDelegate> +@interface FIRAuthURLPresenter () <SFSafariViewControllerDelegate, FIRAuthWebViewDelegate> @end @implementation FIRAuthURLPresenter { @@ -50,6 +45,11 @@ NS_ASSUME_NONNULL_BEGIN */ 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. */ @@ -74,17 +74,20 @@ NS_ASSUME_NONNULL_BEGIN _isPresenting = YES; _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(), ^() { + _UIDelegate = UIDelegate ?: [FIRAuthDefaultUIDelegate defaultUIDelegate]; + 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 +101,45 @@ NS_ASSUME_NONNULL_BEGIN #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 +162,14 @@ NS_ASSUME_NONNULL_BEGIN }; 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 0000000..f4a8d1b --- /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 <UIKit/UIKit.h> + +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 0000000..80b90f0 --- /dev/null +++ b/Firebase/Auth/Source/FIRAuthWebView.m @@ -0,0 +1,86 @@ +/* + * 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; +} + +/** @fn initializeSubviews + @brief Initializes the subviews of this view. + */ +- (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; +} + +- (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; +} + +/** @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 + // 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; +} + +/** @fn createSpinner + @brief Creates a spinner to be used by this view. + @return The newly created 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 0000000..5c2c042 --- /dev/null +++ b/Firebase/Auth/Source/FIRAuthWebViewController.h @@ -0,0 +1,67 @@ +/* + * 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 <UIKit/UIKit.h> + +@class FIRAuthWebViewController; + +NS_ASSUME_NONNULL_BEGIN + +@protocol FIRAuthWebViewDelegate <NSObject> + +/** @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 + +/** @fn initWithNibName:bundle: + * @brief Please call initWithURL:delegate: + */ +- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil + bundle:(nullable NSBundle *)nibBundleOrNil NS_UNAVAILABLE; + +/** @fn initWithCoder: + * @brief Please call initWithURL:delegate: + */ +- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE; + +- (instancetype)initWithURL:(NSURL *)URL + delegate:(__weak id<FIRAuthWebViewDelegate>)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 0000000..b9a2473 --- /dev/null +++ b/Firebase/Auth/Source/FIRAuthWebViewController.m @@ -0,0 +1,111 @@ +/* + * 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 () <UIWebViewDelegate> +@end + +@implementation FIRAuthWebViewController { + /** @var _URL + @brief The initial URL to display. + */ + NSURL *_URL; + + /** @var _delegate + @brief The delegate to call. + */ + __weak id<FIRAuthWebViewDelegate> _delegate; + + /** @var _webView; + @brief The web view instance for easier access. + */ + __weak FIRAuthWebView *_webView; +} + +- (instancetype)initWithURL:(NSURL *)URL + delegate:(__weak id<FIRAuthWebViewDelegate>)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; + 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 307a93d..735dac6 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' |