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>
234 lines
8.4 KiB
Swift
234 lines
8.4 KiB
Swift
import SwiftUI
|
|
|
|
private enum HomeTab: Int, CaseIterable {
|
|
case overview, qrs, payments, payouts
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .overview: return "Overview"
|
|
case .qrs: return "QRs"
|
|
case .payments: return "Payments"
|
|
case .payouts: return "Payouts"
|
|
}
|
|
}
|
|
|
|
var icon: String {
|
|
switch self {
|
|
case .overview: return "house.fill"
|
|
case .qrs: return "qrcode"
|
|
case .payments: return "arrow.down.left.circle.fill"
|
|
case .payouts: return "banknote.fill"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct HomeView: View {
|
|
let onNavigate: (AppRoute) -> Void
|
|
let onLoggedOut: () -> Void
|
|
|
|
@EnvironmentObject private var session: SessionStore
|
|
@EnvironmentObject private var pushRouter: PushNotificationRouter
|
|
@StateObject private var vm = HomeViewModel()
|
|
@State private var tab: HomeTab = .overview
|
|
@State private var showMenu = false
|
|
@State private var showCreateQr = false
|
|
@State private var showNotifications = false
|
|
@State private var showAfia = false
|
|
@State private var selectedQrId: Int64?
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
homeTopBar
|
|
|
|
Group {
|
|
switch tab {
|
|
case .overview:
|
|
OverviewTab(vm: vm, onCreateQr: { showCreateQr = true }, onSeePayments: { tab = .payments }, onOpenWallet: { onNavigate(.wallet) })
|
|
case .qrs:
|
|
PaymentQrsTab(vm: vm, onCreateQr: { showCreateQr = true }, onOpenQr: { selectedQrId = $0 })
|
|
case .payments:
|
|
PaymentsTab(vm: vm)
|
|
case .payouts:
|
|
PayoutsTab(vm: vm, onOpenWallet: { onNavigate(.wallet) })
|
|
}
|
|
}
|
|
.frame(maxHeight: .infinity)
|
|
|
|
homeBottomBar
|
|
}
|
|
.background(LadillColors.page)
|
|
.navigationBarHidden(true)
|
|
.task {
|
|
await vm.loadUnreadCount()
|
|
await vm.loadPayoutStatus()
|
|
}
|
|
.onChange(of: pushRouter.shouldOpenInbox) { _, shouldOpen in
|
|
if shouldOpen {
|
|
showNotifications = true
|
|
pushRouter.consumeOpenInboxRequest()
|
|
}
|
|
}
|
|
.sheet(isPresented: $showMenu) {
|
|
UserMenuSheet(
|
|
name: session.name,
|
|
email: session.email,
|
|
onProfile: { showMenu = false; onNavigate(.profile) },
|
|
onSettings: { showMenu = false; onNavigate(.settings) },
|
|
onSecurity: { showMenu = false; onNavigate(.security) },
|
|
onWallet: { showMenu = false; onNavigate(.wallet) },
|
|
onBilling: { showMenu = false; onNavigate(.billing) },
|
|
onSupport: { showMenu = false; onNavigate(.support) },
|
|
onSignOut: {
|
|
showMenu = false
|
|
Task { await vm.logout(onComplete: onLoggedOut) }
|
|
}
|
|
)
|
|
.presentationDetents([.medium, .large])
|
|
}
|
|
.sheet(isPresented: $showCreateQr) {
|
|
CreatePaymentQrSheet(onCreated: { id in
|
|
showCreateQr = false
|
|
selectedQrId = id
|
|
Task { await vm.loadQrs(); await vm.loadOverview() }
|
|
})
|
|
}
|
|
.sheet(isPresented: Binding(
|
|
get: { selectedQrId != nil },
|
|
set: { if !$0 { selectedQrId = nil } }
|
|
)) {
|
|
if let qrId = selectedQrId {
|
|
PaymentQrDetailSheet(qrId: qrId, onDismiss: { selectedQrId = nil }, onDeleted: {
|
|
selectedQrId = nil
|
|
Task { await vm.loadQrs(); await vm.loadOverview() }
|
|
})
|
|
}
|
|
}
|
|
.sheet(isPresented: $showNotifications) {
|
|
NotificationsSheet(onDismiss: {
|
|
showNotifications = false
|
|
Task { await vm.loadUnreadCount() }
|
|
}, onUnreadChanged: { vm.unreadCount = $0 })
|
|
}
|
|
.sheet(isPresented: $showAfia) {
|
|
AfiaSheet()
|
|
}
|
|
}
|
|
|
|
private var homeTopBar: some View {
|
|
HStack {
|
|
UserAvatar(name: session.name, photoURL: session.avatarURL, onTap: { showMenu = true })
|
|
Spacer()
|
|
Button(action: { showAfia = true }) {
|
|
Image(systemName: "sparkles")
|
|
.font(.system(size: 20))
|
|
.foregroundStyle(LadillColors.indigo)
|
|
.frame(width: 40, height: 40)
|
|
.background(LadillColors.indigoSoft)
|
|
.clipShape(Circle())
|
|
}
|
|
ZStack(alignment: .topTrailing) {
|
|
Button(action: { showNotifications = true }) {
|
|
Image(systemName: "bell.fill")
|
|
.font(.system(size: 20))
|
|
.foregroundStyle(LadillColors.ink)
|
|
.frame(width: 40, height: 40)
|
|
}
|
|
if vm.unreadCount > 0 {
|
|
Text(vm.unreadCount > 9 ? "9+" : "\(vm.unreadCount)")
|
|
.font(.ladill(size: 10, weight: .bold))
|
|
.foregroundStyle(.white)
|
|
.padding(4)
|
|
.background(LadillColors.indigo)
|
|
.clipShape(Circle())
|
|
.offset(x: 4, y: -2)
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 20)
|
|
.padding(.vertical, 12)
|
|
}
|
|
|
|
private var homeBottomBar: some View {
|
|
HStack {
|
|
ForEach(HomeTab.allCases, id: \.rawValue) { item in
|
|
Button {
|
|
tab = item
|
|
} label: {
|
|
VStack(spacing: 4) {
|
|
Image(systemName: item.icon)
|
|
.font(.system(size: 20))
|
|
Text(item.title)
|
|
.font(.ladill(size: 11, weight: .medium))
|
|
}
|
|
.foregroundStyle(tab == item ? LadillColors.indigo : LadillColors.muted)
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(.horizontal, 8)
|
|
.padding(.top, 8)
|
|
.padding(.bottom, 4)
|
|
.background(LadillColors.surface)
|
|
.overlay(alignment: .top) { Divider() }
|
|
}
|
|
}
|
|
|
|
struct UserMenuSheet: View {
|
|
let name: String?
|
|
let email: String?
|
|
let onProfile: () -> Void
|
|
let onSettings: () -> Void
|
|
let onSecurity: () -> Void
|
|
let onWallet: () -> Void
|
|
let onBilling: () -> Void
|
|
let onSupport: () -> Void
|
|
let onSignOut: () -> Void
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
List {
|
|
Section {
|
|
HStack(spacing: 12) {
|
|
UserAvatar(name: name, photoURL: nil, size: 48)
|
|
VStack(alignment: .leading) {
|
|
Text(name ?? "Account")
|
|
.font(.ladill(size: 16, weight: .semibold))
|
|
Text(email ?? "")
|
|
.font(.ladill(size: 13))
|
|
.foregroundStyle(LadillColors.muted)
|
|
}
|
|
}
|
|
}
|
|
Section {
|
|
menuRow("Profile", icon: "person.fill", action: onProfile)
|
|
menuRow("Settings", icon: "gearshape.fill", action: onSettings)
|
|
menuRow("Security", icon: "lock.fill", action: onSecurity)
|
|
menuRow("Wallet", icon: "wallet.pass.fill", action: onWallet)
|
|
menuRow("Billing", icon: "creditcard.fill", action: onBilling)
|
|
menuRow("Support", icon: "questionmark.circle.fill", action: onSupport)
|
|
}
|
|
Section {
|
|
Button(role: .destructive, action: onSignOut) {
|
|
Label("Sign out", systemImage: "rectangle.portrait.and.arrow.right")
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Menu")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button("Done") { dismiss() }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func menuRow(_ title: String, icon: String, action: @escaping () -> Void) -> some View {
|
|
Button(action: action) {
|
|
Label(title, systemImage: icon)
|
|
}
|
|
}
|
|
}
|