diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Utils/StringUtils.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Utils/StringUtils.swift index 1dbdc49a48..1f84a883ce 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Utils/StringUtils.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Utils/StringUtils.swift @@ -350,4 +350,11 @@ public class StringUtils { public var privacyPolicyLabel: String { return localizedString(for: "PrivacyPolicy") } + + /// Alert Error title + /// found in: + /// PasswordRecoveryView + public var alertErrorTitle: String { + return localizedString(for: "Error") + } } diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift index cb753567fb..e4c85834ea 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift @@ -11,50 +11,65 @@ public struct AuthPickerView { authService.authenticationFlow = authService .authenticationFlow == .login ? .signUp : .login } -} -extension AuthPickerView: View { - public var body: some View { - VStack { + @ViewBuilder + private var authPickerTitleView: some View { + if authService.authView == .authPicker { Text(authService.string.authPickerTitle) .font(.largeTitle) .fontWeight(.bold) .padding() - if authService.authenticationState == .authenticated { - SignedInView() - } else if authService.authView == .passwordRecovery { - PasswordRecoveryView() - } else if authService.authView == .emailLink { - EmailLinkView() - } else { - if authService.emailSignInEnabled { - Text(authService.authenticationFlow == .login ? authService.string - .emailLoginFlowLabel : authService.string.emailSignUpFlowLabel) - Divider() - EmailAuthView() - } - VStack { - authService.renderButtons() - }.padding(.horizontal) - if authService.emailSignInEnabled { - Divider() - HStack { - Text(authService - .authenticationFlow == .login ? authService.string.dontHaveAnAccountYetLabel : - authService.string.alreadyHaveAnAccountLabel) - Button(action: { - withAnimation { - switchFlow() - } - }) { - Text(authService.authenticationFlow == .signUp ? authService.string + } + } +} + +extension AuthPickerView: View { + public var body: some View { + ScrollView { + VStack { + authPickerTitleView + if authService.authenticationState == .authenticated { + SignedInView() + } else { + switch authService.authView { + case .passwordRecovery: + PasswordRecoveryView() + case .emailLink: + EmailLinkView() + case .authPicker: + if authService.emailSignInEnabled { + Text(authService.authenticationFlow == .login ? authService.string .emailLoginFlowLabel : authService.string.emailSignUpFlowLabel) - .fontWeight(.semibold) - .foregroundColor(.blue) + Divider() + EmailAuthView() + } + VStack { + authService.renderButtons() + }.padding(.horizontal) + if authService.emailSignInEnabled { + Divider() + HStack { + Text(authService + .authenticationFlow == .login ? authService.string.dontHaveAnAccountYetLabel : + authService.string.alreadyHaveAnAccountLabel) + Button(action: { + withAnimation { + switchFlow() + } + }) { + Text(authService.authenticationFlow == .signUp ? authService.string + .emailLoginFlowLabel : authService.string.emailSignUpFlowLabel) + .fontWeight(.semibold) + .foregroundColor(.blue) + } + } } + PrivacyTOCsView(displayMode: .footer) + Text(authService.errorMessage).foregroundColor(.red) + default: + // TODO: - possibly refactor this, see: https://github.com/firebase/FirebaseUI-iOS/pull/1259#discussion_r2105473437 + EmptyView() } - PrivacyTOCsView(displayMode: .footer) - Text(authService.errorMessage).foregroundColor(.red) } } } diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordPromptView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordPromptView.swift index c306d2f433..7d9d2bc813 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordPromptView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordPromptView.swift @@ -1,3 +1,4 @@ +import FirebaseCore import SwiftUI struct PasswordPromptSheet { @@ -9,26 +10,45 @@ struct PasswordPromptSheet { extension PasswordPromptSheet: View { var body: some View { VStack(spacing: 20) { - SecureField(authService.string.passwordInputLabel, text: $password) - .textFieldStyle(.roundedBorder) + Text(authService.string.confirmPasswordInputLabel) + .font(.largeTitle) + .fontWeight(.bold) .padding() - HStack { - Button(authService.string.cancelButtonLabel) { - coordinator.cancel() - } - Spacer() - Button(authService.string.okButtonLabel) { - coordinator.submit(password: password) - } - .disabled(password.isEmpty) + Divider() + + LabeledContent { + TextField(authService.string.passwordInputLabel, text: $password) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .submitLabel(.next) + } label: { + Image(systemName: "lock") + }.padding(.vertical, 10) + .background(Divider(), alignment: .bottom) + .padding(.bottom, 4) + + Button(action: { + coordinator.submit(password: password) + }) { + Text(authService.string.okButtonLabel) + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + } + .disabled(password.isEmpty) + .padding([.top, .bottom, .horizontal], 8) + .frame(maxWidth: .infinity) + .buttonStyle(.borderedProminent) + + Button(authService.string.cancelButtonLabel) { + coordinator.cancel() } - .padding(.horizontal) } .padding() } } #Preview { - PasswordPromptSheet(coordinator: PasswordPromptCoordinator()) + FirebaseOptions.dummyConfigurationForPreview() + return PasswordPromptSheet(coordinator: PasswordPromptCoordinator()).environment(AuthService()) } diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift index c29e157ee6..129270bcc6 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift @@ -1,18 +1,27 @@ import FirebaseCore import SwiftUI +private struct ResultWrapper: Identifiable { + let id = UUID() + let value: Result +} + public struct PasswordRecoveryView { @Environment(AuthService.self) private var authService @State private var email = "" - @State private var showModal = false + @State private var resultWrapper: ResultWrapper? public init() {} private func sendPasswordRecoveryEmail() async { + let recoveryResult: Result + do { try await authService.sendPasswordRecoveryEmail(to: email) - showModal = true - } catch {} + resultWrapper = ResultWrapper(value: .success(())) + } catch { + resultWrapper = ResultWrapper(value: .failure(error)) + } } } @@ -33,9 +42,11 @@ extension PasswordRecoveryView: View { .submitLabel(.next) } label: { Image(systemName: "at") - }.padding(.vertical, 6) - .background(Divider(), alignment: .bottom) - .padding(.bottom, 4) + } + .padding(.vertical, 6) + .background(Divider(), alignment: .bottom) + .padding(.bottom, 4) + Button(action: { Task { await sendPasswordRecoveryEmail() @@ -46,11 +57,29 @@ extension PasswordRecoveryView: View { .frame(maxWidth: .infinity) } .disabled(!CommonUtils.isValidEmail(email)) - .padding([.top, .bottom], 8) + .padding([.top, .bottom, .horizontal], 8) .frame(maxWidth: .infinity) .buttonStyle(.borderedProminent) - }.sheet(isPresented: $showModal) { - VStack { + } + .sheet(item: $resultWrapper) { wrapper in + resultSheet(wrapper.value) + } + .navigationBarItems(leading: Button(action: { + authService.authView = .authPicker + }) { + Image(systemName: "chevron.left") + .foregroundColor(.blue) + Text(authService.string.backButtonLabel) + .foregroundColor(.blue) + }) + } + + @ViewBuilder + @MainActor + private func resultSheet(_ result: Result) -> some View { + VStack { + switch result { + case .success: Text(authService.string.passwordRecoveryEmailSentTitle) .font(.largeTitle) .fontWeight(.bold) @@ -60,25 +89,29 @@ extension PasswordRecoveryView: View { Divider() - Text(authService.string.passwordRecoveryEmailSentMessage) + Text(String(format: authService.string.passwordRecoveryEmailSentMessage, email)) + .padding() + + case .failure: + Text(authService.string.alertErrorTitle) + .font(.title) + .fontWeight(.semibold) + .padding() + + Divider() + + Text(authService.errorMessage) .padding() - Button(authService.string.okButtonLabel) { - showModal = false - } - .padding() + } + + Divider() + + Button(authService.string.okButtonLabel) { + self.resultWrapper = nil } .padding() - }.onOpenURL { _ in - // move the user to email/password View } - .navigationBarItems(leading: Button(action: { - authService.authView = .authPicker - }) { - Image(systemName: "chevron.left") - .foregroundColor(.blue) - Text(authService.string.backButtonLabel) - .foregroundColor(.blue) - }) + .padding() } }