Deploy Ladill Mini / deploy (push) Successful in 35s
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>
233 lines
9.0 KiB
Swift
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)
|
|
}
|
|
}
|