From 6e220b88eca8b6ba42dc2cbd73f1c2b99a73bd99 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 00:15:46 +0200 Subject: [PATCH 01/24] Restore contacts from backup --- .../RestoreFeature/RestoreFeature.swift | 41 ++++- .../Sources/RestoreFeature/RestoreView.swift | 162 +++++++++++------- .../RestoreFeatureTests.swift | 84 +++++++-- 3 files changed, 202 insertions(+), 85 deletions(-) diff --git a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift index 66d7df2f..7179264a 100644 --- a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift +++ b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift @@ -24,7 +24,7 @@ public struct RestoreState: Equatable { public init( file: File? = nil, fileImportFailure: String? = nil, - restoreFailure: String? = nil, + restoreFailures: [String] = [], focusedField: Field? = nil, isImportingFile: Bool = false, passphrase: String = "", @@ -32,7 +32,7 @@ public struct RestoreState: Equatable { ) { self.file = file self.fileImportFailure = fileImportFailure - self.restoreFailure = restoreFailure + self.restoreFailures = restoreFailures self.focusedField = focusedField self.isImportingFile = isImportingFile self.passphrase = passphrase @@ -41,7 +41,7 @@ public struct RestoreState: Equatable { public var file: File? public var fileImportFailure: String? - public var restoreFailure: String? + public var restoreFailures: [String] @BindableState public var focusedField: Field? @BindableState public var isImportingFile: Bool @BindableState public var passphrase: String @@ -53,7 +53,7 @@ public enum RestoreAction: Equatable, BindableAction { case fileImport(Result<URL, NSError>) case restoreTapped case finished - case failed(NSError) + case failed([NSError]) case binding(BindingAction<RestoreState>) } @@ -125,7 +125,7 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm guard let backupData = state.file?.data, backupData.count > 0 else { return .none } let backupPassphrase = state.passphrase state.isRestoring = true - state.restoreFailure = nil + state.restoreFailures = [] return Effect.result { do { let result = try env.messenger.restoreBackup( @@ -140,10 +140,33 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm phone: facts.get(.phone)?.value, createdAt: env.now() )) + let lookupResult = try env.messenger.lookupContacts.callAsFunction( + ids: result.restoredContacts + ) + try lookupResult.contacts.forEach { contact in + let facts = try contact.getFacts() + try env.db().saveContact(XXModels.Contact( + id: try contact.getId(), + marshaled: contact.data, + username: facts.get(.username)?.value, + email: facts.get(.email)?.value, + phone: facts.get(.phone)?.value, + authStatus: .friend, + createdAt: env.now() + )) + } + guard lookupResult.errors.isEmpty else { + return .success(.failed(lookupResult.errors)) + } return .success(.finished) } catch { - try? env.messenger.destroy() - return .success(.failed(error as NSError)) + var errors = [error as NSError] + do { + try env.messenger.destroy() + } catch { + errors.append(error as NSError) + } + return .success(.failed(errors)) } } .subscribe(on: env.bgQueue) @@ -154,9 +177,9 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm state.isRestoring = false return .none - case .failed(let error): + case .failed(let errors): state.isRestoring = false - state.restoreFailure = error.localizedDescription + state.restoreFailures = errors.map(\.localizedDescription) return .none case .binding(_): diff --git a/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift b/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift index 06f7b915..4dcaa5dc 100644 --- a/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift +++ b/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift @@ -21,7 +21,7 @@ public struct RestoreView: View { var isRestoring: Bool var focusedField: RestoreState.Field? var fileImportFailure: String? - var restoreFailure: String? + var restoreFailures: [String] init(state: RestoreState) { file = state.file.map { .init(name: $0.name, size: $0.data.count) } @@ -30,7 +30,7 @@ public struct RestoreView: View { isRestoring = state.isRestoring focusedField = state.focusedField fileImportFailure = state.fileImportFailure - restoreFailure = state.restoreFailure + restoreFailures = state.restoreFailures } } @@ -38,69 +38,9 @@ public struct RestoreView: View { WithViewStore(store, observe: ViewState.init) { viewStore in NavigationView { Form { - Section { - if let file = viewStore.file { - HStack(alignment: .bottom) { - Text(file.name) - Spacer() - Text(format(byteCount: file.size)) - } - } - - Button { - viewStore.send(.importFileTapped) - } label: { - Text("Import backup file") - } - .fileImporter( - isPresented: viewStore.binding( - get: \.isImportingFile, - send: { .set(\.$isImportingFile, $0) } - ), - allowedContentTypes: [.data], - onCompletion: { result in - viewStore.send(.fileImport(result.mapError { $0 as NSError })) - } - ) - - if let failure = viewStore.fileImportFailure { - Text("Error: \(failure)") - } - } header: { - Text("File") - } - .disabled(viewStore.isRestoring) - + fileSection(viewStore) if viewStore.file != nil { - Section { - SecureField("Passphrase", text: viewStore.binding( - get: \.passphrase, - send: { .set(\.$passphrase, $0) } - )) - .textContentType(.password) - .textInputAutocapitalization(.never) - .disableAutocorrection(true) - .focused($focusedField, equals: .passphrase) - - Button { - viewStore.send(.restoreTapped) - } label: { - HStack { - Text("Restore") - Spacer() - if viewStore.isRestoring { - ProgressView() - } - } - } - - if let failure = viewStore.restoreFailure { - Text("Error: \(failure)") - } - } header: { - Text("Backup") - } - .disabled(viewStore.isRestoring) + restoreSection(viewStore) } } .toolbar { @@ -110,7 +50,7 @@ public struct RestoreView: View { } label: { Text("Cancel") } - .disabled(viewStore.isRestoring) + .disabled(viewStore.isImportingFile || viewStore.isRestoring) } } .navigationTitle("Restore") @@ -121,6 +61,84 @@ public struct RestoreView: View { } } + @ViewBuilder func fileSection(_ viewStore: ViewStore<ViewState, RestoreAction>) -> some View { + Section { + if let file = viewStore.file { + HStack(alignment: .bottom) { + Text(file.name) + Spacer() + Text(format(byteCount: file.size)) + } + } else { + Button { + viewStore.send(.importFileTapped) + } label: { + Text("Import backup file") + } + .fileImporter( + isPresented: viewStore.binding( + get: \.isImportingFile, + send: { .set(\.$isImportingFile, $0) } + ), + allowedContentTypes: [.data], + onCompletion: { result in + viewStore.send(.fileImport(result.mapError { $0 as NSError })) + } + ) + .disabled(viewStore.isRestoring) + } + } header: { + Text("File") + } + + if let failure = viewStore.fileImportFailure { + Section { + Text(failure) + } header: { + Text("Error") + } + } + } + + @ViewBuilder func restoreSection(_ viewStore: ViewStore<ViewState, RestoreAction>) -> some View { + Section { + SecureField("Passphrase", text: viewStore.binding( + get: \.passphrase, + send: { .set(\.$passphrase, $0) } + )) + .textContentType(.password) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .focused($focusedField, equals: .passphrase) + .disabled(viewStore.isRestoring) + + Button { + viewStore.send(.restoreTapped) + } label: { + HStack { + Text("Restore") + Spacer() + if viewStore.isRestoring { + ProgressView() + } + } + } + } header: { + Text("Restore") + } + + if !viewStore.restoreFailures.isEmpty { + Section { + ForEach(Array(viewStore.restoreFailures.enumerated()), id: \.offset) { _, failure in + Text(failure) + } + .font(.footnote) + } header: { + Text("Error") + } + } + } + func format(byteCount: Int) -> String { let formatter = ByteCountFormatter() formatter.allowedUnits = [.useMB, .useKB, .useBytes] @@ -133,7 +151,19 @@ public struct RestoreView: View { public struct RestoreView_Previews: PreviewProvider { public static var previews: some View { RestoreView(store: Store( - initialState: RestoreState(), + initialState: RestoreState( + file: .init(name: "preview", data: Data()), + fileImportFailure: nil, + restoreFailures: [ + "Preview failure 1", + "Preview failure 2", + "Preview failure 3", + ], + focusedField: nil, + isImportingFile: false, + passphrase: "", + isRestoring: false + ), reducer: .empty, environment: () )) diff --git a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift index 9b0a7b87..5ec8409e 100644 --- a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift +++ b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift @@ -85,15 +85,22 @@ final class RestoreFeatureTests: XCTestCase { ] let restoreResult = MessengerRestoreBackup.Result( restoredParams: BackupParams(username: "restored-username"), - restoredContacts: [] + restoredContacts: [ + "contact-1-id".data(using: .utf8)!, + "contact-2-id".data(using: .utf8)!, + "contact-3-id".data(using: .utf8)!, + ] ) + let lookedUpContacts = [XXClient.Contact.stub(1), .stub(2), .stub(3)] let now = Date() let contactId = "contact-id".data(using: .utf8)! + var udFacts: [Fact] = [] var didRestoreWithData: [Data] = [] var didRestoreWithPassphrase: [String] = [] var didSaveContact: [XXModels.Contact] = [] + var didLookupContactIds: [[Data]] = [] let store = TestStore( initialState: RestoreState( @@ -126,6 +133,14 @@ final class RestoreFeatureTests: XCTestCase { ud.getFacts.run = { udFacts } return ud } + store.environment.messenger.lookupContacts.run = { contactIds in + didLookupContactIds.append(contactIds) + return .init( + contacts: lookedUpContacts, + failedIds: [], + errors: [] + ) + } store.environment.db.run = { var db: Database = .unimplemented db.saveContact.run = { contact in @@ -145,13 +160,43 @@ final class RestoreFeatureTests: XCTestCase { XCTAssertNoDifference(didRestoreWithData, [backupData]) XCTAssertNoDifference(didRestoreWithPassphrase, [backupPassphrase]) - XCTAssertNoDifference(didSaveContact, [Contact( - id: contactId, - username: restoreResult.restoredParams.username, - email: restoredFacts.get(.email)?.value, - phone: restoredFacts.get(.phone)?.value, - createdAt: now - )]) + XCTAssertNoDifference(didLookupContactIds, [restoreResult.restoredContacts]) + XCTAssertNoDifference(didSaveContact, [ + Contact( + id: contactId, + username: restoreResult.restoredParams.username, + email: restoredFacts.get(.email)?.value, + phone: restoredFacts.get(.phone)?.value, + createdAt: now + ), + Contact( + id: "contact-\(1)-id".data(using: .utf8)!, + marshaled: "contact-\(1)-data".data(using: .utf8)!, + username: "contact-\(1)-username", + email: "contact-\(1)-email", + phone: "contact-\(1)-phone", + authStatus: .friend, + createdAt: now + ), + Contact( + id: "contact-\(2)-id".data(using: .utf8)!, + marshaled: "contact-\(2)-data".data(using: .utf8)!, + username: "contact-\(2)-username", + email: "contact-\(2)-email", + phone: "contact-\(2)-phone", + authStatus: .friend, + createdAt: now + ), + Contact( + id: "contact-\(3)-id".data(using: .utf8)!, + marshaled: "contact-\(3)-data".data(using: .utf8)!, + username: "contact-\(3)-username", + email: "contact-\(3)-email", + phone: "contact-\(3)-phone", + authStatus: .friend, + createdAt: now + ), + ]) store.receive(.finished) { $0.isRestoring = false @@ -195,9 +240,28 @@ final class RestoreFeatureTests: XCTestCase { XCTAssertEqual(didDestroyMessenger, 1) - store.receive(.failed(failure as NSError)) { + store.receive(.failed([failure as NSError])) { $0.isRestoring = false - $0.restoreFailure = failure.localizedDescription + $0.restoreFailures = [failure.localizedDescription] + } + } +} + +private extension XXClient.Contact { + static func stub(_ id: Int) -> XXClient.Contact { + var contact = XXClient.Contact.unimplemented( + "contact-\(id)-data".data(using: .utf8)! + ) + contact.getIdFromContact.run = { _ in + "contact-\(id)-id".data(using: .utf8)! + } + contact.getFactsFromContact.run = { _ in + [ + Fact(type: .username, value: "contact-\(id)-username"), + Fact(type: .email, value: "contact-\(id)-email"), + Fact(type: .phone, value: "contact-\(id)-phone"), + ] } + return contact } } -- GitLab From 62c8a894f698b39772faaf712e50eadf26d20e44 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 00:39:04 +0200 Subject: [PATCH 02/24] Update UdMultiLookupCallback Improve error handling --- .../Callbacks/UdMultiLookupCallback.swift | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/XXClient/Callbacks/UdMultiLookupCallback.swift b/Sources/XXClient/Callbacks/UdMultiLookupCallback.swift index 1782c01e..cc40b606 100644 --- a/Sources/XXClient/Callbacks/UdMultiLookupCallback.swift +++ b/Sources/XXClient/Callbacks/UdMultiLookupCallback.swift @@ -49,21 +49,21 @@ extension UdMultiLookupCallback { if let err = err { result.errors.append(err as NSError) } - if let contactListJSON = contactListJSON { - do { - result.contacts = try JSONDecoder() - .decode([Data].self, from: contactListJSON) - .map { Contact.live($0) } - } catch { - result.errors.append(error as NSError) + do { + if let data = contactListJSON, + let contactListJSON = try JSONDecoder().decode([Data]?.self, from: data) { + result.contacts = contactListJSON.map { Contact.live($0) } } + } catch { + result.errors.append(error as NSError) } - if let failedIDs = failedIDs { - do { - result.failedIds = try JSONDecoder().decode([Data].self, from: failedIDs) - } catch { - result.errors.append(error as NSError) + do { + if let data = failedIDs, + let failedIDs = try JSONDecoder().decode([Data]?.self, from: data) { + result.failedIds = failedIDs } + } catch { + result.errors.append(error as NSError) } callback.handle(result) } -- GitLab From 8b436846f1f61309450e9239531358b9aa8f1245 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 00:39:26 +0200 Subject: [PATCH 03/24] Update RestoreView --- Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift b/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift index 4dcaa5dc..281f3f06 100644 --- a/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift +++ b/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift @@ -126,6 +126,7 @@ public struct RestoreView: View { } header: { Text("Restore") } + .disabled(viewStore.isRestoring) if !viewStore.restoreFailures.isEmpty { Section { @@ -162,7 +163,7 @@ public struct RestoreView_Previews: PreviewProvider { focusedField: nil, isImportingFile: false, passphrase: "", - isRestoring: false + isRestoring: true ), reducer: .empty, environment: () -- GitLab From f83db6a781efc4e38bd19d6f15fcbfa3332bd161 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 01:03:05 +0200 Subject: [PATCH 04/24] Improve error handling in RestoreFeature --- .../RestoreFeature/RestoreFeature.swift | 8 +- .../RestoreFeatureTests.swift | 101 ++++++++++++++++-- 2 files changed, 97 insertions(+), 12 deletions(-) diff --git a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift index 7179264a..b11895fb 100644 --- a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift +++ b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift @@ -156,7 +156,13 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm )) } guard lookupResult.errors.isEmpty else { - return .success(.failed(lookupResult.errors)) + var errors = lookupResult.errors + do { + try env.messenger.destroy() + } catch { + errors.append(error as NSError) + } + return .success(.failed(errors)) } return .success(.finished) } catch { diff --git a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift index 5ec8409e..a103013b 100644 --- a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift +++ b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift @@ -95,7 +95,6 @@ final class RestoreFeatureTests: XCTestCase { let now = Date() let contactId = "contact-id".data(using: .utf8)! - var udFacts: [Fact] = [] var didRestoreWithData: [Data] = [] var didRestoreWithPassphrase: [String] = [] @@ -203,6 +202,85 @@ final class RestoreFeatureTests: XCTestCase { } } + func testRestoreLookupFailure() { + struct FailureA: Error {} + struct FailureB: Error {} + struct DestroyFailure: Error {} + + let restoreResult = MessengerRestoreBackup.Result( + restoredParams: BackupParams(username: "restored-username"), + restoredContacts: [ + "contact-1-id".data(using: .utf8)!, + "contact-2-id".data(using: .utf8)!, + "contact-3-id".data(using: .utf8)!, + ] + ) + let now = Date() + let contactId = "contact-id".data(using: .utf8)! + + let store = TestStore( + initialState: RestoreState( + file: .init(name: "name", data: "data".data(using: .utf8)!) + ), + reducer: restoreReducer, + environment: .unimplemented + ) + + store.environment.bgQueue = .immediate + store.environment.mainQueue = .immediate + store.environment.now = { now } + store.environment.messenger.restoreBackup.run = { _, _ in restoreResult } + 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.messenger.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.getFacts.run = { [] } + return ud + } + store.environment.messenger.lookupContacts.run = { contactIds in + .init( + contacts: [], + failedIds: [], + errors: [ + FailureA() as NSError, + FailureB() as NSError, + ] + ) + } + store.environment.messenger.destroy.run = { + throw DestroyFailure() + } + store.environment.db.run = { + var db: Database = .unimplemented + db.saveContact.run = { $0 } + return db + } + + store.send(.restoreTapped) { + $0.isRestoring = true + } + + store.receive(.failed([ + FailureA() as NSError, + FailureB() as NSError, + DestroyFailure() as NSError + ])) { + $0.isRestoring = false + $0.restoreFailures = [ + FailureA().localizedDescription, + FailureB().localizedDescription, + DestroyFailure().localizedDescription, + ] + } + } + func testRestoreWithoutFile() { let store = TestStore( initialState: RestoreState( @@ -216,10 +294,10 @@ final class RestoreFeatureTests: XCTestCase { } func testRestoreFailure() { - struct Failure: Error {} - let failure = Failure() - - var didDestroyMessenger = 0 + enum Failure: Error { + case restore + case destroy + } let store = TestStore( initialState: RestoreState( @@ -231,18 +309,19 @@ final class RestoreFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate - store.environment.messenger.restoreBackup.run = { _, _ in throw failure } - store.environment.messenger.destroy.run = { didDestroyMessenger += 1 } + store.environment.messenger.restoreBackup.run = { _, _ in throw Failure.restore } + store.environment.messenger.destroy.run = { throw Failure.destroy } store.send(.restoreTapped) { $0.isRestoring = true } - XCTAssertEqual(didDestroyMessenger, 1) - - store.receive(.failed([failure as NSError])) { + store.receive(.failed([Failure.restore as NSError, Failure.destroy as NSError])) { $0.isRestoring = false - $0.restoreFailures = [failure.localizedDescription] + $0.restoreFailures = [ + Failure.restore.localizedDescription, + Failure.destroy.localizedDescription, + ] } } } -- GitLab From f89c6fce67a70264bbb230252cad86edaabfd295 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 01:35:42 +0200 Subject: [PATCH 05/24] Don't lookup restored contacts --- .../RestoreFeature/RestoreFeature.swift | 28 +--- .../RestoreFeatureTests.swift | 140 ++---------------- 2 files changed, 19 insertions(+), 149 deletions(-) diff --git a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift index b11895fb..455f8989 100644 --- a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift +++ b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift @@ -140,29 +140,13 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm phone: facts.get(.phone)?.value, createdAt: env.now() )) - let lookupResult = try env.messenger.lookupContacts.callAsFunction( - ids: result.restoredContacts - ) - try lookupResult.contacts.forEach { contact in - let facts = try contact.getFacts() - try env.db().saveContact(XXModels.Contact( - id: try contact.getId(), - marshaled: contact.data, - username: facts.get(.username)?.value, - email: facts.get(.email)?.value, - phone: facts.get(.phone)?.value, - authStatus: .friend, - createdAt: env.now() - )) - } - guard lookupResult.errors.isEmpty else { - var errors = lookupResult.errors - do { - try env.messenger.destroy() - } catch { - errors.append(error as NSError) + try result.restoredContacts.forEach { contactId in + if try env.db().fetchContacts(.init(id: [contactId])).isEmpty { + try env.db().saveContact(Contact( + id: contactId, + createdAt: env.now() + )) } - return .success(.failed(errors)) } return .success(.finished) } catch { diff --git a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift index a103013b..8cfdb3d6 100644 --- a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift +++ b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift @@ -91,15 +91,14 @@ final class RestoreFeatureTests: XCTestCase { "contact-3-id".data(using: .utf8)!, ] ) - let lookedUpContacts = [XXClient.Contact.stub(1), .stub(2), .stub(3)] let now = Date() let contactId = "contact-id".data(using: .utf8)! var udFacts: [Fact] = [] var didRestoreWithData: [Data] = [] var didRestoreWithPassphrase: [String] = [] + var didFetchContacts: [XXModels.Contact.Query] = [] var didSaveContact: [XXModels.Contact] = [] - var didLookupContactIds: [[Data]] = [] let store = TestStore( initialState: RestoreState( @@ -132,16 +131,12 @@ final class RestoreFeatureTests: XCTestCase { ud.getFacts.run = { udFacts } return ud } - store.environment.messenger.lookupContacts.run = { contactIds in - didLookupContactIds.append(contactIds) - return .init( - contacts: lookedUpContacts, - failedIds: [], - errors: [] - ) - } store.environment.db.run = { var db: Database = .unimplemented + db.fetchContacts.run = { query in + didFetchContacts.append(query) + return [] + } db.saveContact.run = { contact in didSaveContact.append(contact) return contact @@ -159,7 +154,11 @@ final class RestoreFeatureTests: XCTestCase { XCTAssertNoDifference(didRestoreWithData, [backupData]) XCTAssertNoDifference(didRestoreWithPassphrase, [backupPassphrase]) - XCTAssertNoDifference(didLookupContactIds, [restoreResult.restoredContacts]) + XCTAssertNoDifference(didFetchContacts, [ + .init(id: [restoreResult.restoredContacts[0]]), + .init(id: [restoreResult.restoredContacts[1]]), + .init(id: [restoreResult.restoredContacts[2]]), + ]) XCTAssertNoDifference(didSaveContact, [ Contact( id: contactId, @@ -169,30 +168,15 @@ final class RestoreFeatureTests: XCTestCase { createdAt: now ), Contact( - id: "contact-\(1)-id".data(using: .utf8)!, - marshaled: "contact-\(1)-data".data(using: .utf8)!, - username: "contact-\(1)-username", - email: "contact-\(1)-email", - phone: "contact-\(1)-phone", - authStatus: .friend, + id: restoreResult.restoredContacts[0], createdAt: now ), Contact( - id: "contact-\(2)-id".data(using: .utf8)!, - marshaled: "contact-\(2)-data".data(using: .utf8)!, - username: "contact-\(2)-username", - email: "contact-\(2)-email", - phone: "contact-\(2)-phone", - authStatus: .friend, + id: restoreResult.restoredContacts[1], createdAt: now ), Contact( - id: "contact-\(3)-id".data(using: .utf8)!, - marshaled: "contact-\(3)-data".data(using: .utf8)!, - username: "contact-\(3)-username", - email: "contact-\(3)-email", - phone: "contact-\(3)-phone", - authStatus: .friend, + id: restoreResult.restoredContacts[2], createdAt: now ), ]) @@ -202,85 +186,6 @@ final class RestoreFeatureTests: XCTestCase { } } - func testRestoreLookupFailure() { - struct FailureA: Error {} - struct FailureB: Error {} - struct DestroyFailure: Error {} - - let restoreResult = MessengerRestoreBackup.Result( - restoredParams: BackupParams(username: "restored-username"), - restoredContacts: [ - "contact-1-id".data(using: .utf8)!, - "contact-2-id".data(using: .utf8)!, - "contact-3-id".data(using: .utf8)!, - ] - ) - let now = Date() - let contactId = "contact-id".data(using: .utf8)! - - let store = TestStore( - initialState: RestoreState( - file: .init(name: "name", data: "data".data(using: .utf8)!) - ), - reducer: restoreReducer, - environment: .unimplemented - ) - - store.environment.bgQueue = .immediate - store.environment.mainQueue = .immediate - store.environment.now = { now } - store.environment.messenger.restoreBackup.run = { _, _ in restoreResult } - 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.messenger.ud.get = { - var ud: UserDiscovery = .unimplemented - ud.getFacts.run = { [] } - return ud - } - store.environment.messenger.lookupContacts.run = { contactIds in - .init( - contacts: [], - failedIds: [], - errors: [ - FailureA() as NSError, - FailureB() as NSError, - ] - ) - } - store.environment.messenger.destroy.run = { - throw DestroyFailure() - } - store.environment.db.run = { - var db: Database = .unimplemented - db.saveContact.run = { $0 } - return db - } - - store.send(.restoreTapped) { - $0.isRestoring = true - } - - store.receive(.failed([ - FailureA() as NSError, - FailureB() as NSError, - DestroyFailure() as NSError - ])) { - $0.isRestoring = false - $0.restoreFailures = [ - FailureA().localizedDescription, - FailureB().localizedDescription, - DestroyFailure().localizedDescription, - ] - } - } - func testRestoreWithoutFile() { let store = TestStore( initialState: RestoreState( @@ -325,22 +230,3 @@ final class RestoreFeatureTests: XCTestCase { } } } - -private extension XXClient.Contact { - static func stub(_ id: Int) -> XXClient.Contact { - var contact = XXClient.Contact.unimplemented( - "contact-\(id)-data".data(using: .utf8)! - ) - contact.getIdFromContact.run = { _ in - "contact-\(id)-id".data(using: .utf8)! - } - contact.getFactsFromContact.run = { _ in - [ - Fact(type: .username, value: "contact-\(id)-username"), - Fact(type: .email, value: "contact-\(id)-email"), - Fact(type: .phone, value: "contact-\(id)-phone"), - ] - } - return contact - } -} -- GitLab From 7a004c2a5e0719488cd9073d6b64e1a2ce037f65 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 01:55:41 +0200 Subject: [PATCH 06/24] Improve UI in ContactView --- .../Sources/ContactFeature/ContactView.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift index d775df01..a9611140 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift @@ -26,6 +26,10 @@ public struct ContactView: View { var importUsername: Bool var importEmail: Bool var importPhone: Bool + var canSendRequest: Bool + var canVerifyContact: Bool + var canConfirmRequest: Bool + var canCheckAuthorization: Bool init(state: ContactState) { dbContact = state.dbContact @@ -36,6 +40,10 @@ public struct ContactView: View { importUsername = state.importUsername importEmail = state.importEmail importPhone = state.importPhone + canSendRequest = state.xxContact != nil || state.dbContact?.marshaled != nil + canVerifyContact = state.dbContact?.marshaled != nil + canConfirmRequest = state.dbContact?.marshaled != nil + canCheckAuthorization = state.dbContact?.marshaled != nil } } @@ -109,6 +117,7 @@ public struct ContactView: View { Section { ContactAuthStatusView(dbContact.authStatus) + Button { viewStore.send(.sendRequestTapped) } label: { @@ -118,6 +127,8 @@ public struct ContactView: View { Image(systemName: "chevron.forward") } } + .disabled(!viewStore.canSendRequest) + Button { viewStore.send(.verifyContactTapped) } label: { @@ -127,6 +138,8 @@ public struct ContactView: View { Image(systemName: "chevron.forward") } } + .disabled(!viewStore.canVerifyContact) + Button { viewStore.send(.confirmRequestTapped) } label: { @@ -136,6 +149,8 @@ public struct ContactView: View { Image(systemName: "chevron.forward") } } + .disabled(!viewStore.canConfirmRequest) + Button { viewStore.send(.checkAuthTapped) } label: { @@ -145,6 +160,7 @@ public struct ContactView: View { Image(systemName: "chevron.forward") } } + .disabled(!viewStore.canCheckAuthorization) } header: { Text("Auth") } -- GitLab From 13b626fab9591849bda352a3cceccc5b679d145a Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 02:15:00 +0200 Subject: [PATCH 07/24] Add contact ID to ContactView --- .../Sources/AppCore/SharedUI/Data+hexString.swift | 7 +++++++ .../xx-messenger/Sources/ContactFeature/ContactView.swift | 2 ++ 2 files changed, 9 insertions(+) create mode 100644 Examples/xx-messenger/Sources/AppCore/SharedUI/Data+hexString.swift diff --git a/Examples/xx-messenger/Sources/AppCore/SharedUI/Data+hexString.swift b/Examples/xx-messenger/Sources/AppCore/SharedUI/Data+hexString.swift new file mode 100644 index 00000000..05755e40 --- /dev/null +++ b/Examples/xx-messenger/Sources/AppCore/SharedUI/Data+hexString.swift @@ -0,0 +1,7 @@ +import Foundation + +extension Data { + public var hexString: String { + map { String(format: "%02hhx ", $0) }.joined() + } +} diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift index a9611140..b8ab18a7 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift @@ -108,6 +108,8 @@ public struct ContactView: View { if let dbContact = viewStore.dbContact { Section { + Label(dbContact.id.hexString, systemImage: "number") + .font(.footnote.monospaced()) Label(dbContact.username ?? "", systemImage: "person") Label(dbContact.email ?? "", systemImage: "envelope") Label(dbContact.phone ?? "", systemImage: "phone") -- GitLab From 13bc70acb679dceba2bf830090c488ba0baf574b Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 02:17:09 +0200 Subject: [PATCH 08/24] Allow test selection in ContactView --- Examples/xx-messenger/Sources/ContactFeature/ContactView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift index b8ab18a7..2afc08f5 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift @@ -116,6 +116,7 @@ public struct ContactView: View { } header: { Text("Contact") } + .textSelection(.enabled) Section { ContactAuthStatusView(dbContact.authStatus) -- GitLab From c1952b00c10e35878d9811afc22382f51b4161ef Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 02:22:15 +0200 Subject: [PATCH 09/24] Add contact ID to MyContactView --- .../Sources/MyContactFeature/MyContactView.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift index f32af242..eca55d62 100644 --- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift +++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift @@ -1,3 +1,4 @@ +import AppCore import ComposableArchitecture import SwiftUI import XXModels @@ -49,8 +50,17 @@ public struct MyContactView: View { public var body: some View { WithViewStore(store, observe: ViewState.init) { viewStore in Form { + Section { + Text(viewStore.contact?.id.hexString ?? "") + .font(.footnote.monospaced()) + .textSelection(.enabled) + } header: { + Label("ID", systemImage: "number") + } + Section { Text(viewStore.contact?.username ?? "") + .textSelection(.enabled) } header: { Label("Username", systemImage: "person") } @@ -59,6 +69,7 @@ public struct MyContactView: View { if let contact = viewStore.contact { if let email = contact.email { Text(email) + .textSelection(.enabled) Button(role: .destructive) { viewStore.send(.unregisterEmailTapped) } label: { @@ -135,6 +146,7 @@ public struct MyContactView: View { if let contact = viewStore.contact { if let phone = contact.phone { Text(phone) + .textSelection(.enabled) Button(role: .destructive) { viewStore.send(.unregisterPhoneTapped) } label: { -- GitLab From cc1864ab1d3c8757edf89bc012fc1cac6fd4dc15 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 02:53:44 +0200 Subject: [PATCH 10/24] Add ContactLookupFeature library --- .../xcschemes/ContactLookupFeature.xcscheme | 78 +++++++++++++++++++ Examples/xx-messenger/Package.swift | 19 +++++ .../xcschemes/XXMessenger.xcscheme | 10 +++ .../ContactLookupFeature.swift | 46 +++++++++++ .../ContactLookupView.swift | 69 ++++++++++++++++ .../ContactLookupFeatureTests.swift | 31 ++++++++ 6 files changed, 253 insertions(+) create mode 100644 Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactLookupFeature.xcscheme create mode 100644 Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift create mode 100644 Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift create mode 100644 Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift diff --git a/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactLookupFeature.xcscheme b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactLookupFeature.xcscheme new file mode 100644 index 00000000..e0070c64 --- /dev/null +++ b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactLookupFeature.xcscheme @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1400" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "ContactLookupFeature" + BuildableName = "ContactLookupFeature" + BlueprintName = "ContactLookupFeature" + ReferencedContainer = "container:"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> + <Testables> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "ContactLookupFeatureTests" + BuildableName = "ContactLookupFeatureTests" + BlueprintName = "ContactLookupFeatureTests" + ReferencedContainer = "container:"> + </BuildableReference> + </TestableReference> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "ContactLookupFeature" + BuildableName = "ContactLookupFeature" + BlueprintName = "ContactLookupFeature" + ReferencedContainer = "container:"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index 4668d629..1cab487e 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -20,6 +20,7 @@ let package = Package( .library(name: "CheckContactAuthFeature", targets: ["CheckContactAuthFeature"]), .library(name: "ConfirmRequestFeature", targets: ["ConfirmRequestFeature"]), .library(name: "ContactFeature", targets: ["ContactFeature"]), + .library(name: "ContactLookupFeature", targets: ["ContactLookupFeature"]), .library(name: "ContactsFeature", targets: ["ContactsFeature"]), .library(name: "HomeFeature", targets: ["HomeFeature"]), .library(name: "MyContactFeature", targets: ["MyContactFeature"]), @@ -208,6 +209,24 @@ let package = Package( ], swiftSettings: swiftSettings ), + .target( + name: "ContactLookupFeature", + dependencies: [ + .target(name: "AppCore"), + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), + .product(name: "XXModels", package: "client-ios-db"), + ], + swiftSettings: swiftSettings + ), + .testTarget( + name: "ContactLookupFeatureTests", + dependencies: [ + .target(name: "ContactLookupFeature"), + .product(name: "CustomDump", package: "swift-custom-dump"), + ], + swiftSettings: swiftSettings + ), .target( name: "ContactsFeature", dependencies: [ diff --git a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme index b50f1c10..42a3cede 100644 --- a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme +++ b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme @@ -99,6 +99,16 @@ ReferencedContainer = "container:.."> </BuildableReference> </TestableReference> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "ContactLookupFeatureTests" + BuildableName = "ContactLookupFeatureTests" + BlueprintName = "ContactLookupFeatureTests" + ReferencedContainer = "container:.."> + </BuildableReference> + </TestableReference> <TestableReference skipped = "NO"> <BuildableReference diff --git a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift new file mode 100644 index 00000000..7fd2fc8e --- /dev/null +++ b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift @@ -0,0 +1,46 @@ +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay + +public struct ContactLookupState: Equatable { + public init( + id: Data, + isLookingUp: Bool = false + ) { + self.id = id + self.isLookingUp = isLookingUp + } + + public var id: Data + public var isLookingUp: Bool +} + +public enum ContactLookupAction: Equatable { + case task + case cancelTask + case lookupTapped +} + +public struct ContactLookupEnvironment { + public init() {} +} + +#if DEBUG +extension ContactLookupEnvironment { + public static let unimplemented = ContactLookupEnvironment() +} +#endif + +public let contactLookupReducer = Reducer<ContactLookupState, ContactLookupAction, ContactLookupEnvironment> +{ state, action, env in + switch action { + case .task: + return .none + + case .cancelTask: + return .none + + case .lookupTapped: + return .none + } +} diff --git a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift new file mode 100644 index 00000000..1d1e0208 --- /dev/null +++ b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift @@ -0,0 +1,69 @@ +import AppCore +import ComposableArchitecture +import SwiftUI + +public struct ContactLookupView: View { + public init(store: Store<ContactLookupState, ContactLookupAction>) { + self.store = store + } + + let store: Store<ContactLookupState, ContactLookupAction> + + struct ViewState: Equatable { + init(state: ContactLookupState) { + id = state.id + isLookingUp = state.isLookingUp + } + + var id: Data + var isLookingUp: Bool + } + + public var body: some View { + WithViewStore(store, observe: ViewState.init) { viewStore in + Form { + Section { + Label(viewStore.id.hexString, systemImage: "number") + .font(.footnote.monospaced()) + + Button { + viewStore.send(.lookupTapped) + } label: { + HStack { + Text("Lookup") + Spacer() + if viewStore.isLookingUp { + ProgressView() + } else { + Image(systemName: "magnifyingglass") + } + } + } + .disabled(viewStore.isLookingUp) + } header: { + Text("Contact ID") + } + } + .navigationTitle("Lookup") + .task { + await viewStore.send(.task).finish() + } + } + } +} + +#if DEBUG +public struct ContactLookupView_Previews: PreviewProvider { + public static var previews: some View { + NavigationView { + ContactLookupView(store: Store( + initialState: ContactLookupState( + id: "1234".data(using: .utf8)! + ), + reducer: .empty, + environment: () + )) + } + } +} +#endif diff --git a/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift b/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift new file mode 100644 index 00000000..70847105 --- /dev/null +++ b/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift @@ -0,0 +1,31 @@ +import ComposableArchitecture +import XCTest +@testable import ContactLookupFeature + +final class ContactLookupFeatureTests: XCTestCase { + func testTask() { + let store = TestStore( + initialState: ContactLookupState( + id: "1234".data(using: .utf8)! + ), + reducer: contactLookupReducer, + environment: .unimplemented + ) + + store.send(.task) + + store.send(.cancelTask) + } + + func testLookup() { + let store = TestStore( + initialState: ContactLookupState( + id: "1234".data(using: .utf8)! + ), + reducer: contactLookupReducer, + environment: .unimplemented + ) + + store.send(.lookupTapped) + } +} -- GitLab From 89c7eab8a2fead8e6cb3cd28b4195f47445ed8fb Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 03:07:40 +0200 Subject: [PATCH 11/24] Present Lookup from Contact --- Examples/xx-messenger/Package.swift | 2 ++ .../AppFeature/AppEnvironment+Live.swift | 3 ++ .../ContactFeature/ContactFeature.swift | 30 ++++++++++++++++- .../Sources/ContactFeature/ContactView.swift | 23 +++++++++++++ .../ContactFeatureTests.swift | 32 +++++++++++++++++++ 5 files changed, 89 insertions(+), 1 deletion(-) diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index 1cab487e..4dad2c2b 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -90,6 +90,7 @@ let package = Package( .target(name: "CheckContactAuthFeature"), .target(name: "ConfirmRequestFeature"), .target(name: "ContactFeature"), + .target(name: "ContactLookupFeature"), .target(name: "ContactsFeature"), .target(name: "HomeFeature"), .target(name: "MyContactFeature"), @@ -192,6 +193,7 @@ let package = Package( .target(name: "ChatFeature"), .target(name: "CheckContactAuthFeature"), .target(name: "ConfirmRequestFeature"), + .target(name: "ContactLookupFeature"), .target(name: "SendRequestFeature"), .target(name: "VerifyContactFeature"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index 3cf2cb0b..34d9f806 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -37,6 +37,9 @@ extension AppEnvironment { db: dbManager.getDB, mainQueue: mainQueue, bgQueue: bgQueue, + lookup: { + ContactLookupEnvironment() + }, sendRequest: { SendRequestEnvironment( messenger: messenger, diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift index 993796e4..16c6a9a6 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift @@ -4,6 +4,7 @@ import CheckContactAuthFeature import ComposableArchitecture import ComposablePresentation import ConfirmRequestFeature +import ContactLookupFeature import Foundation import SendRequestFeature import VerifyContactFeature @@ -20,6 +21,7 @@ public struct ContactState: Equatable { importUsername: Bool = true, importEmail: Bool = true, importPhone: Bool = true, + lookup: ContactLookupState? = nil, sendRequest: SendRequestState? = nil, verifyContact: VerifyContactState? = nil, confirmRequest: ConfirmRequestState? = nil, @@ -32,6 +34,7 @@ public struct ContactState: Equatable { self.importUsername = importUsername self.importEmail = importEmail self.importPhone = importPhone + self.lookup = lookup self.sendRequest = sendRequest self.verifyContact = verifyContact self.confirmRequest = confirmRequest @@ -45,6 +48,7 @@ public struct ContactState: Equatable { @BindableState public var importUsername: Bool @BindableState public var importEmail: Bool @BindableState public var importPhone: Bool + public var lookup: ContactLookupState? public var sendRequest: SendRequestState? public var verifyContact: VerifyContactState? public var confirmRequest: ConfirmRequestState? @@ -56,6 +60,9 @@ public enum ContactAction: Equatable, BindableAction { case start case dbContactFetched(XXModels.Contact?) case importFactsTapped + case lookupTapped + case lookupDismissed + case lookup(ContactLookupAction) case sendRequestTapped case sendRequestDismissed case sendRequest(SendRequestAction) @@ -80,6 +87,7 @@ public struct ContactEnvironment { db: DBManagerGetDB, mainQueue: AnySchedulerOf<DispatchQueue>, bgQueue: AnySchedulerOf<DispatchQueue>, + lookup: @escaping () -> ContactLookupEnvironment, sendRequest: @escaping () -> SendRequestEnvironment, verifyContact: @escaping () -> VerifyContactEnvironment, confirmRequest: @escaping () -> ConfirmRequestEnvironment, @@ -90,6 +98,7 @@ public struct ContactEnvironment { self.db = db self.mainQueue = mainQueue self.bgQueue = bgQueue + self.lookup = lookup self.sendRequest = sendRequest self.verifyContact = verifyContact self.confirmRequest = confirmRequest @@ -101,6 +110,7 @@ public struct ContactEnvironment { public var db: DBManagerGetDB public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> + public var lookup: () -> ContactLookupEnvironment public var sendRequest: () -> SendRequestEnvironment public var verifyContact: () -> VerifyContactEnvironment public var confirmRequest: () -> ConfirmRequestEnvironment @@ -115,6 +125,7 @@ extension ContactEnvironment { db: .unimplemented, mainQueue: .unimplemented, bgQueue: .unimplemented, + lookup: { .unimplemented }, sendRequest: { .unimplemented }, verifyContact: { .unimplemented }, confirmRequest: { .unimplemented }, @@ -163,6 +174,14 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm .receive(on: env.mainQueue) .eraseToEffect() + case .lookupTapped: + state.lookup = ContactLookupState(id: state.id) + return .none + + case .lookupDismissed: + state.lookup = nil + return .none + case .sendRequestTapped: if let xxContact = state.xxContact { state.sendRequest = SendRequestState(contact: xxContact) @@ -223,11 +242,20 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm state.chat = nil return .none - case .binding(_), .sendRequest(_), .verifyContact(_), .confirmRequest(_), .checkAuth(_), .chat(_): + case .binding(_), .lookup(_), .sendRequest(_), + .verifyContact(_), .confirmRequest(_), + .checkAuth(_), .chat(_): return .none } } .binding() +.presenting( + contactLookupReducer, + state: .keyPath(\.lookup), + id: .notNil(), + action: /ContactAction.lookup, + environment: { $0.lookup() } +) .presenting( sendRequestReducer, state: .keyPath(\.sendRequest), diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift index 2afc08f5..5bfe475b 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift @@ -4,6 +4,7 @@ import CheckContactAuthFeature import ComposableArchitecture import ComposablePresentation import ConfirmRequestFeature +import ContactLookupFeature import SendRequestFeature import SwiftUI import VerifyContactFeature @@ -26,6 +27,7 @@ public struct ContactView: View { var importUsername: Bool var importEmail: Bool var importPhone: Bool + var canLookup: Bool var canSendRequest: Bool var canVerifyContact: Bool var canConfirmRequest: Bool @@ -40,6 +42,7 @@ public struct ContactView: View { importUsername = state.importUsername importEmail = state.importEmail importPhone = state.importPhone + canLookup = state.dbContact?.id != nil canSendRequest = state.xxContact != nil || state.dbContact?.marshaled != nil canVerifyContact = state.dbContact?.marshaled != nil canConfirmRequest = state.dbContact?.marshaled != nil @@ -121,6 +124,17 @@ public struct ContactView: View { Section { ContactAuthStatusView(dbContact.authStatus) + Button { + viewStore.send(.lookupTapped) + } label: { + HStack { + Text("Lookup") + Spacer() + Image(systemName: "chevron.forward") + } + } + .disabled(!viewStore.canLookup) + Button { viewStore.send(.sendRequestTapped) } label: { @@ -186,6 +200,15 @@ public struct ContactView: View { } .navigationTitle("Contact") .task { viewStore.send(.start) } + .background(NavigationLinkWithStore( + store.scope( + state: \.lookup, + action: ContactAction.lookup + ), + mapState: replayNonNil(), + onDeactivate: { viewStore.send(.lookupDismissed) }, + destination: ContactLookupView.init(store:) + )) .background(NavigationLinkWithStore( store.scope( state: \.sendRequest, diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift index feeccc98..a8f9f144 100644 --- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift @@ -3,6 +3,7 @@ import CheckContactAuthFeature import Combine import ComposableArchitecture import ConfirmRequestFeature +import ContactLookupFeature import CustomDump import SendRequestFeature import VerifyContactFeature @@ -99,6 +100,37 @@ final class ContactFeatureTests: XCTestCase { XCTAssertNoDifference(dbDidSaveContact, [expectedSavedContact]) } + func testLookupTapped() { + let contactId = "contact-id".data(using: .utf8)! + let store = TestStore( + initialState: ContactState( + id: contactId + ), + reducer: contactReducer, + environment: .unimplemented + ) + + store.send(.lookupTapped) { + $0.lookup = ContactLookupState(id: contactId) + } + } + + func testLookupDismissed() { + let contactId = "contact-id".data(using: .utf8)! + let store = TestStore( + initialState: ContactState( + id: contactId, + lookup: ContactLookupState(id: contactId) + ), + reducer: contactReducer, + environment: .unimplemented + ) + + store.send(.lookupDismissed) { + $0.lookup = nil + } + } + func testSendRequestWithDBContact() { var dbContact = XXModels.Contact(id: "contact-id".data(using: .utf8)!) dbContact.marshaled = "contact-data".data(using: .utf8)! -- GitLab From 72fdc4358ce5cb1892532e2a0bd5ecec3a2e173b Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 11:08:07 +0200 Subject: [PATCH 12/24] Implement lookup --- .../AppFeature/AppEnvironment+Live.swift | 7 ++- .../ContactLookupFeature.swift | 53 +++++++++++++++---- .../ContactLookupView.swift | 3 -- .../ContactLookupFeatureTests.swift | 48 +++++++++++++---- 4 files changed, 87 insertions(+), 24 deletions(-) diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index 34d9f806..cd5d752e 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -4,6 +4,7 @@ import ChatFeature import CheckContactAuthFeature import ConfirmRequestFeature import ContactFeature +import ContactLookupFeature import ContactsFeature import Foundation import HomeFeature @@ -38,7 +39,11 @@ extension AppEnvironment { mainQueue: mainQueue, bgQueue: bgQueue, lookup: { - ContactLookupEnvironment() + ContactLookupEnvironment( + messenger: messenger, + mainQueue: mainQueue, + bgQueue: bgQueue + ) }, sendRequest: { SendRequestEnvironment( diff --git a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift index 7fd2fc8e..82a775dd 100644 --- a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift +++ b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift @@ -1,46 +1,81 @@ import ComposableArchitecture import Foundation import XCTestDynamicOverlay +import XXClient +import XXMessengerClient public struct ContactLookupState: Equatable { public init( id: Data, - isLookingUp: Bool = false + isLookingUp: Bool = false, + failure: String? = nil ) { self.id = id self.isLookingUp = isLookingUp + self.failure = failure } public var id: Data public var isLookingUp: Bool + public var failure: String? } public enum ContactLookupAction: Equatable { - case task - case cancelTask case lookupTapped + case didLookup(XXClient.Contact) + case didFail(NSError) } public struct ContactLookupEnvironment { - public init() {} + public init( + messenger: Messenger, + mainQueue: AnySchedulerOf<DispatchQueue>, + bgQueue: AnySchedulerOf<DispatchQueue> + ) { + self.messenger = messenger + self.mainQueue = mainQueue + self.bgQueue = bgQueue + } + + public var messenger: Messenger + public var mainQueue: AnySchedulerOf<DispatchQueue> + public var bgQueue: AnySchedulerOf<DispatchQueue> } #if DEBUG extension ContactLookupEnvironment { - public static let unimplemented = ContactLookupEnvironment() + public static let unimplemented = ContactLookupEnvironment( + messenger: .unimplemented, + mainQueue: .unimplemented, + bgQueue: .unimplemented + ) } #endif public let contactLookupReducer = Reducer<ContactLookupState, ContactLookupAction, ContactLookupEnvironment> { state, action, env in switch action { - case .task: - return .none + case .lookupTapped: + state.isLookingUp = true + return Effect.result { [state] in + do { + let contact = try env.messenger.lookupContact(id: state.id) + return .success(.didLookup(contact)) + } catch { + return .success(.didFail(error as NSError)) + } + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() - case .cancelTask: + case .didLookup(_): + state.isLookingUp = false return .none - case .lookupTapped: + case .didFail(let error): + state.failure = error.localizedDescription + state.isLookingUp = false return .none } } diff --git a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift index 1d1e0208..a8bcd339 100644 --- a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift +++ b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift @@ -45,9 +45,6 @@ public struct ContactLookupView: View { } } .navigationTitle("Lookup") - .task { - await viewStore.send(.task).finish() - } } } } diff --git a/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift b/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift index 70847105..34ecabc3 100644 --- a/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift @@ -1,31 +1,57 @@ import ComposableArchitecture import XCTest +import XXClient @testable import ContactLookupFeature final class ContactLookupFeatureTests: XCTestCase { - func testTask() { + func testLookup() { + let id: Data = "1234".data(using: .utf8)! + var didLookupId: [Data] = [] + let lookedUpContact = Contact.unimplemented("123data".data(using: .utf8)!) + let store = TestStore( - initialState: ContactLookupState( - id: "1234".data(using: .utf8)! - ), + initialState: ContactLookupState(id: id), reducer: contactLookupReducer, environment: .unimplemented ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.lookupContact.run = { id in + didLookupId.append(id) + return lookedUpContact + } + + store.send(.lookupTapped) { + $0.isLookingUp = true + } - store.send(.task) + XCTAssertEqual(didLookupId, [id]) - store.send(.cancelTask) + store.receive(.didLookup(lookedUpContact)) { + $0.isLookingUp = false + } } - func testLookup() { + func testLookupFailure() { + let id: Data = "1234".data(using: .utf8)! + let failure = NSError(domain: "test", code: 0) + let store = TestStore( - initialState: ContactLookupState( - id: "1234".data(using: .utf8)! - ), + initialState: ContactLookupState(id: id), reducer: contactLookupReducer, environment: .unimplemented ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.lookupContact.run = { _ in throw failure } + + store.send(.lookupTapped) { + $0.isLookingUp = true + } - store.send(.lookupTapped) + store.receive(.didFail(failure)) { + $0.failure = failure.localizedDescription + $0.isLookingUp = false + } } } -- GitLab From f7f7def9c886402dbd54a33257413ee7c7803087 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 11:13:28 +0200 Subject: [PATCH 13/24] Handle looked up contact in ContactFeature --- .../Sources/ContactFeature/ContactFeature.swift | 4 ++++ .../ContactFeatureTests.swift | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift index 16c6a9a6..1113b489 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift @@ -182,6 +182,10 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm state.lookup = nil return .none + case .lookup(.didLookup(let xxContact)): + state.xxContact = xxContact + return .none + case .sendRequestTapped: if let xxContact = state.xxContact { state.sendRequest = SendRequestState(contact: xxContact) diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift index a8f9f144..b16d7884 100644 --- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift @@ -131,6 +131,23 @@ final class ContactFeatureTests: XCTestCase { } } + func testLookupDidLookup() { + let contactId = "contact-id".data(using: .utf8)! + let contact = Contact.unimplemented("contact-data".data(using: .utf8)!) + let store = TestStore( + initialState: ContactState( + id: contactId, + lookup: ContactLookupState(id: contactId) + ), + reducer: contactReducer, + environment: .unimplemented + ) + + store.send(.lookup(.didLookup(contact))) { + $0.xxContact = contact + } + } + func testSendRequestWithDBContact() { var dbContact = XXModels.Contact(id: "contact-id".data(using: .utf8)!) dbContact.marshaled = "contact-data".data(using: .utf8)! -- GitLab From 618eae3102d18c50a2d3be605e552b8d3381bcf9 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 11:16:03 +0200 Subject: [PATCH 14/24] Show failure in ContactLookupView --- .../ContactLookupFeature/ContactLookupView.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift index a8bcd339..d4a759d5 100644 --- a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift +++ b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift @@ -13,10 +13,12 @@ public struct ContactLookupView: View { init(state: ContactLookupState) { id = state.id isLookingUp = state.isLookingUp + failure = state.failure } var id: Data var isLookingUp: Bool + var failure: String? } public var body: some View { @@ -43,6 +45,14 @@ public struct ContactLookupView: View { } header: { Text("Contact ID") } + + if let failure = viewStore.failure { + Section { + Text(failure) + } header: { + Text("Error") + } + } } .navigationTitle("Lookup") } -- GitLab From 4843a17f8dbac0843cc25502cb0389bd7e26bf1d Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 11:19:54 +0200 Subject: [PATCH 15/24] Dismiss lookup on success --- .../xx-messenger/Sources/ContactFeature/ContactFeature.swift | 1 + .../Tests/ContactFeatureTests/ContactFeatureTests.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift index 1113b489..dc166e4e 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift @@ -184,6 +184,7 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm case .lookup(.didLookup(let xxContact)): state.xxContact = xxContact + state.lookup = nil return .none case .sendRequestTapped: diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift index b16d7884..c200094f 100644 --- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift @@ -145,6 +145,7 @@ final class ContactFeatureTests: XCTestCase { store.send(.lookup(.didLookup(contact))) { $0.xxContact = contact + $0.lookup = nil } } -- GitLab From 7c1394e8a42c767ebe89c8b638aa9bf87376f865 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 11:34:30 +0200 Subject: [PATCH 16/24] Refactor --- .../Sources/AppCore/SharedUI/Data+hexString.swift | 4 ++-- .../xx-messenger/Sources/ContactFeature/ContactView.swift | 2 +- .../Sources/ContactLookupFeature/ContactLookupView.swift | 2 +- .../xx-messenger/Sources/MyContactFeature/MyContactView.swift | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Examples/xx-messenger/Sources/AppCore/SharedUI/Data+hexString.swift b/Examples/xx-messenger/Sources/AppCore/SharedUI/Data+hexString.swift index 05755e40..e8010b95 100644 --- a/Examples/xx-messenger/Sources/AppCore/SharedUI/Data+hexString.swift +++ b/Examples/xx-messenger/Sources/AppCore/SharedUI/Data+hexString.swift @@ -1,7 +1,7 @@ import Foundation extension Data { - public var hexString: String { - map { String(format: "%02hhx ", $0) }.joined() + public func hexString(bytesSeparator: String = " ") -> String { + map { String(format: "%02hhx\(bytesSeparator)", $0) }.joined() } } diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift index 5bfe475b..daa84903 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift @@ -111,7 +111,7 @@ public struct ContactView: View { if let dbContact = viewStore.dbContact { Section { - Label(dbContact.id.hexString, systemImage: "number") + Label(dbContact.id.hexString(), systemImage: "number") .font(.footnote.monospaced()) Label(dbContact.username ?? "", systemImage: "person") Label(dbContact.email ?? "", systemImage: "envelope") diff --git a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift index d4a759d5..6ce83eda 100644 --- a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift +++ b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift @@ -25,7 +25,7 @@ public struct ContactLookupView: View { WithViewStore(store, observe: ViewState.init) { viewStore in Form { Section { - Label(viewStore.id.hexString, systemImage: "number") + Label(viewStore.id.hexString(), systemImage: "number") .font(.footnote.monospaced()) Button { diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift index eca55d62..d32a6f68 100644 --- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift +++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift @@ -51,7 +51,7 @@ public struct MyContactView: View { WithViewStore(store, observe: ViewState.init) { viewStore in Form { Section { - Text(viewStore.contact?.id.hexString ?? "") + Text(viewStore.contact?.id.hexString() ?? "") .font(.footnote.monospaced()) .textSelection(.enabled) } header: { -- GitLab From 5eda3f487f97cc5fd22fe5454ebd3fc7811a9691 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 12:08:24 +0200 Subject: [PATCH 17/24] Improve failure handling in ContactLookupFeature --- .../Sources/ContactLookupFeature/ContactLookupFeature.swift | 4 +++- .../ContactLookupFeatureTests.swift | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift index 82a775dd..0b6f92dd 100644 --- a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift +++ b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift @@ -57,6 +57,7 @@ public let contactLookupReducer = Reducer<ContactLookupState, ContactLookupActio switch action { case .lookupTapped: state.isLookingUp = true + state.failure = nil return Effect.result { [state] in do { let contact = try env.messenger.lookupContact(id: state.id) @@ -71,11 +72,12 @@ public let contactLookupReducer = Reducer<ContactLookupState, ContactLookupActio case .didLookup(_): state.isLookingUp = false + state.failure = nil return .none case .didFail(let error): - state.failure = error.localizedDescription state.isLookingUp = false + state.failure = error.localizedDescription return .none } } diff --git a/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift b/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift index 34ecabc3..76dde8d0 100644 --- a/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift @@ -23,12 +23,14 @@ final class ContactLookupFeatureTests: XCTestCase { store.send(.lookupTapped) { $0.isLookingUp = true + $0.failure = nil } XCTAssertEqual(didLookupId, [id]) store.receive(.didLookup(lookedUpContact)) { $0.isLookingUp = false + $0.failure = nil } } @@ -47,11 +49,12 @@ final class ContactLookupFeatureTests: XCTestCase { store.send(.lookupTapped) { $0.isLookingUp = true + $0.failure = nil } store.receive(.didFail(failure)) { - $0.failure = failure.localizedDescription $0.isLookingUp = false + $0.failure = failure.localizedDescription } } } -- GitLab From 9f37c6eac49d16fffaa140fd49182f64b6fae65d Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 12:36:02 +0200 Subject: [PATCH 18/24] Improve send request feature Use client to retrieve my facts, instead of database --- .../SendRequestFeature.swift | 39 ++++++---- .../SendRequestFeatureTests.swift | 77 ++++++++++++------- 2 files changed, 75 insertions(+), 41 deletions(-) diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift index f2625b91..4204a5ed 100644 --- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift +++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift @@ -1,4 +1,5 @@ import AppCore +import Combine import ComposableArchitecture import Foundation import XCTestDynamicOverlay @@ -40,7 +41,8 @@ public enum SendRequestAction: Equatable, BindableAction { case sendSucceeded case sendFailed(String) case binding(BindingAction<SendRequestState>) - case myContactFetched(XXClient.Contact?) + case myContactFetched(XXClient.Contact) + case myContactFetchFailed(NSError) } public struct SendRequestEnvironment { @@ -75,25 +77,32 @@ extension SendRequestEnvironment { public let sendRequestReducer = Reducer<SendRequestState, SendRequestAction, SendRequestEnvironment> { state, action, env in - enum DBFetchEffectID {} - switch action { case .start: - return Effect - .catching { try env.messenger.e2e.tryGet().getContact().getId() } - .tryMap { try env.db().fetchContactsPublisher(.init(id: [$0])) } - .flatMap { $0 } - .assertNoFailure() - .map(\.first) - .map { $0?.marshaled.map { XXClient.Contact.live($0) } } - .map(SendRequestAction.myContactFetched) - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - .cancellable(id: DBFetchEffectID.self, cancelInFlight: true) + return Effect.run { subscriber in + do { + var contact = try env.messenger.e2e.tryGet().getContact() + let facts = try env.messenger.ud.tryGet().getFacts() + try contact.setFacts(facts) + subscriber.send(.myContactFetched(contact)) + } catch { + subscriber.send(.myContactFetchFailed(error as NSError)) + } + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .receive(on: env.mainQueue) + .subscribe(on: env.bgQueue) + .eraseToEffect() case .myContactFetched(let contact): state.myContact = contact + state.failure = nil + return .none + + case .myContactFetchFailed(let failure): + state.myContact = nil + state.failure = failure.localizedDescription return .none case .sendTapped: diff --git a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift index 9b15f6cd..76812896 100644 --- a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift +++ b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift @@ -8,6 +8,22 @@ import XXModels final class SendRequestFeatureTests: XCTestCase { func testStart() { + var didSetFactsOnE2EContact: [[Fact]] = [] + let e2eContactData = "e2e-contact-data".data(using: .utf8)! + let e2eContactDataWithFacts = "e2e-contact-data-with-facts".data(using: .utf8)! + let e2eContact: XXClient.Contact = { + var contact = XXClient.Contact.unimplemented(e2eContactData) + contact.setFactsOnContact.run = { data, facts in + didSetFactsOnE2EContact.append(facts) + return e2eContactDataWithFacts + } + return contact + }() + let udFacts = [ + Fact(type: .username, value: "ud-username"), + Fact(type: .email, value: "ud-email"), + Fact(type: .phone, value: "ud-phone"), + ] let store = TestStore( initialState: SendRequestState( contact: .unimplemented("contact-data".data(using: .utf8)!) @@ -15,47 +31,56 @@ final class SendRequestFeatureTests: XCTestCase { reducer: sendRequestReducer, environment: .unimplemented ) - - var dbDidFetchContacts: [XXModels.Contact.Query] = [] - let dbContactsPublisher = PassthroughSubject<[XXModels.Contact], Error>() - store.environment.mainQueue = .immediate store.environment.bgQueue = .immediate store.environment.messenger.e2e.get = { var e2e: E2E = .unimplemented - e2e.getContact.run = { - var contact: XXClient.Contact = .unimplemented("my-contact-data".data(using: .utf8)!) - contact.getIdFromContact.run = { _ in "my-contact-id".data(using: .utf8)! } - return contact - } + e2e.getContact.run = { e2eContact } return e2e } - store.environment.db.run = { - var db: Database = .unimplemented - db.fetchContactsPublisher.run = { query in - dbDidFetchContacts.append(query) - return dbContactsPublisher.eraseToAnyPublisher() - } - return db + store.environment.messenger.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.getFacts.run = { udFacts } + return ud } store.send(.start) - XCTAssertNoDifference(dbDidFetchContacts, [.init(id: ["my-contact-id".data(using: .utf8)!])]) + store.receive(.myContactFetched(.unimplemented(e2eContactDataWithFacts))) { + $0.myContact = .unimplemented(e2eContactDataWithFacts) + } + } - dbContactsPublisher.send([]) + func testMyContactFailure() { + struct Failure: Error {} + let failure = Failure() - store.receive(.myContactFetched(nil)) + let store = TestStore( + initialState: SendRequestState( + contact: .unimplemented("contact-data".data(using: .utf8)!) + ), + reducer: sendRequestReducer, + environment: .unimplemented + ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getContact.run = { .unimplemented(Data()) } + return e2e + } + store.environment.messenger.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.getFacts.run = { throw failure } + return ud + } - var myDbContact = XXModels.Contact(id: "my-contact-id".data(using: .utf8)!) - myDbContact.marshaled = "my-contact-data".data(using: .utf8)! - dbContactsPublisher.send([myDbContact]) + store.send(.start) - store.receive(.myContactFetched(.live("my-contact-data".data(using: .utf8)!))) { - $0.myContact = .live("my-contact-data".data(using: .utf8)!) + store.receive(.myContactFetchFailed(failure as NSError)) { + $0.myContact = nil + $0.failure = failure.localizedDescription } - - dbContactsPublisher.send(completion: .finished) } func testSendRequest() { -- GitLab From 3b6d4db726a7c753de4c8113e5ba61a35ac52da9 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 12:44:30 +0200 Subject: [PATCH 19/24] Improve form on BackupView --- .../Sources/BackupFeature/BackupFeature.swift | 8 ++++++++ .../Sources/BackupFeature/BackupView.swift | 15 ++++++++++++--- .../BackupFeatureTests/BackupFeatureTests.swift | 4 ++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift b/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift index f672b312..431dd8ea 100644 --- a/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift +++ b/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift @@ -7,6 +7,10 @@ import XXMessengerClient import XXModels public struct BackupState: Equatable { + public enum Field: String, Hashable { + case passphrase + } + public enum Error: String, Swift.Error, Equatable { case dbContactNotFound case dbContactUsernameMissing @@ -19,6 +23,7 @@ public struct BackupState: Equatable { isStopping: Bool = false, backup: BackupStorage.Backup? = nil, alert: AlertState<BackupAction>? = nil, + focusedField: Field? = nil, passphrase: String = "", isExporting: Bool = false, exportData: Data? = nil @@ -29,6 +34,7 @@ public struct BackupState: Equatable { self.isStopping = isStopping self.backup = backup self.alert = alert + self.focusedField = focusedField self.passphrase = passphrase self.isExporting = isExporting self.exportData = exportData @@ -40,6 +46,7 @@ public struct BackupState: Equatable { public var isStopping: Bool public var backup: BackupStorage.Backup? public var alert: AlertState<BackupAction>? + @BindableState public var focusedField: Field? @BindableState public var passphrase: String @BindableState public var isExporting: Bool public var exportData: Data? @@ -119,6 +126,7 @@ public let backupReducer = Reducer<BackupState, BackupAction, BackupEnvironment> case .startTapped: state.isStarting = true + state.focusedField = nil return Effect.run { [state] subscriber in do { let e2e: E2E = try env.messenger.e2e.tryGet() diff --git a/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift b/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift index 54fc6567..89510b2f 100644 --- a/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift +++ b/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift @@ -8,6 +8,7 @@ public struct BackupView: View { } let store: Store<BackupState, BackupAction> + @FocusState var focusedField: BackupState.Field? struct ViewState: Equatable { struct Backup: Equatable { @@ -23,6 +24,7 @@ public struct BackupView: View { backup = state.backup.map { backup in Backup(date: backup.date, size: backup.data.count) } + focusedField = state.focusedField passphrase = state.passphrase isExporting = state.isExporting exportData = state.exportData @@ -34,6 +36,7 @@ public struct BackupView: View { var isStopping: Bool var isLoading: Bool { isStarting || isResuming || isStopping } var backup: Backup? + var focusedField: BackupState.Field? var passphrase: String var isExporting: Bool var exportData: Data? @@ -57,9 +60,9 @@ public struct BackupView: View { ) } .navigationTitle("Backup") - .task { - await viewStore.send(.task).finish() - } + .task { await viewStore.send(.task).finish() } + .onChange(of: viewStore.focusedField) { focusedField = $0 } + .onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) } } } @@ -75,6 +78,11 @@ public struct BackupView: View { prompt: Text("Backup passphrase"), label: { Text("Backup passphrase") } ) + .textContentType(.password) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .focused($focusedField, equals: .passphrase) + Button { viewStore.send(.startTapped) } label: { @@ -91,6 +99,7 @@ public struct BackupView: View { } header: { Text("New backup") } + .disabled(viewStore.isStarting) } @ViewBuilder func backupSection( diff --git a/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift b/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift index 21afbd54..b916ddc0 100644 --- a/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift +++ b/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift @@ -101,6 +101,9 @@ final class BackupFeatureTests: XCTestCase { } actions = [] + store.send(.set(\.$focusedField, .passphrase)) { + $0.focusedField = .passphrase + } store.send(.set(\.$passphrase, passphrase)) { $0.passphrase = passphrase } @@ -110,6 +113,7 @@ final class BackupFeatureTests: XCTestCase { actions = [] store.send(.startTapped) { $0.isStarting = true + $0.focusedField = nil } XCTAssertNoDifference(actions, [ -- GitLab From 8718aa66750592c3efb69294c9a1851394c86d23 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 12:47:36 +0200 Subject: [PATCH 20/24] Restore username from facts, not from params --- .../Sources/RestoreFeature/RestoreFeature.swift | 2 +- .../Tests/RestoreFeatureTests/RestoreFeatureTests.swift | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift index 455f8989..4d372202 100644 --- a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift +++ b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift @@ -135,7 +135,7 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm let facts = try env.messenger.ud.tryGet().getFacts() try env.db().saveContact(Contact( id: try env.messenger.e2e.tryGet().getContact().getId(), - username: result.restoredParams.username, + username: facts.get(.username)?.value, email: facts.get(.email)?.value, phone: facts.get(.phone)?.value, createdAt: env.now() diff --git a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift index 8cfdb3d6..716d7650 100644 --- a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift +++ b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift @@ -80,11 +80,12 @@ final class RestoreFeatureTests: XCTestCase { let backupData = "backup-data".data(using: .utf8)! let backupPassphrase = "backup-passphrase" let restoredFacts = [ - Fact(type: .email, value: "restored-email"), - Fact(type: .phone, value: "restored-phone"), + Fact(type: .username, value: "restored-fact-username"), + Fact(type: .email, value: "restored-fact-email"), + Fact(type: .phone, value: "restored-fact-phone"), ] let restoreResult = MessengerRestoreBackup.Result( - restoredParams: BackupParams(username: "restored-username"), + restoredParams: BackupParams(username: "restored-param-username"), restoredContacts: [ "contact-1-id".data(using: .utf8)!, "contact-2-id".data(using: .utf8)!, @@ -162,7 +163,7 @@ final class RestoreFeatureTests: XCTestCase { XCTAssertNoDifference(didSaveContact, [ Contact( id: contactId, - username: restoreResult.restoredParams.username, + username: restoredFacts.get(.username)?.value, email: restoredFacts.get(.email)?.value, phone: restoredFacts.get(.phone)?.value, createdAt: now -- GitLab From b783ad91e7323598ce6918970a52c340e06dde10 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 15:44:36 +0200 Subject: [PATCH 21/24] Use MessengerMyContact in BackupFeature --- Examples/xx-messenger/Package.swift | 2 - .../AppFeature/AppEnvironment+Live.swift | 1 - .../Sources/BackupFeature/BackupFeature.swift | 21 +--- .../BackupFeatureTests.swift | 109 +++++------------- 4 files changed, 33 insertions(+), 100 deletions(-) diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index 4dad2c2b..d863e016 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -119,10 +119,8 @@ let package = Package( .target( name: "BackupFeature", dependencies: [ - .target(name: "AppCore"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), - .product(name: "XXModels", package: "client-ios-db"), ], swiftSettings: swiftSettings ), diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index cd5d752e..ce52387f 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -164,7 +164,6 @@ extension AppEnvironment { backup: { BackupEnvironment( messenger: messenger, - db: dbManager.getDB, backupStorage: backupStorage, mainQueue: mainQueue, bgQueue: bgQueue diff --git a/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift b/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift index 431dd8ea..8010f17d 100644 --- a/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift +++ b/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift @@ -1,10 +1,8 @@ -import AppCore import Combine import ComposableArchitecture import Foundation import XXClient import XXMessengerClient -import XXModels public struct BackupState: Equatable { public enum Field: String, Hashable { @@ -12,8 +10,7 @@ public struct BackupState: Equatable { } public enum Error: String, Swift.Error, Equatable { - case dbContactNotFound - case dbContactUsernameMissing + case contactUsernameMissing } public init( @@ -71,20 +68,17 @@ public enum BackupAction: Equatable, BindableAction { public struct BackupEnvironment { public init( messenger: Messenger, - db: DBManagerGetDB, backupStorage: BackupStorage, mainQueue: AnySchedulerOf<DispatchQueue>, bgQueue: AnySchedulerOf<DispatchQueue> ) { self.messenger = messenger - self.db = db self.backupStorage = backupStorage self.mainQueue = mainQueue self.bgQueue = bgQueue } public var messenger: Messenger - public var db: DBManagerGetDB public var backupStorage: BackupStorage public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> @@ -94,7 +88,6 @@ public struct BackupEnvironment { extension BackupEnvironment { public static let unimplemented = BackupEnvironment( messenger: .unimplemented, - db: .unimplemented, backupStorage: .unimplemented, mainQueue: .unimplemented, bgQueue: .unimplemented @@ -129,15 +122,9 @@ public let backupReducer = Reducer<BackupState, BackupAction, BackupEnvironment> state.focusedField = nil return Effect.run { [state] subscriber in do { - let e2e: E2E = try env.messenger.e2e.tryGet() - let contactID = try e2e.getContact().getId() - let db = try env.db() - let query = XXModels.Contact.Query(id: [contactID]) - guard let contact = try db.fetchContacts(query).first else { - throw BackupState.Error.dbContactNotFound - } - guard let username = contact.username else { - throw BackupState.Error.dbContactUsernameMissing + let contact = try env.messenger.myContact(includeFacts: .types([.username])) + guard let username = try contact.getFact(.username)?.value else { + throw BackupState.Error.contactUsernameMissing } try env.messenger.startBackup( password: state.passphrase, diff --git a/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift b/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift index b916ddc0..d0c69eb8 100644 --- a/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift +++ b/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift @@ -2,7 +2,6 @@ import ComposableArchitecture import XCTest import XXClient import XXMessengerClient -import XXModels @testable import BackupFeature final class BackupFeatureTests: XCTestCase { @@ -65,11 +64,7 @@ final class BackupFeatureTests: XCTestCase { func testStartBackup() { var actions: [Action]! var isBackupRunning: [Bool] = [true] - let contactID = "contact-id".data(using: .utf8)! - let dbContact = XXModels.Contact( - id: contactID, - username: "db-contact-username" - ) + let username = "test-username" let passphrase = "backup-password" let store = TestStore( @@ -79,26 +74,18 @@ final class BackupFeatureTests: XCTestCase { ) store.environment.mainQueue = .immediate store.environment.bgQueue = .immediate + store.environment.messenger.myContact.run = { includeFacts in + actions.append(.didGetMyContact(includingFacts: includeFacts)) + var contact = Contact.unimplemented("contact-data".data(using: .utf8)!) + contact.getFactsFromContact.run = { _ in [Fact(type: .username, value: username)] } + return contact + } store.environment.messenger.startBackup.run = { passphrase, params in actions.append(.didStartBackup(passphrase: passphrase, params: params)) } store.environment.messenger.isBackupRunning.run = { isBackupRunning.removeFirst() } - 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 = .unimplemented - db.fetchContacts.run = { _ in return [dbContact] } - return db - } actions = [] store.send(.set(\.$focusedField, .passphrase)) { @@ -117,9 +104,12 @@ final class BackupFeatureTests: XCTestCase { } XCTAssertNoDifference(actions, [ + .didGetMyContact( + includingFacts: .types([.username]) + ), .didStartBackup( passphrase: passphrase, - params: .init(username: dbContact.username!) + params: .init(username: username) ) ]) @@ -130,9 +120,8 @@ final class BackupFeatureTests: XCTestCase { } } - func testStartBackupWithoutDbContact() { + func testStartBackupWithoutContactUsername() { var isBackupRunning: [Bool] = [false] - let contactID = "contact-id".data(using: .utf8)! let store = TestStore( initialState: BackupState( @@ -143,29 +132,20 @@ final class BackupFeatureTests: XCTestCase { ) store.environment.mainQueue = .immediate store.environment.bgQueue = .immediate + store.environment.messenger.myContact.run = { _ in + var contact = Contact.unimplemented("contact-data".data(using: .utf8)!) + contact.getFactsFromContact.run = { _ in [] } + return contact + } store.environment.messenger.isBackupRunning.run = { isBackupRunning.removeFirst() } - 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 = .unimplemented - db.fetchContacts.run = { _ in [] } - return db - } store.send(.startTapped) { $0.isStarting = true } - let failure = BackupState.Error.dbContactNotFound + let failure = BackupState.Error.contactUsernameMissing store.receive(.didStart(failure: failure as NSError)) { $0.isRunning = false $0.isStarting = false @@ -173,13 +153,10 @@ final class BackupFeatureTests: XCTestCase { } } - func testStartBackupWithoutDbContactUsername() { + func testStartBackupMyContactFailure() { + struct Failure: Error {} + let failure = Failure() var isBackupRunning: [Bool] = [false] - let contactID = "contact-id".data(using: .utf8)! - let dbContact = XXModels.Contact( - id: contactID, - username: nil - ) let store = TestStore( initialState: BackupState( @@ -190,29 +167,15 @@ final class BackupFeatureTests: XCTestCase { ) store.environment.mainQueue = .immediate store.environment.bgQueue = .immediate + store.environment.messenger.myContact.run = { _ in throw failure } store.environment.messenger.isBackupRunning.run = { isBackupRunning.removeFirst() } - 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 = .unimplemented - db.fetchContacts.run = { _ in [dbContact] } - return db - } store.send(.startTapped) { $0.isStarting = true } - let failure = BackupState.Error.dbContactUsernameMissing store.receive(.didStart(failure: failure as NSError)) { $0.isRunning = false $0.isStarting = false @@ -220,15 +183,10 @@ final class BackupFeatureTests: XCTestCase { } } - func testStartBackupFailure() { + func testStartBackupStartFailure() { struct Failure: Error {} let failure = Failure() var isBackupRunning: [Bool] = [false] - let contactID = "contact-id".data(using: .utf8)! - let dbContact = XXModels.Contact( - id: contactID, - username: "db-contact-username" - ) let store = TestStore( initialState: BackupState( @@ -239,26 +197,17 @@ final class BackupFeatureTests: XCTestCase { ) store.environment.mainQueue = .immediate store.environment.bgQueue = .immediate + store.environment.messenger.myContact.run = { _ in + var contact = Contact.unimplemented("data".data(using: .utf8)!) + contact.getFactsFromContact.run = { _ in [Fact(type: .username, value: "username")] } + return contact + } store.environment.messenger.startBackup.run = { _, _ in throw failure } store.environment.messenger.isBackupRunning.run = { isBackupRunning.removeFirst() } - 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 = .unimplemented - db.fetchContacts.run = { _ in return [dbContact] } - return db - } store.send(.startTapped) { $0.isStarting = true @@ -463,5 +412,5 @@ private enum Action: Equatable { case didResumeBackup case didStopBackup case didRemoveBackup - case didFetchContacts(XXModels.Contact.Query) + case didGetMyContact(includingFacts: MessengerMyContact.IncludeFacts?) } -- GitLab From 20e23f555e33a5f38e77cfe9201e6f8afe2e826b Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 16:10:53 +0200 Subject: [PATCH 22/24] Use MessengerMyContact in RegisterFeature --- .../RegisterFeature/RegisterFeature.swift | 19 ++- .../RegisterFeatureTests.swift | 153 +++++++++++++----- 2 files changed, 131 insertions(+), 41 deletions(-) diff --git a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift index cb43c430..9b9754c6 100644 --- a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift +++ b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift @@ -7,6 +7,10 @@ import XXMessengerClient import XXModels public struct RegisterState: Equatable { + public enum Error: Swift.Error, Equatable { + case usernameMismatch(registering: String, registered: String?) + } + public enum Field: String, Hashable { case username } @@ -82,14 +86,22 @@ public let registerReducer = Reducer<RegisterState, RegisterAction, RegisterEnvi do { let db = try env.db() try env.messenger.register(username: username) - var contact = try env.messenger.e2e.tryGet().getContact() - try contact.setFact(.username, username) + let contact = try env.messenger.myContact() + let facts = try contact.getFacts() try db.saveContact(Contact( id: try contact.getId(), marshaled: contact.data, - username: username, + username: facts.get(.username)?.value, + email: facts.get(.email)?.value, + phone: facts.get(.phone)?.value, createdAt: env.now() )) + guard facts.get(.username)?.value == username else { + throw RegisterState.Error.usernameMismatch( + registering: username, + registered: facts.get(.username)?.value + ) + } fulfill(.success(.finished)) } catch { @@ -106,6 +118,7 @@ public let registerReducer = Reducer<RegisterState, RegisterAction, RegisterEnvi return .none case .finished: + state.isRegistering = false return .none } } diff --git a/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift b/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift index b156335b..e0ffc5ef 100644 --- a/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift +++ b/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift @@ -8,38 +8,40 @@ import XXModels final class RegisterFeatureTests: XCTestCase { func testRegister() throws { + let now = Date() + let username = "registering-username" + let myContactId = "my-contact-id".data(using: .utf8)! + let myContactData = "my-contact-data".data(using: .utf8)! + let myContactUsername = username + let myContactEmail = "my-contact-email" + let myContactPhone = "my-contact-phone" + let myContactFacts = [ + Fact(type: .username, value: myContactUsername), + Fact(type: .email, value: myContactEmail), + Fact(type: .phone, value: myContactPhone), + ] + + var messengerDidRegisterUsername: [String] = [] + var didGetMyContact: [MessengerMyContact.IncludeFacts?] = [] + var dbDidSaveContact: [XXModels.Contact] = [] + let store = TestStore( initialState: RegisterState(), reducer: registerReducer, environment: .unimplemented ) - - let now = Date() - let mainQueue = DispatchQueue.test - let bgQueue = DispatchQueue.test - var didSetFactsOnContact: [[XXClient.Fact]] = [] - var dbDidSaveContact: [XXModels.Contact] = [] - var messengerDidRegisterUsername: [String] = [] - store.environment.now = { now } - store.environment.mainQueue = mainQueue.eraseToAnyScheduler() - store.environment.bgQueue = bgQueue.eraseToAnyScheduler() + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate store.environment.messenger.register.run = { username in messengerDidRegisterUsername.append(username) } - store.environment.messenger.e2e.get = { - var e2e: E2E = .unimplemented - e2e.getContact.run = { - var contact = XXClient.Contact.unimplemented("contact-data".data(using: .utf8)!) - contact.getIdFromContact.run = { _ in "contact-id".data(using: .utf8)! } - contact.getFactsFromContact.run = { _ in [] } - contact.setFactsOnContact.run = { data, facts in - didSetFactsOnContact.append(facts) - return data - } - return contact - } - return e2e + store.environment.messenger.myContact.run = { includeFacts in + didGetMyContact.append(includeFacts) + var contact = XXClient.Contact.unimplemented(myContactData) + contact.getIdFromContact.run = { _ in myContactId } + contact.getFactsFromContact.run = { _ in myContactFacts } + return contact } store.environment.db.run = { var db: Database = .unimplemented @@ -50,33 +52,34 @@ final class RegisterFeatureTests: XCTestCase { return db } - store.send(.set(\.$username, "NewUser")) { - $0.username = "NewUser" + store.send(.set(\.$focusedField, .username)) { + $0.focusedField = .username + } + + store.send(.set(\.$username, myContactUsername)) { + $0.username = myContactUsername } store.send(.registerTapped) { + $0.focusedField = nil $0.isRegistering = true } - XCTAssertNoDifference(messengerDidRegisterUsername, []) - XCTAssertNoDifference(dbDidSaveContact, []) - - bgQueue.advance() - - XCTAssertNoDifference(messengerDidRegisterUsername, ["NewUser"]) - XCTAssertNoDifference(didSetFactsOnContact, [[Fact(type: .username, value: "NewUser")]]) + XCTAssertNoDifference(messengerDidRegisterUsername, [username]) XCTAssertNoDifference(dbDidSaveContact, [ XXModels.Contact( - id: "contact-id".data(using: .utf8)!, - marshaled: "contact-data".data(using: .utf8)!, - username: "NewUser", + id: myContactId, + marshaled: myContactData, + username: myContactUsername, + email: myContactEmail, + phone: myContactPhone, createdAt: now ) ]) - mainQueue.advance() - - store.receive(.finished) + store.receive(.finished) { + $0.isRegistering = false + } } func testGetDbFailure() throws { @@ -139,4 +142,78 @@ final class RegisterFeatureTests: XCTestCase { $0.failure = error.localizedDescription } } + + func testRegisterUsernameMismatchFailure() throws { + let now = Date() + let username = "registering-username" + let myContactId = "my-contact-id".data(using: .utf8)! + let myContactData = "my-contact-data".data(using: .utf8)! + let myContactUsername = "my-contact-username" + let myContactEmail = "my-contact-email" + let myContactPhone = "my-contact-phone" + let myContactFacts = [ + Fact(type: .username, value: myContactUsername), + Fact(type: .email, value: myContactEmail), + Fact(type: .phone, value: myContactPhone), + ] + + var messengerDidRegisterUsername: [String] = [] + var didGetMyContact: [MessengerMyContact.IncludeFacts?] = [] + var dbDidSaveContact: [XXModels.Contact] = [] + + let store = TestStore( + initialState: RegisterState( + username: username + ), + reducer: registerReducer, + environment: .unimplemented + ) + store.environment.now = { now } + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.register.run = { username in + messengerDidRegisterUsername.append(username) + } + store.environment.messenger.myContact.run = { includeFacts in + didGetMyContact.append(includeFacts) + var contact = XXClient.Contact.unimplemented(myContactData) + contact.getIdFromContact.run = { _ in myContactId } + contact.getFactsFromContact.run = { _ in myContactFacts } + return contact + } + store.environment.db.run = { + var db: Database = .unimplemented + db.saveContact.run = { contact in + dbDidSaveContact.append(contact) + return contact + } + return db + } + + store.send(.registerTapped) { + $0.focusedField = nil + $0.isRegistering = true + } + + XCTAssertNoDifference(messengerDidRegisterUsername, [username]) + XCTAssertNoDifference(dbDidSaveContact, [ + XXModels.Contact( + id: myContactId, + marshaled: myContactData, + username: myContactUsername, + email: myContactEmail, + phone: myContactPhone, + createdAt: now + ) + ]) + + let failure = RegisterState.Error.usernameMismatch( + registering: username, + registered: myContactUsername + ) + store.receive(.failed(failure.localizedDescription)) { + $0.isRegistering = false + $0.failure = failure.localizedDescription + } + } } -- GitLab From bb1e81e4f005e7c7263f4585f5b58e4b11a973b5 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 16:17:45 +0200 Subject: [PATCH 23/24] Use MessengerMyContact in SendRequestFeature --- .../SendRequestFeature.swift | 4 +- .../SendRequestFeatureTests.swift | 48 +++++-------------- 2 files changed, 12 insertions(+), 40 deletions(-) diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift index 4204a5ed..18075179 100644 --- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift +++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift @@ -81,9 +81,7 @@ public let sendRequestReducer = Reducer<SendRequestState, SendRequestAction, Sen case .start: return Effect.run { subscriber in do { - var contact = try env.messenger.e2e.tryGet().getContact() - let facts = try env.messenger.ud.tryGet().getFacts() - try contact.setFacts(facts) + let contact = try env.messenger.myContact() subscriber.send(.myContactFetched(contact)) } catch { subscriber.send(.myContactFetchFailed(error as NSError)) diff --git a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift index 76812896..4c68abab 100644 --- a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift +++ b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift @@ -3,27 +3,16 @@ import ComposableArchitecture import CustomDump import XCTest import XXClient +import XXMessengerClient import XXModels @testable import SendRequestFeature final class SendRequestFeatureTests: XCTestCase { func testStart() { - var didSetFactsOnE2EContact: [[Fact]] = [] - let e2eContactData = "e2e-contact-data".data(using: .utf8)! - let e2eContactDataWithFacts = "e2e-contact-data-with-facts".data(using: .utf8)! - let e2eContact: XXClient.Contact = { - var contact = XXClient.Contact.unimplemented(e2eContactData) - contact.setFactsOnContact.run = { data, facts in - didSetFactsOnE2EContact.append(facts) - return e2eContactDataWithFacts - } - return contact - }() - let udFacts = [ - Fact(type: .username, value: "ud-username"), - Fact(type: .email, value: "ud-email"), - Fact(type: .phone, value: "ud-phone"), - ] + let myContact = XXClient.Contact.unimplemented("my-contact-data".data(using: .utf8)!) + + var didGetMyContact: [MessengerMyContact.IncludeFacts?] = [] + let store = TestStore( initialState: SendRequestState( contact: .unimplemented("contact-data".data(using: .utf8)!) @@ -33,21 +22,15 @@ final class SendRequestFeatureTests: XCTestCase { ) store.environment.mainQueue = .immediate store.environment.bgQueue = .immediate - store.environment.messenger.e2e.get = { - var e2e: E2E = .unimplemented - e2e.getContact.run = { e2eContact } - return e2e - } - store.environment.messenger.ud.get = { - var ud: UserDiscovery = .unimplemented - ud.getFacts.run = { udFacts } - return ud + store.environment.messenger.myContact.run = { includeFacts in + didGetMyContact.append(includeFacts) + return myContact } store.send(.start) - store.receive(.myContactFetched(.unimplemented(e2eContactDataWithFacts))) { - $0.myContact = .unimplemented(e2eContactDataWithFacts) + store.receive(.myContactFetched(myContact)) { + $0.myContact = myContact } } @@ -64,16 +47,7 @@ final class SendRequestFeatureTests: XCTestCase { ) store.environment.mainQueue = .immediate store.environment.bgQueue = .immediate - store.environment.messenger.e2e.get = { - var e2e: E2E = .unimplemented - e2e.getContact.run = { .unimplemented(Data()) } - return e2e - } - store.environment.messenger.ud.get = { - var ud: UserDiscovery = .unimplemented - ud.getFacts.run = { throw failure } - return ud - } + store.environment.messenger.myContact.run = { _ in throw failure } store.send(.start) -- GitLab From dcef4f39c15953cd0e035539d48daf10100b6f7d Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 30 Sep 2022 16:27:18 +0200 Subject: [PATCH 24/24] Load username fact in MyContactFeature --- .../Sources/MyContactFeature/MyContactFeature.swift | 1 + .../Tests/MyContactFeatureTests/MyContactFeatureTests.swift | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift index b25d0212..434a1aca 100644 --- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift +++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift @@ -285,6 +285,7 @@ public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContact let contactId = try env.messenger.e2e.tryGet().getContact().getId() if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first { let facts = try env.messenger.ud.tryGet().getFacts() + dbContact.username = facts.get(.username)?.value dbContact.email = facts.get(.email)?.value dbContact.phone = facts.get(.phone)?.value try env.db().saveContact(dbContact) diff --git a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift index 0e553296..830e9715 100644 --- a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift +++ b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift @@ -645,6 +645,7 @@ final class MyContactFeatureTests: XCTestCase { func testLoadFactsFromClient() { let contactId = "contact-id".data(using: .utf8)! let dbContact = XXModels.Contact(id: contactId) + let username = "user234" let email = "test@email.com" let phone = "123456789" @@ -672,6 +673,7 @@ final class MyContactFeatureTests: XCTestCase { var ud: UserDiscovery = .unimplemented ud.getFacts.run = { [ + Fact(type: .username, value: username), Fact(type: .email, value: email), Fact(type: .phone, value: phone), ] @@ -697,6 +699,7 @@ final class MyContactFeatureTests: XCTestCase { XCTAssertNoDifference(didFetchContacts, [.init(id: [contactId])]) var expectedSavedContact = dbContact + expectedSavedContact.username = username expectedSavedContact.email = email expectedSavedContact.phone = phone XCTAssertNoDifference(didSaveContact, [expectedSavedContact]) -- GitLab