2017-05-15 12:27:07 -07:00
|
|
|
|
/*
|
|
|
|
|
|
* 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
|
|
|
|
|
|
|
2017-11-21 17:02:22 -08:00
|
|
|
|
import FirebaseAuth
|
2017-06-20 17:02:45 -07:00
|
|
|
|
import GoogleSignIn
|
2017-05-15 12:27:07 -07:00
|
|
|
|
|
2017-09-19 14:01:06 -07:00
|
|
|
|
final class ViewController: UIViewController, UITextFieldDelegate, AuthUIDelegate {
|
2017-05-15 12:27:07 -07:00
|
|
|
|
/// 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!
|
|
|
|
|
|
|
2017-05-19 11:31:48 -07:00
|
|
|
|
/// 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?
|
|
|
|
|
|
|
2017-05-15 12:27:07 -07:00
|
|
|
|
/// 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))!
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-05-19 11:31:48 -07:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-06-01 11:52:04 -07:00
|
|
|
|
func verify(phoneNumber: String, completion: @escaping (PhoneAuthCredential?, Error?) -> Void) {
|
|
|
|
|
|
if #available(iOS 8.0, *) {
|
2017-09-06 09:25:19 -07:00
|
|
|
|
PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate:self) {
|
|
|
|
|
|
verificationID, error in
|
2017-06-01 11:52:04 -07:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2017-05-15 12:27:07 -07:00
|
|
|
|
/// 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)
|
|
|
|
|
|
}
|
2017-05-19 11:31:48 -07:00
|
|
|
|
phoneField.delegate = self
|
|
|
|
|
|
registerForKeyboardNotifications()
|
|
|
|
|
|
|
|
|
|
|
|
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
|
|
|
|
|
|
scrollView.addGestureRecognizer(tap)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override func viewWillDisappear(_ animated: Bool) {
|
|
|
|
|
|
deregisterFromKeyboardNotifications()
|
2017-05-15 12:27:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 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
|
2017-10-25 16:49:59 -07:00
|
|
|
|
Auth.auth().signInAndRetrieveData(with: credential) { authData, error in
|
2017-05-15 12:27:07 -07:00
|
|
|
|
self.ifNoError(error) {
|
2017-10-25 16:49:59 -07:00
|
|
|
|
self.showAlert(title: "Signed In With Credential",
|
|
|
|
|
|
message: authData?.user.textDescription)
|
2017-05-15 12:27:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
case .createUser:
|
|
|
|
|
|
Auth.auth().createUser(withEmail: emailField.text!, password: passwordField.text!) {
|
2018-04-25 21:50:19 -07:00
|
|
|
|
result, error in
|
2017-05-15 12:27:07 -07:00
|
|
|
|
self.ifNoError(error) {
|
2018-04-25 21:50:19 -07:00
|
|
|
|
self.showAlert(title: "Signed In With Credential", message: result?.user.textDescription)
|
2017-05-15 12:27:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2017-06-01 11:52:04 -07:00
|
|
|
|
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())
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
2017-05-15 12:27:07 -07:00
|
|
|
|
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
|
2017-10-25 16:49:59 -07:00
|
|
|
|
self.user!.reauthenticateAndRetrieveData(with: credential) { authData, error in
|
2017-05-15 12:27:07 -07:00
|
|
|
|
self.ifNoError(error) {
|
2017-10-25 16:49:59 -07:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2017-05-15 12:27:07 -07:00
|
|
|
|
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
|
2017-10-25 16:49:59 -07:00
|
|
|
|
self.user!.linkAndRetrieveData(with: credential) { authData, error in
|
2017-05-15 12:27:07 -07:00
|
|
|
|
self.ifNoError(error) {
|
2017-10-25 16:49:59 -07:00
|
|
|
|
self.showAlert(title: "Linked With Credential",
|
|
|
|
|
|
message: authData?.user.textDescription)
|
2017-05-15 12:27:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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!,
|
2017-05-19 11:31:48 -07:00
|
|
|
|
password: passwordField.text!))
|
|
|
|
|
|
case .phone:
|
2017-06-01 11:52:04 -07:00
|
|
|
|
let phoneNumber = phoneField.text
|
|
|
|
|
|
self.verify(phoneNumber: phoneNumber!, completion: { (phoneAuthCredential, error) in
|
|
|
|
|
|
guard error == nil else {
|
|
|
|
|
|
self.showAlert(title: "Error", message: error!.localizedDescription)
|
|
|
|
|
|
return
|
2017-05-19 11:31:48 -07:00
|
|
|
|
}
|
2017-06-01 11:52:04 -07:00
|
|
|
|
completion(phoneAuthCredential!)
|
|
|
|
|
|
})
|
2017-05-15 12:27:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 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
|
2017-06-01 13:53:49 -07:00
|
|
|
|
let queue: DispatchQueue
|
|
|
|
|
|
if #available(iOS 8.0, *) {
|
|
|
|
|
|
queue = DispatchQueue.global(qos: .background)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
queue = DispatchQueue.global(priority: DispatchQueue.GlobalQueuePriority.background)
|
|
|
|
|
|
}
|
|
|
|
|
|
queue.async {
|
2017-05-15 12:27:07 -07:00
|
|
|
|
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
|
2017-06-01 11:52:04 -07:00
|
|
|
|
phoneField.isEnabled = credentialType.requiresPhone || action.requiresPhoneNumber
|
2017-05-15 12:27:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fileprivate func showAlert(title: String, message: String? = "") {
|
2017-06-01 13:53:49 -07:00
|
|
|
|
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()
|
|
|
|
|
|
}
|
2017-05-15 12:27:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 }
|
|
|
|
|
|
|
2017-06-01 11:52:04 -07:00
|
|
|
|
/// Whether or not the action requires a credential.
|
2017-05-15 12:27:07 -07:00
|
|
|
|
var requiresCredential : Bool { get }
|
|
|
|
|
|
|
2017-06-01 11:52:04 -07:00
|
|
|
|
/// Whether or not the action requires an email.
|
2017-05-15 12:27:07 -07:00
|
|
|
|
var requiresEmail: Bool { get }
|
|
|
|
|
|
|
2017-06-01 11:52:04 -07:00
|
|
|
|
/// Whether or not the credential requires a password.
|
2017-05-15 12:27:07 -07:00
|
|
|
|
var requiresPassword: Bool { get }
|
2017-06-01 11:52:04 -07:00
|
|
|
|
|
|
|
|
|
|
/// Whether or not the credential requires a phone number.
|
|
|
|
|
|
var requiresPhoneNumber: Bool { get }
|
2017-05-15 12:27:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 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
|
|
|
|
|
|
}
|
2017-06-01 11:52:04 -07:00
|
|
|
|
|
|
|
|
|
|
var requiresPhoneNumber: Bool {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
2017-05-15 12:27:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// The list of all possible actions the operator can take on the User object.
|
|
|
|
|
|
fileprivate enum UserAction: Int, Action {
|
|
|
|
|
|
|
2017-06-01 11:52:04 -07:00
|
|
|
|
case updateEmail, updatePhone, updatePassword, reload, reauthenticate, getToken,
|
|
|
|
|
|
linkWithCredential, deleteAccount
|
2017-05-15 12:27:07 -07:00
|
|
|
|
|
|
|
|
|
|
/// Total number of user actions.
|
|
|
|
|
|
static var count: Int {
|
|
|
|
|
|
return UserAction.deleteAccount.rawValue + 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var text : String {
|
|
|
|
|
|
switch self {
|
|
|
|
|
|
case .updateEmail:
|
|
|
|
|
|
return "Update Email ⬇️"
|
2017-06-01 11:52:04 -07:00
|
|
|
|
case .updatePhone:
|
|
|
|
|
|
if #available(iOS 8.0, *) {
|
|
|
|
|
|
return "Update Phone ⬇️"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return "-"
|
|
|
|
|
|
}
|
2017-05-15 12:27:07 -07:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2017-06-01 11:52:04 -07:00
|
|
|
|
|
|
|
|
|
|
var requiresPhoneNumber : Bool {
|
|
|
|
|
|
return self == .updatePhone
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-05-15 12:27:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// The list of all possible credential types the operator can use to sign in or link.
|
|
|
|
|
|
fileprivate enum CredentialType: Int {
|
|
|
|
|
|
|
2017-05-19 11:31:48 -07:00
|
|
|
|
case google, password, phone
|
2017-05-15 12:27:07 -07:00
|
|
|
|
|
|
|
|
|
|
/// Total number of enum values.
|
|
|
|
|
|
static var count: Int {
|
2017-05-19 11:31:48 -07:00
|
|
|
|
return CredentialType.phone.rawValue + 1
|
2017-05-15 12:27:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// The text description for a particular enum value.
|
|
|
|
|
|
var text : String {
|
|
|
|
|
|
switch self {
|
|
|
|
|
|
case .google:
|
|
|
|
|
|
return "Google"
|
|
|
|
|
|
case .password:
|
|
|
|
|
|
return "Password ➡️️"
|
2017-05-19 11:31:48 -07:00
|
|
|
|
case .phone:
|
|
|
|
|
|
if #available(iOS 8.0, *) {
|
|
|
|
|
|
return "Phone ➡️️"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return "-"
|
|
|
|
|
|
}
|
2017-05-15 12:27:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-06-01 11:52:04 -07:00
|
|
|
|
/// Whether or not the credential requires an email.
|
2017-05-15 12:27:07 -07:00
|
|
|
|
var requiresEmail : Bool {
|
|
|
|
|
|
return self == .password
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2017-06-01 11:52:04 -07:00
|
|
|
|
/// Whether or not the credential requires a password.
|
2017-05-15 12:27:07 -07:00
|
|
|
|
var requiresPassword : Bool {
|
|
|
|
|
|
return self == .password
|
|
|
|
|
|
}
|
2017-05-19 11:31:48 -07:00
|
|
|
|
|
2017-06-01 11:52:04 -07:00
|
|
|
|
/// Whether or not the credential requires a phone number.
|
2017-05-19 11:31:48 -07:00
|
|
|
|
var requiresPhone : Bool {
|
|
|
|
|
|
return self == .phone
|
|
|
|
|
|
}
|
2017-05-15 12:27:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fileprivate extension User {
|
|
|
|
|
|
var textDescription: String {
|
|
|
|
|
|
return self.displayName ?? self.email ?? self.uid
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|