diff --git a/apple/OmnivoreKit/Sources/App/Views/DebugMenuView.swift b/apple/OmnivoreKit/Sources/App/Views/DebugMenuView.swift index 6b6f83eae..abff9974b 100644 --- a/apple/OmnivoreKit/Sources/App/Views/DebugMenuView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/DebugMenuView.swift @@ -26,7 +26,7 @@ struct DebugMenuView: View { Button( action: { - authenticator.logout() + authenticator.logout(dataService: dataService) dataService.switchAppEnvironment(appEnvironment: selectedEnvironment) }, label: { Text("Apply Changes") } diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ManageAccountView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ManageAccountView.swift new file mode 100644 index 000000000..940c5059c --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ManageAccountView.swift @@ -0,0 +1,61 @@ +import Services +import SwiftUI +import Views + +struct ManageAccountView: View { + @EnvironmentObject var authenticator: Authenticator + @EnvironmentObject var dataService: DataService + + @State private var showDeleteAccountConfirmation = false + @StateObject private var viewModel = ProfileContainerViewModel() + + var body: some View { + #if os(iOS) + Form { + innerBody + } + #elseif os(macOS) + List { + innerBody + } + .listStyle(InsetListStyle()) + #endif + } + + var innerBody: some View { + Group { + Section { + ProfileCard(data: viewModel.profileCardData) + .task { + await viewModel.loadProfileData(dataService: dataService) + } + } + Section { + Button( + action: { + showDeleteAccountConfirmation = true + }, + label: { Text("Delete Account") } + ) + .alert(isPresented: $showDeleteAccountConfirmation) { + Alert( + title: Text("Are you sure you want to delete your account? This action can't be undone."), + primaryButton: .destructive(Text("Delete Account")) { + Task { + await viewModel.deleteAccount(dataService: dataService, authenticator: authenticator) + } + }, + secondaryButton: .cancel() + ) + } + } + + if let errorMessage = viewModel.deleteAccountErrorMessage { + Text(errorMessage) + .font(.appBody) + .foregroundColor(.red) + .multilineTextAlignment(.leading) + } + } + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index 2d4eef29d..8a5c1688c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -7,6 +7,7 @@ import Views @MainActor final class ProfileContainerViewModel: ObservableObject { @Published var isLoading = false @Published var profileCardData = ProfileCardData() + @Published var deleteAccountErrorMessage: String? var appVersionString: String { if let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String { @@ -31,6 +32,20 @@ import Views } } + func deleteAccount(dataService: DataService, authenticator: Authenticator) async { + guard let currentViewer = dataService.currentViewer else { + deleteAccountErrorMessage = "Unable to load account information." + return + } + + do { + try await dataService.deleteAccount(userID: currentViewer.unwrappedUserID) + authenticator.logout(dataService: dataService) + } catch { + deleteAccountErrorMessage = "We were unable to delete your account." + } + } + private func loadProfileCardData(viewer: Viewer) { profileCardData = ProfileCardData( name: viewer.unwrappedName, @@ -106,14 +121,10 @@ struct ProfileView: View { } Section(footer: Text(viewModel.appVersionString)) { - if FeatureFlag.showAccountDeletion { - NavigationLink( - destination: ManageAccountView(handleAccountDeletion: { - print("delete account") - }) - ) { - Text("Manage Account") - } + NavigationLink( + destination: ManageAccountView() + ) { + Text("Manage Account") } Text("Logout") @@ -124,7 +135,7 @@ struct ProfileView: View { Alert( title: Text("Are you sure you want to logout?"), primaryButton: .destructive(Text("Confirm")) { - authenticator.logout() + authenticator.logout(dataService: dataService) }, secondaryButton: .cancel() ) diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift index 9acf9a501..033f6f5e4 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift @@ -27,7 +27,7 @@ public final class RootViewModel: ObservableObject { #if DEBUG if CommandLine.arguments.contains("--uitesting") { - services.authenticator.logout() + services.authenticator.logout(dataService: services.dataService) } #endif } diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift b/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift index cd2f0c66b..432508e68 100644 --- a/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift +++ b/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift @@ -36,7 +36,8 @@ public final class Authenticator: ObservableObject { ValetKey.authToken.value() } - public func logout() { + public func logout(dataService: DataService) { + dataService.resetCoreData() clearCreds() Authenticator.unregisterIntercomUser?() isLoggedIn = false diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index 078a6ffea..483cb3521 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -95,7 +95,7 @@ public final class DataService: ObservableObject { } } - private func resetCoreData() { + func resetCoreData() { clearCoreData() persistentContainer = PersistentContainer.make() diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/DeleteAccount.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/DeleteAccount.swift new file mode 100644 index 000000000..197de9219 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/DeleteAccount.swift @@ -0,0 +1,46 @@ +import Foundation +import Models +import SwiftGraphQL + +public extension DataService { + func deleteAccount(userID: String) async throws { + enum MutationResult { + case success(id: String) + case error(errorMessage: String) + } + + let selection = Selection { + try $0.on( + deleteAccountError: .init { + .error(errorMessage: (try $0.errorCodes().first ?? .forbidden).rawValue) + }, + deleteAccountSuccess: .init { + .success(id: try $0.userId()) + } + ) + } + + let mutation = Selection.Mutation { + try $0.deleteAccount(userId: userID, selection: selection) + } + + let path = appEnvironment.graphqlPath + let headers = networker.defaultHeaders + + return try await withCheckedThrowingContinuation { continuation in + send(mutation, to: path, headers: headers) { mutationResult in + guard let payload = try? mutationResult.get() else { + continuation.resume(throwing: BasicError.message(messageText: "failed to delete user")) + return + } + + switch payload.data { + case .success: + continuation.resume() + case .error: + continuation.resume(throwing: BasicError.message(messageText: "failed to delete user")) + } + } + } + } +} diff --git a/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift b/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift index 5642e89eb..5c0aab9af 100644 --- a/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift +++ b/apple/OmnivoreKit/Sources/Utils/FeatureFlags.swift @@ -7,7 +7,6 @@ import Foundation #endif public enum FeatureFlag { - public static let showAccountDeletion = false public static let enableSnoozeFromShareExtension = false public static let enableRemindersFromShareExtension = false public static let enableReadNow = false diff --git a/apple/OmnivoreKit/Sources/Views/UserSettings/ManageAccountView.swift b/apple/OmnivoreKit/Sources/Views/UserSettings/ManageAccountView.swift deleted file mode 100644 index 1b6d589ca..000000000 --- a/apple/OmnivoreKit/Sources/Views/UserSettings/ManageAccountView.swift +++ /dev/null @@ -1,28 +0,0 @@ -import SwiftUI - -public struct ManageAccountView: View { - let handleAccountDeletion: () -> Void - @State private var showDeleteAccountConfirmation = false - - public init(handleAccountDeletion: @escaping () -> Void) { - self.handleAccountDeletion = handleAccountDeletion - } - - public var body: some View { - Button( - action: { - showDeleteAccountConfirmation = true - }, - label: { Text("Delete my account") } - ) - .alert(isPresented: $showDeleteAccountConfirmation) { - Alert( - title: Text("Are you sure you want to delete your account? This action can't be undone."), - primaryButton: .destructive(Text("Delete Account")) { - handleAccountDeletion() - }, - secondaryButton: .cancel() - ) - } - } -}