aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Xiangtian Dai <xiangtian@google.com>2017-09-12 13:49:05 -0700
committerGravatar GitHub <noreply@github.com>2017-09-12 13:49:05 -0700
commitc8ea66eecfb256925e457d14b64bc08984da4387 (patch)
treea5c01d7f36c447111092ec80cbde68cfdb7fca07
parente8cf906d21501513f53666a34d4c4eb3e95bef6e (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.m86
-rw-r--r--Example/Auth/Tests/FIRUserTests.m1
-rw-r--r--Firebase/Auth/FirebaseAuth.podspec2
-rw-r--r--Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m9
-rw-r--r--Firebase/Auth/Source/FIRAuthDefaultUIDelegate.h38
-rw-r--r--Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m14
-rw-r--r--Firebase/Auth/Source/FIRAuthURLPresenter.m99
-rw-r--r--Firebase/Auth/Source/FIRAuthWebView.h35
-rw-r--r--Firebase/Auth/Source/FIRAuthWebView.m86
-rw-r--r--Firebase/Auth/Source/FIRAuthWebViewController.h67
-rw-r--r--Firebase/Auth/Source/FIRAuthWebViewController.m111
-rw-r--r--FirebaseCommunity.podspec2
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'