Files
ladill-mini/apps/ios/LadillMini/Features/QR/QrSheets.swift
T
isaaccladandCursor ea6afb80a4
Deploy Ladill Mini / deploy (push) Successful in 35s
Add Ladill Mini native Android and iOS apps.
Ship full-featured mobile clients with auth, payments, QRs, push notifications, and iOS polish including Figtree typography, app icons, and QR previews.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 22:30:35 +00:00

233 lines
9.0 KiB
Swift

import SwiftUI
struct CreatePaymentQrSheet: View {
let onCreated: (Int64) -> Void
@Environment(\.dismiss) private var dismiss
@State private var label = ""
@State private var businessName = ""
@State private var branchLabel = ""
@State private var loading = false
@State private var error: String?
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 16) {
LadillTextField(label: "Label", placeholder: "e.g. Main till", text: $label)
LadillTextField(label: "Business name", placeholder: "Shown to payers", text: $businessName)
LadillTextField(label: "Branch (optional)", placeholder: "e.g. Osu branch", text: $branchLabel)
if let error {
Text(error).font(.ladill(13)).foregroundStyle(LadillColors.red)
}
LadillButton(
title: "Create QR",
loading: loading,
enabled: !label.isEmpty && !businessName.isEmpty
) {
Task { await create() }
}
}
.padding(20)
}
.background(LadillColors.page)
.navigationTitle("New payment QR")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Cancel") { dismiss() }
}
}
}
}
private func create() async {
loading = true
error = nil
defer { loading = false }
do {
let qr = try await APIClient.shared.createPaymentQr(CreatePaymentQrRequest(
label: label.trimmingCharacters(in: .whitespaces),
businessName: businessName.trimmingCharacters(in: .whitespaces),
branchLabel: branchLabel.trimmingCharacters(in: .whitespaces).isEmpty ? nil : branchLabel.trimmingCharacters(in: .whitespaces)
))
dismiss()
onCreated(qr.id)
} catch {
self.error = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
}
}
}
struct PaymentQrDetailSheet: View {
let qrId: Int64
let onDismiss: () -> Void
let onDeleted: () -> Void
@Environment(\.dismiss) private var dismiss
@State private var qr: PaymentQr?
@State private var loading = true
@State private var error: String?
@State private var working = false
var body: some View {
NavigationStack {
Group {
if loading {
LoadingView()
} else if let error {
ErrorView(message: error) { Task { await load() } }
} else if let qr {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
LadillCard {
VStack(spacing: 14) {
PaymentQrPreview(
publicUrl: qr.publicUrl,
previewUrl: qr.previewUrl
)
.frame(maxWidth: .infinity)
.padding(16)
.background(LadillColors.surface)
.clipShape(RoundedRectangle(cornerRadius: 16))
Text(qr.label)
.font(.ladill(20, weight: .semibold))
.foregroundStyle(LadillColors.ink)
if let subtitle = qrSubtitle(qr) {
Text(subtitle)
.font(.ladill(14))
.foregroundStyle(LadillColors.muted)
}
if let url = qr.publicUrl {
Text(url)
.font(.ladill(14))
.foregroundStyle(LadillColors.indigo)
}
}
}
LadillCard {
detailRow("Scans", value: "\(qr.scansTotal ?? 0)")
Divider().padding(.vertical, 12)
detailRow("Currency", value: qr.currency ?? "GHS")
Divider().padding(.vertical, 12)
HStack {
VStack(alignment: .leading, spacing: 2) {
Text("Active")
.font(.ladill(14, weight: .medium))
Text((qr.isActive ?? true) ? "Accepting payments" : "Paused — customers can't pay")
.font(.ladill(12))
.foregroundStyle(LadillColors.muted)
}
Spacer()
Toggle("", isOn: Binding(
get: { qr.isActive ?? true },
set: { _ in Task { await toggleActive(qr) } }
))
.labelsHidden()
.tint(LadillColors.indigo)
.disabled(working)
}
}
if let url = qr.publicUrl {
ShareLink(item: url) {
Text("Share payment link")
.font(.ladill(15, weight: .semibold))
.foregroundStyle(LadillColors.ink)
.frame(maxWidth: .infinity)
.frame(height: 52)
.background(LadillColors.surfaceSoft)
.clipShape(Capsule())
}
}
Button(role: .destructive) {
Task { await deleteQr() }
} label: {
Text("Delete QR")
.font(.ladill(15, weight: .semibold))
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
}
.disabled(working)
}
.padding(20)
}
}
}
.background(LadillColors.page)
.navigationTitle("Payment QR")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Done") {
dismiss()
onDismiss()
}
}
}
.task { await load() }
}
}
private func load() async {
loading = true
error = nil
defer { loading = false }
do {
qr = try await APIClient.shared.paymentQr(id: qrId)
} catch {
self.error = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
}
}
private func toggleActive(_ qr: PaymentQr) async {
working = true
defer { working = false }
do {
self.qr = try await APIClient.shared.updatePaymentQr(
id: qrId,
body: UpdatePaymentQrRequest(isActive: !(qr.isActive ?? true))
)
} catch {
self.error = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
}
}
private func deleteQr() async {
working = true
defer { working = false }
do {
try await APIClient.shared.deletePaymentQr(id: qrId)
dismiss()
onDeleted()
} catch {
self.error = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
}
}
}
private func qrSubtitle(_ qr: PaymentQr) -> String? {
let parts = [qr.businessName, qr.branchLabel]
.compactMap { $0 }
.filter { !$0.isEmpty }
return parts.isEmpty ? nil : parts.joined(separator: " · ")
}
@ViewBuilder
private func detailRow(_ label: String, value: String) -> some View {
HStack {
Text(label)
.font(.ladill(14))
.foregroundStyle(LadillColors.muted)
Spacer()
Text(value)
.font(.ladill(14, weight: .medium))
.foregroundStyle(LadillColors.ink)
}
}