Skip to content

Commit 2504703

Browse files
refactor: reuse coordinator for email/phone
1 parent 7511d9c commit 2504703

File tree

7 files changed

+128
-145
lines changed

7 files changed

+128
-145
lines changed

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService+Email.swift

Lines changed: 0 additions & 49 deletions
This file was deleted.

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,6 @@ public final class AuthService {
138138

139139
private var listenerManager: AuthListenerManager?
140140

141-
private var emailProvider: EmailProviderSwift?
142-
143-
public var passwordPrompt: PasswordPromptCoordinator {
144-
emailProvider?.passwordPrompt ?? PasswordPromptCoordinator()
145-
}
146-
147141
var emailSignInEnabled = false
148142
private var emailSignInCallback: (() -> Void)?
149143

@@ -315,16 +309,14 @@ public extension AuthService {
315309

316310
public extension AuthService {
317311
/// Enable email sign-in with default behavior (navigates to email link view)
318-
func withEmailSignIn(_ provider: EmailProviderSwift? = nil) -> AuthService {
319-
return withEmailSignIn(provider) { [weak self] in
312+
func withEmailSignIn() -> AuthService {
313+
return withEmailSignIn { [weak self] in
320314
self?.navigator.push(.emailLink)
321315
}
322316
}
323317

324318
/// Enable email sign-in with custom callback
325-
func withEmailSignIn(_ provider: EmailProviderSwift? = nil,
326-
onTap: @escaping () -> Void) -> AuthService {
327-
emailProvider = provider ?? EmailProviderSwift()
319+
func withEmailSignIn(onTap: @escaping () -> Void) -> AuthService {
328320
emailSignInEnabled = true
329321
emailSignInCallback = onTap
330322
return self

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/EmailProviderAuthUI.swift

Lines changed: 0 additions & 33 deletions
This file was deleted.

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/ReauthenticationCoordinator.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public final class ReauthenticationCoordinator {
2323
public var reauthContext: ReauthContext?
2424
public var showingPhoneReauth = false
2525
public var showingPhoneReauthAlert = false
26+
public var showingEmailPasswordPrompt = false
2627

2728
private var continuation: CheckedContinuation<Void, Error>?
2829

@@ -34,10 +35,14 @@ public final class ReauthenticationCoordinator {
3435
self.continuation = continuation
3536
self.reauthContext = context
3637

37-
// Show alert first for all providers (including phone)
38-
if context.providerId == PhoneAuthProviderID {
38+
// Route to appropriate flow based on provider
39+
switch context.providerId {
40+
case PhoneAuthProviderID:
3941
self.showingPhoneReauthAlert = true
40-
} else {
42+
case EmailAuthProviderID:
43+
self.showingEmailPasswordPrompt = true
44+
default:
45+
// For simple providers (Google, Apple, etc.)
4146
self.isReauthenticating = true
4247
}
4348
}
@@ -66,6 +71,7 @@ public final class ReauthenticationCoordinator {
6671
isReauthenticating = false
6772
showingPhoneReauth = false
6873
showingPhoneReauthAlert = false
74+
showingEmailPasswordPrompt = false
6975
reauthContext = nil
7076
}
7177
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ extension AuthPickerView: View {
3636
content()
3737
.sheet(isPresented: $authService.isPresented) {
3838
@Bindable var navigator = authService.navigator
39-
@Bindable var passwordPrompt = authService.passwordPrompt
4039
NavigationStack(path: $navigator.routes) {
4140
authPickerViewInternal
4241
.navigationTitle(authService.authenticationState == .unauthenticated ? authService
@@ -79,10 +78,6 @@ extension AuthPickerView: View {
7978
.accountConflictHandler()
8079
// Apply MFA handling at NavigationStack level
8180
.mfaHandler()
82-
// Centralized password prompt sheet inside auth flow
83-
.sheet(isPresented: $passwordPrompt.isPromptingPassword) {
84-
PasswordPromptSheet(coordinator: passwordPrompt)
85-
}
8681
}
8782
}
8883

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordPromptView.swift

Lines changed: 105 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -12,64 +12,127 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
import FirebaseAuth
1516
import FirebaseAuthUIComponents
1617
import FirebaseCore
1718
import SwiftUI
1819

19-
struct PasswordPromptSheet {
20+
@MainActor
21+
public struct EmailReauthView {
2022
@Environment(AuthService.self) private var authService
21-
@Bindable var coordinator: PasswordPromptCoordinator
23+
@Environment(\.reportError) private var reportError
24+
25+
let email: String
26+
let coordinator: ReauthenticationCoordinator
27+
2228
@State private var password = ""
29+
@State private var isLoading = false
30+
@State private var error: AlertError?
31+
32+
private func verifyPassword() {
33+
guard !password.isEmpty else { return }
34+
35+
Task { @MainActor in
36+
isLoading = true
37+
do {
38+
let credential = EmailAuthProvider.credential(withEmail: email, password: password)
39+
try await authService.reauthenticate(with: credential)
40+
coordinator.reauthCompleted()
41+
isLoading = false
42+
} catch {
43+
if let reportError = reportError {
44+
reportError(error)
45+
} else {
46+
self.error = AlertError(
47+
title: "Error",
48+
message: error.localizedDescription,
49+
underlyingError: error
50+
)
51+
}
52+
isLoading = false
53+
}
54+
}
55+
}
2356
}
2457

25-
extension PasswordPromptSheet: View {
26-
var body: some View {
27-
VStack(spacing: 20) {
28-
Text(authService.string.confirmPasswordInputLabel)
29-
.font(.largeTitle)
30-
.fontWeight(.bold)
58+
extension EmailReauthView: View {
59+
public var body: some View {
60+
NavigationStack {
61+
VStack(spacing: 24) {
62+
// Header
63+
VStack(spacing: 12) {
64+
Image(systemName: "lock.circle.fill")
65+
.font(.system(size: 60))
66+
.foregroundColor(.blue)
67+
68+
Text("Confirm Password")
69+
.font(.title)
70+
.fontWeight(.bold)
71+
72+
Text("For security, please enter your password")
73+
.font(.body)
74+
.foregroundColor(.secondary)
75+
.multilineTextAlignment(.center)
76+
}
3177
.padding()
32-
33-
Divider()
34-
35-
AuthTextField(
36-
text: $password,
37-
label: authService.string.passwordFieldLabel,
38-
prompt: authService.string.passwordInputLabel,
39-
contentType: .password,
40-
isSecureTextField: true,
41-
onSubmit: { _ in
42-
if !password.isEmpty {
43-
coordinator.submit(password: password)
78+
79+
VStack(spacing: 20) {
80+
Text("Email: \(email)")
81+
.font(.caption)
82+
.frame(maxWidth: .infinity, alignment: .leading)
83+
.padding(.bottom, 8)
84+
85+
AuthTextField(
86+
text: $password,
87+
label: authService.string.passwordFieldLabel,
88+
prompt: authService.string.passwordInputLabel,
89+
contentType: .password,
90+
isSecureTextField: true,
91+
onSubmit: { _ in
92+
verifyPassword()
93+
},
94+
leading: {
95+
Image(systemName: "lock")
96+
}
97+
)
98+
.submitLabel(.done)
99+
.accessibilityIdentifier("email-reauth-password-field")
100+
101+
Button(action: verifyPassword) {
102+
if isLoading {
103+
ProgressView()
104+
.frame(height: 32)
105+
.frame(maxWidth: .infinity)
106+
} else {
107+
Text("Confirm")
108+
.frame(height: 32)
109+
.frame(maxWidth: .infinity)
110+
}
111+
}
112+
.buttonStyle(.borderedProminent)
113+
.disabled(password.isEmpty || isLoading)
114+
.accessibilityIdentifier("confirm-password-button")
115+
116+
Button(authService.string.cancelButtonLabel) {
117+
coordinator.reauthCancelled()
44118
}
45-
},
46-
leading: {
47-
Image(systemName: "lock")
48119
}
49-
)
50-
.submitLabel(.next)
51-
52-
Button(action: {
53-
coordinator.submit(password: password)
54-
}) {
55-
Text(authService.string.okButtonLabel)
56-
.padding(.vertical, 8)
57-
.frame(maxWidth: .infinity)
58-
}
59-
.disabled(password.isEmpty)
60-
.padding([.top, .bottom, .horizontal], 8)
61-
.frame(maxWidth: .infinity)
62-
.buttonStyle(.borderedProminent)
63-
64-
Button(authService.string.cancelButtonLabel) {
65-
coordinator.cancel()
120+
.padding(.horizontal)
121+
122+
Spacer()
66123
}
124+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
125+
.navigationBarTitleDisplayMode(.inline)
67126
}
68-
.padding()
127+
.errorAlert(error: $error, okButtonLabel: authService.string.okButtonLabel)
69128
}
70129
}
71130

72131
#Preview {
73132
FirebaseOptions.dummyConfigurationForPreview()
74-
return PasswordPromptSheet(coordinator: PasswordPromptCoordinator()).environment(AuthService())
133+
return EmailReauthView(
134+
135+
coordinator: ReauthenticationCoordinator()
136+
)
137+
.environment(AuthService())
75138
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/ReauthenticationModifier.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ struct ReauthenticationModifier: ViewModifier {
2222

2323
func body(content: Content) -> some View {
2424
content
25-
// Alert for non-phone providers
25+
// Alert for simple providers only (Google, Apple, etc.)
2626
.alert(
2727
"Authentication Required",
2828
isPresented: $coordinator.isReauthenticating
@@ -54,7 +54,7 @@ struct ReauthenticationModifier: ViewModifier {
5454
Text("For security, we need to verify your phone number: \(phoneNumber)")
5555
}
5656
}
57-
// Sheet for phone reauthentication (shown after alert confirmation)
57+
// Sheet for phone reauthentication
5858
.sheet(isPresented: $coordinator.showingPhoneReauth) {
5959
if let phoneNumber = coordinator.reauthContext?.phoneNumber {
6060
PhoneReauthView(
@@ -63,6 +63,15 @@ struct ReauthenticationModifier: ViewModifier {
6363
)
6464
}
6565
}
66+
// Sheet for email reauthentication
67+
.sheet(isPresented: $coordinator.showingEmailPasswordPrompt) {
68+
if let email = coordinator.reauthContext?.email {
69+
EmailReauthView(
70+
email: email,
71+
coordinator: coordinator
72+
)
73+
}
74+
}
6675
}
6776

6877
private func performReauth() {

0 commit comments

Comments
 (0)