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

112 lines
4.1 KiB
Swift

import SwiftUI
@MainActor
final class NotificationsViewModel: ObservableObject {
@Published var notifications: [AppNotification] = []
@Published var unreadCount = 0
@Published var loading = false
@Published var isRefreshing = false
@Published var error: String?
func load() async {
loading = notifications.isEmpty
error = nil
defer { loading = false }
do {
let response = try await APIClient.shared.notifications()
notifications = response.data
unreadCount = response.unreadCount ?? 0
} catch {
self.error = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
}
}
func refresh() async {
isRefreshing = true
defer { isRefreshing = false }
await load()
}
func markAllRead() async {
try? await APIClient.shared.markAllNotificationsRead()
await load()
}
func markRead(_ notification: AppNotification) async {
guard notification.readAt == nil else { return }
try? await APIClient.shared.markNotificationRead(id: notification.id)
await load()
}
}
struct NotificationsSheet: View {
let onDismiss: () -> Void
let onUnreadChanged: (Int) -> Void
@StateObject private var vm = NotificationsViewModel()
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationStack {
Group {
if vm.loading {
LoadingView()
} else if let error = vm.error {
ErrorView(message: error) { Task { await vm.load() } }
} else if vm.notifications.isEmpty {
VStack(spacing: 12) {
Image(systemName: "bell.slash")
.font(.system(size: 40))
.foregroundStyle(LadillColors.muted)
Text("You're all caught up")
.font(.ladill(size: 16, weight: .semibold))
Text("Payment alerts and account updates will show up here.")
.font(.ladill(size: 14))
.foregroundStyle(LadillColors.muted)
.multilineTextAlignment(.center)
}
.padding(32)
} else {
List(vm.notifications) { notification in
VStack(alignment: .leading, spacing: 4) {
Text(notification.title)
.font(.ladill(size: 15, weight: notification.isRead ? .regular : .semibold))
Text(notification.message)
.font(.ladill(size: 13))
.foregroundStyle(LadillColors.muted)
if let date = notification.createdAt {
Text(Formatters.date(iso: date))
.font(.ladill(size: 11))
.foregroundStyle(LadillColors.subtle)
}
}
.opacity(notification.isRead ? 0.7 : 1)
.onTapGesture { Task { await vm.markRead(notification) } }
}
.listStyle(.plain)
}
}
.background(LadillColors.page)
.navigationTitle("Notifications")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
if vm.unreadCount > 0 {
Button("Mark all read") { Task { await vm.markAllRead() } }
}
}
ToolbarItem(placement: .topBarTrailing) {
Button("Done") {
onUnreadChanged(vm.unreadCount)
dismiss()
onDismiss()
}
}
}
.refreshable { await vm.refresh() }
.task { await vm.load() }
}
.presentationDetents([.medium, .large])
}
}