From e318afbc0a65854cd5d33e0427042e92948664ce Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 13 Dec 2023 14:42:46 +0800 Subject: [PATCH] Better account deletion / logout handling of local data --- .../Sources/App/Views/DeleteAccountView.swift | 24 ++++++++++++++ .../Sources/App/Views/LogoutView.swift | 33 +++++++++++++++++++ .../App/Views/Profile/ProfileView.swift | 2 +- .../Sources/App/Views/RootView/RootView.swift | 10 ++++-- .../Authentication/Authenticator.swift | 16 ++++++--- .../Services/DataService/DataService.swift | 20 +++++++++++ 6 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/Views/DeleteAccountView.swift create mode 100644 apple/OmnivoreKit/Sources/App/Views/LogoutView.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/DeleteAccountView.swift b/apple/OmnivoreKit/Sources/App/Views/DeleteAccountView.swift new file mode 100644 index 000000000..0d27b8c37 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/DeleteAccountView.swift @@ -0,0 +1,24 @@ +import Models +import Services +import SwiftUI +import Utils +import Views + +struct DeleteAccountView: View { + @EnvironmentObject var dataService: DataService + @EnvironmentObject var authenticator: Authenticator + + public var body: some View { + VStack(alignment: .center) { + Text("Deleting account...") + ProgressView() + .frame(maxWidth: .infinity) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .task { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) { + authenticator.logout(dataService: dataService, isAccountDeletion: true) + } + } + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/LogoutView.swift b/apple/OmnivoreKit/Sources/App/Views/LogoutView.swift new file mode 100644 index 000000000..1ebd97ee1 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/LogoutView.swift @@ -0,0 +1,33 @@ +import Models +import Services +import SwiftUI +import Utils +import Views + +struct LogoutView: View { + @EnvironmentObject var dataService: DataService + @EnvironmentObject var authenticator: Authenticator + @Environment(\.openURL) var openURL + + let deletedAccountConfirmationMessage = "Your account has been deleted. Additional steps may be needed if Sign in with Apple was used to register." + + public var body: some View { + VStack(alignment: .center) { + Text("Logging out...") + ProgressView() + .frame(maxWidth: .infinity) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .task { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) { + authenticator.logout(dataService: dataService) + } + } + .alert(deletedAccountConfirmationMessage, isPresented: $authenticator.showAppleRevokeTokenAlert) { + Button("View Details") { + openURL(URL(string: "https://support.apple.com/en-us/HT210426")!) + } + Button(LocalText.dismissButton) { self.authenticator.showAppleRevokeTokenAlert = false } + } + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index c489f8a14..732b543dd 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -196,7 +196,7 @@ struct ProfileView: View { primaryButton: .destructive(Text(LocalText.genericConfirm)) { dismiss() DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { - authenticator.logout(dataService: dataService) + authenticator.beginLogout() } }, secondaryButton: .cancel() diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift index 144c374e0..7f2c9c461 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift @@ -52,9 +52,13 @@ struct InnerRootView: View { if authenticator.isLoggedIn { PrimaryContentView() } else { - WelcomeView() - .accessibilityElement() - .accessibilityIdentifier("welcomeView") + if authenticator.isLoggingOut { + LogoutView() + } else { + WelcomeView() + .accessibilityElement() + .accessibilityIdentifier("welcomeView") + } } } diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift b/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift index 6ef7df88a..c639057d9 100644 --- a/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift +++ b/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift @@ -1,3 +1,4 @@ +import CoreData import Foundation import GoogleSignIn import Models @@ -18,6 +19,7 @@ public final class Authenticator: ObservableObject { } @Published public internal(set) var isLoggedIn: Bool + @Published public internal(set) var isLoggingOut = false @Published public var showAppleRevokeTokenAlert = false let networker: Networker @@ -37,15 +39,21 @@ public final class Authenticator: ObservableObject { ValetKey.authToken.value() } + public func beginLogout() { + isLoggingOut = true + isLoggedIn = false + } + public func logout(dataService: DataService, isAccountDeletion: Bool = false) { + dataService.resetLocalStorage() + clearCreds() Authenticator.unregisterIntercomUser?() - isLoggedIn = false showAppleRevokeTokenAlert = isAccountDeletion EventTracker.reset() - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) { - dataService.resetLocalStorage() - } + + isLoggedIn = false + isLoggingOut = false } public func clearCreds() { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index dc8e30de7..21d80df65 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -128,6 +128,17 @@ public final class DataService: ObservableObject { } } + func deleteAllEntities(entityName: String, inContext context: NSManagedObjectContext) { + let deleteFetch = NSFetchRequest(entityName: entityName) + let deleteRequest = NSBatchDeleteRequest(fetchRequest: deleteFetch) + do { + try context.execute(deleteRequest) + try context.save() + } catch { + print("Error deleting all \(entityName) items.", error) + } + } + private func clearDownloadedFiles() { let relevantTypes = ["pdf", "mp3", "speechMarks"] let fileMgr = FileManager() @@ -176,6 +187,15 @@ public final class DataService: ObservableObject { } public func resetLocalStorage() { + viewContext.perform { + // We want to specify the order of deleting items to better handle relationships + let entities = ["LibraryItem", "Viewer", "Filter", "Highlight", "NewsletterEmail", + "LinkedItemLabel", "RecentSearchItem", "Recommendation", "RecommendationGroup", "UserProfile"] + entities.forEach { entityName in + self.deleteAllEntities(entityName: entityName, inContext: self.viewContext) + } + } + lastItemSyncTime = Date(timeIntervalSinceReferenceDate: 0) clearCoreData()