Files
scan-receipts/app/ios/ScanReceipts/Features/Settings/ChangePasswordView.swift

108 lines
3.9 KiB
Swift

import SwiftUI
/// `POST /api/auth/change-password` (see `Networking/AccountAPI.swift`)
/// rotates EVERY session for the account on success, including this
/// device's and the new session is only delivered as a Set-Cookie, which
/// this Bearer-token client never sees. There is no fresh token to recover
/// from that response, so trying to "stay logged in" here would leave the
/// app holding a dead token. Instead: show a brief confirmation, then log
/// out locally and let the user sign back in with the new password. This is
/// a deliberate consequence of how the backend's session rotation works,
/// not an arbitrary UX choice don't "fix" this into a silent success.
struct ChangePasswordView: View {
@EnvironmentObject private var appState: AppState
@State private var currentPassword = ""
@State private var newPassword = ""
@State private var confirmPassword = ""
@State private var isSubmitting = false
@State private var errorMessage: String?
@State private var showSuccessAlert = false
private var passwordsMatch: Bool {
!newPassword.isEmpty && newPassword == confirmPassword
}
private var canSubmit: Bool {
!currentPassword.isEmpty && passwordsMatch && !isSubmitting
}
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: ZenithSpacing.md) {
ZenithTextField(
label: "Aktuelles Passwort",
text: $currentPassword,
placeholder: "Aktuelles Passwort",
isSecure: true,
textContentType: .password
)
ZenithDivider()
ZenithTextField(
label: "Neues Passwort",
text: $newPassword,
placeholder: "Neues Passwort",
isSecure: true,
textContentType: .newPassword
)
ZenithTextField(
label: "Neues Passwort bestätigen",
text: $confirmPassword,
placeholder: "Neues Passwort bestätigen",
isSecure: true,
textContentType: .newPassword
)
if !confirmPassword.isEmpty && !passwordsMatch {
Text("Die Passwörter stimmen nicht überein.")
.zenithBodySmStyle(color: .zenithError)
}
if let errorMessage {
Text(errorMessage)
.zenithBodySmStyle(color: .zenithError)
}
Button {
Task { await submit() }
} label: {
if isSubmitting {
ProgressView()
.tint(.white)
} else {
Text("Passwort ändern")
}
}
.buttonStyle(.zenithPrimary)
.disabled(!canSubmit)
.padding(.top, ZenithSpacing.sm)
}
.padding(ZenithSpacing.sm)
}
.background(Color.zenithBg)
.navigationTitle("Passwort ändern")
.navigationBarTitleDisplayMode(.inline)
.alert("Passwort geändert", isPresented: $showSuccessAlert) {
Button("OK") {
Task { await appState.logout() }
}
} message: {
Text("Passwort geändert. Bitte melde dich mit dem neuen Passwort erneut an.")
}
}
private func submit() async {
errorMessage = nil
isSubmitting = true
defer { isSubmitting = false }
do {
try await AccountAPI.changePassword(currentPassword: currentPassword, newPassword: newPassword)
showSuccessAlert = true
} catch {
errorMessage = error.localizedDescription
}
}
}