From 726aab7e32844079f39ccdb0bbb301d30dfa70be Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 13:43:51 +0200
Subject: [PATCH] Implement facts unregistration

---
 .../MyContactFeature/MyContactFeature.swift   |  52 +++++-
 .../MyContactFeature/MyContactView.swift      |  22 ++-
 .../MyContactFeatureTests.swift               | 176 +++++++++++++++++-
 3 files changed, 242 insertions(+), 8 deletions(-)

diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
index 99486c8a..b25d0212 100644
--- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
+++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
@@ -23,11 +23,13 @@ public struct MyContactState: Equatable {
     emailConfirmationCode: String = "",
     isRegisteringEmail: Bool = false,
     isConfirmingEmail: Bool = false,
+    isUnregisteringEmail: Bool = false,
     phone: String = "",
     phoneConfirmationID: String? = nil,
     phoneConfirmationCode: String = "",
     isRegisteringPhone: Bool = false,
     isConfirmingPhone: Bool = false,
+    isUnregisteringPhone: Bool = false,
     isLoadingFacts: Bool = false,
     alert: AlertState<MyContactAction>? = nil
   ) {
@@ -38,11 +40,13 @@ public struct MyContactState: Equatable {
     self.emailConfirmationCode = emailConfirmationCode
     self.isRegisteringEmail = isRegisteringEmail
     self.isConfirmingEmail = isConfirmingEmail
+    self.isUnregisteringEmail = isUnregisteringEmail
     self.phone = phone
     self.phoneConfirmationID = phoneConfirmationID
     self.phoneConfirmationCode = phoneConfirmationCode
     self.isRegisteringPhone = isRegisteringPhone
     self.isConfirmingPhone = isConfirmingPhone
+    self.isUnregisteringPhone = isUnregisteringPhone
     self.isLoadingFacts = isLoadingFacts
     self.alert = alert
   }
@@ -54,11 +58,13 @@ public struct MyContactState: Equatable {
   @BindableState public var emailConfirmationCode: String
   @BindableState public var isRegisteringEmail: Bool
   @BindableState public var isConfirmingEmail: Bool
+  @BindableState public var isUnregisteringEmail: Bool
   @BindableState public var phone: String
   @BindableState public var phoneConfirmationID: String?
   @BindableState public var phoneConfirmationCode: String
   @BindableState public var isRegisteringPhone: Bool
   @BindableState public var isConfirmingPhone: Bool
+  @BindableState public var isUnregisteringPhone: Bool
   @BindableState public var isLoadingFacts: Bool
   public var alert: AlertState<MyContactAction>?
 }
@@ -178,7 +184,28 @@ public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContact
     .eraseToEffect()
 
   case .unregisterEmailTapped:
-    return .none
+    guard let email = state.contact?.email else { return .none }
+    state.isUnregisteringEmail = true
+    return Effect.run { [state] subscriber in
+      do {
+        let ud: UserDiscovery = try env.messenger.ud.tryGet()
+        let fact = Fact(type: .email, value: email)
+        try ud.removeFact(fact)
+        let contactId = try env.messenger.e2e.tryGet().getContact().getId()
+        if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first {
+          dbContact.email = nil
+          try env.db().saveContact(dbContact)
+        }
+      } catch {
+        subscriber.send(.didFail(error.localizedDescription))
+      }
+      subscriber.send(.set(\.$isUnregisteringEmail, false))
+      subscriber.send(completion: .finished)
+      return AnyCancellable {}
+    }
+    .subscribe(on: env.bgQueue)
+    .receive(on: env.mainQueue)
+    .eraseToEffect()
 
   case .registerPhoneTapped:
     state.focusedField = nil
@@ -228,7 +255,28 @@ public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContact
     .eraseToEffect()
 
   case .unregisterPhoneTapped:
-    return .none
+    guard let phone = state.contact?.phone else { return .none }
+    state.isUnregisteringPhone = true
+    return Effect.run { [state] subscriber in
+      do {
+        let ud: UserDiscovery = try env.messenger.ud.tryGet()
+        let fact = Fact(type: .phone, value: phone)
+        try ud.removeFact(fact)
+        let contactId = try env.messenger.e2e.tryGet().getContact().getId()
+        if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first {
+          dbContact.phone = nil
+          try env.db().saveContact(dbContact)
+        }
+      } catch {
+        subscriber.send(.didFail(error.localizedDescription))
+      }
+      subscriber.send(.set(\.$isUnregisteringPhone, false))
+      subscriber.send(completion: .finished)
+      return AnyCancellable {}
+    }
+    .subscribe(on: env.bgQueue)
+    .receive(on: env.mainQueue)
+    .eraseToEffect()
 
   case .loadFactsTapped:
     state.isLoadingFacts = true
diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
index 3f65ba80..f32af242 100644
--- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
+++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
@@ -19,11 +19,13 @@ public struct MyContactView: View {
       emailCode = state.emailConfirmationCode
       isRegisteringEmail = state.isRegisteringEmail
       isConfirmingEmail = state.isConfirmingEmail
+      isUnregisteringEmail = state.isUnregisteringEmail
       phone = state.phone
       phoneConfirmation = state.phoneConfirmationID != nil
       phoneCode = state.phoneConfirmationCode
       isRegisteringPhone = state.isRegisteringPhone
       isConfirmingPhone = state.isConfirmingPhone
+      isUnregisteringPhone = state.isUnregisteringPhone
       isLoadingFacts = state.isLoadingFacts
     }
 
@@ -34,11 +36,13 @@ public struct MyContactView: View {
     var emailCode: String
     var isRegisteringEmail: Bool
     var isConfirmingEmail: Bool
+    var isUnregisteringEmail: Bool
     var phone: String
     var phoneConfirmation: Bool
     var phoneCode: String
     var isRegisteringPhone: Bool
     var isConfirmingPhone: Bool
+    var isUnregisteringPhone: Bool
     var isLoadingFacts: Bool
   }
 
@@ -58,8 +62,15 @@ public struct MyContactView: View {
               Button(role: .destructive) {
                 viewStore.send(.unregisterEmailTapped)
               } label: {
-                Text("Unregister")
+                HStack {
+                  Text("Unregister")
+                  Spacer()
+                  if viewStore.isUnregisteringEmail {
+                    ProgressView()
+                  }
+                }
               }
+              .disabled(viewStore.isUnregisteringEmail)
             } else {
               TextField(
                 text: viewStore.binding(
@@ -127,8 +138,15 @@ public struct MyContactView: View {
               Button(role: .destructive) {
                 viewStore.send(.unregisterPhoneTapped)
               } label: {
-                Text("Unregister")
+                HStack {
+                  Text("Unregister")
+                  Spacer()
+                  if viewStore.isUnregisteringPhone {
+                    ProgressView()
+                  }
+                }
               }
+              .disabled(viewStore.isUnregisteringPhone)
             } else {
               TextField(
                 text: viewStore.binding(
diff --git a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
index a4f25954..8d5fac94 100644
--- a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
@@ -257,13 +257,97 @@ final class MyContactFeatureTests: XCTestCase {
   }
 
   func testUnregisterEmail() {
+    let contactID = "contact-id".data(using: .utf8)!
+    let email = "test@email.com"
+    let dbContact = XXModels.Contact(id: contactID, email: email)
+
+    var didRemoveFact: [Fact] = []
+    var didFetchContacts: [XXModels.Contact.Query] = []
+    var didSaveContact: [XXModels.Contact] = []
+
     let store = TestStore(
-      initialState: MyContactState(),
+      initialState: MyContactState(
+        contact: dbContact
+      ),
+      reducer: myContactReducer,
+      environment: .unimplemented
+    )
+
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.removeFact.run = { didRemoveFact.append($0) }
+      return ud
+    }
+    store.environment.messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in contactID }
+        return contact
+      }
+      return e2e
+    }
+    store.environment.db.run = {
+      var db: Database = .failing
+      db.fetchContacts.run = { query in
+        didFetchContacts.append(query)
+        return [dbContact]
+      }
+      db.saveContact.run = { contact in
+        didSaveContact.append(contact)
+        return contact
+      }
+      return db
+    }
+
+    store.send(.unregisterEmailTapped) {
+      $0.isUnregisteringEmail = true
+    }
+
+    XCTAssertNoDifference(didRemoveFact, [.init(type: .email, value: email)])
+    XCTAssertNoDifference(didFetchContacts, [.init(id: [contactID])])
+    var expectedSavedContact = dbContact
+    expectedSavedContact.email = nil
+    XCTAssertNoDifference(didSaveContact, [expectedSavedContact])
+
+    store.receive(.set(\.$isUnregisteringEmail, false)) {
+      $0.isUnregisteringEmail = false
+    }
+  }
+
+  func testUnregisterEmailFailure() {
+    struct Failure: Error {}
+    let failure = Failure()
+
+    let store = TestStore(
+      initialState: MyContactState(
+        contact: .init(id: Data(), email: "test@email.com")
+      ),
       reducer: myContactReducer,
       environment: .unimplemented
     )
 
-    store.send(.unregisterEmailTapped)
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.removeFact.run = { _ in throw failure }
+      return ud
+    }
+
+    store.send(.unregisterEmailTapped) {
+      $0.isUnregisteringEmail = true
+    }
+
+    store.receive(.didFail(failure.localizedDescription)) {
+      $0.alert = .error(failure.localizedDescription)
+    }
+
+    store.receive(.set(\.$isUnregisteringEmail, false)) {
+      $0.isUnregisteringEmail = false
+    }
   }
 
   func testRegisterPhone() {
@@ -465,13 +549,97 @@ final class MyContactFeatureTests: XCTestCase {
   }
   
   func testUnregisterPhone() {
+    let contactID = "contact-id".data(using: .utf8)!
+    let phone = "+123456789"
+    let dbContact = XXModels.Contact(id: contactID, phone: phone)
+
+    var didRemoveFact: [Fact] = []
+    var didFetchContacts: [XXModels.Contact.Query] = []
+    var didSaveContact: [XXModels.Contact] = []
+
     let store = TestStore(
-      initialState: MyContactState(),
+      initialState: MyContactState(
+        contact: dbContact
+      ),
+      reducer: myContactReducer,
+      environment: .unimplemented
+    )
+
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.removeFact.run = { didRemoveFact.append($0) }
+      return ud
+    }
+    store.environment.messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in contactID }
+        return contact
+      }
+      return e2e
+    }
+    store.environment.db.run = {
+      var db: Database = .failing
+      db.fetchContacts.run = { query in
+        didFetchContacts.append(query)
+        return [dbContact]
+      }
+      db.saveContact.run = { contact in
+        didSaveContact.append(contact)
+        return contact
+      }
+      return db
+    }
+
+    store.send(.unregisterPhoneTapped) {
+      $0.isUnregisteringPhone = true
+    }
+
+    XCTAssertNoDifference(didRemoveFact, [.init(type: .phone, value: phone)])
+    XCTAssertNoDifference(didFetchContacts, [.init(id: [contactID])])
+    var expectedSavedContact = dbContact
+    expectedSavedContact.phone = nil
+    XCTAssertNoDifference(didSaveContact, [expectedSavedContact])
+
+    store.receive(.set(\.$isUnregisteringPhone, false)) {
+      $0.isUnregisteringPhone = false
+    }
+  }
+
+  func testUnregisterPhoneFailure() {
+    struct Failure: Error {}
+    let failure = Failure()
+
+    let store = TestStore(
+      initialState: MyContactState(
+        contact: .init(id: Data(), phone: "+123456789")
+      ),
       reducer: myContactReducer,
       environment: .unimplemented
     )
 
-    store.send(.unregisterPhoneTapped)
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.removeFact.run = { _ in throw failure }
+      return ud
+    }
+
+    store.send(.unregisterPhoneTapped) {
+      $0.isUnregisteringPhone = true
+    }
+
+    store.receive(.didFail(failure.localizedDescription)) {
+      $0.alert = .error(failure.localizedDescription)
+    }
+
+    store.receive(.set(\.$isUnregisteringPhone, false)) {
+      $0.isUnregisteringPhone = false
+    }
   }
 
   func testLoadFactsFromClient() {
-- 
GitLab