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