diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample.xcodeproj/project.pbxproj b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample.xcodeproj/project.pbxproj index 2160a626445..4df627ce768 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample.xcodeproj/project.pbxproj +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample.xcodeproj/project.pbxproj @@ -46,6 +46,7 @@ EA20B50C249E8F0700B5E581 /* AuthMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA20B50B249E8F0700B5E581 /* AuthMenu.swift */; }; EA20B510249FDB4400B5E581 /* OtherAuthMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA20B50F249FDB4400B5E581 /* OtherAuthMethods.swift */; }; EA217895248979E200736757 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = EA217894248979E200736757 /* LaunchScreen.storyboard */; }; + EA3348322C90EFF40091D7C2 /* LoginViewSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3348312C90EFE40091D7C2 /* LoginViewSwiftUI.swift */; }; EA527CAA24A0766D00ADB9A2 /* OtherAuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA527CA924A0766D00ADB9A2 /* OtherAuthViewController.swift */; }; EA527CAC24A0EE9600ADB9A2 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA527CAB24A0EE9600ADB9A2 /* LoginView.swift */; }; EAB3A1792494433500385291 /* DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB3A1782494433500385291 /* DataSourceProvider.swift */; }; @@ -56,7 +57,6 @@ EAE4CBC924855E3A00245E92 /* AuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAE4CBC824855E3A00245E92 /* AuthViewController.swift */; }; EAE4CBCE24855E3D00245E92 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EAE4CBCD24855E3D00245E92 /* Assets.xcassets */; }; EAE4CBE724855E3E00245E92 /* AuthenticationExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAE4CBE624855E3E00245E92 /* AuthenticationExampleUITests.swift */; }; - EAE4CBF524857A5100245E92 /* LoginController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAE4CBF424857A5100245E92 /* LoginController.swift */; }; EAEBCE0F2489FFDE00FCEA92 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAEBCE0E2489FFDE00FCEA92 /* Extensions.swift */; }; EAEBCE11248A9AA000FCEA92 /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAEBCE10248A9AA000FCEA92 /* Section.swift */; }; EAFDF2BE2490439F0082B6F1 /* Animator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAFDF2BD2490439F0082B6F1 /* Animator.swift */; }; @@ -124,8 +124,8 @@ EA20B50B249E8F0700B5E581 /* AuthMenu.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = AuthMenu.swift; sourceTree = ""; }; EA20B50F249FDB4400B5E581 /* OtherAuthMethods.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherAuthMethods.swift; sourceTree = ""; }; EA217894248979E200736757 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + EA3348312C90EFE40091D7C2 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; EA527CA924A0766D00ADB9A2 /* OtherAuthViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OtherAuthViewController.swift; sourceTree = ""; }; - EA527CAB24A0EE9600ADB9A2 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; EAB3A1782494433500385291 /* DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSourceProvider.swift; sourceTree = ""; }; EAB3A17B2494628200385291 /* UserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserViewController.swift; sourceTree = ""; }; EAE08EB424CF5D09006FA3A5 /* AccountLinkingViewController.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = AccountLinkingViewController.swift; sourceTree = ""; }; @@ -138,7 +138,6 @@ EAE4CBE224855E3E00245E92 /* AuthenticationExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AuthenticationExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; EAE4CBE624855E3E00245E92 /* AuthenticationExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationExampleUITests.swift; sourceTree = ""; }; EAE4CBE824855E3E00245E92 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - EAE4CBF424857A5100245E92 /* LoginController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginController.swift; sourceTree = ""; }; EAEBCE0E2489FFDE00FCEA92 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; EAEBCE10248A9AA000FCEA92 /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = ""; }; EAFDF2BD2490439F0082B6F1 /* Animator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Animator.swift; sourceTree = ""; }; @@ -222,7 +221,6 @@ children = ( EAE08EB424CF5D09006FA3A5 /* AccountLinkingViewController.swift */, EAE4CBC824855E3A00245E92 /* AuthViewController.swift */, - EAE4CBF424857A5100245E92 /* LoginController.swift */, EAB3A17B2494628200385291 /* UserViewController.swift */, EA02F68E24A0714B0079D000 /* OtherAuthMethodControllers */, DEC2E5DC2A95331D0090260A /* SettingsViewController.swift */, @@ -245,7 +243,7 @@ isa = PBXGroup; children = ( EA20B46B2495A9F900B5E581 /* SignedOutView.swift */, - EA527CAB24A0EE9600ADB9A2 /* LoginView.swift */, + EA3348312C90EFE40091D7C2 /* LoginView.swift */, ); path = CustomViews; sourceTree = ""; @@ -547,6 +545,7 @@ buildActionMask = 2147483647; files = ( EA20B46E2495B2C700B5E581 /* DataSourceProtocols.swift in Sources */, + EA3348322C90EFF40091D7C2 /* LoginView.swift in Sources */, EAB3A1792494433500385291 /* DataSourceProvider.swift in Sources */, EA20B46C2495A9F900B5E581 /* SignedOutView.swift in Sources */, EA527CAA24A0766D00ADB9A2 /* OtherAuthViewController.swift in Sources */, @@ -557,7 +556,6 @@ EA02F68524A000E00079D000 /* UserActions.swift in Sources */, EA02F68D24A063E90079D000 /* LoginDelegate.swift in Sources */, EA20B50A249D3D8600B5E581 /* PasswordlessViewController.swift in Sources */, - EAE4CBF524857A5100245E92 /* LoginController.swift in Sources */, EA20B510249FDB4400B5E581 /* OtherAuthMethods.swift in Sources */, EA12697F29E33A5D00D79E66 /* CryptoUtils.swift in Sources */, EAEBCE11248A9AA000FCEA92 /* Section.swift in Sources */, @@ -568,7 +566,6 @@ EAFDF2BE2490439F0082B6F1 /* Animator.swift in Sources */, EAE4CBC724855E3A00245E92 /* SceneDelegate.swift in Sources */, EAE08EB524CF5D09006FA3A5 /* AccountLinkingViewController.swift in Sources */, - EA527CAC24A0EE9600ADB9A2 /* LoginView.swift in Sources */, EAEBCE0F2489FFDE00FCEA92 /* Extensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -713,7 +710,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -768,7 +765,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_LDFLAGS = "-ObjC"; @@ -789,7 +786,7 @@ CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = AuthenticationExample/SwiftApplication.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -811,8 +808,9 @@ "CODE_SIGN_IDENTITY[sdk=*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = EQHXZ8M8AV; INFOPLIST_FILE = AuthenticationExample/SwiftApplication.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -835,7 +833,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = SwiftApiTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -860,7 +858,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = SwiftApiTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/Contents.json b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/Contents.json index da4a164c918..73c00596a7f 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/Contents.json +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/firebaseLogo.imageset/Contents.json b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/firebaseLogo.imageset/Contents.json deleted file mode 100644 index 4a7c90e06a3..00000000000 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/firebaseLogo.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "logo-1024px.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/firebaseLogo.imageset/logo-1024px.png b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/firebaseLogo.imageset/logo-1024px.png deleted file mode 100644 index 1bbdb22d593..00000000000 Binary files a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/firebaseLogo.imageset/logo-1024px.png and /dev/null differ diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/CustomViews/LoginView.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/CustomViews/LoginView.swift index 86123a701ee..106c21c2a35 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/CustomViews/LoginView.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/CustomViews/LoginView.swift @@ -1,4 +1,4 @@ -// Copyright 2020 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,162 +12,187 @@ // See the License for the specific language governing permissions and // limitations under the License. -import UIKit - -/// Login View presented when performing Email & Password Login Flow -class LoginView: UIView { - var emailTextField: UITextField! { - didSet { - emailTextField.textContentType = .emailAddress +import SwiftUI + +import FirebaseAuth + +struct LoginView: View { + @Environment(\.dismiss) private var dismiss + @State private var multiFactorResolver: MultiFactorResolver? = nil + @State private var onetimePasscode = "" + @State private var showingAlert = false + + @State private var email: String = "" + @State private var password: String = "" + + var body: some View { + Group { + VStack { + Group { + HStack { + VStack { + Text("Email/Password Auth") + .font(.title) + .bold() + } + Spacer() + } + HStack { + Text( + "Login or create an account using the Email/Password auth " + + "provider.\n\nEnsure that the Email/Password provider is " + + "enabled on the Firebase console for the given project." + ) + .fixedSize(horizontal: false, vertical: true) + Spacer() + } + } + .padding(.vertical) + + Spacer() + TextField("Email", text: $email) + .textFieldStyle(SymbolTextField(symbolName: "person.crop.circle")) + TextField("Password", text: $password) + .textFieldStyle(SymbolTextField(symbolName: "lock.fill")) + Spacer() + Group { + LoginViewButton( + text: "Login", + accentColor: .white, + backgroundColor: .orange, + action: login + ) + LoginViewButton( + text: "Create Account", + accentColor: .orange, + backgroundColor: .primary, + action: createUser + ) + } + .disabled(email.isEmpty || password.isEmpty) + } + Spacer() } - } - - var passwordTextField: UITextField! { - didSet { - passwordTextField.textContentType = .password + .padding() + .alert("Enter one time passcode.", isPresented: $showingAlert) { + TextField("Verification Code", text: $onetimePasscode) + .textInputAutocapitalization(.never) + Button("Cancel", role: .cancel) {} + Button("Submit", action: submitOnetimePasscode) } } - var emailTopConstraint: NSLayoutConstraint! - var passwordTopConstraint: NSLayoutConstraint! - - lazy var loginButton: UIButton = { - let button = UIButton() - button.setTitle("Login", for: .normal) - button.setTitleColor(.white, for: .normal) - button.setTitleColor(.highlightedLabel, for: .highlighted) - button.setBackgroundImage(UIColor.systemOrange.image, for: .normal) - button.setBackgroundImage(UIColor.systemOrange.highlighted.image, for: .highlighted) - button.clipsToBounds = true - button.layer.cornerRadius = 14 - return button - }() - - lazy var createAccountButton: UIButton = { - let button = UIButton() - button.setTitle("Create Account", for: .normal) - button.setTitleColor(.secondaryLabel, for: .normal) - button.setTitleColor(UIColor.secondaryLabel.highlighted, for: .highlighted) - return button - }() - - convenience init() { - self.init(frame: .zero) - setupSubviews() - } - - // MARK: - Subviews Setup - - private func setupSubviews() { - backgroundColor = .systemBackground - clipsToBounds = true - - setupFirebaseLogoImage() - setupEmailTextfield() - setupPasswordTextField() - setupLoginButton() - setupCreateAccountButton() - } - - private func setupFirebaseLogoImage() { - let firebaseLogo = UIImage(named: "firebaseLogo") - let imageView = UIImageView(image: firebaseLogo) - imageView.contentMode = .scaleAspectFit - addSubview(imageView) - imageView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: -55), - imageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 55), - imageView.widthAnchor.constraint(equalToConstant: 325), - imageView.heightAnchor.constraint(equalToConstant: 325), - ]) + private func login() { + Task { + do { + _ = try await AppManager.shared + .auth() + .signIn(withEmail: email, password: password) + // } catch let error as AuthErrorCode + // where error.code == .secondFactorRequired { + // // error as? AuthErrorCode == nil because AuthErrorUtils returns generic + // /Errors + // // https://firebase.google.com/docs/auth/ios/totp-mfa#sign_in_users_with_a_second_factor + // // TODO(ncooke3): Fix? + } catch let error as NSError + where error.code == AuthErrorCode.secondFactorRequired.rawValue { + let mfaKey = AuthErrorUserInfoMultiFactorResolverKey + guard + let resolver = error.userInfo[mfaKey] as? MultiFactorResolver, + let multiFactorInfo = resolver.hints.first + else { return } + if multiFactorInfo.factorID == TOTPMultiFactorID { + // Show the alert to enter the TOTP verification code. + multiFactorResolver = resolver + showingAlert = true + } else { + // TODO(ncooke3): Implement handling of other MFA provider (phone). + } + } catch { + print(error.localizedDescription) + } + } } - private func setupEmailTextfield() { - emailTextField = textField(placeholder: "Email", symbolName: "person.crop.circle") - emailTextField.translatesAutoresizingMaskIntoConstraints = false - addSubview(emailTextField) - NSLayoutConstraint.activate([ - emailTextField.leadingAnchor.constraint( - equalTo: safeAreaLayoutGuide.leadingAnchor, - constant: 15 - ), - emailTextField.trailingAnchor.constraint( - equalTo: safeAreaLayoutGuide.trailingAnchor, - constant: -15 - ), - emailTextField.heightAnchor.constraint(equalToConstant: 45), - ]) - - let constant: CGFloat = UIDevice.current.orientation.isLandscape ? 15 : 50 - emailTopConstraint = emailTextField.topAnchor.constraint( - equalTo: safeAreaLayoutGuide.topAnchor, - constant: constant - ) - emailTopConstraint.isActive = true + private func createUser() { + Task { + do { + _ = try await AppManager.shared.auth().createUser( + withEmail: email, + password: password + ) + // Sign-in was successful. + dismiss() + } catch { + // TODO(ncooke3): Implement error display. + print(error.localizedDescription) + } + } } - private func setupPasswordTextField() { - passwordTextField = textField(placeholder: "Password", symbolName: "lock.fill") - passwordTextField.translatesAutoresizingMaskIntoConstraints = false - addSubview(passwordTextField) - NSLayoutConstraint.activate([ - passwordTextField.leadingAnchor.constraint( - equalTo: safeAreaLayoutGuide.leadingAnchor, - constant: 15 - ), - passwordTextField.trailingAnchor.constraint( - equalTo: safeAreaLayoutGuide.trailingAnchor, - constant: -15 - ), - passwordTextField.heightAnchor.constraint(equalToConstant: 45), - ]) - - let constant: CGFloat = UIDevice.current.orientation.isLandscape ? 5 : 20 - passwordTopConstraint = - passwordTextField.topAnchor.constraint( - equalTo: emailTextField.bottomAnchor, - constant: constant + private func submitOnetimePasscode() { + Task { + guard onetimePasscode.count > 0 else { return } + let multiFactorInfo = multiFactorResolver!.hints[0] + let assertion = TOTPMultiFactorGenerator.assertionForSignIn( + withEnrollmentID: multiFactorInfo.uid, + // TODO(ncooke3): Probably should avoid network request if empty passcode. + oneTimePassword: self.onetimePasscode ) - passwordTopConstraint.isActive = true + do { + _ = try await multiFactorResolver!.resolveSignIn(with: assertion) + // MFA login was successful. + dismiss() + } catch { + // Wrong or expired OTP. Re-prompt the user. + // TODO(ncooke3): Show error to user. + print(error) + } + } } +} - private func setupLoginButton() { - addSubview(loginButton) - loginButton.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - loginButton.leadingAnchor.constraint( - equalTo: safeAreaLayoutGuide.leadingAnchor, - constant: 15 - ), - loginButton.trailingAnchor.constraint( - equalTo: safeAreaLayoutGuide.trailingAnchor, - constant: -15 - ), - loginButton.heightAnchor.constraint(equalToConstant: 45), - loginButton.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 5), - ]) +private struct SymbolTextField: TextFieldStyle { + let symbolName: String + + func _body(configuration: TextField) -> some View { + HStack { + Image(systemName: symbolName) + .foregroundColor(.orange) + .imageScale(.large) + .padding(.leading) + configuration + .padding([.vertical, .trailing]) + } + .background(Color(uiColor: .secondarySystemBackground)) + .cornerRadius(14) + .textInputAutocapitalization(.never) } +} - private func setupCreateAccountButton() { - addSubview(createAccountButton) - createAccountButton.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - createAccountButton.centerXAnchor.constraint(equalTo: centerXAnchor), - createAccountButton.topAnchor.constraint(equalTo: loginButton.bottomAnchor, constant: 5), - ]) +// TODO(ncooke3): Use view modifiers? +private struct LoginViewButton: View { + let text: String + let accentColor: Color + let backgroundColor: Color + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack { + Spacer() + Text(text) + .bold() + .accentColor(accentColor) + Spacer() + } + .padding() + .background(backgroundColor) + .cornerRadius(14) + } } +} - // MARK: - Private Helpers - - private func textField(placeholder: String, symbolName: String) -> UITextField { - let textfield = UITextField() - textfield.backgroundColor = .secondarySystemBackground - textfield.layer.cornerRadius = 14 - textfield.placeholder = placeholder - textfield.tintColor = .systemOrange - let symbol = UIImage(systemName: symbolName) - textfield.setImage(symbol) - return textfield - } +#Preview { + LoginView() } diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Utility/Extensions.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Utility/Extensions.swift index a004534ad7d..33aab86f922 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Utility/Extensions.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Utility/Extensions.swift @@ -13,6 +13,7 @@ // limitations under the License. import FirebaseAuth +import SwiftUI import UIKit // MARK: - Extending a `Firebase User` to conform to `DataSourceProvidable` diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift index fdde2e0757d..930c059d693 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift @@ -16,6 +16,8 @@ // [START auth_import] import FirebaseCore +import SwiftUI + // For Sign in with Facebook import FBSDKLoginKit @@ -178,7 +180,7 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { phoneEnroll() case .totpEnroll: - totpEnroll() + Task { await totpEnroll() } case .multifactorUnenroll: mfaUnenroll() @@ -338,9 +340,9 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { } private func performDemoEmailPasswordLoginFlow() { - let loginController = LoginController() - loginController.delegate = self - navigationController?.pushViewController(loginController, animated: true) + let loginView = LoginView() + let hostingController = UIHostingController(rootView: loginView) + navigationController?.pushViewController(hostingController, animated: true) } private func performPasswordlessLoginFlow() { @@ -780,89 +782,53 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { } } - private func totpEnroll() { - guard let user = AppManager.shared.auth().currentUser else { - print("Error: User must be logged in first.") + private func totpEnroll() async { + guard + let user = AppManager.shared.auth().currentUser, + let accountName = user.email + else { + showAlert(for: "Enrollment failed: User must be logged and have email address.") 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.") - } + guard let issuer = AppManager.shared.auth().app?.name else { + showAlert(for: "Enrollment failed: Firebase app is missing name.") + return + } + + do { + let session = try await user.multiFactor.session() + let secret = try await TOTPMultiFactorGenerator.generateSecret(with: session) + print("Secret: " + secret.sharedSecretKey()) + + let url = secret.generateQRCodeURL(withAccountName: accountName, issuer: issuer) + guard !url.isEmpty else { + showAlert(for: "Enrollment failed") + print("Multi factor finalize enroll failed. Could not generate URL.") return } + secret.openInOTPApp(withQRCodeURL: url) - 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)") - } else { - self.showAlert(for: "Enrollment failed") - print("Error generating TOTP secret.") - } - 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 - } + guard + let oneTimePassword = await showTextInputPrompt(with: "Enter the one time passcode.") + else { + showAlert(for: "Enrollment failed: one time passcode not entered.") + return + } - let assertion = TOTPMultiFactorGenerator.assertionForEnrollment( - with: secret, - oneTimePassword: oneTimePassword - ) + 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 - } + // TODO(nickcooke): Provide option to enter display name. + try await user.multiFactor.enroll(with: assertion, displayName: "TOTP") - 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.") - } - } - } - } - } - } + showAlert(for: "Successfully enrolled: TOTP") + print("Multi factor finalize enroll succeeded.") + } catch { + print(error) + showAlert(for: "Enrollment failed: \(error.localizedDescription)") } } @@ -958,60 +924,12 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { 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) - - // 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), - ]) - - // Add actions - let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) - let submitAction = UIAlertAction(title: "Submit", style: .default) { _ in - if let completion, - let text = alertController.textFields?.first?.text { - completion(text) + private func showTextInputPrompt(with message: String) async -> String? { + await withCheckedContinuation { continuation in + showTextInputPrompt(with: message) { inputText in + continuation.resume(returning: inputText.isEmpty ? nil : inputText) } } - - 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 diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/LoginController.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/LoginController.swift deleted file mode 100644 index 1ca3c9df197..00000000000 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/LoginController.swift +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import FirebaseAuth -import UIKit - -class LoginController: UIViewController { - weak var delegate: (any LoginDelegate)? - - private var loginView: LoginView { view as! LoginView } - - private var email: String { loginView.emailTextField.text! } - private var password: String { loginView.passwordTextField.text! } - - // Hides tab bar when view controller is presented - override var hidesBottomBarWhenPushed: Bool { get { true } set {} } - - // MARK: - View Controller Lifecycle Methods - - override func loadView() { - view = LoginView() - } - - override func viewDidLoad() { - super.viewDidLoad() - configureNavigationBar() - configureDelegatesAndHandlers() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.setTitleColor(.label) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - view.endEditing(true) - navigationController?.setTitleColor(.systemOrange) - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - navigationController?.popViewController(animated: false) - } - - // Dismisses keyboard when view is tapped - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - super.touchesBegan(touches, with: event) - view.endEditing(true) - } - - // MARK: - Firebase 🔥 - - private func login(with email: String, password: String) { - AppManager.shared.auth().signIn(withEmail: email, password: password) { result, error in - guard error == nil else { return self.displayError(error) } - self.delegate?.loginDidOccur() - } - } - - private func createUser(email: String, password: String) { - AppManager.shared.auth().createUser(withEmail: email, password: password) { authResult, error in - guard error == nil else { return self.displayError(error) } - self.delegate?.loginDidOccur() - } - } - - // MARK: - Action Handlers - - @objc - private func handleLogin() { - login(with: email, password: password) - } - - @objc - private func handleCreateAccount() { - createUser(email: email, password: password) - } - - // MARK: - UI Configuration - - private func configureNavigationBar() { - navigationItem.title = "Welcome" - navigationItem.backBarButtonItem?.tintColor = .systemYellow - navigationController?.navigationBar.prefersLargeTitles = true - } - - private func configureDelegatesAndHandlers() { - loginView.emailTextField.delegate = self - loginView.passwordTextField.delegate = self - loginView.loginButton.addTarget(self, action: #selector(handleLogin), for: .touchUpInside) - loginView.createAccountButton.addTarget( - self, - action: #selector(handleCreateAccount), - for: .touchUpInside - ) - } - - override func viewWillTransition(to size: CGSize, - with coordinator: any UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - loginView.emailTopConstraint.constant = UIDevice.current.orientation.isLandscape ? 15 : 50 - loginView.passwordTopConstraint.constant = UIDevice.current.orientation.isLandscape ? 5 : 20 - } -} - -// MARK: - UITextFieldDelegate - -extension LoginController: UITextFieldDelegate { - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if loginView.emailTextField.isFirstResponder, loginView.passwordTextField.text!.isEmpty { - loginView.passwordTextField.becomeFirstResponder() - } else { - textField.resignFirstResponder() - } - return true - } -}