Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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).