From a020aae2f3cc8e55ff20de85552e1d4c30cdc30c Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Mon, 5 Sep 2022 11:18:41 +0200 Subject: [PATCH] Implement messenger account deletion example --- .../AppFeature/AppEnvironment+Live.swift | 1 + .../Sources/AppFeature/AppFeature.swift | 2 +- .../Sources/HomeFeature/Alerts.swift | 22 ++++ .../Sources/HomeFeature/HomeFeature.swift | 48 +++++++- .../Sources/HomeFeature/HomeView.swift | 23 ++++ .../AppFeatureTests/AppFeatureTests.swift | 30 +++++ .../HomeFeatureTests/HomeFeatureTests.swift | 109 ++++++++++++++++++ 7 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 Examples/xx-messenger/Sources/HomeFeature/Alerts.swift diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index 3cbca1db..19f1b7f5 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -33,6 +33,7 @@ extension AppEnvironment { home: { HomeEnvironment( messenger: messenger, + db: dbManager.getDB, mainQueue: mainQueue, bgQueue: bgQueue, register: { diff --git a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift b/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift index 535d69fc..582478d4 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift @@ -68,7 +68,7 @@ extension AppEnvironment { let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, env in switch action { - case .start, .welcome(.finished), .restore(.finished): + case .start, .welcome(.finished), .restore(.finished), .home(.didDeleteAccount): state.screen = .loading return .run { subscriber in do { diff --git a/Examples/xx-messenger/Sources/HomeFeature/Alerts.swift b/Examples/xx-messenger/Sources/HomeFeature/Alerts.swift new file mode 100644 index 00000000..a3347be4 --- /dev/null +++ b/Examples/xx-messenger/Sources/HomeFeature/Alerts.swift @@ -0,0 +1,22 @@ +import ComposableArchitecture + +extension AlertState { + public static func confirmAccountDeletion() -> AlertState<HomeAction> { + AlertState<HomeAction>( + title: TextState("Delete Account"), + message: TextState("This will permanently delete your account and can't be undone."), + buttons: [ + .destructive(TextState("Delete"), action: .send(.deleteAccountConfirmed)), + .cancel(TextState("Cancel")) + ] + ) + } + + public static func accountDeletionFailed(_ error: Error) -> AlertState<HomeAction> { + AlertState<HomeAction>( + title: TextState("Error"), + message: TextState(error.localizedDescription), + buttons: [] + ) + } +} diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift index 5c20364d..279a5628 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift @@ -1,3 +1,4 @@ +import AppCore import Combine import ComposableArchitecture import ComposablePresentation @@ -9,18 +10,27 @@ import XXMessengerClient public struct HomeState: Equatable { public init( failure: String? = nil, - register: RegisterState? = nil + register: RegisterState? = nil, + alert: AlertState<HomeAction>? = nil, + isDeletingAccount: Bool = false ) { self.failure = failure self.register = register + self.alert = alert + self.isDeletingAccount = isDeletingAccount } @BindableState public var failure: String? @BindableState public var register: RegisterState? + @BindableState public var alert: AlertState<HomeAction>? + @BindableState public var isDeletingAccount: Bool } public enum HomeAction: Equatable, BindableAction { case start + case deleteAccountButtonTapped + case deleteAccountConfirmed + case didDeleteAccount case binding(BindingAction<HomeState>) case register(RegisterAction) } @@ -28,17 +38,20 @@ public enum HomeAction: Equatable, BindableAction { public struct HomeEnvironment { public init( messenger: Messenger, + db: DBManagerGetDB, mainQueue: AnySchedulerOf<DispatchQueue>, bgQueue: AnySchedulerOf<DispatchQueue>, register: @escaping () -> RegisterEnvironment ) { self.messenger = messenger + self.db = db self.mainQueue = mainQueue self.bgQueue = bgQueue self.register = register } public var messenger: Messenger + public var db: DBManagerGetDB public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> public var register: () -> RegisterEnvironment @@ -47,6 +60,7 @@ public struct HomeEnvironment { extension HomeEnvironment { public static let unimplemented = HomeEnvironment( messenger: .unimplemented, + db: .unimplemented, mainQueue: .unimplemented, bgQueue: .unimplemented, register: { .unimplemented } @@ -83,6 +97,38 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> .receive(on: env.mainQueue) .eraseToEffect() + case .deleteAccountButtonTapped: + state.alert = .confirmAccountDeletion() + return .none + + case .deleteAccountConfirmed: + state.isDeletingAccount = true + return .run { subscriber in + do { + let contactId = try env.messenger.e2e.tryGet().getContact().getId() + let contact = try env.db().fetchContacts(.init(id: [contactId])).first + if let username = contact?.username { + let ud = try env.messenger.ud.tryGet() + try ud.permanentDeleteAccount(username: Fact(fact: username, type: 0)) + } + try env.messenger.destroy() + try env.db().drop() + subscriber.send(.didDeleteAccount) + } catch { + subscriber.send(.set(\.$isDeletingAccount, false)) + subscriber.send(.set(\.$alert, .accountDeletionFailed(error))) + } + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .didDeleteAccount: + state.isDeletingAccount = false + return .none + case .register(.finished): state.register = nil return Effect(value: .start) diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift index 7dd8f668..f73f6b8a 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift @@ -12,9 +12,11 @@ public struct HomeView: View { struct ViewState: Equatable { var failure: String? + var isDeletingAccount: Bool init(state: HomeState) { failure = state.failure + isDeletingAccount = state.isDeletingAccount } } @@ -34,8 +36,29 @@ public struct HomeView: View { Text("Error") } } + + Section { + Button(role: .destructive) { + viewStore.send(.deleteAccountButtonTapped) + } label: { + HStack { + Text("Delete Account") + Spacer() + if viewStore.isDeletingAccount { + ProgressView() + } + } + } + .disabled(viewStore.isDeletingAccount) + } header: { + Text("Account") + } } .navigationTitle("Home") + .alert( + store.scope(state: \.alert), + dismiss: HomeAction.set(\.$alert, nil) + ) } .navigationViewStyle(.stack) .task { viewStore.send(.start) } diff --git a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift b/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift index 5f055492..5ea96813 100644 --- a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift +++ b/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift @@ -141,6 +141,36 @@ final class AppFeatureTests: XCTestCase { } } + func testHomeDidDeleteAccount() { + let store = TestStore( + initialState: AppState( + screen: .home(HomeState()) + ), + reducer: appReducer, + environment: .unimplemented + ) + + let mainQueue = DispatchQueue.test + let bgQueue = DispatchQueue.test + + store.environment.mainQueue = mainQueue.eraseToAnyScheduler() + store.environment.bgQueue = bgQueue.eraseToAnyScheduler() + store.environment.dbManager.hasDB.run = { true } + store.environment.messenger.isLoaded.run = { false } + store.environment.messenger.isCreated.run = { false } + + store.send(.home(.didDeleteAccount)) { + $0.screen = .loading + } + + bgQueue.advance() + mainQueue.advance() + + store.receive(.set(\.$screen, .welcome(WelcomeState()))) { + $0.screen = .welcome(WelcomeState()) + } + } + func testWelcomeRestoreTapped() { let store = TestStore( initialState: AppState( diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift index ddb6b035..4fbad5b8 100644 --- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift +++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift @@ -3,6 +3,7 @@ import RegisterFeature import XCTest import XXClient import XXMessengerClient +import XXModels @testable import HomeFeature final class HomeFeatureTests: XCTestCase { @@ -201,4 +202,112 @@ final class HomeFeatureTests: XCTestCase { $0.failure = error.localizedDescription } } + + func testAccountDeletion() { + let store = TestStore( + initialState: HomeState(), + reducer: homeReducer, + environment: .unimplemented + ) + + var dbDidFetchContacts: [XXModels.Contact.Query] = [] + var udDidPermanentDeleteAccount: [Fact] = [] + var messengerDidDestroy = 0 + var dbDidDrop = 0 + + store.environment.bgQueue = .immediate + store.environment.mainQueue = .immediate + store.environment.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getContact.run = { + var contact = Contact.unimplemented("contact-data".data(using: .utf8)!) + contact.getIdFromContact.run = { _ in "contact-id".data(using: .utf8)! } + return contact + } + return e2e + } + store.environment.db.run = { + var db: Database = .failing + db.fetchContacts.run = { query in + dbDidFetchContacts.append(query) + return [ + XXModels.Contact( + id: "contact-id".data(using: .utf8)!, + marshaled: "contact-data".data(using: .utf8)!, + username: "MyUsername" + ) + ] + } + db.drop.run = { + dbDidDrop += 1 + } + return db + } + store.environment.messenger.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.permanentDeleteAccount.run = { usernameFact in + udDidPermanentDeleteAccount.append(usernameFact) + } + return ud + } + store.environment.messenger.destroy.run = { + messengerDidDestroy += 1 + } + + store.send(.deleteAccountButtonTapped) { + $0.alert = .confirmAccountDeletion() + } + + store.send(.set(\.$alert, nil)) { + $0.alert = nil + } + + store.send(.deleteAccountConfirmed) { + $0.isDeletingAccount = true + } + + XCTAssertNoDifference(dbDidFetchContacts, [.init(id: ["contact-id".data(using: .utf8)!])]) + XCTAssertNoDifference(udDidPermanentDeleteAccount, [Fact(fact: "MyUsername", type: 0)]) + XCTAssertNoDifference(messengerDidDestroy, 1) + XCTAssertNoDifference(dbDidDrop, 1) + + store.receive(.didDeleteAccount) { + $0.isDeletingAccount = false + } + } + + func testAccountDeletionFailure() { + let store = TestStore( + initialState: HomeState(), + reducer: homeReducer, + environment: .unimplemented + ) + + struct Failure: Error {} + let error = Failure() + + store.environment.bgQueue = .immediate + store.environment.mainQueue = .immediate + store.environment.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getContact.run = { + var contact = Contact.unimplemented("contact-data".data(using: .utf8)!) + contact.getIdFromContact.run = { _ in throw error } + return contact + } + return e2e + } + + store.send(.deleteAccountConfirmed) { + $0.isDeletingAccount = true + } + + store.receive(.set(\.$isDeletingAccount, false)) { + $0.isDeletingAccount = false + } + + store.receive(.set(\.$alert, .accountDeletionFailed(error))) { + $0.alert = .accountDeletionFailed(error) + } + } } -- GitLab