aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Zsika Phillip <protocol86@users.noreply.github.com>2017-09-01 13:38:12 -0700
committerGravatar GitHub <noreply@github.com>2017-09-01 13:38:12 -0700
commit36c20d92b7a1f4ac5cb950df61d381ee60be9670 (patch)
treeaff9aa101a52b54ce04357766ef1d3c3c9b380f6
parent0ce42970fcc94116700c9e6914bc97ce4dd73d00 (diff)
Add FirAuthUrlPresenter (#222)
* Adds FIRAuthURLPResenter and FIRAuthUIDelegate
-rw-r--r--AuthSamples/Samples.xcodeproj/project.pbxproj4
-rw-r--r--Example/Auth/Tests/FIRAuthURLPresenterTests.m183
-rw-r--r--Example/Firebase.xcodeproj/project.pbxproj4
-rw-r--r--Firebase/Auth/FirebaseAuth.podspec1
-rw-r--r--Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m85
-rw-r--r--Firebase/Auth/Source/FIRAuthErrorUtils.h14
-rw-r--r--Firebase/Auth/Source/FIRAuthErrorUtils.m28
-rw-r--r--Firebase/Auth/Source/FIRAuthInternalErrors.h11
-rw-r--r--Firebase/Auth/Source/FIRAuthURLPresenter.h61
-rw-r--r--Firebase/Auth/Source/FIRAuthURLPresenter.m137
-rw-r--r--Firebase/Auth/Source/Public/FIRAuthErrors.h9
-rw-r--r--Firebase/Auth/Source/Public/FIRAuthUIDelegate.h51
12 files changed, 588 insertions, 0 deletions
diff --git a/AuthSamples/Samples.xcodeproj/project.pbxproj b/AuthSamples/Samples.xcodeproj/project.pbxproj
index 647c175..69990c0 100644
--- a/AuthSamples/Samples.xcodeproj/project.pbxproj
+++ b/AuthSamples/Samples.xcodeproj/project.pbxproj
@@ -26,6 +26,7 @@
52C975C71EE10B1304EBBEB2 /* Pods_SwiftSample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BC8C39EF1F42A0C750FF5186 /* Pods_SwiftSample.framework */; };
569C3F4E18627674CABE02AE /* Pods_EarlGreyTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AEE2E563FADF8C3382956B4F /* Pods_EarlGreyTests.framework */; };
67AFFB52FF0FC4668D92F2E4 /* Pods_Sample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D5FE06BD9AA795DFBA9EFAAD /* Pods_Sample.framework */; };
+ 7E04ACA41F565EA900788114 /* FIRAuthURLPresenterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E04ACA31F565EA900788114 /* FIRAuthURLPresenterTests.m */; };
7E9969EE1F4E277900627C2B /* FIRGetProjectConfigRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E9969EC1F4E277900627C2B /* FIRGetProjectConfigRequestTests.m */; };
7EDFD35B1F0EA29200B29DC5 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7EDFD35D1F0EA29200B29DC5 /* Localizable.strings */; };
7EEEFEE61F4E4F75000FF966 /* FIRGetProjectConfigResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7EEA8ADD1F4E4E840014A23B /* FIRGetProjectConfigResponseTests.m */; };
@@ -183,6 +184,7 @@
4FFAD3F37BC4D7CEF0CAD579 /* Pods_FirebaseAuthUnitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FirebaseAuthUnitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
57150555A6B03949ECB58AD9 /* Pods-TestApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TestApp.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TestApp/Pods-TestApp.debug.xcconfig"; sourceTree = "<group>"; };
6EC09307D636721EAAB89BB2 /* Pods_ApiTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ApiTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 7E04ACA31F565EA900788114 /* FIRAuthURLPresenterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRAuthURLPresenterTests.m; path = ../../Example/Auth/Tests/FIRAuthURLPresenterTests.m; sourceTree = "<group>"; };
7E0BC64A1F199D86008BE4E0 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/Localizable.strings"; sourceTree = "<group>"; };
7E0BC64E1F19A77C008BE4E0 /* ru-RU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ru-RU"; path = "ru-RU.lproj/Localizable.strings"; sourceTree = "<group>"; };
7E9969EC1F4E277900627C2B /* FIRGetProjectConfigRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRGetProjectConfigRequestTests.m; path = ../../Example/Auth/Tests/FIRGetProjectConfigRequestTests.m; sourceTree = "<group>"; };
@@ -557,6 +559,7 @@
DE5371B21EA7E89D000DA57F /* Tests-Info.plist */,
7E9969EC1F4E277900627C2B /* FIRGetProjectConfigRequestTests.m */,
7EEA8ADD1F4E4E840014A23B /* FIRGetProjectConfigResponseTests.m */,
+ 7E04ACA31F565EA900788114 /* FIRAuthURLPresenterTests.m */,
);
path = UnitTests;
sourceTree = "<group>";
@@ -1332,6 +1335,7 @@
DECEA56E1EBBED1200273585 /* FIRAuthAppCredentialManagerTests.m in Sources */,
DE5371DE1EA7E89D000DA57F /* OCMStubRecorder+FIRAuthUnitTests.m in Sources */,
DE5371DC1EA7E89D000DA57F /* FIRVerifyPhoneNumberRequestTests.m in Sources */,
+ 7E04ACA41F565EA900788114 /* FIRAuthURLPresenterTests.m in Sources */,
DE5371B51EA7E89D000DA57F /* FIRAuthAppCredentialTests.m in Sources */,
DE5371CE1EA7E89D000DA57F /* FIRSetAccountInfoRequestTests.m in Sources */,
DE5371D41EA7E89D000DA57F /* FIRVerifyAssertionRequestTests.m in Sources */,
diff --git a/Example/Auth/Tests/FIRAuthURLPresenterTests.m b/Example/Auth/Tests/FIRAuthURLPresenterTests.m
new file mode 100644
index 0000000..602c54a
--- /dev/null
+++ b/Example/Auth/Tests/FIRAuthURLPresenterTests.m
@@ -0,0 +1,183 @@
+/*
+ * 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 <SafariServices/SafariServices.h>
+#import <XCTest/XCTest.h>
+
+#import "FIRAuthURLPresenter.h"
+#import "FIRAuthUIDelegate.h"
+#import <OCMock/OCMock.h>
+
+/** @var kExpectationTimeout
+ @brief The maximum time waiting for expectations to fulfill.
+ */
+static NSTimeInterval kExpectationTimeout = 1;
+
+@interface FIRAuthDefaultUIDelegate : NSObject <FIRAuthUIDelegate>
+/** @fn defaultUIDelegate
+ @brief Returns a default FIRAuthUIDelegate object.
+ @return The default FIRAuthUIDelegate object.
+ */
++ (id<FIRAuthUIDelegate>)defaultUIDelegate;
+@end
+
+@interface FIRAuthURLPresenterTests : XCTestCase
+
+@end
+
+@implementation FIRAuthURLPresenterTests
+
+/** @fn testFIRAuthURLPresenterNonNilUIDelegate
+ @brief Tests @c FIRAuthURLPresenter class showing UI with a non-nil UIDelegate.
+ */
+- (void)testFIRAuthURLPresenterNonNilUIDelegate {
+ id mockUIDelegate = OCMProtocolMock(@protocol(FIRAuthUIDelegate));
+ NSURL *presenterURL = [NSURL URLWithString:@"https://presenter.url"];
+ FIRAuthURLPresenter *presenter = [[FIRAuthURLPresenter alloc] init];
+
+ if ([SFSafariViewController class]) {
+ XCTestExpectation *callbackMatcherExpectation =
+ [self expectationWithDescription:@"callbackMatcher callback"];
+ FIRAuthURLCallbackMatcher callbackMatcher = ^BOOL(NSURL *_Nonnull callbackURL) {
+ XCTAssertEqualObjects(callbackURL, presenterURL);
+ [callbackMatcherExpectation fulfill];
+ return YES;
+ };
+
+ XCTestExpectation *completionBlockExpectation =
+ [self expectationWithDescription:@"completion callback"];
+ FIRAuthURLPresentationCompletion completionBlock = ^(NSURL *_Nullable callbackURL,
+ NSError *_Nullable error) {
+ XCTAssertEqualObjects(callbackURL, presenterURL);
+ XCTAssertNil(error);
+ [completionBlockExpectation fulfill];
+ };
+
+ 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);
+ XCTAssertTrue([viewController isKindOfClass:[SFSafariViewController class]]);
+ });
+ [presenter presentURL:presenterURL
+ UIDelegate:mockUIDelegate
+ callbackMatcher:callbackMatcher
+ completion:completionBlock];
+ OCMVerifyAll(mockUIDelegate);
+ 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();
+ });
+ [presenter canHandleURL:presenterURL];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(mockUIDelegate);
+ }
+}
+
+/** @fn testFIRAuthURLPresenterNilUIDelegate
+ @brief Tests @c FIRAuthURLPresenter class showing UI with a nil UIDelegate.
+ */
+- (void)testFIRAuthURLPresenterNilUIDelegate {
+ NSURL *presenterURL = [NSURL URLWithString:@"https://presenter.url"];
+ FIRAuthURLPresenter *presenter = [[FIRAuthURLPresenter alloc] init];
+
+ if ([SFSafariViewController class]) {
+ XCTestExpectation *callbackMatcherExpectation =
+ [self expectationWithDescription:@"callbackMatcher callback"];
+ FIRAuthURLCallbackMatcher callbackMatcher = ^BOOL(NSURL *_Nonnull callbackURL) {
+ XCTAssertEqualObjects(callbackURL, presenterURL);
+ [callbackMatcherExpectation fulfill];
+ return YES;
+ };
+ XCTestExpectation *completionBlockExpectation =
+ [self expectationWithDescription:@"completion callback"];
+ FIRAuthURLPresentationCompletion completionBlock = ^(NSURL *_Nullable callbackURL,
+ NSError *_Nullable error) {
+ XCTAssertEqualObjects(callbackURL, presenterURL);
+ XCTAssertNil(error);
+ [completionBlockExpectation fulfill];
+ };
+
+ id mockUIDelegate = OCMProtocolMock(@protocol(FIRAuthUIDelegate));
+
+ // Swizzle default UIDelegate
+ Method method = class_getClassMethod([FIRAuthDefaultUIDelegate class],
+ @selector(defaultUIDelegate));
+ __block IMP originalImplementation;
+ IMP newImplmentation = imp_implementationWithBlock(^id(id object) {
+ return mockUIDelegate;
+ });
+ originalImplementation = method_setImplementation(method, newImplmentation);
+ if ([SFSafariViewController class]) {
+ id presenterArg = [OCMArg isKindOfClass:[SFSafariViewController class]];
+ OCMExpect([mockUIDelegate presentViewController:presenterArg
+ animated:[OCMArg any]
+ completion:[OCMArg any]]).
+ 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);
+ XCTAssertTrue([viewController isKindOfClass:[SFSafariViewController class]]);
+ });
+ }
+ [presenter presentURL:presenterURL
+ UIDelegate:nil
+ callbackMatcher:callbackMatcher
+ completion:completionBlock];
+ OCMVerifyAll(mockUIDelegate);
+
+ 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();
+ });
+ [presenter canHandleURL:presenterURL];
+ [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];
+ OCMVerifyAll(mockUIDelegate);
+ // Unswizzle.
+ imp_removeBlock(method_setImplementation(method, originalImplementation));
+ }
+}
+
+#pragma mark - Method Swizzling
+
++ (id)mockDefaultUIDelegate {
+ return OCMProtocolMock(@protocol(FIRAuthUIDelegate));
+}
+
+@end
diff --git a/Example/Firebase.xcodeproj/project.pbxproj b/Example/Firebase.xcodeproj/project.pbxproj
index 5658767..e07302d 100644
--- a/Example/Firebase.xcodeproj/project.pbxproj
+++ b/Example/Firebase.xcodeproj/project.pbxproj
@@ -104,6 +104,7 @@
79A15731AA31012CD937CF3A /* Pods_Core_Example_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CAD129FFEC477E1129AE6AA1 /* Pods_Core_Example_iOS.framework */; };
7A02646DEF386689CCFB9011 /* Pods_Core_Tests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE6C9DD139E1FD21DC0F1082 /* Pods_Core_Tests_iOS.framework */; };
7AE9A7433F2BD9A52022AC71 /* Pods_Messaging_Tests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F902A29FA956ADD762F6921 /* Pods_Messaging_Tests_iOS.framework */; };
+ 7E9485421F578AC4005A3939 /* FIRAuthURLPresenterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E94853F1F578A9D005A3939 /* FIRAuthURLPresenterTests.m */; };
960665EC1C5F7A0E843A354F /* Pods_Database_Tests_macOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97768125F45377F35CA86EDC /* Pods_Database_Tests_macOS.framework */; };
AFAF36F51EC28C25004BDEE5 /* Shared.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AFAF36F41EC28C25004BDEE5 /* Shared.xcassets */; };
AFAF36F61EC28C25004BDEE5 /* Shared.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AFAF36F41EC28C25004BDEE5 /* Shared.xcassets */; };
@@ -739,6 +740,7 @@
6F7376F39846E902979416D4 /* Pods_Database_Example_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Database_Example_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
6FAA689FDCBD3261300292D5 /* Pods-Database_IntegrationTests_macOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Database_IntegrationTests_macOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Database_IntegrationTests_macOS/Pods-Database_IntegrationTests_macOS.debug.xcconfig"; sourceTree = "<group>"; };
73B480AA654FC97FA72C6293 /* Pods_Storage_Example_macOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Storage_Example_macOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 7E94853F1F578A9D005A3939 /* FIRAuthURLPresenterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAuthURLPresenterTests.m; sourceTree = "<group>"; };
81E83B5ABAE219234F213B27 /* Pods_Database_Tests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Database_Tests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
8496034D8156555C5FCF8F14 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
862CC98282A6123508E8CA49 /* Pods-Database_Example_macOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Database_Example_macOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Database_Example_macOS/Pods-Database_Example_macOS.release.xcconfig"; sourceTree = "<group>"; };
@@ -1690,6 +1692,7 @@
DE9314F91E86C6FF0083EDBF /* Tests */ = {
isa = PBXGroup;
children = (
+ 7E94853F1F578A9D005A3939 /* FIRAuthURLPresenterTests.m */,
DE750DB51EB3DD4000A75E47 /* FIRAuthAPNSTokenManagerTests.m */,
DE750DB61EB3DD4000A75E47 /* FIRAuthAPNSTokenTests.m */,
DE750DB71EB3DD4000A75E47 /* FIRAuthAppCredentialManagerTests.m */,
@@ -4187,6 +4190,7 @@
D9B0D4201F578F7200A567C2 /* FIRGetProjectConfigResponseTests.m in Sources */,
DE9315711E86C71C0083EDBF /* FIRSetAccountInfoResponseTests.m in Sources */,
DE93155F1E86C71C0083EDBF /* FIRAuthTests.m in Sources */,
+ 7E9485421F578AC4005A3939 /* FIRAuthURLPresenterTests.m in Sources */,
DE750DBD1EB3DD5B00A75E47 /* FIRAuthAPNSTokenTests.m in Sources */,
DE0E5BBE1EA7D93500FAA825 /* FIRAuthAppDelegateProxyTests.m in Sources */,
DE0E5BBC1EA7D92E00FAA825 /* FIRVerifyClientResponseTests.m in Sources */,
diff --git a/Firebase/Auth/FirebaseAuth.podspec b/Firebase/Auth/FirebaseAuth.podspec
index 3d7eb2f..b213941 100644
--- a/Firebase/Auth/FirebaseAuth.podspec
+++ b/Firebase/Auth/FirebaseAuth.podspec
@@ -31,6 +31,7 @@ Simplify your iOS development, grow your user base, and monetize more effectivel
'Source/**/FIRAuthAPNSTokenType.[mh]',
'Source/**/FIRAuthAPNSToken.[mh]',
'Source/**/FIRPhoneAuthCredential.[mh]',
+ 'Source/**/FIRAuthDefaultUIDelegate.[mh]',
'Source/**/FIRPhoneAuthProvider.[mh]'
s.public_header_files = 'Source/Public/*.h'
s.preserve_paths =
diff --git a/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m b/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m
new file mode 100644
index 0000000..118b73c
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m
@@ -0,0 +1,85 @@
+/*
+ * 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 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.
+ */
+ UIViewController *_viewController;
+}
+
+- (instancetype)initWithViewController:(UIViewController *)viewController {
+ self = [super init];
+ if (self) {
+ _viewController = viewController;
+ }
+ return self;
+}
+
+- (void)presentViewController:(UIViewController *)viewControllerToPresent
+ animated:(BOOL)flag
+ completion:(nullable void (^)(void))completion {
+ [_viewController presentViewController:viewControllerToPresent
+ animated:flag
+ completion:completion];
+}
+
+- (void)dismissViewControllerAnimated:(BOOL)flag completion:(nullable void (^)(void))completion {
+ [_viewController dismissViewControllerAnimated:flag completion:completion];
+}
+
++ (id<FIRAuthUIDelegate>)defaultUIDelegate {
+ UIViewController *topViewController =
+ [UIApplication sharedApplication].keyWindow.rootViewController;
+ while (true){
+ if (topViewController.presentedViewController) {
+ topViewController = topViewController.presentedViewController;
+ } else if ([topViewController isKindOfClass:[UINavigationController class]]) {
+ UINavigationController *nav = (UINavigationController *)topViewController;
+ topViewController = nav.topViewController;
+ } else if ([topViewController isKindOfClass:[UITabBarController class]]) {
+ UITabBarController *tab = (UITabBarController *)topViewController;
+ topViewController = tab.selectedViewController;
+ } else {
+ break;
+ }
+ }
+ return [[FIRAuthDefaultUIDelegate alloc] initWithViewController:topViewController];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthErrorUtils.h b/Firebase/Auth/Source/FIRAuthErrorUtils.h
index c2c9171..4fdfe9b 100644
--- a/Firebase/Auth/Source/FIRAuthErrorUtils.h
+++ b/Firebase/Auth/Source/FIRAuthErrorUtils.h
@@ -451,6 +451,20 @@ NS_ASSUME_NONNULL_BEGIN
*/
+ (NSError *)captchaCheckFailedErrorWithMessage:(nullable NSString *)message;
+/** @fn webContextAlreadyPresentedErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeWebContextAlreadyPresented code.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)webContextAlreadyPresentedErrorWithMessage:(nullable NSString *)message;
+
+/** @fn webContextCancelledErrorWithMessage:
+ @brief Constructs an @c NSError with the @c FIRAuthErrorCodeWebContextCancelledcode.
+ @param message Error message from the backend, if any.
+ @return The NSError instance associated with the given FIRAuthError.
+ */
++ (NSError *)webContextCancelledErrorWithMessage:(nullable NSString *)message;
+
/** @fn keychainErrorWithFunction:status:
@brief Constructs an @c NSError with the @c FIRAuthErrorCodeKeychainError code.
@param keychainFunction The keychain function which was invoked and yielded an unexpected
diff --git a/Firebase/Auth/Source/FIRAuthErrorUtils.m b/Firebase/Auth/Source/FIRAuthErrorUtils.m
index 0439f81..ad9ad55 100644
--- a/Firebase/Auth/Source/FIRAuthErrorUtils.m
+++ b/Firebase/Auth/Source/FIRAuthErrorUtils.m
@@ -345,6 +345,18 @@ static NSString *const kFIRAuthErrorMessageAppNotVerified = @"Firebase could not
static NSString *const kFIRAuthErrorMessageCaptchaCheckFailed = @"The reCAPTCHA response token "
"provided is either invalid, expired or already";
+/** @var kFIRAuthErrorMessageWebContextAlreadyPresented
+ @brief Message for @c FIRAuthErrorCodeWebContextAlreadyPresented error code.
+ */
+static NSString *const kFIRAuthErrorMessageWebContextAlreadyPresented = @"User interaction is "
+ "still ongoing, another view cannot be presented.";
+
+/** @var kFIRAuthErrorMessageWebContextCancelled
+ @brief Message for @c FIRAuthErrorCodeWebContextCancelled error code.
+ */
+static NSString *const kFIRAuthErrorMessageWebContextCancelled = @"The interaction was cancelled "
+ "by the user.";
+
/** @var kFIRAuthErrorMessageInternalError
@brief Message for @c FIRAuthErrorCodeInternalError error code.
*/
@@ -455,6 +467,10 @@ static NSString *FIRAuthErrorDescription(FIRAuthErrorCode code) {
return kFIRAuthErrorMessageAppNotVerified;
case FIRAuthErrorCodeCaptchaCheckFailed:
return kFIRAuthErrorMessageCaptchaCheckFailed;
+ case FIRAuthErrorCodeWebContextAlreadyPresented:
+ return kFIRAuthErrorMessageWebContextAlreadyPresented;
+ case FIRAuthErrorCodeWebContextCancelled:
+ return kFIRAuthErrorMessageWebContextCancelled;
}
}
@@ -562,6 +578,10 @@ static NSString *const FIRAuthErrorCodeString(FIRAuthErrorCode code) {
return @"ERROR_APP_NOT_VERIFIED";
case FIRAuthErrorCodeCaptchaCheckFailed:
return @"ERROR_CAPTCHA_CHECK_FAILED";
+ case FIRAuthErrorCodeWebContextAlreadyPresented:
+ return @"ERROR_WEB_CONTEXT_ALREADY_PRESENTED";
+ case FIRAuthErrorCodeWebContextCancelled:
+ return @"ERROR_WEB_CONTEXT_CANCELLED";
}
}
@@ -873,6 +893,14 @@ static NSString *const FIRAuthErrorCodeString(FIRAuthErrorCode code) {
return [self errorWithCode:FIRAuthInternalErrorCodeCaptchaCheckFailed message:message];
}
++ (NSError *)webContextAlreadyPresentedErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeWebContextAlreadyPresented message:message];
+}
+
++ (NSError *)webContextCancelledErrorWithMessage:(nullable NSString *)message {
+ return [self errorWithCode:FIRAuthInternalErrorCodeWebContextCancelled message:message];
+}
+
+ (NSError *)keychainErrorWithFunction:(NSString *)keychainFunction status:(OSStatus)status {
NSString *failureReason = [NSString stringWithFormat:@"%@ (%li)", keychainFunction, (long)status];
return [self errorWithCode:FIRAuthInternalErrorCodeKeychainError userInfo:@{
diff --git a/Firebase/Auth/Source/FIRAuthInternalErrors.h b/Firebase/Auth/Source/FIRAuthInternalErrors.h
index 724b95c..ffdb068 100644
--- a/Firebase/Auth/Source/FIRAuthInternalErrors.h
+++ b/Firebase/Auth/Source/FIRAuthInternalErrors.h
@@ -322,6 +322,17 @@ typedef NS_ENUM(NSInteger, FIRAuthInternalErrorCode) {
FIRAuthInternalErrorCodeQuotaExceeded =
FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeQuotaExceeded,
+ /** Indicates that an attempt was made to present a new web context while one was already being
+ presented.
+ */
+ FIRAuthInternalErrorCodeWebContextAlreadyPresented =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeWebContextAlreadyPresented,
+
+ /** Indicates that the URL presentation was cancelled prematurely by the user.
+ */
+ FIRAuthInternalErrorCodeWebContextCancelled =
+ FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeWebContextCancelled,
+
// The enum values between 17046 and 17051 are reserved and should NOT be used for new error
// codes.
diff --git a/Firebase/Auth/Source/FIRAuthURLPresenter.h b/Firebase/Auth/Source/FIRAuthURLPresenter.h
new file mode 100644
index 0000000..694b398
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthURLPresenter.h
@@ -0,0 +1,61 @@
+/*
+ * 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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol FIRAuthUIDelegate;
+
+/** @typedef FIRAuthURLPresentationCompletion
+ @brief The type of block invoked when the URLPresentation completes.
+ @param callbackURL The callback URL if the presentation ends with a matching callback.
+ @param error The error if the presentation fails to start or ends with an error.
+ */
+typedef void (^FIRAuthURLPresentationCompletion)(NSURL *_Nullable callbackURL,
+ NSError *_Nullable error);
+
+/** @typedef FIRAuthCallbackMatcher
+ @brief The type of block invoked for checking whether a callback URL matches.
+ @param callbackURL The callback URL to check for match.
+ @return Whether or not the specific callback URL matches or not.
+ */
+typedef BOOL (^FIRAuthURLCallbackMatcher)(NSURL * _Nullable callbackURL);
+
+@interface FIRAuthURLPresenter : NSObject
+
+/** @fn presentURL:UIDelegate:callbackMatcher:completion:
+ @brief Presents an URL to interact with user.
+ @param URL The URL to present.
+ @param UIDelegate The UI delegate to present view controller.
+ @param completion A block to be called either synchronously if the presentation fails to start,
+ or asynchronously in future on an unspecified thread once the presentation finishes.
+ */
+- (void)presentURL:(NSURL *)URL
+ UIDelegate:(nullable id<FIRAuthUIDelegate>)UIDelegate
+ callbackMatcher:(FIRAuthURLCallbackMatcher)callbackMatcher
+ completion:(FIRAuthURLPresentationCompletion)completion;
+
+/** @fn canHandleURL:
+ @brief Determines if a URL was produced by the currently presented URL.
+ @param URL The URL to handle.
+ @return Whether the URL could be handled or not.
+ */
+- (BOOL)canHandleURL:(NSURL *)URL;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/FIRAuthURLPresenter.m b/Firebase/Auth/Source/FIRAuthURLPresenter.m
new file mode 100644
index 0000000..90ceed6
--- /dev/null
+++ b/Firebase/Auth/Source/FIRAuthURLPresenter.m
@@ -0,0 +1,137 @@
+/*
+ * 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 "FIRAuthURLPresenter.h"
+
+#import "FIRAuthErrorUtils.h"
+#import "FIRAuthUIDelegate.h"
+
+@import SafariServices;
+
+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>
+@end
+
+@implementation FIRAuthURLPresenter {
+ /** @var _callbackMatcher
+ @brief The callback URL matcher for the current presentation, if one is active.
+ */
+ FIRAuthURLCallbackMatcher _Nullable _callbackMatcher;
+
+ /** @var _safariViewController
+ @brief The SFSafariViewController used for the current presentation, if any.
+ */
+ SFSafariViewController *_Nullable _safariViewController;
+
+ /** @var _UIDelegate
+ @brief The UIDelegate used to present the SFSafariViewController.
+ */
+ id<FIRAuthUIDelegate> _UIDelegate;
+
+ /** @var _completion
+ @brief The completion handler for the current presentaion, if one is active.
+ @remarks This variable is also used as a flag to indicate a presentation is active.
+ */
+ FIRAuthURLPresentationCompletion _Nullable _completion;
+}
+
+- (void)presentURL:(NSURL *)URL
+ UIDelegate:(nullable id<FIRAuthUIDelegate>)UIDelegate
+ callbackMatcher:(FIRAuthURLCallbackMatcher)callbackMatcher
+ completion:(FIRAuthURLPresentationCompletion)completion {
+ _callbackMatcher = callbackMatcher;
+ _completion = completion;
+ _UIDelegate = UIDelegate ?: [FIRAuthDefaultUIDelegate defaultUIDelegate];
+ [self presentWebContextWithController:_UIDelegate URL:URL];
+}
+
+- (BOOL)canHandleURL:(NSURL *)URL {
+ if (_callbackMatcher(URL)) {
+ _callbackMatcher = nil;
+ [self finishPresentationWithURL:URL error:nil];
+ return YES;
+ }
+ return NO;
+}
+
+/** @fn presentWebContextWithController:URL:
+ @brief Presents a SFSafariViewController or WKWebView to display the contents of the URL
+ provided.
+ @param controller The controller used to present the SFSafariViewController or WKWebView.
+ @param URL The URL to display in the SFSafariViewController or WKWebView.
+ */
+- (void)presentWebContextWithController:(id)controller URL:(NSURL *)URL {
+ if ([SFSafariViewController class]) {
+ if (_safariViewController) {
+ // Unable to start a new presentation on top of modal SFSVC presentation.
+ _completion(nil, [FIRAuthErrorUtils webContextAlreadyPresentedErrorWithMessage:nil]);
+ return;
+ }
+ SFSafariViewController *safariViewController = [[SFSafariViewController alloc] initWithURL:URL];
+ _safariViewController = safariViewController;
+ _safariViewController.delegate = self;
+ [controller presentViewController:safariViewController animated:YES completion:nil];
+ return;
+ }
+}
+
+#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]];
+ }
+}
+
+#pragma mark - Private methods
+
+/** @fn finishPresentationWithURL:error:
+ @brief Finishes the presentation for a given URL, if any.
+ @param URL The URL to finish presenting.
+ @param error The error with which to finish presenting, if any.
+ */
+- (void)finishPresentationWithURL:(nullable NSURL *)URL
+ error:(nullable NSError *)error {
+ FIRAuthURLPresentationCompletion completion = _completion;
+ void (^finishBlock)() = ^() {
+ completion(URL, nil);
+ };
+ _completion = nil;
+ if ([SFSafariViewController class]) {
+ SFSafariViewController *safariViewController = _safariViewController;
+ _safariViewController = nil;
+ if (safariViewController) {
+ [_UIDelegate dismissViewControllerAnimated:YES completion:finishBlock];
+ }
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Auth/Source/Public/FIRAuthErrors.h b/Firebase/Auth/Source/Public/FIRAuthErrors.h
index 6ab6900..2b7ef96 100644
--- a/Firebase/Auth/Source/Public/FIRAuthErrors.h
+++ b/Firebase/Auth/Source/Public/FIRAuthErrors.h
@@ -276,6 +276,15 @@ typedef NS_ENUM(NSInteger, FIRAuthErrorCode) {
*/
FIRAuthErrorCodeCaptchaCheckFailed = 17056,
+ /** Indicates that an attempt was made to present a new web context while one was already being
+ presented.
+ */
+ FIRAuthErrorCodeWebContextAlreadyPresented = 17057,
+
+ /** Indicates that the URL presentation was cancelled prematurely by the user.
+ */
+ FIRAuthErrorCodeWebContextCancelled = 17058,
+
/** Indicates an error occurred while attempting to access the keychain.
*/
FIRAuthErrorCodeKeychainError = 17995,
diff --git a/Firebase/Auth/Source/Public/FIRAuthUIDelegate.h b/Firebase/Auth/Source/Public/FIRAuthUIDelegate.h
new file mode 100644
index 0000000..7f9adb0
--- /dev/null
+++ b/Firebase/Auth/Source/Public/FIRAuthUIDelegate.h
@@ -0,0 +1,51 @@
+/*
+ * 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 <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @protocol FIRAuthUIDelegate
+ @brief A protocol to handle user interface interactions for Firebase Auth.
+*/
+
+@protocol FIRAuthUIDelegate <NSObject>
+
+/** @fn presentViewController:animated:completion:
+ @brief If implemented, this method will be invoked when Firebase Auth needs to display a view
+ controller.
+ @param viewControllerToPresent The view controller to be presented.
+ @param flag Decides whether the view controller presentation should be animated or not.
+ @param completion The block to execute after the presentation finishes. This block has no return
+ value and takes no parameters.
+*/
+- (void)presentViewController:(UIViewController *)viewControllerToPresent
+ animated:(BOOL)flag
+ completion:(void (^ _Nullable)(void))completion;
+
+/** @fn dismissViewControllerAnimated:completion:
+ @brief If implemented, this method will be invoked when Firebase Auth needs to display a view
+ controller.
+ @param flag Decides whether removing the view controller should be animated or not.
+ @param completion The block to execute after the presentation finishes. This block has no return
+ value and takes no parameters.
+*/
+- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^ _Nullable)(void))completion;
+
+@end
+
+NS_ASSUME_NONNULL_END