AuthViewController.swift 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248
  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. @testable import FirebaseAuth
  15. // For Sign in with Facebook
  16. import FBSDKLoginKit
  17. // [START auth_import]
  18. import FirebaseCore
  19. import GameKit
  20. // For Sign in with Google
  21. // [START google_import]
  22. import GoogleSignIn
  23. import UIKit
  24. // For Sign in with Apple
  25. import AuthenticationServices
  26. import CryptoKit
  27. private let kFacebookAppID = "ENTER APP ID HERE"
  28. private let kContinueUrl = "Enter URL"
  29. class AuthViewController: UIViewController, DataSourceProviderDelegate {
  30. // var tableView: UITableView { view as! UITableView }
  31. var dataSourceProvider: DataSourceProvider<AuthMenuData>!
  32. var authStateDidChangeListeners: [AuthStateDidChangeListenerHandle] = []
  33. var IDTokenDidChangeListeners: [IDTokenDidChangeListenerHandle] = []
  34. var actionCodeContinueURL: URL?
  35. var actionCodeRequestType: ActionCodeRequestType = .inApp
  36. let spinner = UIActivityIndicatorView(style: .medium)
  37. var tableView: UITableView { view as! UITableView }
  38. override func loadView() {
  39. view = UITableView(frame: .zero, style: .insetGrouped)
  40. }
  41. override func viewDidLoad() {
  42. super.viewDidLoad()
  43. configureNavigationBar()
  44. configureDataSourceProvider()
  45. }
  46. private func showSpinner() {
  47. spinner.center = view.center
  48. spinner.startAnimating()
  49. view.addSubview(spinner)
  50. }
  51. private func hideSpinner() {
  52. spinner.stopAnimating()
  53. spinner.removeFromSuperview()
  54. }
  55. private func actionCodeSettings() -> ActionCodeSettings {
  56. let settings = ActionCodeSettings()
  57. settings.url = actionCodeContinueURL
  58. settings.handleCodeInApp = (actionCodeRequestType == .inApp)
  59. return settings
  60. }
  61. // MARK: - DataSourceProviderDelegate
  62. func didSelectRowAt(_ indexPath: IndexPath, on tableView: UITableView) {
  63. let item = dataSourceProvider.item(at: indexPath)
  64. let itemName = item.title!
  65. // guard let provider = AuthMenu(rawValue: providerName) else {
  66. // // The row tapped has no affiliated action.
  67. // return
  68. // }
  69. if let provider = AuthMenu(rawValue: itemName) {
  70. switch provider {
  71. case .settings:
  72. performSettings()
  73. case .google:
  74. performGoogleSignInFlow()
  75. case .apple:
  76. performAppleSignInFlow()
  77. case .facebook:
  78. performFacebookSignInFlow()
  79. case .twitter, .microsoft, .gitHub, .yahoo:
  80. performOAuthLoginFlow(for: provider)
  81. case .gameCenter:
  82. performGameCenterLoginFlow()
  83. case .emailPassword:
  84. performDemoEmailPasswordLoginFlow()
  85. case .passwordless:
  86. performPasswordlessLoginFlow()
  87. case .phoneNumber:
  88. performPhoneNumberLoginFlow()
  89. case .anonymous:
  90. performAnonymousLoginFlow()
  91. case .custom:
  92. performCustomAuthLoginFlow()
  93. case .initRecaptcha:
  94. performInitRecaptcha()
  95. case .customAuthDomain:
  96. performCustomAuthDomainFlow()
  97. case .getToken:
  98. getUserTokenResult(force: false)
  99. case .getTokenForceRefresh:
  100. getUserTokenResult(force: true)
  101. case .addAuthStateChangeListener:
  102. addAuthStateListener()
  103. case .removeLastAuthStateChangeListener:
  104. removeAuthStateListener()
  105. case .addIdTokenChangeListener:
  106. addIDTokenListener()
  107. case .removeLastIdTokenChangeListener:
  108. removeIDTokenListener()
  109. case .verifyClient:
  110. verifyClient()
  111. case .deleteApp:
  112. deleteApp()
  113. case .actionType:
  114. toggleActionCodeRequestType(at: indexPath)
  115. case .continueURL:
  116. changeActionCodeContinueURL(at: indexPath)
  117. case .requestVerifyEmail:
  118. requestVerifyEmail()
  119. case .requestPasswordReset:
  120. requestPasswordReset()
  121. case .resetPassword:
  122. resetPassword()
  123. case .checkActionCode:
  124. checkActionCode()
  125. case .applyActionCode:
  126. applyActionCode()
  127. case .verifyPasswordResetCode:
  128. verifyPasswordResetCode()
  129. }
  130. // } else if let mfaOption = MultiFactorMenu(rawValue: itemName) {
  131. // switch mfaOption {
  132. // case .phoneEnroll:
  133. // phoneEnroll()
  134. //
  135. // case .totpEnroll:
  136. // totpEnroll()
  137. //
  138. // case .multifactorUnenroll:
  139. // mfaUnenroll()
  140. // }
  141. } else {
  142. return
  143. }
  144. }
  145. // MARK: - Firebase 🔥
  146. private func performSettings() {
  147. let settingsController = SettingsViewController()
  148. navigationController?.pushViewController(settingsController, animated: true)
  149. }
  150. func authenticate(withSecondFactorError error: Error?, workflow: String) {
  151. guard let error = error as NSError?,
  152. let resolver = error.userInfo[AuthErrorUserInfoMultiFactorResolverKey] as? MultiFactorResolver else {
  153. // No resolver found, possibly no multi-factor setup required.
  154. return
  155. }
  156. var factorTypes: [String] = []
  157. var selectedFactor: MultiFactorInfo?
  158. for hint in resolver.hints {
  159. if let displayName = hint.displayName {
  160. factorTypes.append(displayName)
  161. }
  162. }
  163. let alertController = UIAlertController(
  164. title: "Select factor type to \(workflow)",
  165. message: nil,
  166. preferredStyle: .actionSheet
  167. )
  168. for factorType in factorTypes {
  169. let action = UIAlertAction(title: factorType, style: .default) { _ in
  170. selectedFactor = resolver.hints.first(where: { $0.displayName == factorType })
  171. self.handleSelectedFactor(selectedFactor, forWorkflow: workflow, withResolver: resolver)
  172. }
  173. alertController.addAction(action)
  174. }
  175. let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
  176. alertController.addAction(cancelAction)
  177. present(alertController, animated: true, completion: nil)
  178. }
  179. private func handleSelectedFactor(_ factor: MultiFactorInfo?, forWorkflow workflow: String,
  180. withResolver resolver: MultiFactorResolver) {
  181. guard let factor = factor else { return }
  182. if factor.factorID == "phone", let phoneHint = factor as? PhoneMultiFactorInfo {
  183. PhoneAuthProvider.provider().verifyPhoneNumber(with: phoneHint, uiDelegate: nil, multiFactorSession: resolver.session, completion: {
  184. verificationID, error in
  185. if let error = error {
  186. self.showAlert(for: "Multi factor signin failed.")
  187. print("Multi factor start \(workflow) failed. Error: \(error)")
  188. } else {
  189. self.showTextInputPrompt(with: "Verification code for \(phoneHint.displayName)") {
  190. verificationCode in
  191. guard !verificationCode.isEmpty else {
  192. print("Verification code not entered.")
  193. return
  194. }
  195. let credential = PhoneAuthProvider.provider().credential(
  196. withVerificationID: verificationID!,
  197. verificationCode: verificationCode
  198. )
  199. let assertion = PhoneMultiFactorGenerator.assertion(with: credential)
  200. resolver.resolveSignIn(with: assertion) {
  201. authResult, error in
  202. if let error = error {
  203. print("Phone Multi factor finalize \(workflow) failed. Error: \(error)")
  204. } else {
  205. print("Phone Multi factor finalize \(workflow) succeeded.")
  206. self.transitionToUserViewController()
  207. }
  208. }
  209. }
  210. }
  211. })
  212. } else if factor.factorID == "totp" {
  213. showTextInputPrompt(with: "TOTP Verification code for \(factor.displayName)") { oneTimePassword in
  214. guard !oneTimePassword.isEmpty else {
  215. print("TOTP Verification code not entered.")
  216. return
  217. }
  218. let assertion = TOTPMultiFactorGenerator.assertionForSignIn(
  219. withEnrollmentID: factor.uid,
  220. oneTimePassword: oneTimePassword
  221. )
  222. resolver.resolveSignIn(with: assertion) { authResult, error in
  223. if let error = error {
  224. print("TOTP Multi factor finalize \(workflow) failed. Error: \(error)")
  225. } else {
  226. print("TOTP Multi factor finalize \(workflow) succeeded.")
  227. self.transitionToUserViewController()
  228. }
  229. }
  230. }
  231. } else {
  232. showAlert(for: "Factor ID not supported.")
  233. print("Multi factor sign in does not support factor ID: \(factor.factorID).")
  234. }
  235. }
  236. private func performGoogleSignInFlow() {
  237. // [START headless_google_auth]
  238. guard let clientID = FirebaseApp.app()?.options.clientID else { return }
  239. // Create Google Sign In configuration object.
  240. // [START_EXCLUDE silent]
  241. // TODO: Move configuration to Info.plist
  242. // [END_EXCLUDE]
  243. let config = GIDConfiguration(clientID: clientID)
  244. GIDSignIn.sharedInstance.configuration = config
  245. // Start the sign in flow!
  246. GIDSignIn.sharedInstance.signIn(withPresenting: self) { [unowned self] result, error in
  247. guard error == nil else {
  248. // [START_EXCLUDE]
  249. return displayError(error)
  250. // [END_EXCLUDE]
  251. }
  252. guard let user = result?.user,
  253. let idToken = user.idToken?.tokenString
  254. else {
  255. // [START_EXCLUDE]
  256. let error = NSError(
  257. domain: "GIDSignInError",
  258. code: -1,
  259. userInfo: [
  260. NSLocalizedDescriptionKey: "Unexpected sign in result: required authentication data is missing.",
  261. ]
  262. )
  263. return displayError(error)
  264. // [END_EXCLUDE]
  265. }
  266. let credential = GoogleAuthProvider.credential(withIDToken: idToken,
  267. accessToken: user.accessToken.tokenString)
  268. // [START_EXCLUDE]
  269. signIn(with: credential)
  270. // [END_EXCLUDE]
  271. }
  272. // [END headless_google_auth]
  273. }
  274. func signIn(with credential: AuthCredential) {
  275. // [START signin_google_credential]
  276. AppManager.shared.auth().signIn(with: credential) { result, error in
  277. // [START_EXCLUDE silent]
  278. guard error == nil else {
  279. if let error = error as NSError?, error.code == AuthErrorCode.secondFactorRequired.rawValue {
  280. self.authenticate(withSecondFactorError: error, workflow: "sign in")
  281. return
  282. } else {
  283. return self.displayError(error)
  284. }
  285. }
  286. // [END_EXCLUDE]
  287. // At this point, our user is signed in
  288. // [START_EXCLUDE silent]
  289. // so we advance to the User View Controller
  290. self.transitionToUserViewController()
  291. // [END_EXCLUDE]
  292. }
  293. // [END signin_google_credential]
  294. }
  295. // For Sign in with Apple
  296. var currentNonce: String?
  297. private func performAppleSignInFlow() {
  298. do {
  299. let nonce = try CryptoUtils.randomNonceString()
  300. currentNonce = nonce
  301. let appleIDProvider = ASAuthorizationAppleIDProvider()
  302. let request = appleIDProvider.createRequest()
  303. request.requestedScopes = [.fullName, .email]
  304. request.nonce = CryptoUtils.sha256(nonce)
  305. let authorizationController = ASAuthorizationController(authorizationRequests: [request])
  306. authorizationController.delegate = self
  307. authorizationController.presentationContextProvider = self
  308. authorizationController.performRequests()
  309. } catch {
  310. // In the unlikely case that nonce generation fails, show error view.
  311. displayError(error)
  312. }
  313. }
  314. private func performFacebookSignInFlow() {
  315. // The following config can also be stored in the project's .plist
  316. Settings.shared.appID = kFacebookAppID
  317. Settings.shared.displayName = "AuthenticationExample"
  318. // Create a Facebook `LoginManager` instance
  319. let loginManager = LoginManager()
  320. loginManager.logIn(permissions: ["email"], from: self) { result, error in
  321. guard error == nil else { return self.displayError(error) }
  322. guard let accessToken = AccessToken.current else { return }
  323. let credential = FacebookAuthProvider.credential(withAccessToken: accessToken.tokenString)
  324. self.signin(with: credential)
  325. }
  326. }
  327. // Maintain a strong reference to an OAuthProvider for login
  328. private var oauthProvider: OAuthProvider!
  329. private func performOAuthLoginFlow(for provider: AuthMenu) {
  330. oauthProvider = OAuthProvider(providerID: provider.id)
  331. oauthProvider.getCredentialWith(nil) { credential, error in
  332. guard error == nil else { return self.displayError(error) }
  333. guard let credential = credential else { return }
  334. self.signin(with: credential)
  335. }
  336. }
  337. private func performGameCenterLoginFlow() {
  338. // Step 1: System Game Center Login
  339. GKLocalPlayer.local.authenticateHandler = { viewController, error in
  340. if let error = error {
  341. // Handle Game Center login error
  342. print("Error logging into Game Center: \(error.localizedDescription)")
  343. } else if let authViewController = viewController {
  344. // Present Game Center login UI if needed
  345. self.present(authViewController, animated: true)
  346. } else {
  347. // Game Center login successful, proceed to Firebase
  348. self.linkGameCenterToFirebase()
  349. }
  350. }
  351. }
  352. // Step 2: Link to Firebase
  353. private func linkGameCenterToFirebase() {
  354. GameCenterAuthProvider.getCredential { credential, error in
  355. if let error = error {
  356. // Handle Firebase credential retrieval error
  357. print("Error getting Game Center credential: \(error.localizedDescription)")
  358. } else if let credential = credential {
  359. Auth.auth().signIn(with: credential) { authResult, error in
  360. if let error = error {
  361. // Handle Firebase sign-in error
  362. print("Error signing into Firebase with Game Center: \(error.localizedDescription)")
  363. } else {
  364. // Firebase sign-in successful
  365. print("Successfully linked Game Center to Firebase")
  366. }
  367. }
  368. }
  369. }
  370. }
  371. private func performDemoEmailPasswordLoginFlow() {
  372. let loginController = LoginController()
  373. loginController.delegate = self
  374. navigationController?.pushViewController(loginController, animated: true)
  375. }
  376. private func performPasswordlessLoginFlow() {
  377. let passwordlessViewController = PasswordlessViewController()
  378. passwordlessViewController.delegate = self
  379. let navPasswordlessAuthController =
  380. UINavigationController(rootViewController: passwordlessViewController)
  381. navigationController?.present(navPasswordlessAuthController, animated: true)
  382. }
  383. private func performPhoneNumberLoginFlow() {
  384. let phoneAuthViewController = PhoneAuthViewController()
  385. phoneAuthViewController.delegate = self
  386. let navPhoneAuthController = UINavigationController(rootViewController: phoneAuthViewController)
  387. navigationController?.present(navPhoneAuthController, animated: true)
  388. }
  389. private func performAnonymousLoginFlow() {
  390. AppManager.shared.auth().signInAnonymously { result, error in
  391. guard error == nil else { return self.displayError(error) }
  392. self.transitionToUserViewController()
  393. }
  394. }
  395. private func performCustomAuthLoginFlow() {
  396. let customAuthController = CustomAuthViewController()
  397. customAuthController.delegate = self
  398. let navCustomAuthController = UINavigationController(rootViewController: customAuthController)
  399. navigationController?.present(navCustomAuthController, animated: true)
  400. }
  401. private func signin(with credential: AuthCredential) {
  402. AppManager.shared.auth().signIn(with: credential) { result, error in
  403. guard error == nil else { return self.displayError(error) }
  404. self.transitionToUserViewController()
  405. }
  406. }
  407. private func performInitRecaptcha() {
  408. Task {
  409. do {
  410. try await AppManager.shared.auth().initializeRecaptchaConfig()
  411. print("Initializing Recaptcha config succeeded.")
  412. } catch {
  413. print("Initializing Recaptcha config failed: \(error).")
  414. }
  415. }
  416. }
  417. private func performCustomAuthDomainFlow() {
  418. showTextInputPrompt(with: "Enter Custom Auth Domain For Auth: ", completion: { newDomain in
  419. AppManager.shared.auth().customAuthDomain = newDomain
  420. })
  421. }
  422. private func getUserTokenResult(force: Bool) {
  423. guard let currentUser = Auth.auth().currentUser else {
  424. print("Error: No user logged in")
  425. return
  426. }
  427. currentUser.getIDTokenResult(forcingRefresh: force, completion: { tokenResult, error in
  428. if error != nil {
  429. print("Error: Error refreshing token")
  430. return // Handle error case, returning early
  431. }
  432. if let tokenResult = tokenResult, let claims = tokenResult.claims as? [String: Any] {
  433. var message = "Token refresh succeeded\n\n"
  434. for (key, value) in claims {
  435. message += "\(key): \(value)\n"
  436. }
  437. self.displayInfo(title: "Info", message: message, style: .alert)
  438. } else {
  439. print("Error: Unable to access claims.")
  440. }
  441. })
  442. }
  443. private func addAuthStateListener() {
  444. weak var weakSelf = self
  445. let index = authStateDidChangeListeners.count
  446. print("Auth State Did Change Listener #\(index) was added.")
  447. let handle = Auth.auth().addStateDidChangeListener { [weak weakSelf] auth, user in
  448. guard weakSelf != nil else { return }
  449. print("Auth State Did Change Listener #\(index) was invoked on user '\(user?.uid ?? "nil")'")
  450. }
  451. authStateDidChangeListeners.append(handle)
  452. }
  453. private func removeAuthStateListener() {
  454. guard !authStateDidChangeListeners.isEmpty else {
  455. print("No remaining Auth State Did Change Listeners.")
  456. return
  457. }
  458. let index = authStateDidChangeListeners.count - 1
  459. let handle = authStateDidChangeListeners.last!
  460. Auth.auth().removeStateDidChangeListener(handle)
  461. authStateDidChangeListeners.removeLast()
  462. print("Auth State Did Change Listener #\(index) was removed.")
  463. }
  464. private func addIDTokenListener() {
  465. weak var weakSelf = self
  466. let index = IDTokenDidChangeListeners.count
  467. print("ID Token Did Change Listener #\(index) was added.")
  468. let handle = Auth.auth().addIDTokenDidChangeListener { [weak weakSelf] auth, user in
  469. guard weakSelf != nil else { return }
  470. print("ID Token Did Change Listener #\(index) was invoked on user '\(user?.uid ?? "")'.")
  471. }
  472. IDTokenDidChangeListeners.append(handle)
  473. }
  474. private func removeIDTokenListener() {
  475. guard !IDTokenDidChangeListeners.isEmpty else {
  476. print("No remaining ID Token Did Change Listeners.")
  477. return
  478. }
  479. let index = IDTokenDidChangeListeners.count - 1
  480. let handle = IDTokenDidChangeListeners.last!
  481. Auth.auth().removeIDTokenDidChangeListener(handle)
  482. IDTokenDidChangeListeners.removeLast()
  483. print("ID Token Did Change Listener #\(index) was removed.")
  484. }
  485. private func verifyClient() {
  486. AppManager.shared.auth().tokenManager.getTokenInternal { token, error in
  487. if token == nil {
  488. print("Verify iOS Client failed.")
  489. return
  490. }
  491. let request = VerifyClientRequest(
  492. withAppToken: token?.string,
  493. isSandbox: token?.type == .sandbox,
  494. requestConfiguration: AppManager.shared.auth().requestConfiguration
  495. )
  496. Task {
  497. do {
  498. let verifyResponse = try await AuthBackend.call(with: request)
  499. guard let receipt = verifyResponse.receipt,
  500. let timeoutDate = verifyResponse.suggestedTimeOutDate else {
  501. print("Internal Auth Error: invalid VerifyClientResponse.")
  502. return
  503. }
  504. let timeout = timeoutDate.timeIntervalSinceNow
  505. do {
  506. let credential = await AppManager.shared.auth().appCredentialManager
  507. .didStartVerification(
  508. withReceipt: receipt,
  509. timeout: timeout
  510. )
  511. guard credential.secret != nil else {
  512. print("Failed to receive remote notification to verify App ID.")
  513. return
  514. }
  515. let testPhoneNumber = "+16509964692"
  516. let request = SendVerificationCodeRequest(
  517. phoneNumber: testPhoneNumber,
  518. codeIdentity: CodeIdentity.credential(credential),
  519. requestConfiguration: AppManager.shared.auth().requestConfiguration
  520. )
  521. do {
  522. _ = try await AuthBackend.call(with: request)
  523. print("Verify iOS client succeeded")
  524. } catch {
  525. print("Verify iOS Client failed: \(error.localizedDescription)")
  526. }
  527. }
  528. } catch {
  529. print("Verify iOS Client failed: \(error.localizedDescription)")
  530. }
  531. }
  532. }
  533. }
  534. private func deleteApp() {
  535. AppManager.shared.app.delete { success in
  536. if success {
  537. print("App deleted successfully.")
  538. } else {
  539. print("Failed to delete app.")
  540. }
  541. }
  542. }
  543. private func toggleActionCodeRequestType(at indexPath: IndexPath) {
  544. switch actionCodeRequestType {
  545. case .inApp:
  546. actionCodeRequestType = .continue
  547. case .continue:
  548. actionCodeRequestType = .email
  549. case .email:
  550. actionCodeRequestType = .inApp
  551. }
  552. dataSourceProvider.updateItem(
  553. at: indexPath,
  554. item: Item(title: AuthMenu.actionType.name, detailTitle: actionCodeRequestType.name)
  555. )
  556. tableView.reloadData()
  557. }
  558. private func changeActionCodeContinueURL(at indexPath: IndexPath) {
  559. showTextInputPrompt(with: "Continue URL:", completion: { newContinueURL in
  560. self.actionCodeContinueURL = URL(string: newContinueURL)
  561. print("Successfully set Continue URL to: \(newContinueURL)")
  562. self.dataSourceProvider.updateItem(
  563. at: indexPath,
  564. item: Item(
  565. title: AuthMenu.continueURL.name,
  566. detailTitle: self.actionCodeContinueURL?.absoluteString,
  567. isEditable: true
  568. )
  569. )
  570. self.tableView.reloadData()
  571. })
  572. }
  573. private func requestVerifyEmail() {
  574. showSpinner()
  575. let completionHandler: (Error?) -> Void = { [weak self] error in
  576. guard let self = self else { return }
  577. self.hideSpinner()
  578. if let error = error {
  579. let errorMessage = "Error sending verification email: \(error.localizedDescription)"
  580. showAlert(for: errorMessage)
  581. print(errorMessage)
  582. } else {
  583. let successMessage = "Verification email sent successfully!"
  584. showAlert(for: successMessage)
  585. print(successMessage)
  586. }
  587. }
  588. if actionCodeRequestType == .email {
  589. AppManager.shared.auth().currentUser?.sendEmailVerification(completion: completionHandler)
  590. } else {
  591. if actionCodeContinueURL == nil {
  592. print("Error: Action code continue URL is nil.")
  593. return
  594. }
  595. AppManager.shared.auth().currentUser?.sendEmailVerification(
  596. with: actionCodeSettings(),
  597. completion: completionHandler
  598. )
  599. }
  600. }
  601. func requestPasswordReset() {
  602. showTextInputPrompt(with: "Email:", completion: { email in
  603. print("Sending password reset link to: \(email)")
  604. self.showSpinner()
  605. let completionHandler: (Error?) -> Void = { [weak self] error in
  606. guard let self = self else { return }
  607. self.hideSpinner()
  608. if let error = error {
  609. print("Request password reset failed: \(error)")
  610. showAlert(for: error.localizedDescription)
  611. return
  612. }
  613. print("Request password reset succeeded.")
  614. showAlert(for: "Sent!")
  615. }
  616. if self.actionCodeRequestType == .email {
  617. AppManager.shared.auth().sendPasswordReset(withEmail: email, completion: completionHandler)
  618. } else {
  619. guard let actionCodeContinueURL = self.actionCodeContinueURL else {
  620. print("Error: Action code continue URL is nil.")
  621. return
  622. }
  623. AppManager.shared.auth().sendPasswordReset(
  624. withEmail: email,
  625. actionCodeSettings: self.actionCodeSettings(),
  626. completion: completionHandler
  627. )
  628. }
  629. })
  630. }
  631. private func resetPassword() {
  632. showSpinner()
  633. let completionHandler: (Error?) -> Void = { [weak self] error in
  634. guard let self = self else { return }
  635. self.hideSpinner()
  636. if let error = error {
  637. print("Password reset failed \(error)")
  638. showAlert(for: error.localizedDescription)
  639. return
  640. }
  641. print("Password reset succeeded")
  642. showAlert(for: "Password reset succeeded!")
  643. }
  644. showTextInputPrompt(with: "OOB Code:") {
  645. code in
  646. self.showTextInputPrompt(with: "New Password") {
  647. password in
  648. AppManager.shared.auth().confirmPasswordReset(
  649. withCode: code,
  650. newPassword: password,
  651. completion: completionHandler
  652. )
  653. }
  654. }
  655. }
  656. private func nameForActionCodeOperation(_ operation: ActionCodeOperation) -> String {
  657. switch operation {
  658. case .verifyEmail:
  659. return "Verify Email"
  660. case .recoverEmail:
  661. return "Recover Email"
  662. case .passwordReset:
  663. return "Password Reset"
  664. case .emailLink:
  665. return "Email Sign-In Link"
  666. case .verifyAndChangeEmail:
  667. return "Verify Before Change Email"
  668. case .revertSecondFactorAddition:
  669. return "Revert Second Factor Addition"
  670. case .unknown:
  671. return "Unknown action"
  672. }
  673. }
  674. private func checkActionCode() {
  675. showSpinner()
  676. let completionHandler: (ActionCodeInfo?, Error?) -> Void = { [weak self] info, error in
  677. guard let self = self else { return }
  678. self.hideSpinner()
  679. if let error = error {
  680. print("Check action code failed: \(error)")
  681. showAlert(for: error.localizedDescription)
  682. return
  683. }
  684. guard let info = info else { return }
  685. print("Check action code succeeded")
  686. let email = info.email
  687. let previousEmail = info.previousEmail
  688. let message = previousEmail != nil ? "\(previousEmail!) -> \(email)" : email
  689. let operation = self.nameForActionCodeOperation(info.operation)
  690. showAlert(for: operation)
  691. }
  692. showTextInputPrompt(with: "OOB Code:") {
  693. oobCode in
  694. AppManager.shared.auth().checkActionCode(oobCode, completion: completionHandler)
  695. }
  696. }
  697. private func applyActionCode() {
  698. showSpinner()
  699. let completionHandler: (Error?) -> Void = { [weak self] error in
  700. guard let self = self else { return }
  701. self.hideSpinner()
  702. if let error = error {
  703. print("Apply action code failed \(error)")
  704. showAlert(for: error.localizedDescription)
  705. return
  706. }
  707. print("Apply action code succeeded")
  708. showAlert(for: "Action code was properly applied")
  709. }
  710. showTextInputPrompt(with: "OOB Code: ") {
  711. oobCode in
  712. AppManager.shared.auth().applyActionCode(oobCode, completion: completionHandler)
  713. }
  714. }
  715. private func verifyPasswordResetCode() {
  716. showSpinner()
  717. let completionHandler: (String?, Error?) -> Void = { [weak self] email, error in
  718. guard let self = self else { return }
  719. self.hideSpinner()
  720. if let error = error {
  721. print("Verify password reset code failed \(error)")
  722. showAlert(for: error.localizedDescription)
  723. return
  724. }
  725. print("Verify password resest code succeeded.")
  726. showAlert(for: "Code verified for email: \(email)")
  727. }
  728. showTextInputPrompt(with: "OOB Code: ") {
  729. oobCode in
  730. AppManager.shared.auth().verifyPasswordResetCode(oobCode, completion: completionHandler)
  731. }
  732. }
  733. private func phoneEnroll() {
  734. guard let user = AppManager.shared.auth().currentUser else {
  735. showAlert(for: "No user logged in!")
  736. print("Error: User must be logged in first.")
  737. return
  738. }
  739. showTextInputPrompt(with: "Phone Number:") { phoneNumber in
  740. user.multiFactor.getSessionWithCompletion { session, error in
  741. guard let session = session else { return }
  742. guard error == nil else {
  743. self.showAlert(for: "Enrollment failed")
  744. print("Multi factor start enroll failed. Error: \(error!)")
  745. return
  746. }
  747. PhoneAuthProvider.provider()
  748. .verifyPhoneNumber(phoneNumber, multiFactorSession: session) { verificationID, error in
  749. guard error == nil else {
  750. self.showAlert(for: "Enrollment failed")
  751. print("Multi factor start enroll failed. Error: \(error!)")
  752. return
  753. }
  754. self.showTextInputPrompt(with: "Verification Code: ") { verificationCode in
  755. let credential = PhoneAuthProvider.provider().credential(
  756. withVerificationID: verificationID!,
  757. verificationCode: verificationCode
  758. )
  759. let assertion = PhoneMultiFactorGenerator.assertion(with: credential)
  760. self.showTextInputPrompt(with: "Display Name:") { displayName in
  761. user.multiFactor.enroll(with: assertion, displayName: displayName) { error in
  762. if let error = error {
  763. self.showAlert(for: "Enrollment failed")
  764. print("Multi factor finalize enroll failed. Error: \(error)")
  765. } else {
  766. self.showAlert(for: "Successfully enrolled: \(displayName)")
  767. print("Multi factor finalize enroll succeeded.")
  768. }
  769. }
  770. }
  771. }
  772. }
  773. }
  774. }
  775. }
  776. private func totpEnroll() {
  777. guard let user = AppManager.shared.auth().currentUser else {
  778. print("Error: User must be logged in first.")
  779. return
  780. }
  781. user.multiFactor.getSessionWithCompletion { session, error in
  782. guard let session = session, error == nil else {
  783. if let error = error {
  784. self.showAlert(for: "Enrollment failed")
  785. print("Multi factor start enroll failed. Error: \(error.localizedDescription)")
  786. } else {
  787. self.showAlert(for: "Enrollment failed")
  788. print("Multi factor start enroll failed with unknown error.")
  789. }
  790. return
  791. }
  792. TOTPMultiFactorGenerator.generateSecret(with: session) { secret, error in
  793. guard let secret = secret, error == nil else {
  794. if let error = error {
  795. self.showAlert(for: "Enrollment failed")
  796. print("Error generating TOTP secret. Error: \(error.localizedDescription)")
  797. }
  798. return
  799. }
  800. guard let accountName = user.email, let issuer = Auth.auth().app?.name else {
  801. self.showAlert(for: "Enrollment failed")
  802. print("Multi factor finalize enroll failed. Could not get account details.")
  803. return
  804. }
  805. DispatchQueue.main.async {
  806. let url = secret.generateQRCodeURL(withAccountName: accountName, issuer: issuer)
  807. guard !url.isEmpty else {
  808. self.showAlert(for: "Enrollment failed")
  809. print("Multi factor finalize enroll failed. Could not generate URL.")
  810. return
  811. }
  812. secret.openInOTPApp(withQRCodeURL: url)
  813. self
  814. .showQRCodePromptWithTextInput(with: "Scan this QR code and enter OTP:",
  815. url: url) { oneTimePassword in
  816. guard !oneTimePassword.isEmpty else {
  817. self.showAlert(for: "Display name must not be empty")
  818. print("OTP not entered.")
  819. return
  820. }
  821. let assertion = TOTPMultiFactorGenerator.assertionForEnrollment(
  822. with: secret,
  823. oneTimePassword: oneTimePassword
  824. )
  825. self.showTextInputPrompt(with: "Display Name") { displayName in
  826. guard !displayName.isEmpty else {
  827. self.showAlert(for: "Display name must not be empty")
  828. print("Display name not entered.")
  829. return
  830. }
  831. user.multiFactor.enroll(with: assertion, displayName: displayName) { error in
  832. if let error = error {
  833. self.showAlert(for: "Enrollment failed")
  834. print(
  835. "Multi factor finalize enroll failed. Error: \(error.localizedDescription)"
  836. )
  837. } else {
  838. self.showAlert(for: "Successfully enrolled: \(displayName)")
  839. print("Multi factor finalize enroll succeeded.")
  840. }
  841. }
  842. }
  843. }
  844. }
  845. }
  846. }
  847. }
  848. func mfaUnenroll() {
  849. var displayNames: [String] = []
  850. guard let currentUser = Auth.auth().currentUser else {
  851. print("Error: No current user")
  852. return
  853. }
  854. for factorInfo in currentUser.multiFactor.enrolledFactors {
  855. if let displayName = factorInfo.displayName {
  856. displayNames.append(displayName)
  857. }
  858. }
  859. let alertController = UIAlertController(
  860. title: "Select Multi Factor to Unenroll",
  861. message: nil,
  862. preferredStyle: .actionSheet
  863. )
  864. for displayName in displayNames {
  865. let action = UIAlertAction(title: displayName, style: .default) { _ in
  866. self.unenrollFactor(with: displayName)
  867. }
  868. alertController.addAction(action)
  869. }
  870. let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
  871. alertController.addAction(cancelAction)
  872. present(alertController, animated: true, completion: nil)
  873. }
  874. private func unenrollFactor(with displayName: String) {
  875. guard let currentUser = Auth.auth().currentUser else {
  876. showAlert(for: "User must be logged in")
  877. print("Error: No current user")
  878. return
  879. }
  880. var factorInfoToUnenroll: MultiFactorInfo?
  881. for factorInfo in currentUser.multiFactor.enrolledFactors {
  882. if factorInfo.displayName == displayName {
  883. factorInfoToUnenroll = factorInfo
  884. break
  885. }
  886. }
  887. if let factorInfo = factorInfoToUnenroll {
  888. currentUser.multiFactor.unenroll(withFactorUID: factorInfo.uid) { error in
  889. if let error = error {
  890. self.showAlert(for: "Failed to unenroll factor: \(displayName)")
  891. print("Multi factor unenroll failed. Error: \(error.localizedDescription)")
  892. } else {
  893. self.showAlert(for: "Successfully unenrolled: \(displayName)")
  894. print("Multi factor unenroll succeeded.")
  895. }
  896. }
  897. }
  898. }
  899. // MARK: - Private Helpers
  900. private func showTextInputPrompt(with message: String, completion: ((String) -> Void)? = nil) {
  901. let editController = UIAlertController(
  902. title: message,
  903. message: nil,
  904. preferredStyle: .alert
  905. )
  906. editController.addTextField()
  907. let saveHandler: (UIAlertAction) -> Void = { _ in
  908. let text = editController.textFields?.first?.text ?? ""
  909. completion?(text)
  910. // completion?()
  911. }
  912. let cancelHandler: (UIAlertAction) -> Void = { _ in
  913. completion?("")
  914. // completion?()
  915. }
  916. editController.addAction(UIAlertAction(title: "Save", style: .default, handler: saveHandler))
  917. editController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: cancelHandler))
  918. // Assuming `self` is a view controller
  919. present(editController, animated: true, completion: nil)
  920. }
  921. private func showQRCodePromptWithTextInput(with message: String, url: String, completion: ((String) -> Void)? = nil) {
  922. // // Create a UIAlertController
  923. // let alertController = UIAlertController(title: "QR Code Prompt", message: message, preferredStyle: .alert)
  924. //
  925. // // Add a text field for input
  926. // alertController.addTextField { (textField) in
  927. // textField.placeholder = "Enter text"
  928. // }
  929. //
  930. // Create a UIImage from the URL
  931. guard let image = generateQRCode(from: url) else {
  932. print("Failed to generate QR code")
  933. return
  934. }
  935. //
  936. // // Create an image view to display the QR code
  937. let imageView = UIImageView(image: image)
  938. // imageView.contentMode = .scaleAspectFit
  939. // imageView.translatesAutoresizingMaskIntoConstraints = false
  940. // // Add the image view to the alert controller
  941. // alertController.view.addSubview(imageView)
  942. // alertController.view.heightAnchor.constraint(equalToConstant: 240)
  943. // // Add constraints to position the image view
  944. // NSLayoutConstraint.activate([
  945. // imageView.topAnchor.constraint(equalTo: alertController.view.topAnchor, constant: 20),
  946. // imageView.centerXAnchor.constraint(equalTo: alertController.view.centerXAnchor),
  947. // imageView.widthAnchor.constraint(equalToConstant: 200),
  948. // imageView.heightAnchor.constraint(equalToConstant: 200)
  949. // ])
  950. let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .alert)
  951. // Add image view
  952. imageView.contentMode = .scaleAspectFit
  953. let imageSize = CGSize(width: 200, height: 200) // Adjust the size as needed
  954. imageView.frame = CGRect(origin: .zero, size: imageSize)
  955. alertController.view.addSubview(imageView)
  956. // Add message label
  957. let messageLabel = UILabel()
  958. messageLabel.text = message
  959. messageLabel.textAlignment = .center
  960. messageLabel.numberOfLines = 0 // Allow multiple lines
  961. alertController.view.addSubview(messageLabel)
  962. // Add text field
  963. alertController.addTextField { textField in
  964. textField.placeholder = "Enter text"
  965. }
  966. // Add actions
  967. let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
  968. let submitAction = UIAlertAction(title: "Submit", style: .default) { (_) in
  969. if let text = alertController.textFields?.first?.text {
  970. completion?(text)
  971. }
  972. }
  973. alertController.addAction(cancelAction)
  974. alertController.addAction(submitAction)
  975. // Present the alert controller
  976. UIApplication.shared.windows.first?.rootViewController?.present(alertController, animated: true, completion: nil)
  977. }
  978. // Function to generate QR code from a string
  979. private func generateQRCode(from string: String) -> UIImage? {
  980. let data = string.data(using: String.Encoding.ascii)
  981. if let filter = CIFilter(name: "CIQRCodeGenerator") {
  982. filter.setValue(data, forKey: "inputMessage")
  983. let transform = CGAffineTransform(scaleX: 10, y: 10)
  984. if let output = filter.outputImage?.transformed(by: transform) {
  985. return UIImage(ciImage: output)
  986. }
  987. }
  988. return nil
  989. }
  990. func showAlert(for message: String) {
  991. let alertController = UIAlertController(
  992. title: message,
  993. message: nil,
  994. preferredStyle: .alert
  995. )
  996. alertController.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default))
  997. }
  998. private func configureDataSourceProvider() {
  999. dataSourceProvider = DataSourceProvider(
  1000. dataSource: AuthMenuData.sections,
  1001. tableView: tableView
  1002. )
  1003. dataSourceProvider.delegate = self
  1004. }
  1005. private func configureNavigationBar() {
  1006. navigationItem.title = "Firebase Auth"
  1007. guard let navigationBar = navigationController?.navigationBar else { return }
  1008. navigationBar.prefersLargeTitles = true
  1009. navigationBar.titleTextAttributes = [.foregroundColor: UIColor.systemOrange]
  1010. navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.systemOrange]
  1011. }
  1012. private func transitionToUserViewController() {
  1013. // UserViewController is at index 1 in the tabBarController.viewControllers array
  1014. tabBarController?.transitionToViewController(atIndex: 1)
  1015. }
  1016. }
  1017. // MARK: - LoginDelegate
  1018. extension AuthViewController: LoginDelegate {
  1019. public func loginDidOccur() {
  1020. transitionToUserViewController()
  1021. }
  1022. }
  1023. // MARK: - Implementing Sign in with Apple with Firebase
  1024. extension AuthViewController: ASAuthorizationControllerDelegate,
  1025. ASAuthorizationControllerPresentationContextProviding {
  1026. // MARK: ASAuthorizationControllerDelegate
  1027. func authorizationController(controller: ASAuthorizationController,
  1028. didCompleteWithAuthorization authorization: ASAuthorization) {
  1029. guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential
  1030. else {
  1031. print("Unable to retrieve AppleIDCredential")
  1032. return
  1033. }
  1034. guard let nonce = currentNonce else {
  1035. fatalError("Invalid state: A login callback was received, but no login request was sent.")
  1036. }
  1037. guard let appleIDToken = appleIDCredential.identityToken else {
  1038. print("Unable to fetch identity token")
  1039. return
  1040. }
  1041. guard let appleAuthCode = appleIDCredential.authorizationCode else {
  1042. print("Unable to fetch authorization code")
  1043. return
  1044. }
  1045. guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
  1046. print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
  1047. return
  1048. }
  1049. guard let _ = String(data: appleAuthCode, encoding: .utf8) else {
  1050. print("Unable to serialize auth code string from data: \(appleAuthCode.debugDescription)")
  1051. return
  1052. }
  1053. // use this call to create the authentication credential and set the user's full name
  1054. let credential = OAuthProvider.appleCredential(withIDToken: idTokenString,
  1055. rawNonce: nonce,
  1056. fullName: appleIDCredential.fullName)
  1057. AppManager.shared.auth().signIn(with: credential) { result, error in
  1058. // Error. If error.code == .MissingOrInvalidNonce, make sure
  1059. // you're sending the SHA256-hashed nonce as a hex string with
  1060. // your request to Apple.
  1061. guard error == nil else { return self.displayError(error) }
  1062. // At this point, our user is signed in
  1063. // so we advance to the User View Controller
  1064. self.transitionToUserViewController()
  1065. }
  1066. }
  1067. func authorizationController(controller: ASAuthorizationController,
  1068. didCompleteWithError error: Error) {
  1069. // Ensure that you have:
  1070. // - enabled `Sign in with Apple` on the Firebase console
  1071. // - added the `Sign in with Apple` capability for this project
  1072. print("Sign in with Apple failed: \(error)")
  1073. }
  1074. // MARK: ASAuthorizationControllerPresentationContextProviding
  1075. func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
  1076. return view.window!
  1077. }
  1078. }