diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/PushNotificationDevicesView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/PushNotificationDevicesView.swift new file mode 100644 index 000000000..00c90617c --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/PushNotificationDevicesView.swift @@ -0,0 +1,82 @@ + +import Models +import Services +import SwiftUI +import Utils +import Views + +@MainActor final class PushNotificationDevicesViewModel: ObservableObject { + @Published var isLoading = false + @Published var devices = [InternalDeviceToken]() + + func loadDevices(dataService: DataService) { + isLoading = true + Task { + self.devices = (try? await dataService.devices()) ?? [] + isLoading = false + } + } + + func removeToken(dataService: DataService, tokenID: String) { + if let idx = devices.firstIndex(where: { $0.id == tokenID }) { + Task { + try await dataService.syncDeviceToken(deviceTokenOperation: .deleteToken(tokenID: tokenID)) + devices.remove(at: idx) + } + } + } +} + +struct PushNotificationDevicesView: View { + @EnvironmentObject var dataService: DataService + @StateObject var viewModel = PushNotificationDevicesViewModel() + + var body: some View { + Group { + #if os(iOS) + Form { + innerBody + } + #elseif os(macOS) + List { + innerBody + } + .listStyle(InsetListStyle()) + #endif + } + .task { viewModel.loadDevices(dataService: dataService) } + } + + func createdStr(_ device: InternalDeviceToken) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .short + dateFormatter.timeStyle = .short + + if let createdAt = device.createdAt { + return dateFormatter.string(from: createdAt) + } + return "" + } + + private var innerBody: some View { + List { + Section(header: Text("Registered device tokens (swipe to remove)")) { + ForEach(viewModel.devices) { device in + Text("Created: \(createdStr(device))") + .swipeActions(edge: .trailing) { + Button( + role: .destructive, + action: { + viewModel.removeToken(dataService: dataService, tokenID: device.id) + }, + label: { + Image(systemName: "trash") + } + ) + } + } + } + } + .navigationTitle("Devices") + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/PushNotificationSettings.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/PushNotificationSettingsView.swift similarity index 91% rename from apple/OmnivoreKit/Sources/App/Views/Profile/PushNotificationSettings.swift rename to apple/OmnivoreKit/Sources/App/Views/Profile/PushNotificationSettingsView.swift index 210974b0e..8cbd83784 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/PushNotificationSettings.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/PushNotificationSettingsView.swift @@ -28,7 +28,8 @@ import Views self.desiredNotificationsEnabled = granted Task { if let savedToken = UserDefaults.standard.string(forKey: UserDefaultKey.firebasePushToken.rawValue) { - try? await dataService.syncDeviceToken(deviceTokenOperation: DeviceTokenOperation.addToken(token: savedToken)) + try? await dataService.syncDeviceToken( + deviceTokenOperation: DeviceTokenOperation.addToken(token: savedToken)) } NotificationCenter.default.post(name: Notification.Name("ReconfigurePushNotifications"), object: nil) } @@ -83,6 +84,12 @@ struct PushNotificationSettingsView: View { """) .accentColor(.blue) } + + Section { + NavigationLink("Devices") { + PushNotificationDevicesView() + } + } } .navigationTitle("Push Notifications") } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/Devices.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/Devices.swift new file mode 100644 index 000000000..cd9732466 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/Devices.swift @@ -0,0 +1,55 @@ +import CoreData +import Foundation +import Models +import SwiftGraphQL + +public extension DataService { + func devices() async throws -> [InternalDeviceToken] { + enum QueryResult { + case success(result: [InternalDeviceToken]) + case error(error: String) + } + + let deviceTokensSelection = Selection.DeviceToken { + InternalDeviceToken( + id: try $0.id(), + createdAt: try $0.createdAt().value + ) + } + + let selection = Selection { + try $0.on( + deviceTokensError: .init { + QueryResult.error(error: try $0.errorCodes().description) + }, + deviceTokensSuccess: .init { + QueryResult.success(result: try $0.deviceTokens(selection: deviceTokensSelection.list)) + } + ) + } + + let query = Selection.Query { + try $0.deviceTokens(selection: selection) + } + + let path = appEnvironment.graphqlPath + let headers = networker.defaultHeaders + let context = backgroundContext + + return try await withCheckedThrowingContinuation { continuation in + send(query, to: path, headers: headers) { queryResult in + guard let payload = try? queryResult.get() else { + continuation.resume(throwing: BasicError.message(messageText: "network request failed")) + return + } + + switch payload.data { + case let .success(result: result): + continuation.resume(returning: result) + case .error: + continuation.resume(throwing: BasicError.message(messageText: "DeviceToken Email fetch error")) + } + } + } + } +} diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalDeviceToken.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalDeviceToken.swift new file mode 100644 index 000000000..0b178c860 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalDeviceToken.swift @@ -0,0 +1,8 @@ +import CoreData +import Foundation +import Models + +public struct InternalDeviceToken: Identifiable { + public let id: String + public let createdAt: Date? +}