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