UserViewController.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. // Copyright 2020 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import AuthenticationServices
  15. import CryptoKit
  16. import FirebaseAuth
  17. import UIKit
  18. class UserViewController: UIViewController, DataSourceProviderDelegate {
  19. var dataSourceProvider: DataSourceProvider<User>!
  20. var userImage = UIImageView(systemImageName: "person.circle.fill", tintColor: .secondaryLabel)
  21. var tableView: UITableView { view as! UITableView }
  22. private var _user: User?
  23. var user: User? {
  24. get { _user ?? AppManager.shared.auth().currentUser }
  25. set { _user = newValue }
  26. }
  27. /// Init allows for injecting a `User` instance during UI Testing
  28. /// - Parameter user: A Firebase User instance
  29. init(_ user: User? = nil) {
  30. super.init(nibName: nil, bundle: nil)
  31. self.user = user
  32. }
  33. @available(*, unavailable)
  34. required init?(coder: NSCoder) {
  35. fatalError("init(coder:) has not been implemented")
  36. }
  37. // MARK: - UIViewController Life Cycle
  38. override func loadView() {
  39. view = UITableView(frame: .zero, style: .insetGrouped)
  40. }
  41. override func viewDidLoad() {
  42. super.viewDidLoad()
  43. configureNavigationBar()
  44. }
  45. override func viewWillAppear(_ animated: Bool) {
  46. super.viewWillAppear(animated)
  47. configureDataSourceProvider()
  48. updateUserImage()
  49. }
  50. // MARK: - DataSourceProviderDelegate
  51. func tableViewDidScroll(_ tableView: UITableView) {
  52. adjustUserImageAlpha(tableView.contentOffset.y)
  53. }
  54. func didSelectRowAt(_ indexPath: IndexPath, on tableView: UITableView) {
  55. let item = dataSourceProvider.item(at: indexPath)
  56. let actionName = item.isEditable ? item.detailTitle! : item.title!
  57. guard let action = UserAction(rawValue: actionName) else {
  58. // The row tapped has no affiliated action.
  59. return
  60. }
  61. switch action {
  62. case .signOut:
  63. signCurrentUserOut()
  64. case .link:
  65. linkUserToOtherAuthProviders()
  66. case .requestVerifyEmail:
  67. requestVerifyEmail()
  68. case .tokenRefresh:
  69. refreshCurrentUserIDToken()
  70. case .tokenRefreshAsync:
  71. refreshCurrentUserIDTokenAsync()
  72. case .delete:
  73. deleteCurrentUser()
  74. case .updateEmail:
  75. presentEditUserInfoController(for: actionName, to: updateUserEmail)
  76. case .updatePassword:
  77. presentEditUserInfoController(for: actionName, to: updatePassword)
  78. case .updateDisplayName:
  79. presentEditUserInfoController(for: actionName, to: updateUserDisplayName)
  80. case .updatePhotoURL:
  81. presentEditUserInfoController(for: actionName, to: updatePhotoURL)
  82. case .updatePhoneNumber:
  83. presentEditUserInfoController(
  84. for: actionName + " formatted like +16509871234",
  85. to: updatePhoneNumber
  86. )
  87. case .refreshUserInfo:
  88. refreshUserInfo()
  89. case .passkey:
  90. user?.passkeyName
  91. }
  92. }
  93. // MARK: - Firebase 🔥
  94. public func signCurrentUserOut() {
  95. try? AppManager.shared.auth().signOut()
  96. updateUI()
  97. }
  98. public func linkUserToOtherAuthProviders() {
  99. guard let user = user else { return }
  100. let accountLinkingController = AccountLinkingViewController(for: user)
  101. let navController = UINavigationController(rootViewController: accountLinkingController)
  102. navigationController?.present(navController, animated: true, completion: nil)
  103. }
  104. public func requestVerifyEmail() {
  105. user?.sendEmailVerification { error in
  106. guard error == nil else { return self.displayError(error) }
  107. print("Verification email sent!")
  108. }
  109. }
  110. public func refreshCurrentUserIDToken() {
  111. let forceRefresh = true
  112. user?.getIDTokenForcingRefresh(forceRefresh) { token, error in
  113. guard error == nil else { return self.displayError(error) }
  114. if let token = token {
  115. print("New token: \(token)")
  116. }
  117. }
  118. }
  119. public func refreshCurrentUserIDTokenAsync() {
  120. Task {
  121. do {
  122. let token = try await user!.idTokenForcingRefresh(true)
  123. print("New token: \(token)")
  124. } catch {
  125. self.displayError(error)
  126. }
  127. }
  128. }
  129. public func refreshUserInfo() {
  130. user?.reload { error in
  131. if let error = error {
  132. print(error)
  133. }
  134. self.updateUI()
  135. }
  136. }
  137. public func updateUserDisplayName(to newDisplayName: String) {
  138. let changeRequest = user?.createProfileChangeRequest()
  139. changeRequest?.displayName = newDisplayName
  140. changeRequest?.commitChanges { error in
  141. guard error == nil else { return self.displayError(error) }
  142. self.updateUI()
  143. }
  144. }
  145. public func updateUserEmail(to newEmail: String) {
  146. user?.updateEmail(to: newEmail, completion: { error in
  147. guard error == nil else { return self.displayError(error) }
  148. self.updateUI()
  149. })
  150. }
  151. public func updatePassword(to newPassword: String) {
  152. user?.updatePassword(to: newPassword, completion: {
  153. error in
  154. if let error = error {
  155. print("Update password failed. \(error)", error)
  156. return
  157. } else {
  158. print("Password updated!")
  159. }
  160. self.updateUI()
  161. })
  162. }
  163. public func updatePhotoURL(to newPhotoURL: String) {
  164. guard let newPhotoURL = URL(string: newPhotoURL) else {
  165. print("Could not create new photo URL!")
  166. return
  167. }
  168. let changeRequest = user?.createProfileChangeRequest()
  169. changeRequest?.photoURL = newPhotoURL
  170. changeRequest?.commitChanges { error in
  171. guard error == nil else { return self.displayError(error) }
  172. self.updateUI()
  173. }
  174. }
  175. public func updatePhoneNumber(to newPhoneNumber: String) {
  176. Task {
  177. do {
  178. let phoneAuthProvider = PhoneAuthProvider.provider()
  179. let verificationID = try await phoneAuthProvider.verifyPhoneNumber(newPhoneNumber)
  180. let verificationCode = try await getVerificationCode()
  181. let credential = phoneAuthProvider.credential(withVerificationID: verificationID,
  182. verificationCode: verificationCode)
  183. try await user?.updatePhoneNumber(credential)
  184. self.updateUI()
  185. } catch {
  186. self.displayError(error)
  187. }
  188. }
  189. }
  190. // MARK: - Sign in with Apple Token Revocation Flow
  191. /// Used for Sign in with Apple token revocation flow.
  192. private var continuation: CheckedContinuation<ASAuthorizationAppleIDCredential, Error>?
  193. private func deleteCurrentUser() {
  194. Task {
  195. guard let user else { return }
  196. do {
  197. let needsTokenRevocation = user.providerData
  198. .contains { $0.providerID == AuthProviderID.apple.rawValue }
  199. if needsTokenRevocation {
  200. let appleIDCredential = try await signInWithApple()
  201. guard let appleIDToken = appleIDCredential.identityToken else {
  202. print("Unable to fetch identify token.")
  203. return
  204. }
  205. guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
  206. print("Unable to serialise token string from data: \(appleIDToken.debugDescription)")
  207. return
  208. }
  209. let nonce = try CryptoUtils.randomNonceString()
  210. let credential = OAuthProvider.credential(providerID: .apple,
  211. idToken: idTokenString,
  212. rawNonce: nonce)
  213. try await user.reauthenticate(with: credential)
  214. if
  215. let authorizationCode = appleIDCredential.authorizationCode,
  216. let authCodeString = String(data: authorizationCode, encoding: .utf8) {
  217. try await Auth.auth().revokeToken(withAuthorizationCode: authCodeString)
  218. }
  219. }
  220. try await user.delete()
  221. } catch {
  222. displayError(error)
  223. }
  224. }
  225. }
  226. // MARK: - Private Helpers
  227. private func getVerificationCode() async throws -> String {
  228. return try await withCheckedThrowingContinuation { continuation in
  229. self.presentEditUserInfoController(for: "Phone Auth Verification Code") { code in
  230. if code != "" {
  231. continuation.resume(returning: code)
  232. } else {
  233. // Cancelled
  234. continuation.resume(throwing: NSError())
  235. }
  236. }
  237. }
  238. }
  239. private func configureNavigationBar() {
  240. navigationItem.title = "User"
  241. guard let navigationBar = navigationController?.navigationBar else { return }
  242. navigationBar.prefersLargeTitles = true
  243. navigationBar.titleTextAttributes = [.foregroundColor: UIColor.systemOrange]
  244. navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.systemOrange]
  245. navigationBar.addProfilePic(userImage)
  246. }
  247. private func updateUserImage() {
  248. guard let photoURL = user?.photoURL else {
  249. let defaultImage = UIImage(systemName: "person.circle.fill")
  250. userImage.image = defaultImage?.withTintColor(.secondaryLabel, renderingMode: .alwaysOriginal)
  251. return
  252. }
  253. userImage.setImage(from: photoURL)
  254. }
  255. private func configureDataSourceProvider() {
  256. dataSourceProvider = DataSourceProvider(
  257. dataSource: user?.sections,
  258. emptyStateView: SignedOutView(),
  259. tableView: tableView
  260. )
  261. dataSourceProvider.delegate = self
  262. }
  263. private func updateUI() {
  264. configureDataSourceProvider()
  265. animateUpdates(for: tableView)
  266. updateUserImage()
  267. }
  268. private func animateUpdates(for tableView: UITableView) {
  269. UIView.transition(with: tableView, duration: 0.2,
  270. options: .transitionCrossDissolve,
  271. animations: { tableView.reloadData() })
  272. }
  273. private func presentEditUserInfoController(for title: String,
  274. to saveHandler: @escaping (String) -> Void) {
  275. let editController = UIAlertController(
  276. title: "Update \(title)",
  277. message: nil,
  278. preferredStyle: .alert
  279. )
  280. editController.addTextField { $0.placeholder = "New \(title)" }
  281. let saveHandler1: (UIAlertAction) -> Void = { _ in
  282. let text = editController.textFields!.first!.text!
  283. saveHandler(text)
  284. }
  285. let cancel: (UIAlertAction) -> Void = { _ in
  286. saveHandler("")
  287. }
  288. editController.addAction(UIAlertAction(title: "Save", style: .default, handler: saveHandler1))
  289. editController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: cancel))
  290. present(editController, animated: true, completion: nil)
  291. }
  292. private var originalOffset: CGFloat?
  293. private func adjustUserImageAlpha(_ offset: CGFloat) {
  294. originalOffset = originalOffset ?? offset
  295. let verticalOffset = offset - originalOffset!
  296. userImage.alpha = 1 - (verticalOffset * 0.05)
  297. }
  298. }
  299. // MARK: - Implementing Sign in with Apple for the Token Revocation Flow
  300. extension UserViewController: ASAuthorizationControllerDelegate,
  301. ASAuthorizationControllerPresentationContextProviding {
  302. // MARK: ASAuthorizationControllerDelegate
  303. func signInWithApple() async throws -> ASAuthorizationAppleIDCredential {
  304. return try await withCheckedThrowingContinuation { continuation in
  305. self.continuation = continuation
  306. let appleIDProvider = ASAuthorizationAppleIDProvider()
  307. let request = appleIDProvider.createRequest()
  308. request.requestedScopes = [.fullName, .email]
  309. let authorizationController = ASAuthorizationController(authorizationRequests: [request])
  310. authorizationController.delegate = self
  311. authorizationController.performRequests()
  312. }
  313. }
  314. func authorizationController(controller: ASAuthorizationController,
  315. didCompleteWithAuthorization authorization: ASAuthorization) {
  316. if case let appleIDCredential as ASAuthorizationAppleIDCredential = authorization.credential {
  317. continuation?.resume(returning: appleIDCredential)
  318. } else {
  319. fatalError("Unexpected authorization credential type.")
  320. }
  321. }
  322. func authorizationController(controller: ASAuthorizationController,
  323. didCompleteWithError error: Error) {
  324. continuation?.resume(throwing: error)
  325. }
  326. // MARK: ASAuthorizationControllerPresentationContextProviding
  327. func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
  328. return view.window!
  329. }
  330. }