refactor: notifications and settings view models; enhance login and registration UI

This commit is contained in:
2026-03-15 21:40:20 +07:00
parent 0947c048c1
commit 37b87ececd
45 changed files with 985 additions and 680 deletions
+9 -28
View File
@@ -4,6 +4,7 @@ import SwiftUI
@MainActor
class AuthViewModel: ObservableObject {
@Published var isAuthenticated = false
@Published var isCheckingAuth = true
@Published var currentUser: UserResponse?
@Published var isLoading = false
@Published var error: String?
@@ -12,39 +13,28 @@ class AuthViewModel: ObservableObject {
private let keychain = KeychainService.shared
func checkAuthStatus() async {
#if DEBUG
if PreviewData.isPreviewMode {
currentUser = PreviewData.mockUser
isAuthenticated = true
return
}
#endif
guard keychain.loadAccessToken() != nil else {
isAuthenticated = false
isCheckingAuth = false
return
}
isLoading = true
defer { isLoading = false }
do {
currentUser = try await auth.getMe()
isAuthenticated = true
isCheckingAuth = false
await requestPushIfNeeded()
} catch APIError.unauthorized {
isAuthenticated = false
isCheckingAuth = false
} catch {
isAuthenticated = false
// Network/transient errors keep authenticated if we already were
if !isAuthenticated {
isAuthenticated = false
}
isCheckingAuth = false
}
}
#if DEBUG
func enterPreviewMode() async {
PreviewData.isPreviewMode = true
currentUser = PreviewData.mockUser
isAuthenticated = true
await PreviewData.startMockLiveActivity()
}
#endif
func login(email: String, password: String) async {
isLoading = true
error = nil
@@ -84,15 +74,6 @@ class AuthViewModel: ObservableObject {
}
func logout() async {
#if DEBUG
if PreviewData.isPreviewMode {
await PreviewData.stopMockLiveActivity()
PreviewData.isPreviewMode = false
isAuthenticated = false
currentUser = nil
return
}
#endif
isLoading = true
defer { isLoading = false }
do {
+24 -29
View File
@@ -10,6 +10,7 @@ class NotificationsViewModel: ObservableObject {
@Published var isLoadingMore = false
@Published var error: String?
@Published var hasMore = true
private var hasLoadedOnce = false
private let service = NotificationsAPIService.shared
private let limit = 50
@@ -17,17 +18,13 @@ class NotificationsViewModel: ObservableObject {
private var pollingTask: Task<Void, Never>?
func load() async {
#if DEBUG
if PreviewData.isPreviewMode {
notifications = PreviewData.mockNotifications
hasMore = false
return
}
#endif
isLoading = true
isLoading = !hasLoadedOnce
error = nil
currentOffset = 0
defer { isLoading = false }
defer {
isLoading = false
hasLoadedOnce = true
}
do {
let page = try await service.getNotifications(limit: limit, offset: 0)
notifications = page.notifications
@@ -40,9 +37,6 @@ class NotificationsViewModel: ObservableObject {
}
func loadMore() async {
#if DEBUG
if PreviewData.isPreviewMode { return }
#endif
guard !isLoadingMore && hasMore else { return }
isLoadingMore = true
defer { isLoadingMore = false }
@@ -60,29 +54,33 @@ class NotificationsViewModel: ObservableObject {
func markAsRead(_ notification: AppNotification) async {
guard !notification.isRead else { return }
#if DEBUG
if PreviewData.isPreviewMode {
if let index = notifications.firstIndex(where: { $0.id == notification.id }) {
notifications[index] = notification.withReadAt(Date())
}
return
// Optimistic update reflect read state immediately so the list
// shows the correct card style even if the user navigates back
// before the API call completes.
if let index = notifications.firstIndex(where: { $0.id == notification.id }) {
notifications[index] = notification.withReadAt(Date())
unreadCount = max(0, unreadCount - 1)
updateBadge()
}
#endif
do {
try await service.markAsRead(id: notification.id)
if let index = notifications.firstIndex(where: { $0.id == notification.id }) {
notifications[index] = notification.withReadAt(Date())
}
updateBadge()
} catch is CancellationError {
// View disappeared before the request finished keep
// optimistic state; polling will reconcile if needed.
} catch {
// Rollback on real failure
if let index = notifications.firstIndex(where: { $0.id == notification.id }) {
notifications[index] = notification
unreadCount += 1
updateBadge()
}
self.error = error.localizedDescription
}
}
func markAllAsRead() async {
#if DEBUG
if PreviewData.isPreviewMode { return }
#endif
do {
try await service.markAllAsRead()
await load()
@@ -92,9 +90,6 @@ class NotificationsViewModel: ObservableObject {
}
func startPolling() {
#if DEBUG
if PreviewData.isPreviewMode { return }
#endif
guard pollingTask == nil else { return }
pollingTask = Task {
while !Task.isCancelled {
-18
View File
@@ -11,12 +11,6 @@ class SettingsViewModel: ObservableObject {
private let service = NotificationsAPIService.shared
func loadSessions() async {
#if DEBUG
if PreviewData.isPreviewMode {
sessions = PreviewData.mockSessions
return
}
#endif
isLoading = true
defer { isLoading = false }
do {
@@ -27,12 +21,6 @@ class SettingsViewModel: ObservableObject {
}
func deleteSession(_ session: SessionResponse) async {
#if DEBUG
if PreviewData.isPreviewMode {
sessions.removeAll { $0.id == session.id }
return
}
#endif
do {
try await service.deleteSession(id: session.id)
sessions.removeAll { $0.id == session.id }
@@ -42,12 +30,6 @@ class SettingsViewModel: ObservableObject {
}
func changePassword(current: String, new: String) async -> Bool {
#if DEBUG
if PreviewData.isPreviewMode {
successMessage = String(localized: "password_changed_success")
return true
}
#endif
isLoading = true
error = nil
defer { isLoading = false }