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