Skip to content

Commit e4d8e4e

Browse files
Merge pull request #1311 from firebase/make-props-private
2 parents f8d7d4a + b533c03 commit e4d8e4e

File tree

9 files changed

+90
-49
lines changed

9 files changed

+90
-49
lines changed

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Auth/MultiFactor.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,12 @@ public struct EnrollmentSession {
100100
}
101101
}
102102

103-
public enum MFAHint {
103+
public enum MFAHint: Hashable {
104104
case phone(displayName: String?, uid: String, phoneNumber: String?)
105105
case totp(displayName: String?, uid: String)
106106
}
107107

108-
public struct MFARequired {
108+
public struct MFARequired: Hashable {
109109
public let hints: [MFAHint]
110110

111111
public init(hints: [MFAHint]) {

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,24 @@ import Observation
1818
@MainActor
1919
@Observable
2020
public final class PasswordPromptCoordinator {
21-
var isPromptingPassword = false
21+
public var isPromptingPassword = false
2222
private var continuation: CheckedContinuation<String, Error>?
2323

24-
func confirmPassword() async throws -> String {
24+
public init() {}
25+
26+
public func confirmPassword() async throws -> String {
2527
return try await withCheckedThrowingContinuation { continuation in
2628
self.continuation = continuation
2729
self.isPromptingPassword = true
2830
}
2931
}
3032

31-
func submit(password: String) {
33+
public func submit(password: String) {
3234
continuation?.resume(returning: password)
3335
cleanup()
3436
}
3537

36-
func cancel() {
38+
public func cancel() {
3739
continuation?
3840
.resume(throwing: AuthServiceError
3941
.signInCancelled("Password entry cancelled for Email provider"))

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public enum AuthView: Hashable {
4949
case updatePassword
5050
case mfaEnrollment
5151
case mfaManagement
52-
case mfaResolution
52+
case mfaResolution(MFARequired)
5353
case enterPhoneNumber
5454
case enterVerificationCode(verificationID: String, fullPhoneNumber: String)
5555
}
@@ -132,14 +132,18 @@ public final class AuthService {
132132
public var authenticationState: AuthenticationState = .unauthenticated
133133
public var authenticationFlow: AuthenticationFlow = .signIn
134134

135-
public let passwordPrompt: PasswordPromptCoordinator = .init()
136-
public var currentMFARequired: MFARequired?
137135
private var currentMFAResolver: MultiFactorResolver?
138136

139137
// MARK: - Provider APIs
140138

141139
private var listenerManager: AuthListenerManager?
142140

141+
private var emailProvider: EmailProviderSwift?
142+
143+
public var passwordPrompt: PasswordPromptCoordinator {
144+
emailProvider?.passwordPrompt ?? PasswordPromptCoordinator()
145+
}
146+
143147
var emailSignInEnabled = false
144148
private var emailSignInCallback: (() -> Void)?
145149

@@ -199,7 +203,7 @@ public final class AuthService {
199203
: .authenticated
200204
}
201205

202-
public var shouldHandleAnonymousUpgrade: Bool {
206+
private var shouldHandleAnonymousUpgrade: Bool {
203207
currentUser?.isAnonymous == true && configuration.shouldAutoUpgradeAnonymousUsers
204208
}
205209

@@ -317,14 +321,16 @@ public extension AuthService {
317321

318322
public extension AuthService {
319323
/// Enable email sign-in with default behavior (navigates to email link view)
320-
func withEmailSignIn() -> AuthService {
321-
return withEmailSignIn { [weak self] in
324+
func withEmailSignIn(_ provider: EmailProviderSwift? = nil) -> AuthService {
325+
return withEmailSignIn(provider) { [weak self] in
322326
self?.navigator.push(.emailLink)
323327
}
324328
}
325329

326330
/// Enable email sign-in with custom callback
327-
func withEmailSignIn(onTap: @escaping () -> Void) -> AuthService {
331+
func withEmailSignIn(_ provider: EmailProviderSwift? = nil,
332+
onTap: @escaping () -> Void) -> AuthService {
333+
emailProvider = provider ?? EmailProviderSwift()
328334
emailSignInEnabled = true
329335
emailSignInCallback = onTap
330336
return self
@@ -747,8 +753,14 @@ public extension AuthService {
747753
guard let email = user.email else {
748754
throw AuthServiceError.invalidCredentials("User does not have an email address")
749755
}
750-
let password = try await passwordPrompt.confirmPassword()
751-
let credential = EmailAuthProvider.credential(withEmail: email, password: password)
756+
757+
guard let emailProvider = emailProvider else {
758+
throw AuthServiceError.providerNotFound(
759+
"Email provider not configured. Call withEmailSignIn() first."
760+
)
761+
}
762+
763+
let credential = try await emailProvider.createReauthCredential(email: email)
752764
_ = try await user.reauthenticate(with: credential)
753765
} else if providerId == PhoneAuthProviderID {
754766
// Phone auth requires manual reauthentication via sign out and sign in otherwise it will take
@@ -879,7 +891,6 @@ public extension AuthService {
879891

880892
private func handleMFARequiredError(resolver: MultiFactorResolver) -> SignInOutcome {
881893
let hints = extractMFAHints(from: resolver)
882-
currentMFARequired = MFARequired(hints: hints)
883894
currentMFAResolver = resolver
884895
return .mfaRequired(MFARequired(hints: hints))
885896
}
@@ -957,7 +968,6 @@ public extension AuthService {
957968
updateAuthenticationState()
958969

959970
// Clear MFA resolution state
960-
currentMFARequired = nil
961971
currentMFAResolver = nil
962972

963973
} catch {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright 2025 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+
15+
import FirebaseAuth
16+
17+
/// Email/Password authentication provider
18+
/// This provider is special and doesn't render in the button list
19+
@MainActor
20+
public class EmailProviderSwift: AuthProviderSwift {
21+
public let passwordPrompt: PasswordPromptCoordinator
22+
public let providerId = EmailAuthProviderID
23+
24+
public init(passwordPrompt: PasswordPromptCoordinator = .init()) {
25+
self.passwordPrompt = passwordPrompt
26+
}
27+
28+
/// Create credential for reauthentication
29+
func createReauthCredential(email: String) async throws -> AuthCredential {
30+
let password = try await passwordPrompt.confirmPassword()
31+
return EmailAuthProvider.credential(withEmail: email, password: password)
32+
}
33+
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ public struct AuthPickerView<Content: View> {
3333
extension AuthPickerView: View {
3434
public var body: some View {
3535
@Bindable var authService = authService
36-
@Bindable var passwordPrompt = authService.passwordPrompt
3736
content()
3837
.sheet(isPresented: $authService.isPresented) {
3938
@Bindable var navigator = authService.navigator
39+
@Bindable var passwordPrompt = authService.passwordPrompt
4040
NavigationStack(path: $navigator.routes) {
4141
authPickerViewInternal
4242
.navigationTitle(authService.authenticationState == .unauthenticated ? authService
@@ -57,8 +57,8 @@ extension AuthPickerView: View {
5757
MFAEnrolmentView()
5858
case AuthView.mfaManagement:
5959
MFAManagementView()
60-
case AuthView.mfaResolution:
61-
MFAResolutionView()
60+
case let .mfaResolution(mfaRequired):
61+
MFAResolutionView(mfaRequired: mfaRequired)
6262
case AuthView.enterPhoneNumber:
6363
EnterPhoneNumberView()
6464
case let .enterVerificationCode(verificationID, fullPhoneNumber):
@@ -79,10 +79,10 @@ extension AuthPickerView: View {
7979
.accountConflictHandler()
8080
// Apply MFA handling at NavigationStack level
8181
.mfaHandler()
82-
}
83-
// Centralized password prompt sheet to prevent conflicts
84-
.sheet(isPresented: $passwordPrompt.isPromptingPassword) {
85-
PasswordPromptSheet(coordinator: authService.passwordPrompt)
82+
// Centralized password prompt sheet inside auth flow
83+
.sheet(isPresented: $passwordPrompt.isPromptingPassword) {
84+
PasswordPromptSheet(coordinator: passwordPrompt)
85+
}
8686
}
8787
}
8888

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAHandlerModifier.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ struct MFAHandlerModifier: ViewModifier {
3939
}
4040

4141
/// Handle MFA required - navigate to MFA resolution view
42-
func handleMFARequired(_: MFARequired) {
43-
authService.navigator.push(.mfaResolution)
42+
func handleMFARequired(_ mfaRequired: MFARequired) {
43+
authService.navigator.push(.mfaResolution(mfaRequired))
4444
}
4545
}
4646

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAResolutionView.swift

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ private enum FocusableField: Hashable {
2323

2424
@MainActor
2525
public struct MFAResolutionView {
26+
let mfaRequired: MFARequired
27+
2628
@Environment(AuthService.self) private var authService
2729
@Environment(\.reportError) private var reportError
2830

@@ -34,16 +36,12 @@ public struct MFAResolutionView {
3436

3537
@FocusState private var focus: FocusableField?
3638

37-
public init() {}
38-
39-
private var mfaRequired: MFARequired? {
40-
// This would be set by the sign-in flow when MFA is required
41-
authService.currentMFARequired
39+
public init(mfaRequired: MFARequired) {
40+
self.mfaRequired = mfaRequired
4241
}
4342

4443
private var selectedHint: MFAHint? {
45-
guard let mfaRequired = mfaRequired,
46-
selectedHintIndex < mfaRequired.hints.count else {
44+
guard selectedHintIndex < mfaRequired.hints.count else {
4745
return nil
4846
}
4947
return mfaRequired.hints[selectedHintIndex]
@@ -63,7 +61,7 @@ public struct MFAResolutionView {
6361
}
6462

6563
private func startSMSChallenge() {
66-
guard selectedHintIndex < (mfaRequired?.hints.count ?? 0) else { return }
64+
guard selectedHintIndex < mfaRequired.hints.count else { return }
6765

6866
Task {
6967
isLoading = true
@@ -128,7 +126,7 @@ extension MFAResolutionView: View {
128126
.padding(.horizontal)
129127

130128
// MFA Hints Selection (if multiple available)
131-
if let mfaRequired = mfaRequired, mfaRequired.hints.count > 1 {
129+
if mfaRequired.hints.count > 1 {
132130
mfaHintsSelectionView(mfaRequired: mfaRequired)
133131
}
134132

@@ -368,34 +366,36 @@ private extension MFAHint {
368366
#Preview("Phone SMS Only") {
369367
FirebaseOptions.dummyConfigurationForPreview()
370368
let authService = AuthService()
371-
authService.currentMFARequired = MFARequired(hints: [
369+
let mfaRequired = MFARequired(hints: [
372370
.phone(displayName: "Work Phone", uid: "phone-uid-1", phoneNumber: "+15551234567"),
373371
])
374-
return MFAResolutionView().environment(authService)
372+
return MFAResolutionView(mfaRequired: mfaRequired).environment(authService)
375373
}
376374

377375
#Preview("TOTP Only") {
378376
FirebaseOptions.dummyConfigurationForPreview()
379377
let authService = AuthService()
380-
authService.currentMFARequired = MFARequired(hints: [
378+
let mfaRequired = MFARequired(hints: [
381379
.totp(displayName: "Authenticator App", uid: "totp-uid-1"),
382380
])
383-
return MFAResolutionView().environment(authService)
381+
return MFAResolutionView(mfaRequired: mfaRequired).environment(authService)
384382
}
385383

386384
#Preview("Multiple Methods") {
387385
FirebaseOptions.dummyConfigurationForPreview()
388386
let authService = AuthService()
389-
authService.currentMFARequired = MFARequired(hints: [
387+
let mfaRequired = MFARequired(hints: [
390388
.phone(displayName: "Mobile", uid: "phone-uid-1", phoneNumber: "+15551234567"),
391389
.totp(displayName: "Google Authenticator", uid: "totp-uid-1"),
392390
])
393-
return MFAResolutionView().environment(authService)
391+
return MFAResolutionView(mfaRequired: mfaRequired).environment(authService)
394392
}
395393

396-
#Preview("No MFA Required") {
394+
#Preview("Single TOTP") {
397395
FirebaseOptions.dummyConfigurationForPreview()
398396
let authService = AuthService()
399-
// currentMFARequired is nil by default
400-
return MFAResolutionView().environment(authService)
397+
let mfaRequired = MFARequired(hints: [
398+
.totp(displayName: "Authenticator", uid: "totp-uid-1"),
399+
])
400+
return MFAResolutionView(mfaRequired: mfaRequired).environment(authService)
401401
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePasswordView.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ public struct UpdatePasswordView {
5353

5454
extension UpdatePasswordView: View {
5555
public var body: some View {
56-
@Bindable var passwordPrompt = authService.passwordPrompt
5756
VStack(spacing: 24) {
5857
AuthTextField(
5958
text: $password,
@@ -115,9 +114,6 @@ extension UpdatePasswordView: View {
115114
} message: {
116115
Text("Your password has been successfully updated.")
117116
}
118-
.sheet(isPresented: $passwordPrompt.isPromptingPassword) {
119-
PasswordPromptSheet(coordinator: authService.passwordPrompt)
120-
}
121117
}
122118
}
123119

samples/swiftui/FirebaseSwiftUISample/FirebaseSwiftUISample/Application/ContentView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import FirebaseOAuthSwiftUI
2525

2626
struct ContentView: View {
2727
init() {
28-
Auth.auth().useEmulator(withHost: "127.0.0.1", port: 9099)
28+
// Auth.auth().useEmulator(withHost: "127.0.0.1", port: 9099)
2929

3030
let actionCodeSettings = ActionCodeSettings()
3131

0 commit comments

Comments
 (0)