Skip to content

Commit ef70a9d

Browse files
Merge pull request #1259 from firebase/password-recovery-password-prompt-ui
2 parents 0a5962f + 03aeb61 commit ef70a9d

File tree

4 files changed

+148
-73
lines changed

4 files changed

+148
-73
lines changed

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Utils/StringUtils.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,4 +350,11 @@ public class StringUtils {
350350
public var privacyPolicyLabel: String {
351351
return localizedString(for: "PrivacyPolicy")
352352
}
353+
354+
/// Alert Error title
355+
/// found in:
356+
/// PasswordRecoveryView
357+
public var alertErrorTitle: String {
358+
return localizedString(for: "Error")
359+
}
353360
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift

Lines changed: 51 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,50 +11,65 @@ public struct AuthPickerView {
1111
authService.authenticationFlow = authService
1212
.authenticationFlow == .login ? .signUp : .login
1313
}
14-
}
1514

16-
extension AuthPickerView: View {
17-
public var body: some View {
18-
VStack {
15+
@ViewBuilder
16+
private var authPickerTitleView: some View {
17+
if authService.authView == .authPicker {
1918
Text(authService.string.authPickerTitle)
2019
.font(.largeTitle)
2120
.fontWeight(.bold)
2221
.padding()
23-
if authService.authenticationState == .authenticated {
24-
SignedInView()
25-
} else if authService.authView == .passwordRecovery {
26-
PasswordRecoveryView()
27-
} else if authService.authView == .emailLink {
28-
EmailLinkView()
29-
} else {
30-
if authService.emailSignInEnabled {
31-
Text(authService.authenticationFlow == .login ? authService.string
32-
.emailLoginFlowLabel : authService.string.emailSignUpFlowLabel)
33-
Divider()
34-
EmailAuthView()
35-
}
36-
VStack {
37-
authService.renderButtons()
38-
}.padding(.horizontal)
39-
if authService.emailSignInEnabled {
40-
Divider()
41-
HStack {
42-
Text(authService
43-
.authenticationFlow == .login ? authService.string.dontHaveAnAccountYetLabel :
44-
authService.string.alreadyHaveAnAccountLabel)
45-
Button(action: {
46-
withAnimation {
47-
switchFlow()
48-
}
49-
}) {
50-
Text(authService.authenticationFlow == .signUp ? authService.string
22+
}
23+
}
24+
}
25+
26+
extension AuthPickerView: View {
27+
public var body: some View {
28+
ScrollView {
29+
VStack {
30+
authPickerTitleView
31+
if authService.authenticationState == .authenticated {
32+
SignedInView()
33+
} else {
34+
switch authService.authView {
35+
case .passwordRecovery:
36+
PasswordRecoveryView()
37+
case .emailLink:
38+
EmailLinkView()
39+
case .authPicker:
40+
if authService.emailSignInEnabled {
41+
Text(authService.authenticationFlow == .login ? authService.string
5142
.emailLoginFlowLabel : authService.string.emailSignUpFlowLabel)
52-
.fontWeight(.semibold)
53-
.foregroundColor(.blue)
43+
Divider()
44+
EmailAuthView()
45+
}
46+
VStack {
47+
authService.renderButtons()
48+
}.padding(.horizontal)
49+
if authService.emailSignInEnabled {
50+
Divider()
51+
HStack {
52+
Text(authService
53+
.authenticationFlow == .login ? authService.string.dontHaveAnAccountYetLabel :
54+
authService.string.alreadyHaveAnAccountLabel)
55+
Button(action: {
56+
withAnimation {
57+
switchFlow()
58+
}
59+
}) {
60+
Text(authService.authenticationFlow == .signUp ? authService.string
61+
.emailLoginFlowLabel : authService.string.emailSignUpFlowLabel)
62+
.fontWeight(.semibold)
63+
.foregroundColor(.blue)
64+
}
65+
}
5466
}
67+
PrivacyTOCsView(displayMode: .footer)
68+
Text(authService.errorMessage).foregroundColor(.red)
69+
default:
70+
// TODO: - possibly refactor this, see: https://github.com/firebase/FirebaseUI-iOS/pull/1259#discussion_r2105473437
71+
EmptyView()
5572
}
56-
PrivacyTOCsView(displayMode: .footer)
57-
Text(authService.errorMessage).foregroundColor(.red)
5873
}
5974
}
6075
}
Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import FirebaseCore
12
import SwiftUI
23

34
struct PasswordPromptSheet {
@@ -9,26 +10,45 @@ struct PasswordPromptSheet {
910
extension PasswordPromptSheet: View {
1011
var body: some View {
1112
VStack(spacing: 20) {
12-
SecureField(authService.string.passwordInputLabel, text: $password)
13-
.textFieldStyle(.roundedBorder)
13+
Text(authService.string.confirmPasswordInputLabel)
14+
.font(.largeTitle)
15+
.fontWeight(.bold)
1416
.padding()
1517

16-
HStack {
17-
Button(authService.string.cancelButtonLabel) {
18-
coordinator.cancel()
19-
}
20-
Spacer()
21-
Button(authService.string.okButtonLabel) {
22-
coordinator.submit(password: password)
23-
}
24-
.disabled(password.isEmpty)
18+
Divider()
19+
20+
LabeledContent {
21+
TextField(authService.string.passwordInputLabel, text: $password)
22+
.textInputAutocapitalization(.never)
23+
.disableAutocorrection(true)
24+
.submitLabel(.next)
25+
} label: {
26+
Image(systemName: "lock")
27+
}.padding(.vertical, 10)
28+
.background(Divider(), alignment: .bottom)
29+
.padding(.bottom, 4)
30+
31+
Button(action: {
32+
coordinator.submit(password: password)
33+
}) {
34+
Text(authService.string.okButtonLabel)
35+
.padding(.vertical, 8)
36+
.frame(maxWidth: .infinity)
37+
}
38+
.disabled(password.isEmpty)
39+
.padding([.top, .bottom, .horizontal], 8)
40+
.frame(maxWidth: .infinity)
41+
.buttonStyle(.borderedProminent)
42+
43+
Button(authService.string.cancelButtonLabel) {
44+
coordinator.cancel()
2545
}
26-
.padding(.horizontal)
2746
}
2847
.padding()
2948
}
3049
}
3150

3251
#Preview {
33-
PasswordPromptSheet(coordinator: PasswordPromptCoordinator())
52+
FirebaseOptions.dummyConfigurationForPreview()
53+
return PasswordPromptSheet(coordinator: PasswordPromptCoordinator()).environment(AuthService())
3454
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
11
import FirebaseCore
22
import SwiftUI
33

4+
private struct ResultWrapper: Identifiable {
5+
let id = UUID()
6+
let value: Result<Void, Error>
7+
}
8+
49
public struct PasswordRecoveryView {
510
@Environment(AuthService.self) private var authService
611
@State private var email = ""
7-
@State private var showModal = false
12+
@State private var resultWrapper: ResultWrapper?
813

914
public init() {}
1015

1116
private func sendPasswordRecoveryEmail() async {
17+
let recoveryResult: Result<Void, Error>
18+
1219
do {
1320
try await authService.sendPasswordRecoveryEmail(to: email)
14-
showModal = true
15-
} catch {}
21+
resultWrapper = ResultWrapper(value: .success(()))
22+
} catch {
23+
resultWrapper = ResultWrapper(value: .failure(error))
24+
}
1625
}
1726
}
1827

@@ -33,9 +42,11 @@ extension PasswordRecoveryView: View {
3342
.submitLabel(.next)
3443
} label: {
3544
Image(systemName: "at")
36-
}.padding(.vertical, 6)
37-
.background(Divider(), alignment: .bottom)
38-
.padding(.bottom, 4)
45+
}
46+
.padding(.vertical, 6)
47+
.background(Divider(), alignment: .bottom)
48+
.padding(.bottom, 4)
49+
3950
Button(action: {
4051
Task {
4152
await sendPasswordRecoveryEmail()
@@ -46,11 +57,29 @@ extension PasswordRecoveryView: View {
4657
.frame(maxWidth: .infinity)
4758
}
4859
.disabled(!CommonUtils.isValidEmail(email))
49-
.padding([.top, .bottom], 8)
60+
.padding([.top, .bottom, .horizontal], 8)
5061
.frame(maxWidth: .infinity)
5162
.buttonStyle(.borderedProminent)
52-
}.sheet(isPresented: $showModal) {
53-
VStack {
63+
}
64+
.sheet(item: $resultWrapper) { wrapper in
65+
resultSheet(wrapper.value)
66+
}
67+
.navigationBarItems(leading: Button(action: {
68+
authService.authView = .authPicker
69+
}) {
70+
Image(systemName: "chevron.left")
71+
.foregroundColor(.blue)
72+
Text(authService.string.backButtonLabel)
73+
.foregroundColor(.blue)
74+
})
75+
}
76+
77+
@ViewBuilder
78+
@MainActor
79+
private func resultSheet(_ result: Result<Void, Error>) -> some View {
80+
VStack {
81+
switch result {
82+
case .success:
5483
Text(authService.string.passwordRecoveryEmailSentTitle)
5584
.font(.largeTitle)
5685
.fontWeight(.bold)
@@ -60,25 +89,29 @@ extension PasswordRecoveryView: View {
6089

6190
Divider()
6291

63-
Text(authService.string.passwordRecoveryEmailSentMessage)
92+
Text(String(format: authService.string.passwordRecoveryEmailSentMessage, email))
93+
.padding()
94+
95+
case .failure:
96+
Text(authService.string.alertErrorTitle)
97+
.font(.title)
98+
.fontWeight(.semibold)
99+
.padding()
100+
101+
Divider()
102+
103+
Text(authService.errorMessage)
64104
.padding()
65-
Button(authService.string.okButtonLabel) {
66-
showModal = false
67-
}
68-
.padding()
105+
}
106+
107+
Divider()
108+
109+
Button(authService.string.okButtonLabel) {
110+
self.resultWrapper = nil
69111
}
70112
.padding()
71-
}.onOpenURL { _ in
72-
// move the user to email/password View
73113
}
74-
.navigationBarItems(leading: Button(action: {
75-
authService.authView = .authPicker
76-
}) {
77-
Image(systemName: "chevron.left")
78-
.foregroundColor(.blue)
79-
Text(authService.string.backButtonLabel)
80-
.foregroundColor(.blue)
81-
})
114+
.padding()
82115
}
83116
}
84117

0 commit comments

Comments
 (0)