ViewController.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. /*
  2. * Copyright 2017 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import UIKit
  17. import FirebaseDev // FirebaseAuth
  18. import GoogleSignIn // GoogleSignIn
  19. final class ViewController: UIViewController {
  20. /// The profile image for the currently signed-in user.
  21. @IBOutlet weak var profileImage: UIImageView!
  22. /// The display name for the currently signed-in user.
  23. @IBOutlet weak var displayNameLabel: UILabel!
  24. /// The email for the currently signed-in user.
  25. @IBOutlet weak var emailLabel: UILabel!
  26. /// The ID for the currently signed-in user.
  27. @IBOutlet weak var userIDLabel: UILabel!
  28. /// The list of providers for the currently signed-in user.
  29. @IBOutlet weak var providerListLabel: UILabel!
  30. /// The picker for the list of action types.
  31. @IBOutlet weak var actionTypePicker: UIPickerView!
  32. /// The picker for the list of actions.
  33. @IBOutlet weak var actionPicker: UIPickerView!
  34. /// The picker for the list of credential types.
  35. @IBOutlet weak var credentialTypePicker: UIPickerView!
  36. /// The label for the "email" text field.
  37. @IBOutlet weak var emailInputLabel: UILabel!
  38. /// The "email" text field.
  39. @IBOutlet weak var emailField: UITextField!
  40. /// The label for the "password" text field.
  41. @IBOutlet weak var passwordInputLabel: UILabel!
  42. /// The "password" text field.
  43. @IBOutlet weak var passwordField: UITextField!
  44. /// The currently selected action type.
  45. fileprivate var actionType = ActionType(rawValue: 0)! {
  46. didSet {
  47. if actionType != oldValue {
  48. actionPicker.reloadAllComponents()
  49. actionPicker.selectRow(actionType == .auth ? authAction.rawValue : userAction.rawValue,
  50. inComponent: 0, animated: false)
  51. }
  52. }
  53. }
  54. /// The currently selected auth action.
  55. fileprivate var authAction = AuthAction(rawValue: 0)!
  56. /// The currently selected user action.
  57. fileprivate var userAction = UserAction(rawValue: 0)!
  58. /// The currently selected credential.
  59. fileprivate var credentialType = CredentialType(rawValue: 0)!
  60. /// The current Firebase user.
  61. fileprivate var user: User? = nil {
  62. didSet {
  63. if user?.uid != oldValue?.uid {
  64. actionTypePicker.reloadAllComponents()
  65. actionType = ActionType(rawValue: actionTypePicker.selectedRow(inComponent: 0))!
  66. }
  67. }
  68. }
  69. /// The user's photo URL used by the last network request for its contents.
  70. fileprivate var lastPhotoURL: URL? = nil
  71. override func viewDidLoad() {
  72. GIDSignIn.sharedInstance().uiDelegate = self
  73. updateUserInfo(Auth.auth())
  74. NotificationCenter.default.addObserver(forName: .AuthStateDidChange,
  75. object: Auth.auth(), queue: nil) { notification in
  76. self.updateUserInfo(notification.object as? Auth)
  77. }
  78. }
  79. /// Executes the action designated by the operator on the UI.
  80. @IBAction func execute(_ sender: UIButton) {
  81. switch actionType {
  82. case .auth:
  83. switch authAction {
  84. case .fetchProviderForEmail:
  85. Auth.auth().fetchProviders(forEmail: emailField.text!) { providers, error in
  86. self.ifNoError(error) {
  87. self.showAlert(title: "Providers", message: providers?.joined(separator: ", "))
  88. }
  89. }
  90. case .signInAnonymously:
  91. Auth.auth().signInAnonymously() { user, error in
  92. self.ifNoError(error) {
  93. self.showAlert(title: "Signed In Anonymously")
  94. }
  95. }
  96. case .signInWithCredential:
  97. getCredential() { credential in
  98. Auth.auth().signIn(with: credential) { user, error in
  99. self.ifNoError(error) {
  100. self.showAlert(title: "Signed In With Credential", message: user?.textDescription)
  101. }
  102. }
  103. }
  104. case .createUser:
  105. Auth.auth().createUser(withEmail: emailField.text!, password: passwordField.text!) {
  106. user, error in
  107. self.ifNoError(error) {
  108. self.showAlert(title: "Signed In With Credential", message: user?.textDescription)
  109. }
  110. }
  111. case .signOut:
  112. try! Auth.auth().signOut()
  113. GIDSignIn.sharedInstance().signOut()
  114. }
  115. case .user:
  116. switch userAction {
  117. case .updateEmail:
  118. user!.updateEmail(to: emailField.text!) { error in
  119. self.ifNoError(error) {
  120. self.showAlert(title: "Updated Email", message: self.user?.email)
  121. }
  122. }
  123. case .updatePassword:
  124. user!.updatePassword(to: passwordField.text!) { error in
  125. self.ifNoError(error) {
  126. self.showAlert(title: "Updated Password")
  127. }
  128. }
  129. case .reload:
  130. user!.reload() { error in
  131. self.ifNoError(error) {
  132. self.showAlert(title: "Reloaded", message: self.user?.textDescription)
  133. }
  134. }
  135. case .reauthenticate:
  136. getCredential() { credential in
  137. self.user!.reauthenticate(with: credential) { error in
  138. self.ifNoError(error) {
  139. self.showAlert(title: "Reauthenticated", message: self.user?.textDescription)
  140. }
  141. }
  142. }
  143. case .getToken:
  144. user!.getIDToken() { token, error in
  145. self.ifNoError(error) {
  146. self.showAlert(title: "Got ID Token", message: token)
  147. }
  148. }
  149. case .linkWithCredential:
  150. getCredential() { credential in
  151. self.user!.link(with: credential) { user, error in
  152. self.ifNoError(error) {
  153. self.showAlert(title: "Linked With Credential", message: user?.textDescription)
  154. }
  155. }
  156. }
  157. case .deleteAccount:
  158. user!.delete() { error in
  159. self.ifNoError(error) {
  160. self.showAlert(title: "Deleted Account")
  161. }
  162. }
  163. }
  164. }
  165. }
  166. /// Gets an AuthCredential potentially asynchronously.
  167. private func getCredential(completion: @escaping (AuthCredential) -> Void) {
  168. switch credentialType {
  169. case .google:
  170. GIDSignIn.sharedInstance().delegate = GoogleSignInDelegate(completion: { user, error in
  171. self.ifNoError(error) {
  172. completion(GoogleAuthProvider.credential(
  173. withIDToken: user!.authentication.idToken,
  174. accessToken: user!.authentication.accessToken))
  175. }
  176. })
  177. GIDSignIn.sharedInstance().signIn()
  178. case .password:
  179. completion(EmailAuthProvider.credential(withEmail: emailField.text!,
  180. password: passwordField.text!))
  181. }
  182. }
  183. /// Updates user's profile image and info text.
  184. private func updateUserInfo(_ auth: Auth?) {
  185. user = auth?.currentUser
  186. displayNameLabel.text = user?.displayName
  187. emailLabel.text = user?.email
  188. userIDLabel.text = user?.uid
  189. let providers = user?.providerData.map { userInfo in userInfo.providerID }
  190. providerListLabel.text = providers?.joined(separator: ", ")
  191. if let photoURL = user?.photoURL {
  192. lastPhotoURL = photoURL
  193. DispatchQueue.global(priority: DispatchQueue.GlobalQueuePriority.background).async {
  194. if let imageData = try? Data(contentsOf: photoURL) {
  195. let image = UIImage(data: imageData)
  196. DispatchQueue.main.async {
  197. if self.lastPhotoURL == photoURL {
  198. self.profileImage.image = image
  199. }
  200. }
  201. }
  202. }
  203. } else {
  204. lastPhotoURL = nil
  205. self.profileImage.image = nil
  206. }
  207. updateControls()
  208. }
  209. // Updates the states of the UI controls.
  210. fileprivate func updateControls() {
  211. let action: Action
  212. switch actionType {
  213. case .auth:
  214. action = authAction
  215. case .user:
  216. action = userAction
  217. }
  218. let isCredentialEnabled = action.requiresCredential
  219. credentialTypePicker.isUserInteractionEnabled = isCredentialEnabled
  220. credentialTypePicker.alpha = isCredentialEnabled ? 1.0 : 0.6
  221. let isEmailEnabled = isCredentialEnabled && credentialType.requiresEmail || action.requiresEmail
  222. emailInputLabel.alpha = isEmailEnabled ? 1.0 : 0.6
  223. emailField.isEnabled = isEmailEnabled
  224. let isPasswordEnabled = isCredentialEnabled && credentialType.requiresPassword ||
  225. action.requiresPassword
  226. passwordInputLabel.alpha = isPasswordEnabled ? 1.0 : 0.6
  227. passwordField.isEnabled = isPasswordEnabled
  228. }
  229. fileprivate func showAlert(title: String, message: String? = "") {
  230. UIAlertView(title: title, message: message ?? "(NULL)", delegate: nil, cancelButtonTitle: nil,
  231. otherButtonTitles: "OK").show()
  232. }
  233. private func ifNoError(_ error: Error?, execute: () -> Void) {
  234. guard error == nil else {
  235. showAlert(title: "Error", message: error!.localizedDescription)
  236. return
  237. }
  238. execute()
  239. }
  240. }
  241. extension ViewController : GIDSignInUIDelegate {
  242. func sign(_ signIn: GIDSignIn!, present viewController: UIViewController!) {
  243. present(viewController, animated: true, completion: nil)
  244. }
  245. func sign(_ signIn: GIDSignIn!, dismiss viewController: UIViewController!) {
  246. dismiss(animated: true, completion: nil)
  247. }
  248. }
  249. extension ViewController : UIPickerViewDataSource {
  250. func numberOfComponents(in pickerView: UIPickerView) -> Int {
  251. return 1
  252. }
  253. func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
  254. switch pickerView {
  255. case actionTypePicker:
  256. if Auth.auth().currentUser != nil {
  257. return ActionType.countWithUser
  258. } else {
  259. return ActionType.countWithoutUser
  260. }
  261. case actionPicker:
  262. switch actionType {
  263. case .auth:
  264. return AuthAction.count
  265. case .user:
  266. return UserAction.count
  267. }
  268. case credentialTypePicker:
  269. return CredentialType.count
  270. default:
  271. return 0
  272. }
  273. }
  274. }
  275. extension ViewController : UIPickerViewDelegate {
  276. func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int)
  277. -> String? {
  278. switch pickerView {
  279. case actionTypePicker:
  280. return ActionType(rawValue: row)!.text
  281. case actionPicker:
  282. switch actionType {
  283. case .auth:
  284. return AuthAction(rawValue: row)!.text
  285. case .user:
  286. return UserAction(rawValue: row)!.text
  287. }
  288. case credentialTypePicker:
  289. return CredentialType(rawValue: row)!.text
  290. default:
  291. return nil
  292. }
  293. }
  294. func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
  295. switch pickerView {
  296. case actionTypePicker:
  297. actionType = ActionType(rawValue: row)!
  298. case actionPicker:
  299. switch actionType {
  300. case .auth:
  301. authAction = AuthAction(rawValue: row)!
  302. case .user:
  303. userAction = UserAction(rawValue: row)!
  304. }
  305. case credentialTypePicker:
  306. credentialType = CredentialType(rawValue: row)!
  307. default:
  308. break
  309. }
  310. updateControls()
  311. }
  312. }
  313. /// An adapter class to pass GoogleSignIn delegate method to a block.
  314. fileprivate final class GoogleSignInDelegate: NSObject, GIDSignInDelegate {
  315. private let completion: (GIDGoogleUser?, Error?) -> Void
  316. private var retainedSelf: GoogleSignInDelegate?
  317. init(completion: @escaping (GIDGoogleUser?, Error?) -> Void) {
  318. self.completion = completion
  319. super.init()
  320. retainedSelf = self
  321. }
  322. func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser?, withError error: Error?) {
  323. completion(user, error)
  324. retainedSelf = nil
  325. }
  326. }
  327. /// The list of all possible action types.
  328. fileprivate enum ActionType: Int {
  329. case auth, user
  330. // Count of action types when no user is signed in.
  331. static var countWithoutUser: Int {
  332. return ActionType.auth.rawValue + 1
  333. }
  334. // Count of action types when a user is signed in.
  335. static var countWithUser: Int {
  336. return ActionType.user.rawValue + 1
  337. }
  338. /// The text description for a particular enum value.
  339. var text : String {
  340. switch self {
  341. case .auth:
  342. return "Auth"
  343. case .user:
  344. return "User"
  345. }
  346. }
  347. }
  348. fileprivate protocol Action {
  349. /// The text description for the particular action.
  350. var text: String { get }
  351. /// Whether or not the action requires credential.
  352. var requiresCredential : Bool { get }
  353. /// Whether or not the action requires email.
  354. var requiresEmail: Bool { get }
  355. /// Whether or not the credential requires password.
  356. var requiresPassword: Bool { get }
  357. }
  358. /// The list of all possible actions the operator can take on the Auth object.
  359. fileprivate enum AuthAction: Int, Action {
  360. case fetchProviderForEmail, signInAnonymously, signInWithCredential, createUser, signOut
  361. /// Total number of auth actions.
  362. static var count: Int {
  363. return AuthAction.signOut.rawValue + 1
  364. }
  365. var text : String {
  366. switch self {
  367. case .fetchProviderForEmail:
  368. return "Fetch Provider ⬇️"
  369. case .signInAnonymously:
  370. return "Sign In Anonymously"
  371. case .signInWithCredential:
  372. return "Sign In w/ Credential ↙️"
  373. case .createUser:
  374. return "Create User ⬇️"
  375. case .signOut:
  376. return "Sign Out"
  377. }
  378. }
  379. var requiresCredential : Bool {
  380. return self == .signInWithCredential
  381. }
  382. var requiresEmail : Bool {
  383. return self == .fetchProviderForEmail || self == .createUser
  384. }
  385. var requiresPassword : Bool {
  386. return self == .createUser
  387. }
  388. }
  389. /// The list of all possible actions the operator can take on the User object.
  390. fileprivate enum UserAction: Int, Action {
  391. case updateEmail, updatePassword, reload, reauthenticate, getToken, linkWithCredential,
  392. deleteAccount
  393. /// Total number of user actions.
  394. static var count: Int {
  395. return UserAction.deleteAccount.rawValue + 1
  396. }
  397. var text : String {
  398. switch self {
  399. case .updateEmail:
  400. return "Update Email ⬇️"
  401. case .updatePassword:
  402. return "Update Password ⬇️"
  403. case .reload:
  404. return "Reload"
  405. case .reauthenticate:
  406. return "Reauthenticate ↙️"
  407. case .getToken:
  408. return "Get Token"
  409. case .linkWithCredential:
  410. return "Link With Credential ↙️"
  411. case .deleteAccount:
  412. return "Delete Account"
  413. }
  414. }
  415. var requiresCredential : Bool {
  416. return self == .reauthenticate || self == .linkWithCredential
  417. }
  418. var requiresEmail : Bool {
  419. return self == .updateEmail
  420. }
  421. var requiresPassword : Bool {
  422. return self == .updatePassword
  423. }
  424. }
  425. /// The list of all possible credential types the operator can use to sign in or link.
  426. fileprivate enum CredentialType: Int {
  427. case google, password
  428. /// Total number of enum values.
  429. static var count: Int {
  430. return CredentialType.password.rawValue + 1
  431. }
  432. /// The text description for a particular enum value.
  433. var text : String {
  434. switch self {
  435. case .google:
  436. return "Google"
  437. case .password:
  438. return "Password ➡️️"
  439. }
  440. }
  441. /// Whether or not the credential requires email.
  442. var requiresEmail : Bool {
  443. return self == .password
  444. }
  445. /// Whether or not the credential requires password.
  446. var requiresPassword : Bool {
  447. return self == .password
  448. }
  449. }
  450. fileprivate extension User {
  451. var textDescription: String {
  452. return self.displayName ?? self.email ?? self.uid
  453. }
  454. }