import SwiftUI struct ProfileView: View { let onBack: () -> Void @State private var name = SessionStore.shared.name ?? "" @State private var phone = "" @State private var phoneCc = Countries.default.dialCode @State private var loading = false @State private var saving = false @State private var message: String? @State private var error: String? var body: some View { MenuScaffold(title: "Profile", onBack: onBack) { ScrollView { VStack(spacing: 16) { LadillTextField(label: "Name", placeholder: "Your name", text: $name) LadillTextField(label: "Phone", placeholder: "Mobile number", text: $phone, keyboard: .phonePad) if let message { Text(message).font(.ladill(size: 13)).foregroundStyle(LadillColors.money) } if let error { Text(error).font(.ladill(size: 13)).foregroundStyle(LadillColors.red) } LadillButton(title: "Save changes", loading: saving, enabled: !name.isEmpty) { Task { await save() } } } .padding(20) } .task { await load() } } } private func load() async { loading = true defer { loading = false } if let settings = try? await APIClient.shared.accountSettings() { name = settings.profile.name ?? name phone = settings.profile.phone ?? "" phoneCc = settings.profile.phoneCc ?? phoneCc } } private func save() async { saving = true error = nil message = nil defer { saving = false } do { let profile = try await APIClient.shared.updateProfile(UpdateProfileRequest( name: name.trimmingCharacters(in: .whitespaces), phoneCc: phoneCc, phone: phone.trimmingCharacters(in: .whitespaces).isEmpty ? nil : phone.trimmingCharacters(in: .whitespaces) )) SessionStore.shared.updateName(profile.name) message = "Profile updated." } catch { self.error = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription } } } struct SettingsView: View { let onBack: () -> Void @State private var notifyEmail = "" @State private var productUpdates = true @State private var notifyRegistrations = true @State private var notifyPayouts = true @State private var saving = false @State private var message: String? @State private var error: String? var body: some View { MenuScaffold(title: "Settings", onBack: onBack) { ScrollView { VStack(alignment: .leading, spacing: 16) { Text("Notifications") .font(.ladill(size: 16, weight: .semibold)) LadillTextField(label: "Notification email", placeholder: "Optional", text: $notifyEmail, keyboard: .emailAddress) Toggle("Product updates", isOn: $productUpdates).tint(LadillColors.indigo) Toggle("Payment alerts", isOn: $notifyRegistrations).tint(LadillColors.indigo) Toggle("Payout updates", isOn: $notifyPayouts).tint(LadillColors.indigo) if let message { Text(message).font(.ladill(size: 13)).foregroundStyle(LadillColors.money) } if let error { Text(error).font(.ladill(size: 13)).foregroundStyle(LadillColors.red) } LadillButton(title: "Save", loading: saving) { Task { await save() } } } .padding(20) } .task { await load() } } } private func load() async { if let settings = try? await APIClient.shared.accountSettings() { notifyEmail = settings.notifications.notifyEmail ?? "" productUpdates = settings.notifications.productUpdates ?? true notifyRegistrations = settings.notifications.notifyRegistrations ?? true notifyPayouts = settings.notifications.notifyPayouts ?? true } } private func save() async { saving = true error = nil message = nil defer { saving = false } do { _ = try await APIClient.shared.updateSettings(UpdateSettingsRequest( notifyEmail: notifyEmail.trimmingCharacters(in: .whitespaces).isEmpty ? nil : notifyEmail.trimmingCharacters(in: .whitespaces), productUpdates: productUpdates, notifyRegistrations: notifyRegistrations, notifyPayouts: notifyPayouts )) message = "Settings saved." } catch { self.error = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription } } } struct SecurityView: View { let onBack: () -> Void @State private var currentPassword = "" @State private var password = "" @State private var confirmation = "" @State private var saving = false @State private var message: String? @State private var error: String? var body: some View { MenuScaffold(title: "Security", onBack: onBack) { ScrollView { VStack(alignment: .leading, spacing: 16) { Text("Change password") .font(.ladill(size: 16, weight: .semibold)) LadillTextField(label: "Current password", placeholder: "••••••••", text: $currentPassword, secure: true) LadillTextField(label: "New password", placeholder: "At least 8 characters", text: $password, secure: true) LadillTextField(label: "Confirm password", placeholder: "Repeat password", text: $confirmation, secure: true) if PinStore.shared.hasPin() { Button("Remove app PIN") { PinStore.shared.clearPin() message = "PIN removed." } .font(.ladill(size: 14, weight: .semibold)) .foregroundStyle(LadillColors.indigo) } else { Text("Set an app PIN from your device settings after your next sign-in.") .font(.ladill(size: 13)) .foregroundStyle(LadillColors.muted) } if let message { Text(message).font(.ladill(size: 13)).foregroundStyle(LadillColors.money) } if let error { Text(error).font(.ladill(size: 13)).foregroundStyle(LadillColors.red) } LadillButton(title: "Update password", loading: saving, enabled: password.count >= 8) { Task { await save() } } } .padding(20) } } } private func save() async { if password != confirmation { error = "Passwords do not match." return } saving = true error = nil message = nil defer { saving = false } do { let response = try await APIClient.shared.changePassword(ChangePasswordRequest( currentPassword: currentPassword, password: password, passwordConfirmation: confirmation )) message = response.message ?? "Password updated." currentPassword = "" password = "" confirmation = "" } catch { self.error = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription } } } struct WalletView: View { let onBack: () -> Void @State private var wallet: WalletInfo? @State private var payout: PayoutAccount? @State private var withdrawals: [Withdrawal] = [] @State private var amount = "" @State private var loading = true @State private var error: String? var body: some View { MenuScaffold(title: "Wallet", onBack: onBack) { ScrollView { VStack(alignment: .leading, spacing: 16) { if loading { LoadingView() } else if let wallet { StatCard( label: "Balance", value: Formatters.money(minor: wallet.balanceMinor ?? 0, currency: wallet.currency ?? "GHS"), accent: true ) Text("Ladill Mini deducts a small platform fee from each payment you collect. Your balance is what's left to withdraw.") .font(.ladill(size: 13)) .foregroundStyle(LadillColors.muted) if let payout { LadillCard { VStack(alignment: .leading, spacing: 4) { Text("Payout account") .font(.ladill(size: 12, weight: .medium)) .foregroundStyle(LadillColors.muted) Text(payout.accountName ?? "") Text(payout.bankName ?? payout.accountType ?? "") .font(.ladill(size: 13)) .foregroundStyle(LadillColors.muted) } } } LadillTextField(label: "Withdraw amount (GHS)", placeholder: "0.00", text: $amount, keyboard: .decimalPad) LadillButton(title: "Request withdrawal", enabled: Double(amount) ?? 0 > 0) { Task { await withdraw() } } if !withdrawals.isEmpty { Text("Recent withdrawals") .font(.ladill(size: 16, weight: .semibold)) .padding(.top, 8) ForEach(withdrawals) { w in LadillCard { HStack { Text(Formatters.money(minor: Int64((w.amountGhs ?? 0) * 100), currency: "GHS")) Spacer() Text(w.status?.capitalized ?? "Pending") .font(.ladill(size: 12)) .foregroundStyle(LadillColors.muted) } } } } } if let error { Text(error).font(.ladill(size: 13)).foregroundStyle(LadillColors.red) } } .padding(20) } .task { await load() } } } private func load() async { loading = true defer { loading = false } do { wallet = try await APIClient.shared.wallet() payout = try await APIClient.shared.payoutAccount() withdrawals = try await APIClient.shared.withdrawals() } catch { self.error = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription } } private func withdraw() async { guard let value = Double(amount), value > 0 else { return } do { _ = try await APIClient.shared.withdraw(amount: value) amount = "" await load() } catch { self.error = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription } } } struct BillingView: View { let onBack: () -> Void var body: some View { MenuScaffold(title: "Billing", onBack: onBack) { ScrollView { VStack(alignment: .leading, spacing: 12) { Text("Usage & platform fees") .font(.ladill(size: 16, weight: .semibold)) Text("Ladill Mini charges a 5% platform fee on payments you collect. There are no monthly fees for payment QRs.") .font(.ladill(size: 14)) .foregroundStyle(LadillColors.muted) LadillCard { VStack(alignment: .leading, spacing: 8) { Text("Platform fee") .font(.ladill(size: 12, weight: .medium)) .foregroundStyle(LadillColors.muted) Text("5% per payment") .font(.ladill(size: 20, weight: .semibold)) .foregroundStyle(LadillColors.indigoText) } } } .padding(20) } } } } struct SupportView: View { let onBack: () -> Void @State private var tickets: [SupportTicket] = [] @State private var subject = "" @State private var message = "" @State private var loading = true @State private var sending = false @State private var error: String? var body: some View { MenuScaffold(title: "Support", onBack: onBack) { ScrollView { VStack(spacing: 16) { LadillTextField(label: "Subject", placeholder: "How can we help?", text: $subject) LadillTextField(label: "Message", placeholder: "Describe your issue", text: $message) LadillButton(title: "Submit ticket", loading: sending, enabled: !subject.isEmpty && !message.isEmpty) { Task { await submit() } } if !tickets.isEmpty { Text("Your tickets") .font(.ladill(size: 16, weight: .semibold)) .frame(maxWidth: .infinity, alignment: .leading) ForEach(tickets) { ticket in LadillCard { VStack(alignment: .leading, spacing: 4) { Text(ticket.subject ?? "") .font(.ladill(size: 15, weight: .semibold)) Text(ticket.status?.capitalized ?? "Open") .font(.ladill(size: 12)) .foregroundStyle(LadillColors.muted) } } } } if let error { Text(error).font(.ladill(size: 13)).foregroundStyle(LadillColors.red) } } .padding(20) } .task { await load() } } } private func load() async { loading = true defer { loading = false } tickets = (try? await APIClient.shared.supportTickets()) ?? [] } private func submit() async { sending = true error = nil defer { sending = false } do { _ = try await APIClient.shared.createSupportTicket(CreateTicketRequest( subject: subject.trimmingCharacters(in: .whitespaces), message: message.trimmingCharacters(in: .whitespaces), priority: "normal" )) subject = "" message = "" await load() } catch { self.error = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription } } }