| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708 |
- /*
- * Copyright 2017 Google
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- import UIKit
- import FirebaseAuth
- import GoogleSignIn
- final class ViewController: UIViewController, UITextFieldDelegate, AuthUIDelegate {
- /// The profile image for the currently signed-in user.
- @IBOutlet weak var profileImage: UIImageView!
- /// The display name for the currently signed-in user.
- @IBOutlet weak var displayNameLabel: UILabel!
- /// The email for the currently signed-in user.
- @IBOutlet weak var emailLabel: UILabel!
- /// The ID for the currently signed-in user.
- @IBOutlet weak var userIDLabel: UILabel!
- /// The list of providers for the currently signed-in user.
- @IBOutlet weak var providerListLabel: UILabel!
- /// The picker for the list of action types.
- @IBOutlet weak var actionTypePicker: UIPickerView!
- /// The picker for the list of actions.
- @IBOutlet weak var actionPicker: UIPickerView!
- /// The picker for the list of credential types.
- @IBOutlet weak var credentialTypePicker: UIPickerView!
- /// The label for the "email" text field.
- @IBOutlet weak var emailInputLabel: UILabel!
- /// The "email" text field.
- @IBOutlet weak var emailField: UITextField!
- /// The label for the "password" text field.
- @IBOutlet weak var passwordInputLabel: UILabel!
- /// The "password" text field.
- @IBOutlet weak var passwordField: UITextField!
- /// The "phone" text field.
- @IBOutlet weak var phoneField: UITextField!
- /// The scroll view holding all content.
- @IBOutlet weak var scrollView: UIScrollView!
- // The active keyboard input field.
- var activeField: UITextField?
- /// The currently selected action type.
- fileprivate var actionType = ActionType(rawValue: 0)! {
- didSet {
- if actionType != oldValue {
- actionPicker.reloadAllComponents()
- actionPicker.selectRow(actionType == .auth ? authAction.rawValue : userAction.rawValue,
- inComponent: 0, animated: false)
- }
- }
- }
- /// The currently selected auth action.
- fileprivate var authAction = AuthAction(rawValue: 0)!
- /// The currently selected user action.
- fileprivate var userAction = UserAction(rawValue: 0)!
- /// The currently selected credential.
- fileprivate var credentialType = CredentialType(rawValue: 0)!
- /// The current Firebase user.
- fileprivate var user: User? = nil {
- didSet {
- if user?.uid != oldValue?.uid {
- actionTypePicker.reloadAllComponents()
- actionType = ActionType(rawValue: actionTypePicker.selectedRow(inComponent: 0))!
- }
- }
- }
- func registerForKeyboardNotifications() {
- NotificationCenter.default.addObserver(self,
- selector:
- #selector(keyboardWillBeShown(notification:)),
- name: NSNotification.Name.UIKeyboardWillShow,
- object: nil)
- NotificationCenter.default.addObserver(self,
- selector: #selector(keyboardWillBeHidden(notification:)),
- name: NSNotification.Name.UIKeyboardWillHide,
- object: nil)
- }
- func deregisterFromKeyboardNotifications() {
- NotificationCenter.default.removeObserver(self,
- name: NSNotification.Name.UIKeyboardWillShow,
- object: nil)
- NotificationCenter.default.removeObserver(self,
- name: NSNotification.Name.UIKeyboardWillHide,
- object: nil)
- }
- func keyboardWillBeShown(notification: NSNotification) {
- scrollView.isScrollEnabled = true
- let info = notification.userInfo!
- let keyboardSize = (info[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size
- let contentInsets : UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardSize!.height, 0.0)
- scrollView.contentInset = contentInsets
- scrollView.scrollIndicatorInsets = contentInsets
- var aRect = self.view.frame
- aRect.size.height -= keyboardSize!.height
- if let activeField = activeField {
- if (!aRect.contains(activeField.frame.origin)) {
- scrollView.scrollRectToVisible(activeField.frame, animated: true)
- }
- }
- }
- func keyboardWillBeHidden(notification: NSNotification){
- let info = notification.userInfo!
- let keyboardSize = (info[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size
- let contentInsets : UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, -keyboardSize!.height, 0.0)
- scrollView.contentInset = contentInsets
- scrollView.scrollIndicatorInsets = contentInsets
- self.view.endEditing(true)
- scrollView.isScrollEnabled = false
- }
- func textFieldDidBeginEditing(_ textField: UITextField) {
- activeField = textField
- }
- func textFieldDidEndEditing(_ textField: UITextField) {
- activeField = nil
- }
- func dismissKeyboard() {
- view.endEditing(true)
- }
- func verify(phoneNumber: String, completion: @escaping (PhoneAuthCredential?, Error?) -> Void) {
- if #available(iOS 8.0, *) {
- PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate:self) {
- verificationID, error in
- guard error == nil else {
- completion(nil, error)
- return
- }
- let codeAlertController =
- UIAlertController(title: "Enter Code", message: nil, preferredStyle: .alert)
- codeAlertController.addTextField { textfield in
- textfield.placeholder = "SMS Code"
- textfield.keyboardType = UIKeyboardType.numberPad
- }
- codeAlertController.addAction(UIAlertAction(title: "OK",
- style: .default,
- handler: { (UIAlertAction) in
- let code = codeAlertController.textFields!.first!.text!
- let phoneCredential =
- PhoneAuthProvider.provider().credential(withVerificationID: verificationID ?? "",
- verificationCode: code)
- completion(phoneCredential, nil)
- }))
- self.present(codeAlertController, animated: true, completion: nil)
- }
- }
- }
- /// The user's photo URL used by the last network request for its contents.
- fileprivate var lastPhotoURL: URL? = nil
- override func viewDidLoad() {
- GIDSignIn.sharedInstance().uiDelegate = self
- updateUserInfo(Auth.auth())
- NotificationCenter.default.addObserver(forName: .AuthStateDidChange,
- object: Auth.auth(), queue: nil) { notification in
- self.updateUserInfo(notification.object as? Auth)
- }
- phoneField.delegate = self
- registerForKeyboardNotifications()
- let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
- scrollView.addGestureRecognizer(tap)
- }
- override func viewWillDisappear(_ animated: Bool) {
- deregisterFromKeyboardNotifications()
- }
- /// Executes the action designated by the operator on the UI.
- @IBAction func execute(_ sender: UIButton) {
- switch actionType {
- case .auth:
- switch authAction {
- case .fetchProviderForEmail:
- Auth.auth().fetchProviders(forEmail: emailField.text!) { providers, error in
- self.ifNoError(error) {
- self.showAlert(title: "Providers", message: providers?.joined(separator: ", "))
- }
- }
- case .signInAnonymously:
- Auth.auth().signInAnonymously() { user, error in
- self.ifNoError(error) {
- self.showAlert(title: "Signed In Anonymously")
- }
- }
- case .signInWithCredential:
- getCredential() { credential in
- Auth.auth().signInAndRetrieveData(with: credential) { authData, error in
- self.ifNoError(error) {
- self.showAlert(title: "Signed In With Credential",
- message: authData?.user.textDescription)
- }
- }
- }
- case .createUser:
- Auth.auth().createUser(withEmail: emailField.text!, password: passwordField.text!) {
- user, error in
- self.ifNoError(error) {
- self.showAlert(title: "Signed In With Credential", message: user?.textDescription)
- }
- }
- case .signOut:
- try! Auth.auth().signOut()
- GIDSignIn.sharedInstance().signOut()
- }
- case .user:
- switch userAction {
- case .updateEmail:
- user!.updateEmail(to: emailField.text!) { error in
- self.ifNoError(error) {
- self.showAlert(title: "Updated Email", message: self.user?.email)
- }
- }
- case .updatePhone:
- let phoneNumber = phoneField.text
- self.verify(phoneNumber: phoneNumber!, completion: { (phoneAuthCredential, error) in
- guard error == nil else {
- self.showAlert(title: "Error", message: error!.localizedDescription)
- return
- }
- self.user!.updatePhoneNumber(phoneAuthCredential!, completion: { error in
- self.ifNoError(error) {
- self.showAlert(title: "Updated Phone Number")
- self.updateUserInfo(Auth.auth())
- }
- })
- })
- case .updatePassword:
- user!.updatePassword(to: passwordField.text!) { error in
- self.ifNoError(error) {
- self.showAlert(title: "Updated Password")
- }
- }
- case .reload:
- user!.reload() { error in
- self.ifNoError(error) {
- self.showAlert(title: "Reloaded", message: self.user?.textDescription)
- }
- }
- case .reauthenticate:
- getCredential() { credential in
- self.user!.reauthenticateAndRetrieveData(with: credential) { authData, error in
- self.ifNoError(error) {
- if (authData?.user.uid != self.user?.uid) {
- let message = "The reauthenticated user must be the same as the original user"
- self.showAlert(title: "Reauthention error",
- message: message)
- return
- }
- self.showAlert(title: "Reauthenticated", message: self.user?.textDescription)
- }
- }
- }
- case .getToken:
- user!.getIDToken() { token, error in
- self.ifNoError(error) {
- self.showAlert(title: "Got ID Token", message: token)
- }
- }
- case .linkWithCredential:
- getCredential() { credential in
- self.user!.linkAndRetrieveData(with: credential) { authData, error in
- self.ifNoError(error) {
- self.showAlert(title: "Linked With Credential",
- message: authData?.user.textDescription)
- }
- }
- }
- case .deleteAccount:
- user!.delete() { error in
- self.ifNoError(error) {
- self.showAlert(title: "Deleted Account")
- }
- }
- }
- }
- }
- /// Gets an AuthCredential potentially asynchronously.
- private func getCredential(completion: @escaping (AuthCredential) -> Void) {
- switch credentialType {
- case .google:
- GIDSignIn.sharedInstance().delegate = GoogleSignInDelegate(completion: { user, error in
- self.ifNoError(error) {
- completion(GoogleAuthProvider.credential(
- withIDToken: user!.authentication.idToken,
- accessToken: user!.authentication.accessToken))
- }
- })
- GIDSignIn.sharedInstance().signIn()
- case .password:
- completion(EmailAuthProvider.credential(withEmail: emailField.text!,
- password: passwordField.text!))
- case .phone:
- let phoneNumber = phoneField.text
- self.verify(phoneNumber: phoneNumber!, completion: { (phoneAuthCredential, error) in
- guard error == nil else {
- self.showAlert(title: "Error", message: error!.localizedDescription)
- return
- }
- completion(phoneAuthCredential!)
- })
- }
- }
- /// Updates user's profile image and info text.
- private func updateUserInfo(_ auth: Auth?) {
- user = auth?.currentUser
- displayNameLabel.text = user?.displayName
- emailLabel.text = user?.email
- userIDLabel.text = user?.uid
- let providers = user?.providerData.map { userInfo in userInfo.providerID }
- providerListLabel.text = providers?.joined(separator: ", ")
- if let photoURL = user?.photoURL {
- lastPhotoURL = photoURL
- let queue: DispatchQueue
- if #available(iOS 8.0, *) {
- queue = DispatchQueue.global(qos: .background)
- } else {
- queue = DispatchQueue.global(priority: DispatchQueue.GlobalQueuePriority.background)
- }
- queue.async {
- if let imageData = try? Data(contentsOf: photoURL) {
- let image = UIImage(data: imageData)
- DispatchQueue.main.async {
- if self.lastPhotoURL == photoURL {
- self.profileImage.image = image
- }
- }
- }
- }
- } else {
- lastPhotoURL = nil
- self.profileImage.image = nil
- }
- updateControls()
- }
- // Updates the states of the UI controls.
- fileprivate func updateControls() {
- let action: Action
- switch actionType {
- case .auth:
- action = authAction
- case .user:
- action = userAction
- }
- let isCredentialEnabled = action.requiresCredential
- credentialTypePicker.isUserInteractionEnabled = isCredentialEnabled
- credentialTypePicker.alpha = isCredentialEnabled ? 1.0 : 0.6
- let isEmailEnabled = isCredentialEnabled && credentialType.requiresEmail || action.requiresEmail
- emailInputLabel.alpha = isEmailEnabled ? 1.0 : 0.6
- emailField.isEnabled = isEmailEnabled
- let isPasswordEnabled = isCredentialEnabled && credentialType.requiresPassword ||
- action.requiresPassword
- passwordInputLabel.alpha = isPasswordEnabled ? 1.0 : 0.6
- passwordField.isEnabled = isPasswordEnabled
- phoneField.isEnabled = credentialType.requiresPhone || action.requiresPhoneNumber
- }
- fileprivate func showAlert(title: String, message: String? = "") {
- if #available(iOS 8.0, *) {
- let alertController =
- UIAlertController(title: title, message: message, preferredStyle: .alert)
- alertController.addAction(UIAlertAction(title: "OK",
- style: .default,
- handler: { (UIAlertAction) in
- alertController.dismiss(animated: true, completion: nil)
- }))
- self.present(alertController, animated: true, completion: nil)
- } else {
- UIAlertView(title: title,
- message: message ?? "(NULL)",
- delegate: nil,
- cancelButtonTitle: nil,
- otherButtonTitles: "OK").show()
- }
- }
- private func ifNoError(_ error: Error?, execute: () -> Void) {
- guard error == nil else {
- showAlert(title: "Error", message: error!.localizedDescription)
- return
- }
- execute()
- }
- }
- extension ViewController : GIDSignInUIDelegate {
- func sign(_ signIn: GIDSignIn!, present viewController: UIViewController!) {
- present(viewController, animated: true, completion: nil)
- }
- func sign(_ signIn: GIDSignIn!, dismiss viewController: UIViewController!) {
- dismiss(animated: true, completion: nil)
- }
- }
- extension ViewController : UIPickerViewDataSource {
- func numberOfComponents(in pickerView: UIPickerView) -> Int {
- return 1
- }
- func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
- switch pickerView {
- case actionTypePicker:
- if Auth.auth().currentUser != nil {
- return ActionType.countWithUser
- } else {
- return ActionType.countWithoutUser
- }
- case actionPicker:
- switch actionType {
- case .auth:
- return AuthAction.count
- case .user:
- return UserAction.count
- }
- case credentialTypePicker:
- return CredentialType.count
- default:
- return 0
- }
- }
- }
- extension ViewController : UIPickerViewDelegate {
- func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int)
- -> String? {
- switch pickerView {
- case actionTypePicker:
- return ActionType(rawValue: row)!.text
- case actionPicker:
- switch actionType {
- case .auth:
- return AuthAction(rawValue: row)!.text
- case .user:
- return UserAction(rawValue: row)!.text
- }
- case credentialTypePicker:
- return CredentialType(rawValue: row)!.text
- default:
- return nil
- }
- }
- func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
- switch pickerView {
- case actionTypePicker:
- actionType = ActionType(rawValue: row)!
- case actionPicker:
- switch actionType {
- case .auth:
- authAction = AuthAction(rawValue: row)!
- case .user:
- userAction = UserAction(rawValue: row)!
- }
- case credentialTypePicker:
- credentialType = CredentialType(rawValue: row)!
- default:
- break
- }
- updateControls()
- }
- }
- /// An adapter class to pass GoogleSignIn delegate method to a block.
- fileprivate final class GoogleSignInDelegate: NSObject, GIDSignInDelegate {
- private let completion: (GIDGoogleUser?, Error?) -> Void
- private var retainedSelf: GoogleSignInDelegate?
- init(completion: @escaping (GIDGoogleUser?, Error?) -> Void) {
- self.completion = completion
- super.init()
- retainedSelf = self
- }
- func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser?, withError error: Error?) {
- completion(user, error)
- retainedSelf = nil
- }
- }
- /// The list of all possible action types.
- fileprivate enum ActionType: Int {
- case auth, user
- // Count of action types when no user is signed in.
- static var countWithoutUser: Int {
- return ActionType.auth.rawValue + 1
- }
- // Count of action types when a user is signed in.
- static var countWithUser: Int {
- return ActionType.user.rawValue + 1
- }
- /// The text description for a particular enum value.
- var text : String {
- switch self {
- case .auth:
- return "Auth"
- case .user:
- return "User"
- }
- }
- }
- fileprivate protocol Action {
- /// The text description for the particular action.
- var text: String { get }
- /// Whether or not the action requires a credential.
- var requiresCredential : Bool { get }
- /// Whether or not the action requires an email.
- var requiresEmail: Bool { get }
- /// Whether or not the credential requires a password.
- var requiresPassword: Bool { get }
- /// Whether or not the credential requires a phone number.
- var requiresPhoneNumber: Bool { get }
- }
- /// The list of all possible actions the operator can take on the Auth object.
- fileprivate enum AuthAction: Int, Action {
- case fetchProviderForEmail, signInAnonymously, signInWithCredential, createUser, signOut
- /// Total number of auth actions.
- static var count: Int {
- return AuthAction.signOut.rawValue + 1
- }
- var text : String {
- switch self {
- case .fetchProviderForEmail:
- return "Fetch Provider ⬇️"
- case .signInAnonymously:
- return "Sign In Anonymously"
- case .signInWithCredential:
- return "Sign In w/ Credential ↙️"
- case .createUser:
- return "Create User ⬇️"
- case .signOut:
- return "Sign Out"
- }
- }
- var requiresCredential : Bool {
- return self == .signInWithCredential
- }
- var requiresEmail : Bool {
- return self == .fetchProviderForEmail || self == .createUser
- }
- var requiresPassword : Bool {
- return self == .createUser
- }
- var requiresPhoneNumber: Bool {
- return false
- }
- }
- /// The list of all possible actions the operator can take on the User object.
- fileprivate enum UserAction: Int, Action {
- case updateEmail, updatePhone, updatePassword, reload, reauthenticate, getToken,
- linkWithCredential, deleteAccount
- /// Total number of user actions.
- static var count: Int {
- return UserAction.deleteAccount.rawValue + 1
- }
- var text : String {
- switch self {
- case .updateEmail:
- return "Update Email ⬇️"
- case .updatePhone:
- if #available(iOS 8.0, *) {
- return "Update Phone ⬇️"
- } else {
- return "-"
- }
- case .updatePassword:
- return "Update Password ⬇️"
- case .reload:
- return "Reload"
- case .reauthenticate:
- return "Reauthenticate ↙️"
- case .getToken:
- return "Get Token"
- case .linkWithCredential:
- return "Link With Credential ↙️"
- case .deleteAccount:
- return "Delete Account"
- }
- }
- var requiresCredential : Bool {
- return self == .reauthenticate || self == .linkWithCredential
- }
- var requiresEmail : Bool {
- return self == .updateEmail
- }
- var requiresPassword : Bool {
- return self == .updatePassword
- }
- var requiresPhoneNumber : Bool {
- return self == .updatePhone
- }
- }
- /// The list of all possible credential types the operator can use to sign in or link.
- fileprivate enum CredentialType: Int {
- case google, password, phone
- /// Total number of enum values.
- static var count: Int {
- return CredentialType.phone.rawValue + 1
- }
- /// The text description for a particular enum value.
- var text : String {
- switch self {
- case .google:
- return "Google"
- case .password:
- return "Password ➡️️"
- case .phone:
- if #available(iOS 8.0, *) {
- return "Phone ➡️️"
- } else {
- return "-"
- }
- }
- }
- /// Whether or not the credential requires an email.
- var requiresEmail : Bool {
- return self == .password
- }
- /// Whether or not the credential requires a password.
- var requiresPassword : Bool {
- return self == .password
- }
- /// Whether or not the credential requires a phone number.
- var requiresPhone : Bool {
- return self == .phone
- }
- }
- fileprivate extension User {
- var textDescription: String {
- return self.displayName ?? self.email ?? self.uid
- }
- }
|