Files
ladill-mini/apps/ios/LadillMini/Features/Home/HomeView.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

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)
}
}
}