diff options
author | Paul Beusterien <paulbeusterien@google.com> | 2017-09-19 11:18:04 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-09-19 11:18:04 -0700 |
commit | 9447e72cab40c9ea59e49a726d2890bcf356d38a (patch) | |
tree | dc25e027769cc1fbe6bd6e0ae0d877858e0780e7 /Example/Auth/Sample | |
parent | a34d091971d05ef8e2625074157eb9ff6dda3cbd (diff) |
Consolidate AuthSamples into main Firebase Xcode project (#288)
Diffstat (limited to 'Example/Auth/Sample')
61 files changed, 6305 insertions, 0 deletions
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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew file mode 100644 index 0000000..40a1a84 --- /dev/null +++ b/Example/Auth/Sample/Images.xcassets/close.imageset/ic_clear_black_1x_ios_24dp.png 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 Binary files differnew file mode 100644 index 0000000..6bc4372 --- /dev/null +++ b/Example/Auth/Sample/Images.xcassets/close.imageset/ic_clear_black_2x_ios_24dp.png 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 Binary files differnew file mode 100644 index 0000000..51b4401 --- /dev/null +++ b/Example/Auth/Sample/Images.xcassets/close.imageset/ic_clear_black_3x_ios_24dp.png 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" = "设置"; |