feat: add iOS support and harden receipt scanning
This commit is contained in:
107
app/ios/ScanReceipts/Features/Settings/ChangePasswordView.swift
Normal file
107
app/ios/ScanReceipts/Features/Settings/ChangePasswordView.swift
Normal file
@@ -0,0 +1,107 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user