Skip to content

Commit 243afcb

Browse files
committed
Implement verification code flow in new SwiftUI view
1 parent 7d96b99 commit 243afcb

File tree

4 files changed

+102
-268
lines changed

4 files changed

+102
-268
lines changed

FirebaseAuth/Tests/SampleSwift/AuthenticationExample.xcodeproj/project.pbxproj

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@
5757
EAE4CBC924855E3A00245E92 /* AuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAE4CBC824855E3A00245E92 /* AuthViewController.swift */; };
5858
EAE4CBCE24855E3D00245E92 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EAE4CBCD24855E3D00245E92 /* Assets.xcassets */; };
5959
EAE4CBE724855E3E00245E92 /* AuthenticationExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAE4CBE624855E3E00245E92 /* AuthenticationExampleUITests.swift */; };
60-
EAE4CBF524857A5100245E92 /* LoginController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAE4CBF424857A5100245E92 /* LoginController.swift */; };
6160
EAEBCE0F2489FFDE00FCEA92 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAEBCE0E2489FFDE00FCEA92 /* Extensions.swift */; };
6261
EAEBCE11248A9AA000FCEA92 /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAEBCE10248A9AA000FCEA92 /* Section.swift */; };
6362
EAFDF2BE2490439F0082B6F1 /* Animator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAFDF2BD2490439F0082B6F1 /* Animator.swift */; };
@@ -140,7 +139,6 @@
140139
EAE4CBE224855E3E00245E92 /* AuthenticationExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AuthenticationExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
141140
EAE4CBE624855E3E00245E92 /* AuthenticationExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationExampleUITests.swift; sourceTree = "<group>"; };
142141
EAE4CBE824855E3E00245E92 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
143-
EAE4CBF424857A5100245E92 /* LoginController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginController.swift; sourceTree = "<group>"; };
144142
EAEBCE0E2489FFDE00FCEA92 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
145143
EAEBCE10248A9AA000FCEA92 /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = "<group>"; };
146144
EAFDF2BD2490439F0082B6F1 /* Animator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Animator.swift; sourceTree = "<group>"; };
@@ -224,7 +222,6 @@
224222
children = (
225223
EAE08EB424CF5D09006FA3A5 /* AccountLinkingViewController.swift */,
226224
EAE4CBC824855E3A00245E92 /* AuthViewController.swift */,
227-
EAE4CBF424857A5100245E92 /* LoginController.swift */,
228225
EAB3A17B2494628200385291 /* UserViewController.swift */,
229226
EA02F68E24A0714B0079D000 /* OtherAuthMethodControllers */,
230227
DEC2E5DC2A95331D0090260A /* SettingsViewController.swift */,
@@ -561,7 +558,6 @@
561558
EA02F68524A000E00079D000 /* UserActions.swift in Sources */,
562559
EA02F68D24A063E90079D000 /* LoginDelegate.swift in Sources */,
563560
EA20B50A249D3D8600B5E581 /* PasswordlessViewController.swift in Sources */,
564-
EAE4CBF524857A5100245E92 /* LoginController.swift in Sources */,
565561
EA20B510249FDB4400B5E581 /* OtherAuthMethods.swift in Sources */,
566562
EA12697F29E33A5D00D79E66 /* CryptoUtils.swift in Sources */,
567563
EAEBCE11248A9AA000FCEA92 /* Section.swift in Sources */,

FirebaseAuth/Tests/SampleSwift/AuthenticationExample/CustomViews/LoginViewSwiftUI.swift

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@ import SwiftUI
1919
import FirebaseAuth
2020

2121
struct LoginViewSwiftUI: View {
22+
@Environment(\.dismiss) private var dismiss
23+
@State private var multiFactorResolver: MultiFactorResolver? = nil
24+
@State private var onetimePasscode = ""
25+
@State private var showingAlert = false
26+
2227
@State private var email: String = ""
2328
@State private var password: String = ""
29+
2430
var body: some View {
2531
Group {
2632
VStack {
@@ -62,15 +68,56 @@ struct LoginViewSwiftUI: View {
6268
_ = try await AppManager.shared
6369
.auth()
6470
.signIn(withEmail: email, password: password)
65-
} catch let error as AuthErrorCode
66-
where error.code == .secondFactorRequired {
67-
// TODO(ncooke3): Implement.
71+
// } catch let error as AuthErrorCode
72+
// where error.code == .secondFactorRequired {
73+
// // error as? AuthErrorCode == nil because AuthErrorUtils returns generic Errors
74+
// // https://firebase.google.com/docs/auth/ios/totp-mfa#sign_in_users_with_a_second_factor
75+
// // TODO(ncooke3): Fix?
76+
} catch let error as NSError
77+
where error.code == AuthErrorCode.secondFactorRequired.rawValue {
78+
let mfaKey = AuthErrorUserInfoMultiFactorResolverKey
79+
guard
80+
let resolver = error.userInfo[mfaKey] as? MultiFactorResolver,
81+
let multiFactorInfo = resolver.hints.first
82+
else { return }
83+
if multiFactorInfo.factorID == TOTPMultiFactorID {
84+
// Show the alert to enter the TOTP verification code.
85+
multiFactorResolver = resolver
86+
showingAlert = true
87+
} else {
88+
// TODO(ncooke3): Implement handling of other MFA provider (phone).
89+
}
6890
} catch {
69-
// TODO(ncooke3): Implement error display.
7091
print(error.localizedDescription)
7192
}
7293
}
7394
}
95+
.alert("Enter one time passcode.", isPresented: $showingAlert) {
96+
TextField("Verification Code", text: $onetimePasscode)
97+
.textInputAutocapitalization(.never)
98+
Button("Cancel", role: .cancel) {}
99+
Button("Submit") {
100+
Task {
101+
guard onetimePasscode.count > 0 else { return }
102+
let multiFactorInfo = multiFactorResolver!.hints[0]
103+
let assertion = TOTPMultiFactorGenerator.assertionForSignIn(
104+
withEnrollmentID: multiFactorInfo.uid,
105+
// TODO(ncooke3): Probably should avoid network request if empty passcode.
106+
oneTimePassword: self.onetimePasscode
107+
)
108+
do {
109+
_ = try await multiFactorResolver!.resolveSignIn(with: assertion)
110+
// MFA login was successful.
111+
dismiss()
112+
} catch {
113+
// Wrong or expired OTP. Re-prompt the user.
114+
// TODO(ncooke3): Show error to user.
115+
print(error)
116+
}
117+
}
118+
}
119+
}
120+
74121
LoginViewButton(
75122
text: "Create Account",
76123
accentColor: .orange,
@@ -82,7 +129,8 @@ struct LoginViewSwiftUI: View {
82129
withEmail: email,
83130
password: password
84131
)
85-
// TODO(ncooke3): Transition... `self.delegate?.loginDidOccur()`
132+
// Sign-in was successful.
133+
dismiss()
86134
} catch {
87135
// TODO(ncooke3): Implement error display.
88136
print(error.localizedDescription)
@@ -112,6 +160,7 @@ private struct SymbolTextField: TextFieldStyle {
112160
}
113161
.background(Color.color(uiColor: .secondarySystemBackground))
114162
.cornerRadius(14)
163+
.textInputAutocapitalization(.never)
115164
}
116165
}
117166

FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift

Lines changed: 48 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
// [START auth_import]
1717
import FirebaseCore
1818

19+
import SwiftUI
20+
1921
// For Sign in with Facebook
2022
import FBSDKLoginKit
2123

@@ -178,7 +180,7 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
178180
phoneEnroll()
179181

180182
case .totpEnroll:
181-
totpEnroll()
183+
Task { await totpEnroll() }
182184

183185
case .multifactorUnenroll:
184186
mfaUnenroll()
@@ -338,9 +340,9 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
338340
}
339341

340342
private func performDemoEmailPasswordLoginFlow() {
341-
let loginController = LoginController()
342-
loginController.delegate = self
343-
navigationController?.pushViewController(loginController, animated: true)
343+
let loginView = LoginViewSwiftUI()
344+
let hostingController = UIHostingController(rootView: loginView)
345+
navigationController?.pushViewController(hostingController, animated: true)
344346
}
345347

346348
private func performPasswordlessLoginFlow() {
@@ -780,89 +782,53 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
780782
}
781783
}
782784

783-
private func totpEnroll() {
784-
guard let user = AppManager.shared.auth().currentUser else {
785-
print("Error: User must be logged in first.")
785+
private func totpEnroll() async {
786+
guard
787+
let user = AppManager.shared.auth().currentUser,
788+
let accountName = user.email
789+
else {
790+
showAlert(for: "Enrollment failed: User must be logged and have email address.")
786791
return
787792
}
788793

789-
user.multiFactor.getSessionWithCompletion { session, error in
790-
guard let session = session, error == nil else {
791-
if let error = error {
792-
self.showAlert(for: "Enrollment failed")
793-
print("Multi factor start enroll failed. Error: \(error.localizedDescription)")
794-
} else {
795-
self.showAlert(for: "Enrollment failed")
796-
print("Multi factor start enroll failed with unknown error.")
797-
}
794+
guard let issuer = AppManager.shared.auth().app?.name else {
795+
showAlert(for: "Enrollment failed: Firebase app is missing name.")
796+
return
797+
}
798+
799+
do {
800+
let session = try await user.multiFactor.session()
801+
let secret = try await TOTPMultiFactorGenerator.generateSecret(with: session)
802+
print("Secret: " + secret.sharedSecretKey())
803+
804+
let url = secret.generateQRCodeURL(withAccountName: accountName, issuer: issuer)
805+
guard !url.isEmpty else {
806+
showAlert(for: "Enrollment failed")
807+
print("Multi factor finalize enroll failed. Could not generate URL.")
798808
return
799809
}
810+
secret.openInOTPApp(withQRCodeURL: url)
800811

801-
TOTPMultiFactorGenerator.generateSecret(with: session) { secret, error in
802-
guard let secret = secret, error == nil else {
803-
if let error = error {
804-
self.showAlert(for: "Enrollment failed")
805-
print("Error generating TOTP secret. Error: \(error.localizedDescription)")
806-
} else {
807-
self.showAlert(for: "Enrollment failed")
808-
print("Error generating TOTP secret.")
809-
}
810-
return
811-
}
812-
813-
guard let accountName = user.email, let issuer = Auth.auth().app?.name else {
814-
self.showAlert(for: "Enrollment failed")
815-
print("Multi factor finalize enroll failed. Could not get account details.")
816-
return
817-
}
818-
819-
DispatchQueue.main.async {
820-
let url = secret.generateQRCodeURL(withAccountName: accountName, issuer: issuer)
821-
822-
guard !url.isEmpty else {
823-
self.showAlert(for: "Enrollment failed")
824-
print("Multi factor finalize enroll failed. Could not generate URL.")
825-
return
826-
}
827-
828-
secret.openInOTPApp(withQRCodeURL: url)
829-
830-
self
831-
.showQRCodePromptWithTextInput(with: "Scan this QR code and enter OTP:",
832-
url: url) { oneTimePassword in
833-
guard !oneTimePassword.isEmpty else {
834-
self.showAlert(for: "Display name must not be empty")
835-
print("OTP not entered.")
836-
return
837-
}
812+
guard
813+
let oneTimePassword = await showTextInputPrompt(with: "Enter the one time passcode.")
814+
else {
815+
showAlert(for: "Enrollment failed: one time passcode not entered.")
816+
return
817+
}
838818

839-
let assertion = TOTPMultiFactorGenerator.assertionForEnrollment(
840-
with: secret,
841-
oneTimePassword: oneTimePassword
842-
)
819+
let assertion = TOTPMultiFactorGenerator.assertionForEnrollment(
820+
with: secret,
821+
oneTimePassword: oneTimePassword
822+
)
843823

844-
self.showTextInputPrompt(with: "Display Name") { displayName in
845-
guard !displayName.isEmpty else {
846-
self.showAlert(for: "Display name must not be empty")
847-
print("Display name not entered.")
848-
return
849-
}
824+
// TODO(nickcooke): Provide option to enter display name.
825+
try await user.multiFactor.enroll(with: assertion, displayName: "TOTP")
850826

851-
user.multiFactor.enroll(with: assertion, displayName: displayName) { error in
852-
if let error = error {
853-
self.showAlert(for: "Enrollment failed")
854-
print(
855-
"Multi factor finalize enroll failed. Error: \(error.localizedDescription)"
856-
)
857-
} else {
858-
self.showAlert(for: "Successfully enrolled: \(displayName)")
859-
print("Multi factor finalize enroll succeeded.")
860-
}
861-
}
862-
}
863-
}
864-
}
865-
}
827+
showAlert(for: "Successfully enrolled: TOTP")
828+
print("Multi factor finalize enroll succeeded.")
829+
} catch {
830+
print(error)
831+
showAlert(for: "Enrollment failed: \(error.localizedDescription)")
866832
}
867833
}
868834

@@ -958,60 +924,12 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
958924
present(editController, animated: true, completion: nil)
959925
}
960926

961-
private func showQRCodePromptWithTextInput(with message: String, url: String,
962-
completion: ((String) -> Void)? = nil) {
963-
// Create a UIAlertController
964-
let alertController = UIAlertController(
965-
title: "QR Code Prompt",
966-
message: message,
967-
preferredStyle: .alert
968-
)
969-
970-
// Add a text field for input
971-
alertController.addTextField { textField in
972-
textField.placeholder = "Enter text"
973-
}
974-
975-
// Create a UIImage from the URL
976-
guard let image = generateQRCode(from: url) else {
977-
print("Failed to generate QR code")
978-
return
979-
}
980-
981-
// Create an image view to display the QR code
982-
let imageView = UIImageView(image: image)
983-
imageView.contentMode = .scaleAspectFit
984-
imageView.translatesAutoresizingMaskIntoConstraints = false
985-
986-
// Add the image view to the alert controller
987-
alertController.view.addSubview(imageView)
988-
989-
// Add constraints to position the image view
990-
NSLayoutConstraint.activate([
991-
imageView.topAnchor.constraint(equalTo: alertController.view.topAnchor, constant: 20),
992-
imageView.centerXAnchor.constraint(equalTo: alertController.view.centerXAnchor),
993-
imageView.widthAnchor.constraint(equalToConstant: 200),
994-
imageView.heightAnchor.constraint(equalToConstant: 200),
995-
])
996-
997-
// Add actions
998-
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
999-
let submitAction = UIAlertAction(title: "Submit", style: .default) { _ in
1000-
if let completion,
1001-
let text = alertController.textFields?.first?.text {
1002-
completion(text)
927+
private func showTextInputPrompt(with message: String) async -> String? {
928+
await withCheckedContinuation { continuation in
929+
showTextInputPrompt(with: message) { inputText in
930+
continuation.resume(returning: inputText.isEmpty ? nil : inputText)
1003931
}
1004932
}
1005-
1006-
alertController.addAction(cancelAction)
1007-
alertController.addAction(submitAction)
1008-
1009-
// Present the alert controller
1010-
UIApplication.shared.windows.first?.rootViewController?.present(
1011-
alertController,
1012-
animated: true,
1013-
completion: nil
1014-
)
1015933
}
1016934

1017935
// Function to generate QR code from a string

0 commit comments

Comments
 (0)