aboutsummaryrefslogtreecommitdiffhomepage
path: root/Example/Auth
diff options
context:
space:
mode:
authorGravatar Paul Beusterien <paulbeusterien@google.com>2017-09-19 11:18:04 -0700
committerGravatar GitHub <noreply@github.com>2017-09-19 11:18:04 -0700
commit9447e72cab40c9ea59e49a726d2890bcf356d38a (patch)
treedc25e027769cc1fbe6bd6e0ae0d877858e0780e7 /Example/Auth
parenta34d091971d05ef8e2625074157eb9ff6dda3cbd (diff)
Consolidate AuthSamples into main Firebase Xcode project (#288)
Diffstat (limited to 'Example/Auth')
-rw-r--r--Example/Auth/ApiTests/AuthCredentialsTemplate.h64
-rw-r--r--Example/Auth/ApiTests/FirebaseAuthApiTests.m540
-rw-r--r--Example/Auth/ApiTests/Info.plist22
-rw-r--r--Example/Auth/EarlGreyTests/FirebaseAuthEarlGreyTests.m193
-rw-r--r--Example/Auth/EarlGreyTests/Info.plist22
-rw-r--r--Example/Auth/README.md93
-rw-r--r--Example/Auth/Sample/AppManager.h80
-rw-r--r--Example/Auth/Sample/AppManager.m135
-rw-r--r--Example/Auth/Sample/ApplicationDelegate.h48
-rw-r--r--Example/Auth/Sample/ApplicationDelegate.m91
-rw-r--r--Example/Auth/Sample/ApplicationTemplate.plist88
-rw-r--r--Example/Auth/Sample/AuthCredentialsTemplate.h33
-rw-r--r--Example/Auth/Sample/AuthProviders.h74
-rw-r--r--Example/Auth/Sample/AuthProviders.m40
-rw-r--r--Example/Auth/Sample/CustomTokenDataEntryViewController.h55
-rw-r--r--Example/Auth/Sample/CustomTokenDataEntryViewController.m148
-rw-r--r--Example/Auth/Sample/FacebookAuthProvider.h29
-rw-r--r--Example/Auth/Sample/FacebookAuthProvider.m79
-rw-r--r--Example/Auth/Sample/GoogleAuthProvider.h29
-rw-r--r--Example/Auth/Sample/GoogleAuthProvider.m130
-rw-r--r--Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/Contents.json197
-rw-r--r--Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_21in29dp-1.pngbin0 -> 169 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_21in29dp.pngbin0 -> 169 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_29in40dp-1.pngbin0 -> 194 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_38in50dp.pngbin0 -> 222 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_42in57dp.pngbin0 -> 251 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_53in72dp.pngbin0 -> 303 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_56in76dp.pngbin0 -> 315 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_21in29dp-1.pngbin0 -> 248 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_21in29dp.pngbin0 -> 248 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_29in40dp-1.pngbin0 -> 317 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_29in40dp-2.pngbin0 -> 317 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_38in50dp.pngbin0 -> 390 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_42in57dp.pngbin0 -> 424 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_44in60dp.pngbin0 -> 438 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_53in72dp.pngbin0 -> 540 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_56in76dp.pngbin0 -> 573 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_21in29dp.pngbin0 -> 330 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_29in40dp-1.pngbin0 -> 433 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_44in60dp.pngbin0 -> 661 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/Contents.json6
-rw-r--r--Example/Auth/Sample/Images.xcassets/close.imageset/Contents.json23
-rw-r--r--Example/Auth/Sample/Images.xcassets/close.imageset/ic_clear_black_1x_ios_24dp.pngbin0 -> 164 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/close.imageset/ic_clear_black_2x_ios_24dp.pngbin0 -> 235 bytes
-rw-r--r--Example/Auth/Sample/Images.xcassets/close.imageset/ic_clear_black_3x_ios_24dp.pngbin0 -> 309 bytes
-rw-r--r--Example/Auth/Sample/MainViewController.h92
-rw-r--r--Example/Auth/Sample/MainViewController.m2858
-rw-r--r--Example/Auth/Sample/MainViewController.xib330
-rw-r--r--Example/Auth/Sample/SampleTemplate.entitlements14
-rw-r--r--Example/Auth/Sample/SettingsViewController.h37
-rw-r--r--Example/Auth/Sample/SettingsViewController.m376
-rw-r--r--Example/Auth/Sample/SettingsViewController.xib55
-rw-r--r--Example/Auth/Sample/StaticContentTableViewManager.h255
-rw-r--r--Example/Auth/Sample/StaticContentTableViewManager.m231
-rw-r--r--Example/Auth/Sample/UIViewController+Alerts.h91
-rw-r--r--Example/Auth/Sample/UIViewController+Alerts.m346
-rw-r--r--Example/Auth/Sample/UserInfoViewController.h55
-rw-r--r--Example/Auth/Sample/UserInfoViewController.m79
-rw-r--r--Example/Auth/Sample/UserInfoViewController.xib55
-rw-r--r--Example/Auth/Sample/UserTableViewCell.h58
-rw-r--r--Example/Auth/Sample/UserTableViewCell.m55
-rw-r--r--Example/Auth/Sample/en.lproj/Localizable.strings2
-rw-r--r--Example/Auth/Sample/es-MX.lproj/Localizable.strings2
-rw-r--r--Example/Auth/Sample/fr-FR.lproj/Localizable.strings2
-rw-r--r--Example/Auth/Sample/main.m23
-rw-r--r--Example/Auth/Sample/ru-RU.lproj/Localizable.strings2
-rw-r--r--Example/Auth/Sample/zh-Hans.lproj/Localizable.strings2
-rw-r--r--Example/Auth/SwiftSample/AppDelegate.swift62
-rw-r--r--Example/Auth/SwiftSample/AuthCredentialsTemplate.swift42
-rw-r--r--Example/Auth/SwiftSample/InfoTemplate.plist79
-rw-r--r--Example/Auth/SwiftSample/LaunchScreen.storyboard27
-rw-r--r--Example/Auth/SwiftSample/Main.storyboard227
-rw-r--r--Example/Auth/SwiftSample/Sample.entitlements10
-rw-r--r--Example/Auth/SwiftSample/Stubs.swift45
-rw-r--r--Example/Auth/SwiftSample/ViewController.swift700
75 files changed, 8431 insertions, 0 deletions
diff --git a/Example/Auth/ApiTests/AuthCredentialsTemplate.h b/Example/Auth/ApiTests/AuthCredentialsTemplate.h
new file mode 100644
index 0000000..a8bf379
--- /dev/null
+++ b/Example/Auth/ApiTests/AuthCredentialsTemplate.h
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+
+/*
+Some of the Auth Credentials needs to be populated for the ApiTests to work.
+
+Please follow the following steps to populate the valid AuthCredentials
+and copy it to AuthCredentials.h file
+
+You will need to replace the following values:
+
+$KGOOGLE_CLIENT_ID
+Get the value of the CLIENT_ID key in the GoogleService-Info.plist file..
+
+$KGOOGLE_TEST_ACCOUNT_REFRESH_TOKEN
+GOOGLE_TEST_ACCOUNT_REFRESH_TOKEN is the Google SignIn refresh token obtained for the Google client
+ID, saved for continuous tests.
+
+$KGOOGLE_USER_NAME
+The name of the test user for Google SignIn.
+
+$KFACEBOOK_APP_ID
+FACEBOOK_APP_ID is the developer's Facebook app's ID, to be used to test the
+'Signing in with Facebook' feature of Firebase Auth. Follow the instructions
+on the Facebook developer site: https://developers.facebook.com/docs/apps/register
+to obtain such an id.
+
+$KFACEBOOK_APP_ACCESS_TOKEN
+Once you have an Facebook App Id, click on dashboard from your app you can see
+both your App ID and the App Secret. Once you have both of these generate the
+access token using the step 13 of https://smashballoon.com/custom-facebook-feed/access-token/
+Follow the same link for comprehensive information on how to get the access token.
+
+$KFACEBOOK_USER_NAME
+The name of the test user for Facebook Login.
+
+$KCUSTOM_AUTH_TOKEN_URL
+A URL to return a Custom Auth token.
+
+$KCUSTOM_AUTH_USER_ID
+The ID of the test user in the Custom Auth token.
+*/
+
+#define KGOOGLE_CLIENT_ID $KGOOGLE_CLIENT_ID
+#define KGOOGLE_TEST_ACCOUNT_REFRESH_TOKEN $KGOOGLE_TEST_ACCOUNT_REFRESH_TOKEN
+#define KGOOGLE_USER_NAME $KGOOGLE_USER_NAME
+#define KFACEBOOK_APP_ID $KFACEBOOK_APP_ID
+#define KFACEBOOK_APP_ACCESS_TOKEN $KFACEBOOK_APP_ACCESS_TOKEN
+#define KFACEBOOK_USER_NAME $KFACEBOOK_USER_NAME
+#define KCUSTOM_AUTH_TOKEN_URL $KCUSTOM_AUTH_TOKEN_URL
+#define KCUSTOM_AUTH_USER_ID $KCUSTOM_AUTH_USER_ID
diff --git a/Example/Auth/ApiTests/FirebaseAuthApiTests.m b/Example/Auth/ApiTests/FirebaseAuthApiTests.m
new file mode 100644
index 0000000..7aa0b73
--- /dev/null
+++ b/Example/Auth/ApiTests/FirebaseAuthApiTests.m
@@ -0,0 +1,540 @@
+/*
+ * 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 <XCTest/XCTest.h>
+
+#import "FIRApp.h"
+#import "FirebaseAuth.h"
+#import "AuthCredentials.h"
+
+#ifdef NO_NETWORK
+#import "ITUIOSTestUtil.h"
+#import "ioReplayer/IORManager.h"
+#import "ioReplayer/IORTestCase.h"
+#endif
+
+#import <GTMSessionFetcher/GTMSessionFetcher.h>
+#import <GTMSessionFetcher/GTMSessionFetcherService.h>
+
+/** The user name string for Custom Auth testing account. */
+static NSString *const kCustomAuthTestingAccountUserID = KCUSTOM_AUTH_USER_ID;
+
+/** The url for obtaining a valid custom token string used to test Custom Auth. */
+static NSString *const kCustomTokenUrl = KCUSTOM_AUTH_TOKEN_URL;
+
+/** Facebook app access token that will be used for Facebook Graph API, which is different from
+ * account access token.
+ */
+static NSString *const kFacebookAppAccessToken = KFACEBOOK_APP_ACCESS_TOKEN;
+
+/** Facebook app ID that will be used for Facebook Graph API. */
+static NSString *const kFacebookAppID = KFACEBOOK_APP_ID;
+
+static NSString *const kFacebookGraphApiAuthority = @"graph.facebook.com";
+
+static NSString *const kFacebookTestAccountName = KFACEBOOK_USER_NAME;
+
+static NSString *const kGoogleTestAccountName = KGOOGLE_USER_NAME;
+
+/** The invalid custom token string for testing Custom Auth. */
+static NSString *const kInvalidCustomToken = @"invalid token.";
+
+/** The testing email address for testCreateAccountWithEmailAndPassword. */
+static NSString *const kTestingEmailToCreateUser = @"abc@xyz.com";
+
+/** The testing email address for testSignInExistingUserWithEmailAndPassword. */
+static NSString *const kExistingTestingEmailToSignIn = @"456@abc.com";
+
+/** Error message for invalid custom token sign in. */
+NSString *kInvalidTokenErrorMessage =
+ @"The custom token format is incorrect. Please check the documentation.";
+
+NSString *kGoogleCliendId = KGOOGLE_CLIENT_ID;
+
+/** Refresh token of Google test account to exchange for access token. Refresh token never expires
+ * unless user revokes it. If this refresh token expires, tests in record mode will fail and this
+ * token needs to be updated.
+ */
+NSString *kGoogleTestAccountRefreshToken = KGOOGLE_TEST_ACCOUNT_REFRESH_TOKEN;
+
+static NSTimeInterval const kExpectationsTimeout = 30;
+
+#ifdef NO_NETWORK
+#define SKIP_IF_ON_MOBILE_HARNESS \
+ if ([ITUIOSTestUtil isOnMobileHarness]) { \
+ NSLog(@"Skipping '%@' on mobile harness", NSStringFromSelector(_cmd)); \
+ return; \
+ }
+#else
+#define SKIP_IF_ON_MOBILE_HARNESS
+#endif
+
+#ifdef NO_NETWORK
+@interface ApiTests : IORTestCase
+#else
+@interface ApiTests : XCTestCase
+#endif
+@end
+
+@implementation ApiTests
+
+/** To reset the app so that each test sees the app in a clean state. */
+- (void)setUp {
+ [super setUp];
+ [self signOut];
+}
+
+#pragma mark - Tests
+
+/**
+ * This test runs in replay mode by default. To run in a different mode follow the instructions
+ * below.
+ *
+ * Blaze: --test_arg=\'--networkReplayMode=(replay|record|disabled|observe)\'
+ *
+ * Xcode:
+ * Update the following flag in the xcscheme.
+ * --networkReplayMode=(replay|record|disabled|observe)
+ */
+- (void)testCreateAccountWithEmailAndPassword {
+ SKIP_IF_ON_MOBILE_HARNESS
+ FIRAuth *auth = [FIRAuth auth];
+ if (!auth) {
+ XCTFail(@"Could not obtain auth object.");
+ }
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Created account with email and password."];
+ [auth createUserWithEmail:kTestingEmailToCreateUser
+ password:@"password"
+ completion:^(FIRUser *user, NSError *error) {
+ if (error) {
+ NSLog(@"createUserWithEmail has error: %@", error);
+ }
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationsTimeout
+ handler:^(NSError *error) {
+ if (error != nil) {
+ XCTFail(@"Failed to wait for expectations "
+ @"in creating account. Error: %@",
+ error.localizedDescription);
+ }
+ }];
+
+ XCTAssertEqualObjects(auth.currentUser.email, kTestingEmailToCreateUser);
+
+ // Clean up the created Firebase user for future runs.
+ [self deleteCurrentFirebaseUser];
+}
+
+- (void)testLinkAnonymousAccountToFacebookAccount {
+ FIRAuth *auth = [FIRAuth auth];
+ if (!auth) {
+ XCTFail(@"Could not obtain auth object.");
+ }
+ [self signInAnonymously];
+
+ NSDictionary *userInfoDict = [self createFacebookTestingAccount];
+ NSString *facebookAccessToken = userInfoDict[@"access_token"];
+ NSLog(@"Facebook testing account access token is: %@", facebookAccessToken);
+ NSString *facebookAccountId = userInfoDict[@"id"];
+ NSLog(@"Facebook testing account id is: %@", facebookAccountId);
+
+ FIRAuthCredential *credential =
+ [FIRFacebookAuthProvider credentialWithAccessToken:facebookAccessToken];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Facebook linking finished."];
+ [auth.currentUser linkWithCredential:credential
+ completion:^(FIRUser *user, NSError *error) {
+ if (error) {
+ NSLog(@"Link to Facebok error: %@", error);
+ }
+ [expectation fulfill];
+ }];
+
+ [self waitForExpectationsWithTimeout:kExpectationsTimeout
+ handler:^(NSError *error) {
+ if (error != nil) {
+ XCTFail(@"Failed to wait for expectations "
+ @"in linking to Facebook. Error: %@",
+ error.localizedDescription);
+ }
+ }];
+ NSArray<id<FIRUserInfo>> *providerData = auth.currentUser.providerData;
+ XCTAssertEqual([providerData count], 1);
+ XCTAssertEqualObjects([providerData[0] providerID], @"facebook.com");
+
+ // Clean up the created Firebase/Facebook user for future runs.
+ [self deleteCurrentFirebaseUser];
+ [self deleteFacebookTestingAccountbyId:facebookAccountId];
+}
+
+- (void)testSignInAnonymously {
+ [self signInAnonymously];
+ XCTAssertTrue([FIRAuth auth].currentUser.anonymous);
+ [self deleteCurrentFirebaseUser];
+}
+
+- (void)testSignInExistingUserWithEmailAndPassword {
+ FIRAuth *auth = [FIRAuth auth];
+ if (!auth) {
+ XCTFail(@"Could not obtain auth object.");
+ }
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Signed in existing account with email and password."];
+ [auth signInWithEmail:kExistingTestingEmailToSignIn
+ password:@"password"
+ completion:^(FIRUser *user, NSError *error) {
+ if (error) {
+ NSLog(@"Signing in existing account has error: %@", error);
+ }
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationsTimeout
+ handler:^(NSError *error) {
+ if (error != nil) {
+ XCTFail(@"Failed to wait for expectations "
+ @"in signing in existing account. Error: %@",
+ error.localizedDescription);
+ }
+ }];
+
+ XCTAssertEqualObjects(auth.currentUser.email, kExistingTestingEmailToSignIn);
+}
+
+- (void)testSignInWithValidCustomAuthToken {
+ FIRAuth *auth = [FIRAuth auth];
+ if (!auth) {
+ XCTFail(@"Could not obtain auth object.");
+ }
+
+ NSError *error;
+ NSString *customToken = [NSString stringWithContentsOfURL:[NSURL URLWithString:kCustomTokenUrl]
+ encoding:NSUTF8StringEncoding
+ error:&error];
+ if (!customToken) {
+ XCTFail(@"There was an error retrieving the custom token: %@", error);
+ }
+ NSLog(@"The valid token is: %@", customToken);
+
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"CustomAuthToken sign-in finished."];
+
+ [auth signInWithCustomToken:customToken
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (error) {
+ NSLog(@"Valid token sign in error: %@", error);
+ }
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationsTimeout
+ handler:^(NSError *error) {
+ if (error != nil) {
+ XCTFail(@"Failed to wait for expectations "
+ @"in CustomAuthToken sign in. Error: %@",
+ error.localizedDescription);
+ }
+ }];
+
+ XCTAssertEqualObjects(auth.currentUser.uid, kCustomAuthTestingAccountUserID);
+}
+
+- (void)testSignInWithInvalidCustomAuthToken {
+ FIRAuth *auth = [FIRAuth auth];
+ if (!auth) {
+ XCTFail(@"Could not obtain auth object.");
+ }
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Invalid CustomAuthToken sign-in finished."];
+
+ [auth signInWithCustomToken:kInvalidCustomToken
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+
+ XCTAssertEqualObjects(error.localizedDescription, kInvalidTokenErrorMessage);
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationsTimeout
+ handler:^(NSError *error) {
+ if (error != nil) {
+ XCTFail(@"Failed to wait for expectations "
+ @"in CustomAuthToken sign in. Error: %@",
+ error.localizedDescription);
+ }
+ }];
+}
+
+- (void)testSignInWithFaceboook {
+ FIRAuth *auth = [FIRAuth auth];
+ if (!auth) {
+ XCTFail(@"Could not obtain auth object.");
+ }
+
+ NSDictionary *userInfoDict = [self createFacebookTestingAccount];
+ NSString *facebookAccessToken = userInfoDict[@"access_token"];
+ NSLog(@"Facebook testing account access token is: %@", facebookAccessToken);
+ NSString *facebookAccountId = userInfoDict[@"id"];
+ NSLog(@"Facebook testing account id is: %@", facebookAccountId);
+
+ FIRAuthCredential *credential =
+ [FIRFacebookAuthProvider credentialWithAccessToken:facebookAccessToken];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Facebook sign-in finished."];
+
+ [auth signInWithCredential:credential
+ completion:^(FIRUser *user, NSError *error) {
+ if (error) {
+ NSLog(@"Facebook sign in error: %@", error);
+ }
+ [expectation fulfill];
+ }];
+
+ [self waitForExpectationsWithTimeout:kExpectationsTimeout
+ handler:^(NSError *error) {
+ if (error != nil) {
+ XCTFail(@"Failed to wait for expectations "
+ @"in Facebook sign in. Error: %@",
+ error.localizedDescription);
+ }
+ }];
+ XCTAssertEqualObjects(auth.currentUser.displayName, kFacebookTestAccountName);
+
+ // Clean up the created Firebase/Facebook user for future runs.
+ [self deleteCurrentFirebaseUser];
+ [self deleteFacebookTestingAccountbyId:facebookAccountId];
+}
+
+- (void)testSignInWithGoogle {
+ FIRAuth *auth = [FIRAuth auth];
+ if (!auth) {
+ XCTFail(@"Could not obtain auth object.");
+ }
+ NSDictionary *userInfoDict = [self getGoogleAccessToken];
+ NSString *googleAccessToken = userInfoDict[@"access_token"];
+ NSString *googleIdToken = userInfoDict[@"id_token"];
+ FIRAuthCredential *credential =
+ [FIRGoogleAuthProvider credentialWithIDToken:googleIdToken accessToken:googleAccessToken];
+
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Signing in with Google finished."];
+ [auth signInWithCredential:credential
+ completion:^(FIRUser *user, NSError *error) {
+ if (error) {
+ NSLog(@"Signing in with Google had error: %@", error);
+ }
+ [expectation fulfill];
+ }];
+
+ [self waitForExpectationsWithTimeout:kExpectationsTimeout
+ handler:^(NSError *error) {
+ if (error != nil) {
+ XCTFail(@"Failed to wait for expectations "
+ @"in Signing in with Google. Error: %@",
+ error.localizedDescription);
+ }
+ }];
+ XCTAssertEqualObjects(auth.currentUser.displayName, kGoogleTestAccountName);
+
+ // Clean up the created Firebase/Facebook user for future runs.
+ [self deleteCurrentFirebaseUser];
+}
+
+#pragma mark - Helpers
+
+/** Sign out current account. */
+- (void)signOut {
+ NSError *signOutError;
+ BOOL status = [[FIRAuth auth] signOut:&signOutError];
+
+ // Just log the error because we don't want to fail the test if signing out
+ // fails.
+ if (!status) {
+ NSLog(@"Error signing out: %@", signOutError);
+ }
+}
+
+/** Creates a Facebook testing account using Facebook Graph API and return a dictionary that
+ * constains "id", "access_token", "login_url", "email" and "password" of the created account.
+ */
+- (NSDictionary *)createFacebookTestingAccount {
+ // Build the URL.
+ NSString *urltoCreateTestUser =
+ [NSString stringWithFormat:@"https://%@/%@/accounts/test-users", kFacebookGraphApiAuthority,
+ kFacebookAppID];
+ // Build the POST request.
+ NSString *bodyString =
+ [NSString stringWithFormat:@"installed=true&name=%@&permissions=read_stream&access_token=%@",
+ kFacebookTestAccountName, kFacebookAppAccessToken];
+ NSData *postData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
+ GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init];
+ GTMSessionFetcher *fetcher = [service fetcherWithURLString:urltoCreateTestUser];
+ fetcher.bodyData = postData;
+ [fetcher setRequestValue:@"text/plain" forHTTPHeaderField:@"Content-Type"];
+
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Creating Facebook account finished."];
+ __block NSData *data = nil;
+ [fetcher beginFetchWithCompletionHandler:^(NSData *receivedData, NSError *error) {
+ if (error) {
+ NSLog(@"Creating Facebook account finished with error: %@", error);
+ return;
+ }
+ data = receivedData;
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationsTimeout
+ handler:^(NSError *error) {
+ if (error != nil) {
+ XCTFail(@"Failed to wait for expectations "
+ @"in creating Facebook account. Error: %@",
+ error.localizedDescription);
+ }
+ }];
+ NSString *userInfo = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ NSLog(@"The info of created Facebook testing account is: %@", userInfo);
+ // Parses the access token from the JSON data.
+ NSDictionary *userInfoDict =
+ [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
+ return userInfoDict;
+}
+
+/** Clean up the created user for tests' future runs. */
+- (void)deleteCurrentFirebaseUser {
+ FIRAuth *auth = [FIRAuth auth];
+ if (!auth) {
+ NSLog(@"Could not obtain auth object.");
+ }
+
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Delete current user finished."];
+ [auth.currentUser deleteWithCompletion:^(NSError *_Nullable error) {
+ if (error) {
+ XCTFail(@"Failed to delete user. Error: %@.", error);
+ }
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationsTimeout
+ handler:^(NSError *error) {
+ if (error != nil) {
+ XCTFail(@"Failed to wait for expectations "
+ @"in deleting user. Error: %@",
+ error.localizedDescription);
+ }
+ }];
+}
+
+- (void)signInAnonymously {
+ FIRAuth *auth = [FIRAuth auth];
+ if (!auth) {
+ XCTFail(@"Could not obtain auth object.");
+ }
+
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Anonymousy sign-in finished."];
+ [auth signInAnonymouslyWithCompletion:^(FIRUser *user, NSError *error) {
+ if (error) {
+ NSLog(@"Anonymousy sign in error: %@", error);
+ }
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationsTimeout
+ handler:^(NSError *error) {
+ if (error != nil) {
+ XCTFail(@"Failed to wait for expectations "
+ @"in anonymousy sign in. Error: %@",
+ error.localizedDescription);
+ }
+ }];
+}
+
+/** Delete a Facebook testing account by account Id using Facebook Graph API. */
+- (void)deleteFacebookTestingAccountbyId:(NSString *)accountId {
+ // Build the URL.
+ NSString *urltoDeleteTestUser =
+ [NSString stringWithFormat:@"https://%@/%@", kFacebookGraphApiAuthority, accountId];
+
+ // Build the POST request.
+ NSString *bodyString =
+ [NSString stringWithFormat:@"method=delete&access_token=%@", kFacebookAppAccessToken];
+ NSData *postData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
+ GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init];
+ GTMSessionFetcher *fetcher = [service fetcherWithURLString:urltoDeleteTestUser];
+ fetcher.bodyData = postData;
+ [fetcher setRequestValue:@"text/plain" forHTTPHeaderField:@"Content-Type"];
+
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Deleting Facebook account finished."];
+ [fetcher beginFetchWithCompletionHandler:^(NSData *receivedData, NSError *error) {
+ NSString *deleteResult =
+ [[NSString alloc] initWithData:receivedData encoding:NSUTF8StringEncoding];
+ NSLog(@"The result of deleting Facebook account is: %@", deleteResult);
+ if (error) {
+ NSLog(@"Deleting Facebook account finished with error: %@", error);
+ }
+ [expectation fulfill];
+ }];
+
+ [self waitForExpectationsWithTimeout:kExpectationsTimeout
+ handler:^(NSError *error) {
+ if (error != nil) {
+ XCTFail(@"Failed to wait for expectations "
+ @"in deleting Facebook account. Error: %@",
+ error.localizedDescription);
+ }
+ }];
+}
+
+/** Sends http request to Google OAuth2 token server to use refresh token to exchange for Google
+ * access token. Returns a dictionary that constains "access_token", "token_type", "expires_in" and
+ * "id_token".
+ */
+- (NSDictionary *)getGoogleAccessToken {
+ NSString *googleOauth2TokenServerUrl = @"https://www.googleapis.com/oauth2/v4/token";
+ NSString *bodyString =
+ [NSString stringWithFormat:@"client_id=%@&grant_type=refresh_token&refresh_token=%@",
+ kGoogleCliendId, kGoogleTestAccountRefreshToken];
+ NSData *postData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
+ GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init];
+ GTMSessionFetcher *fetcher = [service fetcherWithURLString:googleOauth2TokenServerUrl];
+ fetcher.bodyData = postData;
+ [fetcher setRequestValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
+
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Exchanging Google account tokens finished."];
+ __block NSData *data = nil;
+ [fetcher beginFetchWithCompletionHandler:^(NSData *receivedData, NSError *error) {
+ if (error) {
+ NSLog(@"Exchanging Google account tokens finished with error: %@", error);
+ return;
+ }
+ data = receivedData;
+ [expectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kExpectationsTimeout
+ handler:^(NSError *error) {
+ if (error != nil) {
+ XCTFail(@"Failed to wait for expectations "
+ @"in exchanging Google account tokens. Error: %@",
+ error.localizedDescription);
+ }
+ }];
+ NSString *userInfo = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ NSLog(@"The info of exchanged result is: %@", userInfo);
+ NSDictionary *userInfoDict =
+ [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
+ return userInfoDict;
+}
+@end
diff --git a/Example/Auth/ApiTests/Info.plist b/Example/Auth/ApiTests/Info.plist
new file mode 100644
index 0000000..6c6c23c
--- /dev/null
+++ b/Example/Auth/ApiTests/Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>BNDL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/Example/Auth/EarlGreyTests/FirebaseAuthEarlGreyTests.m b/Example/Auth/EarlGreyTests/FirebaseAuthEarlGreyTests.m
new file mode 100644
index 0000000..7f07e2a
--- /dev/null
+++ b/Example/Auth/EarlGreyTests/FirebaseAuthEarlGreyTests.m
@@ -0,0 +1,193 @@
+/*
+ * 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 <EarlGrey/EarlGrey.h>
+#import <Foundation/Foundation.h>
+#import <XCTest/XCTest.h>
+
+#import "FIRApp.h"
+#import "FirebaseAuth.h"
+
+#ifdef NO_NETWORK
+#import "ioReplayer/IORTestCase.h"
+#endif
+
+/** The url for obtaining a valid custom token string used to test BYOAuth. */
+static NSString *const kCustomTokenUrl = @"https://fb-sa-1211.appspot.com/token";
+
+/** The invalid custom token string for testing BYOAuth. */
+static NSString *const kInvalidCustomToken = @"invalid token.";
+
+/** The user name string for BYOAuth testing account. */
+static NSString *const kTestingAccountUserID = @"BYU_Test_User_ID";
+
+static CGFloat const kShortScrollDistance = 100;
+
+static NSTimeInterval const kWaitForElementTimeOut = 5;
+
+#ifdef NO_NETWORK
+@interface BasicUITest : IORTestCase
+#else
+@interface BasicUITest :XCTestCase
+#endif
+@end
+
+/** Convenience function for EarlGrey tests. */
+id<GREYMatcher> grey_scrollView(void) {
+ return [GREYMatchers matcherForKindOfClass:[UIScrollView class]];
+}
+
+@implementation BasicUITest
+
+/** To reset the app so that each test sees the app in a clean state. */
+- (void)setUp {
+ [super setUp];
+
+ [self signOut];
+
+ [[EarlGrey selectElementWithMatcher:grey_allOf(grey_scrollView(),
+ grey_kindOfClass([UITableView class]), nil)]
+ performAction:grey_scrollToContentEdge(kGREYContentEdgeTop)];
+}
+
+#pragma mark - Tests
+
+/**
+ * This test runs in replay mode by default. To run in a different mode
+ * follow the instructions below.
+ *
+ * Blaze:
+ * --test_arg=\'--networkReplayMode=(replay|record|disabled|observe)\'
+ *
+ * Xcode:
+ * Update the following flag in the xcscheme.
+ * --networkReplayMode=(replay|record|disabled|observe)
+ */
+- (void)testSignInExistingUser {
+ NSString *email = @"123@abc.com";
+ [[[EarlGrey selectElementWithMatcher:grey_allOf(grey_text(@"Sign in with Email/Password"),
+ grey_sufficientlyVisible(), nil)]
+ usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, kShortScrollDistance)
+ onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]),
+ nil)] performAction:grey_tap()];
+
+ id<GREYMatcher> comfirmationButtonMatcher =
+ grey_allOf(grey_kindOfClass([UILabel class]), grey_accessibilityLabel(@"OK"), nil);
+
+ [[EarlGrey selectElementWithMatcher:
+ #warning TODO Add accessibilityIdentifiers for the elements.
+ grey_kindOfClass(NSClassFromString(@"_UIAlertControllerView"))]
+ performAction:grey_typeText(email)];
+
+ [[EarlGrey selectElementWithMatcher:comfirmationButtonMatcher] performAction:grey_tap()];
+
+ [[EarlGrey
+ selectElementWithMatcher:grey_kindOfClass(NSClassFromString(@"_UIAlertControllerView"))]
+ performAction:grey_typeText(@"password")];
+
+ [[EarlGrey selectElementWithMatcher:comfirmationButtonMatcher] performAction:grey_tap()];
+
+ [[[EarlGrey
+ selectElementWithMatcher:grey_allOf(grey_text(email), grey_sufficientlyVisible(), nil)]
+ usingSearchAction:grey_scrollInDirection(kGREYDirectionUp, kShortScrollDistance)
+ onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]),
+ nil)] assertWithMatcher:grey_sufficientlyVisible()];
+}
+
+/** Test sign in with a valid BYOAuth token retrived from a remote server. */
+- (void)testSignInWithValidBYOAuthToken {
+ NSError *error;
+ NSString *customToken = [NSString stringWithContentsOfURL:[NSURL URLWithString:kCustomTokenUrl]
+ encoding:NSUTF8StringEncoding
+ error:&error];
+ if (!customToken) {
+ GREYFail(@"There was an error retrieving the custom token: %@", error);
+ }
+
+ [[[EarlGrey selectElementWithMatcher:grey_allOf(grey_text(@"Sign In (BYOAuth)"),
+ grey_sufficientlyVisible(), nil)]
+ usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, kShortScrollDistance)
+ onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]),
+ nil)] performAction:grey_tap()];
+
+ [[[EarlGrey selectElementWithMatcher:grey_kindOfClass([UITextView class])]
+ performAction:grey_replaceText(customToken)] assertWithMatcher:grey_text(customToken)];
+
+ [[EarlGrey selectElementWithMatcher:grey_text(@"Done")] performAction:grey_tap()];
+
+ [self waitForElementWithText:@"OK" withDelay:kWaitForElementTimeOut];
+
+ [[EarlGrey selectElementWithMatcher:grey_text(@"OK")] performAction:grey_tap()];
+
+ [[[EarlGrey
+ selectElementWithMatcher:grey_allOf(grey_text(kTestingAccountUserID),
+ grey_sufficientlyVisible(), nil)]
+ usingSearchAction:grey_scrollInDirection(kGREYDirectionUp,
+ kShortScrollDistance)
+ onElementWithMatcher:grey_allOf(grey_scrollView(),
+ grey_kindOfClass([UITableView class]),
+ nil)]
+ assertWithMatcher:grey_sufficientlyVisible()];
+}
+
+- (void)testSignInWithInvalidBYOAuthToken {
+ [[[EarlGrey selectElementWithMatcher:grey_allOf(grey_text(@"Sign In (BYOAuth)"),
+ grey_sufficientlyVisible(), nil)]
+ usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, kShortScrollDistance)
+ onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]),
+ nil)] performAction:grey_tap()];
+
+ [[[EarlGrey selectElementWithMatcher:grey_kindOfClass([UITextView class])]
+ performAction:grey_replaceText(kInvalidCustomToken)]
+ assertWithMatcher:grey_text(kInvalidCustomToken)];
+
+ [[EarlGrey selectElementWithMatcher:grey_text(@"Done")] performAction:grey_tap()];
+
+ NSString *invalidTokenErrorMessage =
+ @"The custom token format is incorrect. Please check the documentation.";
+
+ [self waitForElementWithText:invalidTokenErrorMessage withDelay:kWaitForElementTimeOut];
+
+ [[EarlGrey selectElementWithMatcher:grey_text(@"OK")] performAction:grey_tap()];
+}
+
+#pragma mark - Helpers
+
+/** Sign out current account. */
+- (void)signOut {
+ NSError *signOutError;
+ BOOL status = [[FIRAuth auth] signOut:&signOutError];
+
+ // Just log the error because we don't want to fail the test if signing out fails.
+ if (!status) {
+ NSLog(@"Error signing out: %@", signOutError);
+ }
+}
+
+/** Wait for an element with text to appear. */
+- (void)waitForElementWithText:(NSString *)text withDelay:(NSTimeInterval)maxDelay {
+ GREYCondition *displayed =
+ [GREYCondition conditionWithName:@"Wait for element"
+ block:^BOOL {
+ NSError *error = nil;
+ [[EarlGrey selectElementWithMatcher:grey_text(text)]
+ assertWithMatcher:grey_sufficientlyVisible()
+ error:&error];
+ return !error;
+ }];
+ GREYAssertTrue([displayed waitWithTimeout:maxDelay], @"Failed to wait for element '%@'.", text);
+}
+@end
diff --git a/Example/Auth/EarlGreyTests/Info.plist b/Example/Auth/EarlGreyTests/Info.plist
new file mode 100644
index 0000000..6c6c23c
--- /dev/null
+++ b/Example/Auth/EarlGreyTests/Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>BNDL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/Example/Auth/README.md b/Example/Auth/README.md
new file mode 100644
index 0000000..499ad00
--- /dev/null
+++ b/Example/Auth/README.md
@@ -0,0 +1,93 @@
+### Running Sample Application
+
+In order to run this application, you'll need to follow the following steps!
+
+#### GoogleService-Info.plist files
+
+You'll need valid `GoogleService-Info.plist` files for those samples. To get your own
+`GoogleService-Info.plist` files:
+1. Go to the [Firebase Console](https://console.firebase.google.com/)
+2. Create a new Firebase project, if you don't already have one
+3. For each sample app you want to test, create a new Firebase app with the sample app's bundle
+identifier (e.g. `com.google.FirebaseExperimental1.dev`)
+4. Download the resulting `GoogleService-Info.plist` and place it in
+[Sample/GoogleService-Info.plist](Sample/GoogleService-Info.plist)
+
+#### GoogleService-Info_multi.plist files
+
+This feature is for advanced testing.
+1. The developer would need to get a GoogleService-Info.plist from a different iOS client (which
+can be in a different Firebase project)
+2. Save this plist file as GoogleService-Info_multi.plist in
+[Sample/GoogleService-Info_multi.plist](Sample/GoogleService-Info_multi.plist).
+This enables testing that FirebaseAuth continues to work after switching the Firebase App in the
+runtime.
+
+#### Application.plist file
+
+Please follow the instructions in
+[Sample/ApplicationTemplate.plist](Sample/ApplicationTemplate.plist)
+to generate the right Application.plist file.
+
+### Sample.entitlements file
+
+In order to test the "Reset Password In App" feature you will need to create a dynamic link for your
+Firebase project in the Dynamic Links section of the Firebase Console. Once the link is created,
+please copy the contents of
+[Sample/SampleTemplate.entitlements](Sample/SampleTemplate.entitlements)
+into a file named `Sample/Sample.entitlements` and replace `$KAPP_LINKS_DOMAIN` with your own
+relevant appLinks domain. Your appLinks domains are domains that your app will handle as universal
+links, in this particular case you can obtain this domain from the aforementioned Dynamic Links
+section of the Firebase Console.
+
+#### Getting your own Credential files
+
+Please follow the instructions in
+[Sample/AuthCredentialsTemplate.h](Sample/AuthCredentialsTemplate.h)
+to generate the AuthCredentials.h file.
+
+
+### Running SwiftSample Application
+
+In order to run this application, you'll need to follow the following steps!
+
+#### GoogleService-Info.plist files
+
+You'll need valid `GoogleService-Info.plist` files for those samples. To get your own
+`GoogleService-Info.plist` files:
+1. Go to the [Firebase Console](https://console.firebase.google.com/)
+2. Create a new Firebase project, if you don't already have one
+3. For each sample app you want to test, create a new Firebase app with the sample app's bundle
+identifier (e.g. `com.google.FirebaseExperimental2.dev`)
+4. Download the resulting `GoogleService-Info.plist` and place it in
+[SwiftSample/GoogleService-Info.plist](SwiftSample/GoogleService-Info.plist)
+
+#### Info.plist file
+
+Please follow the instructions in
+[SwiftSample/InfoTemplate.plist](SwiftSample/InfoTemplate.plist)
+to generate the right Info.plist file
+
+#### Getting your own Credential files
+
+Please follow the instructions in
+[SwiftSample/AuthCredentialsTemplate.swift](SwiftSample/AuthCredentialsTemplate.swift)
+to generate the AuthCredentials.swift file.
+
+### Running API tests
+
+In order to run the API tests, you'll need to follow the following steps!
+
+#### Getting your own Credential files
+
+Please follow the instructions in
+[ApiTests/AuthCredentialsTemplate.h](ApiTests/AuthCredentialsTemplate.h)
+to generate the AuthCredentials.h file.
+
+## Usage
+
+```
+$ pod update
+$ open Firebase.xcworkspace
+```
+Then select an Auth scheme and run.
diff --git a/Example/Auth/Sample/AppManager.h b/Example/Auth/Sample/AppManager.h
new file mode 100644
index 0000000..e8e53a7
--- /dev/null
+++ b/Example/Auth/Sample/AppManager.h
@@ -0,0 +1,80 @@
+/*
+ * 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>
+
+@class FIRApp;
+@class FIRAuth;
+@class FIROptions;
+@class FIRPhoneAuthProvider;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class AppManager
+ @brief A manager of global FIRApp instances.
+ */
+@interface AppManager : NSObject
+
+/** @property count
+ @brief The total count of apps under management, including the default app.
+ */
+@property(nonatomic, assign, readonly) int count;
+
+/** @property active
+ @brief The index of the currently active app, 0 being the default app.
+ */
+@property(nonatomic, assign) int active;
+
+/** @fn appAtIndex:
+ @brief Retrieves the app at the given index.
+ @param index The index of the app to be retrieved, 0 being the default app.
+ @return The app at the given index.
+ */
+- (nullable FIRApp *)appAtIndex:(int)index;
+
+/** @fn recreateAppAtIndex:withOptions:completion:
+ @brief Deletes the app at the given index, and optionally creates it again with given options.
+ @param index The index of the app to be recreated, 0 being the default app.
+ @param options Optionally, the new options with which app should be created.
+ @param completion The block to call when completes.
+ */
+- (void)recreateAppAtIndex:(int)index
+ withOptions:(nullable FIROptions *)options
+ completion:(void (^)())completion;
+
+/** @fn sharedInstance
+ @brief Gets a shared instance of the class.
+ */
++ (instancetype)sharedInstance;
+
+/** @fn app
+ @brief A shortcut to get the currently active app.
+ */
++ (FIRApp *)app;
+
+/** @fn auth
+ @brief A shortcut to get the auth instance for the currently active app.
+ */
++ (FIRAuth *)auth;
+
+/** @fn phoneAuthProvider
+ @brief A shortcut to get the phone auth provider for the currently active app.
+ */
++ (FIRPhoneAuthProvider *)phoneAuthProvider;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Auth/Sample/AppManager.m b/Example/Auth/Sample/AppManager.m
new file mode 100644
index 0000000..757642e
--- /dev/null
+++ b/Example/Auth/Sample/AppManager.m
@@ -0,0 +1,135 @@
+/*
+ * 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 "AppManager.h"
+
+#import "FIRApp.h"
+#import "FIRPhoneAuthProvider.h"
+#import "FirebaseAuth.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+// Declares a private method of FIRInstanceID to work around a bug.
+@interface FIRInstanceID : NSObject
++ (void)notifyTokenRefresh;
+@end
+
+@implementation AppManager {
+ /** @var _createdAppNames
+ @brief The set of names of live (created but not deleted) app, to avoid iCore warnings.
+ */
+ NSMutableSet<NSString *> *_liveAppNames;
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _count = 2;
+ _liveAppNames = [[NSMutableSet<NSString *> alloc] initWithCapacity:_count - 1];
+ }
+ return self;
+}
+
+- (nullable FIRApp *)appAtIndex:(int)index {
+ if (index == 0) {
+ return [FIRApp defaultApp];
+ }
+ NSString *name = [self appNameWithIndex:index];
+ if ([_liveAppNames containsObject:name]) {
+ return [FIRApp appNamed:[self appNameWithIndex:index]];
+ }
+ return nil;
+}
+
+- (void)recreateAppAtIndex:(int)index
+ withOptions:(nullable FIROptions *)options
+ completion:(void (^)())completion {
+ [self deleteAppAtIndex:index completion:^() {
+ if (index == 0) {
+ [FIRInstanceID notifyTokenRefresh]; // b/28967043
+ if (options) {
+ [FIRApp configureWithOptions:options];
+ }
+ } else {
+ NSString *name = [self appNameWithIndex:index];
+ if (options) {
+ [FIRApp configureWithName:name options:options];
+ [_liveAppNames addObject:name];
+ } else {
+ [_liveAppNames removeObject:name];
+ }
+ }
+ completion();
+ }];
+}
+
++ (instancetype)sharedInstance {
+ static dispatch_once_t onceToken;
+ static AppManager *sharedInstance;
+ dispatch_once(&onceToken, ^{
+ sharedInstance = [[self alloc] init];
+ });
+ return sharedInstance;
+}
+
++ (FIRApp *)app {
+ AppManager *manager = [self sharedInstance];
+ return [manager appAtIndex:manager.active];
+}
+
++ (FIRAuth *)auth {
+ return [FIRAuth authWithApp:[self app]];
+}
+
++ (FIRPhoneAuthProvider *)phoneAuthProvider {
+ return [FIRPhoneAuthProvider providerWithAuth:[self auth]];
+}
+
+#pragma mark - Helpers
+
+/** @fn appNameWithIndex:
+ @brief Gets the app name for the given index.
+ @param index The index of the app managed by this instance.
+ @returns The app name for the FIRApp instance.
+ */
+- (NSString *)appNameWithIndex:(int)index {
+ return [NSString stringWithFormat:@"APP_%02d", index];
+}
+
+/** @fn deleteAppAtIndex:withOptions:completion:
+ @brief Deletes the app at the given index.
+ @param index The index of the app to be deleted, 0 being the default app.
+ @param completion The block to call when completes.
+ */
+- (void)deleteAppAtIndex:(int)index
+ completion:(void (^)())completion {
+ FIRApp *app = [self appAtIndex:index];
+ if (app) {
+ [app deleteApp:^(BOOL success) {
+ if (success) {
+ completion();
+ } else {
+ NSLog(@"Failed to delete app '%@'.", app.name);
+ }
+ }];
+ } else {
+ completion();
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Auth/Sample/ApplicationDelegate.h b/Example/Auth/Sample/ApplicationDelegate.h
new file mode 100644
index 0000000..110348d
--- /dev/null
+++ b/Example/Auth/Sample/ApplicationDelegate.h
@@ -0,0 +1,48 @@
+/*
+ * 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
+
+@protocol OpenURLDelegate <NSObject>
+
+/** @fn handleOpenURL:sourceApplication:
+ @brief Handles application:openURL:... methods for @c UIApplicationDelegate .
+ */
+- (BOOL)handleOpenURL:(NSURL *)url sourceApplication:(nullable NSString *)sourceApplication;
+
+@end
+
+/** @class ApplicationDelegate
+ @brief The sample application's delegate.
+ */
+@interface ApplicationDelegate : UIResponder <UIApplicationDelegate>
+
+/** @property window
+ @brief The sample application's @c UIWindow.
+ */
+@property(strong, nonatomic) UIWindow *window;
+
+/** @fn setOpenURLDelegate:
+ @brief Sets the delegate to handle application:openURL:... methods.
+ @param openURLDelegate The delegate which is not retained by this method.
+ */
++ (void)setOpenURLDelegate:(nullable id<OpenURLDelegate>)openURLDelegate;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Auth/Sample/ApplicationDelegate.m b/Example/Auth/Sample/ApplicationDelegate.m
new file mode 100644
index 0000000..08e1a7e
--- /dev/null
+++ b/Example/Auth/Sample/ApplicationDelegate.m
@@ -0,0 +1,91 @@
+/*
+ * 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 "ApplicationDelegate.h"
+
+#import "AuthProviders.h"
+#import "FirebaseCommunity/FIRApp.h"
+#import "FirebaseAuth.h"
+#import "FirebaseCommunity/FIRLogger.h"
+#import "GTMSessionFetcherLogging.h"
+#import "MainViewController.h"
+
+/** @var gOpenURLDelegate
+ @brief The delegate to for application:openURL:... method.
+ */
+static __weak id<OpenURLDelegate> gOpenURLDelegate;
+
+@implementation ApplicationDelegate {
+ // The main view controller of the sample app.
+ MainViewController *_sampleAppMainViewController;
+}
+
++ (void)setOpenURLDelegate:(nullable id<OpenURLDelegate>)openURLDelegate {
+ gOpenURLDelegate = openURLDelegate;
+}
+
+- (BOOL)application:(UIApplication *)application
+ didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+ [GTMSessionFetcher setLoggingEnabled:YES];
+ FIRSetLoggerLevel(FIRLoggerLevelInfo);
+
+ // Configure the default Firebase application:
+ [FIRApp configure];
+
+ // Load and present the UI:
+ UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
+ _sampleAppMainViewController =
+ [[MainViewController alloc] initWithNibName:NSStringFromClass([MainViewController class])
+ bundle:nil];
+ window.rootViewController = _sampleAppMainViewController;
+ self.window = window;
+ [self.window makeKeyAndVisible];
+
+ return YES;
+}
+
+- (BOOL)application:(nonnull UIApplication *)application
+ openURL:(nonnull NSURL *)url
+ options:(nonnull NSDictionary<NSString *, id> *)options {
+ return [self application:application
+ openURL:url
+ sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey]
+ annotation:options[UIApplicationOpenURLOptionsAnnotationKey]];
+}
+
+- (BOOL)application:(UIApplication *)application
+ openURL:(NSURL *)url
+ sourceApplication:(NSString *)sourceApplication
+ annotation:(id)annotation {
+ if ([gOpenURLDelegate handleOpenURL:url sourceApplication:sourceApplication]) {
+ return YES;
+ }
+ if ([_sampleAppMainViewController handleIncomingLinkWithURL:url]) {
+ return YES;
+ }
+ return NO;
+}
+
+- (BOOL)application:(UIApplication *)application
+ continueUserActivity:(NSUserActivity *)userActivity
+ restorationHandler:(void (^)(NSArray *))restorationHandler {
+ if (userActivity.webpageURL) {
+ return [_sampleAppMainViewController handleIncomingLinkWithURL:userActivity.webpageURL];
+ }
+ return NO;
+}
+
+@end
diff --git a/Example/Auth/Sample/ApplicationTemplate.plist b/Example/Auth/Sample/ApplicationTemplate.plist
new file mode 100644
index 0000000..c7eaf55
--- /dev/null
+++ b/Example/Auth/Sample/ApplicationTemplate.plist
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>armv7</string>
+ </array>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>UIBackgroundModes</key>
+ <array>
+ <string>remote-notification</string>
+ </array>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>CFBundleDisplayName</key>
+ <string>FirebaseAuth Sample</string>
+ <key>LSApplicationQueriesSchemes</key>
+ <array>
+ <string>fbauth2</string>
+ </array>
+ <key>CFBundleURLTypes</key>
+ <array>
+ <dict>
+ <key>CFBundleURLName</key>
+ <string>$REVERSE_CLIENT_ID</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>$REVERSE_CLIENT_ID</string>
+ </array>
+ <key>CFBundleTypeRole</key>
+ <string>Editor</string>
+ </dict>
+ <dict>
+ <key>CFBundleURLName</key>
+ <string>$REVERSE_CLIENT_MULTI_ID</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>$REVERSE_CLIENT_MULTI_ID</string>
+ </array>
+ <key>CFBundleTypeRole</key>
+ <string>Editor</string>
+ </dict>
+ <dict>
+ <key>CFBundleURLName</key>
+ <string>$BUNDLE_ID</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>$BUNDLE_ID</string>
+ </array>
+ <key>CFBundleTypeRole</key>
+ <string>Editor</string>
+ </dict>
+ </array>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+</dict>
+</plist>
diff --git a/Example/Auth/Sample/AuthCredentialsTemplate.h b/Example/Auth/Sample/AuthCredentialsTemplate.h
new file mode 100644
index 0000000..d0278b1
--- /dev/null
+++ b/Example/Auth/Sample/AuthCredentialsTemplate.h
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+/*
+Some of the Auth Credentials needs to be populated for the Sample build to work.
+
+Please follow the following steps to populate the valid AuthCredentials
+and copy it to AuthCredentials.h file
+
+You will need to replace the following values:
+
+$KFACEBOOK_APP_ID
+FACEBOOK_APP_ID is the developer's Facebook app's ID, to be used to test the
+'Signing in with Facebook' feature of Firebase Auth. Follow the instructions
+on the Facebook developer site: https://developers.facebook.com/docs/apps/register
+to obtain such an id.
+*/
+
+#define KFACEBOOK_APP_ID $KFACEBOOK_APP_ID
+#define KCONTINUE_URL $KCONTINUE_URL
diff --git a/Example/Auth/Sample/AuthProviders.h b/Example/Auth/Sample/AuthProviders.h
new file mode 100644
index 0000000..eccbad9
--- /dev/null
+++ b/Example/Auth/Sample/AuthProviders.h
@@ -0,0 +1,74 @@
+/*
+ * 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 FIRAuthCredential;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef AuthCredentialCallback
+ @brief The type of block invoked when a @c FIRAuthCredential object is ready or an error has
+ occurred.
+ @param credential The auth credential if any.
+ @param error The error which occurred, if any.
+ */
+typedef void (^AuthCredentialCallback)(FIRAuthCredential *_Nullable credential,
+ NSError *_Nullable error);
+/** @protocol AuthProvider
+ @brief A common interface for auth providers to be used by the sample app.
+ */
+@protocol AuthProvider <NSObject>
+
+/** @fn getAuthCredentialWithPresentingViewController:callback:
+ @brief Gets a @c FIRAuthCredential instance for use with Firebase headless API by signing in.
+ @param viewController The view controller to present the UI.
+ @param callback A block which is invoked when the sign-in flow finishes. Invoked asynchronously
+ on an unspecified thread in the future.
+ */
+- (void)getAuthCredentialWithPresentingViewController:(UIViewController *)viewController
+ callback:(AuthCredentialCallback)callback;
+
+/** @fn signOut
+ @brief Logs out the current provider session, which invalidates any cached crendential.
+ */
+- (void)signOut;
+
+@end
+
+/** @class AuthProviders
+ @brief Namespace for @c AuthProvider instances.
+ */
+@interface AuthProviders : NSObject
+
+/** @fn google
+ @brief Returns a Google auth provider.
+ */
++ (id<AuthProvider>)google;
+
+/** @fn facebook
+ @brief Returns a Facebook auth provider.
+ */
++ (id<AuthProvider>)facebook;
+
+/** @fn init
+ @brief This class is not supposed to be instantiated.
+ */
+- (nullable instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Auth/Sample/AuthProviders.m b/Example/Auth/Sample/AuthProviders.m
new file mode 100644
index 0000000..825935f
--- /dev/null
+++ b/Example/Auth/Sample/AuthProviders.m
@@ -0,0 +1,40 @@
+/*
+ * 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 "AuthProviders.h"
+
+#import "FacebookAuthProvider.h"
+#import "GoogleAuthProvider.h"
+
+@implementation AuthProviders
+
++ (id<AuthProvider>)google {
+ static id<AuthProvider> googleAuthProvider;
+ if (!googleAuthProvider) {
+ googleAuthProvider = [[GoogleAuthProvider alloc] init];
+ }
+ return googleAuthProvider;
+}
+
++ (id<AuthProvider>)facebook {
+ static id<AuthProvider> facebookAuthProvider;
+ if (!facebookAuthProvider) {
+ facebookAuthProvider = [[FacebookAuthProvider alloc] init];
+ }
+ return facebookAuthProvider;
+}
+
+@end
diff --git a/Example/Auth/Sample/CustomTokenDataEntryViewController.h b/Example/Auth/Sample/CustomTokenDataEntryViewController.h
new file mode 100644
index 0000000..e783bc7
--- /dev/null
+++ b/Example/Auth/Sample/CustomTokenDataEntryViewController.h
@@ -0,0 +1,55 @@
+/*
+ * 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
+
+/** @typedef CustomTokenDataEntryViewControllerCompletion
+ @brief The type of callback block invoked when a @c CustomTokenDataEntryViewController is
+ dismissed (by either being cancelled or completed by the user.)
+ @param cancelled Indicates the user cancelled the flow and didn't want to enter a token.
+ @param userEnteredTokenText The token text the user entered.
+ */
+typedef void (^CustomTokenDataEntryViewControllerCompletion)
+ (BOOL cancelled, NSString *_Nullable userEnteredTokenText);
+
+/** @class CustomTokenDataEntryViewController
+ @brief Simple view controller to allow data entry of custom BYOAuth tokens.
+ */
+@interface CustomTokenDataEntryViewController : UIViewController
+
+/** @fn initWithNibName:bundle:
+ @brief Please use initWithCompletion:
+ */
+- (instancetype)initWithNibName:(NSString *_Nullable)nibNameOrNil
+ bundle:(NSBundle *_Nullable)nibBundleOrNil NS_UNAVAILABLE;
+
+/** @fn initWithCoder:
+ @brief Please use initWithCompletion:
+ */
+- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE;
+
+/** @fn initWithCompletion:
+ @brief Designated initializer.
+ @param completion A block which will be invoked when the user either chooses "cancel" or "done".
+ */
+- (nullable instancetype)initWithCompletion:
+ (CustomTokenDataEntryViewControllerCompletion)completion NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Auth/Sample/CustomTokenDataEntryViewController.m b/Example/Auth/Sample/CustomTokenDataEntryViewController.m
new file mode 100644
index 0000000..b65c244
--- /dev/null
+++ b/Example/Auth/Sample/CustomTokenDataEntryViewController.m
@@ -0,0 +1,148 @@
+/*
+ * 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 "CustomTokenDataEntryViewController.h"
+
+/** @var navigationBarDefaultHeight
+ @brief The default height to use for new navigation bars' frames.
+ */
+static const NSUInteger navigationBarDefaultHeight = 55;
+
+/** @var navigationBarSystemHeight
+ @brief Set after a navigation bar has been created to obtain the system-specified height of the
+ navigation bar.
+ */
+static NSUInteger navigationBarSystemHeight = navigationBarDefaultHeight;
+
+/** @var kTitle
+ @brief The title of the view controller as it appears at the top of the screen in the navigation
+ bar.
+ */
+static NSString *const kTitle = @"Enter Token";
+
+/** @var kCancel
+ @brief The text for the "Cancel" button.
+ */
+static NSString *const kCancel = @"Cancel";
+
+/** @var kDone
+ @brief The text for the "Done" button.
+ */
+static NSString *const kDone = @"Done";
+
+@implementation CustomTokenDataEntryViewController {
+ /** @var _completion
+ @brief The block we will call when the user presses the "cancel" or "done" buttons.
+ @remarks Passed into the initializer.
+ */
+ CustomTokenDataEntryViewControllerCompletion _completion;
+
+ /** @var _tokenTextView
+ @brief The text view allowing the user to enter their custom token text.
+ @remarks Constructed and set in the method: @c loadTextView.
+ */
+ __weak UITextView *_Nullable _tokenTextView;
+}
+
+- (nullable instancetype)initWithCompletion:
+ (CustomTokenDataEntryViewControllerCompletion)completion {
+ self = [super initWithNibName:nil bundle:nil];
+ if (self) {
+ _completion = completion;
+ }
+ return self;
+}
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ [self loadHeader];
+ [self loadTextView];
+}
+
+#pragma mark - View
+
+/** @fn loadHeader
+ @brief Loads the header bar along the top of the view with "Cancel" and "Done" buttons, as well
+ as a brief title asking the user to enter the custom token text.
+ @remarks Updates navigationBarSystemHeight, so should be called before any method which depends
+ on that variable being updated (like the @c loadTextView method, which uses the value to
+ determine how much room is left on the screen.)
+ */
+- (void)loadHeader {
+ CGRect navBarFrame = CGRectMake(0, 0, self.view.bounds.size.width, navigationBarDefaultHeight);
+ UINavigationBar *navBar = [[UINavigationBar alloc] initWithFrame:navBarFrame];
+ navBar.autoresizingMask = UIViewAutoresizingFlexibleWidth
+ | UIViewAutoresizingFlexibleBottomMargin;
+
+ UINavigationItem *navItem = [[UINavigationItem alloc] initWithTitle:kTitle];
+ UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc] initWithTitle:kCancel
+ style:UIBarButtonItemStylePlain
+ target:self
+ action:@selector(cancelPressed:)];
+ UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithTitle:kDone
+ style:UIBarButtonItemStylePlain
+ target:self
+ action:@selector(donePressed:)];
+ navItem.leftBarButtonItem = cancelButton;
+ navItem.rightBarButtonItem = doneButton;
+
+ [navBar setItems:@[ navItem ] animated:NO];
+
+ [self.view addSubview:navBar];
+
+ // Obtain the system-specified height of the navigation bar.
+ navigationBarSystemHeight = navBar.frame.size.height;
+}
+
+/** @fn loadTextView
+ @brief Loads the text field for the user to enter their custom token text.
+ @remarks Relies on the navigationBarSystemHeight variable being correct.
+ */
+- (void)loadTextView {
+ CGRect tokenTextViewFrame =
+ CGRectMake(0,
+ navigationBarSystemHeight,
+ self.view.bounds.size.width,
+ self.view.bounds.size.height - navigationBarSystemHeight);
+ UITextView *tokenTextView = [[UITextView alloc] initWithFrame:tokenTextViewFrame];
+ tokenTextView.backgroundColor = [UIColor whiteColor];
+ tokenTextView.textAlignment = NSTextAlignmentLeft;
+
+ [self.view addSubview:tokenTextView];
+ _tokenTextView = tokenTextView;
+}
+
+#pragma mark - Actions
+
+- (void)cancelPressed:(id)sender {
+ [self finishByCancelling:YES withUserEnteredTokenText:nil];
+}
+
+- (void)donePressed:(id)sender {
+ [self finishByCancelling:NO withUserEnteredTokenText:_tokenTextView.text];
+}
+
+#pragma mark - Workflow
+
+- (void)finishByCancelling:(BOOL)cancelled
+ withUserEnteredTokenText:(nullable NSString *)userEnteredTokenText {
+ [self dismissViewControllerAnimated:YES completion:^{
+ _completion(cancelled,
+ cancelled ? nil : userEnteredTokenText);
+ }];
+}
+
+@end
diff --git a/Example/Auth/Sample/FacebookAuthProvider.h b/Example/Auth/Sample/FacebookAuthProvider.h
new file mode 100644
index 0000000..6c2edaf
--- /dev/null
+++ b/Example/Auth/Sample/FacebookAuthProvider.h
@@ -0,0 +1,29 @@
+/*
+ * 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>
+
+#import "AuthProviders.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class FacebookAuthProvider
+ @brief The implementation for Facebook auth provider related methods.
+ */
+@interface FacebookAuthProvider : NSObject <AuthProvider>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Auth/Sample/FacebookAuthProvider.m b/Example/Auth/Sample/FacebookAuthProvider.m
new file mode 100644
index 0000000..0260536
--- /dev/null
+++ b/Example/Auth/Sample/FacebookAuthProvider.m
@@ -0,0 +1,79 @@
+/*
+ * 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 "FacebookAuthProvider.h"
+
+#import <FBSDKCoreKit/FBSDKCoreKit.h>
+#import <FBSDKLoginKit/FBSDKLoginKit.h>
+
+#import "FIRFacebookAuthProvider.h"
+#import "ApplicationDelegate.h"
+#import "AuthCredentials.h"
+
+/** @var kFacebookAppId
+ @brief The App ID for the Facebook SDK.
+ */
+static NSString *const kFacebookAppID = KFACEBOOK_APP_ID;
+
+@interface FacebookAuthProvider () <OpenURLDelegate>
+@end
+
+@implementation FacebookAuthProvider {
+ FBSDKLoginManager *_loginManager;
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _loginManager = [[FBSDKLoginManager alloc] init];
+ }
+ return self;
+}
+
+- (void)getAuthCredentialWithPresentingViewController:(UIViewController *)viewController
+ callback:(AuthCredentialCallback)callback {
+ [self signOut];
+
+ [ApplicationDelegate setOpenURLDelegate:self];
+ [FBSDKSettings setAppID:kFacebookAppID];
+ [_loginManager logInWithReadPermissions:@[ @"email" ]
+ fromViewController:viewController
+ handler:^(FBSDKLoginManagerLoginResult *result, NSError *error) {
+ [ApplicationDelegate setOpenURLDelegate:nil];
+ if (!error && result.isCancelled) {
+ error = [NSError errorWithDomain:@"com.google.FirebaseAuthSample" code:-1 userInfo:nil];
+ }
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ NSString *accessToken = [FBSDKAccessToken currentAccessToken].tokenString;
+ callback([FIRFacebookAuthProvider credentialWithAccessToken:accessToken], nil);
+ }];
+}
+
+- (void)signOut {
+ [_loginManager logOut];
+}
+
+- (BOOL)handleOpenURL:(NSURL *)URL sourceApplication:(NSString *)sourceApplication {
+ return [[FBSDKApplicationDelegate sharedInstance] application:[UIApplication sharedApplication]
+ openURL:URL
+ sourceApplication:sourceApplication
+ annotation:nil];
+}
+
+@end
diff --git a/Example/Auth/Sample/GoogleAuthProvider.h b/Example/Auth/Sample/GoogleAuthProvider.h
new file mode 100644
index 0000000..50679a6
--- /dev/null
+++ b/Example/Auth/Sample/GoogleAuthProvider.h
@@ -0,0 +1,29 @@
+/*
+ * 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>
+
+#import "AuthProviders.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @class GoogleAuthProvider
+ @brief The implementation for Google auth provider related methods.
+ */
+@interface GoogleAuthProvider : NSObject <AuthProvider>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Auth/Sample/GoogleAuthProvider.m b/Example/Auth/Sample/GoogleAuthProvider.m
new file mode 100644
index 0000000..e63ade0
--- /dev/null
+++ b/Example/Auth/Sample/GoogleAuthProvider.m
@@ -0,0 +1,130 @@
+/*
+ * 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 "GoogleAuthProvider.h"
+
+#import <GoogleSignIn/GoogleSignIn.h>
+
+#import "AppManager.h"
+#import "FIRApp.h"
+#import "FIROptions.h"
+#import "FIRGoogleAuthProvider.h"
+#import "ApplicationDelegate.h"
+
+/** @typedef GoogleSignInCallback
+ @brief The type of block invoked when a @c GIDGoogleUser object is ready or an error has
+ occurred.
+ @param user The Google user if any.
+ @param error The error which occurred, if any.
+ */
+typedef void (^GoogleSignInCallback)(GIDGoogleUser *user, NSError *error);
+
+/** @class GoogleAuthDelegate
+ @brief The designated delegate class for Google Sign-In.
+ */
+@interface GoogleAuthDelegate : NSObject <GIDSignInDelegate, GIDSignInUIDelegate, OpenURLDelegate>
+
+/** @fn initWithPresentingViewController:callback:
+ @brief Initializes the new instance with the callback.
+ @param presentingViewController The view controller to present the UI.
+ @param callback A block which is invoked when the sign-in flow finishes. Invoked asynchronously
+ on an unspecified thread in the future.
+ */
+- (instancetype)initWithPresentingViewController:(UIViewController *)presentingViewController
+ callback:(nullable GoogleSignInCallback)callback;
+
+@end
+
+@implementation GoogleAuthDelegate {
+ UIViewController *_presentingViewController;
+ GoogleSignInCallback _callback;
+}
+
+- (instancetype)initWithPresentingViewController:(UIViewController *)presentingViewController
+ callback:(nullable GoogleSignInCallback)callback {
+ self = [super init];
+ if (self) {
+ _presentingViewController = presentingViewController;
+ _callback = callback;
+ }
+ return self;
+}
+
+- (void)signIn:(GIDSignIn *)signIn
+ didSignInForUser:(GIDGoogleUser *)user
+ withError:(NSError *)error {
+ GoogleSignInCallback callback = _callback;
+ _callback = nil;
+ if (callback) {
+ callback(user, error);
+ }
+}
+
+- (void)signIn:(GIDSignIn *)signIn presentViewController:(UIViewController *)viewController {
+ [_presentingViewController presentViewController:viewController animated:YES completion:nil];
+}
+
+- (void)signIn:(GIDSignIn *)signIn dismissViewController:(UIViewController *)viewController {
+ [_presentingViewController dismissViewControllerAnimated:YES completion:nil];
+}
+
+- (BOOL)handleOpenURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication {
+ return [[GIDSignIn sharedInstance] handleURL:url
+ sourceApplication:sourceApplication
+ annotation:nil];
+}
+
+@end
+
+@implementation GoogleAuthProvider
+
+- (void)getAuthCredentialWithPresentingViewController:(UIViewController *)viewController
+ callback:(AuthCredentialCallback)callback {
+ [self signOut];
+
+ // The delegate needs to be retained.
+ __block GoogleAuthDelegate *delegate = [[GoogleAuthDelegate alloc]
+ initWithPresentingViewController:viewController
+ callback:^(GIDGoogleUser *user, NSError *error) {
+ [ApplicationDelegate setOpenURLDelegate:nil];
+ delegate = nil;
+ if (error) {
+ callback(nil, error);
+ return;
+ }
+ GIDAuthentication *auth = user.authentication;
+ FIRAuthCredential *credential = [FIRGoogleAuthProvider credentialWithIDToken:auth.idToken
+ accessToken:auth.accessToken];
+ callback(credential, error);
+ }];
+ GIDSignIn *signIn = [GIDSignIn sharedInstance];
+ signIn.clientID = [self googleClientID];
+ signIn.shouldFetchBasicProfile = YES;
+ signIn.delegate = delegate;
+ signIn.uiDelegate = delegate;
+ [ApplicationDelegate setOpenURLDelegate:delegate];
+ [signIn signIn];
+}
+
+- (void)signOut {
+ [[GIDSignIn sharedInstance] signOut];
+}
+
+- (NSString *)googleClientID {
+ return [AppManager app].options.clientID;
+}
+
+@end
diff --git a/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/Contents.json b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..328f185
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,197 @@
+{
+ "images" : [
+ {
+ "idiom" : "iphone",
+ "size" : "20x20",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "20x20",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "logo_avatar_square_grey_color_1x_ios_21in29dp-1.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "logo_avatar_square_grey_color_2x_ios_21in29dp-1.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "logo_avatar_square_grey_color_3x_ios_21in29dp.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "logo_avatar_square_grey_color_2x_ios_29in40dp-1.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "logo_avatar_square_grey_color_3x_ios_29in40dp-1.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "57x57",
+ "idiom" : "iphone",
+ "filename" : "logo_avatar_square_grey_color_1x_ios_42in57dp.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "57x57",
+ "idiom" : "iphone",
+ "filename" : "logo_avatar_square_grey_color_2x_ios_42in57dp.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "logo_avatar_square_grey_color_2x_ios_44in60dp.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "logo_avatar_square_grey_color_3x_ios_44in60dp.png",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "20x20",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "20x20",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "logo_avatar_square_grey_color_1x_ios_21in29dp.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "logo_avatar_square_grey_color_2x_ios_21in29dp.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "logo_avatar_square_grey_color_1x_ios_29in40dp-1.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "logo_avatar_square_grey_color_2x_ios_29in40dp-2.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "50x50",
+ "idiom" : "ipad",
+ "filename" : "logo_avatar_square_grey_color_1x_ios_38in50dp.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "50x50",
+ "idiom" : "ipad",
+ "filename" : "logo_avatar_square_grey_color_2x_ios_38in50dp.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "72x72",
+ "idiom" : "ipad",
+ "filename" : "logo_avatar_square_grey_color_1x_ios_53in72dp.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "72x72",
+ "idiom" : "ipad",
+ "filename" : "logo_avatar_square_grey_color_2x_ios_53in72dp.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "logo_avatar_square_grey_color_1x_ios_56in76dp.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "logo_avatar_square_grey_color_2x_ios_56in76dp.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "83.5x83.5",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "16x16",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "16x16",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "32x32",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "32x32",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "128x128",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "128x128",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "256x256",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "256x256",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "512x512",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "512x512",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+} \ No newline at end of file
diff --git a/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_21in29dp-1.png b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_21in29dp-1.png
new file mode 100644
index 0000000..2976035
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_21in29dp-1.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_21in29dp.png b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_21in29dp.png
new file mode 100644
index 0000000..2976035
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_21in29dp.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_29in40dp-1.png b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_29in40dp-1.png
new file mode 100644
index 0000000..32684ce
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_29in40dp-1.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_38in50dp.png b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_38in50dp.png
new file mode 100644
index 0000000..0c98554
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_38in50dp.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_42in57dp.png b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_42in57dp.png
new file mode 100644
index 0000000..3ef403a
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_42in57dp.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_53in72dp.png b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_53in72dp.png
new file mode 100644
index 0000000..ff6c804
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_53in72dp.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_56in76dp.png b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_56in76dp.png
new file mode 100644
index 0000000..df8a953
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_1x_ios_56in76dp.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_21in29dp-1.png b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_21in29dp-1.png
new file mode 100644
index 0000000..4067017
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_21in29dp-1.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_21in29dp.png b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_21in29dp.png
new file mode 100644
index 0000000..4067017
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_21in29dp.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_29in40dp-1.png b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_29in40dp-1.png
new file mode 100644
index 0000000..9452a26
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_29in40dp-1.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_29in40dp-2.png b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_29in40dp-2.png
new file mode 100644
index 0000000..9452a26
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_29in40dp-2.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_38in50dp.png b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_38in50dp.png
new file mode 100644
index 0000000..436be10
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_38in50dp.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_42in57dp.png b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_42in57dp.png
new file mode 100644
index 0000000..e9c869e
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_42in57dp.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_44in60dp.png b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_44in60dp.png
new file mode 100644
index 0000000..8c5ce9d
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_44in60dp.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_53in72dp.png b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_53in72dp.png
new file mode 100644
index 0000000..0ddd720
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_53in72dp.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_56in76dp.png b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_56in76dp.png
new file mode 100644
index 0000000..2f028cb
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_2x_ios_56in76dp.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_21in29dp.png b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_21in29dp.png
new file mode 100644
index 0000000..69bb8d3
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_21in29dp.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_29in40dp-1.png b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_29in40dp-1.png
new file mode 100644
index 0000000..7045675
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_29in40dp-1.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_44in60dp.png b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_44in60dp.png
new file mode 100644
index 0000000..211ef93
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/AppIcon.appiconset/logo_avatar_square_grey_color_3x_ios_44in60dp.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/Contents.json b/Example/Auth/Sample/Images.xcassets/Contents.json
new file mode 100644
index 0000000..da4a164
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+} \ No newline at end of file
diff --git a/Example/Auth/Sample/Images.xcassets/close.imageset/Contents.json b/Example/Auth/Sample/Images.xcassets/close.imageset/Contents.json
new file mode 100644
index 0000000..b38b207
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/close.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "ic_clear_black_1x_ios_24dp.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "ic_clear_black_2x_ios_24dp.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "ic_clear_black_3x_ios_24dp.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+} \ No newline at end of file
diff --git a/Example/Auth/Sample/Images.xcassets/close.imageset/ic_clear_black_1x_ios_24dp.png b/Example/Auth/Sample/Images.xcassets/close.imageset/ic_clear_black_1x_ios_24dp.png
new file mode 100644
index 0000000..40a1a84
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/close.imageset/ic_clear_black_1x_ios_24dp.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/close.imageset/ic_clear_black_2x_ios_24dp.png b/Example/Auth/Sample/Images.xcassets/close.imageset/ic_clear_black_2x_ios_24dp.png
new file mode 100644
index 0000000..6bc4372
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/close.imageset/ic_clear_black_2x_ios_24dp.png
Binary files differ
diff --git a/Example/Auth/Sample/Images.xcassets/close.imageset/ic_clear_black_3x_ios_24dp.png b/Example/Auth/Sample/Images.xcassets/close.imageset/ic_clear_black_3x_ios_24dp.png
new file mode 100644
index 0000000..51b4401
--- /dev/null
+++ b/Example/Auth/Sample/Images.xcassets/close.imageset/ic_clear_black_3x_ios_24dp.png
Binary files differ
diff --git a/Example/Auth/Sample/MainViewController.h b/Example/Auth/Sample/MainViewController.h
new file mode 100644
index 0000000..a4d3583
--- /dev/null
+++ b/Example/Auth/Sample/MainViewController.h
@@ -0,0 +1,92 @@
+/*
+ * 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 StaticContentTableViewManager;
+@class UserTableViewCell;
+
+/** @var kCreateUserAccessibilityID
+ @brief The "Create User" button accessibility ID.
+ */
+extern NSString *const kCreateUserAccessibilityID;
+
+/** @class MainViewController
+ @brief The first view controller presented when the application is started.
+ */
+@interface MainViewController : UIViewController
+
+/** @property tableViewManager
+ @brief A @c StaticContentTableViewManager which is used to manage the contents of the table
+ view.
+ */
+@property(nonatomic, strong) IBOutlet StaticContentTableViewManager *tableViewManager;
+
+/** @property tableView
+ @brief A UITableView which is used to display user info and a list of actions.
+ */
+@property(nonatomic, weak) IBOutlet UITableView *tableView;
+
+/** @property userInfoTableViewCell
+ @brief A custom UITableViewCell for displaying the user info.
+ */
+@property(nonatomic, strong) IBOutlet UserTableViewCell *userInfoTableViewCell;
+
+/** @property userInMemoryInfoTableViewCell
+ @brief A custom UITableViewCell for displaying the user info.
+ */
+@property(nonatomic, strong) IBOutlet UserTableViewCell *userInMemoryInfoTableViewCell;
+
+/** @property userToUseCell
+ @brief A custom UITableViewCell for choosing which user to use for user operations (either the
+ currently signed-in user, or the user in "memory".
+ */
+@property(nonatomic, strong) IBOutlet UITableViewCell *userToUseCell;
+
+/** @property consoleTextView
+ @brief A UITextView with a log of the actions performed in the sample app.
+ */
+@property(nonatomic, weak) IBOutlet UITextView *consoleTextView;
+
+/** @fn userToUseDidChange:
+ @brief Should be invoked when the user wishes to switch which user to use for user-related
+ operations in the sample app.
+ @param sender The UISegmentedControl which prompted the change in value. It is assumed that the
+ segment at index 0 represents the "signed-in user" and the segment at index 1 represents the
+ "user in memeory".
+ */
+- (IBAction)userToUseDidChange:(UISegmentedControl *)sender;
+
+/** @fn memoryPlus
+ @brief Works like the "M+" button on a calculator; stores the currently signed-in user as the
+ "user in memory" for the application.
+ */
+- (IBAction)memoryPlus;
+
+/** @fn memoryClear
+ @brief Works like the "MC" button on a calculator; clears the currently stored "user in memory"
+ for the application.
+ */
+- (IBAction)memoryClear;
+
+/** @fn handleIncomingLinkWithURL:
+ @brief Handles an incoming link to trigger the appropriate OOBCode if possible.
+ @param URL The webURL of the incoming universal link.
+ @return Boolean value indicating whether the incoming link could be handled or not.
+ */
+- (BOOL)handleIncomingLinkWithURL:(NSURL *)URL;
+
+@end
diff --git a/Example/Auth/Sample/MainViewController.m b/Example/Auth/Sample/MainViewController.m
new file mode 100644
index 0000000..8d87c27
--- /dev/null
+++ b/Example/Auth/Sample/MainViewController.m
@@ -0,0 +1,2858 @@
+/*
+ * 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 "MainViewController.h"
+
+#import <objc/runtime.h>
+
+#import "AppManager.h"
+#import "AuthCredentials.h"
+#import "FIRAdditionalUserInfo.h"
+#import "FirebaseCommunity/FIRApp.h"
+#import "FirebaseCommunity/FIRAppInternal.h"
+#import "FirebaseCommunity/FIRAppAssociationRegistration.h"
+#import "FIROAuthProvider.h"
+#import "FIRPhoneAuthCredential.h"
+#import "FIRPhoneAuthProvider.h"
+#import "FirebaseAuth.h"
+#import "CustomTokenDataEntryViewController.h"
+#import "FacebookAuthProvider.h"
+#import "GoogleAuthProvider.h"
+#import "SettingsViewController.h"
+#import "StaticContentTableViewManager.h"
+#import "UIViewController+Alerts.h"
+#import "UserInfoViewController.h"
+#import "UserTableViewCell.h"
+
+
+/*! @typedef textInputCompletionBlock
+ @brief The type of callback used to report text input prompt results.
+ */
+typedef void (^textInputCompletionBlock)(NSString *_Nullable userInput);
+
+/** @var kTokenGetButtonText
+ @brief The text of the "Get Token" button.
+ */
+static NSString *const kTokenGetButtonText = @"Get Token";
+
+/** @var kTokenRefreshButtonText
+ @brief The text of the "Refresh Token" button.
+ */
+static NSString *const kTokenRefreshButtonText = @"Force Refresh Token";
+
+/** @var kTokenRefreshedAlertTitle
+ @brief The title of the "Token Refreshed" alert.
+ */
+static NSString *const kTokenRefreshedAlertTitle = @"Token";
+
+/** @var kTokenRefreshErrorAlertTitle
+ @brief The title of the "Token Refresh error" alert.
+ */
+static NSString *const kTokenRefreshErrorAlertTitle = @"Get Token Error";
+
+/** @var kSettingsButtonTextDesription
+ @brief The description for the text of the "Settings" button.
+ */
+static NSString *const kSettingsButtonTextDesription =
+ @"The button text to open sample app settings";
+
+/** @var kUserInfoButtonText
+ @brief The text of the "Show User Info" button.
+ */
+static NSString *const kUserInfoButtonText = @"[Show User Info]";
+
+/** @var kSetImageURLText
+ @brief The text of the "Set Photo url" button.
+ */
+static NSString *const kSetPhotoURLText = @"Set Photo url";
+
+/** @var kSignInButtonText
+ @brief The text of the "Sign In" button.
+ */
+static NSString *const kSignInButtonText = @"Sign In (HEADFUL)";
+
+/** @var kSignInGoogleButtonText
+ @brief The text of the "Google SignIn" button.
+ */
+static NSString *const kSignInGoogleButtonText = @"Sign in with Google";
+
+/** @var kSignInAndRetrieveGoogleButtonText
+ @brief The text of the "Sign in with Google and retrieve data" button.
+ */
+static NSString *const kSignInGoogleAndRetrieveDataButtonText =
+ @"Sign in with Google and retrieve data";
+
+/** @var kSignInFacebookButtonText
+ @brief The text of the "Facebook SignIn" button.
+ */
+static NSString *const kSignInFacebookButtonText = @"Sign in with Facebook";
+
+/** @var kSignInFacebookAndRetrieveDataButtonText
+ @brief The text of the "Facebook SignIn and retrieve data" button.
+ */
+static NSString *const kSignInFacebookAndRetrieveDataButtonText =
+ @"Sign in with Facebook and retrieve data";
+
+/** @var kSignInEmailPasswordButtonText
+ @brief The text of the "Email/Password SignIn" button.
+ */
+static NSString *const kSignInEmailPasswordButtonText = @"Sign in with Email/Password";
+
+/** @var kSignInWithCustomTokenButtonText
+ @brief The text of the "Sign In (BYOAuth)" button.
+ */
+static NSString *const kSignInWithCustomTokenButtonText = @"Sign In (BYOAuth)";
+
+/** @var kSignInAnonymouslyButtonText
+ @brief The text of the "Sign In Anonymously" button.
+ */
+static NSString *const kSignInAnonymouslyButtonText = @"Sign In Anonymously";
+
+/** @var kSignedInAlertTitle
+ @brief The text of the "Sign In Succeeded" alert.
+ */
+static NSString *const kSignedInAlertTitle = @"Signed In";
+
+/** @var kSignInErrorAlertTitle
+ @brief The text of the "Sign In Encountered an Error" alert.
+ */
+static NSString *const kSignInErrorAlertTitle = @"Sign-In Error";
+
+/** @var kSignOutButtonText
+ @brief The text of the "Sign Out" button.
+ */
+static NSString *const kSignOutButtonText = @"Sign Out";
+
+/** @var kDeleteAccountText
+ @brief The text of the "Delete Account" button.
+ */
+static NSString *const kDeleteUserText = @"Delete Account";
+
+/** @var kReauthenticateGoogleText
+ @brief The text of the "Reathenticate Google" button.
+ */
+static NSString *const kReauthenticateGoogleText = @"Reauthenticate Google";
+
+/** @var kReauthenticateGoogleAndRetrieveDataText
+ @brief The text of the "Reathenticate Google and retrieve data" button.
+ */
+static NSString *const kReauthenticateGoogleAndRetrieveDataText =
+ @"Reauthenticate Google and retrieve data";
+
+/** @var kReauthenticateFBText
+ @brief The text of the "Reathenticate Facebook" button.
+ */
+static NSString *const kReauthenticateFBText = @"Reauthenticate FB";
+
+/** @var kReauthenticateFBAndRetrieveDataText
+ @brief The text of the "Reathenticate Facebook and retrieve data" button.
+ */
+static NSString *const kReauthenticateFBAndRetrieveDataText =
+ @"Reauthenticate FB and retrieve data";
+
+/** @var kReAuthenticateEmail
+ @brief The text of the "Reathenticate Email" button.
+ */
+static NSString *const kReauthenticateEmailText = @"Reauthenticate Email/Password";
+
+/** @var kOKButtonText
+ @brief The text of the "OK" button for the Sign In result dialogs.
+ */
+static NSString *const kOKButtonText = @"OK";
+
+/** @var kSetDisplayNameTitle
+ @brief The title of the "Set Display Name" error dialog.
+ */
+static NSString *const kSetDisplayNameTitle = @"Set Display Name";
+
+/** @var kUpdateEmailText
+ @brief The title of the "Update Email" button.
+ */
+static NSString *const kUpdateEmailText = @"Update Email";
+
+/** @var kUpdatePasswordText
+ @brief The title of the "Update Password" button.
+ */
+static NSString *const kUpdatePasswordText = @"Update Password";
+
+/** @var kUpdatePhoneNumber
+ @brief The title of the "Update Photo" button.
+ */
+static NSString *const kUpdatePhoneNumber = @"Update Phone Number";
+
+/** @var kLinkPhoneNumber
+ @brief The title of the "Link phone" button.
+ */
+static NSString *const kLinkPhoneNumber = @"Link Phone Number";
+
+/** @var kUnlinkPhone
+ @brief The title of the "Unlink Phone" button for unlinking phone auth provider.
+ */
+static NSString *const kUnlinkPhoneNumber = @"Unlink Phone Number";
+
+/** @var kReloadText
+ @brief The title of the "Reload User" button.
+ */
+static NSString *const kReloadText = @"Reload User";
+
+/** @var kLinkWithGoogleText
+ @brief The title of the "Link with Google" button.
+ */
+static NSString *const kLinkWithGoogleText = @"Link with Google";
+
+/** @var kLinkWithGoogleAndRetrieveDataText
+ @brief The title of the "Link with Google and retrieve data" button.
+ */
+static NSString *const kLinkWithGoogleAndRetrieveDataText = @"Link with Google and retrieve data";
+
+/** @var kLinkWithFacebookText
+ @brief The title of the "Link with Facebook Account" button.
+ */
+static NSString *const kLinkWithFacebookText = @"Link with Facebook";
+
+/** @var kLinkWithFacebookAndRetrieveDataText
+ @brief The title of the "Link with Facebook and retrieve data" button.
+ */
+static NSString *const kLinkWithFacebookAndRetrieveDataText =
+ @"Link with Facebook and retrieve data";
+
+/** @var kLinkWithEmailPasswordText
+ @brief The title of the "Link with Email/Password Account" button.
+ */
+static NSString *const kLinkWithEmailPasswordText = @"Link with Email/Password";
+
+/** @var kUnlinkTitle
+ @brief The text of the "Unlink from Provider" error Dialog.
+ */
+static NSString *const kUnlinkTitle = @"Unlink from Provider";
+
+/** @var kUnlinkFromGoogle
+ @brief The text of the "Unlink from Google" button.
+ */
+static NSString *const kUnlinkFromGoogle = @"Unlink from Google";
+
+/** @var kUnlinkFromFacebook
+ @brief The text of the "Unlink from Facebook" button.
+ */
+static NSString *const kUnlinkFromFacebook = @"Unlink from Facebook";
+
+/** @var kUnlinkFromEmailPassword
+ @brief The text of the "Unlink from Google" button.
+ */
+static NSString *const kUnlinkFromEmailPassword = @"Unlink from Email/Password";
+
+/** @var kGetProvidersForEmail
+ @brief The text of the "Get Provider IDs for Email" button.
+ */
+static NSString *const kGetProvidersForEmail = @"Get Provider IDs for Email";
+
+/** @var kActionCodeTypeDescription
+ @brief The description of the "Action Type" entry.
+ */
+static NSString *const kActionCodeTypeDescription = @"Action Type";
+
+/** @var kContinueURLDescription
+ @brief The description of the "Continue URL" entry.
+ */
+static NSString *const kContinueURLDescription = @"Continue URL";
+
+/** @var kRequestVerifyEmail
+ @brief The text of the "Request Verify Email Link" button.
+ */
+static NSString *const kRequestVerifyEmail = @"Request Verify Email Link";
+
+/** @var kRequestPasswordReset
+ @brief The text of the "Email Password Reset" button.
+ */
+static NSString *const kRequestPasswordReset = @"Send Password Reset Email";
+
+/** @var kResetPassword
+ @brief The text of the "Password Reset" button.
+ */
+static NSString *const kResetPassword = @"Reset Password";
+
+/** @var kCheckActionCode
+ @brief The text of the "Check action code" button.
+ */
+static NSString *const kCheckActionCode = @"Check action code";
+
+/** @var kApplyActionCode
+ @brief The text of the "Apply action code" button.
+ */
+static NSString *const kApplyActionCode = @"Apply action code";
+
+/** @var kVerifyPasswordResetCode
+ @brief The text of the "Verify password reset code" button.
+ */
+static NSString *const kVerifyPasswordResetCode = @"Verify password reset code";
+
+/** @var kSectionTitleSettings
+ @brief The text for the title of the "Settings" section.
+ */
+static NSString *const kSectionTitleSettings = @"SETTINGS";
+
+/** @var kSectionTitleSignIn
+ @brief The text for the title of the "Sign-In" section.
+ */
+static NSString *const kSectionTitleSignIn = @"SIGN-IN";
+
+/** @var kSectionTitleReauthenticate
+ @brief The text for the title of the "Reauthenticate" section.
+ */
+static NSString *const kSectionTitleReauthenticate = @"REAUTHENTICATE";
+
+/** @var kSectionTitleTokenActions
+ @brief The text for the title of the "Token Actions" section.
+ */
+static NSString *const kSectionTitleTokenActions = @"TOKEN ACTIONS";
+
+/** @var kSectionTitleEditUser
+ @brief The text for the title of the "Edit User" section.
+ */
+static NSString *const kSectionTitleEditUser = @"EDIT USER";
+
+/** @var kSectionTitleLinkUnlinkAccount
+ @brief The text for the title of the "Link/Unlink account" section.
+ */
+static NSString *const kSectionTitleLinkUnlinkAccounts = @"LINK/UNLINK ACCOUNT";
+
+/** @var kSectionTitleUserActions
+ @brief The text for the title of the "User Actions" section.
+ */
+static NSString *const kSectionTitleUserActions = @"USER ACTIONS";
+
+/** @var kSectionTitleOOBAction
+ @brief The text for the title of the "OOB Actions" section.
+ */
+static NSString *const kSectionTitleOOBActions = @"OOB ACTIONS";
+
+/** @var kSectionTitleUserDetails
+ @brief The text for the title of the "User Details" section.
+ */
+static NSString *const kSectionTitleUserDetails = @"SIGNED-IN USER DETAILS";
+
+/** @var kSectionTitleListeners
+ @brief The title for the table view section dedicated to auth state did change listeners.
+ */
+static NSString *const kSectionTitleListeners = @"Listeners";
+
+/** @var kAddAuthStateListenerTitle
+ @brief The title for the table view row which adds a block to the auth state did change
+ listeners.
+ */
+static NSString *const kAddAuthStateListenerTitle = @"Add Auth State Change Listener";
+
+/** @var kRemoveAuthStateListenerTitle
+ @brief The title for the table view row which removes a block to the auth state did change
+ listeners.
+ */
+static NSString *const kRemoveAuthStateListenerTitle = @"Remove Last Auth State Change Listener";
+
+/** @var kAddIDTokenListenerTitle
+ @brief The title for the table view row which adds a block to the ID token did change
+ listeners.
+ */
+static NSString *const kAddIDTokenListenerTitle = @"Add ID Token Change Listener";
+
+/** @var kRemoveIDTokenListenerTitle
+ @brief The title for the table view row which removes a block to the ID token did change
+ listeners.
+ */
+static NSString *const kRemoveIDTokenListenerTitle = @"Remove Last ID Token Change Listener";
+
+/** @var kSectionTitleApp
+ @brief The text for the title of the "App" section.
+ */
+static NSString *const kSectionTitleApp = @"APP";
+
+/** @var kCreateUserTitle
+ @brief The text of the "Create User" button.
+ */
+static NSString *const kCreateUserTitle = @"Create User";
+
+/** @var kDeleteAppTitle
+ @brief The text of the "Delete App" button.
+ */
+static NSString *const kDeleteAppTitle = @"Delete App";
+
+/** @var kTimeAuthInitTitle
+ @brief The text of the "Time Auth Initialization" button.
+ */
+static NSString *const kTimeAuthInitTitle = @"Time Auth Initialization";
+
+/** @var kSectionTitleManualTests
+ @brief The section title for automated manual tests.
+ */
+static NSString *const kSectionTitleManualTests = @"Automated (Manual) Tests";
+
+/** @var kAutoBYOAuthTitle
+ @brief The button title for automated BYOAuth operation.
+ */
+static NSString *const kAutoBYOAuthTitle = @"BYOAuth";
+
+/** @var kAutoSignInGoogle
+ @brief The button title for automated Google sign-in operation.
+ */
+static NSString *const kAutoSignInGoogle = @"Sign In With Google";
+
+/** @var kAutoSignInFacebook
+ @brief The button title for automated Facebook sign-in operation.
+ */
+static NSString *const kAutoSignInFacebook = @"Sign In With Facebook";
+
+/** @var kAutoSignUpEmailPassword
+ @brief The button title for automated sign-up with email/password.
+ */
+static NSString *const kAutoSignUpEmailPassword = @"Sign Up With Email/Password";
+
+/** @var kAutoSignInAnonymously
+ @brief The button title for automated sign-in anonymously.
+ */
+static NSString *const kAutoSignInAnonymously = @"Sign In Anonymously";
+
+/** @var kAutoAccountLinking
+ @brief The button title for automated account linking.
+ */
+static NSString *const kAutoAccountLinking = @"Link with Google";
+
+/** @var kGitHubSignInButtonText
+ @brief The button title for signing in with github.
+ */
+static NSString *const kGitHubSignInButtonText = @"Sign In with GitHub";
+
+/** @var kExpiredCustomTokenUrl
+ @brief The url for obtaining a valid custom token string used to test BYOAuth.
+ */
+static NSString *const kCustomTokenUrl = @"https://fb-sa-1211.appspot.com/token";
+
+/** @var kCustomTokenUrl
+ @brief The url for obtaining an expired custom token string used to test BYOAuth.
+ */
+static NSString *const kExpiredCustomTokenUrl = @"https://fb-sa-1211.appspot.com/expired_token";
+
+/** @var kFakeDisplayPhotoUrl
+ @brief The url for obtaining a display displayPhoto used for testing.
+ */
+static NSString *const kFakeDisplayPhotoUrl =
+ @"https://www.gstatic.com/images/branding/product/1x/play_apps_48dp.png";
+
+/** @var kFakeDisplayName
+ @brief Fake display name for testing.
+ */
+static NSString *const kFakeDisplayName = @"John GoogleSpeed";
+
+/** @var kFakeEmail
+ @brief Fake email for testing.
+ */
+static NSString *const kFakeEmail =@"firemail@example.com";
+
+/** @var kFakePassword
+ @brief Fake password for testing.
+ */
+static NSString *const kFakePassword =@"fakePassword";
+
+/** @var kInvalidCustomToken
+ @brief The custom token string for testing BYOAuth.
+ */
+static NSString *const kInvalidCustomToken = @"invalid custom token.";
+
+/** @var kSafariGoogleSignOutMessagePrompt
+ @brief The message text informing user to sign-out from Google on safari before continuing.
+ */
+static NSString *const kSafariGoogleSignOutMessagePrompt = @"This automated test assumes that no "
+ "Google account is signed in on Safari, if your are not prompted for a password, sign out on "
+ "Safari and rerun the test.";
+
+/** @var kSafariFacebookSignOutMessagePrompt
+ @brief The message text informing user to sign-out from Facebook on safari before continuing.
+ */
+static NSString *const kSafariFacebookSignOutMessagePrompt = @"This automated test assumes that no "
+ "Facebook account is signed in on Safari, if your are not prompted for a password, sign out on "
+ "Safari and rerun the test.";
+
+/** @var kUnlinkAccountMessagePrompt
+ @brief The message text informing user to use an unlinked account for account linking.
+ */
+static NSString *const kUnlinkAccountMessagePrompt = @"Sign into gmail with an email address "
+ "that has not been linked to this sample application before. Delete account if necessary.";
+
+/** @var kPasswordResetAction
+ @brief The value for password reset mode in the action code URL.
+ */
+static NSString *const kPasswordResetAction = @"resetPassword";
+
+/** @var kVerifyEmailAction
+ @brief The value for verify email mode in the action code URL.
+ */
+static NSString *const kVerifyEmailAction = @"verifyEmail";
+
+// Declared extern in .h file.
+NSString *const kCreateUserAccessibilityID = @"CreateUserAccessibilityID";
+
+/** @var kPhoneAuthSectionTitle
+ @brief The title for the phone auth section of the test app.
+ */
+static NSString *const kPhoneAuthSectionTitle = @"Phone Auth";
+
+/** @var kPhoneNumberSignInTitle
+ @brief The title for button to sign in with phone number.
+ */
+static NSString *const kPhoneNumberSignInTitle = @"Sign in With Phone Number";
+
+/** @var kPhoneNumberSignInTitle
+ @brief The title for button to sign in with phone number using reCAPTCHA.
+ */
+static NSString *const kPhoneNumberSignInReCaptchaTitle = @"Sign in With Phone Number (reCAPTCHA)";
+
+/** @typedef showEmailPasswordDialogCompletion
+ @brief The type of block which gets called to complete the Email/Password dialog flow.
+ */
+typedef void (^ShowEmailPasswordDialogCompletion)(FIRAuthCredential *credential);
+
+/** @typedef FIRTokenCallback
+ @brief The type of block which gets called when a token is ready.
+ */
+typedef void (^FIRTokenCallback)(NSString *_Nullable token, NSError *_Nullable error);
+
+/** @brief The request type for OOB action codes.
+ */
+typedef enum {
+ /** No action code settings. */
+ ActionCodeRequestTypeEmail,
+ /** With continue URL but not handled in-app. */
+ ActionCodeRequestTypeContinue,
+ /** Handled in-app. */
+ ActionCodeRequestTypeInApp,
+} ActionCodeRequestType;
+
+/** @category FIRAppAssociationRegistration(Deregistration)
+ @brief The category for the deregistration method.
+ */
+@interface FIRAppAssociationRegistration (Deregistration)
+/** @fn deregisteredObjectWithHost:key:
+ @brief Removes the object that was registered with a particular host and key, if one exists.
+ @param host The host object.
+ @param key The key to specify the registered object on the host.
+ */
++ (void)deregisterObjectWithHost:(id)host key:(NSString *)key;
+@end
+
+@implementation FIRAppAssociationRegistration (Deregistration)
+
++ (void)deregisterObjectWithHost:(id)host key:(NSString *)key {
+ @synchronized(self) {
+ SEL dictKey = @selector(registeredObjectWithHost:key:creationBlock:);
+ NSMutableDictionary<NSString *, id> *objectsByKey = objc_getAssociatedObject(host, dictKey);
+ [objectsByKey removeObjectForKey:key];
+ }
+}
+
+@end
+
+@implementation MainViewController {
+ NSMutableString *_consoleString;
+
+ /** @var _authStateDidChangeListeners
+ @brief An array of handles created during calls to @c FIRAuth.addAuthStateDidChangeListener:
+ */
+ NSMutableArray<FIRAuthStateDidChangeListenerHandle> *_authStateDidChangeListeners;
+
+ /** @var _IDTokenDidChangeListeners
+ @brief An array of handles created during calls to @c FIRAuth.addIDTokenDidChangeListener:
+ */
+ NSMutableArray<FIRAuthStateDidChangeListenerHandle> *_IDTokenDidChangeListeners;
+
+ /** @var _userInMemory
+ @brief Acts like the "memory" function of a calculator. An operation allows sample app users
+ to assign this value based on @c FIRAuth.currentUser or clear this value.
+ */
+ FIRUser *_userInMemory;
+
+ /** @var _useUserInMemory
+ @brief Instructs the application to use _userInMemory instead of @c FIRAuth.currentUser for
+ testing operations. This allows us to test if things still work with a user who is not
+ the @c FIRAuth.currentUser, and also allows us to test those things while
+ @c FIRAuth.currentUser remains nil (after a sign-out) and also when @c FIRAuth.currentUser
+ is non-nil (do to a subsequent sign-in.)
+ */
+ BOOL _useUserInMemory;
+
+ /** @var _actionCodeRequestType
+ @brief The type for the next action code request.
+ */
+ ActionCodeRequestType _actionCodeRequestType;
+
+ /** @var _actionCodeContinueURL
+ @brief The continue URL to be used in the next action code request.
+ */
+ NSURL *_actionCodeContinueURL;
+}
+
+/** @fn initWithNibName:bundle:
+ @brief Overridden default initializer.
+ */
+- (id)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil {
+ self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
+ if (self) {
+ _actionCodeRequestType = ActionCodeRequestTypeInApp;
+ _actionCodeContinueURL = [NSURL URLWithString:KCONTINUE_URL];
+ _authStateDidChangeListeners = [NSMutableArray array];
+ _IDTokenDidChangeListeners = [NSMutableArray array];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(authStateChangedForAuth:)
+ name:FIRAuthStateDidChangeNotification
+ object:nil];
+ self.useStatusBarSpinner = YES;
+ }
+ return self;
+}
+
+- (void)viewDidLoad {
+ // Give us a circle for the image view:
+ _userInfoTableViewCell.userInfoProfileURLImageView.layer.cornerRadius =
+ _userInfoTableViewCell.userInfoProfileURLImageView.frame.size.width / 2.0f;
+ _userInfoTableViewCell.userInfoProfileURLImageView.layer.masksToBounds = YES;
+ _userInMemoryInfoTableViewCell.userInfoProfileURLImageView.layer.cornerRadius =
+ _userInMemoryInfoTableViewCell.userInfoProfileURLImageView.frame.size.width / 2.0f;
+ _userInMemoryInfoTableViewCell.userInfoProfileURLImageView.layer.masksToBounds = YES;
+}
+
+- (void)viewWillAppear:(BOOL)animated {
+ [super viewWillAppear:animated];
+ [self updateTable];
+ [self updateUserInfo];
+}
+
+- (void)updateTable {
+ __weak typeof(self) weakSelf = self;
+ _tableViewManager.contents =
+ [StaticContentTableViewContent contentWithSections:@[
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleUserDetails cells:@[
+ [StaticContentTableViewCell cellWithCustomCell:_userInfoTableViewCell action:^{
+ [weakSelf presentUserInfo];
+ }],
+ [StaticContentTableViewCell cellWithCustomCell:_userToUseCell],
+ [StaticContentTableViewCell cellWithCustomCell:_userInMemoryInfoTableViewCell action:^{
+ [weakSelf presentUserInMemoryInfo];
+ }],
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleSettings cells:@[
+ [StaticContentTableViewCell cellWithTitle:NSLocalizedString(@"SETTINGSKEY",
+ kSettingsButtonTextDesription)
+ action:^{ [weakSelf presentSettings]; }]
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kPhoneAuthSectionTitle cells:@[
+ [StaticContentTableViewCell cellWithTitle:kPhoneNumberSignInReCaptchaTitle
+ action:^{ [weakSelf signInWithPhoneNumberRecaptcha]; }],
+ [StaticContentTableViewCell cellWithTitle:kPhoneNumberSignInTitle
+ action:^{ [weakSelf signInWithPhoneNumber]; }],
+ [StaticContentTableViewCell cellWithTitle:kUpdatePhoneNumber
+ action:^{ [weakSelf updatePhoneNumber]; }],
+ [StaticContentTableViewCell cellWithTitle:kLinkPhoneNumber
+ action:^{ [weakSelf linkPhoneNumber]; }],
+ [StaticContentTableViewCell cellWithTitle:kUnlinkPhoneNumber
+ action:^{
+ [weakSelf unlinkFromProvider:FIRPhoneAuthProviderID];
+ }],
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleSignIn cells:@[
+ [StaticContentTableViewCell cellWithTitle:kCreateUserTitle
+ value:nil
+ action:^{ [weakSelf createUser]; }
+ accessibilityID:kCreateUserAccessibilityID],
+ [StaticContentTableViewCell cellWithTitle:kSignInGoogleButtonText
+ action:^{ [weakSelf signInGoogle]; }],
+ [StaticContentTableViewCell cellWithTitle:kSignInGoogleAndRetrieveDataButtonText
+ action:^{ [weakSelf signInGoogleAndRetrieveData]; }],
+ [StaticContentTableViewCell cellWithTitle:kSignInFacebookButtonText
+ action:^{ [weakSelf signInFacebook]; }],
+ [StaticContentTableViewCell cellWithTitle:kSignInFacebookAndRetrieveDataButtonText
+ action:^{ [weakSelf signInFacebookAndRetrieveData]; }],
+ [StaticContentTableViewCell cellWithTitle:kSignInEmailPasswordButtonText
+ action:^{ [weakSelf signInEmailPassword]; }],
+ [StaticContentTableViewCell cellWithTitle:kSignInWithCustomTokenButtonText
+ action:^{ [weakSelf signInWithCustomToken]; }],
+ [StaticContentTableViewCell cellWithTitle:kSignInAnonymouslyButtonText
+ action:^{ [weakSelf signInAnonymously]; }],
+ [StaticContentTableViewCell cellWithTitle:kGitHubSignInButtonText
+ action:^{ [weakSelf signInWithGitHub]; }],
+ [StaticContentTableViewCell cellWithTitle:kSignOutButtonText
+ action:^{ [weakSelf signOut]; }]
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleUserActions cells:@[
+ [StaticContentTableViewCell cellWithTitle:kSetDisplayNameTitle
+ action:^{ [weakSelf setDisplayName]; }],
+ [StaticContentTableViewCell cellWithTitle:kSetPhotoURLText
+ action:^{ [weakSelf setPhotoURL]; }],
+ [StaticContentTableViewCell cellWithTitle:kReloadText
+ action:^{ [weakSelf reloadUser]; }],
+ [StaticContentTableViewCell cellWithTitle:kGetProvidersForEmail
+ action:^{ [weakSelf getProvidersForEmail]; }],
+ [StaticContentTableViewCell cellWithTitle:kUpdateEmailText
+ action:^{ [weakSelf updateEmail]; }],
+ [StaticContentTableViewCell cellWithTitle:kUpdatePasswordText
+ action:^{ [weakSelf updatePassword]; }],
+ [StaticContentTableViewCell cellWithTitle:kDeleteUserText
+ action:^{ [weakSelf deleteAccount]; }],
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleOOBActions cells:@[
+ [StaticContentTableViewCell cellWithTitle:kActionCodeTypeDescription
+ value:[self actionCodeRequestTypeString]
+ action:^{ [weakSelf toggleActionCodeRequestType]; }],
+ [StaticContentTableViewCell cellWithTitle:kContinueURLDescription
+ value:_actionCodeContinueURL.absoluteString ?: @"(nil)"
+ action:^{ [weakSelf changeActionCodeContinueURL]; }],
+ [StaticContentTableViewCell cellWithTitle:kRequestVerifyEmail
+ action:^{ [weakSelf requestVerifyEmail]; }],
+ [StaticContentTableViewCell cellWithTitle:kRequestPasswordReset
+ action:^{ [weakSelf requestPasswordReset]; }],
+ [StaticContentTableViewCell cellWithTitle:kResetPassword
+ action:^{ [weakSelf resetPassword]; }],
+ [StaticContentTableViewCell cellWithTitle:kCheckActionCode
+ action:^{ [weakSelf checkActionCode]; }],
+ [StaticContentTableViewCell cellWithTitle:kApplyActionCode
+ action:^{ [weakSelf applyActionCode]; }],
+ [StaticContentTableViewCell cellWithTitle:kVerifyPasswordResetCode
+ action:^{ [weakSelf verifyPasswordResetCode]; }],
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleReauthenticate cells:@[
+ [StaticContentTableViewCell cellWithTitle:kReauthenticateGoogleText
+ action:^{ [weakSelf reauthenticateGoogle]; }],
+ [StaticContentTableViewCell
+ cellWithTitle:kReauthenticateGoogleAndRetrieveDataText
+ action:^{ [weakSelf reauthenticateGoogleAndRetrieveData]; }],
+ [StaticContentTableViewCell cellWithTitle:kReauthenticateFBText
+ action:^{ [weakSelf reauthenticateFB]; }],
+ [StaticContentTableViewCell cellWithTitle:kReauthenticateFBAndRetrieveDataText
+ action:^{ [weakSelf reauthenticateFBAndRetrieveData]; }],
+ [StaticContentTableViewCell cellWithTitle:kReauthenticateEmailText
+ action:^{ [weakSelf reauthenticateEmailPassword]; }]
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleTokenActions cells:@[
+ [StaticContentTableViewCell cellWithTitle:kTokenGetButtonText
+ action:^{ [weakSelf getUserTokenWithForce:NO]; }],
+ [StaticContentTableViewCell cellWithTitle:kTokenRefreshButtonText
+ action:^{ [weakSelf getUserTokenWithForce:YES]; }]
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleLinkUnlinkAccounts cells:@[
+ [StaticContentTableViewCell cellWithTitle:kLinkWithGoogleText
+ action:^{ [weakSelf linkWithGoogle]; }],
+ [StaticContentTableViewCell cellWithTitle:kLinkWithGoogleAndRetrieveDataText
+ action:^{ [weakSelf linkWithGoogleAndRetrieveData]; }],
+ [StaticContentTableViewCell cellWithTitle:kLinkWithFacebookText
+ action:^{ [weakSelf linkWithFacebook]; }],
+ [StaticContentTableViewCell cellWithTitle:kLinkWithFacebookAndRetrieveDataText
+ action:^{ [weakSelf linkWithFacebookAndRetrieveData]; }],
+ [StaticContentTableViewCell cellWithTitle:kLinkWithEmailPasswordText
+ action:^{ [weakSelf linkWithEmailPassword]; }],
+ [StaticContentTableViewCell cellWithTitle:kUnlinkFromGoogle
+ action:^{
+ [weakSelf unlinkFromProvider:FIRGoogleAuthProviderID];
+ }],
+ [StaticContentTableViewCell cellWithTitle:kUnlinkFromFacebook
+ action:^{
+ [weakSelf unlinkFromProvider:FIRFacebookAuthProviderID];
+ }],
+ [StaticContentTableViewCell cellWithTitle:kUnlinkFromEmailPassword
+ action:^{
+ [weakSelf unlinkFromProvider:FIREmailAuthProviderID];
+ }]
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleApp cells:@[
+ [StaticContentTableViewCell cellWithTitle:kDeleteAppTitle
+ action:^{ [weakSelf deleteApp]; }],
+ [StaticContentTableViewCell cellWithTitle:kTokenGetButtonText
+ action:^{ [weakSelf getAppTokenWithForce:NO]; }],
+ [StaticContentTableViewCell cellWithTitle:kTokenRefreshButtonText
+ action:^{ [weakSelf getAppTokenWithForce:YES]; }],
+ [StaticContentTableViewCell cellWithTitle:kTimeAuthInitTitle
+ action:^{ [weakSelf timeAuthInitialization]; }]
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleListeners cells:@[
+ [StaticContentTableViewCell cellWithTitle:kAddAuthStateListenerTitle
+ action:^{ [weakSelf addAuthStateListener]; }],
+ [StaticContentTableViewCell cellWithTitle:kRemoveAuthStateListenerTitle
+ action:^{ [weakSelf removeAuthStateListener]; }],
+ [StaticContentTableViewCell cellWithTitle:kAddIDTokenListenerTitle
+ action:^{ [weakSelf addIDTokenListener]; }],
+ [StaticContentTableViewCell cellWithTitle:kRemoveIDTokenListenerTitle
+ action:^{ [weakSelf removeIDTokenListener]; }],
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:kSectionTitleManualTests cells:@[
+ [StaticContentTableViewCell cellWithTitle:kAutoBYOAuthTitle
+ action:^{ [weakSelf automatedBYOAuth]; }],
+ [StaticContentTableViewCell cellWithTitle:kAutoSignInGoogle
+ action:^{ [weakSelf automatedSignInGoogle]; }],
+ [StaticContentTableViewCell cellWithTitle:kAutoSignInFacebook
+ action:^{ [weakSelf automatedSignInFacebook]; }],
+ [StaticContentTableViewCell cellWithTitle:kAutoSignUpEmailPassword
+ action:^{ [weakSelf automatedEmailSignUp]; }],
+ [StaticContentTableViewCell cellWithTitle:kAutoSignInAnonymously
+ action:^{ [weakSelf automatedAnonymousSignIn]; }],
+ [StaticContentTableViewCell cellWithTitle:kAutoAccountLinking
+ action:^{ [weakSelf automatedAccountLinking]; }]
+ ]]
+ ]];
+}
+
+#pragma mark - Interface Builder Actions
+
+- (IBAction)userToUseDidChange:(UISegmentedControl *)sender {
+ _useUserInMemory = (sender.selectedSegmentIndex == 1);
+}
+
+- (IBAction)memoryPlus {
+ _userInMemory = [AppManager auth].currentUser;
+ [self updateUserInfo];
+}
+
+- (IBAction)memoryClear {
+ _userInMemory = nil;
+ [self updateUserInfo];
+}
+
+/** @fn parseURL
+ @brief Parses an incoming URL into all available query items.
+ @param urlString The url to be parsed.
+ @return A dictionary of available query items in the target URL.
+ */
+static NSDictionary<NSString *, NSString *> *parseURL(NSString *urlString) {
+ NSString *linkURL = [NSURLComponents componentsWithString:urlString].query;
+ NSArray<NSString *> *URLComponents = [linkURL componentsSeparatedByString:@"&"];
+ NSMutableDictionary<NSString *, NSString *> *queryItems =
+ [[NSMutableDictionary alloc] initWithCapacity:URLComponents.count];
+ for (NSString *component in URLComponents) {
+ NSRange equalRange = [component rangeOfString:@"="];
+ if (equalRange.location != NSNotFound) {
+ NSString *queryItemKey =
+ [[component substringToIndex:equalRange.location] stringByRemovingPercentEncoding];
+ NSString *queryItemValue =
+ [[component substringFromIndex:equalRange.location + 1] stringByRemovingPercentEncoding];
+ if (queryItemKey && queryItemValue) {
+ queryItems[queryItemKey] = queryItemValue;
+ }
+ }
+ }
+ return queryItems;
+}
+
+#pragma mark public methods
+
+- (BOOL)handleIncomingLinkWithURL:(NSURL *)URL {
+ // Parse the query portion of the incoming URL.
+ NSDictionary<NSString *, NSString *> *queryItems =
+ parseURL([NSURLComponents componentsWithString:URL.absoluteString].query);
+
+ // Check that all necessary query items are available.
+ NSString *actionCode = queryItems[@"oobCode"];
+ NSString *mode = queryItems[@"mode"];
+ if (!actionCode || !mode) {
+ return NO;
+ }
+ // Handle Password Reset action.
+ if ([mode isEqualToString:kPasswordResetAction]) {
+ [self showTextInputPromptWithMessage:@"New Password:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable newPassword) {
+ if (!userPressedOK || !newPassword.length) {
+ [UIPasteboard generalPasteboard].string = actionCode;
+ return;
+ }
+ [self showSpinner:^() {
+ [[AppManager auth] confirmPasswordResetWithCode:actionCode
+ newPassword:newPassword
+ completion:^(NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"Password reset in app failed" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+ [self logSuccess:@"Password reset in app succeeded."];
+ [self showMessagePrompt:@"Password reset in app succeeded."];
+ }];
+ }];
+ }];
+ }];
+ return YES;
+ }
+ if ([mode isEqualToString:kVerifyEmailAction]) {
+ [self showMessagePromptWithTitle:@"Tap OK to verify email"
+ message:actionCode
+ showCancelButton:YES
+ completion:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK) {
+ return;
+ }
+ [self showSpinner:^() {
+ [[AppManager auth] applyActionCode:actionCode completion:^(NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"Verify email in app failed" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+ [self logSuccess:@"Verify email in app succeeded."];
+ [self showMessagePrompt:@"Verify email in app succeeded."];
+ }];
+ }];
+ }];
+ }];
+ return YES;
+ }
+ return NO;
+}
+
+#pragma mark - Actions
+
+/** @fn signInWithProvider:provider:
+ @brief Perform sign in with credential operataion, for given auth provider.
+ @param provider The auth provider.
+ @param callback The callback to continue the flow which executed this sign-in.
+ */
+- (void)signInWithProvider:(nonnull id<AuthProvider>)provider callback:(void(^)(void))callback {
+ if (!provider) {
+ [self logFailedTest:@"A valid auth provider was not provided to the signInWithProvider."];
+ return;
+ }
+ [provider getAuthCredentialWithPresentingViewController:self
+ callback:^(FIRAuthCredential *credential,
+ NSError *error) {
+ if (!credential) {
+ [self logFailedTest:@"The test needs a valid credential to continue."];
+ return;
+ }
+ [[AppManager auth] signInWithCredential:credential completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in with provider failed" error:error];
+ [self logFailedTest:@"Sign-in should succeed"];
+ return;
+ } else {
+ [self logSuccess:@"sign-in with provider succeeded."];
+ callback();
+ }
+ }];
+ }];
+}
+
+/** @fn automatedSignInGoogle
+ @brief Automatically executes the manual test for sign-in with Google.
+ */
+- (void)automatedSignInGoogle {
+ [self showMessagePromptWithTitle:kAutoSignInGoogle
+ message:kSafariGoogleSignOutMessagePrompt
+ showCancelButton:NO
+ completion:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ FIRAuth *auth = [AppManager auth];
+ if (!auth) {
+ [self logFailedTest:@"Could not obtain auth object."];
+ return;
+ }
+ [auth signOut:NULL];
+ [self log:@"INITIATING AUTOMATED MANUAL TEST FOR GOOGLE SIGN IN:"];
+ [self signInWithProvider:[AuthProviders google] callback:^{
+ [self logSuccess:@"sign-in with Google provider succeeded."];
+ [auth signOut:NULL];
+ [self signInWithProvider:[AuthProviders google] callback:^{
+ [self logSuccess:@"sign-in with Google provider succeeded."];
+ [self updateEmailPasswordWithCompletion:^{
+ [self automatedSignInGoogleDisplayNamePhotoURL];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn automatedSignInGoogleDisplayNamePhotoURL
+ @brief Automatically executes the manual test for setting email and password for sign in with
+ Google.
+ */
+- (void)automatedSignInGoogleDisplayNamePhotoURL {
+ [self signInWithProvider:[AuthProviders google] callback:^{
+ [self updateDisplayNameAndPhotoURlWithCompletion:^{
+ [self log:@"FINISHED AUTOMATED MANUAL TEST FOR SIGN-IN WITH GOOGlE."];
+ [self reloadUser];
+ }];
+ }];
+}
+
+/** @fn automatedSignInFacebook
+ @brief Automatically executes the manual test for sign-in with Facebook.
+ */
+- (void)automatedSignInFacebook {
+ [self showMessagePromptWithTitle:kAutoSignInFacebook
+ message:kSafariFacebookSignOutMessagePrompt
+ showCancelButton:NO
+ completion:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ FIRAuth *auth = [AppManager auth];
+ if (!auth) {
+ [self logFailedTest:@"Could not obtain auth object."];
+ return;
+ }
+ [auth signOut:NULL];
+ [self log:@"INITIATING AUTOMATED MANUAL TEST FOR FACEBOOK SIGN IN:"];
+ [self signInWithProvider:[AuthProviders facebook] callback:^{
+ [self logSuccess:@"sign-in with Facebook provider succeeded."];
+ [auth signOut:NULL];
+ [self signInWithProvider:[AuthProviders facebook] callback:^{
+ [self logSuccess:@"sign-in with Facebook provider succeeded."];
+ [self updateEmailPasswordWithCompletion:^{
+ [self automatedSignInFacebookDisplayNamePhotoURL];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn automatedEmailSignUp
+ @brief Automatically executes the manual test for sign-up with email/password.
+ */
+- (void)automatedEmailSignUp {
+ [self log:@"INITIATING AUTOMATED MANUAL TEST FOR FACEBOOK SIGN IN:"];
+ FIRAuth *auth = [AppManager auth];
+ if (!auth) {
+ [self logFailedTest:@"Could not obtain auth object."];
+ return;
+ }
+ [self signUpNewEmail:kFakeEmail password:kFakePassword callback:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailedTest: @" Email/Password Account account creation failed"];
+ return;
+ }
+ [auth signOut:NULL];
+ FIRAuthCredential *credential = [FIREmailAuthProvider credentialWithEmail:kFakeEmail
+ password:kFakePassword];
+ [auth signInWithCredential:credential
+ completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in with Email/Password failed" error:error];
+ [self logFailedTest:@"sign-in with Email/Password should succeed."];
+ return;
+ }
+ [self logSuccess:@"sign-in with Email/Password succeeded."];
+ [self log:@"FINISHED AUTOMATED MANUAL TEST FOR SIGN-IN WITH EMAIL/PASSWORD."];
+ // Delete the user so that we can reuse the fake email address for subsequent tests.
+ [auth.currentUser deleteWithCompletion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"Failed to delete user" error:error];
+ [self logFailedTest:@"Deleting a user that was recently signed-in should succeed."];
+ return;
+ }
+ [self logSuccess:@"User deleted."];
+ }];
+ }];
+ }];
+}
+
+/** @fn automatedAnonymousSignIn
+ @brief Automatically executes the manual test for sign-in anonymously.
+ */
+- (void)automatedAnonymousSignIn {
+ [self log:@"INITIATING AUTOMATED MANUAL TEST FOR ANONYMOUS SIGN IN:"];
+ FIRAuth *auth = [AppManager auth];
+ if (!auth) {
+ [self logFailedTest:@"Could not obtain auth object."];
+ return;
+ }
+ [auth signOut:NULL];
+ [self signInAnonymouslyWithCallback:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (user) {
+ NSString *anonymousUID = user.uid;
+ [self signInAnonymouslyWithCallback:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (![user.uid isEqual:anonymousUID]) {
+ [self logFailedTest:@"Consecutive anonymous sign-ins should yeild the same User ID"];
+ return;
+ }
+ [self log:@"FINISHED AUTOMATED MANUAL TEST FOR ANONYMOUS SIGN IN."];
+ }];
+ }
+ }];
+}
+
+/** @fn signInAnonymouslyWithCallback:
+ @brief Performs anonymous sign in and then executes callback.
+ @param callback The callback to be executed.
+ */
+- (void)signInAnonymouslyWithCallback:(nullable FIRAuthResultCallback)callback {
+ FIRAuth *auth = [AppManager auth];
+ if (!auth) {
+ [self logFailedTest:@"Could not obtain auth object."];
+ return;
+ }
+ [auth signInAnonymouslyWithCompletion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in anonymously failed" error:error];
+ [self logFailedTest:@"Recently signed out user should be able to sign in anonymously."];
+ return;
+ }
+ [self logSuccess:@"sign-in anonymously succeeded."];
+ if (callback) {
+ callback(user, nil);
+ }
+ }];
+}
+
+/** @fn automatedAccountLinking
+ @brief Automatically executes the manual test for account linking.
+ */
+- (void)automatedAccountLinking {
+ [self log:@"INITIATING AUTOMATED MANUAL TEST FOR ACCOUNT LINKING:"];
+ FIRAuth *auth = [AppManager auth];
+ if (!auth) {
+ [self logFailedTest:@"Could not obtain auth object."];
+ return;
+ }
+ [auth signOut:NULL];
+ [self signInAnonymouslyWithCallback:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (user) {
+ NSString *anonymousUID = user.uid;
+ [self showMessagePromptWithTitle:@"Sign In Instructions"
+ message:kUnlinkAccountMessagePrompt
+ showCancelButton:NO
+ completion:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ [[AuthProviders google]
+ getAuthCredentialWithPresentingViewController:self
+ callback:^(FIRAuthCredential *credential,
+ NSError *error) {
+ if (credential) {
+ [user linkWithCredential:credential completion:^(FIRUser *user, NSError *error) {
+ if (error) {
+ [self logFailure:@"link auth provider failed" error:error];
+ [self logFailedTest:@"Account needs to be linked to complete the test."];
+ return;
+ }
+ [self logSuccess:@"link auth provider succeeded."];
+ if (user.isAnonymous) {
+ [self logFailure:@"link auth provider failed, user still anonymous" error:error];
+ [self logFailedTest:@"Account needs to be linked to complete the test."];
+ }
+ if (![user.uid isEqual:anonymousUID]){
+ [self logFailedTest:@"link auth provider failed, UID's are different. Make sure "
+ "you link an account that has NOT been Linked nor Signed-In before."];
+ return;
+ }
+ [self log:@"FINISHED AUTOMATED MANUAL TEST FOR ACCOUNT LINKING."];
+ }];
+ }
+ }];
+ }];
+ }
+ }];
+}
+
+/** @fn automatedSignInFacebookDisplayNamePhotoURL
+ @brief Automatically executes the manual test for setting email and password for sign-in with
+ Facebook.
+ */
+- (void)automatedSignInFacebookDisplayNamePhotoURL {
+ [self signInWithProvider:[AuthProviders facebook] callback:^{
+ [self updateDisplayNameAndPhotoURlWithCompletion:^{
+ [self log:@"FINISHED AUTOMATED MANUAL TEST FOR SIGN-IN WITH FACEBOOK."];
+ [self reloadUser];
+ }];
+ }];
+}
+
+/** @fn automatedBYOauth
+ @brief Automatically executes the manual test for BYOAuth.
+ */
+- (void)automatedBYOAuth {
+ [self log:@"INITIATING AUTOMATED MANUAL TEST FOR BYOAUTH:"];
+ [self showSpinner:^{
+ NSError *error;
+ NSString *customToken = [NSString stringWithContentsOfURL:[NSURL URLWithString:kCustomTokenUrl]
+ encoding:NSUTF8StringEncoding
+ error:&error];
+ NSString *expiredCustomToken =
+ [NSString stringWithContentsOfURL:[NSURL URLWithString:kExpiredCustomTokenUrl]
+ encoding:NSUTF8StringEncoding
+ error:&error];
+ [self hideSpinner:^{
+ if (error) {
+ [self log:@"There was an error retrieving the custom token."];
+ return;
+ }
+ FIRAuth *auth = [AppManager auth];
+ [auth signInWithCustomToken:customToken
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in with custom token failed" error:error];
+ [self logFailedTest:@"A fresh custom token should succeed in signing-in."];
+ return;
+ }
+ [self logSuccess:@"sign-in with custom token succeeded."];
+ [auth.currentUser getIDTokenForcingRefresh:NO
+ completion:^(NSString *_Nullable token,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"refresh token failed" error:error];
+ [self logFailedTest:@"Refresh token should be available."];
+ return;
+ }
+ [self logSuccess:@"refresh token succeeded."];
+ [auth signOut:NULL];
+ [auth signInWithCustomToken:expiredCustomToken
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (!error) {
+ [self logSuccess:@"sign-in with custom token succeeded."];
+ [self logFailedTest:@"sign-in with an expired custom token should NOT succeed."];
+ return;
+ }
+ [self logFailure:@"sign-in with custom token failed" error:error];
+ [auth signInWithCustomToken:kInvalidCustomToken
+ completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (!error) {
+ [self logSuccess:@"sign-in with custom token succeeded."];
+ [self logFailedTest:@"sign-in with an invalid custom token should NOT succeed."];
+ return;
+ }
+ [self logFailure:@"sign-in with custom token failed" error:error];
+ //next step of automated test.
+ [self automatedBYOAuthEmailPassword];
+ }];
+ }];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn automatedBYOAuthEmailPassword
+ @brief Automatically executes the manual test for setting email and password in BYOAuth.
+ */
+- (void)automatedBYOAuthEmailPassword {
+ [self showSpinner:^{
+ NSError *error;
+ NSString *customToken = [NSString stringWithContentsOfURL:[NSURL URLWithString:kCustomTokenUrl]
+ encoding:NSUTF8StringEncoding
+ error:&error];
+ [self hideSpinner:^{
+ if (error) {
+ [self log:@"There was an error retrieving the custom token."];
+ return;
+ }
+ [[AppManager auth] signInWithCustomToken:customToken
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in with custom token failed" error:error];
+ [self logFailedTest:@"A fresh custom token should succeed in signing-in."];
+ return;
+ }
+ [self logSuccess:@"sign-in with custom token succeeded."];
+ [self updateEmailPasswordWithCompletion:^{
+ [self automatedBYOAuthDisplayNameAndPhotoURl];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn automatedBYOAuthDisplayNameAndPhotoURl
+ @brief Automatically executes the manual test for setting display name and photo url in BYOAuth.
+ */
+- (void)automatedBYOAuthDisplayNameAndPhotoURl {
+ [self showSpinner:^{
+ NSError *error;
+ NSString *customToken = [NSString stringWithContentsOfURL:[NSURL URLWithString:kCustomTokenUrl]
+ encoding:NSUTF8StringEncoding
+ error:&error];
+ [self hideSpinner:^{
+ if (error) {
+ [self log:@"There was an error retrieving the custom token."];
+ return;
+ }
+ [[AppManager auth] signInWithCustomToken:customToken
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in with custom token failed" error:error];
+ [self logFailedTest:@"A fresh custom token should succeed in signing-in."];
+ return;
+ }
+ [self logSuccess:@"sign-in with custom token succeeded."];
+ [self updateDisplayNameAndPhotoURlWithCompletion:^{
+ [self log:@"FINISHED AUTOMATED MANUAL TEST FOR BYOAUTH."];
+ [self reloadUser];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn updateEmailPasswordWithCompletion:
+ @brief Updates email and password for automatic manual tests, and signs user in with new email
+ and password.
+ @param completion The completion block to continue the automatic test flow.
+ */
+- (void)updateEmailPasswordWithCompletion:(void(^)(void))completion {
+ FIRAuth *auth = [AppManager auth];
+ [auth.currentUser updateEmail:kFakeEmail completion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"update email failed" error:error];
+ [self logFailedTest:@"Update email should succeed when properly signed-in."];
+ return;
+ }
+ [self logSuccess:@"update email succeeded."];
+ [auth.currentUser updatePassword:kFakePassword completion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"update password failed" error:error];
+ [self logFailedTest:@"Update password should succeed when properly signed-in."];
+ return;
+ }
+ [self logSuccess:@"update password succeeded."];
+ [auth signOut:NULL];
+ FIRAuthCredential *credential =
+ [FIREmailAuthProvider credentialWithEmail:kFakeEmail password:kFakePassword];
+ [auth signInWithCredential:credential
+ completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in with Email/Password failed" error:error];
+ [self logFailedTest:@"sign-in with Email/Password should succeed."];
+ return;
+ }
+ [self logSuccess:@"sign-in with Email/Password succeeded."];
+ // Delete the user so that we can reuse the fake email address for subsequent tests.
+ [auth.currentUser deleteWithCompletion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"Failed to delete user." error:error];
+ [self logFailedTest:@"Deleting a user that was recently signed-in should succeed"];
+ return;
+ }
+ [self logSuccess:@"User deleted."];
+ completion();
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn updateDisplayNameAndPhotoURlWithCompletion:
+ @brief Automatically executes the manual test for setting displayName and photoUrl.
+ @param completion The completion block to continue the automatic test flow.
+ */
+- (void)updateDisplayNameAndPhotoURlWithCompletion:(void(^)(void))completion {
+ FIRAuth *auth = [AppManager auth];
+ FIRUserProfileChangeRequest *changeRequest = [auth.currentUser profileChangeRequest];
+ changeRequest.photoURL = [NSURL URLWithString:kFakeDisplayPhotoUrl];
+ [changeRequest commitChangesWithCompletion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"set photo URL failed" error:error];
+ [self logFailedTest:@"Change photo Url should succeed when signed-in."];
+ return;
+ }
+ [self logSuccess:@"set PhotoURL succeeded."];
+ FIRUserProfileChangeRequest *changeRequest = [auth.currentUser profileChangeRequest];
+ changeRequest.displayName = kFakeDisplayName;
+ [changeRequest commitChangesWithCompletion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"set display name failed" error:error];
+ [self logFailedTest:@"Change display name should succeed when signed-in."];
+ return;
+ }
+ [self logSuccess:@"set display name succeeded."];
+ completion();
+ }];
+ }];
+}
+
+/** @fn addAuthStateListener
+ @brief Adds an auth state did change listener (block).
+ */
+- (void)addAuthStateListener {
+ __weak typeof(self) weakSelf = self;
+ NSUInteger index = _authStateDidChangeListeners.count;
+ [self log:[NSString stringWithFormat:@"Auth State Did Change Listener #%lu was added.",
+ (unsigned long)index]];
+ FIRAuthStateDidChangeListenerHandle handle =
+ [[AppManager auth] addAuthStateDidChangeListener:^(FIRAuth *_Nonnull auth,
+ FIRUser *_Nullable user) {
+ [weakSelf log:[NSString stringWithFormat:
+ @"Auth State Did Change Listener #%lu was invoked on user '%@'.",
+ (unsigned long)index, user.uid]];
+ }];
+ [_authStateDidChangeListeners addObject:handle];
+}
+
+/** @fn removeAuthStateListener
+ @brief Removes an auth state did change listener (block).
+ */
+- (void)removeAuthStateListener {
+ if (!_authStateDidChangeListeners.count) {
+ [self log:@"No remaining Auth State Did Change Listeners."];
+ return;
+ }
+ NSUInteger index = _authStateDidChangeListeners.count - 1;
+ FIRAuthStateDidChangeListenerHandle handle = _authStateDidChangeListeners.lastObject;
+ [[AppManager auth] removeAuthStateDidChangeListener:handle];
+ [_authStateDidChangeListeners removeObject:handle];
+ NSString *logString =
+ [NSString stringWithFormat:@"Auth State Did Change Listener #%lu was removed.",
+ (unsigned long)index];
+ [self log:logString];
+}
+
+/** @fn addIDTokenListener
+ @brief Adds an ID token did change listener (block).
+ */
+- (void)addIDTokenListener {
+ __weak typeof(self) weakSelf = self;
+ NSUInteger index = _IDTokenDidChangeListeners.count;
+ [self log:[NSString stringWithFormat:@"ID Token Did Change Listener #%lu was added.",
+ (unsigned long)index]];
+ FIRIDTokenDidChangeListenerHandle handle =
+ [[AppManager auth] addIDTokenDidChangeListener:^(FIRAuth *_Nonnull auth,
+ FIRUser *_Nullable user) {
+ [weakSelf log:[NSString stringWithFormat:
+ @"ID Token Did Change Listener #%lu was invoked on user '%@'.",
+ (unsigned long)index, user.uid]];
+ }];
+ [_IDTokenDidChangeListeners addObject:handle];
+}
+
+/** @fn removeIDTokenListener
+ @brief Removes an ID token did change listener (block).
+ */
+- (void)removeIDTokenListener {
+ if (!_IDTokenDidChangeListeners.count) {
+ [self log:@"No remaining ID Token Did Change Listeners."];
+ return;
+ }
+ NSUInteger index = _IDTokenDidChangeListeners.count - 1;
+ FIRIDTokenDidChangeListenerHandle handle = _IDTokenDidChangeListeners.lastObject;
+ [[AppManager auth] removeIDTokenDidChangeListener:handle];
+ [_IDTokenDidChangeListeners removeObject:handle];
+ NSString *logString =
+ [NSString stringWithFormat:@"ID Token Did Change Listener #%lu was removed.",
+ (unsigned long)index];
+ [self log:logString];
+}
+
+/** @fn log:
+ @brief Prints a log message to the sample app console.
+ @param string The string to add to the console.
+ */
+- (void)log:(NSString *)string {
+ NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
+ dateFormatter.dateFormat = @"yyyy-MM-dd HH:mm:ss";
+ NSString *date = [dateFormatter stringFromDate:[NSDate date]];
+ if (!_consoleString) {
+ _consoleString = [NSMutableString string];
+ }
+ [_consoleString appendString:[NSString stringWithFormat:@"%@ %@\n", date, string]];
+ _consoleTextView.text = _consoleString;
+
+ CGRect targetRect = CGRectMake(0, _consoleTextView.contentSize.height - 1, 1, 1);
+ [_consoleTextView scrollRectToVisible:targetRect animated:YES];
+}
+
+/** @fn logSuccess:
+ @brief Wraps a string into a succeful log message format.
+ @param string Part of the log message.
+ @remarks The @string parameter should be a string ending with a period, as it is the end of the
+ log message.
+ */
+- (void)logSuccess:(NSString *)string {
+ [self log:[NSString stringWithFormat:@"SUCCESS: %@", string]];
+}
+
+/** @fn logFailure:
+ @brief Wraps a string into a failed log message format.
+ @param string Part of the message to wrap.
+ @remarks The @string parameter should be a string that NEVER ends with a period, as it is
+ guaranteed not to be the last part fo the log message.
+ */
+- (void)logFailure:(NSString *)string error:(NSError *) error {
+ NSString *message =
+ [NSString stringWithFormat:@"FAILURE: %@ Error Description: %@.", string, error.description];
+ [self log:message];
+}
+
+/** @fn logTestFailed
+ @brief Logs test failure to the console.
+ @param reason The reason why the test is considered a failure.
+ @remarks The calling method should immediately terminate after invoking this method i.e by
+ return statement or end of fucntions. The @reason parameter should be a string ending with a
+ period, as it is the end of the log message.
+ */
+- (void)logFailedTest:( NSString *_Nonnull )reason {
+ [self log:[NSString stringWithFormat:@"FAILIURE: TEST FAILED - %@", reason]];
+}
+
+/** @fn presentSettings
+ @brief Invoked when the settings row is pressed.
+ */
+- (void)presentSettings {
+ SettingsViewController *settingsViewController = [[SettingsViewController alloc]
+ initWithNibName:NSStringFromClass([SettingsViewController class])
+ bundle:nil];
+ [self presentViewController:settingsViewController animated:YES completion:nil];
+}
+
+/** @fn presentUserInfo
+ @brief Invoked when the user info row is pressed.
+ */
+- (void)presentUserInfo {
+ UserInfoViewController *userInfoViewController =
+ [[UserInfoViewController alloc] initWithUser:[AppManager auth].currentUser];
+ [self presentViewController:userInfoViewController animated:YES completion:nil];
+}
+
+/** @fn presentUserInMemoryInfo
+ @brief Invoked when the user in memory info row is pressed.
+ */
+- (void)presentUserInMemoryInfo {
+ UserInfoViewController *userInfoViewController =
+ [[UserInfoViewController alloc] initWithUser:_userInMemory];
+ [self presentViewController:userInfoViewController animated:YES completion:nil];
+}
+
+/** @fn signInGoogle
+ @brief Invoked when "Sign in with Google" row is pressed.
+ */
+- (void)signInGoogle {
+ [self signinWithProvider:[AuthProviders google] retrieveData:NO];
+}
+
+/** @fn signInGoogleAndRetrieveData
+ @brief Invoked when "Sign in with Google and retrieve data" row is pressed.
+ */
+- (void)signInGoogleAndRetrieveData {
+ [self signinWithProvider:[AuthProviders google] retrieveData:YES];
+}
+
+/** @fn signInFacebook
+ @brief Invoked when "Sign in with Facebook" row is pressed.
+ */
+- (void)signInFacebook {
+ [self signinWithProvider:[AuthProviders facebook] retrieveData:NO];
+}
+
+/** @fn signInFacebookAndRetrieveData
+ @brief Invoked when "Sign in with Facebook and retrieve data" row is pressed.
+ */
+- (void)signInFacebookAndRetrieveData {
+ [self signinWithProvider:[AuthProviders facebook] retrieveData:YES];
+}
+
+/** @fn signInEmailPassword
+ @brief Invoked when "sign in with Email/Password" row is pressed.
+ */
+- (void)signInEmailPassword {
+ [self showTextInputPromptWithMessage:@"Email Address:"
+ keyboardType:UIKeyboardTypeEmailAddress
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable email) {
+ if (!userPressedOK || !email.length) {
+ return;
+ }
+ [self showTextInputPromptWithMessage:@"Password:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable password) {
+ if (!userPressedOK) {
+ return;
+ }
+ FIRAuthCredential *credential =
+ [FIREmailAuthProvider credentialWithEmail:email
+ password:password];
+ [self showSpinner:^{
+ [[AppManager auth] signInWithCredential:credential
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"sign-in with Email/Password failed" error:error];
+ } else {
+ [self logSuccess:@"sign-in with Email/Password succeeded."];
+ }
+ [self showTypicalUIForUserUpdateResultsWithTitle:@"Sign-In Error" error:error];
+ }];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn signUpNewEmail
+ @brief Invoked if sign-in is attempted with new email/password.
+ @remarks Should only be called if @c FIRAuthErrorCodeInvalidEmail is encountered on attepmt to
+ sign in with email/password.
+ */
+- (void)signUpNewEmail:(NSString *)email
+ password:(NSString *)password
+ callback:(nullable FIRAuthResultCallback)callback {
+ [[AppManager auth] createUserWithEmail:email
+ password:password
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-up with Email/Password failed" error:error];
+ if (callback) {
+ callback(nil, error);
+ }
+ } else {
+ [self logSuccess:@"sign-up with Email/Password succeeded."];
+ if (callback) {
+ callback(user, nil);
+ }
+ }
+ [self showTypicalUIForUserUpdateResultsWithTitle:@"Sign-In" error:error];
+ }];
+}
+
+/** @fn signInWithCustomToken
+ @brief Signs the user in using a manually-entered custom token.
+ */
+- (void)signInWithCustomToken {
+ CustomTokenDataEntryViewControllerCompletion action =
+ ^(BOOL cancelled, NSString *_Nullable userEnteredTokenText) {
+ if (cancelled) {
+ [self log:@"CANCELLED:sign-in with custom token cancelled."];
+ return;
+ }
+
+ [self doSignInWithCustomToken:userEnteredTokenText];
+ };
+ CustomTokenDataEntryViewController *dataEntryViewController =
+ [[CustomTokenDataEntryViewController alloc] initWithCompletion:action];
+ [self presentViewController:dataEntryViewController animated:YES completion:nil];
+}
+
+/** @fn signOut
+ @brief Signs the user out.
+ */
+- (void)signOut {
+ [[AuthProviders google] signOut];
+ [[AuthProviders facebook] signOut];
+ [[AppManager auth] signOut:NULL];
+}
+
+/** @fn deleteAccount
+ @brief Deletes the current user account and signs the user out.
+ */
+- (void)deleteAccount {
+ FIRUser *user = [self user];
+ [user deleteWithCompletion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"delete account failed" error:error];
+ }
+ [self showTypicalUIForUserUpdateResultsWithTitle:kDeleteUserText error:error];
+ }];
+}
+
+/** @fn reauthenticateGoogle
+ @brief Asks the user to reauthenticate with Google.
+ */
+- (void)reauthenticateGoogle {
+ [self reauthenticate:[AuthProviders google] retrieveData:NO];
+}
+
+/** @fn reauthenticateGoogleAndRetrieveData
+ @brief Asks the user to reauthenticate with Google and retrieve additional data.
+ */
+- (void)reauthenticateGoogleAndRetrieveData {
+ [self reauthenticate:[AuthProviders google] retrieveData:YES];
+}
+
+/** @fn reauthenticateFB
+ @brief Asks the user to reauthenticate with Facebook.
+ */
+- (void)reauthenticateFB {
+ [self reauthenticate:[AuthProviders facebook] retrieveData:NO];
+}
+
+/** @fn reauthenticateFBAndRetrieveData
+ @brief Asks the user to reauthenticate with Facebook and retrieve additional data.
+ */
+- (void)reauthenticateFBAndRetrieveData {
+ [self reauthenticate:[AuthProviders facebook] retrieveData:YES];
+}
+
+/** @fn reauthenticateEmailPassword
+ @brief Asks the user to reauthenticate with email/password.
+ */
+- (void)reauthenticateEmailPassword {
+ FIRUser *user = [self user];
+ if (!user) {
+ NSString *title = @"Missing User";
+ NSString *message = @"There is no signed-in email/password user.";
+ [self showMessagePromptWithTitle:title message:message showCancelButton:NO completion:nil];
+ return;
+ }
+ [self showEmailPasswordDialogWithCompletion:^(FIRAuthCredential *credential) {
+ [self showSpinner:^{
+ [[self user] reauthenticateWithCredential:credential
+ completion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"reauthicate with email/password failed" error:error];
+ } else {
+ [self logSuccess:@"reauthicate with email/password succeeded."];
+ }
+ [self hideSpinner:^{
+ [self showTypicalUIForUserUpdateResultsWithTitle:kReauthenticateEmailText error:error];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn reauthenticate:
+ @brief Reauthenticates user.
+ @param authProvider The auth provider to use for reauthentication.
+ @param retrieveData Defines if additional provider data should be read.
+ */
+- (void)reauthenticate:(id<AuthProvider>)authProvider retrieveData:(BOOL)retrieveData {
+ FIRUser *user = [self user];
+ if (!user) {
+ NSString *provider = @"Firebase";
+ if ([authProvider isKindOfClass:[GoogleAuthProvider class]]) {
+ provider = @"Google";
+ } else if ([authProvider isKindOfClass:[FacebookAuthProvider class]]) {
+ provider = @"Facebook";
+ }
+ NSString *title = @"Missing User";
+ NSString *message =
+ [NSString stringWithFormat:@"There is no signed-in %@ user.", provider];
+ [self showMessagePromptWithTitle:title message:message showCancelButton:NO completion:nil];
+ return;
+ }
+ [authProvider getAuthCredentialWithPresentingViewController:self
+ callback:^(FIRAuthCredential *credential,
+ NSError *error) {
+ if (credential) {
+ FIRAuthDataResultCallback completion = ^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"reauthenticate operation failed" error:error];
+ } else {
+ [self logSuccess:@"reauthenticate operation succeeded."];
+ }
+ if (authResult.additionalUserInfo) {
+ [self logSuccess:
+ [NSString stringWithFormat:@"%@", authResult.additionalUserInfo.profile]];
+ }
+ [self showTypicalUIForUserUpdateResultsWithTitle:@"Reauthenticate" error:error];
+ };
+ FIRUserProfileChangeCallback callback = ^(NSError *_Nullable error) {
+ completion(nil, error);
+ };
+ if (retrieveData) {
+ [user reauthenticateAndRetrieveDataWithCredential:credential completion:completion];
+ } else {
+ [user reauthenticateWithCredential:credential completion:callback];
+ }
+ }
+ }];
+}
+
+/** @fn signinWithProvider:
+ @brief Signs in the user with provided auth provider.
+ @param authProvider The auth provider to use for sign-in.
+ @param retrieveData Defines if additional provider data should be read.
+ */
+- (void)signinWithProvider:(id<AuthProvider>)authProvider retrieveData:(BOOL)retrieveData {
+ FIRAuth *auth = [AppManager auth];
+ if (!auth) {
+ return;
+ }
+ [authProvider getAuthCredentialWithPresentingViewController:self
+ callback:^(FIRAuthCredential *credential,
+ NSError *error) {
+ if (credential) {
+ FIRAuthDataResultCallback completion = ^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in with provider failed" error:error];
+ } else {
+ [self logSuccess:@"sign-in with provider succeeded."];
+ }
+ if (authResult.additionalUserInfo) {
+ [self logSuccess:
+ [NSString stringWithFormat:@"%@", authResult.additionalUserInfo.profile]];
+ }
+ [self showTypicalUIForUserUpdateResultsWithTitle:@"Sign-In" error:error];
+ };
+ FIRAuthResultCallback callback = ^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ completion(nil, error);
+ };
+ if (retrieveData) {
+ [auth signInAndRetrieveDataWithCredential:credential completion:completion];
+ } else {
+ [auth signInWithCredential:credential completion:callback];
+ }
+ }
+ }];
+}
+
+/** @fn tokenCallback
+ @return A callback block to show the token.
+ */
+- (FIRAuthTokenCallback)tokenCallback {
+ return ^(NSString *_Nullable token, NSError *_Nullable error) {
+ if (error) {
+ [self showMessagePromptWithTitle:kTokenRefreshErrorAlertTitle
+ message:error.localizedDescription
+ showCancelButton:NO
+ completion:nil];
+ [self logFailure:@"refresh token failed" error:error];
+ return;
+ }
+ [self logSuccess:@"refresh token succeeded."];
+ [self showMessagePromptWithTitle:kTokenRefreshedAlertTitle
+ message:token
+ showCancelButton:NO
+ completion:nil];
+ };
+}
+
+/** @fn getUserTokenWithForce:
+ @brief Gets the token from @c FIRUser , optionally a refreshed one.
+ @param force Whether the refresh is forced or not.
+ */
+- (void)getUserTokenWithForce:(BOOL)force {
+ [[self user] getIDTokenForcingRefresh:force completion:[self tokenCallback]];
+}
+
+/** @fn getAppTokenWithForce:
+ @brief Gets the token from @c FIRApp , optionally a refreshed one.
+ @param force Whether the refresh is forced or not.
+ */
+- (void)getAppTokenWithForce:(BOOL)force {
+ [[FIRApp defaultApp] getTokenForcingRefresh:force withCallback:[self tokenCallback]];
+}
+
+/** @fn setDisplayName
+ @brief Changes the display name of the current user.
+ */
+- (void)setDisplayName {
+ [self showTextInputPromptWithMessage:@"Display Name:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK || !userInput.length) {
+ return;
+ }
+ [self showSpinner:^{
+ FIRUserProfileChangeRequest *changeRequest = [[self user] profileChangeRequest];
+ changeRequest.displayName = userInput;
+ [changeRequest commitChangesWithCompletion:^(NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"set display name failed" error:error];
+ } else {
+ [self logSuccess:@"set display name succeeded."];
+ }
+ [self showTypicalUIForUserUpdateResultsWithTitle:kSetDisplayNameTitle error:error];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn setPhotoURL
+ @brief Changes the photo url of the current user.
+ */
+- (void)setPhotoURL {
+ [self showTextInputPromptWithMessage:@"Photo URL:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK || !userInput.length) {
+ return;
+ }
+
+ [self showSpinner:^{
+ FIRUserProfileChangeRequest *changeRequest = [[self user] profileChangeRequest];
+ changeRequest.photoURL = [NSURL URLWithString:userInput];
+ [changeRequest commitChangesWithCompletion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"set photo URL failed" error:error];
+ } else {
+ [self logSuccess:@"set Photo URL succeeded."];
+ }
+ [self hideSpinner:^{
+ [self showTypicalUIForUserUpdateResultsWithTitle:kSetPhotoURLText error:error];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn reloadUser
+ @brief Reloads the user from server.
+ */
+- (void)reloadUser {
+ [self showSpinner:^() {
+ [[self user] reloadWithCompletion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"reload user failed" error:error];
+ } else {
+ [self logSuccess:@"reload user succeeded."];
+ }
+ [self hideSpinner:^() {
+ [self showTypicalUIForUserUpdateResultsWithTitle:kReloadText error:error];
+ }];
+ }];
+ }];
+}
+
+/** @fn linkWithAuthProvider:retrieveData:
+ @brief Asks the user to sign in with an auth provider and link the current user with it.
+ @param authProvider The auth provider to sign in and link with.
+ @param retrieveData Defines if additional provider data should be read.
+ */
+- (void)linkWithAuthProvider:(id<AuthProvider>)authProvider retrieveData:(BOOL)retrieveData {
+ FIRUser *user = [self user];
+ if (!user) {
+ return;
+ }
+ [authProvider getAuthCredentialWithPresentingViewController:self
+ callback:^(FIRAuthCredential *credential,
+ NSError *error) {
+ if (credential) {
+ FIRAuthDataResultCallback completion = ^(FIRAuthDataResult *_Nullable authResult,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"link auth provider failed" error:error];
+ } else {
+ [self logSuccess:@"link auth provider succeeded."];
+ }
+ if (authResult.additionalUserInfo) {
+ [self logSuccess:
+ [NSString stringWithFormat:@"%@", authResult.additionalUserInfo.profile]];
+ }
+ if (retrieveData) {
+ [self showUIForAuthDataResultWithResult:authResult error:error];
+ } else {
+ [self showTypicalUIForUserUpdateResultsWithTitle:@"Link Account" error:error];
+ }
+ };
+ FIRAuthResultCallback callback = ^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ completion(nil, error);
+ };
+ if (retrieveData) {
+ [user linkAndRetrieveDataWithCredential:credential completion:completion];
+ } else {
+ [user linkWithCredential:credential completion:callback];
+ }
+ }
+ }];
+}
+
+/** @fn linkWithGoogle
+ @brief Asks the user to sign in with Google and link the current user with this Google account.
+ */
+- (void)linkWithGoogle {
+ [self linkWithAuthProvider:[AuthProviders google] retrieveData:NO];
+}
+
+/** @fn linkWithGoogleAndRetrieveData
+ @brief Asks the user to sign in with Google and link the current user with this Google account
+ and retrieve additional data.
+ */
+- (void)linkWithGoogleAndRetrieveData {
+ [self linkWithAuthProvider:[AuthProviders google] retrieveData:YES];
+}
+
+/** @fn linkWithFacebook
+ @brief Asks the user to sign in with Facebook and link the current user with this Facebook
+ account.
+ */
+- (void)linkWithFacebook {
+ [self linkWithAuthProvider:[AuthProviders facebook] retrieveData:NO];
+}
+
+/** @fn linkWithFacebookAndRetrieveData
+ @brief Asks the user to sign in with Facebook and link the current user with this Facebook
+ account and retrieve additional data.
+ */
+- (void)linkWithFacebookAndRetrieveData {
+ [self linkWithAuthProvider:[AuthProviders facebook] retrieveData:YES];
+}
+
+/** @fn linkWithEmailPassword
+ @brief Asks the user to sign in with Facebook and link the current user with this Facebook
+ account.
+ */
+- (void)linkWithEmailPassword {
+ [self showEmailPasswordDialogWithCompletion:^(FIRAuthCredential *credential) {
+ [self showSpinner:^{
+ [[self user] linkWithCredential:credential
+ completion:^(FIRUser *user, NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"link Email/Password failed" error:error];
+ } else {
+ [self logSuccess:@"link Email/Password succeeded."];
+ }
+ [self hideSpinner:^{
+ [self showTypicalUIForUserUpdateResultsWithTitle:kLinkWithEmailPasswordText error:error];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn showEmailPasswordDialogWithCompletion:
+ @brief shows email/password input dialog.
+ @param completion The completion block that will do some operation on the credential email
+ /passwowrd credential obtained.
+ */
+- (void)showEmailPasswordDialogWithCompletion:(ShowEmailPasswordDialogCompletion)completion {
+ [self showTextInputPromptWithMessage:@"Email Address:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable email) {
+ if (!userPressedOK || !email.length) {
+ return;
+ }
+ [self showTextInputPromptWithMessage:@"Password:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable password) {
+ if (!userPressedOK || !password.length) {
+ return;
+ }
+
+ FIRAuthCredential *credential = [FIREmailAuthProvider credentialWithEmail:email
+ password:password];
+ completion(credential);
+ }];
+ }];
+}
+
+/** @fn unlinkFromProvider:
+ @brief Unlinks the current user from the provider with the specified provider ID.
+ @param provider The provider ID of the provider to unlink the current user's account from.
+ */
+- (void)unlinkFromProvider:(NSString *)provider {
+ [[self user] unlinkFromProvider:provider
+ completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"unlink auth provider failed" error:error];
+ } else {
+ [self logSuccess:@"unlink auth provider succeeded."];
+ }
+ [self showTypicalUIForUserUpdateResultsWithTitle:kUnlinkTitle error:error];
+ }];
+}
+
+/** @fn getProvidersForEmail
+ @brief Prompts the user for an email address, calls @c FIRAuth.getProvidersForEmail:callback:
+ and displays the result.
+ */
+- (void)getProvidersForEmail {
+ [self showTextInputPromptWithMessage:@"Email:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK || !userInput.length) {
+ return;
+ }
+
+ [self showSpinner:^{
+ [[AppManager auth] fetchProvidersForEmail:userInput
+ completion:^(NSArray<NSString *> *_Nullable providers,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"get providers for email failed" error:error];
+ } else {
+ [self logSuccess:@"get providers for email succeeded."];
+ }
+ [self hideSpinner:^{
+ if (error) {
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+
+ [self showMessagePrompt:[providers componentsJoinedByString:@", "]];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn actionCodeRequestTypeString
+ @brief Returns a string description for the type of the next action code request.
+ */
+- (NSString *)actionCodeRequestTypeString {
+ switch (_actionCodeRequestType) {
+ case ActionCodeRequestTypeInApp:
+ return @"In-App + Continue URL";
+ case ActionCodeRequestTypeContinue:
+ return @"Continue URL";
+ case ActionCodeRequestTypeEmail:
+ return @"Email Only";
+ }
+}
+
+/** @fn toggleActionCodeRequestType
+ @brief Toggle the next action code request type.
+ */
+- (void)toggleActionCodeRequestType {
+ switch (_actionCodeRequestType) {
+ case ActionCodeRequestTypeInApp:
+ _actionCodeRequestType = ActionCodeRequestTypeContinue;
+ break;
+ case ActionCodeRequestTypeContinue:
+ _actionCodeRequestType = ActionCodeRequestTypeEmail;
+ break;
+ case ActionCodeRequestTypeEmail:
+ _actionCodeRequestType = ActionCodeRequestTypeInApp;
+ break;
+ }
+ [self updateTable];
+}
+
+- (void)changeActionCodeContinueURL {
+ [self showTextInputPromptWithMessage:kContinueURLDescription
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (userPressedOK) {
+ _actionCodeContinueURL = userInput.length ? [NSURL URLWithString:userInput] : nil;
+ [self updateTable];
+ }
+ }];
+}
+
+/** @fn requestVerifyEmail
+ @brief Requests a "verify email" email be sent.
+ */
+- (void)requestVerifyEmail {
+ [self showSpinner:^{
+ void (^sendEmailVerification)(void (^)(NSError *)) = ^(void (^completion)(NSError *)) {
+ if (_actionCodeRequestType == ActionCodeRequestTypeEmail) {
+ [[self user] sendEmailVerificationWithCompletion:completion];
+ } else {
+ [[self user] sendEmailVerificationWithActionCodeSettings:[self actionCodeSettings]
+ completion:completion];
+ }
+ };
+ sendEmailVerification(^(NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"request verify email failed" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+ [self logSuccess:@"request verify email succeeded."];
+ [self showMessagePrompt:@"Sent"];
+ }];
+ });
+ }];
+}
+
+/** @fn requestPasswordReset
+ @brief Requests a "password reset" email be sent.
+ */
+- (void)requestPasswordReset {
+ [self showTextInputPromptWithMessage:@"Email:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK || !userInput.length) {
+ return;
+ }
+ [self showSpinner:^{
+ void (^requestPasswordReset)(void (^)(NSError *)) = ^(void (^completion)(NSError *)) {
+ if (_actionCodeRequestType == ActionCodeRequestTypeEmail) {
+ [[AppManager auth] sendPasswordResetWithEmail:userInput completion:completion];
+ } else {
+ [[AppManager auth] sendPasswordResetWithEmail:userInput
+ actionCodeSettings:[self actionCodeSettings]
+ completion:completion];
+ }
+ };
+ requestPasswordReset(^(NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"request password reset failed" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+ [self logSuccess:@"request password reset succeeded."];
+ [self showMessagePrompt:@"Sent"];
+ }];
+ });
+ }];
+ }];
+}
+
+/** @fn resetPassword
+ @brief Performs a password reset operation.
+ */
+- (void)resetPassword {
+ [self showTextInputPromptWithMessage:@"OOB Code:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK || !userInput.length) {
+ return;
+ }
+ NSString *code = userInput;
+
+ [self showTextInputPromptWithMessage:@"New Password:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK || !userInput.length) {
+ return;
+ }
+
+ [self showSpinner:^{
+ [[AppManager auth] confirmPasswordResetWithCode:code
+ newPassword:userInput
+ completion:^(NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"Password reset failed" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+ [self logSuccess:@"Password reset succeeded."];
+ [self showMessagePrompt:@"Password reset succeeded."];
+ }];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn checkActionCode
+ @brief Performs a "check action code" operation.
+ */
+- (void)checkActionCode {
+ [self showTextInputPromptWithMessage:@"OOB Code:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK || !userInput.length) {
+ return;
+ }
+ [self showSpinner:^{
+ [[AppManager auth] checkActionCode:userInput completion:^(FIRActionCodeInfo *_Nullable info,
+ NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"Check action code failed" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+ [self logSuccess:@"Check action code succeeded."];
+ NSString *email = [info dataForKey:FIRActionCodeEmailKey];
+ NSString *fromEmail = [info dataForKey:FIRActionCodeFromEmailKey];
+ NSString *message =
+ fromEmail ? [NSString stringWithFormat:@"%@ -> %@", fromEmail, email] : email;
+ NSString *operation = [self nameForActionCodeOperation:info.operation];
+ [self showMessagePromptWithTitle:operation
+ message:message
+ showCancelButton:NO
+ completion:nil];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn applyActionCode
+ @brief Performs a "apply action code" operation.
+ */
+- (void)applyActionCode {
+ [self showTextInputPromptWithMessage:@"OOB Code:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK || !userInput.length) {
+ return;
+ }
+ [self showSpinner:^{
+
+ [[AppManager auth] applyActionCode:userInput completion:^(NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"Apply action code failed" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+ [self logSuccess:@"Apply action code succeeded."];
+ [self showMessagePrompt:@"Action code was properly applied."];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn verifyPasswordResetCode
+ @brief Performs a "verify password reset code" operation.
+ */
+- (void)verifyPasswordResetCode {
+ [self showTextInputPromptWithMessage:@"OOB Code:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK || !userInput.length) {
+ return;
+ }
+ [self showSpinner:^{
+ [[AppManager auth] verifyPasswordResetCode:userInput completion:^(NSString *_Nullable email,
+ NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"Verify password reset code failed" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+ [self logSuccess:@"Verify password resest code succeeded."];
+ NSString *alertMessage =
+ [[NSString alloc] initWithFormat:@"Code verified for email: %@", email];
+ [self showMessagePrompt:alertMessage];
+ }];
+ }];
+ }];
+ }];
+}
+
+
+/** @fn nameForActionCodeOperation
+ @brief Returns the string value of the provided FIRActionCodeOperation value.
+ @param operation the FIRActionCodeOperation value to convert to string.
+ @return String conversion of FIRActionCodeOperation value.
+ */
+- (NSString *)nameForActionCodeOperation:(FIRActionCodeOperation)operation {
+ switch (operation) {
+ case FIRActionCodeOperationVerifyEmail:
+ return @"Verify Email";
+ case FIRActionCodeOperationRecoverEmail:
+ return @"Recover Email";
+ case FIRActionCodeOperationPasswordReset:
+ return @"Password Reset";
+ case FIRActionCodeOperationUnknown:
+ return @"Unknown action";
+ }
+}
+
+/** @fn updateEmail
+ @brief Changes the email address of the current user.
+ */
+- (void)updateEmail {
+ [self showTextInputPromptWithMessage:@"Email Address:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK || !userInput.length) {
+ return;
+ }
+
+ [self showSpinner:^{
+ [[self user] updateEmail:userInput completion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"update email failed" error:error];
+ } else {
+ [self logSuccess:@"update email succeeded."];
+ }
+ [self hideSpinner:^{
+ [self showTypicalUIForUserUpdateResultsWithTitle:kUpdateEmailText error:error];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn updatePassword
+ @brief Updates the password of the current user.
+ */
+- (void)updatePassword {
+ [self showTextInputPromptWithMessage:@"New Password:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (!userPressedOK) {
+ return;
+ }
+
+ [self showSpinner:^{
+ [[self user] updatePassword:userInput completion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"update password failed" error:error];
+ } else {
+ [self logSuccess:@"update password succeeded."];
+ }
+ [self hideSpinner:^{
+ [self showTypicalUIForUserUpdateResultsWithTitle:kUpdatePasswordText error:error];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn createUser
+ @brief Creates a new user.
+ */
+- (void)createUser {
+ [self showTextInputPromptWithMessage:@"Email:"
+ keyboardType:UIKeyboardTypeEmailAddress
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable email) {
+ if (!userPressedOK || !email.length) {
+ return;
+ }
+
+ [self showTextInputPromptWithMessage:@"Password:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable password) {
+ if (!userPressedOK) {
+ return;
+ }
+
+ [self showSpinner:^{
+ [[AppManager auth] createUserWithEmail:email
+ password:password
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"create user failed" error:error];
+ } else {
+ [self logSuccess:@"create user succeeded."];
+ }
+ [self hideSpinner:^{
+ [self showTypicalUIForUserUpdateResultsWithTitle:kCreateUserTitle error:error];
+ }];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn signInWithPhoneNumber
+ @brief Allows sign in with phone number.
+ */
+- (void)signInWithPhoneNumber {
+ [self commonPhoneNumberInputWithTitle:@"Phone #" Completion:^(NSString *_Nullable phone) {
+ [self showSpinner:^{
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ [[AppManager phoneAuthProvider] verifyPhoneNumber:phone
+ completion:^(NSString *_Nullable verificationID,
+ NSError *_Nullable error) {
+#pragma clang diagnostic pop
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"failed to send verification code" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+ [self logSuccess:@"Code sent"];
+
+ [self commonPhoneNumberInputWithTitle:@"Code"
+ Completion:^(NSString *_Nullable verificationCode) {
+ [self commontPhoneVerificationWithVerificationID:verificationID
+ verificationCode:verificationCode];
+ }];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn signInWithPhoneNumberRecaptcha
+ @brief Allows sign in with phone number using reCAPTCHA
+ */
+- (void)signInWithPhoneNumberRecaptcha {
+ [self commonPhoneNumberInputWithTitle:@"Phone #" Completion:^(NSString *_Nullable phone) {
+ [self showSpinner:^{
+ [[AppManager phoneAuthProvider] verifyPhoneNumber:phone
+ UIDelegate:nil
+ completion:^(NSString *_Nullable verificationID,
+ NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"failed to send verification code" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+ [self logSuccess:@"Code sent"];
+
+ [self commonPhoneNumberInputWithTitle:@"Code"
+ Completion:^(NSString *_Nullable verificationCode) {
+ [self commontPhoneVerificationWithVerificationID:verificationID
+ verificationCode:verificationCode];
+ }];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn commonPhoneNumberInputWithLabel:Completion
+ @brief Allows user input into a text field.
+ @param title of the promt.
+ */
+- (void)commonPhoneNumberInputWithTitle:(NSString *)title
+ Completion:(textInputCompletionBlock)completion {
+ [self showTextInputPromptWithMessage:title
+ keyboardType:UIKeyboardTypePhonePad
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable phoneNumber) {
+ if (!userPressedOK || !phoneNumber.length) {
+ return;
+ }
+ completion(phoneNumber);
+ }];
+}
+
+/** @fn commonPhoneNumberInputWithLabel:Completion
+ @brief Finishes the phone number verification flow.
+ @param verificationID The verificationID from the backend.
+ @param verificationCode The verificationCode from the SMS message.
+ */
+- (void)commontPhoneVerificationWithVerificationID:(NSString *)verificationID
+ verificationCode:(NSString *)verificationCode {
+ [self showSpinner:^{
+ FIRAuthCredential *credential =
+ [[AppManager phoneAuthProvider] credentialWithVerificationID:verificationID
+ verificationCode:verificationCode];
+ [[AppManager auth] signInWithCredential:credential
+ completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"failed to verify phone number" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+ }];
+ }];
+ }];
+}
+
+/** @fn updatePhoneNumber
+ @brief Allows adding a verified phone number to the currently signed user.
+ */
+- (void)updatePhoneNumber {
+ [self showTextInputPromptWithMessage:@"Phone #:"
+ keyboardType:UIKeyboardTypePhonePad
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable phoneNumber) {
+ if (!userPressedOK || !phoneNumber.length) {
+ return;
+ }
+ [self showSpinner:^{
+ [[AppManager phoneAuthProvider] verifyPhoneNumber:phoneNumber
+ UIDelegate:nil
+ completion:^(NSString *_Nullable verificationID,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"failed to send verification code" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+ [self logSuccess:@"Code sent"];
+
+ [self showTextInputPromptWithMessage:@"Verification code:"
+ keyboardType:UIKeyboardTypeNumberPad
+ completionBlock:^(BOOL userPressedOK,
+ NSString *_Nullable verificationCode) {
+ if (!userPressedOK || !verificationCode.length) {
+ return;
+ }
+ [self showSpinner:^{
+ FIRPhoneAuthCredential *credential =
+ [[AppManager phoneAuthProvider] credentialWithVerificationID:verificationID
+ verificationCode:verificationCode];
+ [[self user] updatePhoneNumberCredential:credential
+ completion:^(NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"update phone number failed" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ } else {
+ [self logSuccess:@"update phone number succeeded."];
+ }
+ }];
+ }];
+ }];
+ [self hideSpinner:^{
+ [self showTypicalUIForUserUpdateResultsWithTitle:kCreateUserTitle error:error];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn linkPhoneNumber
+ @brief Allows linking a verified phone number to the currently signed user.
+ */
+- (void)linkPhoneNumber {
+ [self showTextInputPromptWithMessage:@"Phone #:"
+ keyboardType:UIKeyboardTypePhonePad
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable phoneNumber) {
+ if (!userPressedOK || !phoneNumber.length) {
+ return;
+ }
+ [self showSpinner:^{
+ [[AppManager phoneAuthProvider] verifyPhoneNumber:phoneNumber
+ UIDelegate:nil
+ completion:^(NSString *_Nullable verificationID,
+ NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"failed to send verification code" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+ [self logSuccess:@"Code sent"];
+
+ [self showTextInputPromptWithMessage:@"Verification code:"
+ keyboardType:UIKeyboardTypeNumberPad
+ completionBlock:^(BOOL userPressedOK,
+ NSString *_Nullable verificationCode) {
+ if (!userPressedOK || !verificationCode.length) {
+ return;
+ }
+ [self showSpinner:^{
+ FIRPhoneAuthCredential *credential =
+ [[AppManager phoneAuthProvider] credentialWithVerificationID:verificationID
+ verificationCode:verificationCode];
+ [[self user] linkWithCredential:credential
+ completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ if (error.code == FIRAuthErrorCodeCredentialAlreadyInUse) {
+ [self showMessagePromptWithTitle:@"Phone number is already linked to "
+ @"another user"
+ message:@"Tap Ok to sign in with that user now."
+ showCancelButton:YES
+ completion:^(BOOL userPressedOK,
+ NSString *_Nullable userInput) {
+ if (userPressedOK) {
+ // If FIRAuthErrorCodeCredentialAlreadyInUse error, sign in with the
+ // provided credential.
+ [self showSpinner:^{
+ FIRPhoneAuthCredential *credential =
+ error.userInfo[FIRAuthUpdatedCredentialKey];
+ [[AppManager auth] signInWithCredential:credential
+ completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ [self hideSpinner:^{
+ if (error) {
+ [self logFailure:@"failed to verify phone number" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ return;
+ }
+ }];
+ }];
+ }];
+ }
+ }];
+ } else {
+ [self logFailure:@"link phone number failed" error:error];
+ [self showMessagePrompt:error.localizedDescription];
+ }
+ return;
+ }
+ [self logSuccess:@"link phone number succeeded."];
+ }];
+ }];
+ }];
+ }];
+ }];
+ }];
+ }];
+ }];
+}
+
+/** @fn signInAnonymously
+ @brief Signs in as an anonymous user.
+ */
+- (void)signInAnonymously {
+ [[AppManager auth] signInAnonymouslyWithCompletion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in anonymously failed" error:error];
+ } else {
+ [self logSuccess:@"sign-in anonymously succeeded."];
+ }
+ [self showTypicalUIForUserUpdateResultsWithTitle:kSignInAnonymouslyButtonText error:error];
+ }];
+}
+
+/** @fn signInWithGitHub
+ @brief Signs in as a GitHub user. Prompts the user for an access token and uses this access
+ token to create a GitHub (generic) credential for signing in.
+ */
+- (void)signInWithGitHub {
+ [self showTextInputPromptWithMessage:@"GitHub Access Token:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable accessToken) {
+ if (!userPressedOK || !accessToken.length) {
+ return;
+ }
+ FIRAuthCredential *credential =
+ [FIROAuthProvider credentialWithProviderID:FIRGitHubAuthProviderID accessToken:accessToken];
+ if (credential) {
+ [[AppManager auth] signInWithCredential:credential completion:^(FIRUser *_Nullable user,
+ NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in with provider failed" error:error];
+ } else {
+ [self logSuccess:@"sign-in with provider succeeded."];
+ }
+ [self showTypicalUIForUserUpdateResultsWithTitle:@"Sign-In" error:error];
+ }];
+ }
+ }];
+}
+
+/** @fn deleteApp
+ @brief Deletes the @c FIRApp associated with our @c FIRAuth instance.
+ */
+- (void)deleteApp {
+ [[FIRApp defaultApp] deleteApp:^(BOOL success) {
+ [self log:success ? @"App deleted successfully." : @"Failed to delete app."];
+ }];
+}
+
+/** @fn timeAuthInitialization
+ @brief Times FIRAuth instance initialization time.
+ */
+- (void)timeAuthInitialization {
+ // Temporarily disable auth state listener to avoid interfering with the result.
+ [[NSNotificationCenter defaultCenter] removeObserver:self
+ name:FIRAuthStateDidChangeNotification
+ object:nil];
+ [self showSpinner:^() {
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^() {
+ const int numberOfRuns = 4096;
+ FIRApp *app = [FIRApp defaultApp];
+ NSString *key = NSStringFromClass([FIRAuth class]);
+ NSDate *startTime = [NSDate date];
+ for (int i = 0; i < numberOfRuns; i++) {
+ @autoreleasepool {
+ [FIRAppAssociationRegistration deregisterObjectWithHost:app key:key];
+ [FIRAuth auth];
+ }
+ }
+ NSDate *endTime = [NSDate date];
+ dispatch_async(dispatch_get_main_queue(), ^() {
+ // Re-enable auth state listener.
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(authStateChangedForAuth:)
+ name:FIRAuthStateDidChangeNotification
+ object:nil];
+ [self hideSpinner:^() {
+ NSTimeInterval averageTime = [endTime timeIntervalSinceDate:startTime] / numberOfRuns;
+ NSString *message = [NSString stringWithFormat:
+ @"Each [FIRAuth auth] takes average of %.3f ms for %d runs",
+ averageTime * 1000, numberOfRuns];
+ [self showMessagePromptWithTitle:@"Timing Result"
+ message:message
+ showCancelButton:NO
+ completion:nil];
+ }];
+ });
+ });
+ }];
+}
+
+#pragma mark - Helpers
+
+/** @fn user
+ @brief The user to use for user operations. Takes into account the "use signed-in user vs. use
+ user in memory" setting.
+ */
+- (FIRUser *)user {
+ return _useUserInMemory ? _userInMemory : [AppManager auth].currentUser;
+}
+
+/** @fn actionCodeSettings
+ @brief Returns the action code settings for this app.
+ */
+- (FIRActionCodeSettings *)actionCodeSettings {
+ FIRActionCodeSettings *actionCodeSettings = [[FIRActionCodeSettings alloc] init];
+ actionCodeSettings.URL = _actionCodeContinueURL;
+ actionCodeSettings.handleCodeInApp = _actionCodeRequestType == ActionCodeRequestTypeInApp;
+ return actionCodeSettings;
+}
+
+/** @fn showTypicalUIForUserUpdateResultsWithTitle:error:
+ @brief Shows a prompt if error is non-nil with the localized description of the error.
+ @param resultsTitle The title of the prompt
+ @param error The error details to display if non-nil.
+ */
+- (void)showTypicalUIForUserUpdateResultsWithTitle:(NSString *)resultsTitle
+ error:(NSError *)error {
+ if (error) {
+ NSString *message = [NSString stringWithFormat:@"%@ (%ld)\n%@",
+ error.domain,
+ (long)error.code,
+ error.localizedDescription];
+ if (error.code == FIRAuthErrorCodeAccountExistsWithDifferentCredential) {
+ NSString *errorEmail = error.userInfo[FIRAuthErrorUserInfoEmailKey];
+ resultsTitle = [NSString stringWithFormat:@"Existing email : %@", errorEmail];
+ }
+ [self showMessagePromptWithTitle:resultsTitle
+ message:message
+ showCancelButton:NO
+ completion:nil];
+ return;
+ }
+ [self updateUserInfo];
+}
+
+/** @fn showUIForAuthDataResultWithResult:error:
+ @brief Shows a prompt if error is non-nil with the localized description of the error.
+ @param result The auth data result if non-nil.
+ @param error The error details to display if non-nil.
+ */
+- (void)showUIForAuthDataResultWithResult:(FIRAuthDataResult *)result
+ error:(NSError *)error {
+ NSString *errorMessage = [NSString stringWithFormat:@"%@ (%ld)\n%@",
+ error.domain ?: @"",
+ (long)error.code,
+ error.localizedDescription ?: @""];
+ [self showMessagePromptWithTitle:@"Error"
+ message:errorMessage
+ showCancelButton:NO
+ completion:^(BOOL userPressedOK,
+ NSString *_Nullable userInput) {
+ NSString *profileMessaage =
+ [NSString stringWithFormat:@"%@", result.additionalUserInfo.profile ?: @""];
+ [self showMessagePromptWithTitle:@"Profile Info"
+ message:profileMessaage
+ showCancelButton:NO
+ completion:nil];
+ [self updateUserInfo];
+ }];
+}
+
+- (void)doSignInWithCustomToken:(NSString *_Nullable)userEnteredTokenText {
+ [[AppManager auth] signInWithCustomToken:userEnteredTokenText
+ completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+ if (error) {
+ [self logFailure:@"sign-in with custom token failed" error:error];
+ [self showMessagePromptWithTitle:kSignInErrorAlertTitle
+ message:error.localizedDescription
+ showCancelButton:NO
+ completion:nil];
+ return;
+ }
+ [self logSuccess:@"sign-in with custom token succeeded."];
+ [self showMessagePromptWithTitle:kSignedInAlertTitle
+ message:user.displayName
+ showCancelButton:NO
+ completion:nil];
+ }];
+}
+
+- (void)updateUserInfo {
+ [_userInfoTableViewCell updateContentsWithUser:[AppManager auth].currentUser];
+ [_userInMemoryInfoTableViewCell updateContentsWithUser:_userInMemory];
+}
+
+- (void)authStateChangedForAuth:(NSNotification *)notification {
+ [self updateUserInfo];
+ if (notification) {
+ [self log:[NSString stringWithFormat:
+ @"received FIRAuthStateDidChange notification on user '%@'.",
+ ((FIRAuth *)notification.object).currentUser.uid]];
+ }
+}
+
+/** @fn clearConsole
+ @brief Clears the console text.
+ */
+- (IBAction)clearConsole:(id)sender {
+ [_consoleString appendString:@"\n\n"];
+ _consoleTextView.text = @"";
+}
+
+/** @fn copyConsole
+ @brief Copies the current console string to the clipboard.
+ */
+- (IBAction)copyConsole:(id)sender {
+ UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
+ pasteboard.string = _consoleString ?: @"";
+}
+
+@end
diff --git a/Example/Auth/Sample/MainViewController.xib b/Example/Auth/Sample/MainViewController.xib
new file mode 100644
index 0000000..ec4b75a
--- /dev/null
+++ b/Example/Auth/Sample/MainViewController.xib
@@ -0,0 +1,330 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="10116" systemVersion="15E65" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+ <capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
+ <capability name="Constraints to layout margins" minToolsVersion="6.0"/>
+ </dependencies>
+ <objects>
+ <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MainViewController">
+ <connections>
+ <outlet property="consoleTextView" destination="n2N-pY-4Y5" id="8Rx-K5-QmU"/>
+ <outlet property="tableView" destination="Hll-rc-gfA" id="0mG-y8-irt"/>
+ <outlet property="tableViewManager" destination="A6y-Y4-frp" id="2mZ-dg-wS4"/>
+ <outlet property="userInMemoryInfoTableViewCell" destination="zh0-45-TuQ" id="Bi6-s7-kLG"/>
+ <outlet property="userInfoTableViewCell" destination="bYf-Pj-jVY" id="p0a-Bx-hvv"/>
+ <outlet property="userToUseCell" destination="LsZ-4s-P51" id="X9v-it-wLD"/>
+ <outlet property="view" destination="iN0-l3-epB" id="sGX-Mq-1Uy"/>
+ </connections>
+ </placeholder>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+ <view contentMode="scaleToFill" id="iN0-l3-epB">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" style="grouped" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" translatesAutoresizingMaskIntoConstraints="NO" id="Hll-rc-gfA">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="417"/>
+ <color key="backgroundColor" red="0.93725490199999995" green="0.93725490199999995" blue="0.95686274510000002" alpha="1" colorSpace="calibratedRGB"/>
+ <connections>
+ <outlet property="dataSource" destination="A6y-Y4-frp" id="R4X-fh-aFx"/>
+ <outlet property="delegate" destination="A6y-Y4-frp" id="jHu-Pv-9wv"/>
+ </connections>
+ </tableView>
+ <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="n2N-pY-4Y5">
+ <rect key="frame" x="0.0" y="425" width="600" height="175"/>
+ <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="175" id="s61-xh-P20"/>
+ </constraints>
+ <color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
+ </textView>
+ <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hEI-H1-vZs">
+ <rect key="frame" x="570" y="425" width="30" height="30"/>
+ <state key="normal" title="CL"/>
+ <connections>
+ <action selector="clearConsole:" destination="-1" eventType="touchUpInside" id="Hn2-Sp-p8a"/>
+ </connections>
+ </button>
+ <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ONk-oy-W6d">
+ <rect key="frame" x="570" y="449" width="30" height="30"/>
+ <state key="normal" title="CP"/>
+ <connections>
+ <action selector="copyConsole:" destination="-1" eventType="touchUpInside" id="r9j-zp-kN7"/>
+ </connections>
+ </button>
+ </subviews>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ <constraints>
+ <constraint firstAttribute="trailing" secondItem="ONk-oy-W6d" secondAttribute="trailing" id="22R-Wv-nx7"/>
+ <constraint firstAttribute="trailing" secondItem="Hll-rc-gfA" secondAttribute="trailing" id="GWn-K4-V3D"/>
+ <constraint firstAttribute="bottom" secondItem="Hll-rc-gfA" secondAttribute="bottom" constant="183" id="K99-aB-Iqr"/>
+ <constraint firstItem="Hll-rc-gfA" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="UAz-Ca-RCH"/>
+ <constraint firstAttribute="bottom" secondItem="n2N-pY-4Y5" secondAttribute="bottom" id="ZVy-cZ-eIb"/>
+ <constraint firstItem="n2N-pY-4Y5" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="aaF-Ps-Qpu"/>
+ <constraint firstAttribute="bottom" secondItem="ONk-oy-W6d" secondAttribute="bottom" constant="121" id="bcX-TK-lMF"/>
+ <constraint firstAttribute="trailing" secondItem="n2N-pY-4Y5" secondAttribute="trailing" id="jsn-wx-OIr"/>
+ <constraint firstAttribute="trailing" secondItem="hEI-H1-vZs" secondAttribute="trailing" id="mpz-9q-ALh"/>
+ <constraint firstAttribute="bottom" secondItem="hEI-H1-vZs" secondAttribute="bottom" constant="145" id="otK-c8-X66"/>
+ <constraint firstItem="Hll-rc-gfA" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="yT4-GR-MIH"/>
+ </constraints>
+ <point key="canvasLocation" x="448" y="402"/>
+ </view>
+ <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="107" id="bYf-Pj-jVY" customClass="UserTableViewCell">
+ <rect key="frame" x="0.0" y="0.0" width="500" height="107"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="bYf-Pj-jVY" id="Ulk-yV-azP">
+ <rect key="frame" x="0.0" y="0.0" width="500" height="106.5"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <subviews>
+ <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="hPP-eY-9fW">
+ <rect key="frame" x="8" y="8" width="90" height="90"/>
+ <constraints>
+ <constraint firstAttribute="width" constant="90" id="5y3-rD-gXk"/>
+ <constraint firstAttribute="width" secondItem="hPP-eY-9fW" secondAttribute="height" multiplier="1:1" id="AqJ-2w-dcT"/>
+ <constraint firstAttribute="width" secondItem="hPP-eY-9fW" secondAttribute="height" multiplier="1:1" id="z7U-Am-95c"/>
+ </constraints>
+ <variation key="default">
+ <mask key="constraints">
+ <exclude reference="z7U-Am-95c"/>
+ </mask>
+ </variation>
+ </imageView>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p6v-63-AuV">
+ <rect key="frame" x="106" y="8" width="314" height="21"/>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
+ <color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Email" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="EfM-PI-Xnj">
+ <rect key="frame" x="106" y="32" width="314" height="21"/>
+ <constraints>
+ <constraint firstAttribute="width" constant="314" id="KEA-3Z-pMi"/>
+ <constraint firstAttribute="height" constant="21" id="jOF-DJ-Sml"/>
+ </constraints>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
+ <nil key="highlightedColor"/>
+ <variation key="default">
+ <mask key="constraints">
+ <exclude reference="KEA-3Z-pMi"/>
+ </mask>
+ </variation>
+ </label>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Provider List" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="my0-p6-pH6">
+ <rect key="frame" x="106" y="78" width="314" height="21"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="21" id="62Z-fc-RHm"/>
+ </constraints>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="iaQ-zf-G2U">
+ <rect key="frame" x="428" y="8" width="64" height="90"/>
+ <constraints>
+ <constraint firstAttribute="width" constant="64" id="R5X-0j-Lao"/>
+ </constraints>
+ <state key="normal" title="M+"/>
+ <connections>
+ <action selector="memoryPlus" destination="-1" eventType="touchUpInside" id="gvb-Km-J8l"/>
+ </connections>
+ </button>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="User ID" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="h1y-Rb-peJ">
+ <rect key="frame" x="106" y="55" width="314" height="21"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="21" id="780-NW-d3a"/>
+ </constraints>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
+ <nil key="highlightedColor"/>
+ </label>
+ </subviews>
+ <constraints>
+ <constraint firstItem="hPP-eY-9fW" firstAttribute="top" secondItem="Ulk-yV-azP" secondAttribute="topMargin" id="0ry-HJ-3vx"/>
+ <constraint firstItem="EfM-PI-Xnj" firstAttribute="leading" secondItem="hPP-eY-9fW" secondAttribute="trailing" constant="8" symbolic="YES" id="564-bQ-COy"/>
+ <constraint firstItem="iaQ-zf-G2U" firstAttribute="bottom" secondItem="hPP-eY-9fW" secondAttribute="bottom" id="9hR-kP-6Tf"/>
+ <constraint firstItem="iaQ-zf-G2U" firstAttribute="leading" secondItem="h1y-Rb-peJ" secondAttribute="trailing" constant="8" symbolic="YES" id="BmY-H5-2NO"/>
+ <constraint firstItem="iaQ-zf-G2U" firstAttribute="trailing" secondItem="Ulk-yV-azP" secondAttribute="trailingMargin" id="CeI-mb-oeM"/>
+ <constraint firstItem="EfM-PI-Xnj" firstAttribute="leading" secondItem="my0-p6-pH6" secondAttribute="leading" id="D91-aG-k83"/>
+ <constraint firstItem="EfM-PI-Xnj" firstAttribute="top" secondItem="Ulk-yV-azP" secondAttribute="top" constant="32" id="DLn-9X-y8c"/>
+ <constraint firstItem="iaQ-zf-G2U" firstAttribute="bottom" secondItem="hPP-eY-9fW" secondAttribute="bottom" id="EAV-7c-fG4"/>
+ <constraint firstAttribute="bottomMargin" secondItem="my0-p6-pH6" secondAttribute="bottom" constant="-0.5" id="LTJ-U8-4RR"/>
+ <constraint firstItem="iaQ-zf-G2U" firstAttribute="leading" secondItem="EfM-PI-Xnj" secondAttribute="trailing" constant="8" symbolic="YES" id="NnB-3j-eyJ"/>
+ <constraint firstItem="iaQ-zf-G2U" firstAttribute="leading" secondItem="my0-p6-pH6" secondAttribute="trailing" constant="8" symbolic="YES" id="OVL-5A-xCf"/>
+ <constraint firstItem="hPP-eY-9fW" firstAttribute="leading" secondItem="Ulk-yV-azP" secondAttribute="leadingMargin" id="P1h-Uy-1Ac"/>
+ <constraint firstItem="iaQ-zf-G2U" firstAttribute="leading" secondItem="p6v-63-AuV" secondAttribute="trailing" constant="8" symbolic="YES" id="WaP-nw-Pa4"/>
+ <constraint firstItem="my0-p6-pH6" firstAttribute="top" secondItem="h1y-Rb-peJ" secondAttribute="bottom" constant="2" id="YWo-9n-6NP"/>
+ <constraint firstItem="EfM-PI-Xnj" firstAttribute="leading" secondItem="p6v-63-AuV" secondAttribute="leading" id="adW-uj-LKW"/>
+ <constraint firstItem="iaQ-zf-G2U" firstAttribute="top" secondItem="p6v-63-AuV" secondAttribute="top" id="c9T-vM-kCC"/>
+ <constraint firstItem="iaQ-zf-G2U" firstAttribute="trailing" secondItem="Ulk-yV-azP" secondAttribute="trailingMargin" id="cru-24-h18"/>
+ <constraint firstItem="iaQ-zf-G2U" firstAttribute="top" secondItem="p6v-63-AuV" secondAttribute="top" id="eEr-FK-szm"/>
+ <constraint firstItem="h1y-Rb-peJ" firstAttribute="top" secondItem="EfM-PI-Xnj" secondAttribute="bottom" constant="2" id="iYn-Tj-cB5"/>
+ <constraint firstItem="EfM-PI-Xnj" firstAttribute="top" secondItem="p6v-63-AuV" secondAttribute="bottom" constant="3" id="qhp-Cy-lfn"/>
+ <constraint firstItem="hPP-eY-9fW" firstAttribute="bottom" secondItem="Ulk-yV-azP" secondAttribute="bottomMargin" id="stR-OV-FUd"/>
+ <constraint firstItem="EfM-PI-Xnj" firstAttribute="leading" secondItem="h1y-Rb-peJ" secondAttribute="leading" id="tQp-sn-zMN"/>
+ </constraints>
+ <variation key="default">
+ <mask key="constraints">
+ <exclude reference="DLn-9X-y8c"/>
+ <exclude reference="EAV-7c-fG4"/>
+ <exclude reference="cru-24-h18"/>
+ <exclude reference="eEr-FK-szm"/>
+ </mask>
+ </variation>
+ </tableViewCellContentView>
+ <connections>
+ <outlet property="userInfoDisplayNameLabel" destination="p6v-63-AuV" id="jrN-fo-dpo"/>
+ <outlet property="userInfoEmailLabel" destination="EfM-PI-Xnj" id="lup-fo-bWg"/>
+ <outlet property="userInfoProfileURLImageView" destination="hPP-eY-9fW" id="YAH-VZ-TUG"/>
+ <outlet property="userInfoProviderListLabel" destination="my0-p6-pH6" id="MSK-wX-Nst"/>
+ <outlet property="userInfoUserIDLabel" destination="h1y-Rb-peJ" id="3bJ-bB-ofq"/>
+ </connections>
+ <point key="canvasLocation" x="-191" y="325.5"/>
+ </tableViewCell>
+ <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="107" id="zh0-45-TuQ" customClass="UserTableViewCell">
+ <rect key="frame" x="0.0" y="0.0" width="500" height="107"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="zh0-45-TuQ" id="XNx-rO-smo">
+ <rect key="frame" x="0.0" y="0.0" width="500" height="106.5"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <subviews>
+ <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="hLa-iS-QtA">
+ <rect key="frame" x="8" y="8" width="90" height="90"/>
+ <constraints>
+ <constraint firstAttribute="width" secondItem="hLa-iS-QtA" secondAttribute="height" multiplier="1:1" id="3WU-f2-f8i"/>
+ <constraint firstAttribute="width" constant="90" id="dGC-3Y-Lqd"/>
+ <constraint firstAttribute="width" secondItem="hLa-iS-QtA" secondAttribute="height" multiplier="1:1" id="kAm-w5-Ron"/>
+ </constraints>
+ <variation key="default">
+ <mask key="constraints">
+ <exclude reference="3WU-f2-f8i"/>
+ </mask>
+ </variation>
+ </imageView>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ycp-Vc-BY7">
+ <rect key="frame" x="106" y="8" width="314" height="21"/>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
+ <color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Email" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5WQ-KD-Jcl">
+ <rect key="frame" x="106" y="32" width="314" height="21"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="21" id="umR-qC-7qF"/>
+ <constraint firstAttribute="width" constant="314" id="vXg-uI-D8c"/>
+ </constraints>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
+ <nil key="highlightedColor"/>
+ <variation key="default">
+ <mask key="constraints">
+ <exclude reference="vXg-uI-D8c"/>
+ </mask>
+ </variation>
+ </label>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Provider List" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="6FV-lo-RuN">
+ <rect key="frame" x="106" y="78" width="314" height="21"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="21" id="RVn-Wa-Xbp"/>
+ </constraints>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="bEJ-Be-y7a">
+ <rect key="frame" x="428" y="8" width="64" height="90"/>
+ <constraints>
+ <constraint firstAttribute="width" constant="64" id="yRu-DH-Oet"/>
+ </constraints>
+ <state key="normal" title="MC"/>
+ <connections>
+ <action selector="memoryClear" destination="-1" eventType="touchUpInside" id="joZ-N8-BDY"/>
+ </connections>
+ </button>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="User ID" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="FQp-Lq-EBp">
+ <rect key="frame" x="106" y="55" width="314" height="21"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="21" id="tsz-9R-eJf"/>
+ </constraints>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
+ <nil key="highlightedColor"/>
+ </label>
+ </subviews>
+ <constraints>
+ <constraint firstAttribute="bottomMargin" secondItem="6FV-lo-RuN" secondAttribute="bottom" constant="-0.5" id="1Hv-LI-PbT"/>
+ <constraint firstItem="bEJ-Be-y7a" firstAttribute="leading" secondItem="5WQ-KD-Jcl" secondAttribute="trailing" constant="8" symbolic="YES" id="7j5-BA-KFD"/>
+ <constraint firstItem="bEJ-Be-y7a" firstAttribute="trailing" secondItem="XNx-rO-smo" secondAttribute="trailingMargin" id="G45-sQ-DYR"/>
+ <constraint firstItem="bEJ-Be-y7a" firstAttribute="leading" secondItem="ycp-Vc-BY7" secondAttribute="trailing" constant="8" symbolic="YES" id="OL1-KX-AJU"/>
+ <constraint firstItem="5WQ-KD-Jcl" firstAttribute="leading" secondItem="6FV-lo-RuN" secondAttribute="leading" id="Q3L-KT-htb"/>
+ <constraint firstItem="bEJ-Be-y7a" firstAttribute="leading" secondItem="6FV-lo-RuN" secondAttribute="trailing" constant="8" symbolic="YES" id="RD5-7i-oWq"/>
+ <constraint firstItem="5WQ-KD-Jcl" firstAttribute="leading" secondItem="ycp-Vc-BY7" secondAttribute="leading" id="RQv-20-b4H"/>
+ <constraint firstItem="bEJ-Be-y7a" firstAttribute="top" secondItem="ycp-Vc-BY7" secondAttribute="top" id="U5d-lG-KWY"/>
+ <constraint firstItem="5WQ-KD-Jcl" firstAttribute="leading" secondItem="hLa-iS-QtA" secondAttribute="trailing" constant="8" symbolic="YES" id="Xme-JH-VMQ"/>
+ <constraint firstItem="hLa-iS-QtA" firstAttribute="bottom" secondItem="XNx-rO-smo" secondAttribute="bottomMargin" id="Zlj-2B-bQb"/>
+ <constraint firstItem="hLa-iS-QtA" firstAttribute="leading" secondItem="XNx-rO-smo" secondAttribute="leadingMargin" id="a7r-k3-LXl"/>
+ <constraint firstItem="bEJ-Be-y7a" firstAttribute="bottom" secondItem="hLa-iS-QtA" secondAttribute="bottom" id="dlc-Um-0f3"/>
+ <constraint firstItem="5WQ-KD-Jcl" firstAttribute="leading" secondItem="FQp-Lq-EBp" secondAttribute="leading" id="ewD-ct-OhC"/>
+ <constraint firstItem="bEJ-Be-y7a" firstAttribute="top" secondItem="ycp-Vc-BY7" secondAttribute="top" id="f9v-kk-otf"/>
+ <constraint firstItem="bEJ-Be-y7a" firstAttribute="leading" secondItem="FQp-Lq-EBp" secondAttribute="trailing" constant="8" symbolic="YES" id="fNb-Xp-gKX"/>
+ <constraint firstItem="5WQ-KD-Jcl" firstAttribute="top" secondItem="ycp-Vc-BY7" secondAttribute="bottom" constant="3" id="gkP-oU-TGY"/>
+ <constraint firstItem="hLa-iS-QtA" firstAttribute="top" secondItem="XNx-rO-smo" secondAttribute="topMargin" id="p02-MJ-1ff"/>
+ <constraint firstItem="bEJ-Be-y7a" firstAttribute="bottom" secondItem="hLa-iS-QtA" secondAttribute="bottom" id="qFD-Ju-CRU"/>
+ <constraint firstItem="5WQ-KD-Jcl" firstAttribute="top" secondItem="XNx-rO-smo" secondAttribute="top" constant="32" id="qmQ-z2-aTk"/>
+ <constraint firstItem="6FV-lo-RuN" firstAttribute="top" secondItem="FQp-Lq-EBp" secondAttribute="bottom" constant="2" id="vEE-lZ-IV7"/>
+ <constraint firstItem="FQp-Lq-EBp" firstAttribute="top" secondItem="5WQ-KD-Jcl" secondAttribute="bottom" constant="2" id="wG2-Da-pns"/>
+ <constraint firstItem="bEJ-Be-y7a" firstAttribute="trailing" secondItem="XNx-rO-smo" secondAttribute="trailingMargin" id="yt3-Zp-StK"/>
+ </constraints>
+ <variation key="default">
+ <mask key="constraints">
+ <exclude reference="qmQ-z2-aTk"/>
+ <exclude reference="dlc-Um-0f3"/>
+ <exclude reference="yt3-Zp-StK"/>
+ <exclude reference="f9v-kk-otf"/>
+ </mask>
+ </variation>
+ </tableViewCellContentView>
+ <connections>
+ <outlet property="userInfoDisplayNameLabel" destination="ycp-Vc-BY7" id="8Og-Xh-auj"/>
+ <outlet property="userInfoEmailLabel" destination="5WQ-KD-Jcl" id="wdv-q0-g1A"/>
+ <outlet property="userInfoProfileURLImageView" destination="hLa-iS-QtA" id="3X8-jL-d24"/>
+ <outlet property="userInfoProviderListLabel" destination="6FV-lo-RuN" id="FbB-Op-xQz"/>
+ <outlet property="userInfoUserIDLabel" destination="FQp-Lq-EBp" id="SlI-ZP-yuo"/>
+ </connections>
+ <point key="canvasLocation" x="-191" y="574.5"/>
+ </tableViewCell>
+ <customObject id="A6y-Y4-frp" customClass="StaticContentTableViewManager">
+ <connections>
+ <outlet property="tableView" destination="Hll-rc-gfA" id="Lk4-jl-dP0"/>
+ </connections>
+ </customObject>
+ <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="LsZ-4s-P51">
+ <rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="LsZ-4s-P51" id="G9s-Xs-aPm">
+ <rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <subviews>
+ <segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="Vev-VJ-WgW">
+ <rect key="frame" x="50" y="8" width="221" height="29"/>
+ <segments>
+ <segment title="Signed In User"/>
+ <segment title="User in Memory"/>
+ </segments>
+ <connections>
+ <action selector="userToUseDidChange:" destination="-1" eventType="valueChanged" id="gPh-ZP-M2b"/>
+ </connections>
+ </segmentedControl>
+ </subviews>
+ <constraints>
+ <constraint firstItem="Vev-VJ-WgW" firstAttribute="centerX" secondItem="G9s-Xs-aPm" secondAttribute="centerX" id="E6z-Xc-Mjr"/>
+ <constraint firstItem="Vev-VJ-WgW" firstAttribute="centerY" secondItem="G9s-Xs-aPm" secondAttribute="centerY" id="ijR-6Y-K29"/>
+ </constraints>
+ </tableViewCellContentView>
+ <point key="canvasLocation" x="-191" y="449"/>
+ </tableViewCell>
+ </objects>
+</document>
diff --git a/Example/Auth/Sample/SampleTemplate.entitlements b/Example/Auth/Sample/SampleTemplate.entitlements
new file mode 100644
index 0000000..87371f3
--- /dev/null
+++ b/Example/Auth/Sample/SampleTemplate.entitlements
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>application-identifier</key>
+ <string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
+ <key>aps-environment</key>
+ <string>development</string>
+ <key>com.apple.developer.associated-domains</key>
+ <array>
+ <string>applinks:$KAPP_LINKS_DOMAIN</string>
+ </array>
+</dict>
+</plist>
diff --git a/Example/Auth/Sample/SettingsViewController.h b/Example/Auth/Sample/SettingsViewController.h
new file mode 100644
index 0000000..be7b752
--- /dev/null
+++ b/Example/Auth/Sample/SettingsViewController.h
@@ -0,0 +1,37 @@
+/*
+ * 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 StaticContentTableViewManager;
+
+/** @class SettingsViewController
+ @brief A view controller for sample app info and settings.
+ */
+@interface SettingsViewController : UIViewController
+
+/** @property tableViewManager
+ @brief A @c StaticContentTableViewManager which is used to manage the contents of the table
+ view.
+ */
+@property(nonatomic, strong) IBOutlet StaticContentTableViewManager *tableViewManager;
+
+/** @fn done
+ @brief Called when user taps the "Done" button.
+ */
+- (IBAction)done:(id)sender;
+
+@end
diff --git a/Example/Auth/Sample/SettingsViewController.m b/Example/Auth/Sample/SettingsViewController.m
new file mode 100644
index 0000000..b589b69
--- /dev/null
+++ b/Example/Auth/Sample/SettingsViewController.m
@@ -0,0 +1,376 @@
+/*
+ * 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 "SettingsViewController.h"
+
+#import <objc/runtime.h>
+
+#import "AppManager.h"
+#import "FIRApp.h"
+#import "FIRAuth_Internal.h"
+#import "FIRAuthAPNSToken.h"
+#import "FIRAuthAPNSTokenManager.h"
+#import "FIRAuthAppCredential.h"
+#import "FIRAuthAppCredentialManager.h"
+#import "FIROptions.h"
+#import "FirebaseAuth.h"
+#import "StaticContentTableViewManager.h"
+#import "UIViewController+Alerts.h"
+
+/** @var kIdentityToolkitRequestClassName
+ @brief The class name of Identity Toolkit requests.
+ */
+static NSString *const kIdentityToolkitRequestClassName = @"FIRIdentityToolkitRequest";
+
+/** @var kSecureTokenRequestClassName
+ @brief The class name of Secure Token Service requests.
+ */
+static NSString *const kSecureTokenRequestClassName = @"FIRSecureTokenRequest";
+
+/** @var kIdentityToolkitSandboxHost
+ @brief The host of Identity Toolkit sandbox server.
+ */
+static NSString *const kIdentityToolkitSandboxHost = @"www-googleapis-staging.sandbox.google.com";
+
+/** @var kSecureTokenSandboxHost
+ @brief The host of Secure Token Service sandbox server.
+ */
+static NSString *const kSecureTokenSandboxHost = @"staging-securetoken.sandbox.googleapis.com";
+
+/** @var kGoogleServiceInfoPlists
+ @brief a C-array of plist file base names of Google service info to initialize FIRApp.
+ */
+static NSString *const kGoogleServiceInfoPlists[] = {
+ @"GoogleService-Info",
+ @"GoogleService-Info_multi"
+};
+
+/** @var gAPIEndpoints
+ @brief List of API Hosts by request class name.
+ */
+static NSDictionary<NSString *, NSArray<NSString *> *> *gAPIHosts;
+
+/** @var gFirebaseAppOptions
+ @brief List of FIROptions.
+ */
+static NSArray<FIROptions *> *gFirebaseAppOptions;
+
+/** @protocol RequestClass
+ @brief A de-facto protocol followed by request class objects to access its API host.
+ */
+@protocol RequestClass <NSObject>
+- (NSString *)host;
+- (void)setHost:(NSString *)host;
+@end
+
+/** @fn versionString
+ @brief Constructs a version string to display.
+ @param string The version in string form.
+ @param number The version in number form.
+ */
+static NSString *versionString(const unsigned char *string, const double number) {
+ return [NSString stringWithFormat:@"\"%s\" (%g)", string, number];
+}
+
+/** @fn requestHost
+ @brief Retrieves the API host for the request class.
+ @param requestClassName The name of the request class.
+ */
+static NSString *APIHost(NSString *requestClassName) {
+ return [(id<RequestClass>)NSClassFromString(requestClassName) host];
+}
+
+/** @fn truncatedString
+ @brief Truncates a string under a maximum length.
+ @param string The original string to be truncated.
+ @param length The maximum length of the truncated string.
+ @return The truncated string, which is not longer than @c length.
+ */
+static NSString *truncatedString(NSString *string, NSUInteger length) {
+ if (string.length <= length) {
+ return string;
+ }
+ NSUInteger half = (length - 3) / 2;
+ return [NSString stringWithFormat:@"%@...%@",
+ [string substringToIndex:half],
+ [string substringFromIndex:string.length - half]];
+}
+
+@implementation SettingsViewController
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ [self setUpAPIHosts];
+ [self setUpFirebaseAppOptions];
+ [self loadTableView];
+}
+
+- (IBAction)done:(id)sender {
+ [self dismissViewControllerAnimated:YES completion:nil];
+}
+
+- (void)setUpAPIHosts {
+ if (gAPIHosts) {
+ return;
+ }
+ gAPIHosts = @{
+ kIdentityToolkitRequestClassName : @[
+ APIHost(kIdentityToolkitRequestClassName),
+ kIdentityToolkitSandboxHost,
+ ],
+ kSecureTokenRequestClassName : @[
+ APIHost(kSecureTokenRequestClassName),
+ kSecureTokenSandboxHost,
+ ],
+ };
+}
+
+- (void)setUpFirebaseAppOptions {
+ if (gFirebaseAppOptions) {
+ return;
+ }
+ int numberOfOptions = sizeof(kGoogleServiceInfoPlists) / sizeof(*kGoogleServiceInfoPlists);
+ NSMutableArray *appOptions = [[NSMutableArray alloc] initWithCapacity:numberOfOptions];
+ for (int i = 0; i < numberOfOptions; i++) {
+ NSString *plistFileName = kGoogleServiceInfoPlists[i];
+ NSString *plistFilePath = [[NSBundle mainBundle] pathForResource:plistFileName
+ ofType:@"plist"];
+ FIROptions *options = [[FIROptions alloc] initWithContentsOfFile:plistFilePath];
+ [appOptions addObject:options];
+ }
+ gFirebaseAppOptions = [appOptions copy];
+}
+
+- (void)loadTableView {
+ __weak typeof(self) weakSelf = self;
+ _tableViewManager.contents = [StaticContentTableViewContent contentWithSections:@[
+ [StaticContentTableViewSection sectionWithTitle:@"Versions" cells:@[
+ [StaticContentTableViewCell cellWithTitle:@"FirebaseAuth"
+ value:versionString(
+ FirebaseAuthVersionString, FirebaseAuthVersionNumber)],
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:@"API Hosts" cells:@[
+ [StaticContentTableViewCell cellWithTitle:@"Identity Toolkit"
+ value:APIHost(kIdentityToolkitRequestClassName)
+ action:^{
+ [weakSelf toggleAPIHostWithRequestClassName:kIdentityToolkitRequestClassName];
+ }],
+ [StaticContentTableViewCell cellWithTitle:@"Secure Token"
+ value:APIHost(kSecureTokenRequestClassName)
+ action:^{
+ [weakSelf toggleAPIHostWithRequestClassName:kSecureTokenRequestClassName];
+ }],
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:@"Firebase Apps" cells:@[
+ [StaticContentTableViewCell cellWithTitle:@"Active App"
+ value:[self activeAppDescription]
+ action:^{
+ [weakSelf toggleActiveApp];
+ }],
+ [StaticContentTableViewCell cellWithTitle:@"Default App"
+ value:[self projectIDForAppAtIndex:0]
+ action:^{
+ [weakSelf toggleProjectForAppAtIndex:0];
+ }],
+ [StaticContentTableViewCell cellWithTitle:@"Other App"
+ value:[self projectIDForAppAtIndex:1]
+ action:^{
+ [weakSelf toggleProjectForAppAtIndex:1];
+ }],
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:@"Phone Auth" cells:@[
+ [StaticContentTableViewCell cellWithTitle:@"APNs Token"
+ value:[self APNSTokenString]
+ action:^{
+ [weakSelf clearAPNSToken];
+ }],
+ [StaticContentTableViewCell cellWithTitle:@"App Credential"
+ value:[self appCredentialString]
+ action:^{
+ [weakSelf clearAppCredential];
+ }],
+ ]],
+ [StaticContentTableViewSection sectionWithTitle:@"Language" cells:@[
+ [StaticContentTableViewCell cellWithTitle:@"Auth Language"
+ value:[AppManager auth].languageCode ?: @"[none]"
+ action:^{
+ [weakSelf showLanguageInput];
+ }],
+ [StaticContentTableViewCell cellWithTitle:@"Use App language" action:^{
+ [[AppManager auth] useAppLanguage];
+ [weakSelf loadTableView];
+ }],
+ ]],
+ ]];
+}
+
+/** @fn toggleAPIHostWithRequestClassName:
+ @brief Toggles the host name of the server that handles RPCs.
+ @param requestClassName The name of the RPC request class.
+ */
+- (void)toggleAPIHostWithRequestClassName:(NSString *)requestClassName {
+ NSString *currentHost = APIHost(requestClassName);
+ NSArray<NSString *> *allHosts = gAPIHosts[requestClassName];
+ NSString *newHost = allHosts[([allHosts indexOfObject:currentHost] + 1) % allHosts.count];
+ [(id<RequestClass>)NSClassFromString(requestClassName) setHost:newHost];
+ [self loadTableView];
+}
+
+/** @fn activeAppDescription
+ @brief Returns the description for the currently active Firebase app.
+ */
+- (NSString *)activeAppDescription {
+ return [AppManager sharedInstance].active == 0 ? @"[Default]" : @"[Other]";
+}
+
+/** @fn toggleActiveApp
+ @brief Toggles the active Firebase app for the rest of the application.
+ */
+- (void)toggleActiveApp {
+ AppManager *apps = [AppManager sharedInstance];
+ // This changes the FIRAuth instance returned from `[AppManager auth]` to be one that is
+ // associated with a different `FIRApp` instance. The sample app uses `[AppManager auth]`
+ // instead of `[FIRAuth auth]` almost everywhere. Thus, this statement switches between default
+ // and non-default `FIRApp` instances for the sample app to test against.
+ apps.active = (apps.active + 1) % apps.count;
+ [self loadTableView];
+}
+
+/** @fn projectIDForAppAtIndex:
+ @brief Returns the Firebase project ID for the Firebase app at the given index.
+ @param index The index for the app in the app manager.
+ @return The ID of the project.
+ */
+- (NSString *)projectIDForAppAtIndex:(int)index {
+ NSString *APIKey = [[AppManager sharedInstance] appAtIndex:index].options.APIKey;
+ for (FIROptions *options in gFirebaseAppOptions) {
+ if ([options.APIKey isEqualToString:APIKey]) {
+ return options.projectID;
+ }
+ }
+ return @"[none]";
+}
+
+/** @fn toggleProjectForAppAtIndex:
+ @brief Toggles the Firebase project for the Firebase app at the given index by recreating the
+ FIRApp instance with different options.
+ @param index The index for the app to be recreated in the app manager.
+ */
+- (void)toggleProjectForAppAtIndex:(int)index {
+ NSString *APIKey = [[AppManager sharedInstance] appAtIndex:index].options.APIKey;
+ int optionIndex;
+ for (optionIndex = 0; optionIndex < gFirebaseAppOptions.count; optionIndex++) {
+ FIROptions *options = gFirebaseAppOptions[optionIndex];
+ if ([options.APIKey isEqualToString:APIKey]) {
+ break;
+ }
+ }
+ // For non-default apps, `nil` is considered the next option after the last options in the array.
+ int useNil = index > 0;
+ optionIndex = (optionIndex + 1 + useNil) % (gFirebaseAppOptions.count + useNil) - useNil;
+ FIROptions *options = optionIndex >= 0 ? gFirebaseAppOptions[optionIndex] : nil;
+ __weak typeof(self) weakSelf = self;
+ [[AppManager sharedInstance] recreateAppAtIndex:index withOptions:options completion:^() {
+ dispatch_async(dispatch_get_main_queue(), ^() {
+ [weakSelf loadTableView];
+ });
+ }];
+}
+
+/** @fn APNSTokenString
+ @brief Returns a string representing APNS token.
+ */
+- (NSString *)APNSTokenString {
+ FIRAuthAPNSToken *token = [AppManager auth].tokenManager.token;
+ if (!token) {
+ return @"";
+ }
+ return [NSString stringWithFormat:@"%@(%@)",
+ truncatedString(token.string, 19),
+ token.type == FIRAuthAPNSTokenTypeProd ? @"P" : @"S"];
+}
+
+/** @fn clearAPNSToken
+ @brief Clears the saved app credential.
+ */
+- (void)clearAPNSToken {
+ FIRAuthAPNSToken *token = [AppManager auth].tokenManager.token;
+ if (!token) {
+ return;
+ }
+ NSString *tokenType = token.type == FIRAuthAPNSTokenTypeProd ? @"Production" : @"Sandbox";
+ NSString *message = [NSString stringWithFormat:@"token: %@\ntype: %@",
+ token.string, tokenType];
+ [self showMessagePromptWithTitle:@"Clear APNs Token?"
+ message:message
+ showCancelButton:YES
+ completion:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (userPressedOK) {
+ [AppManager auth].tokenManager.token = nil;
+ [self loadTableView];
+ }
+ }];
+}
+
+/** @fn appCredentialString
+ @brief Returns a string representing app credential.
+ */
+- (NSString *)appCredentialString {
+ FIRAuthAppCredential *credential = [AppManager auth].appCredentialManager.credential;
+ if (!credential) {
+ return @"";
+ }
+ return [NSString stringWithFormat:@"%@/%@",
+ truncatedString(credential.receipt, 13),
+ truncatedString(credential.secret, 13)];
+}
+
+/** @fn clearAppCredential
+ @brief Clears the saved app credential.
+ */
+- (void)clearAppCredential {
+ FIRAuthAppCredential *credential = [AppManager auth].appCredentialManager.credential;
+ if (!credential) {
+ return;
+ }
+ NSString *message = [NSString stringWithFormat:@"receipt: %@\nsecret: %@",
+ credential.receipt, credential.secret];
+ [self showMessagePromptWithTitle:@"Clear App Credential?"
+ message:message
+ showCancelButton:YES
+ completion:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+ if (userPressedOK) {
+ [[AppManager auth].appCredentialManager clearCredential];
+ [self loadTableView];
+ }
+ }];
+}
+
+/** @fn showLanguageInput
+ @brief Show language code input field.
+ */
+- (void)showLanguageInput {
+ [self showTextInputPromptWithMessage:@"Enter Language Code For Auth:"
+ completionBlock:^(BOOL userPressedOK, NSString *_Nullable languageCode) {
+ if (!userPressedOK) {
+ return;
+ }
+ [AppManager auth].languageCode = languageCode.length ? languageCode : nil;
+ [self loadTableView];
+ }];
+}
+
+@end
diff --git a/Example/Auth/Sample/SettingsViewController.xib b/Example/Auth/Sample/SettingsViewController.xib
new file mode 100644
index 0000000..4540047
--- /dev/null
+++ b/Example/Auth/Sample/SettingsViewController.xib
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="9531" systemVersion="15D21" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9529"/>
+ </dependencies>
+ <objects>
+ <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="SettingsViewController">
+ <connections>
+ <outlet property="tableViewManager" destination="U72-zk-rRG" id="KxN-ZQ-cAa"/>
+ <outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
+ </connections>
+ </placeholder>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+ <view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rRb-jS-QTO">
+ <rect key="frame" x="8" y="16" width="46" height="30"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="30" id="6TV-T6-63H"/>
+ <constraint firstAttribute="width" constant="46" id="kVF-SE-Qzn"/>
+ </constraints>
+ <state key="normal" title="Done"/>
+ <connections>
+ <action selector="done:" destination="-1" eventType="touchUpInside" id="O91-6u-80J"/>
+ </connections>
+ </button>
+ <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" translatesAutoresizingMaskIntoConstraints="NO" id="4BU-Kb-3Zx">
+ <rect key="frame" x="0.0" y="54" width="600" height="546"/>
+ <color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
+ <connections>
+ <outlet property="dataSource" destination="U72-zk-rRG" id="GFT-r0-uBK"/>
+ <outlet property="delegate" destination="U72-zk-rRG" id="uzl-Rm-2qd"/>
+ </connections>
+ </tableView>
+ </subviews>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ <constraints>
+ <constraint firstItem="rRb-jS-QTO" firstAttribute="leading" secondItem="i5M-Pr-FkT" secondAttribute="leading" constant="8" id="1PF-3b-y4w"/>
+ <constraint firstItem="4BU-Kb-3Zx" firstAttribute="width" secondItem="i5M-Pr-FkT" secondAttribute="width" id="5Tz-J0-2Dq"/>
+ <constraint firstItem="rRb-jS-QTO" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" constant="16" id="K7t-0g-JBo"/>
+ <constraint firstItem="4BU-Kb-3Zx" firstAttribute="top" secondItem="rRb-jS-QTO" secondAttribute="bottom" constant="8" id="MyI-Bl-nGS"/>
+ <constraint firstAttribute="bottom" secondItem="4BU-Kb-3Zx" secondAttribute="bottom" id="QMm-Wv-q60"/>
+ <constraint firstItem="4BU-Kb-3Zx" firstAttribute="centerX" secondItem="i5M-Pr-FkT" secondAttribute="centerX" id="deo-wi-Nmq"/>
+ </constraints>
+ </view>
+ <customObject id="U72-zk-rRG" customClass="StaticContentTableViewManager">
+ <connections>
+ <outlet property="tableView" destination="4BU-Kb-3Zx" id="0OK-KI-QdH"/>
+ </connections>
+ </customObject>
+ </objects>
+</document>
diff --git a/Example/Auth/Sample/StaticContentTableViewManager.h b/Example/Auth/Sample/StaticContentTableViewManager.h
new file mode 100644
index 0000000..5a7c123
--- /dev/null
+++ b/Example/Auth/Sample/StaticContentTableViewManager.h
@@ -0,0 +1,255 @@
+/*
+ * 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>
+
+#pragma mark - Forward Declarations
+
+@class StaticContentTableViewCell;
+@class StaticContentTableViewContent;
+@class StaticContentTableViewSection;
+
+#pragma mark - Block Type Definitions
+
+/** @typedef StaticContentTableViewCellAction
+ @brief The type of block invoked when a cell is tapped.
+ */
+typedef void(^StaticContentTableViewCellAction)(void);
+
+#pragma mark -
+
+/** @class StaticContentTableViewManager
+ @brief Generic class useful for populating a @c UITableView with static content.
+ @remarks Because I keep writing the same UITableView code for every internal testing app, and
+ it's getting too tedious and ugly to keep writing the same thing over and over. It makes
+ our sample apps harder to maintain with all this code sprinkled around everywhere, and
+ we end up cutting corners and making junky testing apps, and spending more time in the
+ process.
+ */
+@interface StaticContentTableViewManager : NSObject<UITableViewDelegate, UITableViewDataSource>
+
+/** @property contents
+ @brief The static contents of the @c UITableView.
+ @remarks Setting this property will reload the @c UITableView.
+ */
+@property(nonatomic, strong, nullable) StaticContentTableViewContent *contents;
+
+/** @property tableView
+ @brief A reference to the managed @c UITableView.
+ @remarks This is needed to automatically reload the table view when the @c contents are changed.
+ */
+@property(nonatomic, weak, nullable) IBOutlet UITableView *tableView;
+
+@end
+
+#pragma mark -
+
+/** @class StaticContentTableViewContent
+ @brief Represents the contents of a @c UITableView.
+ */
+@interface StaticContentTableViewContent : NSObject
+
+/** @property sections
+ @brief The sections for the @c UITableView.
+ */
+@property(nonatomic, copy, readonly, nullable) NSArray<StaticContentTableViewSection *> *sections;
+
+/** @fn contentWithSections:
+ @brief Convenience factory method for creating a new instance of
+ @c StaticContentTableViewContent.
+ @param sections The sections for the @c UITableView.
+ */
++ (nullable instancetype)contentWithSections:
+ (nullable NSArray<StaticContentTableViewSection *> *)sections;
+
+/** @fn init
+ @brief Please use initWithSections:
+ */
+- (nullable instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithSections:
+ @brief Designated initializer.
+ @param sections The sections in the @c UITableView.
+ */
+- (nullable instancetype)initWithSections:
+ (nullable NSArray<StaticContentTableViewSection *> *)sections;
+
+@end
+
+#pragma mark -
+
+/** @class StaticContentTableViewSection
+ @brief Represents a section in a @c UITableView.
+ @remarks Each section has a title (used for the section title in the @c UITableView) and an
+ array of cells.
+ */
+@interface StaticContentTableViewSection : NSObject
+
+/** @property title
+ @brief The title of the section in the @c UITableView.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *title;
+
+/** @property cells
+ @brief The cells in this section of the @c UITableView.
+ */
+@property(nonatomic, copy, readonly, nullable) NSArray<StaticContentTableViewCell *> *cells;
+
+/** @fn sectionWithTitle:cells:
+ @brief Convenience factory method for creating a new instance of
+ @c StaticContentTableViewSection.
+ @param title The title of the section in the @c UITableView.
+ @param cells The cells in this section of the @c UITableView.
+ */
++ (nullable instancetype)sectionWithTitle:(nullable NSString *)title
+ cells:(nullable NSArray<StaticContentTableViewCell *> *)cells;
+
+/** @fn init
+ @brief Please use initWithTitle:cells:
+ */
+- (nullable instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithTitle:cells:
+ @brief Designated initializer.
+ @param title The title of the section in the @c UITableView.
+ @param cells The cells in this section of the @c UITableView.
+ */
+- (nullable instancetype)initWithTitle:(nullable NSString *)title
+ cells:(nullable NSArray<StaticContentTableViewCell *> *)cells;
+
+@end
+
+#pragma mark -
+
+/** @class StaticContentTableViewCell
+ @brief Represents a cell in a @c UITableView.
+ @remarks Cells may be custom cells (in which you specify a @c UITableViewCell to use), or
+ simple single-label cells which you supply the title text for. It does not make sense to
+ specify both @c customCell and also @c title, but if a @c customCell is specified, it will
+ be used instead of the @c title.
+ */
+@interface StaticContentTableViewCell : NSObject
+
+/** @property customCell
+ @brief The custom @c UITableViewCell to use for this cell.
+ */
+@property(nonatomic, strong, readonly, nullable) UITableViewCell *customCell;
+
+/** @property title
+ @brief If no custom cell is being used, this is the text of the @c titleLabel of the
+ @c UITableViewCell.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *title;
+
+/** @property value
+ @brief If no custom cell is being used, this is the text of the @c detailTextLabel of the
+ @c UITableViewCell.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *value;
+
+/** @property accessibilityIdentifier
+ @brief The accessibility ID for the corresponding @c UITableViewCell.
+ */
+@property(nonatomic, copy, readonly, nullable) NSString *accessibilityIdentifier;
+
+/** @property action
+ @brief A block which is executed when the cell is selected.
+ @remarks Avoid retain cycles. Since these blocked are retained here, and your
+ @c UIViewController's object graph likely retains this object, you don't want these blocks
+ to retain your @c UIViewController. The easiest thing is just to create a weak reference to
+ your @c UIViewController and pass it a message as the only thing the block does.
+ */
+@property(nonatomic, copy, readonly, nullable) StaticContentTableViewCellAction action;
+
+/** @fn cellWithTitle:
+ @brief Convenience factory method for a new instance of @c StaticContentTableViewCell.
+ @param title The text of the @c titleLabel of the @c UITableViewCell.
+ */
++ (nullable instancetype)cellWithTitle:(nullable NSString *)title;
+
+/** @fn cellWithTitle:value:
+ @brief Convenience factory method for a new instance of @c StaticContentTableViewCell.
+ @param title The text of the @c titleLabel of the @c UITableViewCell.
+ @param value The text of the @c detailTextLabel of the @c UITableViewCell.
+ */
++ (nullable instancetype)cellWithTitle:(nullable NSString *)title
+ value:(nullable NSString *)value;
+
+/** @fn cellWithTitle:action:
+ @brief Convenience factory method for a new instance of @c StaticContentTableViewCell.
+ @param title The text of the @c titleLabel of the @c UITableViewCell.
+ @param action A block which is executed when the cell is selected.
+ */
++ (nullable instancetype)cellWithTitle:(nullable NSString *)title
+ action:(nullable StaticContentTableViewCellAction)action;
+
+/** @fn cellWithTitle:value:action:
+ @brief Convenience factory method for a new instance of @c StaticContentTableViewCell.
+ @param title The text of the @c titleLabel of the @c UITableViewCell.
+ @param value The text of the @c detailTextLabel of the @c UITableViewCell.
+ @param action A block which is executed when the cell is selected.
+ */
++ (nullable instancetype)cellWithTitle:(nullable NSString *)title
+ value:(nullable NSString *)value
+ action:(nullable StaticContentTableViewCellAction)action;
+
+/** @fn cellWithTitle:value:action:accessibilityLabel:
+ @brief Convenience factory method for a new instance of @c StaticContentTableViewCell.
+ @param title The text of the @c titleLabel of the @c UITableViewCell.
+ @param value The text of the @c detailTextLabel of the @c UITableViewCell.
+ @param action A block which is executed when the cell is selected.
+ @param accessibilityID The accessibility ID to add to the cell.
+ */
++ (nullable instancetype)cellWithTitle:(nullable NSString *)title
+ value:(nullable NSString *)value
+ action:(nullable StaticContentTableViewCellAction)action
+ accessibilityID:(nullable NSString *)accessibilityID;
+
+/** @fn cellWithCustomCell:
+ @brief Convenience factory method for a new instance of @c StaticContentTableViewCell.
+ @param customCell The custom @c UITableViewCell to use for this cell.
+ */
++ (nullable instancetype)cellWithCustomCell:(nullable UITableViewCell *)customCell;
+
+/** @fn cellWithCustomCell:action:
+ @brief Convenience factory method for a new instance of @c StaticContentTableViewCell.
+ @param customCell The custom @c UITableViewCell to use for this cell.
+ @param action A block which is executed when the cell is selected.
+ */
++ (nullable instancetype)cellWithCustomCell:(nullable UITableViewCell *)customCell
+ action:(nullable StaticContentTableViewCellAction)action;
+
+/** @fn init
+ @brief Please use initWithCustomCell:title:action:
+ */
+- (nullable instancetype)init NS_UNAVAILABLE;
+
+/** @fn initWithCustomCell:title:action:
+ @brief Designated initializer.
+ @param customCell The custom @c UITableViewCell to use for this cell.
+ @param title If no custom cell is being used, this is the text of the @c titleLabel of the
+ @c UITableViewCell.
+ @param action A block which is executed when the cell is selected.
+ @param accessibilityID The accessibility ID to add to the cell.
+ */
+- (nullable instancetype)initWithCustomCell:(nullable UITableViewCell *)customCell
+ title:(nullable NSString *)title
+ value:(nullable NSString *)value
+ action:(nullable StaticContentTableViewCellAction)action
+ accessibilityID:(nullable NSString *)accessibilityID
+ NS_DESIGNATED_INITIALIZER;
+
+@end
diff --git a/Example/Auth/Sample/StaticContentTableViewManager.m b/Example/Auth/Sample/StaticContentTableViewManager.m
new file mode 100644
index 0000000..7ac7eb7
--- /dev/null
+++ b/Example/Auth/Sample/StaticContentTableViewManager.m
@@ -0,0 +1,231 @@
+/*
+ * 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 "StaticContentTableViewManager.h"
+
+/** @var kCellReuseIdentitfier
+ @brief The reuse identifier for default style table view cell.
+ */
+static NSString *const kCellReuseIdentitfier = @"reuseIdentifier";
+
+/** @var kCellReuseIdentitfier
+ @brief The reuse identifier for value style table view cell.
+ */
+static NSString *const kValueCellReuseIdentitfier = @"reuseValueIdentifier";
+
+#pragma mark -
+
+@implementation StaticContentTableViewManager
+
+- (void)setContents:(StaticContentTableViewContent *)contents {
+ _contents = contents;
+ [self.tableView reloadData];
+}
+
+- (void)setTableView:(UITableView *)tableView {
+ _tableView = tableView;
+ [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kCellReuseIdentitfier];
+}
+
+#pragma mark - UITableViewDataSource
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
+ return _contents.sections.count;
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
+ return _contents.sections[section].cells.count;
+}
+
+- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
+ return _contents.sections[section].title;
+}
+
+#pragma mark - UITableViewDelegate
+
+- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
+ StaticContentTableViewCell *cellData = _contents.sections[indexPath.section].cells[indexPath.row];
+ if (cellData.customCell) {
+ return cellData.customCell.frame.size.height;
+ }
+ return 44;
+}
+
+- (UITableViewCell *)tableView:(UITableView *)tableView
+ cellForRowAtIndexPath:(NSIndexPath *)indexPath {
+ StaticContentTableViewCell *cellData = _contents.sections[indexPath.section].cells[indexPath.row];
+ UITableViewCell *cell = cellData.customCell;
+ if (cell) {
+ return cell;
+ }
+ if (cellData.value.length) {
+ cell = [tableView dequeueReusableCellWithIdentifier:kValueCellReuseIdentitfier];
+ if (!cell) {
+ cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1
+ reuseIdentifier:kValueCellReuseIdentitfier];
+ cell.detailTextLabel.adjustsFontSizeToFitWidth = YES;
+ cell.detailTextLabel.minimumScaleFactor = 0.5;
+ }
+ cell.detailTextLabel.text = cellData.value;
+ } else {
+ // kCellReuseIdentitfier has already been registered.
+ cell = [tableView dequeueReusableCellWithIdentifier:kCellReuseIdentitfier
+ forIndexPath:indexPath];
+ }
+ cell.textLabel.text = cellData.title;
+ cell.accessibilityIdentifier = cellData.accessibilityIdentifier;
+ return cell;
+}
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
+ StaticContentTableViewCell *cellData = _contents.sections[indexPath.section].cells[indexPath.row];
+ BOOL hasAssociatedAction = cellData.action != nil;
+ if (hasAssociatedAction) {
+ cellData.action();
+ }
+ [tableView deselectRowAtIndexPath:indexPath animated:hasAssociatedAction];
+}
+
+@end
+
+#pragma mark -
+
+@implementation StaticContentTableViewContent
+
++ (nullable instancetype)contentWithSections:
+ (nullable NSArray<StaticContentTableViewSection *> *)sections {
+ return [[self alloc] initWithSections:sections];
+}
+
+- (nullable instancetype)initWithSections:
+ (nullable NSArray<StaticContentTableViewSection *> *)sections {
+ self = [super init];
+ if (self) {
+ _sections = [sections copy];
+ }
+ return self;
+}
+
+@end
+
+#pragma mark -
+
+@implementation StaticContentTableViewSection
+
++ (nullable instancetype)sectionWithTitle:(nullable NSString *)title
+ cells:(nullable NSArray<StaticContentTableViewCell *> *)cells {
+ return [[self alloc] initWithTitle:title cells:cells];
+}
+
+- (nullable instancetype)initWithTitle:(nullable NSString *)title
+ cells:(nullable NSArray<StaticContentTableViewCell *> *)cells {
+ self = [super init];
+ if (self) {
+ _title = [title copy];
+ _cells = [cells copy];
+ }
+ return self;
+}
+
+@end
+
+#pragma mark -
+
+@implementation StaticContentTableViewCell
+
++ (nullable instancetype)cellWithTitle:(nullable NSString *)title {
+ return [[self alloc] initWithCustomCell:nil
+ title:title
+ value:nil
+ action:nil
+ accessibilityID:nil];
+}
+
++ (nullable instancetype)cellWithTitle:(nullable NSString *)title
+ value:(nullable NSString *)value {
+ return [[self alloc] initWithCustomCell:nil
+ title:title
+ value:value
+ action:nil
+ accessibilityID:nil];
+}
+
++ (nullable instancetype)cellWithTitle:(nullable NSString *)title
+ action:(nullable StaticContentTableViewCellAction)action {
+ return [[self alloc] initWithCustomCell:nil
+ title:title
+ value:nil
+ action:action
+ accessibilityID:nil];
+}
+
++ (nullable instancetype)cellWithTitle:(nullable NSString *)title
+ value:(nullable NSString *)value
+ action:(nullable StaticContentTableViewCellAction)action {
+ return [[self alloc] initWithCustomCell:nil
+ title:title
+ value:value
+ action:action
+ accessibilityID:nil];
+}
+
++ (nullable instancetype)cellWithTitle:(nullable NSString *)title
+ value:(nullable NSString *)value
+ action:(nullable StaticContentTableViewCellAction)action
+ accessibilityID:(nullable NSString *)accessibilityID {
+ return [[self alloc] initWithCustomCell:nil
+ title:title
+ value:value
+ action:action
+ accessibilityID:accessibilityID];
+}
+
++ (nullable instancetype)cellWithCustomCell:(nullable UITableViewCell *)customCell {
+ return [[self alloc] initWithCustomCell:customCell
+ title:nil
+ value:nil
+ action:nil
+ accessibilityID:nil];
+}
+
++ (nullable instancetype)cellWithCustomCell:(nullable UITableViewCell *)customCell
+ action:(nullable StaticContentTableViewCellAction)action {
+ return [[self alloc] initWithCustomCell:customCell
+ title:nil
+ value:nil action:action
+ accessibilityID:nil];
+}
+
+- (nullable instancetype)initWithCustomCell:(nullable UITableViewCell *)customCell
+ title:(nullable NSString *)title
+ value:(nullable NSString *)value
+ action:(nullable StaticContentTableViewCellAction)action
+ accessibilityID:(nullable NSString *)accessibilityID {
+ self = [super init];
+ if (self) {
+ _customCell = customCell;
+ _title = [title copy];
+ _value = [value copy];
+ _action = action;
+ if (accessibilityID) {
+ _accessibilityIdentifier = [accessibilityID copy];
+ self.isAccessibilityElement = YES;
+ }
+ }
+ return self;
+}
+
+@end
diff --git a/Example/Auth/Sample/UIViewController+Alerts.h b/Example/Auth/Sample/UIViewController+Alerts.h
new file mode 100644
index 0000000..88686c1
--- /dev/null
+++ b/Example/Auth/Sample/UIViewController+Alerts.h
@@ -0,0 +1,91 @@
+/*
+ * 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
+
+/*! @typedef AlertPromptCompletionBlock
+ @brief The type of callback used to report text input prompt results.
+ */
+typedef void (^AlertPromptCompletionBlock)(BOOL userPressedOK, NSString *_Nullable userInput);
+
+/*! @category UIViewController(Alerts)
+ @brief Wrapper for @c UIAlertController and @c UIAlertView for backwards compatability with
+ iOS 6+.
+ */
+@interface UIViewController (Alerts)
+
+/*! @property useStatusBarSpinner
+ @brief Uses the status bar to indicate work is occuring instead of a modal "please wait" dialog.
+ This is generally useful for allowing user interaction while things are happening.
+ */
+@property(nonatomic, assign) BOOL useStatusBarSpinner;
+
+/*! @fn showMessagePrompt:
+ @brief Displays an alert with an 'OK' button and a message.
+ @param message The message to display.
+ @remarks The message is also copied to the pasteboard.
+ */
+- (void)showMessagePrompt:(NSString *)message;
+
+/*! @fn showMessagePromptWithTitle:message:
+ @brief Displays a titled alert with an 'OK' button and a message.
+ @param title The title of the alert if it exists.
+ @param message The message to display.
+ @param showCancelButton A flag indicating whether or not a cancel option is available.
+ @param completion The completion block to be executed after the alert is dismissed, if it
+ exists.
+ @remarks The message is also copied to the pasteboard.
+ */
+- (void)showMessagePromptWithTitle:(nullable NSString *)title
+ message:(NSString *)message
+ showCancelButton:(BOOL)showCancelButton
+ completion:(nullable AlertPromptCompletionBlock)completion;
+
+/*! @fn showTextInputPromptWithMessage:keyboardType:completionBlock:
+ @brief Shows a prompt with a text field and 'OK'/'Cancel' buttons.
+ @param message The message to display.
+ @param keyboardType The type of keyboard to display for the UITextView in the prompt.
+ @param completion A block to call when the user taps 'OK' or 'Cancel'.
+ */
+- (void)showTextInputPromptWithMessage:(NSString *)message
+ keyboardType:(UIKeyboardType)keyboardType
+ completionBlock:(AlertPromptCompletionBlock)completion;
+
+/*! @fn showTextInputPromptWithMessage:completionBlock:
+ @brief Shows a prompt with a text field and 'OK'/'Cancel' buttons.
+ @param message The message to display.
+ @param completion A block to call when the user taps 'OK' or 'Cancel'.
+ */
+- (void)showTextInputPromptWithMessage:(NSString *)message
+ completionBlock:(AlertPromptCompletionBlock)completion;
+
+/*! @fn showSpinner
+ @brief Shows the please wait spinner.
+ @param completion Called after the spinner has been hidden.
+ */
+- (void)showSpinner:(nullable void(^)(void))completion;
+
+/*! @fn hideSpinner
+ @brief Hides the please wait spinner.
+ @param completion Called after the spinner has been hidden.
+ */
+- (void)hideSpinner:(nullable void(^)(void))completion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Auth/Sample/UIViewController+Alerts.m b/Example/Auth/Sample/UIViewController+Alerts.m
new file mode 100644
index 0000000..76ef067
--- /dev/null
+++ b/Example/Auth/Sample/UIViewController+Alerts.m
@@ -0,0 +1,346 @@
+/*
+ * 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 "UIViewController+Alerts.h"
+
+#import <objc/runtime.h>
+
+/*! @var kPleaseWaitAssociatedObjectKey
+ @brief Key used to identify the "please wait" spinner associated object.
+ */
+static NSString *const kPleaseWaitAssociatedObjectKey =
+ @"_UIViewControllerAlertCategory_PleaseWaitScreenAssociatedObject";
+
+/*! @var kUseStatusBarSpinnerAssociatedObjectKey
+ @brief The address of this constant is the key used to identify the "use status bar spinner"
+ associated object.
+ */
+static const void *const kUseStatusBarSpinnerAssociatedObjectKey;
+
+/*! @var kOK
+ @brief Text for an 'OK' button.
+ */
+static NSString *const kOK = @"OK";
+
+/*! @var kCancel
+ @brief Text for an 'Cancel' button.
+ */
+static NSString *const kCancel = @"Cancel";
+
+/*! @class SimpleTextPromptDelegate
+ @brief A @c UIAlertViewDelegate which allows @c UIAlertView to be used with blocks more easily.
+ */
+@interface SimpleTextPromptDelegate : NSObject <UIAlertViewDelegate>
+
+/*! @fn init
+ @brief Please use initWithCompletionHandler.
+ */
+- (nullable instancetype)init NS_UNAVAILABLE;
+
+/*! @fn initWithCompletionHandler:
+ @brief Designated initializer.
+ @param completionHandler The block to call when the alert view is dismissed.
+ */
+- (nullable instancetype)initWithCompletionHandler:(AlertPromptCompletionBlock)completionHandler
+ NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@implementation UIViewController (Alerts)
+
+- (void)setUseStatusBarSpinner:(BOOL)useStatusBarSpinner {
+ objc_setAssociatedObject(self,
+ &kUseStatusBarSpinnerAssociatedObjectKey,
+ useStatusBarSpinner ? @(YES) : nil,
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+
+- (BOOL)useStatusBarSpinner {
+ return objc_getAssociatedObject(self, &kUseStatusBarSpinnerAssociatedObjectKey) ? YES : NO;
+}
+
+/*! @fn supportsAlertController
+ @brief Determines if the current platform supports @c UIAlertController.
+ @return YES if the current platform supports @c UIAlertController.
+ */
+- (BOOL)supportsAlertController {
+ return NSClassFromString(@"UIAlertController") != nil;
+}
+
+- (void)showMessagePrompt:(NSString *)message {
+ [self showMessagePromptWithTitle:nil message:message showCancelButton:NO completion:nil];
+}
+
+- (void)showMessagePromptWithTitle:(nullable NSString *)title
+ message:(NSString *)message
+ showCancelButton:(BOOL)showCancelButton
+ completion:(nullable AlertPromptCompletionBlock)completion {
+ if (message) {
+ [UIPasteboard generalPasteboard].string = message;
+ }
+ if ([self supportsAlertController]) {
+ UIAlertController *alert =
+ [UIAlertController alertControllerWithTitle:title
+ message:message
+ preferredStyle:UIAlertControllerStyleAlert];
+ UIAlertAction *okAction =
+ [UIAlertAction actionWithTitle:kOK
+ style:UIAlertActionStyleDefault
+ handler:^(UIAlertAction * _Nonnull action) {
+ if (completion) {
+ completion(YES, nil);
+ }
+ }];
+ [alert addAction:okAction];
+
+ if (showCancelButton) {
+ UIAlertAction *cancelAction =
+ [UIAlertAction actionWithTitle:kCancel
+ style:UIAlertActionStyleCancel
+ handler:^(UIAlertAction * _Nonnull action) {
+ completion(NO, nil);
+ }];
+ [alert addAction:cancelAction];
+ }
+ [self presentViewController:alert animated:YES completion:nil];
+ } else {
+ UIAlertView *alert =
+ [[UIAlertView alloc] initWithTitle:title
+ message:message
+ delegate:nil
+ cancelButtonTitle:nil
+ otherButtonTitles:kOK, nil];
+ [alert show];
+ }
+}
+
+- (void)showTextInputPromptWithMessage:(NSString *)message
+ completionBlock:(AlertPromptCompletionBlock)completion {
+ [self showTextInputPromptWithMessage:message
+ keyboardType:UIKeyboardTypeDefault
+ completionBlock:completion];
+}
+
+- (void)showTextInputPromptWithMessage:(NSString *)message
+ keyboardType:(UIKeyboardType)keyboardType
+ completionBlock:(nonnull AlertPromptCompletionBlock)completion {
+ if ([self supportsAlertController]) {
+ UIAlertController *prompt =
+ [UIAlertController alertControllerWithTitle:nil
+ message:message
+ preferredStyle:UIAlertControllerStyleAlert];
+ __weak UIAlertController *weakPrompt = prompt;
+ UIAlertAction *cancelAction =
+ [UIAlertAction actionWithTitle:kCancel
+ style:UIAlertActionStyleCancel
+ handler:^(UIAlertAction * _Nonnull action) {
+ completion(NO, nil);
+ }];
+ UIAlertAction *okAction = [UIAlertAction actionWithTitle:kOK
+ style:UIAlertActionStyleDefault
+ handler:^(UIAlertAction * _Nonnull action) {
+ UIAlertController *strongPrompt = weakPrompt;
+ completion(YES, strongPrompt.textFields[0].text);
+ }];
+ [prompt addTextFieldWithConfigurationHandler:^(UITextField *_Nonnull textField) {
+ textField.keyboardType = keyboardType;
+ }];
+ [prompt addAction:cancelAction];
+ [prompt addAction:okAction];
+ [self presentViewController:prompt animated:YES completion:nil];
+ } else {
+ SimpleTextPromptDelegate *prompt =
+ [[SimpleTextPromptDelegate alloc] initWithCompletionHandler:completion];
+ UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:nil
+ message:message
+ delegate:prompt
+ cancelButtonTitle:@"Cancel"
+ otherButtonTitles:@"Ok", nil];
+ alertView.alertViewStyle = UIAlertViewStylePlainTextInput;
+ [alertView show];
+ }
+}
+
+- (void)showSpinner:(nullable void(^)(void))completion {
+ if (self.useStatusBarSpinner) {
+ [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
+ completion();
+ return;
+ }
+ if ([self supportsAlertController]) {
+ [self showModernSpinner:completion];
+ } else {
+ [self showIOS7Spinner:completion];
+ }
+}
+
+- (void)showModernSpinner:(nullable void (^)(void))completion {
+ UIAlertController *pleaseWaitAlert =
+ objc_getAssociatedObject(self,
+ (__bridge const void *)kPleaseWaitAssociatedObjectKey);
+ if (pleaseWaitAlert) {
+ if (completion) {
+ completion();
+ }
+ return;
+ }
+ pleaseWaitAlert = [UIAlertController alertControllerWithTitle:nil
+ message:@"Please Wait...\n\n\n\n"
+ preferredStyle:UIAlertControllerStyleAlert];
+
+ UIActivityIndicatorView *spinner =
+ [[UIActivityIndicatorView alloc]
+ initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
+ spinner.color = [UIColor blackColor];
+ spinner.center = CGPointMake(pleaseWaitAlert.view.bounds.size.width / 2,
+ pleaseWaitAlert.view.bounds.size.height / 2);
+ spinner.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin |
+ UIViewAutoresizingFlexibleTopMargin |
+ UIViewAutoresizingFlexibleLeftMargin |
+ UIViewAutoresizingFlexibleRightMargin;
+ [spinner startAnimating];
+ [pleaseWaitAlert.view addSubview:spinner];
+
+ objc_setAssociatedObject(self,
+ (__bridge const void *)(kPleaseWaitAssociatedObjectKey),
+ pleaseWaitAlert,
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ [self presentViewController:pleaseWaitAlert animated:YES completion:completion];
+}
+
+- (void)showIOS7Spinner:(nullable void (^)(void))completion {
+ UIWindow *pleaseWaitWindow =
+ objc_getAssociatedObject(self,
+ (__bridge const void *)kPleaseWaitAssociatedObjectKey);
+
+ if (pleaseWaitWindow) {
+ if (completion) {
+ completion();
+ }
+ return;
+ }
+
+ pleaseWaitWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
+ pleaseWaitWindow.backgroundColor = [UIColor clearColor];
+ pleaseWaitWindow.windowLevel = UIWindowLevelStatusBar - 1;
+
+ UIView *pleaseWaitView = [[UIView alloc] initWithFrame:pleaseWaitWindow.bounds];
+ pleaseWaitView.autoresizingMask = UIViewAutoresizingFlexibleWidth |
+ UIViewAutoresizingFlexibleHeight;
+ pleaseWaitView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5];
+ UIActivityIndicatorView *spinner =
+ [[UIActivityIndicatorView alloc]
+ initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
+ spinner.center = pleaseWaitView.center;
+ [pleaseWaitView addSubview:spinner];
+ [spinner startAnimating];
+
+ pleaseWaitView.layer.opacity = 0.0;
+ [self.view addSubview:pleaseWaitView];
+
+ [pleaseWaitWindow addSubview:pleaseWaitView];
+
+ [pleaseWaitWindow makeKeyAndVisible];
+
+ objc_setAssociatedObject(self,
+ (__bridge const void *)(kPleaseWaitAssociatedObjectKey),
+ pleaseWaitWindow,
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+
+ [UIView animateWithDuration:0.5f animations:^{
+ pleaseWaitView.layer.opacity = 1.0f;
+ } completion:^(BOOL finished) {
+ if (completion) {
+ completion();
+ }
+ }];
+}
+
+- (void)hideSpinner:(nullable void(^)(void))completion {
+ if (self.useStatusBarSpinner) {
+ [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
+ completion();
+ return;
+ }
+ if ([self supportsAlertController]) {
+ [self hideModernSpinner:completion];
+ } else {
+ [self hideIOS7Spinner:completion];
+ }
+}
+
+- (void)hideModernSpinner:(nullable void(^)(void))completion {
+ UIAlertController *pleaseWaitAlert =
+ objc_getAssociatedObject(self,
+ (__bridge const void *)kPleaseWaitAssociatedObjectKey);
+
+ [pleaseWaitAlert dismissViewControllerAnimated:YES completion:completion];
+
+ objc_setAssociatedObject(self,
+ (__bridge const void *)(kPleaseWaitAssociatedObjectKey),
+ nil,
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+
+- (void)hideIOS7Spinner:(nullable void(^)(void))completion {
+ UIWindow *pleaseWaitWindow =
+ objc_getAssociatedObject(self,
+ (__bridge const void *)kPleaseWaitAssociatedObjectKey);
+
+ UIView *pleaseWaitView;
+ pleaseWaitView = pleaseWaitWindow.subviews.firstObject;
+
+ [UIView animateWithDuration:0.5f animations:^{
+ pleaseWaitView.layer.opacity = 0.0f;
+ } completion:^(BOOL finished) {
+ [pleaseWaitWindow resignKeyWindow];
+ objc_setAssociatedObject(self,
+ (__bridge const void *)(kPleaseWaitAssociatedObjectKey),
+ nil,
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ if (completion) {
+ completion();
+ }
+ }];
+}
+
+@end
+
+@implementation SimpleTextPromptDelegate {
+ AlertPromptCompletionBlock _completionHandler;
+ SimpleTextPromptDelegate *_retainedSelf;
+}
+
+- (instancetype)initWithCompletionHandler:(AlertPromptCompletionBlock)completionHandler {
+ self = [super init];
+ if (self) {
+ _completionHandler = completionHandler;
+ _retainedSelf = self;
+ }
+ return self;
+}
+
+- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
+ if (buttonIndex == alertView.firstOtherButtonIndex) {
+ _completionHandler(YES, [alertView textFieldAtIndex:0].text);
+ } else {
+ _completionHandler(NO, nil);
+ }
+ _completionHandler = nil;
+ _retainedSelf = nil;
+}
+
+@end
diff --git a/Example/Auth/Sample/UserInfoViewController.h b/Example/Auth/Sample/UserInfoViewController.h
new file mode 100644
index 0000000..de19bfc
--- /dev/null
+++ b/Example/Auth/Sample/UserInfoViewController.h
@@ -0,0 +1,55 @@
+/*
+ * 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 FIRUser;
+@class StaticContentTableViewManager;
+
+/** @class UserInfoViewController
+ @brief A view controller for displaying @c FIRUser data.
+ */
+@interface UserInfoViewController : UIViewController
+
+/** @property tableViewManager
+ @brief A @c StaticContentTableViewManager which is used to manage the contents of the table
+ view.
+ */
+@property(nonatomic, strong) IBOutlet StaticContentTableViewManager *tableViewManager;
+
+/** @fn initWithUser:
+ @biref Initializes with a @c FIRUser instance.
+ @param user The user to be displayed in the view.
+ */
+- (instancetype)initWithUser:(FIRUser *)user NS_DESIGNATED_INITIALIZER;
+
+/** @fn initWithNibName:bundle:
+ @brief Not available. Call initWithUser: instead.
+ */
+- (instancetype)initWithNibName:(NSString *)nibNameOrNil
+ bundle:(NSBundle *)nibBundleOrNil NS_UNAVAILABLE;
+
+/** @fn initWithCoder:
+ @brief Not available. Call initWithUser: instead.
+ */
+- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE;
+
+/** @fn done
+ @brief Called when user taps the "Done" button.
+ */
+- (IBAction)done:(id)sender;
+
+@end
diff --git a/Example/Auth/Sample/UserInfoViewController.m b/Example/Auth/Sample/UserInfoViewController.m
new file mode 100644
index 0000000..aa5b7fe
--- /dev/null
+++ b/Example/Auth/Sample/UserInfoViewController.m
@@ -0,0 +1,79 @@
+/*
+ * 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 "UserInfoViewController.h"
+
+#import "FIRUser.h"
+#import "FIRUserInfo.h"
+#import "StaticContentTableViewManager.h"
+
+/** @fn stringWithBool
+ @brief Converts a boolean value to a string for display.
+ @param boolValue the boolean value.
+ @return The string form of the boolean value.
+ */
+static NSString *stringWithBool(BOOL boolValue) {
+ return boolValue ? @"YES" : @"NO";
+}
+
+@implementation UserInfoViewController {
+ FIRUser *_user;
+}
+
+- (instancetype)initWithUser:(FIRUser *)user {
+ self = [super initWithNibName:NSStringFromClass([self class]) bundle:nil];
+ if (self) {
+ _user = user;
+ }
+ return self;
+}
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ [self loadTableView];
+}
+
+- (void)loadTableView {
+ NSMutableArray<StaticContentTableViewSection *> *sections = [@[
+ [StaticContentTableViewSection sectionWithTitle:@"User" cells:@[
+ [StaticContentTableViewCell cellWithTitle:@"anonymous" value:stringWithBool(_user.anonymous)],
+ [StaticContentTableViewCell cellWithTitle:@"emailVerified"
+ value:stringWithBool(_user.emailVerified)],
+ [StaticContentTableViewCell cellWithTitle:@"refreshToken" value:_user.refreshToken],
+ ]]
+ ] mutableCopy];
+ [sections addObject:[self sectionWithUserInfo:_user]];
+ for (id<FIRUserInfo> userInfo in _user.providerData) {
+ [sections addObject:[self sectionWithUserInfo:userInfo]];
+ }
+ _tableViewManager.contents = [StaticContentTableViewContent contentWithSections:sections];
+}
+
+- (StaticContentTableViewSection *)sectionWithUserInfo:(id<FIRUserInfo>)userInfo {
+ return [StaticContentTableViewSection sectionWithTitle:userInfo.providerID cells:@[
+ [StaticContentTableViewCell cellWithTitle:@"uid" value:userInfo.uid],
+ [StaticContentTableViewCell cellWithTitle:@"displayName" value:userInfo.displayName],
+ [StaticContentTableViewCell cellWithTitle:@"photoURL" value:[userInfo.photoURL absoluteString]],
+ [StaticContentTableViewCell cellWithTitle:@"email" value:userInfo.email],
+ [StaticContentTableViewCell cellWithTitle:@"phoneNumber" value:userInfo.phoneNumber]
+ ]];
+}
+
+- (IBAction)done:(id)sender {
+ [self dismissViewControllerAnimated:YES completion:nil];
+}
+
+@end
diff --git a/Example/Auth/Sample/UserInfoViewController.xib b/Example/Auth/Sample/UserInfoViewController.xib
new file mode 100644
index 0000000..9f2db98
--- /dev/null
+++ b/Example/Auth/Sample/UserInfoViewController.xib
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="9531" systemVersion="15D21" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9529"/>
+ </dependencies>
+ <objects>
+ <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="UserInfoViewController">
+ <connections>
+ <outlet property="tableViewManager" destination="U72-zk-rRG" id="KxN-ZQ-cAa"/>
+ <outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
+ </connections>
+ </placeholder>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+ <view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rRb-jS-QTO">
+ <rect key="frame" x="8" y="16" width="46" height="30"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="30" id="6TV-T6-63H"/>
+ <constraint firstAttribute="width" constant="46" id="kVF-SE-Qzn"/>
+ </constraints>
+ <state key="normal" title="Done"/>
+ <connections>
+ <action selector="done:" destination="-1" eventType="touchUpInside" id="O91-6u-80J"/>
+ </connections>
+ </button>
+ <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" translatesAutoresizingMaskIntoConstraints="NO" id="4BU-Kb-3Zx">
+ <rect key="frame" x="0.0" y="54" width="600" height="546"/>
+ <color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
+ <connections>
+ <outlet property="dataSource" destination="U72-zk-rRG" id="GFT-r0-uBK"/>
+ <outlet property="delegate" destination="U72-zk-rRG" id="uzl-Rm-2qd"/>
+ </connections>
+ </tableView>
+ </subviews>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ <constraints>
+ <constraint firstItem="rRb-jS-QTO" firstAttribute="leading" secondItem="i5M-Pr-FkT" secondAttribute="leading" constant="8" id="1PF-3b-y4w"/>
+ <constraint firstItem="4BU-Kb-3Zx" firstAttribute="width" secondItem="i5M-Pr-FkT" secondAttribute="width" id="5Tz-J0-2Dq"/>
+ <constraint firstItem="rRb-jS-QTO" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" constant="16" id="K7t-0g-JBo"/>
+ <constraint firstItem="4BU-Kb-3Zx" firstAttribute="top" secondItem="rRb-jS-QTO" secondAttribute="bottom" constant="8" id="MyI-Bl-nGS"/>
+ <constraint firstAttribute="bottom" secondItem="4BU-Kb-3Zx" secondAttribute="bottom" id="QMm-Wv-q60"/>
+ <constraint firstItem="4BU-Kb-3Zx" firstAttribute="centerX" secondItem="i5M-Pr-FkT" secondAttribute="centerX" id="deo-wi-Nmq"/>
+ </constraints>
+ </view>
+ <customObject id="U72-zk-rRG" customClass="StaticContentTableViewManager">
+ <connections>
+ <outlet property="tableView" destination="4BU-Kb-3Zx" id="0OK-KI-QdH"/>
+ </connections>
+ </customObject>
+ </objects>
+</document>
diff --git a/Example/Auth/Sample/UserTableViewCell.h b/Example/Auth/Sample/UserTableViewCell.h
new file mode 100644
index 0000000..072b455
--- /dev/null
+++ b/Example/Auth/Sample/UserTableViewCell.h
@@ -0,0 +1,58 @@
+/*
+ * 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 FIRUser;
+
+/** @class UserTableViewCell
+ @brief Represents a user in a table view.
+ */
+@interface UserTableViewCell : UITableViewCell
+
+/** @property userInfoProfileURLImageView
+ @brief A UIImageView whose image is set to the user's profile URL.
+ */
+@property(nonatomic, weak) IBOutlet UIImageView *userInfoProfileURLImageView;
+
+/** @property userInfoDisplayNameLabel
+ @brief A UILabel whose text is set to the user's display name.
+ */
+@property(nonatomic, weak) IBOutlet UILabel *userInfoDisplayNameLabel;
+
+/** @property userInfoEmailLabel
+ @brief A UILabel whose text is set to the user's email.
+ */
+@property(nonatomic, weak) IBOutlet UILabel *userInfoEmailLabel;
+
+/** @property userInfoUserIDLabel
+ @brief A UILabel whose text is set to the user's User ID.
+ */
+@property(nonatomic, weak) IBOutlet UILabel *userInfoUserIDLabel;
+
+/** @property userInfoProviderListLabel
+ @brief A UILabel whose text is set to the user's comma-delimited list of federated sign in
+ provider IDs.
+ */
+@property(nonatomic, weak) IBOutlet UILabel *userInfoProviderListLabel;
+
+/** @fn updateContentsWithUser:
+ @brief Updates the values of the controls on this table view cell to represent the user.
+ @param user The user whose values should be used to populate this cell.
+ */
+- (void)updateContentsWithUser:(FIRUser *)user;
+
+@end
diff --git a/Example/Auth/Sample/UserTableViewCell.m b/Example/Auth/Sample/UserTableViewCell.m
new file mode 100644
index 0000000..fef3025
--- /dev/null
+++ b/Example/Auth/Sample/UserTableViewCell.m
@@ -0,0 +1,55 @@
+/*
+ * 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 "UserTableViewCell.h"
+
+#import "FIRUser.h"
+
+@implementation UserTableViewCell {
+ /** @var _lastPhotoURL
+ @brief Used to make sure only the last requested image is used to update the UIImageView.
+ */
+ NSURL *_lastPhotoURL;
+}
+
+- (void)updateContentsWithUser:(FIRUser *)user {
+ _userInfoDisplayNameLabel.text = user.displayName;
+ _userInfoEmailLabel.text = user.email;
+ _userInfoUserIDLabel.text = user.uid;
+
+ NSMutableArray<NSString *> *providerIDs = [NSMutableArray array];
+ for (id<FIRUserInfo> userInfo in user.providerData) {
+ [providerIDs addObject:userInfo.providerID];
+ }
+ _userInfoProviderListLabel.text = [providerIDs componentsJoinedByString:@", "];
+
+ NSURL *photoURL = user.photoURL;
+ _lastPhotoURL = photoURL; // to prevent eariler image overwrites later one.
+ if (photoURL) {
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^() {
+ UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:photoURL]];
+ dispatch_async(dispatch_get_main_queue(), ^() {
+ if (photoURL == _lastPhotoURL) {
+ _userInfoProfileURLImageView.image = image;
+ }
+ });
+ });
+ } else {
+ _userInfoProfileURLImageView.image = nil;
+ }
+}
+
+@end
diff --git a/Example/Auth/Sample/en.lproj/Localizable.strings b/Example/Auth/Sample/en.lproj/Localizable.strings
new file mode 100644
index 0000000..bbbc3ae
--- /dev/null
+++ b/Example/Auth/Sample/en.lproj/Localizable.strings
@@ -0,0 +1,2 @@
+/* Text label for button which opens sample app settings */
+"SETTINGSKEY" = "Settings";
diff --git a/Example/Auth/Sample/es-MX.lproj/Localizable.strings b/Example/Auth/Sample/es-MX.lproj/Localizable.strings
new file mode 100644
index 0000000..3a487ce
--- /dev/null
+++ b/Example/Auth/Sample/es-MX.lproj/Localizable.strings
@@ -0,0 +1,2 @@
+/* Text label for button which opens sample app settings */
+"SETTINGSKEY" = "Ajustes";
diff --git a/Example/Auth/Sample/fr-FR.lproj/Localizable.strings b/Example/Auth/Sample/fr-FR.lproj/Localizable.strings
new file mode 100644
index 0000000..a500427
--- /dev/null
+++ b/Example/Auth/Sample/fr-FR.lproj/Localizable.strings
@@ -0,0 +1,2 @@
+/* Text label for button which opens sample app settings */
+"SETTINGSKEY" = "Préférence";
diff --git a/Example/Auth/Sample/main.m b/Example/Auth/Sample/main.m
new file mode 100644
index 0000000..1eadced
--- /dev/null
+++ b/Example/Auth/Sample/main.m
@@ -0,0 +1,23 @@
+/*
+ * 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 "ApplicationDelegate.h"
+
+int main(int argc, char *argv[]) {
+ @autoreleasepool {
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass([ApplicationDelegate class]));
+ }
+}
diff --git a/Example/Auth/Sample/ru-RU.lproj/Localizable.strings b/Example/Auth/Sample/ru-RU.lproj/Localizable.strings
new file mode 100644
index 0000000..7b8fc66
--- /dev/null
+++ b/Example/Auth/Sample/ru-RU.lproj/Localizable.strings
@@ -0,0 +1,2 @@
+/* Text label for button which opens sample app settings */
+"SETTINGSKEY" = "настройки";
diff --git a/Example/Auth/Sample/zh-Hans.lproj/Localizable.strings b/Example/Auth/Sample/zh-Hans.lproj/Localizable.strings
new file mode 100644
index 0000000..4ec1148
--- /dev/null
+++ b/Example/Auth/Sample/zh-Hans.lproj/Localizable.strings
@@ -0,0 +1,2 @@
+/* Text label for button which opens sample app settings */
+"SETTINGSKEY" = "设置";
diff --git a/Example/Auth/SwiftSample/AppDelegate.swift b/Example/Auth/SwiftSample/AppDelegate.swift
new file mode 100644
index 0000000..579385c
--- /dev/null
+++ b/Example/Auth/SwiftSample/AppDelegate.swift
@@ -0,0 +1,62 @@
+/*
+ * 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
+
+import FirebaseCommunity.FirebaseCore
+import GoogleSignIn
+
+@UIApplicationMain
+class AppDelegate: UIResponder, UIApplicationDelegate {
+
+ /** @var kGoogleClientID
+ @brief The Google client ID.
+ */
+ private let kGoogleClientID = AuthCredentials.GOOGLE_CLIENT_ID
+
+ // TODO: add Facebook login support as well.
+
+ /** @var kFacebookAppID
+ @brief The Facebook app ID.
+ */
+ private let kFacebookAppID = AuthCredentials.FACEBOOK_APP_ID
+
+ /** @var window
+ @brief The main window of the app.
+ */
+ var window: UIWindow?
+
+ func application(_ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
+ FirebaseApp.configure()
+ GIDSignIn.sharedInstance().clientID = kGoogleClientID
+ return true
+ }
+
+ @available(iOS 9.0, *)
+ func application(_ application: UIApplication, open url: URL,
+ options: [UIApplicationOpenURLOptionsKey : Any]) -> Bool {
+ return GIDSignIn.sharedInstance().handle(url,
+ sourceApplication: options[UIApplicationOpenURLOptionsKey.sourceApplication] as! String,
+ annotation: nil)
+ }
+
+ func application(_ application: UIApplication, open url: URL, sourceApplication: String?,
+ annotation: Any) -> Bool {
+ return GIDSignIn.sharedInstance().handle(url, sourceApplication: sourceApplication,
+ annotation: annotation)
+ }
+}
diff --git a/Example/Auth/SwiftSample/AuthCredentialsTemplate.swift b/Example/Auth/SwiftSample/AuthCredentialsTemplate.swift
new file mode 100644
index 0000000..eea9335
--- /dev/null
+++ b/Example/Auth/SwiftSample/AuthCredentialsTemplate.swift
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+
+/*
+ Some of the Auth Credentials needs to be populated for the Sample build to work.
+
+ Please follow the following steps to populate the valid AuthCredentials
+ and copy it to AuthCredentials.swift file
+
+ You will need to replace the following values:
+ $KGOOGLE_CLIENT_ID
+ Get the value of the CLIENT_ID key in the GoogleService-Info.plist file.
+
+ $KFACEBOOK_APP_ID
+ FACEBOOK_APP_ID is the developer's Facebook app's ID, to be used to test the
+ 'Signing in with Facebook' feature of Firebase Auth. Follow the instructions
+ on the Facebook developer site: https://developers.facebook.com/docs/apps/register
+ to obtain the id
+
+ */
+
+import Foundation
+
+
+struct AuthCredentials {
+ static let FACEBOOK_APP_ID = "$KFACEBOOK_APP_ID"
+ static let GOOGLE_CLIENT_ID = "$KGOOGLE_CLIENT_ID"
+}
diff --git a/Example/Auth/SwiftSample/InfoTemplate.plist b/Example/Auth/SwiftSample/InfoTemplate.plist
new file mode 100644
index 0000000..e51622b
--- /dev/null
+++ b/Example/Auth/SwiftSample/InfoTemplate.plist
@@ -0,0 +1,79 @@
+<!--
+ For this to be a valid plist file replace the following
+ $REVERSE_CLIENT_ID:
+ Value of REVERSED_CLIENT_ID key in the GoogleService-Info.plist file.
+-->
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleURLTypes</key>
+ <array>
+ <dict>
+ <key>CFBundleTypeRole</key>
+ <string>Editor</string>
+ <key>CFBundleURLName</key>
+ <string>com.google.swiftbear</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>com.google.swiftbear</string>
+ </array>
+ </dict>
+ <dict>
+ <key>CFBundleTypeRole</key>
+ <string>Editor</string>
+ <key>CFBundleURLName</key>
+ <string>$REVERSE_CLIENT_ID</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>$REVERSE_CLIENT_ID</string>
+ </array>
+ </dict>
+ </array>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>armv7</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>LSApplicationQueriesSchemes</key>
+ <array>
+ <string>fbauth2</string>
+ </array>
+</dict>
+</plist>
diff --git a/Example/Auth/SwiftSample/LaunchScreen.storyboard b/Example/Auth/SwiftSample/LaunchScreen.storyboard
new file mode 100644
index 0000000..8326657
--- /dev/null
+++ b/Example/Auth/SwiftSample/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="9531" systemVersion="15D21" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9529"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+ <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Example/Auth/SwiftSample/Main.storyboard b/Example/Auth/SwiftSample/Main.storyboard
new file mode 100644
index 0000000..3525a8b
--- /dev/null
+++ b/Example/Auth/SwiftSample/Main.storyboard
@@ -0,0 +1,227 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12120" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
+ <device id="retina4_7" orientation="portrait">
+ <adaptation id="fullscreen"/>
+ </device>
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/>
+ <capability name="Constraints to layout margins" minToolsVersion="6.0"/>
+ <capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
+ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="tne-QT-ifu">
+ <objects>
+ <viewController id="BYZ-38-t0r" customClass="ViewController" customModule="SwiftSample" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
+ <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+ <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6HK-es-gyf">
+ <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+ <subviews>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Phone" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c6m-cS-hAh">
+ <rect key="frame" x="185" y="426" width="164" height="30"/>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
+ <nil key="textColor"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Password" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sTr-Hf-KVD">
+ <rect key="frame" x="185" y="366" width="164" height="30"/>
+ <fontDescription key="fontDescription" type="system" pointSize="20"/>
+ <nil key="textColor"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" clearButtonMode="always" translatesAutoresizingMaskIntoConstraints="NO" id="zIt-Mq-gk3">
+ <rect key="frame" x="185" y="336" width="164" height="30"/>
+ <nil key="textColor"/>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <textInputTraits key="textInputTraits"/>
+ </textField>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ria-OG-Nbj">
+ <rect key="frame" x="105" y="98" width="264" height="25"/>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
+ <color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ObY-8p-vcQ">
+ <rect key="frame" x="105" y="48" width="264" height="25"/>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
+ <color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <pickerView contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="QOn-3b-keq">
+ <rect key="frame" x="99" y="131" width="250" height="167"/>
+ <connections>
+ <outlet property="dataSource" destination="BYZ-38-t0r" id="2Mt-EE-jxl"/>
+ <outlet property="delegate" destination="BYZ-38-t0r" id="qCy-Bt-yNL"/>
+ </connections>
+ </pickerView>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cWW-O4-BEO">
+ <rect key="frame" x="105" y="23" width="264" height="25"/>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
+ <color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="31t-pr-Jxe">
+ <rect key="frame" x="121" y="565" width="132" height="53"/>
+ <constraints>
+ <constraint firstAttribute="width" constant="132" id="Qvn-OR-BdF"/>
+ <constraint firstAttribute="height" constant="53" id="ST0-u0-dQ9"/>
+ </constraints>
+ <fontDescription key="fontDescription" type="system" pointSize="24"/>
+ <state key="normal" title="Execute"/>
+ <connections>
+ <action selector="execute:" destination="BYZ-38-t0r" eventType="touchUpInside" id="wnf-4v-z5f"/>
+ </connections>
+ </button>
+ <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="D0S-Sb-YNu">
+ <rect key="frame" x="5" y="23" width="100" height="100"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="100" id="3vv-V5-uVt"/>
+ <constraint firstAttribute="width" constant="100" id="Co2-YK-P0d"/>
+ </constraints>
+ </imageView>
+ <pickerView contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cpy-NT-JJH">
+ <rect key="frame" x="5" y="131" width="86" height="167"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="167" id="3Pc-9v-A6F"/>
+ <constraint firstAttribute="width" constant="86" id="l7M-4A-dOO"/>
+ </constraints>
+ <connections>
+ <outlet property="dataSource" destination="BYZ-38-t0r" id="of7-wI-QNH"/>
+ <outlet property="delegate" destination="BYZ-38-t0r" id="Pfb-UW-zEF"/>
+ </connections>
+ </pickerView>
+ <textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="gB8-2Z-VH6">
+ <rect key="frame" x="185" y="454" width="164" height="30"/>
+ <nil key="textColor"/>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <textInputTraits key="textInputTraits" keyboardType="phonePad"/>
+ </textField>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Email" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="EQe-sz-7yJ">
+ <rect key="frame" x="185" y="306" width="164" height="30"/>
+ <constraints>
+ <constraint firstAttribute="width" constant="164" id="rEx-eC-W8w"/>
+ </constraints>
+ <fontDescription key="fontDescription" type="system" pointSize="20"/>
+ <nil key="textColor"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <pickerView contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="QFf-6f-e46">
+ <rect key="frame" x="5" y="306" width="164" height="216"/>
+ <constraints>
+ <constraint firstAttribute="width" constant="164" id="T6v-LA-XU8"/>
+ </constraints>
+ <connections>
+ <outlet property="dataSource" destination="BYZ-38-t0r" id="ZEt-kl-Eml"/>
+ <outlet property="delegate" destination="BYZ-38-t0r" id="Bl0-Bi-SdS"/>
+ </connections>
+ </pickerView>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="8" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ZxB-fU-vRm">
+ <rect key="frame" x="105" y="73" width="264" height="25"/>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
+ <color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" clearButtonMode="always" translatesAutoresizingMaskIntoConstraints="NO" id="sTA-Ti-DZm">
+ <rect key="frame" x="185" y="396" width="164" height="30"/>
+ <nil key="textColor"/>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <textInputTraits key="textInputTraits"/>
+ </textField>
+ </subviews>
+ <constraints>
+ <constraint firstItem="ObY-8p-vcQ" firstAttribute="centerX" secondItem="cWW-O4-BEO" secondAttribute="centerX" id="0ib-aj-WX2"/>
+ <constraint firstItem="sTr-Hf-KVD" firstAttribute="width" secondItem="sTA-Ti-DZm" secondAttribute="width" id="2D7-Mj-W0C"/>
+ <constraint firstItem="zIt-Mq-gk3" firstAttribute="leading" secondItem="sTr-Hf-KVD" secondAttribute="leading" id="2NF-el-Jrp"/>
+ <constraint firstItem="sTA-Ti-DZm" firstAttribute="top" secondItem="sTr-Hf-KVD" secondAttribute="bottom" id="5Pb-vj-i2t"/>
+ <constraint firstItem="zIt-Mq-gk3" firstAttribute="width" secondItem="sTr-Hf-KVD" secondAttribute="width" id="5cR-Eg-Mok"/>
+ <constraint firstItem="c6m-cS-hAh" firstAttribute="height" secondItem="gB8-2Z-VH6" secondAttribute="height" id="5jm-To-vnO"/>
+ <constraint firstItem="gB8-2Z-VH6" firstAttribute="top" secondItem="sTA-Ti-DZm" secondAttribute="bottom" constant="28" id="8X6-in-hde"/>
+ <constraint firstItem="sTA-Ti-DZm" firstAttribute="width" secondItem="c6m-cS-hAh" secondAttribute="width" id="9C4-yu-4ce"/>
+ <constraint firstItem="c6m-cS-hAh" firstAttribute="width" secondItem="gB8-2Z-VH6" secondAttribute="width" id="9HL-oU-ywU"/>
+ <constraint firstItem="EQe-sz-7yJ" firstAttribute="height" secondItem="zIt-Mq-gk3" secondAttribute="height" id="AvD-fF-buO"/>
+ <constraint firstItem="EQe-sz-7yJ" firstAttribute="leading" secondItem="zIt-Mq-gk3" secondAttribute="leading" id="Ava-E1-SS0"/>
+ <constraint firstItem="sTr-Hf-KVD" firstAttribute="leading" secondItem="sTA-Ti-DZm" secondAttribute="leading" id="BMW-DF-S5S"/>
+ <constraint firstItem="31t-pr-Jxe" firstAttribute="top" secondItem="QFf-6f-e46" secondAttribute="bottom" constant="43" id="Em1-jR-L4v"/>
+ <constraint firstItem="ZxB-fU-vRm" firstAttribute="top" secondItem="ObY-8p-vcQ" secondAttribute="bottom" id="F0f-ea-9iX"/>
+ <constraint firstItem="D0S-Sb-YNu" firstAttribute="leading" secondItem="cpy-NT-JJH" secondAttribute="leading" id="FUv-hM-9eb"/>
+ <constraint firstAttribute="trailing" secondItem="QOn-3b-keq" secondAttribute="trailing" constant="26" id="GWC-2P-5UF"/>
+ <constraint firstItem="EQe-sz-7yJ" firstAttribute="top" secondItem="QFf-6f-e46" secondAttribute="top" id="Gsq-E6-6wg"/>
+ <constraint firstItem="zIt-Mq-gk3" firstAttribute="top" secondItem="EQe-sz-7yJ" secondAttribute="bottom" id="HER-e5-sOc"/>
+ <constraint firstItem="EQe-sz-7yJ" firstAttribute="width" secondItem="zIt-Mq-gk3" secondAttribute="width" id="IWr-QC-GQL"/>
+ <constraint firstItem="QOn-3b-keq" firstAttribute="trailing" secondItem="EQe-sz-7yJ" secondAttribute="trailing" id="JWT-sJ-Xwy"/>
+ <constraint firstItem="QOn-3b-keq" firstAttribute="leading" secondItem="cpy-NT-JJH" secondAttribute="trailing" constant="8" id="QIa-72-bhH"/>
+ <constraint firstItem="c6m-cS-hAh" firstAttribute="centerX" secondItem="gB8-2Z-VH6" secondAttribute="centerX" id="QKW-7H-lfO"/>
+ <constraint firstItem="ria-OG-Nbj" firstAttribute="centerX" secondItem="ZxB-fU-vRm" secondAttribute="centerX" id="RGq-CF-6Ca"/>
+ <constraint firstItem="QFf-6f-e46" firstAttribute="top" secondItem="QOn-3b-keq" secondAttribute="bottom" constant="8" id="RGv-Ix-KP5"/>
+ <constraint firstItem="D0S-Sb-YNu" firstAttribute="top" secondItem="6HK-es-gyf" secondAttribute="topMargin" constant="15" id="S58-uf-nql"/>
+ <constraint firstItem="ria-OG-Nbj" firstAttribute="height" secondItem="ZxB-fU-vRm" secondAttribute="height" id="S8E-ed-GlT"/>
+ <constraint firstItem="sTA-Ti-DZm" firstAttribute="height" secondItem="c6m-cS-hAh" secondAttribute="height" id="UpF-XL-ZNe"/>
+ <constraint firstItem="QOn-3b-keq" firstAttribute="top" secondItem="D0S-Sb-YNu" secondAttribute="bottom" constant="8" id="VTf-55-gwC"/>
+ <constraint firstItem="c6m-cS-hAh" firstAttribute="top" secondItem="sTA-Ti-DZm" secondAttribute="bottom" id="VZF-fX-C3a"/>
+ <constraint firstItem="sTr-Hf-KVD" firstAttribute="top" secondItem="zIt-Mq-gk3" secondAttribute="bottom" id="Vm8-Fh-aXe"/>
+ <constraint firstItem="cWW-O4-BEO" firstAttribute="leading" secondItem="D0S-Sb-YNu" secondAttribute="trailing" id="YLB-S5-D80"/>
+ <constraint firstItem="ZxB-fU-vRm" firstAttribute="width" secondItem="ObY-8p-vcQ" secondAttribute="width" id="b1Y-MS-gng"/>
+ <constraint firstItem="sTr-Hf-KVD" firstAttribute="height" secondItem="sTA-Ti-DZm" secondAttribute="height" id="bNv-hH-Noc"/>
+ <constraint firstItem="cpy-NT-JJH" firstAttribute="centerY" secondItem="QOn-3b-keq" secondAttribute="centerY" id="cQ7-Bu-P8o"/>
+ <constraint firstAttribute="trailing" secondItem="cWW-O4-BEO" secondAttribute="trailing" constant="6" id="d3V-Nv-JAE"/>
+ <constraint firstItem="zIt-Mq-gk3" firstAttribute="height" secondItem="sTr-Hf-KVD" secondAttribute="height" id="eEX-vS-Ado"/>
+ <constraint firstItem="ria-OG-Nbj" firstAttribute="width" secondItem="ZxB-fU-vRm" secondAttribute="width" id="fqd-aO-UVU"/>
+ <constraint firstItem="ZxB-fU-vRm" firstAttribute="height" secondItem="ObY-8p-vcQ" secondAttribute="height" id="ghn-WL-o6u"/>
+ <constraint firstItem="ZxB-fU-vRm" firstAttribute="centerX" secondItem="ObY-8p-vcQ" secondAttribute="centerX" id="in5-Lv-vIw"/>
+ <constraint firstItem="cpy-NT-JJH" firstAttribute="leading" secondItem="QFf-6f-e46" secondAttribute="leading" id="kF3-HN-LbD"/>
+ <constraint firstItem="sTA-Ti-DZm" firstAttribute="centerX" secondItem="c6m-cS-hAh" secondAttribute="centerX" id="kiI-Xh-5xo"/>
+ <constraint firstItem="cWW-O4-BEO" firstAttribute="height" secondItem="D0S-Sb-YNu" secondAttribute="height" multiplier="0.25" id="mKj-fA-ORq"/>
+ <constraint firstItem="D0S-Sb-YNu" firstAttribute="leading" secondItem="6HK-es-gyf" secondAttribute="leading" constant="5" id="nBQ-cr-fzi"/>
+ <constraint firstItem="ria-OG-Nbj" firstAttribute="top" secondItem="ZxB-fU-vRm" secondAttribute="bottom" id="pa9-NP-Hfc"/>
+ <constraint firstItem="gB8-2Z-VH6" firstAttribute="top" secondItem="c6m-cS-hAh" secondAttribute="bottom" constant="-2" id="qyL-2s-Bk6"/>
+ <constraint firstItem="cWW-O4-BEO" firstAttribute="top" secondItem="D0S-Sb-YNu" secondAttribute="top" id="r4K-Xo-2Xj"/>
+ <constraint firstItem="ObY-8p-vcQ" firstAttribute="top" secondItem="cWW-O4-BEO" secondAttribute="bottom" id="sSS-aL-VRW"/>
+ <constraint firstItem="ObY-8p-vcQ" firstAttribute="width" secondItem="cWW-O4-BEO" secondAttribute="width" id="vme-8e-MMM"/>
+ <constraint firstItem="EQe-sz-7yJ" firstAttribute="leading" secondItem="QFf-6f-e46" secondAttribute="trailing" constant="16" id="wnz-FV-4IK"/>
+ <constraint firstAttribute="bottom" secondItem="31t-pr-Jxe" secondAttribute="bottom" constant="64" id="yf5-EU-fUm"/>
+ <constraint firstItem="31t-pr-Jxe" firstAttribute="centerX" secondItem="6HK-es-gyf" secondAttribute="centerX" id="ypm-hz-NtG"/>
+ <constraint firstItem="cpy-NT-JJH" firstAttribute="height" secondItem="QOn-3b-keq" secondAttribute="height" id="zEO-wC-OTJ"/>
+ <constraint firstItem="ObY-8p-vcQ" firstAttribute="height" secondItem="cWW-O4-BEO" secondAttribute="height" id="zgf-ji-Ou6"/>
+ </constraints>
+ </scrollView>
+ </subviews>
+ <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+ <constraints>
+ <constraint firstItem="6HK-es-gyf" firstAttribute="bottom" secondItem="wfy-db-euE" secondAttribute="top" id="bTX-8p-mgN"/>
+ <constraint firstItem="6HK-es-gyf" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leading" id="cRU-eQ-2SG"/>
+ <constraint firstItem="6HK-es-gyf" firstAttribute="top" secondItem="8bC-Xf-vdC" secondAttribute="top" id="lgq-Qc-p06"/>
+ <constraint firstAttribute="trailing" secondItem="6HK-es-gyf" secondAttribute="trailing" id="ugY-4G-3fl"/>
+ </constraints>
+ </view>
+ <connections>
+ <outlet property="actionPicker" destination="QOn-3b-keq" id="PFo-aS-Wld"/>
+ <outlet property="actionTypePicker" destination="cpy-NT-JJH" id="zve-OZ-Zfa"/>
+ <outlet property="credentialTypePicker" destination="QFf-6f-e46" id="JP6-KS-WlB"/>
+ <outlet property="displayNameLabel" destination="cWW-O4-BEO" id="7Yf-D1-gbW"/>
+ <outlet property="emailField" destination="zIt-Mq-gk3" id="3wb-SN-KxQ"/>
+ <outlet property="emailInputLabel" destination="EQe-sz-7yJ" id="ARJ-Sl-Hy5"/>
+ <outlet property="emailLabel" destination="ObY-8p-vcQ" id="7LH-QE-OZ3"/>
+ <outlet property="passwordField" destination="sTA-Ti-DZm" id="vGb-gH-1BI"/>
+ <outlet property="passwordInputLabel" destination="sTr-Hf-KVD" id="8uU-J5-Y2Z"/>
+ <outlet property="phoneField" destination="gB8-2Z-VH6" id="nUb-F7-u3i"/>
+ <outlet property="profileImage" destination="D0S-Sb-YNu" id="goh-E4-oQW"/>
+ <outlet property="providerListLabel" destination="ria-OG-Nbj" id="j0I-72-FH7"/>
+ <outlet property="scrollView" destination="6HK-es-gyf" id="lA9-BY-R9E"/>
+ <outlet property="userIDLabel" destination="ZxB-fU-vRm" id="eyR-59-0Os"/>
+ </connections>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="304.80000000000001" y="428.63568215892059"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Example/Auth/SwiftSample/Sample.entitlements b/Example/Auth/SwiftSample/Sample.entitlements
new file mode 100644
index 0000000..9199dae
--- /dev/null
+++ b/Example/Auth/SwiftSample/Sample.entitlements
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>application-identifier</key>
+ <string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
+ <key>aps-environment</key>
+ <string>development</string>
+</dict>
+</plist>
diff --git a/Example/Auth/SwiftSample/Stubs.swift b/Example/Auth/SwiftSample/Stubs.swift
new file mode 100644
index 0000000..6733c4d
--- /dev/null
+++ b/Example/Auth/SwiftSample/Stubs.swift
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+
+/// This file contains a collection of stub functions to verify the Swift syntax of Firebase Auth
+/// APIs in Swift for those that are not already covered by other parts of the app.
+/// These functions are never executed, but just for passing compilation.
+
+import FirebaseCommunity.FirebaseAuth
+
+func actionCodeSettingsStubs() {
+ let actionCodeSettings = ActionCodeSettings()
+ actionCodeSettings.url = URL(string: "http://some.url/path/")
+ actionCodeSettings.setIOSBundleID("some.bundle.id")
+ actionCodeSettings.setAndroidPackageName("some.package.name", installIfNotAvailable: true,
+ minimumVersion: nil)
+ let _: String? = actionCodeSettings.iOSBundleID
+ let _: String? = actionCodeSettings.androidPackageName
+ let _: Bool = actionCodeSettings.androidInstallIfNotAvailable
+ let _: String? = actionCodeSettings.androidMinimumVersion
+ Auth.auth().sendPasswordReset(withEmail: "nobody@nowhere.com",
+ actionCodeSettings: actionCodeSettings) { (error: Error?) -> () in
+ }
+ Auth.auth().currentUser?.sendEmailVerification(with: actionCodeSettings) {
+ (error: Error?) -> () in
+ }
+}
+
+func languageStubs() {
+ let _: String? = Auth.auth().languageCode
+ Auth.auth().languageCode = "asdf"
+ Auth.auth().useAppLanguage()
+}
diff --git a/Example/Auth/SwiftSample/ViewController.swift b/Example/Auth/SwiftSample/ViewController.swift
new file mode 100644
index 0000000..e90b727
--- /dev/null
+++ b/Example/Auth/SwiftSample/ViewController.swift
@@ -0,0 +1,700 @@
+/*
+ * 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
+
+import FirebaseCommunity.FirebaseAuth
+import GoogleSignIn
+
+final class ViewController: UIViewController, UITextFieldDelegate, FIRAuthUIDelegate {
+ /// The profile image for the currently signed-in user.
+ @IBOutlet weak var profileImage: UIImageView!
+
+ /// The display name for the currently signed-in user.
+ @IBOutlet weak var displayNameLabel: UILabel!
+
+ /// The email for the currently signed-in user.
+ @IBOutlet weak var emailLabel: UILabel!
+
+ /// The ID for the currently signed-in user.
+ @IBOutlet weak var userIDLabel: UILabel!
+
+ /// The list of providers for the currently signed-in user.
+ @IBOutlet weak var providerListLabel: UILabel!
+
+ /// The picker for the list of action types.
+ @IBOutlet weak var actionTypePicker: UIPickerView!
+
+ /// The picker for the list of actions.
+ @IBOutlet weak var actionPicker: UIPickerView!
+
+ /// The picker for the list of credential types.
+ @IBOutlet weak var credentialTypePicker: UIPickerView!
+
+ /// The label for the "email" text field.
+ @IBOutlet weak var emailInputLabel: UILabel!
+
+ /// The "email" text field.
+ @IBOutlet weak var emailField: UITextField!
+
+ /// The label for the "password" text field.
+ @IBOutlet weak var passwordInputLabel: UILabel!
+
+ /// The "password" text field.
+ @IBOutlet weak var passwordField: UITextField!
+
+ /// The "phone" text field.
+ @IBOutlet weak var phoneField: UITextField!
+
+ /// The scroll view holding all content.
+ @IBOutlet weak var scrollView: UIScrollView!
+
+ // The active keyboard input field.
+ var activeField: UITextField?
+
+ /// The currently selected action type.
+ fileprivate var actionType = ActionType(rawValue: 0)! {
+ didSet {
+ if actionType != oldValue {
+ actionPicker.reloadAllComponents()
+ actionPicker.selectRow(actionType == .auth ? authAction.rawValue : userAction.rawValue,
+ inComponent: 0, animated: false)
+ }
+ }
+ }
+
+ /// The currently selected auth action.
+ fileprivate var authAction = AuthAction(rawValue: 0)!
+
+ /// The currently selected user action.
+ fileprivate var userAction = UserAction(rawValue: 0)!
+
+ /// The currently selected credential.
+ fileprivate var credentialType = CredentialType(rawValue: 0)!
+
+ /// The current Firebase user.
+ fileprivate var user: User? = nil {
+ didSet {
+ if user?.uid != oldValue?.uid {
+ actionTypePicker.reloadAllComponents()
+ actionType = ActionType(rawValue: actionTypePicker.selectedRow(inComponent: 0))!
+ }
+ }
+ }
+
+ func registerForKeyboardNotifications() {
+ NotificationCenter.default.addObserver(self,
+ selector:
+ #selector(keyboardWillBeShown(notification:)),
+ name: NSNotification.Name.UIKeyboardWillShow,
+ object: nil)
+ NotificationCenter.default.addObserver(self,
+ selector: #selector(keyboardWillBeHidden(notification:)),
+ name: NSNotification.Name.UIKeyboardWillHide,
+ object: nil)
+ }
+
+ func deregisterFromKeyboardNotifications() {
+ NotificationCenter.default.removeObserver(self,
+ name: NSNotification.Name.UIKeyboardWillShow,
+ object: nil)
+ NotificationCenter.default.removeObserver(self,
+ name: NSNotification.Name.UIKeyboardWillHide,
+ object: nil)
+ }
+
+ func keyboardWillBeShown(notification: NSNotification) {
+ scrollView.isScrollEnabled = true
+ let info = notification.userInfo!
+ let keyboardSize = (info[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size
+ let contentInsets : UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardSize!.height, 0.0)
+
+ scrollView.contentInset = contentInsets
+ scrollView.scrollIndicatorInsets = contentInsets
+
+ var aRect = self.view.frame
+ aRect.size.height -= keyboardSize!.height
+ if let activeField = activeField {
+ if (!aRect.contains(activeField.frame.origin)) {
+ scrollView.scrollRectToVisible(activeField.frame, animated: true)
+ }
+ }
+ }
+
+ func keyboardWillBeHidden(notification: NSNotification){
+ let info = notification.userInfo!
+ let keyboardSize = (info[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size
+ let contentInsets : UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, -keyboardSize!.height, 0.0)
+ scrollView.contentInset = contentInsets
+ scrollView.scrollIndicatorInsets = contentInsets
+ self.view.endEditing(true)
+ scrollView.isScrollEnabled = false
+ }
+
+ func textFieldDidBeginEditing(_ textField: UITextField) {
+ activeField = textField
+ }
+
+ func textFieldDidEndEditing(_ textField: UITextField) {
+ activeField = nil
+ }
+
+ func dismissKeyboard() {
+ view.endEditing(true)
+ }
+
+ func verify(phoneNumber: String, completion: @escaping (PhoneAuthCredential?, Error?) -> Void) {
+ if #available(iOS 8.0, *) {
+ PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate:self) {
+ verificationID, error in
+ guard error == nil else {
+ completion(nil, error)
+ return
+ }
+ let codeAlertController =
+ UIAlertController(title: "Enter Code", message: nil, preferredStyle: .alert)
+ codeAlertController.addTextField { textfield in
+ textfield.placeholder = "SMS Code"
+ textfield.keyboardType = UIKeyboardType.numberPad
+ }
+ codeAlertController.addAction(UIAlertAction(title: "OK",
+ style: .default,
+ handler: { (UIAlertAction) in
+ let code = codeAlertController.textFields!.first!.text!
+ let phoneCredential =
+ PhoneAuthProvider.provider().credential(withVerificationID: verificationID ?? "",
+ verificationCode: code)
+ completion(phoneCredential, nil)
+ }))
+ self.present(codeAlertController, animated: true, completion: nil)
+ }
+ }
+ }
+ /// The user's photo URL used by the last network request for its contents.
+ fileprivate var lastPhotoURL: URL? = nil
+
+ override func viewDidLoad() {
+ GIDSignIn.sharedInstance().uiDelegate = self
+ updateUserInfo(Auth.auth())
+ NotificationCenter.default.addObserver(forName: .AuthStateDidChange,
+ object: Auth.auth(), queue: nil) { notification in
+ self.updateUserInfo(notification.object as? Auth)
+ }
+ phoneField.delegate = self
+ registerForKeyboardNotifications()
+
+ let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
+ scrollView.addGestureRecognizer(tap)
+ }
+
+ override func viewWillDisappear(_ animated: Bool) {
+ deregisterFromKeyboardNotifications()
+ }
+
+ /// Executes the action designated by the operator on the UI.
+ @IBAction func execute(_ sender: UIButton) {
+ switch actionType {
+ case .auth:
+ switch authAction {
+ case .fetchProviderForEmail:
+ Auth.auth().fetchProviders(forEmail: emailField.text!) { providers, error in
+ self.ifNoError(error) {
+ self.showAlert(title: "Providers", message: providers?.joined(separator: ", "))
+ }
+ }
+ case .signInAnonymously:
+ Auth.auth().signInAnonymously() { user, error in
+ self.ifNoError(error) {
+ self.showAlert(title: "Signed In Anonymously")
+ }
+ }
+ case .signInWithCredential:
+ getCredential() { credential in
+ Auth.auth().signIn(with: credential) { user, error in
+ self.ifNoError(error) {
+ self.showAlert(title: "Signed In With Credential", message: user?.textDescription)
+ }
+ }
+ }
+ case .createUser:
+ Auth.auth().createUser(withEmail: emailField.text!, password: passwordField.text!) {
+ user, error in
+ self.ifNoError(error) {
+ self.showAlert(title: "Signed In With Credential", message: user?.textDescription)
+ }
+ }
+ case .signOut:
+ try! Auth.auth().signOut()
+ GIDSignIn.sharedInstance().signOut()
+ }
+ case .user:
+ switch userAction {
+ case .updateEmail:
+ user!.updateEmail(to: emailField.text!) { error in
+ self.ifNoError(error) {
+ self.showAlert(title: "Updated Email", message: self.user?.email)
+ }
+ }
+ case .updatePhone:
+ let phoneNumber = phoneField.text
+ self.verify(phoneNumber: phoneNumber!, completion: { (phoneAuthCredential, error) in
+ guard error == nil else {
+ self.showAlert(title: "Error", message: error!.localizedDescription)
+ return
+ }
+ self.user!.updatePhoneNumber(phoneAuthCredential!, completion: { error in
+ self.ifNoError(error) {
+ self.showAlert(title: "Updated Phone Number")
+ self.updateUserInfo(Auth.auth())
+ }
+ })
+ })
+ case .updatePassword:
+ user!.updatePassword(to: passwordField.text!) { error in
+ self.ifNoError(error) {
+ self.showAlert(title: "Updated Password")
+ }
+ }
+ case .reload:
+ user!.reload() { error in
+ self.ifNoError(error) {
+ self.showAlert(title: "Reloaded", message: self.user?.textDescription)
+ }
+ }
+ case .reauthenticate:
+ getCredential() { credential in
+ self.user!.reauthenticate(with: credential) { error in
+ self.ifNoError(error) {
+ self.showAlert(title: "Reauthenticated", message: self.user?.textDescription)
+ }
+ }
+ }
+ case .getToken:
+ user!.getIDToken() { token, error in
+ self.ifNoError(error) {
+ self.showAlert(title: "Got ID Token", message: token)
+ }
+ }
+ case .linkWithCredential:
+ getCredential() { credential in
+ self.user!.link(with: credential) { user, error in
+ self.ifNoError(error) {
+ self.showAlert(title: "Linked With Credential", message: user?.textDescription)
+ }
+ }
+ }
+ case .deleteAccount:
+ user!.delete() { error in
+ self.ifNoError(error) {
+ self.showAlert(title: "Deleted Account")
+ }
+ }
+ }
+ }
+ }
+
+ /// Gets an AuthCredential potentially asynchronously.
+ private func getCredential(completion: @escaping (AuthCredential) -> Void) {
+ switch credentialType {
+ case .google:
+ GIDSignIn.sharedInstance().delegate = GoogleSignInDelegate(completion: { user, error in
+ self.ifNoError(error) {
+ completion(GoogleAuthProvider.credential(
+ withIDToken: user!.authentication.idToken,
+ accessToken: user!.authentication.accessToken))
+ }
+ })
+ GIDSignIn.sharedInstance().signIn()
+ case .password:
+ completion(EmailAuthProvider.credential(withEmail: emailField.text!,
+ password: passwordField.text!))
+ case .phone:
+ let phoneNumber = phoneField.text
+ self.verify(phoneNumber: phoneNumber!, completion: { (phoneAuthCredential, error) in
+ guard error == nil else {
+ self.showAlert(title: "Error", message: error!.localizedDescription)
+ return
+ }
+ completion(phoneAuthCredential!)
+ })
+ }
+ }
+
+ /// Updates user's profile image and info text.
+ private func updateUserInfo(_ auth: Auth?) {
+ user = auth?.currentUser
+ displayNameLabel.text = user?.displayName
+ emailLabel.text = user?.email
+ userIDLabel.text = user?.uid
+ let providers = user?.providerData.map { userInfo in userInfo.providerID }
+ providerListLabel.text = providers?.joined(separator: ", ")
+ if let photoURL = user?.photoURL {
+ lastPhotoURL = photoURL
+ let queue: DispatchQueue
+ if #available(iOS 8.0, *) {
+ queue = DispatchQueue.global(qos: .background)
+ } else {
+ queue = DispatchQueue.global(priority: DispatchQueue.GlobalQueuePriority.background)
+ }
+ queue.async {
+ if let imageData = try? Data(contentsOf: photoURL) {
+ let image = UIImage(data: imageData)
+ DispatchQueue.main.async {
+ if self.lastPhotoURL == photoURL {
+ self.profileImage.image = image
+ }
+ }
+ }
+ }
+ } else {
+ lastPhotoURL = nil
+ self.profileImage.image = nil
+ }
+ updateControls()
+ }
+
+ // Updates the states of the UI controls.
+ fileprivate func updateControls() {
+ let action: Action
+ switch actionType {
+ case .auth:
+ action = authAction
+ case .user:
+ action = userAction
+ }
+ let isCredentialEnabled = action.requiresCredential
+ credentialTypePicker.isUserInteractionEnabled = isCredentialEnabled
+ credentialTypePicker.alpha = isCredentialEnabled ? 1.0 : 0.6
+ let isEmailEnabled = isCredentialEnabled && credentialType.requiresEmail || action.requiresEmail
+ emailInputLabel.alpha = isEmailEnabled ? 1.0 : 0.6
+ emailField.isEnabled = isEmailEnabled
+ let isPasswordEnabled = isCredentialEnabled && credentialType.requiresPassword ||
+ action.requiresPassword
+ passwordInputLabel.alpha = isPasswordEnabled ? 1.0 : 0.6
+ passwordField.isEnabled = isPasswordEnabled
+ phoneField.isEnabled = credentialType.requiresPhone || action.requiresPhoneNumber
+ }
+
+ fileprivate func showAlert(title: String, message: String? = "") {
+ if #available(iOS 8.0, *) {
+ let alertController =
+ UIAlertController(title: title, message: message, preferredStyle: .alert)
+ alertController.addAction(UIAlertAction(title: "OK",
+ style: .default,
+ handler: { (UIAlertAction) in
+ alertController.dismiss(animated: true, completion: nil)
+ }))
+ self.present(alertController, animated: true, completion: nil)
+ } else {
+ UIAlertView(title: title,
+ message: message ?? "(NULL)",
+ delegate: nil,
+ cancelButtonTitle: nil,
+ otherButtonTitles: "OK").show()
+ }
+ }
+
+ private func ifNoError(_ error: Error?, execute: () -> Void) {
+ guard error == nil else {
+ showAlert(title: "Error", message: error!.localizedDescription)
+ return
+ }
+ execute()
+ }
+}
+
+extension ViewController : GIDSignInUIDelegate {
+ func sign(_ signIn: GIDSignIn!, present viewController: UIViewController!) {
+ present(viewController, animated: true, completion: nil)
+ }
+
+ func sign(_ signIn: GIDSignIn!, dismiss viewController: UIViewController!) {
+ dismiss(animated: true, completion: nil)
+ }
+}
+
+extension ViewController : UIPickerViewDataSource {
+ func numberOfComponents(in pickerView: UIPickerView) -> Int {
+ return 1
+ }
+
+ func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
+ switch pickerView {
+ case actionTypePicker:
+ if Auth.auth().currentUser != nil {
+ return ActionType.countWithUser
+ } else {
+ return ActionType.countWithoutUser
+ }
+ case actionPicker:
+ switch actionType {
+ case .auth:
+ return AuthAction.count
+ case .user:
+ return UserAction.count
+ }
+ case credentialTypePicker:
+ return CredentialType.count
+ default:
+ return 0
+ }
+ }
+}
+
+extension ViewController : UIPickerViewDelegate {
+ func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int)
+ -> String? {
+ switch pickerView {
+ case actionTypePicker:
+ return ActionType(rawValue: row)!.text
+ case actionPicker:
+ switch actionType {
+ case .auth:
+ return AuthAction(rawValue: row)!.text
+ case .user:
+ return UserAction(rawValue: row)!.text
+ }
+ case credentialTypePicker:
+ return CredentialType(rawValue: row)!.text
+ default:
+ return nil
+ }
+ }
+
+ func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
+ switch pickerView {
+ case actionTypePicker:
+ actionType = ActionType(rawValue: row)!
+ case actionPicker:
+ switch actionType {
+ case .auth:
+ authAction = AuthAction(rawValue: row)!
+ case .user:
+ userAction = UserAction(rawValue: row)!
+ }
+ case credentialTypePicker:
+ credentialType = CredentialType(rawValue: row)!
+ default:
+ break
+ }
+ updateControls()
+ }
+}
+
+/// An adapter class to pass GoogleSignIn delegate method to a block.
+fileprivate final class GoogleSignInDelegate: NSObject, GIDSignInDelegate {
+
+ private let completion: (GIDGoogleUser?, Error?) -> Void
+ private var retainedSelf: GoogleSignInDelegate?
+
+ init(completion: @escaping (GIDGoogleUser?, Error?) -> Void) {
+ self.completion = completion
+ super.init()
+ retainedSelf = self
+ }
+
+ func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser?, withError error: Error?) {
+ completion(user, error)
+ retainedSelf = nil
+ }
+}
+
+/// The list of all possible action types.
+fileprivate enum ActionType: Int {
+
+ case auth, user
+
+ // Count of action types when no user is signed in.
+ static var countWithoutUser: Int {
+ return ActionType.auth.rawValue + 1
+ }
+
+ // Count of action types when a user is signed in.
+ static var countWithUser: Int {
+ return ActionType.user.rawValue + 1
+ }
+
+ /// The text description for a particular enum value.
+ var text : String {
+ switch self {
+ case .auth:
+ return "Auth"
+ case .user:
+ return "User"
+ }
+ }
+}
+
+fileprivate protocol Action {
+ /// The text description for the particular action.
+ var text: String { get }
+
+ /// Whether or not the action requires a credential.
+ var requiresCredential : Bool { get }
+
+ /// Whether or not the action requires an email.
+ var requiresEmail: Bool { get }
+
+ /// Whether or not the credential requires a password.
+ var requiresPassword: Bool { get }
+
+ /// Whether or not the credential requires a phone number.
+ var requiresPhoneNumber: Bool { get }
+}
+
+/// The list of all possible actions the operator can take on the Auth object.
+fileprivate enum AuthAction: Int, Action {
+
+ case fetchProviderForEmail, signInAnonymously, signInWithCredential, createUser, signOut
+
+ /// Total number of auth actions.
+ static var count: Int {
+ return AuthAction.signOut.rawValue + 1
+ }
+
+ var text : String {
+ switch self {
+ case .fetchProviderForEmail:
+ return "Fetch Provider ⬇️"
+ case .signInAnonymously:
+ return "Sign In Anonymously"
+ case .signInWithCredential:
+ return "Sign In w/ Credential ↙️"
+ case .createUser:
+ return "Create User ⬇️"
+ case .signOut:
+ return "Sign Out"
+ }
+ }
+
+ var requiresCredential : Bool {
+ return self == .signInWithCredential
+ }
+
+ var requiresEmail : Bool {
+ return self == .fetchProviderForEmail || self == .createUser
+ }
+
+ var requiresPassword : Bool {
+ return self == .createUser
+ }
+
+ var requiresPhoneNumber: Bool {
+ return false
+ }
+}
+
+/// The list of all possible actions the operator can take on the User object.
+fileprivate enum UserAction: Int, Action {
+
+ case updateEmail, updatePhone, updatePassword, reload, reauthenticate, getToken,
+ linkWithCredential, deleteAccount
+
+ /// Total number of user actions.
+ static var count: Int {
+ return UserAction.deleteAccount.rawValue + 1
+ }
+
+ var text : String {
+ switch self {
+ case .updateEmail:
+ return "Update Email ⬇️"
+ case .updatePhone:
+ if #available(iOS 8.0, *) {
+ return "Update Phone ⬇️"
+ } else {
+ return "-"
+ }
+ case .updatePassword:
+ return "Update Password ⬇️"
+ case .reload:
+ return "Reload"
+ case .reauthenticate:
+ return "Reauthenticate ↙️"
+ case .getToken:
+ return "Get Token"
+ case .linkWithCredential:
+ return "Link With Credential ↙️"
+ case .deleteAccount:
+ return "Delete Account"
+ }
+ }
+
+ var requiresCredential : Bool {
+ return self == .reauthenticate || self == .linkWithCredential
+ }
+
+ var requiresEmail : Bool {
+ return self == .updateEmail
+ }
+
+ var requiresPassword : Bool {
+ return self == .updatePassword
+ }
+
+ var requiresPhoneNumber : Bool {
+ return self == .updatePhone
+ }
+
+}
+
+/// The list of all possible credential types the operator can use to sign in or link.
+fileprivate enum CredentialType: Int {
+
+ case google, password, phone
+
+ /// Total number of enum values.
+ static var count: Int {
+ return CredentialType.phone.rawValue + 1
+ }
+
+ /// The text description for a particular enum value.
+ var text : String {
+ switch self {
+ case .google:
+ return "Google"
+ case .password:
+ return "Password ➡️️"
+ case .phone:
+ if #available(iOS 8.0, *) {
+ return "Phone ➡️️"
+ } else {
+ return "-"
+ }
+ }
+ }
+
+ /// Whether or not the credential requires an email.
+ var requiresEmail : Bool {
+ return self == .password
+ }
+
+ /// Whether or not the credential requires a password.
+ var requiresPassword : Bool {
+ return self == .password
+ }
+
+ /// Whether or not the credential requires a phone number.
+ var requiresPhone : Bool {
+ return self == .phone
+ }
+}
+
+fileprivate extension User {
+ var textDescription: String {
+ return self.displayName ?? self.email ?? self.uid
+ }
+}