Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ DerivedData
.build/
# Third Party
/sdk
vendor/

# Generated
/target
Expand All @@ -29,4 +30,4 @@ Pods/
Podfile.lock
*.xcworkspace/
samples/**/GoogleService-Info.plist
e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests.xcresult/
e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests.xcresult/
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ import Foundation
import SwiftUI

public struct AuthConfiguration {
public enum DeleteAccountButtonAction {
case showConfirmationSheet
case hidden
case custom(@MainActor () -> Void)
}

public let logo: ImageResource?
public let languageCode: String?
public let shouldHideCancelButton: Bool
Expand All @@ -27,6 +33,7 @@ public struct AuthConfiguration {
public let privacyPolicyUrl: URL?
public let emailLinkSignInActionCodeSettings: ActionCodeSettings?
public let verifyEmailActionCodeSettings: ActionCodeSettings?
public let deleteAccountButtonAction: DeleteAccountButtonAction

// MARK: - MFA Configuration

Expand All @@ -44,6 +51,7 @@ public struct AuthConfiguration {
privacyPolicyUrl: URL? = nil,
emailLinkSignInActionCodeSettings: ActionCodeSettings? = nil,
verifyEmailActionCodeSettings: ActionCodeSettings? = nil,
deleteAccountButtonAction: DeleteAccountButtonAction = .showConfirmationSheet,
mfaEnabled: Bool = false,
allowedSecondFactors: Set<SecondFactorType> = [.sms, .totp],
mfaIssuer: String = "Firebase Auth") {
Expand All @@ -57,6 +65,7 @@ public struct AuthConfiguration {
self.privacyPolicyUrl = privacyPolicyUrl
self.emailLinkSignInActionCodeSettings = emailLinkSignInActionCodeSettings
self.verifyEmailActionCodeSettings = verifyEmailActionCodeSettings
self.deleteAccountButtonAction = deleteAccountButtonAction
self.mfaEnabled = mfaEnabled
self.allowedSecondFactors = allowedSecondFactors
self.mfaIssuer = mfaIssuer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,38 @@ public struct SignedInView {
@State private var showEmailVerificationSent = false
@State private var reauthCoordinator = ReauthenticationCoordinator()

private var showsMfaManagementButton: Bool {
Self.showsMfaManagementButton(configuration: authService.configuration)
}

private var showsDeleteAccountButton: Bool {
Self.showsDeleteAccountButton(configuration: authService.configuration)
}

static func showsMfaManagementButton(configuration: AuthConfiguration) -> Bool {
configuration.mfaEnabled
}

static func showsDeleteAccountButton(configuration: AuthConfiguration) -> Bool {
switch configuration.deleteAccountButtonAction {
case .hidden:
return false
case .showConfirmationSheet, .custom:
return true
}
}

private func performDeleteAccountButtonAction() {
switch authService.configuration.deleteAccountButtonAction {
case .showConfirmationSheet:
showDeleteConfirmation = true
case .hidden:
break
case let .custom(action):
action()
}
}

private func sendEmailVerification() async throws {
do {
try await authService.sendEmailVerification()
Expand Down Expand Up @@ -75,29 +107,33 @@ extension SignedInView: View {
.frame(maxWidth: .infinity)
.accessibilityIdentifier("update-password-button")

Button {
authService.navigator.push(.mfaManagement)
} label: {
Text(authService.string.manageTwoFactorAuthenticationLabel)
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
if showsMfaManagementButton {
Button {
authService.navigator.push(.mfaManagement)
} label: {
Text(authService.string.manageTwoFactorAuthenticationLabel)
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.padding([.top, .bottom], 8)
.frame(maxWidth: .infinity)
.accessibilityIdentifier("mfa-management-button")
}
.buttonStyle(.borderedProminent)
.padding([.top, .bottom], 8)
.frame(maxWidth: .infinity)
.accessibilityIdentifier("mfa-management-button")

Button {
showDeleteConfirmation = true
} label: {
Text(authService.string.deleteAccountButtonLabel)
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
if showsDeleteAccountButton {
Button {
performDeleteAccountButtonAction()
} label: {
Text(authService.string.deleteAccountButtonLabel)
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.padding([.top, .bottom], 8)
.frame(maxWidth: .infinity)
.accessibilityIdentifier("delete-account-button")
}
.buttonStyle(.borderedProminent)
.padding([.top, .bottom], 8)
.frame(maxWidth: .infinity)
.accessibilityIdentifier("delete-account-button")

Button {
Task {
Expand Down Expand Up @@ -175,24 +211,22 @@ private struct DeleteAccountConfirmationSheet: View {
.font(.system(size: 60))
.foregroundColor(.red)

Text("Delete Account?")
Text(authService.string.deleteAccountConfirmationLabel)
.font(.title)
.fontWeight(.bold)

Text(
"This action cannot be undone. All your data will be permanently deleted. You may need to reauthenticate to complete this action."
)
.font(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
Text(authService.string.deleteAccountWarningMessage)
.font(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
}

VStack(spacing: 12) {
Button {
onConfirm()
} label: {
Text("Delete Account")
Text(authService.string.deleteAccountLabel)
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
}
Expand All @@ -205,7 +239,7 @@ private struct DeleteAccountConfirmationSheet: View {
Button {
onCancel()
} label: {
Text("Cancel")
Text(authService.string.cancelButtonLabel)
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

@testable import FirebaseAuthSwiftUI
import Testing

@Suite("SignedInView Tests")
struct SignedInViewTests {
@Test("MFA management button is hidden when MFA is disabled")
func mfaManagementButtonHiddenWhenMfaDisabled() {
let configuration = AuthConfiguration(mfaEnabled: false)

#expect(SignedInView.showsMfaManagementButton(configuration: configuration) == false)
}

@Test("MFA management button is shown when MFA is enabled")
func mfaManagementButtonShownWhenMfaEnabled() {
let configuration = AuthConfiguration(mfaEnabled: true)

#expect(SignedInView.showsMfaManagementButton(configuration: configuration) == true)
}

@Test("Delete account button is shown by default")
func deleteAccountButtonShownByDefault() {
let configuration = AuthConfiguration()

#expect(SignedInView.showsDeleteAccountButton(configuration: configuration) == true)
}

@Test("Delete account button is hidden when configured")
func deleteAccountButtonHiddenWhenConfigured() {
let configuration = AuthConfiguration(deleteAccountButtonAction: .hidden)

#expect(SignedInView.showsDeleteAccountButton(configuration: configuration) == false)
}

@Test("Delete account button is shown for custom actions")
func deleteAccountButtonShownForCustomAction() {
let configuration = AuthConfiguration(deleteAccountButtonAction: .custom {})

#expect(SignedInView.showsDeleteAccountButton(configuration: configuration) == true)
}
}
25 changes: 24 additions & 1 deletion FirebaseSwiftUI/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,28 @@ let configuration = AuthConfiguration(customStringsBundle: .main)

See [example implementation](../../samples/swiftui/FirebaseSwiftUISample/FirebaseSwiftUISample) for a working demo.

### Customizing Account Deletion

By default, `SignedInView` shows a **Delete Account** button that opens the built-in confirmation sheet and deletes the current Firebase Auth user after reauthentication when required.

Hide the button if account deletion is handled elsewhere in your app:

```swift
let configuration = AuthConfiguration(
deleteAccountButtonAction: .hidden
)
```

Or provide a custom tap action to show your own confirmation UI, route to a support flow, or run app-specific cleanup before deleting the account:

```swift
let configuration = AuthConfiguration(
deleteAccountButtonAction: .custom {
// Present your own delete-account flow.
}
)
```


---

Expand All @@ -834,6 +856,7 @@ public struct AuthConfiguration {
privacyPolicyUrl: URL? = nil,
emailLinkSignInActionCodeSettings: ActionCodeSettings? = nil,
verifyEmailActionCodeSettings: ActionCodeSettings? = nil,
deleteAccountButtonAction: DeleteAccountButtonAction = .showConfirmationSheet,
mfaEnabled: Bool = false,
allowedSecondFactors: Set<SecondFactorType> = [.sms, .totp],
mfaIssuer: String = "Firebase Auth"
Expand All @@ -855,6 +878,7 @@ public struct AuthConfiguration {
| `privacyPolicyUrl` | `URL?` | `nil` | URL to your Privacy Policy. When both `tosUrl` and `privacyPolicyUrl` are set, links are displayed in the auth UI. |
| `emailLinkSignInActionCodeSettings` | `ActionCodeSettings?` | `nil` | Configuration for email link (passwordless) sign-in. Must be set to use email link authentication. |
| `verifyEmailActionCodeSettings` | `ActionCodeSettings?` | `nil` | Configuration for email verification. Used when sending verification emails to users. |
| `deleteAccountButtonAction` | `DeleteAccountButtonAction` | `.showConfirmationSheet` | Controls the Delete Account button in `SignedInView`. Use `.showConfirmationSheet` for the built-in deletion flow, `.hidden` to remove the button, or `.custom { ... }` to run your own action when tapped. |
| `mfaEnabled` | `Bool` | `false` | Enables Multi-Factor Authentication support. When enabled, users can enroll in and use MFA. |
| `allowedSecondFactors` | `Set<SecondFactorType>` | `[.sms, .totp]` | Set of allowed MFA factor types. Options are `.sms` (phone-based) and `.totp` (authenticator app). |
| `mfaIssuer` | `String` | `"Firebase Auth"` | The issuer name displayed in TOTP authenticator apps when users enroll. |
Expand Down Expand Up @@ -1737,4 +1761,3 @@ Thrown by sensitive operations when Firebase requires recent authentication. Eac
## Feedback

Please file feedback and issues in the [repository's issue tracker](https://github.com/firebase/FirebaseUI-iOS/issues).