feat: add iOS support and harden receipt scanning

This commit is contained in:
Timo
2026-08-20 23:48:26 +02:00
parent f5e06c32b0
commit cf1b799e1b
107 changed files with 11755 additions and 122 deletions

View File

@@ -0,0 +1,162 @@
import SwiftUI
/// Tab root for "Konto" (see `App/MainTabView.swift`, which references
/// `SettingsView()` with a zero-arg initializer as its third tab). Wrapped in
/// its own `NavigationStack` every push (folders, change password, delete
/// account) and the Pro-upgrade sheet originate from here.
struct SettingsView: View {
@EnvironmentObject private var appState: AppState
@State private var showPaywall = false
var body: some View {
NavigationStack {
Form {
profileSection
foldersSection
accountSection
}
.zenithListBackground()
.navigationTitle("Konto")
.sheet(isPresented: $showPaywall) {
PaywallView()
}
}
}
// MARK: - Profil
@ViewBuilder
private var profileSection: some View {
Section {
if let user = appState.currentUser {
VStack(alignment: .leading, spacing: 4) {
Text(user.name?.isEmpty == false ? user.name! : "Unbenannt")
.zenithHeadlineMdStyle()
if let email = user.email {
Text(email)
.zenithBodySmStyle()
}
}
.padding(.vertical, ZenithSpacing.unit)
.listRowBackground(Color.zenithSurface)
.listRowSeparatorTint(Color.zenithBorder)
if user.isPro {
proStatusRow(for: user)
.listRowBackground(Color.zenithSurface)
.listRowSeparatorTint(Color.zenithBorder)
} else {
freeStatusRow
.listRowBackground(Color.zenithSurface)
.listRowSeparatorTint(Color.zenithBorder)
}
}
}
}
private func proStatusRow(for user: User) -> some View {
VStack(alignment: .leading, spacing: ZenithSpacing.xs) {
HStack(spacing: ZenithSpacing.xs) {
Text("PRO")
.zenithLabelCapsStyle(color: .white)
.padding(.horizontal, 8)
.padding(.vertical, 3)
.background(Color.zenithBlack)
ZenithChip(text: planDisplayName(user.plan))
}
if let expiresAt = user.expiresAt, let date = ISO8601.parse(expiresAt) {
let formatted = date.formatted(date: .abbreviated, time: .omitted)
Text(user.cancelAtPeriodEnd ? "Läuft aus am \(formatted)" : "Verlängert sich am \(formatted)")
.zenithLabelMdStyle(color: .zenithMuted)
}
}
.padding(.vertical, ZenithSpacing.unit)
}
private var freeStatusRow: some View {
VStack(alignment: .leading, spacing: ZenithSpacing.xs) {
Text("Kostenlos")
.zenithBodyStyle(color: .zenithMuted)
Button("Auf Pro upgraden") {
showPaywall = true
}
.buttonStyle(.zenithPrimary)
}
.padding(.vertical, ZenithSpacing.unit)
}
private func planDisplayName(_ plan: String) -> String {
switch plan {
case "weekly": return "Wochen-Pass"
case "annual": return "Jahres-Pass"
case "lifetime": return "Lifetime"
case "free": return "Kostenlos"
default: return plan.capitalized
}
}
// MARK: - Ordner
private var foldersSection: some View {
Section {
NavigationLink {
ProjectsListView()
} label: {
Label {
Text("Ordner verwalten").zenithBodyStyle()
} icon: {
Image(systemName: "folder").foregroundStyle(Color.zenithMuted)
}
}
.listRowBackground(Color.zenithSurface)
.listRowSeparatorTint(Color.zenithBorder)
} header: {
Text("Ordner").zenithLabelCapsStyle()
}
}
// MARK: - Konto
private var accountSection: some View {
Section {
NavigationLink {
ChangePasswordView()
} label: {
Label {
Text("Passwort ändern").zenithBodyStyle()
} icon: {
Image(systemName: "lock").foregroundStyle(Color.zenithMuted)
}
}
.listRowBackground(Color.zenithSurface)
.listRowSeparatorTint(Color.zenithBorder)
Button {
Task { await appState.logout() }
} label: {
Label {
Text("Abmelden")
} icon: {
Image(systemName: "arrow.backward.square")
}
}
.buttonStyle(.zenithPlain)
.listRowBackground(Color.zenithSurface)
.listRowSeparatorTint(Color.zenithBorder)
NavigationLink {
DeleteAccountView()
} label: {
Label {
Text("Konto löschen").zenithBodyStyle(color: .zenithError)
} icon: {
Image(systemName: "trash").foregroundStyle(Color.zenithError)
}
}
.listRowBackground(Color.zenithSurface)
.listRowSeparatorTint(Color.zenithBorder)
} header: {
Text("Konto").zenithLabelCapsStyle()
}
}
}