LoginView.swift 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. // Copyright 2024 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. import SwiftUI
  15. import FirebaseAuth
  16. struct LoginView: View {
  17. @Environment(\.dismiss) private var dismiss
  18. @State private var email: String = ""
  19. @State private var password: String = ""
  20. // Properties for displaying error alerts.
  21. @State private var showingAlert: Bool = false
  22. @State private var error: Error?
  23. private weak var delegate: (any LoginDelegate)?
  24. init(delegate: (any LoginDelegate)? = nil) {
  25. self.delegate = delegate
  26. }
  27. private func login() {
  28. Task {
  29. do {
  30. _ = try await AppManager.shared
  31. .auth()
  32. .signIn(withEmail: email, password: password)
  33. await MainActor.run {
  34. dismiss()
  35. delegate?.loginDidOccur(resolver: nil)
  36. }
  37. // TODO(ncooke3): Investigate possible improvements.
  38. // } catch let error as AuthErrorCode
  39. // where error.code == .secondFactorRequired {
  40. // // error as? AuthErrorCode == nil because AuthErrorUtils returns generic
  41. // /Errors
  42. // // https://firebase.google.com/docs/auth/ios/totp-mfa#sign_in_users_with_a_second_factor
  43. } catch {
  44. let error = error as NSError
  45. if error.code == AuthErrorCode.secondFactorRequired.rawValue {
  46. let mfaKey = AuthErrorUserInfoMultiFactorResolverKey
  47. if let resolver = error.userInfo[mfaKey] as? MultiFactorResolver {
  48. // Multi-factor auth is required is to complete sign-in.
  49. await MainActor.run {
  50. dismiss()
  51. delegate?.loginDidOccur(resolver: resolver)
  52. }
  53. }
  54. }
  55. print(error.localizedDescription)
  56. self.error = error
  57. self.showingAlert.toggle()
  58. }
  59. }
  60. }
  61. private func createUser() {
  62. Task {
  63. do {
  64. _ = try await AppManager.shared.auth().createUser(
  65. withEmail: email,
  66. password: password
  67. )
  68. // Sign-in was successful.
  69. await MainActor.run {
  70. dismiss()
  71. delegate?.loginDidOccur(resolver: nil)
  72. }
  73. } catch {
  74. print(error.localizedDescription)
  75. self.error = error
  76. self.showingAlert.toggle()
  77. }
  78. }
  79. }
  80. }
  81. extension LoginView {
  82. var body: some View {
  83. VStack(alignment: .leading) {
  84. Text(
  85. "Login or create an account using the Email/Password auth " +
  86. "provider.\n\nEnsure that the Email/Password provider is " +
  87. "enabled on the Firebase console for the given project."
  88. )
  89. .fixedSize(horizontal: false, vertical: true)
  90. .padding(.bottom)
  91. TextField("Email", text: $email)
  92. .textFieldStyle(SymbolTextFieldStyle(symbolName: "person.crop.circle"))
  93. TextField("Password", text: $password)
  94. .textFieldStyle(SymbolTextFieldStyle(symbolName: "lock.fill"))
  95. .padding(.bottom)
  96. Group {
  97. Button(action: login) {
  98. Text("Login")
  99. .bold()
  100. }
  101. .buttonStyle(CustomButtonStyle(backgroundColor: .orange, foregroundColor: .white))
  102. Button(action: createUser) {
  103. Text("Create Account")
  104. .bold()
  105. }
  106. .buttonStyle(CustomButtonStyle(backgroundColor: .primary, foregroundColor: .orange))
  107. }
  108. .disabled(email.isEmpty || password.isEmpty)
  109. Spacer()
  110. }
  111. .padding()
  112. .alert("Error", isPresented: $showingAlert) {
  113. if let error {
  114. Text(error.localizedDescription)
  115. }
  116. Button("OK", role: .cancel) {
  117. showingAlert.toggle()
  118. }
  119. }
  120. }
  121. }
  122. private struct SymbolTextFieldStyle: TextFieldStyle {
  123. let symbolName: String
  124. func _body(configuration: TextField<Self._Label>) -> some View {
  125. HStack {
  126. Image(systemName: symbolName)
  127. .foregroundColor(.orange)
  128. .imageScale(.large)
  129. .padding(.leading)
  130. configuration
  131. .padding([.vertical, .trailing])
  132. }
  133. .background(Color(uiColor: .secondarySystemBackground))
  134. .cornerRadius(14)
  135. .textInputAutocapitalization(.never)
  136. }
  137. }
  138. private struct CustomButtonStyle: ButtonStyle {
  139. let backgroundColor: Color
  140. let foregroundColor: Color
  141. func makeBody(configuration: Configuration) -> some View {
  142. HStack {
  143. Spacer()
  144. configuration.label
  145. Spacer()
  146. }
  147. .padding()
  148. .background(backgroundColor, in: RoundedRectangle(cornerRadius: 14))
  149. .foregroundStyle(foregroundColor)
  150. .opacity(configuration.isPressed ? 0.5 : 1)
  151. }
  152. }
  153. #Preview {
  154. LoginView()
  155. }