| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248 |
- // 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.
- @testable import FirebaseAuth
- // For Sign in with Facebook
- import FBSDKLoginKit
- // [START auth_import]
- import FirebaseCore
- import GameKit
- // For Sign in with Google
- // [START google_import]
- import GoogleSignIn
- import UIKit
- // For Sign in with Apple
- import AuthenticationServices
- import CryptoKit
- private let kFacebookAppID = "ENTER APP ID HERE"
- private let kContinueUrl = "Enter URL"
- class AuthViewController: UIViewController, DataSourceProviderDelegate {
- // var tableView: UITableView { view as! UITableView }
- var dataSourceProvider: DataSourceProvider<AuthMenuData>!
- var authStateDidChangeListeners: [AuthStateDidChangeListenerHandle] = []
- var IDTokenDidChangeListeners: [IDTokenDidChangeListenerHandle] = []
- var actionCodeContinueURL: URL?
- var actionCodeRequestType: ActionCodeRequestType = .inApp
- let spinner = UIActivityIndicatorView(style: .medium)
- var tableView: UITableView { view as! UITableView }
- override func loadView() {
- view = UITableView(frame: .zero, style: .insetGrouped)
- }
- override func viewDidLoad() {
- super.viewDidLoad()
- configureNavigationBar()
- configureDataSourceProvider()
- }
- private func showSpinner() {
- spinner.center = view.center
- spinner.startAnimating()
- view.addSubview(spinner)
- }
- private func hideSpinner() {
- spinner.stopAnimating()
- spinner.removeFromSuperview()
- }
- private func actionCodeSettings() -> ActionCodeSettings {
- let settings = ActionCodeSettings()
- settings.url = actionCodeContinueURL
- settings.handleCodeInApp = (actionCodeRequestType == .inApp)
- return settings
- }
- // MARK: - DataSourceProviderDelegate
- func didSelectRowAt(_ indexPath: IndexPath, on tableView: UITableView) {
- let item = dataSourceProvider.item(at: indexPath)
-
- let itemName = item.title!
-
- // guard let provider = AuthMenu(rawValue: providerName) else {
- // // The row tapped has no affiliated action.
- // return
- // }
- if let provider = AuthMenu(rawValue: itemName) {
- switch provider {
- case .settings:
- performSettings()
-
- case .google:
- performGoogleSignInFlow()
-
- case .apple:
- performAppleSignInFlow()
-
- case .facebook:
- performFacebookSignInFlow()
-
- case .twitter, .microsoft, .gitHub, .yahoo:
- performOAuthLoginFlow(for: provider)
-
- case .gameCenter:
- performGameCenterLoginFlow()
-
- case .emailPassword:
- performDemoEmailPasswordLoginFlow()
-
- case .passwordless:
- performPasswordlessLoginFlow()
-
- case .phoneNumber:
- performPhoneNumberLoginFlow()
-
- case .anonymous:
- performAnonymousLoginFlow()
-
- case .custom:
- performCustomAuthLoginFlow()
-
- case .initRecaptcha:
- performInitRecaptcha()
-
- case .customAuthDomain:
- performCustomAuthDomainFlow()
-
- case .getToken:
- getUserTokenResult(force: false)
-
- case .getTokenForceRefresh:
- getUserTokenResult(force: true)
-
- case .addAuthStateChangeListener:
- addAuthStateListener()
-
- case .removeLastAuthStateChangeListener:
- removeAuthStateListener()
-
- case .addIdTokenChangeListener:
- addIDTokenListener()
-
- case .removeLastIdTokenChangeListener:
- removeIDTokenListener()
-
- case .verifyClient:
- verifyClient()
-
- case .deleteApp:
- deleteApp()
-
- case .actionType:
- toggleActionCodeRequestType(at: indexPath)
-
- case .continueURL:
- changeActionCodeContinueURL(at: indexPath)
-
- case .requestVerifyEmail:
- requestVerifyEmail()
-
- case .requestPasswordReset:
- requestPasswordReset()
-
- case .resetPassword:
- resetPassword()
-
- case .checkActionCode:
- checkActionCode()
-
- case .applyActionCode:
- applyActionCode()
-
- case .verifyPasswordResetCode:
- verifyPasswordResetCode()
- }
- // } else if let mfaOption = MultiFactorMenu(rawValue: itemName) {
- // switch mfaOption {
- // case .phoneEnroll:
- // phoneEnroll()
- //
- // case .totpEnroll:
- // totpEnroll()
- //
- // case .multifactorUnenroll:
- // mfaUnenroll()
- // }
- } else {
- return
- }
- }
- // MARK: - Firebase 🔥
- private func performSettings() {
- let settingsController = SettingsViewController()
- navigationController?.pushViewController(settingsController, animated: true)
- }
- func authenticate(withSecondFactorError error: Error?, workflow: String) {
- guard let error = error as NSError?,
- let resolver = error.userInfo[AuthErrorUserInfoMultiFactorResolverKey] as? MultiFactorResolver else {
- // No resolver found, possibly no multi-factor setup required.
- return
- }
- var factorTypes: [String] = []
- var selectedFactor: MultiFactorInfo?
- for hint in resolver.hints {
- if let displayName = hint.displayName {
- factorTypes.append(displayName)
- }
- }
- let alertController = UIAlertController(
- title: "Select factor type to \(workflow)",
- message: nil,
- preferredStyle: .actionSheet
- )
- for factorType in factorTypes {
- let action = UIAlertAction(title: factorType, style: .default) { _ in
- selectedFactor = resolver.hints.first(where: { $0.displayName == factorType })
- self.handleSelectedFactor(selectedFactor, forWorkflow: workflow, withResolver: resolver)
- }
- alertController.addAction(action)
- }
- let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
- alertController.addAction(cancelAction)
- present(alertController, animated: true, completion: nil)
- }
- private func handleSelectedFactor(_ factor: MultiFactorInfo?, forWorkflow workflow: String,
- withResolver resolver: MultiFactorResolver) {
- guard let factor = factor else { return }
- if factor.factorID == "phone", let phoneHint = factor as? PhoneMultiFactorInfo {
- PhoneAuthProvider.provider().verifyPhoneNumber(with: phoneHint, uiDelegate: nil, multiFactorSession: resolver.session, completion: {
- verificationID, error in
- if let error = error {
- self.showAlert(for: "Multi factor signin failed.")
- print("Multi factor start \(workflow) failed. Error: \(error)")
- } else {
- self.showTextInputPrompt(with: "Verification code for \(phoneHint.displayName)") {
- verificationCode in
- guard !verificationCode.isEmpty else {
- print("Verification code not entered.")
- return
- }
- let credential = PhoneAuthProvider.provider().credential(
- withVerificationID: verificationID!,
- verificationCode: verificationCode
- )
- let assertion = PhoneMultiFactorGenerator.assertion(with: credential)
- resolver.resolveSignIn(with: assertion) {
- authResult, error in
- if let error = error {
- print("Phone Multi factor finalize \(workflow) failed. Error: \(error)")
- } else {
- print("Phone Multi factor finalize \(workflow) succeeded.")
- self.transitionToUserViewController()
- }
- }
- }
- }
- })
- } else if factor.factorID == "totp" {
- showTextInputPrompt(with: "TOTP Verification code for \(factor.displayName)") { oneTimePassword in
- guard !oneTimePassword.isEmpty else {
- print("TOTP Verification code not entered.")
- return
- }
- let assertion = TOTPMultiFactorGenerator.assertionForSignIn(
- withEnrollmentID: factor.uid,
- oneTimePassword: oneTimePassword
- )
- resolver.resolveSignIn(with: assertion) { authResult, error in
- if let error = error {
- print("TOTP Multi factor finalize \(workflow) failed. Error: \(error)")
- } else {
- print("TOTP Multi factor finalize \(workflow) succeeded.")
- self.transitionToUserViewController()
- }
- }
- }
- } else {
- showAlert(for: "Factor ID not supported.")
- print("Multi factor sign in does not support factor ID: \(factor.factorID).")
- }
- }
- private func performGoogleSignInFlow() {
- // [START headless_google_auth]
- guard let clientID = FirebaseApp.app()?.options.clientID else { return }
- // Create Google Sign In configuration object.
- // [START_EXCLUDE silent]
- // TODO: Move configuration to Info.plist
- // [END_EXCLUDE]
- let config = GIDConfiguration(clientID: clientID)
- GIDSignIn.sharedInstance.configuration = config
- // Start the sign in flow!
- GIDSignIn.sharedInstance.signIn(withPresenting: self) { [unowned self] result, error in
- guard error == nil else {
- // [START_EXCLUDE]
- return displayError(error)
- // [END_EXCLUDE]
- }
- guard let user = result?.user,
- let idToken = user.idToken?.tokenString
- else {
- // [START_EXCLUDE]
- let error = NSError(
- domain: "GIDSignInError",
- code: -1,
- userInfo: [
- NSLocalizedDescriptionKey: "Unexpected sign in result: required authentication data is missing.",
- ]
- )
- return displayError(error)
- // [END_EXCLUDE]
- }
- let credential = GoogleAuthProvider.credential(withIDToken: idToken,
- accessToken: user.accessToken.tokenString)
- // [START_EXCLUDE]
- signIn(with: credential)
- // [END_EXCLUDE]
- }
- // [END headless_google_auth]
- }
- func signIn(with credential: AuthCredential) {
- // [START signin_google_credential]
- AppManager.shared.auth().signIn(with: credential) { result, error in
- // [START_EXCLUDE silent]
- guard error == nil else {
- if let error = error as NSError?, error.code == AuthErrorCode.secondFactorRequired.rawValue {
- self.authenticate(withSecondFactorError: error, workflow: "sign in")
- return
- } else {
- return self.displayError(error)
- }
- }
- // [END_EXCLUDE]
- // At this point, our user is signed in
- // [START_EXCLUDE silent]
- // so we advance to the User View Controller
- self.transitionToUserViewController()
- // [END_EXCLUDE]
- }
- // [END signin_google_credential]
- }
- // For Sign in with Apple
- var currentNonce: String?
- private func performAppleSignInFlow() {
- 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)
- }
- }
- private func performFacebookSignInFlow() {
- // The following config can also be stored in the project's .plist
- Settings.shared.appID = kFacebookAppID
- Settings.shared.displayName = "AuthenticationExample"
- // Create a Facebook `LoginManager` instance
- let loginManager = LoginManager()
- loginManager.logIn(permissions: ["email"], from: self) { result, error in
- guard error == nil else { return self.displayError(error) }
- guard let accessToken = AccessToken.current else { return }
- let credential = FacebookAuthProvider.credential(withAccessToken: accessToken.tokenString)
- self.signin(with: credential)
- }
- }
- // Maintain a strong reference to an OAuthProvider for login
- private var oauthProvider: OAuthProvider!
- private func performOAuthLoginFlow(for provider: AuthMenu) {
- oauthProvider = OAuthProvider(providerID: provider.id)
- oauthProvider.getCredentialWith(nil) { credential, error in
- guard error == nil else { return self.displayError(error) }
- guard let credential = credential else { return }
- self.signin(with: credential)
- }
- }
- private func performGameCenterLoginFlow() {
- // Step 1: System Game Center Login
- GKLocalPlayer.local.authenticateHandler = { viewController, error in
- if let error = error {
- // Handle Game Center login error
- print("Error logging into Game Center: \(error.localizedDescription)")
- } else if let authViewController = viewController {
- // Present Game Center login UI if needed
- self.present(authViewController, animated: true)
- } else {
- // Game Center login successful, proceed to Firebase
- self.linkGameCenterToFirebase()
- }
- }
- }
- // Step 2: Link to Firebase
- private func linkGameCenterToFirebase() {
- GameCenterAuthProvider.getCredential { credential, error in
- if let error = error {
- // Handle Firebase credential retrieval error
- print("Error getting Game Center credential: \(error.localizedDescription)")
- } else if let credential = credential {
- Auth.auth().signIn(with: credential) { authResult, error in
- if let error = error {
- // Handle Firebase sign-in error
- print("Error signing into Firebase with Game Center: \(error.localizedDescription)")
- } else {
- // Firebase sign-in successful
- print("Successfully linked Game Center to Firebase")
- }
- }
- }
- }
- }
- private func performDemoEmailPasswordLoginFlow() {
- let loginController = LoginController()
- loginController.delegate = self
- navigationController?.pushViewController(loginController, animated: true)
- }
- private func performPasswordlessLoginFlow() {
- let passwordlessViewController = PasswordlessViewController()
- passwordlessViewController.delegate = self
- let navPasswordlessAuthController =
- UINavigationController(rootViewController: passwordlessViewController)
- navigationController?.present(navPasswordlessAuthController, animated: true)
- }
- private func performPhoneNumberLoginFlow() {
- let phoneAuthViewController = PhoneAuthViewController()
- phoneAuthViewController.delegate = self
- let navPhoneAuthController = UINavigationController(rootViewController: phoneAuthViewController)
- navigationController?.present(navPhoneAuthController, animated: true)
- }
- private func performAnonymousLoginFlow() {
- AppManager.shared.auth().signInAnonymously { result, error in
- guard error == nil else { return self.displayError(error) }
- self.transitionToUserViewController()
- }
- }
- private func performCustomAuthLoginFlow() {
- let customAuthController = CustomAuthViewController()
- customAuthController.delegate = self
- let navCustomAuthController = UINavigationController(rootViewController: customAuthController)
- navigationController?.present(navCustomAuthController, animated: true)
- }
- private func signin(with credential: AuthCredential) {
- AppManager.shared.auth().signIn(with: credential) { result, error in
- guard error == nil else { return self.displayError(error) }
- self.transitionToUserViewController()
- }
- }
- private func performInitRecaptcha() {
- Task {
- do {
- try await AppManager.shared.auth().initializeRecaptchaConfig()
- print("Initializing Recaptcha config succeeded.")
- } catch {
- print("Initializing Recaptcha config failed: \(error).")
- }
- }
- }
- private func performCustomAuthDomainFlow() {
- showTextInputPrompt(with: "Enter Custom Auth Domain For Auth: ", completion: { newDomain in
- AppManager.shared.auth().customAuthDomain = newDomain
- })
- }
- private func getUserTokenResult(force: Bool) {
- guard let currentUser = Auth.auth().currentUser else {
- print("Error: No user logged in")
- return
- }
- currentUser.getIDTokenResult(forcingRefresh: force, completion: { tokenResult, error in
- if error != nil {
- print("Error: Error refreshing token")
- return // Handle error case, returning early
- }
- if let tokenResult = tokenResult, let claims = tokenResult.claims as? [String: Any] {
- var message = "Token refresh succeeded\n\n"
- for (key, value) in claims {
- message += "\(key): \(value)\n"
- }
- self.displayInfo(title: "Info", message: message, style: .alert)
- } else {
- print("Error: Unable to access claims.")
- }
- })
- }
- private func addAuthStateListener() {
- weak var weakSelf = self
- let index = authStateDidChangeListeners.count
- print("Auth State Did Change Listener #\(index) was added.")
- let handle = Auth.auth().addStateDidChangeListener { [weak weakSelf] auth, user in
- guard weakSelf != nil else { return }
- print("Auth State Did Change Listener #\(index) was invoked on user '\(user?.uid ?? "nil")'")
- }
- authStateDidChangeListeners.append(handle)
- }
- private func removeAuthStateListener() {
- guard !authStateDidChangeListeners.isEmpty else {
- print("No remaining Auth State Did Change Listeners.")
- return
- }
- let index = authStateDidChangeListeners.count - 1
- let handle = authStateDidChangeListeners.last!
- Auth.auth().removeStateDidChangeListener(handle)
- authStateDidChangeListeners.removeLast()
- print("Auth State Did Change Listener #\(index) was removed.")
- }
- private func addIDTokenListener() {
- weak var weakSelf = self
- let index = IDTokenDidChangeListeners.count
- print("ID Token Did Change Listener #\(index) was added.")
- let handle = Auth.auth().addIDTokenDidChangeListener { [weak weakSelf] auth, user in
- guard weakSelf != nil else { return }
- print("ID Token Did Change Listener #\(index) was invoked on user '\(user?.uid ?? "")'.")
- }
- IDTokenDidChangeListeners.append(handle)
- }
- private func removeIDTokenListener() {
- guard !IDTokenDidChangeListeners.isEmpty else {
- print("No remaining ID Token Did Change Listeners.")
- return
- }
- let index = IDTokenDidChangeListeners.count - 1
- let handle = IDTokenDidChangeListeners.last!
- Auth.auth().removeIDTokenDidChangeListener(handle)
- IDTokenDidChangeListeners.removeLast()
- print("ID Token Did Change Listener #\(index) was removed.")
- }
- private func verifyClient() {
- AppManager.shared.auth().tokenManager.getTokenInternal { token, error in
- if token == nil {
- print("Verify iOS Client failed.")
- return
- }
- let request = VerifyClientRequest(
- withAppToken: token?.string,
- isSandbox: token?.type == .sandbox,
- requestConfiguration: AppManager.shared.auth().requestConfiguration
- )
- Task {
- do {
- let verifyResponse = try await AuthBackend.call(with: request)
- guard let receipt = verifyResponse.receipt,
- let timeoutDate = verifyResponse.suggestedTimeOutDate else {
- print("Internal Auth Error: invalid VerifyClientResponse.")
- return
- }
- let timeout = timeoutDate.timeIntervalSinceNow
- do {
- let credential = await AppManager.shared.auth().appCredentialManager
- .didStartVerification(
- withReceipt: receipt,
- timeout: timeout
- )
- guard credential.secret != nil else {
- print("Failed to receive remote notification to verify App ID.")
- return
- }
- let testPhoneNumber = "+16509964692"
- let request = SendVerificationCodeRequest(
- phoneNumber: testPhoneNumber,
- codeIdentity: CodeIdentity.credential(credential),
- requestConfiguration: AppManager.shared.auth().requestConfiguration
- )
- do {
- _ = try await AuthBackend.call(with: request)
- print("Verify iOS client succeeded")
- } catch {
- print("Verify iOS Client failed: \(error.localizedDescription)")
- }
- }
- } catch {
- print("Verify iOS Client failed: \(error.localizedDescription)")
- }
- }
- }
- }
- private func deleteApp() {
- AppManager.shared.app.delete { success in
- if success {
- print("App deleted successfully.")
- } else {
- print("Failed to delete app.")
- }
- }
- }
- private func toggleActionCodeRequestType(at indexPath: IndexPath) {
- switch actionCodeRequestType {
- case .inApp:
- actionCodeRequestType = .continue
- case .continue:
- actionCodeRequestType = .email
- case .email:
- actionCodeRequestType = .inApp
- }
- dataSourceProvider.updateItem(
- at: indexPath,
- item: Item(title: AuthMenu.actionType.name, detailTitle: actionCodeRequestType.name)
- )
- tableView.reloadData()
- }
- private func changeActionCodeContinueURL(at indexPath: IndexPath) {
- showTextInputPrompt(with: "Continue URL:", completion: { newContinueURL in
- self.actionCodeContinueURL = URL(string: newContinueURL)
- print("Successfully set Continue URL to: \(newContinueURL)")
- self.dataSourceProvider.updateItem(
- at: indexPath,
- item: Item(
- title: AuthMenu.continueURL.name,
- detailTitle: self.actionCodeContinueURL?.absoluteString,
- isEditable: true
- )
- )
- self.tableView.reloadData()
- })
- }
- private func requestVerifyEmail() {
- showSpinner()
- let completionHandler: (Error?) -> Void = { [weak self] error in
- guard let self = self else { return }
- self.hideSpinner()
- if let error = error {
- let errorMessage = "Error sending verification email: \(error.localizedDescription)"
- showAlert(for: errorMessage)
- print(errorMessage)
- } else {
- let successMessage = "Verification email sent successfully!"
- showAlert(for: successMessage)
- print(successMessage)
- }
- }
- if actionCodeRequestType == .email {
- AppManager.shared.auth().currentUser?.sendEmailVerification(completion: completionHandler)
- } else {
- if actionCodeContinueURL == nil {
- print("Error: Action code continue URL is nil.")
- return
- }
- AppManager.shared.auth().currentUser?.sendEmailVerification(
- with: actionCodeSettings(),
- completion: completionHandler
- )
- }
- }
- func requestPasswordReset() {
- showTextInputPrompt(with: "Email:", completion: { email in
- print("Sending password reset link to: \(email)")
- self.showSpinner()
- let completionHandler: (Error?) -> Void = { [weak self] error in
- guard let self = self else { return }
- self.hideSpinner()
- if let error = error {
- print("Request password reset failed: \(error)")
- showAlert(for: error.localizedDescription)
- return
- }
- print("Request password reset succeeded.")
- showAlert(for: "Sent!")
- }
- if self.actionCodeRequestType == .email {
- AppManager.shared.auth().sendPasswordReset(withEmail: email, completion: completionHandler)
- } else {
- guard let actionCodeContinueURL = self.actionCodeContinueURL else {
- print("Error: Action code continue URL is nil.")
- return
- }
- AppManager.shared.auth().sendPasswordReset(
- withEmail: email,
- actionCodeSettings: self.actionCodeSettings(),
- completion: completionHandler
- )
- }
- })
- }
- private func resetPassword() {
- showSpinner()
- let completionHandler: (Error?) -> Void = { [weak self] error in
- guard let self = self else { return }
- self.hideSpinner()
- if let error = error {
- print("Password reset failed \(error)")
- showAlert(for: error.localizedDescription)
- return
- }
- print("Password reset succeeded")
- showAlert(for: "Password reset succeeded!")
- }
- showTextInputPrompt(with: "OOB Code:") {
- code in
- self.showTextInputPrompt(with: "New Password") {
- password in
- AppManager.shared.auth().confirmPasswordReset(
- withCode: code,
- newPassword: password,
- completion: completionHandler
- )
- }
- }
- }
- private func nameForActionCodeOperation(_ operation: ActionCodeOperation) -> String {
- switch operation {
- case .verifyEmail:
- return "Verify Email"
- case .recoverEmail:
- return "Recover Email"
- case .passwordReset:
- return "Password Reset"
- case .emailLink:
- return "Email Sign-In Link"
- case .verifyAndChangeEmail:
- return "Verify Before Change Email"
- case .revertSecondFactorAddition:
- return "Revert Second Factor Addition"
- case .unknown:
- return "Unknown action"
- }
- }
- private func checkActionCode() {
- showSpinner()
- let completionHandler: (ActionCodeInfo?, Error?) -> Void = { [weak self] info, error in
- guard let self = self else { return }
- self.hideSpinner()
- if let error = error {
- print("Check action code failed: \(error)")
- showAlert(for: error.localizedDescription)
- return
- }
- guard let info = info else { return }
- print("Check action code succeeded")
- let email = info.email
- let previousEmail = info.previousEmail
- let message = previousEmail != nil ? "\(previousEmail!) -> \(email)" : email
- let operation = self.nameForActionCodeOperation(info.operation)
- showAlert(for: operation)
- }
- showTextInputPrompt(with: "OOB Code:") {
- oobCode in
- AppManager.shared.auth().checkActionCode(oobCode, completion: completionHandler)
- }
- }
- private func applyActionCode() {
- showSpinner()
- let completionHandler: (Error?) -> Void = { [weak self] error in
- guard let self = self else { return }
- self.hideSpinner()
- if let error = error {
- print("Apply action code failed \(error)")
- showAlert(for: error.localizedDescription)
- return
- }
- print("Apply action code succeeded")
- showAlert(for: "Action code was properly applied")
- }
- showTextInputPrompt(with: "OOB Code: ") {
- oobCode in
- AppManager.shared.auth().applyActionCode(oobCode, completion: completionHandler)
- }
- }
- private func verifyPasswordResetCode() {
- showSpinner()
- let completionHandler: (String?, Error?) -> Void = { [weak self] email, error in
- guard let self = self else { return }
- self.hideSpinner()
- if let error = error {
- print("Verify password reset code failed \(error)")
- showAlert(for: error.localizedDescription)
- return
- }
- print("Verify password resest code succeeded.")
- showAlert(for: "Code verified for email: \(email)")
- }
- showTextInputPrompt(with: "OOB Code: ") {
- oobCode in
- AppManager.shared.auth().verifyPasswordResetCode(oobCode, completion: completionHandler)
- }
- }
- private func phoneEnroll() {
- guard let user = AppManager.shared.auth().currentUser else {
- showAlert(for: "No user logged in!")
- print("Error: User must be logged in first.")
- return
- }
- showTextInputPrompt(with: "Phone Number:") { phoneNumber in
- user.multiFactor.getSessionWithCompletion { session, error in
- guard let session = session else { return }
- guard error == nil else {
- self.showAlert(for: "Enrollment failed")
- print("Multi factor start enroll failed. Error: \(error!)")
- return
- }
- PhoneAuthProvider.provider()
- .verifyPhoneNumber(phoneNumber, multiFactorSession: session) { verificationID, error in
- guard error == nil else {
- self.showAlert(for: "Enrollment failed")
- print("Multi factor start enroll failed. Error: \(error!)")
- return
- }
- self.showTextInputPrompt(with: "Verification Code: ") { verificationCode in
- let credential = PhoneAuthProvider.provider().credential(
- withVerificationID: verificationID!,
- verificationCode: verificationCode
- )
- let assertion = PhoneMultiFactorGenerator.assertion(with: credential)
- self.showTextInputPrompt(with: "Display Name:") { displayName in
- user.multiFactor.enroll(with: assertion, displayName: displayName) { error in
- if let error = error {
- self.showAlert(for: "Enrollment failed")
- print("Multi factor finalize enroll failed. Error: \(error)")
- } else {
- self.showAlert(for: "Successfully enrolled: \(displayName)")
- print("Multi factor finalize enroll succeeded.")
- }
- }
- }
- }
- }
- }
- }
- }
- private func totpEnroll() {
- guard let user = AppManager.shared.auth().currentUser else {
- print("Error: User must be logged in first.")
- return
- }
- user.multiFactor.getSessionWithCompletion { session, error in
- guard let session = session, error == nil else {
- if let error = error {
- self.showAlert(for: "Enrollment failed")
- print("Multi factor start enroll failed. Error: \(error.localizedDescription)")
- } else {
- self.showAlert(for: "Enrollment failed")
- print("Multi factor start enroll failed with unknown error.")
- }
- return
- }
- TOTPMultiFactorGenerator.generateSecret(with: session) { secret, error in
- guard let secret = secret, error == nil else {
- if let error = error {
- self.showAlert(for: "Enrollment failed")
- print("Error generating TOTP secret. Error: \(error.localizedDescription)")
- }
- return
- }
- guard let accountName = user.email, let issuer = Auth.auth().app?.name else {
- self.showAlert(for: "Enrollment failed")
- print("Multi factor finalize enroll failed. Could not get account details.")
- return
- }
- DispatchQueue.main.async {
- let url = secret.generateQRCodeURL(withAccountName: accountName, issuer: issuer)
- guard !url.isEmpty else {
- self.showAlert(for: "Enrollment failed")
- print("Multi factor finalize enroll failed. Could not generate URL.")
- return
- }
- secret.openInOTPApp(withQRCodeURL: url)
- self
- .showQRCodePromptWithTextInput(with: "Scan this QR code and enter OTP:",
- url: url) { oneTimePassword in
- guard !oneTimePassword.isEmpty else {
- self.showAlert(for: "Display name must not be empty")
- print("OTP not entered.")
- return
- }
- let assertion = TOTPMultiFactorGenerator.assertionForEnrollment(
- with: secret,
- oneTimePassword: oneTimePassword
- )
- self.showTextInputPrompt(with: "Display Name") { displayName in
- guard !displayName.isEmpty else {
- self.showAlert(for: "Display name must not be empty")
- print("Display name not entered.")
- return
- }
- user.multiFactor.enroll(with: assertion, displayName: displayName) { error in
- if let error = error {
- self.showAlert(for: "Enrollment failed")
- print(
- "Multi factor finalize enroll failed. Error: \(error.localizedDescription)"
- )
- } else {
- self.showAlert(for: "Successfully enrolled: \(displayName)")
- print("Multi factor finalize enroll succeeded.")
- }
- }
- }
- }
- }
- }
- }
- }
- func mfaUnenroll() {
- var displayNames: [String] = []
- guard let currentUser = Auth.auth().currentUser else {
- print("Error: No current user")
- return
- }
- for factorInfo in currentUser.multiFactor.enrolledFactors {
- if let displayName = factorInfo.displayName {
- displayNames.append(displayName)
- }
- }
- let alertController = UIAlertController(
- title: "Select Multi Factor to Unenroll",
- message: nil,
- preferredStyle: .actionSheet
- )
- for displayName in displayNames {
- let action = UIAlertAction(title: displayName, style: .default) { _ in
- self.unenrollFactor(with: displayName)
- }
- alertController.addAction(action)
- }
- let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
- alertController.addAction(cancelAction)
- present(alertController, animated: true, completion: nil)
- }
- private func unenrollFactor(with displayName: String) {
- guard let currentUser = Auth.auth().currentUser else {
- showAlert(for: "User must be logged in")
- print("Error: No current user")
- return
- }
- var factorInfoToUnenroll: MultiFactorInfo?
- for factorInfo in currentUser.multiFactor.enrolledFactors {
- if factorInfo.displayName == displayName {
- factorInfoToUnenroll = factorInfo
- break
- }
- }
- if let factorInfo = factorInfoToUnenroll {
- currentUser.multiFactor.unenroll(withFactorUID: factorInfo.uid) { error in
- if let error = error {
- self.showAlert(for: "Failed to unenroll factor: \(displayName)")
- print("Multi factor unenroll failed. Error: \(error.localizedDescription)")
- } else {
- self.showAlert(for: "Successfully unenrolled: \(displayName)")
- print("Multi factor unenroll succeeded.")
- }
- }
- }
- }
- // MARK: - Private Helpers
- private func showTextInputPrompt(with message: String, completion: ((String) -> Void)? = nil) {
- let editController = UIAlertController(
- title: message,
- message: nil,
- preferredStyle: .alert
- )
- editController.addTextField()
- let saveHandler: (UIAlertAction) -> Void = { _ in
- let text = editController.textFields?.first?.text ?? ""
- completion?(text)
- // completion?()
- }
- let cancelHandler: (UIAlertAction) -> Void = { _ in
- completion?("")
- // completion?()
- }
- editController.addAction(UIAlertAction(title: "Save", style: .default, handler: saveHandler))
- editController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: cancelHandler))
- // Assuming `self` is a view controller
- present(editController, animated: true, completion: nil)
- }
-
- private func showQRCodePromptWithTextInput(with message: String, url: String, completion: ((String) -> Void)? = nil) {
- // // Create a UIAlertController
- // let alertController = UIAlertController(title: "QR Code Prompt", message: message, preferredStyle: .alert)
- //
- // // Add a text field for input
- // alertController.addTextField { (textField) in
- // textField.placeholder = "Enter text"
- // }
- //
- // Create a UIImage from the URL
- guard let image = generateQRCode(from: url) else {
- print("Failed to generate QR code")
- return
- }
- //
- // // Create an image view to display the QR code
- let imageView = UIImageView(image: image)
- // imageView.contentMode = .scaleAspectFit
- // imageView.translatesAutoresizingMaskIntoConstraints = false
- // // Add the image view to the alert controller
- // alertController.view.addSubview(imageView)
- // alertController.view.heightAnchor.constraint(equalToConstant: 240)
- // // Add constraints to position the image view
- // NSLayoutConstraint.activate([
- // imageView.topAnchor.constraint(equalTo: alertController.view.topAnchor, constant: 20),
- // imageView.centerXAnchor.constraint(equalTo: alertController.view.centerXAnchor),
- // imageView.widthAnchor.constraint(equalToConstant: 200),
- // imageView.heightAnchor.constraint(equalToConstant: 200)
- // ])
-
- let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .alert)
-
- // Add image view
- imageView.contentMode = .scaleAspectFit
- let imageSize = CGSize(width: 200, height: 200) // Adjust the size as needed
- imageView.frame = CGRect(origin: .zero, size: imageSize)
- alertController.view.addSubview(imageView)
-
- // Add message label
- let messageLabel = UILabel()
- messageLabel.text = message
- messageLabel.textAlignment = .center
- messageLabel.numberOfLines = 0 // Allow multiple lines
- alertController.view.addSubview(messageLabel)
-
- // Add text field
- alertController.addTextField { textField in
- textField.placeholder = "Enter text"
- }
-
- // Add actions
- let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
- let submitAction = UIAlertAction(title: "Submit", style: .default) { (_) in
- if let text = alertController.textFields?.first?.text {
- completion?(text)
- }
- }
-
- alertController.addAction(cancelAction)
- alertController.addAction(submitAction)
-
- // Present the alert controller
- UIApplication.shared.windows.first?.rootViewController?.present(alertController, animated: true, completion: nil)
- }
-
- // Function to generate QR code from a string
- private func generateQRCode(from string: String) -> UIImage? {
- let data = string.data(using: String.Encoding.ascii)
- if let filter = CIFilter(name: "CIQRCodeGenerator") {
- filter.setValue(data, forKey: "inputMessage")
- let transform = CGAffineTransform(scaleX: 10, y: 10)
- if let output = filter.outputImage?.transformed(by: transform) {
- return UIImage(ciImage: output)
- }
- }
- return nil
- }
- func showAlert(for message: String) {
- let alertController = UIAlertController(
- title: message,
- message: nil,
- preferredStyle: .alert
- )
- alertController.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default))
- }
- private func configureDataSourceProvider() {
- dataSourceProvider = DataSourceProvider(
- dataSource: AuthMenuData.sections,
- tableView: tableView
- )
- dataSourceProvider.delegate = self
- }
- private func configureNavigationBar() {
- navigationItem.title = "Firebase Auth"
- guard let navigationBar = navigationController?.navigationBar else { return }
- navigationBar.prefersLargeTitles = true
- navigationBar.titleTextAttributes = [.foregroundColor: UIColor.systemOrange]
- navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.systemOrange]
- }
- private func transitionToUserViewController() {
- // UserViewController is at index 1 in the tabBarController.viewControllers array
- tabBarController?.transitionToViewController(atIndex: 1)
- }
- }
- // MARK: - LoginDelegate
- extension AuthViewController: LoginDelegate {
- public func loginDidOccur() {
- transitionToUserViewController()
- }
- }
- // MARK: - Implementing Sign in with Apple with Firebase
- extension AuthViewController: ASAuthorizationControllerDelegate,
- ASAuthorizationControllerPresentationContextProviding {
- // MARK: ASAuthorizationControllerDelegate
- func authorizationController(controller: ASAuthorizationController,
- didCompleteWithAuthorization authorization: ASAuthorization) {
- guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential
- else {
- print("Unable to retrieve AppleIDCredential")
- return
- }
- guard let nonce = currentNonce else {
- fatalError("Invalid state: A login callback was received, but no login request was sent.")
- }
- guard let appleIDToken = appleIDCredential.identityToken else {
- print("Unable to fetch identity token")
- return
- }
- guard let appleAuthCode = appleIDCredential.authorizationCode else {
- print("Unable to fetch authorization code")
- return
- }
- guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
- print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
- return
- }
- guard let _ = String(data: appleAuthCode, encoding: .utf8) else {
- print("Unable to serialize auth code string from data: \(appleAuthCode.debugDescription)")
- return
- }
- // use this call to create the authentication credential and set the user's full name
- let credential = OAuthProvider.appleCredential(withIDToken: idTokenString,
- rawNonce: nonce,
- fullName: appleIDCredential.fullName)
- AppManager.shared.auth().signIn(with: credential) { result, error in
- // Error. If error.code == .MissingOrInvalidNonce, make sure
- // you're sending the SHA256-hashed nonce as a hex string with
- // your request to Apple.
- guard error == nil else { return self.displayError(error) }
- // At this point, our user is signed in
- // so we advance to the User View Controller
- self.transitionToUserViewController()
- }
- }
- func authorizationController(controller: ASAuthorizationController,
- didCompleteWithError error: 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!
- }
- }
|