| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381 |
- // Copyright 2020 Google LLC
- //
- // 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 AuthenticationServices
- import CryptoKit
- import FirebaseAuth
- import UIKit
- class UserViewController: UIViewController, DataSourceProviderDelegate {
- var dataSourceProvider: DataSourceProvider<User>!
- var userImage = UIImageView(systemImageName: "person.circle.fill", tintColor: .secondaryLabel)
- var tableView: UITableView { view as! UITableView }
- private var _user: User?
- var user: User? {
- get { _user ?? AppManager.shared.auth().currentUser }
- set { _user = newValue }
- }
- /// Init allows for injecting a `User` instance during UI Testing
- /// - Parameter user: A Firebase User instance
- init(_ user: User? = nil) {
- super.init(nibName: nil, bundle: nil)
- self.user = user
- }
- @available(*, unavailable)
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- // MARK: - UIViewController Life Cycle
- override func loadView() {
- view = UITableView(frame: .zero, style: .insetGrouped)
- }
- override func viewDidLoad() {
- super.viewDidLoad()
- configureNavigationBar()
- }
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- configureDataSourceProvider()
- updateUserImage()
- }
- // MARK: - DataSourceProviderDelegate
- func tableViewDidScroll(_ tableView: UITableView) {
- adjustUserImageAlpha(tableView.contentOffset.y)
- }
- func didSelectRowAt(_ indexPath: IndexPath, on tableView: UITableView) {
- let item = dataSourceProvider.item(at: indexPath)
- let actionName = item.isEditable ? item.detailTitle! : item.title!
- guard let action = UserAction(rawValue: actionName) else {
- // The row tapped has no affiliated action.
- return
- }
- switch action {
- case .signOut:
- signCurrentUserOut()
- case .link:
- linkUserToOtherAuthProviders()
- case .requestVerifyEmail:
- requestVerifyEmail()
- case .tokenRefresh:
- refreshCurrentUserIDToken()
- case .delete:
- deleteCurrentUser()
- case .updateEmail:
- presentEditUserInfoController(for: actionName, to: updateUserEmail)
- case .updatePassword:
- presentEditUserInfoController(for: actionName, to: updatePassword)
- case .updateDisplayName:
- presentEditUserInfoController(for: actionName, to: updateUserDisplayName)
- case .updatePhotoURL:
- presentEditUserInfoController(for: actionName, to: updatePhotoURL)
- case .updatePhoneNumber:
- presentEditUserInfoController(
- for: actionName + " formatted like +16509871234",
- to: updatePhoneNumber
- )
- case .refreshUserInfo:
- refreshUserInfo()
- }
- }
- // MARK: - Firebase 🔥
- public func signCurrentUserOut() {
- try? AppManager.shared.auth().signOut()
- updateUI()
- }
- public func linkUserToOtherAuthProviders() {
- guard let user = user else { return }
- let accountLinkingController = AccountLinkingViewController(for: user)
- let navController = UINavigationController(rootViewController: accountLinkingController)
- navigationController?.present(navController, animated: true, completion: nil)
- }
- public func requestVerifyEmail() {
- user?.sendEmailVerification { error in
- guard error == nil else { return self.displayError(error) }
- print("Verification email sent!")
- }
- }
- public func refreshCurrentUserIDToken() {
- let forceRefresh = true
- user?.getIDTokenForcingRefresh(forceRefresh) { token, error in
- guard error == nil else { return self.displayError(error) }
- if let token = token {
- print("New token: \(token)")
- }
- }
- }
- public func refreshUserInfo() {
- user?.reload { error in
- if let error = error {
- print(error)
- }
- self.updateUI()
- }
- }
- public func updateUserDisplayName(to newDisplayName: String) {
- let changeRequest = user?.createProfileChangeRequest()
- changeRequest?.displayName = newDisplayName
- changeRequest?.commitChanges { error in
- guard error == nil else { return self.displayError(error) }
- self.updateUI()
- }
- }
- public func updateUserEmail(to newEmail: String) {
- user?.updateEmail(to: newEmail, completion: { error in
- guard error == nil else { return self.displayError(error) }
- self.updateUI()
- })
- }
- public func updatePassword(to newPassword: String) {
- user?.updatePassword(to: newPassword, completion: {
- error in
- if let error = error {
- print("Update password failed. \(error)", error)
- return
- } else {
- print("Password updated!")
- }
- self.updateUI()
- })
- }
- public func updatePhotoURL(to newPhotoURL: String) {
- guard let newPhotoURL = URL(string: newPhotoURL) else {
- print("Could not create new photo URL!")
- return
- }
- let changeRequest = user?.createProfileChangeRequest()
- changeRequest?.photoURL = newPhotoURL
- changeRequest?.commitChanges { error in
- guard error == nil else { return self.displayError(error) }
- self.updateUI()
- }
- }
- public func updatePhoneNumber(to newPhoneNumber: String) {
- Task {
- do {
- let phoneAuthProvider = PhoneAuthProvider.provider()
- let verificationID = try await phoneAuthProvider.verifyPhoneNumber(newPhoneNumber)
- let verificationCode = try await getVerificationCode()
- let credential = phoneAuthProvider.credential(withVerificationID: verificationID,
- verificationCode: verificationCode)
- try await user?.updatePhoneNumber(credential)
- self.updateUI()
- } catch {
- self.displayError(error)
- }
- }
- }
- // MARK: - Sign in with Apple Token Revocation Flow
- // For Sign in with Apple
- private var currentNonce: String?
- // [START token_revocation_deleteuser]
- private func deleteCurrentUser() {
- do {
- let nonce = try CryptoUtils.randomNonceString()
- currentNonce = nonce
- let appleIDProvider = ASAuthorizationAppleIDProvider()
- let request = appleIDProvider.createRequest()
- request.requestedScopes = [.fullName, .email]
- request.nonce = CryptoUtils.sha256(nonce)
- let authorizationController = ASAuthorizationController(authorizationRequests: [request])
- authorizationController.delegate = self
- authorizationController.presentationContextProvider = self
- authorizationController.performRequests()
- } catch {
- // In the unlikely case that nonce generation fails, show error view.
- displayError(error)
- }
- }
- // [END token_revocation_deleteuser]
- // MARK: - Private Helpers
- private func getVerificationCode() async throws -> String {
- return try await withCheckedThrowingContinuation { continuation in
- self.presentEditUserInfoController(for: "Phone Auth Verification Code") { code in
- if code != "" {
- continuation.resume(returning: code)
- } else {
- // Cancelled
- continuation.resume(throwing: NSError())
- }
- }
- }
- }
- private func configureNavigationBar() {
- navigationItem.title = "User"
- guard let navigationBar = navigationController?.navigationBar else { return }
- navigationBar.prefersLargeTitles = true
- navigationBar.titleTextAttributes = [.foregroundColor: UIColor.systemOrange]
- navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.systemOrange]
- navigationBar.addProfilePic(userImage)
- }
- private func updateUserImage() {
- guard let photoURL = user?.photoURL else {
- let defaultImage = UIImage(systemName: "person.circle.fill")
- userImage.image = defaultImage?.withTintColor(.secondaryLabel, renderingMode: .alwaysOriginal)
- return
- }
- userImage.setImage(from: photoURL)
- }
- private func configureDataSourceProvider() {
- dataSourceProvider = DataSourceProvider(
- dataSource: user?.sections,
- emptyStateView: SignedOutView(),
- tableView: tableView
- )
- dataSourceProvider.delegate = self
- }
- private func updateUI() {
- configureDataSourceProvider()
- animateUpdates(for: tableView)
- updateUserImage()
- }
- private func animateUpdates(for tableView: UITableView) {
- UIView.transition(with: tableView, duration: 0.2,
- options: .transitionCrossDissolve,
- animations: { tableView.reloadData() })
- }
- private func presentEditUserInfoController(for title: String,
- to saveHandler: @escaping (String) -> Void) {
- let editController = UIAlertController(
- title: "Update \(title)",
- message: nil,
- preferredStyle: .alert
- )
- editController.addTextField { $0.placeholder = "New \(title)" }
- let saveHandler1: (UIAlertAction) -> Void = { _ in
- let text = editController.textFields!.first!.text!
- saveHandler(text)
- }
- let cancel: (UIAlertAction) -> Void = { _ in
- saveHandler("")
- }
- editController.addAction(UIAlertAction(title: "Save", style: .default, handler: saveHandler1))
- editController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: cancel))
- present(editController, animated: true, completion: nil)
- }
- private var originalOffset: CGFloat?
- private func adjustUserImageAlpha(_ offset: CGFloat) {
- originalOffset = originalOffset ?? offset
- let verticalOffset = offset - originalOffset!
- userImage.alpha = 1 - (verticalOffset * 0.05)
- }
- }
- // MARK: - Implementing Sign in with Apple for the Token Revocation Flow
- extension UserViewController: ASAuthorizationControllerDelegate,
- ASAuthorizationControllerPresentationContextProviding {
- // MARK: ASAuthorizationControllerDelegate
- // [START token_revocation]
- func authorizationController(controller: ASAuthorizationController,
- didCompleteWithAuthorization authorization: ASAuthorization) {
- guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential
- else {
- print("Unable to retrieve AppleIDCredential")
- return
- }
- guard let _ = currentNonce else {
- fatalError("Invalid state: A login callback was received, but no login request was sent.")
- }
- guard let appleAuthCode = appleIDCredential.authorizationCode else {
- print("Unable to fetch authorization code")
- return
- }
- guard let authCodeString = String(data: appleAuthCode, encoding: .utf8) else {
- print("Unable to serialize auth code string from data: \(appleAuthCode.debugDescription)")
- return
- }
- Task {
- do {
- try await AppManager.shared.auth().revokeToken(withAuthorizationCode: authCodeString)
- try await user?.delete()
- self.updateUI()
- } catch {
- self.displayError(error)
- }
- }
- }
- // [END token_revocation]
- func authorizationController(controller: ASAuthorizationController,
- didCompleteWithError error: any Error) {
- // Ensure that you have:
- // - enabled `Sign in with Apple` on the Firebase console
- // - added the `Sign in with Apple` capability for this project
- print("Sign in with Apple failed: \(error)")
- }
- // MARK: ASAuthorizationControllerPresentationContextProviding
- func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
- return view.window!
- }
- }
|