Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used existing "Error" title for alerts/modal.

return localizedString(for: "Error")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,46 +15,50 @@ public struct AuthPickerView {

extension AuthPickerView: View {
public var body: some View {
VStack {
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()
ScrollView {
VStack {
if authService.authView == .authPicker {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the "Welcome" title only appear when on authPicker view, other wise it appeared in the others, e.g. PasswordRecoveryView.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you intend on having a title for the forgot password view later on, you may want to abstract this into an @ViewBuilder header property or something.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made it a computed property: b4ea600 (#1259)

Text(authService.string.authPickerTitle)
.font(.largeTitle)
.fontWeight(.bold)
.padding()
}
VStack {
authService.renderButtons()
}.padding(.horizontal)
if authService.emailSignInEnabled {
Divider()
HStack {
Text(authService
.authenticationFlow == .login ? authService.string.dontHaveAnAccountYetLabel :
authService.string.alreadyHaveAnAccountLabel)
Button(action: {
withAnimation {
switchFlow()
if authService.authenticationState == .authenticated {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could do this slightly more elegantly with a

switch authService.authView {
  case .passwordRecovery:
    // ...
  case .emailLink:
    // ...
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using switch statement: c013f34 (#1259)

The only thing I don't like is including the default case at the end to satisfy compiler as we're not using updatePassword View in AuthPickerView.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can put break in the switch instead of EmptyView

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2025-05-28 at 09 13 23

Doesn't seem possible. I can leave a TODO to refactor this.

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
.emailLoginFlowLabel : authService.string.emailSignUpFlowLabel)
.fontWeight(.semibold)
.foregroundColor(.blue)
}
}) {
Text(authService.authenticationFlow == .signUp ? authService.string
.emailLoginFlowLabel : authService.string.emailSignUpFlowLabel)
.fontWeight(.semibold)
.foregroundColor(.blue)
}
PrivacyTOCsView(displayMode: .footer)
Text(authService.errorMessage).foregroundColor(.red)
}
PrivacyTOCsView(displayMode: .footer)
Text(authService.errorMessage).foregroundColor(.red)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import FirebaseCore
import SwiftUI

struct PasswordPromptSheet {
Expand All @@ -9,26 +10,47 @@ struct PasswordPromptSheet {
extension PasswordPromptSheet: View {
var body: some View {
VStack(spacing: 20) {
SecureField(authService.string.passwordInputLabel, text: $password)
.textFieldStyle(.roundedBorder)
Text(authService.string.confirmPasswordInputLabel)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improved the UI for PasswordPromptSheet:

.font(.largeTitle)
.fontWeight(.bold)
.padding()

HStack {
Button(authService.string.cancelButtonLabel) {
coordinator.cancel()
}
Spacer()
Button(authService.string.okButtonLabel) {
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: {
Task {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this need to be wrapped in a Task?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, removed: b738cf5 (#1259)

coordinator.submit(password: password)
}
.disabled(password.isEmpty)
}) {
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())
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
import FirebaseCore
import SwiftUI

enum PasswordRecoveryResult: Identifiable {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improved logic for password recovery so modal pops up telling user to check email for recovery link, or for failure, modal pops up with error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of creating a custom type, you can initialize a Swift standard library Result from the authService.sendPasswordRecoveryEmail() function, use it in the switch statements below, and then also omit the do...catch in sendPasswordRecoveryEmail().

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used Result but still need an Identifiable so that we could control the sheet: 03ebaf2 (#1259)

Also - I still needed to use do/catch as Result expected a synchronous closure and the operation is async

case success
case failure

var id: String {
switch self {
case .success: return "success"
case .failure: return "failure"
}
}
}

public struct PasswordRecoveryView {
@Environment(AuthService.self) private var authService
@State private var email = ""
@State private var showModal = false
@State private var result: PasswordRecoveryResult?

public init() {}

private func sendPasswordRecoveryEmail() async {
do {
try await authService.sendPasswordRecoveryEmail(to: email)
showModal = true
} catch {}
result = .success
} catch {
result = .failure
}
}
}

Expand Down Expand Up @@ -46,30 +60,48 @@ 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) {
}.sheet(item: $result) { result in
VStack {
Text(authService.string.passwordRecoveryEmailSentTitle)
.font(.largeTitle)
.fontWeight(.bold)
.padding()
Text(authService.string.passwordRecoveryHelperMessage)
switch result {
case .success:
Text(authService.string.passwordRecoveryEmailSentTitle)
.font(.largeTitle)
.fontWeight(.bold)
.padding()
Text(authService.string.passwordRecoveryHelperMessage)
.padding()

Divider()

Text(authService.string.passwordRecoveryEmailSentMessage)
.padding()
Button(authService.string.okButtonLabel) {
self.result = nil
}
.padding()
case .failure:
Text(authService.string.alertErrorTitle)
.font(.title)
.fontWeight(.semibold)
.padding()

Divider()
Divider()

Text(authService.string.passwordRecoveryEmailSentMessage)
Text(authService.errorMessage)
.padding()

Divider()

Button(authService.string.okButtonLabel) {
self.result = nil
}
.padding()
Button(authService.string.okButtonLabel) {
showModal = false
}
.padding()
}
.padding()
}.onOpenURL { _ in
// move the user to email/password View
}
.navigationBarItems(leading: Button(action: {
authService.authView = .authPicker
Expand Down