From 2e50e9c88e80c549d88e2bdf56fffd495f8c1d11 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Tue, 6 Sep 2022 23:51:12 +0200 Subject: [PATCH 01/29] Add UserSearchResultFeature --- .../UserSearchResultFeature.swift | 52 +++++++++++++++ .../UserSearchResultView.swift | 63 +++++++++++++++++++ .../UserSearchResultFeatureTests.swift | 32 ++++++++++ 3 files changed, 147 insertions(+) create mode 100644 Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift create mode 100644 Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift create mode 100644 Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift new file mode 100644 index 00000000..a224384b --- /dev/null +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift @@ -0,0 +1,52 @@ +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay +import XXClient + +public struct UserSearchResultState: Equatable, Identifiable { + public init( + id: Data, + contact: Contact, + username: String? = nil, + email: String? = nil, + phone: String? = nil + ) { + self.id = id + self.contact = contact + self.username = username + self.email = email + self.phone = phone + } + + public var id: Data + public var contact: XXClient.Contact + public var username: String? + public var email: String? + public var phone: String? +} + +public enum UserSearchResultAction: Equatable { + case start +} + +public struct UserSearchResultEnvironment { + public init() {} +} + +#if DEBUG +extension UserSearchResultEnvironment { + public static let unimplemented = UserSearchResultEnvironment() +} +#endif + +public let userSearchResultReducer = Reducer<UserSearchResultState, UserSearchResultAction, UserSearchResultEnvironment> +{ state, action, env in + switch action { + case .start: + let facts = (try? state.contact.getFacts()) ?? [] + state.username = facts.first(where: { $0.type == 0 })?.fact + state.email = facts.first(where: { $0.type == 1 })?.fact + state.phone = facts.first(where: { $0.type == 2 })?.fact + return .none + } +} diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift new file mode 100644 index 00000000..b1a6e267 --- /dev/null +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift @@ -0,0 +1,63 @@ +import ComposableArchitecture +import SwiftUI + +public struct UserSearchResultView: View { + public init(store: Store<UserSearchResultState, UserSearchResultAction>) { + self.store = store + } + + let store: Store<UserSearchResultState, UserSearchResultAction> + + struct ViewState: Equatable { + var username: String? + var email: String? + var phone: String? + + init(state: UserSearchResultState) { + username = state.username + email = state.email + phone = state.phone + } + + var isEmpty: Bool { + username == nil && email == nil && phone == nil + } + } + + public var body: some View { + WithViewStore(store.scope(state: ViewState.init)) { viewStore in + Section { + if viewStore.isEmpty { + Image(systemName: "questionmark") + .frame(maxWidth: .infinity) + } else { + if let username = viewStore.username { + Text(username) + } + if let email = viewStore.email { + Text(email) + } + if let phone = viewStore.phone { + Text(phone) + } + } + } + .task { viewStore.send(.start) } + } + } +} + +#if DEBUG +public struct UserSearchResultView_Previews: PreviewProvider { + public static var previews: some View { + UserSearchResultView(store: Store( + initialState: UserSearchResultState( + id: "contact-id".data(using: .utf8)!, + contact: .unimplemented("contact-data".data(using: .utf8)!) + ), + reducer: .empty, + environment: () + )) + } +} +#endif diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift new file mode 100644 index 00000000..b4f68452 --- /dev/null +++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift @@ -0,0 +1,32 @@ +import ComposableArchitecture +import XCTest +import XXClient +@testable import UserSearchFeature + +final class UserSearchResultFeatureTests: XCTestCase { + func testStart() { + var contact = Contact.unimplemented("contact-data".data(using: .utf8)!) + contact.getFactsFromContact.run = { _ in + [ + Fact(fact: "contact-username", type: 0), + Fact(fact: "contact-email", type: 1), + Fact(fact: "contact-phone", type: 2), + ] + } + + let store = TestStore( + initialState: UserSearchResultState( + id: "contact-id".data(using: .utf8)!, + contact: contact + ), + reducer: userSearchResultReducer, + environment: .unimplemented + ) + + store.send(.start) { + $0.username = "contact-username" + $0.email = "contact-email" + $0.phone = "contact-phone" + } + } +} -- GitLab From 0c2d1aca81121fd603f76715f5e2b7f21858a17c Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 00:07:54 +0200 Subject: [PATCH 02/29] Embed UserSearchResultFeature in UserSearchFeature --- Examples/xx-messenger/Package.swift | 1 + .../AppFeature/AppEnvironment+Live.swift | 5 +- .../UserSearchFeature/UserSearchFeature.swift | 53 +++++++------------ .../UserSearchFeature/UserSearchView.swift | 26 +++------ .../UserSearchFeatureTests.swift | 35 ++---------- 5 files changed, 33 insertions(+), 87 deletions(-) diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index f9ceb75f..565dccc6 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -141,6 +141,7 @@ let package = Package( name: "UserSearchFeature", dependencies: [ .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + .product(name: "ComposablePresentation", package: "swift-composable-presentation"), .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), ], diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index 5d777780..00415b43 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -50,7 +50,10 @@ extension AppEnvironment { UserSearchEnvironment( messenger: messenger, mainQueue: mainQueue, - bgQueue: bgQueue + bgQueue: bgQueue, + result: { + UserSearchResultEnvironment() + } ) } ) diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift index 86a13c0e..9d16d8f5 100644 --- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift @@ -1,4 +1,5 @@ import ComposableArchitecture +import ComposablePresentation import Foundation import XCTestDynamicOverlay import XXClient @@ -11,34 +12,12 @@ public struct UserSearchState: Equatable { case phone } - public struct Result: Equatable, Identifiable { - public init( - id: Data, - contact: Contact, - username: String? = nil, - email: String? = nil, - phone: String? = nil - ) { - self.id = id - self.contact = contact - self.username = username - self.email = email - self.phone = phone - } - - public var id: Data - public var contact: XXClient.Contact - public var username: String? - public var email: String? - public var phone: String? - } - public init( focusedField: Field? = nil, query: MessengerSearchUsers.Query = .init(), isSearching: Bool = false, failure: String? = nil, - results: IdentifiedArrayOf<Result> = [] + results: IdentifiedArrayOf<UserSearchResultState> = [] ) { self.focusedField = focusedField self.query = query @@ -51,7 +30,7 @@ public struct UserSearchState: Equatable { @BindableState public var query: MessengerSearchUsers.Query public var isSearching: Bool public var failure: String? - public var results: IdentifiedArrayOf<Result> + public var results: IdentifiedArrayOf<UserSearchResultState> } public enum UserSearchAction: Equatable, BindableAction { @@ -59,22 +38,26 @@ public enum UserSearchAction: Equatable, BindableAction { case didFail(String) case didSucceed([Contact]) case binding(BindingAction<UserSearchState>) + case result(id: UserSearchResultState.ID, action: UserSearchResultAction) } public struct UserSearchEnvironment { public init( messenger: Messenger, mainQueue: AnySchedulerOf<DispatchQueue>, - bgQueue: AnySchedulerOf<DispatchQueue> + bgQueue: AnySchedulerOf<DispatchQueue>, + result: @escaping () -> UserSearchResultEnvironment ) { self.messenger = messenger self.mainQueue = mainQueue self.bgQueue = bgQueue + self.result = result } public var messenger: Messenger public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> + public var result: () -> UserSearchResultEnvironment } #if DEBUG @@ -82,7 +65,8 @@ extension UserSearchEnvironment { public static let unimplemented = UserSearchEnvironment( messenger: .unimplemented, mainQueue: .unimplemented, - bgQueue: .unimplemented + bgQueue: .unimplemented, + result: { .unimplemented } ) } #endif @@ -111,14 +95,7 @@ public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSe state.failure = nil state.results = IdentifiedArray(uniqueElements: contacts.compactMap { contact in guard let id = try? contact.getId() else { return nil } - let facts = (try? contact.getFacts()) ?? [] - return UserSearchState.Result( - id: id, - contact: contact, - username: facts.first(where: { $0.type == 0 })?.fact, - email: facts.first(where: { $0.type == 1 })?.fact, - phone: facts.first(where: { $0.type == 2 })?.fact - ) + return UserSearchResultState(id: id, contact: contact) }) return .none @@ -128,8 +105,14 @@ public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSe state.results = [] return .none - case .binding(_): + case .binding(_), .result(_, _): return .none } } .binding() +.presenting( + forEach: userSearchResultReducer, + state: \.results, + action: /UserSearchAction.result(id:action:), + environment: { $0.result() } +) diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift index 266938a2..e149ac2a 100644 --- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift @@ -15,14 +15,12 @@ public struct UserSearchView: View { var query: MessengerSearchUsers.Query var isSearching: Bool var failure: String? - var results: IdentifiedArrayOf<UserSearchState.Result> init(state: UserSearchState) { focusedField = state.focusedField query = state.query isSearching = state.isSearching failure = state.failure - results = state.results } } @@ -87,23 +85,13 @@ public struct UserSearchView: View { } } - ForEach(viewStore.results) { result in - Section { - if let username = result.username { - Text(username) - } - if let email = result.email { - Text(email) - } - if let phone = result.phone { - Text(phone) - } - if result.username == nil, result.email == nil, result.phone == nil { - Image(systemName: "questionmark") - .frame(maxWidth: .infinity) - } - } - } + ForEachStore( + store.scope( + state: \.results, + action: UserSearchAction.result(id:action:) + ), + content: UserSearchResultView.init(store:) + ) } .onChange(of: viewStore.focusedField) { focusedField = $0 } .onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) } diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift index 8d2f414a..a96afd95 100644 --- a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift +++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift @@ -19,23 +19,12 @@ final class UserSearchFeatureTests: XCTestCase { var contact1 = Contact.unimplemented("contact-1".data(using: .utf8)!) contact1.getIdFromContact.run = { _ in "contact-1-id".data(using: .utf8)! } - contact1.getFactsFromContact.run = { _ in - [Fact(fact: "contact-1-username", type: 0), - Fact(fact: "contact-1-email", type: 1), - Fact(fact: "contact-1-phone", type: 2)] - } var contact2 = Contact.unimplemented("contact-1".data(using: .utf8)!) contact2.getIdFromContact.run = { _ in "contact-2-id".data(using: .utf8)! } - contact2.getFactsFromContact.run = { _ in - [Fact(fact: "contact-2-username", type: 0), - Fact(fact: "contact-2-email", type: 1), - Fact(fact: "contact-2-phone", type: 2)] - } var contact3 = Contact.unimplemented("contact-3".data(using: .utf8)!) contact3.getIdFromContact.run = { _ in throw GetIdFromContactError() } var contact4 = Contact.unimplemented("contact-4".data(using: .utf8)!) contact4.getIdFromContact.run = { _ in "contact-4-id".data(using: .utf8)! } - contact4.getFactsFromContact.run = { _ in throw GetFactsFromContactError() } let contacts = [contact1, contact2, contact3, contact4] store.environment.bgQueue = .immediate @@ -64,27 +53,9 @@ final class UserSearchFeatureTests: XCTestCase { $0.isSearching = false $0.failure = nil $0.results = [ - .init( - id: "contact-1-id".data(using: .utf8)!, - contact: contact1, - username: "contact-1-username", - email: "contact-1-email", - phone: "contact-1-phone" - ), - .init( - id: "contact-2-id".data(using: .utf8)!, - contact: contact2, - username: "contact-2-username", - email: "contact-2-email", - phone: "contact-2-phone" - ), - .init( - id: "contact-4-id".data(using: .utf8)!, - contact: contact4, - username: nil, - email: nil, - phone: nil - ) + .init(id: "contact-1-id".data(using: .utf8)!, contact: contact1), + .init(id: "contact-2-id".data(using: .utf8)!, contact: contact2), + .init(id: "contact-4-id".data(using: .utf8)!, contact: contact4) ] } } -- GitLab From a35d5b95fce807f9b5dd0b48068d5c3372bca3f9 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 00:10:33 +0200 Subject: [PATCH 03/29] Refactor --- .../Sources/UserSearchFeature/UserSearchFeature.swift | 2 +- .../UserSearchFeature/UserSearchResultFeature.swift | 8 ++++---- .../Sources/UserSearchFeature/UserSearchResultView.swift | 2 +- .../UserSearchFeatureTests/UserSearchFeatureTests.swift | 6 +++--- .../UserSearchResultFeatureTests.swift | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift index 9d16d8f5..84c78195 100644 --- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift @@ -95,7 +95,7 @@ public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSe state.failure = nil state.results = IdentifiedArray(uniqueElements: contacts.compactMap { contact in guard let id = try? contact.getId() else { return nil } - return UserSearchResultState(id: id, contact: contact) + return UserSearchResultState(id: id, xxContact: contact) }) return .none diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift index a224384b..f4c0f856 100644 --- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift @@ -6,20 +6,20 @@ import XXClient public struct UserSearchResultState: Equatable, Identifiable { public init( id: Data, - contact: Contact, + xxContact: Contact, username: String? = nil, email: String? = nil, phone: String? = nil ) { self.id = id - self.contact = contact + self.xxContact = xxContact self.username = username self.email = email self.phone = phone } public var id: Data - public var contact: XXClient.Contact + public var xxContact: XXClient.Contact public var username: String? public var email: String? public var phone: String? @@ -43,7 +43,7 @@ public let userSearchResultReducer = Reducer<UserSearchResultState, UserSearchRe { state, action, env in switch action { case .start: - let facts = (try? state.contact.getFacts()) ?? [] + let facts = (try? state.xxContact.getFacts()) ?? [] state.username = facts.first(where: { $0.type == 0 })?.fact state.email = facts.first(where: { $0.type == 1 })?.fact state.phone = facts.first(where: { $0.type == 2 })?.fact diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift index b1a6e267..129081fe 100644 --- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift @@ -53,7 +53,7 @@ public struct UserSearchResultView_Previews: PreviewProvider { UserSearchResultView(store: Store( initialState: UserSearchResultState( id: "contact-id".data(using: .utf8)!, - contact: .unimplemented("contact-data".data(using: .utf8)!) + xxContact: .unimplemented("contact-data".data(using: .utf8)!) ), reducer: .empty, environment: () diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift index a96afd95..4311b515 100644 --- a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift +++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift @@ -53,9 +53,9 @@ final class UserSearchFeatureTests: XCTestCase { $0.isSearching = false $0.failure = nil $0.results = [ - .init(id: "contact-1-id".data(using: .utf8)!, contact: contact1), - .init(id: "contact-2-id".data(using: .utf8)!, contact: contact2), - .init(id: "contact-4-id".data(using: .utf8)!, contact: contact4) + .init(id: "contact-1-id".data(using: .utf8)!, xxContact: contact1), + .init(id: "contact-2-id".data(using: .utf8)!, xxContact: contact2), + .init(id: "contact-4-id".data(using: .utf8)!, xxContact: contact4) ] } } diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift index b4f68452..c9efe0c7 100644 --- a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift +++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift @@ -17,7 +17,7 @@ final class UserSearchResultFeatureTests: XCTestCase { let store = TestStore( initialState: UserSearchResultState( id: "contact-id".data(using: .utf8)!, - contact: contact + xxContact: contact ), reducer: userSearchResultReducer, environment: .unimplemented -- GitLab From 976cf766afe60447f72706e4f721ed7a17599623 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 00:47:08 +0200 Subject: [PATCH 04/29] Fetch db contact and update UI --- Examples/xx-messenger/Package.swift | 2 + .../AppFeature/AppEnvironment+Live.swift | 6 +- .../UserSearchResultFeature.swift | 45 +++++++++- .../UserSearchResultView.swift | 88 +++++++++++++++++++ .../UserSearchResultFeatureTests.swift | 43 +++++++++ 5 files changed, 180 insertions(+), 4 deletions(-) diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index 565dccc6..8070952e 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -140,10 +140,12 @@ let package = Package( .target( name: "UserSearchFeature", dependencies: [ + .target(name: "AppCore"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "ComposablePresentation", package: "swift-composable-presentation"), .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), .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 00415b43..d8761a65 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -52,7 +52,11 @@ extension AppEnvironment { mainQueue: mainQueue, bgQueue: bgQueue, result: { - UserSearchResultEnvironment() + UserSearchResultEnvironment( + db: dbManager.getDB, + mainQueue: mainQueue, + bgQueue: bgQueue + ) } ) } diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift index f4c0f856..f13c4b10 100644 --- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift @@ -1,18 +1,22 @@ +import AppCore import ComposableArchitecture import Foundation import XCTestDynamicOverlay import XXClient +import XXModels public struct UserSearchResultState: Equatable, Identifiable { public init( id: Data, - xxContact: Contact, + xxContact: XXClient.Contact, + dbContact: XXModels.Contact? = nil, username: String? = nil, email: String? = nil, phone: String? = nil ) { self.id = id self.xxContact = xxContact + self.dbContact = dbContact self.username = username self.email = email self.phone = phone @@ -20,6 +24,7 @@ public struct UserSearchResultState: Equatable, Identifiable { public var id: Data public var xxContact: XXClient.Contact + public var dbContact: XXModels.Contact? public var username: String? public var email: String? public var phone: String? @@ -27,26 +32,60 @@ public struct UserSearchResultState: Equatable, Identifiable { public enum UserSearchResultAction: Equatable { case start + case didUpdateContact(XXModels.Contact?) + case sendRequestButtonTapped } public struct UserSearchResultEnvironment { - public init() {} + public init( + db: DBManagerGetDB, + mainQueue: AnySchedulerOf<DispatchQueue>, + bgQueue: AnySchedulerOf<DispatchQueue> + ) { + self.db = db + self.mainQueue = mainQueue + self.bgQueue = bgQueue + } + + public var db: DBManagerGetDB + public var mainQueue: AnySchedulerOf<DispatchQueue> + public var bgQueue: AnySchedulerOf<DispatchQueue> } #if DEBUG extension UserSearchResultEnvironment { - public static let unimplemented = UserSearchResultEnvironment() + public static let unimplemented = UserSearchResultEnvironment( + db: .unimplemented, + mainQueue: .unimplemented, + bgQueue: .unimplemented + ) } #endif public let userSearchResultReducer = Reducer<UserSearchResultState, UserSearchResultAction, UserSearchResultEnvironment> { state, action, env in + enum DBFetchEffectID {} + switch action { case .start: let facts = (try? state.xxContact.getFacts()) ?? [] state.username = facts.first(where: { $0.type == 0 })?.fact state.email = facts.first(where: { $0.type == 1 })?.fact state.phone = facts.first(where: { $0.type == 2 })?.fact + return try! env.db().fetchContactsPublisher(.init(id: [state.id])) + .assertNoFailure() + .map(\.first) + .map(UserSearchResultAction.didUpdateContact) + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + .cancellable(id: DBFetchEffectID.self, cancelInFlight: true) + + case .didUpdateContact(let contact): + state.dbContact = contact + return .none + + case .sendRequestButtonTapped: return .none } } diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift index 129081fe..4eb4ca8b 100644 --- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift @@ -1,5 +1,6 @@ import ComposableArchitecture import SwiftUI +import XXModels public struct UserSearchResultView: View { public init(store: Store<UserSearchResultState, UserSearchResultAction>) { @@ -12,11 +13,13 @@ public struct UserSearchResultView: View { var username: String? var email: String? var phone: String? + var dbContactAuth: XXModels.Contact.AuthStatus? init(state: UserSearchResultState) { username = state.username email = state.email phone = state.phone + dbContactAuth = state.dbContact?.authStatus } var isEmpty: Bool { @@ -41,6 +44,91 @@ public struct UserSearchResultView: View { Text(phone) } } + switch viewStore.dbContactAuth { + case .none, .stranger: + Button { + viewStore.send(.sendRequestButtonTapped) + } label: { + HStack { + Text("Send request") + Spacer() + Image(systemName: "person.badge.plus") + } + } + + case .requesting: + HStack { + Text("Sending auth request") + Spacer() + ProgressView() + } + + case .requested: + HStack { + Text("Request sent") + Spacer() + Image(systemName: "paperplane") + } + + case .requestFailed: + HStack { + Text("Sending request failed") + Spacer() + Image(systemName: "xmark.diamond.fill") + .foregroundColor(.red) + } + + case .verificationInProgress: + HStack { + Text("Verification is progress") + Spacer() + ProgressView() + } + + case .verified: + HStack { + Text("Verified") + Spacer() + Image(systemName: "person.fill.checkmark") + } + + case .verificationFailed: + HStack { + Text("Verification failed") + Spacer() + Image(systemName: "xmark.diamond.fill") + .foregroundColor(.red) + } + + case .confirming: + HStack { + Text("Confirming auth request") + Spacer() + ProgressView() + } + + case .confirmationFailed: + HStack { + Text("Confirmation failed") + Spacer() + Image(systemName: "xmark.diamond.fill") + .foregroundColor(.red) + } + + case .friend: + HStack { + Text("Friend") + Spacer() + Image(systemName: "person.fill.checkmark") + } + + case .hidden: + HStack { + Text("Hidden") + Spacer() + Image(systemName: "eye.slash") + } + } } .task { viewStore.send(.start) } } diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift index c9efe0c7..e776f803 100644 --- a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift +++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift @@ -1,6 +1,9 @@ +import Combine import ComposableArchitecture import XCTest +import XCTestDynamicOverlay import XXClient +import XXModels @testable import UserSearchFeature final class UserSearchResultFeatureTests: XCTestCase { @@ -23,10 +26,50 @@ final class UserSearchResultFeatureTests: XCTestCase { environment: .unimplemented ) + var dbDidFetchContacts: [XXModels.Contact.Query] = [] + let dbContactsPublisher = PassthroughSubject<[XXModels.Contact], Error>() + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.db.run = { + var db: Database = .failing + db.fetchContactsPublisher.run = { query in + dbDidFetchContacts.append(query) + return dbContactsPublisher.eraseToAnyPublisher() + } + return db + } + store.send(.start) { $0.username = "contact-username" $0.email = "contact-email" $0.phone = "contact-phone" } + + XCTAssertNoDifference(dbDidFetchContacts, [ + .init(id: ["contact-id".data(using: .utf8)!]) + ]) + + let dbContact = XXModels.Contact(id: "contact-id".data(using: .utf8)!) + dbContactsPublisher.send([dbContact]) + + store.receive(.didUpdateContact(dbContact)) { + $0.dbContact = dbContact + } + + dbContactsPublisher.send(completion: .finished) + } + + func testSendRequest() { + let store = TestStore( + initialState: UserSearchResultState( + id: "contact-id".data(using: .utf8)!, + xxContact: .unimplemented("contact-data".data(using: .utf8)!) + ), + reducer: userSearchResultReducer, + environment: .unimplemented + ) + + store.send(.sendRequestButtonTapped) } } -- GitLab From 20f14b6af264204b75fab1254bef710920a85b82 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 10:57:56 +0200 Subject: [PATCH 05/29] Add ContactFeature library --- .../xcschemes/ContactFeature.xcscheme | 78 +++++++++++++++++++ Examples/xx-messenger/Package.swift | 18 +++++ .../xcschemes/XXMessenger.xcscheme | 10 +++ .../ContactFeature/ContactFeature.swift | 28 +++++++ .../Sources/ContactFeature/ContactView.swift | 35 +++++++++ .../ContactFeatureTests.swift | 15 ++++ 6 files changed, 184 insertions(+) create mode 100644 Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactFeature.xcscheme create mode 100644 Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift create mode 100644 Examples/xx-messenger/Sources/ContactFeature/ContactView.swift create mode 100644 Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift diff --git a/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactFeature.xcscheme b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactFeature.xcscheme new file mode 100644 index 00000000..f4e87fe1 --- /dev/null +++ b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactFeature.xcscheme @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1340" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "ContactFeature" + BuildableName = "ContactFeature" + BlueprintName = "ContactFeature" + 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 = "ContactFeatureTests" + BuildableName = "ContactFeatureTests" + BlueprintName = "ContactFeatureTests" + 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 = "ContactFeature" + BuildableName = "ContactFeature" + BlueprintName = "ContactFeature" + 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 8070952e..5ce26cb9 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -20,6 +20,7 @@ let package = Package( products: [ .library(name: "AppCore", targets: ["AppCore"]), .library(name: "AppFeature", targets: ["AppFeature"]), + .library(name: "ContactFeature", targets: ["ContactFeature"]), .library(name: "HomeFeature", targets: ["HomeFeature"]), .library(name: "RegisterFeature", targets: ["RegisterFeature"]), .library(name: "RestoreFeature", targets: ["RestoreFeature"]), @@ -87,6 +88,23 @@ let package = Package( ], swiftSettings: swiftSettings ), + .target( + name: "ContactFeature", + 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: "ContactFeatureTests", + dependencies: [ + .target(name: "ContactFeature"), + ], + swiftSettings: swiftSettings + ), .target( name: "HomeFeature", 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 041cf3f7..0bf8b441 100644 --- a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme +++ b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme @@ -49,6 +49,16 @@ ReferencedContainer = "container:.."> </BuildableReference> </TestableReference> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "ContactFeatureTests" + BuildableName = "ContactFeatureTests" + BlueprintName = "ContactFeatureTests" + ReferencedContainer = "container:.."> + </BuildableReference> + </TestableReference> <TestableReference skipped = "NO"> <BuildableReference diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift new file mode 100644 index 00000000..98344db8 --- /dev/null +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift @@ -0,0 +1,28 @@ +import ComposableArchitecture +import XCTestDynamicOverlay + +public struct ContactState: Equatable { + public init() {} +} + +public enum ContactAction: Equatable { + case start +} + +public struct ContactEnvironment { + public init() {} +} + +#if DEBUG +extension ContactEnvironment { + public static let unimplemented = ContactEnvironment() +} +#endif + +public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironment> +{ state, action, env in + switch action { + case .start: + return .none + } +} diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift new file mode 100644 index 00000000..2b403a21 --- /dev/null +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift @@ -0,0 +1,35 @@ +import ComposableArchitecture +import SwiftUI + +public struct ContactView: View { + public init(store: Store<ContactState, ContactAction>) { + self.store = store + } + + let store: Store<ContactState, ContactAction> + + struct ViewState: Equatable { + init(state: ContactState) {} + } + + public var body: some View { + WithViewStore(store.scope(state: ViewState.init)) { viewStore in + Form { + + } + .navigationTitle("Contact") + } + } +} + +#if DEBUG +public struct ContactView_Previews: PreviewProvider { + public static var previews: some View { + ContactView(store: Store( + initialState: ContactState(), + reducer: .empty, + environment: () + )) + } +} +#endif diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift new file mode 100644 index 00000000..e22cd95b --- /dev/null +++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift @@ -0,0 +1,15 @@ +import ComposableArchitecture +import XCTest +@testable import ContactFeature + +final class ContactFeatureTests: XCTestCase { + func testStart() { + let store = TestStore( + initialState: ContactState(), + reducer: contactReducer, + environment: .unimplemented + ) + + store.send(.start) + } +} -- GitLab From b5bbc0ca1addb287aa42d7e9d923efe5c9ec90a6 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 11:07:16 +0200 Subject: [PATCH 06/29] Update UserSearchResultFeature --- Examples/xx-messenger/Package.swift | 1 - .../UserSearchResultFeature.swift | 43 +------ .../UserSearchResultView.swift | 117 +++--------------- 3 files changed, 24 insertions(+), 137 deletions(-) diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index 5ce26cb9..1254c0e0 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -158,7 +158,6 @@ let package = Package( .target( name: "UserSearchFeature", dependencies: [ - .target(name: "AppCore"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "ComposablePresentation", package: "swift-composable-presentation"), .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift index f13c4b10..ece68742 100644 --- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift @@ -1,22 +1,18 @@ -import AppCore import ComposableArchitecture import Foundation import XCTestDynamicOverlay import XXClient -import XXModels public struct UserSearchResultState: Equatable, Identifiable { public init( id: Data, xxContact: XXClient.Contact, - dbContact: XXModels.Contact? = nil, username: String? = nil, email: String? = nil, phone: String? = nil ) { self.id = id self.xxContact = xxContact - self.dbContact = dbContact self.username = username self.email = email self.phone = phone @@ -24,7 +20,6 @@ public struct UserSearchResultState: Equatable, Identifiable { public var id: Data public var xxContact: XXClient.Contact - public var dbContact: XXModels.Contact? public var username: String? public var email: String? public var phone: String? @@ -32,60 +27,30 @@ public struct UserSearchResultState: Equatable, Identifiable { public enum UserSearchResultAction: Equatable { case start - case didUpdateContact(XXModels.Contact?) - case sendRequestButtonTapped + case tapped } public struct UserSearchResultEnvironment { - public init( - db: DBManagerGetDB, - mainQueue: AnySchedulerOf<DispatchQueue>, - bgQueue: AnySchedulerOf<DispatchQueue> - ) { - self.db = db - self.mainQueue = mainQueue - self.bgQueue = bgQueue - } - - public var db: DBManagerGetDB - public var mainQueue: AnySchedulerOf<DispatchQueue> - public var bgQueue: AnySchedulerOf<DispatchQueue> + public init() {} } #if DEBUG extension UserSearchResultEnvironment { - public static let unimplemented = UserSearchResultEnvironment( - db: .unimplemented, - mainQueue: .unimplemented, - bgQueue: .unimplemented - ) + public static let unimplemented = UserSearchResultEnvironment() } #endif public let userSearchResultReducer = Reducer<UserSearchResultState, UserSearchResultAction, UserSearchResultEnvironment> { state, action, env in - enum DBFetchEffectID {} - switch action { case .start: let facts = (try? state.xxContact.getFacts()) ?? [] state.username = facts.first(where: { $0.type == 0 })?.fact state.email = facts.first(where: { $0.type == 1 })?.fact state.phone = facts.first(where: { $0.type == 2 })?.fact - return try! env.db().fetchContactsPublisher(.init(id: [state.id])) - .assertNoFailure() - .map(\.first) - .map(UserSearchResultAction.didUpdateContact) - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - .cancellable(id: DBFetchEffectID.self, cancelInFlight: true) - - case .didUpdateContact(let contact): - state.dbContact = contact return .none - case .sendRequestButtonTapped: + case .tapped: return .none } } diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift index 4eb4ca8b..fd29a84f 100644 --- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift @@ -13,13 +13,11 @@ public struct UserSearchResultView: View { var username: String? var email: String? var phone: String? - var dbContactAuth: XXModels.Contact.AuthStatus? init(state: UserSearchResultState) { username = state.username email = state.email phone = state.phone - dbContactAuth = state.dbContact?.authStatus } var isEmpty: Bool { @@ -30,103 +28,28 @@ public struct UserSearchResultView: View { public var body: some View { WithViewStore(store.scope(state: ViewState.init)) { viewStore in Section { - if viewStore.isEmpty { - Image(systemName: "questionmark") - .frame(maxWidth: .infinity) - } else { - if let username = viewStore.username { - Text(username) - } - if let email = viewStore.email { - Text(email) - } - if let phone = viewStore.phone { - Text(phone) - } - } - switch viewStore.dbContactAuth { - case .none, .stranger: - Button { - viewStore.send(.sendRequestButtonTapped) - } label: { - HStack { - Text("Send request") - Spacer() - Image(systemName: "person.badge.plus") - } - } - - case .requesting: - HStack { - Text("Sending auth request") - Spacer() - ProgressView() - } - - case .requested: - HStack { - Text("Request sent") - Spacer() - Image(systemName: "paperplane") - } - - case .requestFailed: - HStack { - Text("Sending request failed") - Spacer() - Image(systemName: "xmark.diamond.fill") - .foregroundColor(.red) - } - - case .verificationInProgress: - HStack { - Text("Verification is progress") - Spacer() - ProgressView() - } - - case .verified: - HStack { - Text("Verified") - Spacer() - Image(systemName: "person.fill.checkmark") - } - - case .verificationFailed: - HStack { - Text("Verification failed") - Spacer() - Image(systemName: "xmark.diamond.fill") - .foregroundColor(.red) - } - - case .confirming: - HStack { - Text("Confirming auth request") - Spacer() - ProgressView() - } - - case .confirmationFailed: - HStack { - Text("Confirmation failed") - Spacer() - Image(systemName: "xmark.diamond.fill") - .foregroundColor(.red) - } - - case .friend: + Button { + viewStore.send(.tapped) + } label: { HStack { - Text("Friend") - Spacer() - Image(systemName: "person.fill.checkmark") - } - - case .hidden: - HStack { - Text("Hidden") + VStack { + if viewStore.isEmpty { + Image(systemName: "questionmark") + .frame(maxWidth: .infinity) + } else { + if let username = viewStore.username { + Text(username) + } + if let email = viewStore.email { + Text(email) + } + if let phone = viewStore.phone { + Text(phone) + } + } + } Spacer() - Image(systemName: "eye.slash") + Image(systemName: "chevron.forward") } } } -- GitLab From 94b81c4ce869724070113864a73627f38ba91051 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 11:20:07 +0200 Subject: [PATCH 07/29] Present contact from search results --- Examples/xx-messenger/Package.swift | 2 ++ .../AppFeature/AppEnvironment+Live.swift | 9 +++-- .../UserSearchFeature/UserSearchFeature.swift | 33 +++++++++++++++--- .../UserSearchFeature/UserSearchView.swift | 10 ++++++ .../UserSearchFeatureTests.swift | 34 +++++++++++++++++++ .../UserSearchResultFeatureTests.swift | 33 ++---------------- 6 files changed, 81 insertions(+), 40 deletions(-) diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index 1254c0e0..95a87202 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -69,6 +69,7 @@ let package = Package( name: "AppFeature", dependencies: [ .target(name: "AppCore"), + .target(name: "ContactFeature"), .target(name: "HomeFeature"), .target(name: "RegisterFeature"), .target(name: "RestoreFeature"), @@ -158,6 +159,7 @@ let package = Package( .target( name: "UserSearchFeature", dependencies: [ + .target(name: "ContactFeature"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "ComposablePresentation", package: "swift-composable-presentation"), .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index d8761a65..03c8442c 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -52,11 +52,10 @@ extension AppEnvironment { mainQueue: mainQueue, bgQueue: bgQueue, result: { - UserSearchResultEnvironment( - db: dbManager.getDB, - mainQueue: mainQueue, - bgQueue: bgQueue - ) + UserSearchResultEnvironment() + }, + contact: { + ContactEnvironment() } ) } diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift index 84c78195..425832e2 100644 --- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift @@ -1,5 +1,6 @@ import ComposableArchitecture import ComposablePresentation +import ContactFeature import Foundation import XCTestDynamicOverlay import XXClient @@ -17,13 +18,15 @@ public struct UserSearchState: Equatable { query: MessengerSearchUsers.Query = .init(), isSearching: Bool = false, failure: String? = nil, - results: IdentifiedArrayOf<UserSearchResultState> = [] + results: IdentifiedArrayOf<UserSearchResultState> = [], + contact: ContactState? = nil ) { self.focusedField = focusedField self.query = query self.isSearching = isSearching self.failure = failure self.results = results + self.contact = contact } @BindableState public var focusedField: Field? @@ -31,14 +34,17 @@ public struct UserSearchState: Equatable { public var isSearching: Bool public var failure: String? public var results: IdentifiedArrayOf<UserSearchResultState> + public var contact: ContactState? } public enum UserSearchAction: Equatable, BindableAction { case searchTapped case didFail(String) case didSucceed([Contact]) + case didDismissContact case binding(BindingAction<UserSearchState>) case result(id: UserSearchResultState.ID, action: UserSearchResultAction) + case contact(ContactAction) } public struct UserSearchEnvironment { @@ -46,18 +52,21 @@ public struct UserSearchEnvironment { messenger: Messenger, mainQueue: AnySchedulerOf<DispatchQueue>, bgQueue: AnySchedulerOf<DispatchQueue>, - result: @escaping () -> UserSearchResultEnvironment + result: @escaping () -> UserSearchResultEnvironment, + contact: @escaping () -> ContactEnvironment ) { self.messenger = messenger self.mainQueue = mainQueue self.bgQueue = bgQueue self.result = result + self.contact = contact } public var messenger: Messenger public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> public var result: () -> UserSearchResultEnvironment + public var contact: () -> ContactEnvironment } #if DEBUG @@ -66,7 +75,8 @@ extension UserSearchEnvironment { messenger: .unimplemented, mainQueue: .unimplemented, bgQueue: .unimplemented, - result: { .unimplemented } + result: { .unimplemented }, + contact: { .unimplemented } ) } #endif @@ -105,7 +115,15 @@ public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSe state.results = [] return .none - case .binding(_), .result(_, _): + case .didDismissContact: + state.contact = nil + return .none + + case .result(let id, action: .tapped): + state.contact = ContactState() + return .none + + case .binding(_), .result(_, _), .contact(_): return .none } } @@ -116,3 +134,10 @@ public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSe action: /UserSearchAction.result(id:action:), environment: { $0.result() } ) +.presenting( + contactReducer, + state: .keyPath(\.contact), + id: .notNil(), // TODO: use Contact.ID + action: /UserSearchAction.contact, + environment: { $0.contact() } +) diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift index e149ac2a..f0416b3a 100644 --- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift @@ -1,4 +1,6 @@ import ComposableArchitecture +import ComposablePresentation +import ContactFeature import SwiftUI import XXMessengerClient @@ -96,6 +98,14 @@ public struct UserSearchView: View { .onChange(of: viewStore.focusedField) { focusedField = $0 } .onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) } .navigationTitle("User Search") + .background(NavigationLinkWithStore( + store.scope( + state: \.contact, + action: UserSearchAction.contact + ), + onDeactivate: { viewStore.send(.didDismissContact) }, + destination: ContactView.init(store:) + )) } } } diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift index 4311b515..285d359e 100644 --- a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift +++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift @@ -1,4 +1,5 @@ import ComposableArchitecture +import ContactFeature import XCTest import XXClient import XXMessengerClient @@ -87,4 +88,37 @@ final class UserSearchFeatureTests: XCTestCase { $0.results = [] } } + + func testResultTapped() { + let store = TestStore( + initialState: UserSearchState( + results: [ + .init( + id: "contact-id".data(using: .utf8)!, + xxContact: .unimplemented("contact-data".data(using: .utf8)!) + ) + ] + ), + reducer: userSearchReducer, + environment: .unimplemented + ) + + store.send(.result(id: "contact-id".data(using: .utf8)!, action: .tapped)) { + $0.contact = ContactState() + } + } + + func testDismissingContact() { + let store = TestStore( + initialState: UserSearchState( + contact: ContactState() + ), + reducer: userSearchReducer, + environment: .unimplemented + ) + + store.send(.didDismissContact) { + $0.contact = nil + } + } } diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift index e776f803..c8f2a99b 100644 --- a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift +++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift @@ -1,9 +1,7 @@ -import Combine import ComposableArchitecture import XCTest import XCTestDynamicOverlay import XXClient -import XXModels @testable import UserSearchFeature final class UserSearchResultFeatureTests: XCTestCase { @@ -26,41 +24,14 @@ final class UserSearchResultFeatureTests: XCTestCase { environment: .unimplemented ) - var dbDidFetchContacts: [XXModels.Contact.Query] = [] - let dbContactsPublisher = PassthroughSubject<[XXModels.Contact], Error>() - - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.db.run = { - var db: Database = .failing - db.fetchContactsPublisher.run = { query in - dbDidFetchContacts.append(query) - return dbContactsPublisher.eraseToAnyPublisher() - } - return db - } - store.send(.start) { $0.username = "contact-username" $0.email = "contact-email" $0.phone = "contact-phone" } - - XCTAssertNoDifference(dbDidFetchContacts, [ - .init(id: ["contact-id".data(using: .utf8)!]) - ]) - - let dbContact = XXModels.Contact(id: "contact-id".data(using: .utf8)!) - dbContactsPublisher.send([dbContact]) - - store.receive(.didUpdateContact(dbContact)) { - $0.dbContact = dbContact - } - - dbContactsPublisher.send(completion: .finished) } - func testSendRequest() { + func testTapped() { let store = TestStore( initialState: UserSearchResultState( id: "contact-id".data(using: .utf8)!, @@ -70,6 +41,6 @@ final class UserSearchResultFeatureTests: XCTestCase { environment: .unimplemented ) - store.send(.sendRequestButtonTapped) + store.send(.tapped) } } -- GitLab From f97470fb78aa4fe2048ca2cb3e6fd45f05edc460 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 11:37:58 +0200 Subject: [PATCH 08/29] Update ContactFeature --- .../AppFeature/AppEnvironment+Live.swift | 8 +++- .../ContactFeature/ContactFeature.swift | 43 +++++++++++++++++-- .../Sources/ContactFeature/ContactView.swift | 4 +- .../UserSearchFeature/UserSearchFeature.swift | 5 ++- .../ContactFeatureTests.swift | 4 +- .../UserSearchFeatureTests.swift | 9 +++- 6 files changed, 64 insertions(+), 9 deletions(-) diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index 03c8442c..8e3f48dc 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -1,4 +1,5 @@ import AppCore +import ContactFeature import Foundation import HomeFeature import RegisterFeature @@ -55,7 +56,12 @@ extension AppEnvironment { UserSearchResultEnvironment() }, contact: { - ContactEnvironment() + ContactEnvironment( + messenger: messenger, + db: dbManager.getDB, + mainQueue: mainQueue, + bgQueue: bgQueue + ) } ) } diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift index 98344db8..80fbcc81 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift @@ -1,8 +1,25 @@ +import AppCore import ComposableArchitecture +import Foundation import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels public struct ContactState: Equatable { - public init() {} + public init( + id: Data, + dbContact: XXModels.Contact? = nil, + xxContact: XXClient.Contact? = nil + ) { + self.id = id + self.dbContact = dbContact + self.xxContact = xxContact + } + + public var id: Data + public var dbContact: XXModels.Contact? + public var xxContact: XXClient.Contact? } public enum ContactAction: Equatable { @@ -10,12 +27,32 @@ public enum ContactAction: Equatable { } public struct ContactEnvironment { - public init() {} + public init( + messenger: Messenger, + db: DBManagerGetDB, + mainQueue: AnySchedulerOf<DispatchQueue>, + bgQueue: AnySchedulerOf<DispatchQueue> + ) { + self.messenger = messenger + self.db = db + self.mainQueue = mainQueue + self.bgQueue = bgQueue + } + + public var messenger: Messenger + public var db: DBManagerGetDB + public var mainQueue: AnySchedulerOf<DispatchQueue> + public var bgQueue: AnySchedulerOf<DispatchQueue> } #if DEBUG extension ContactEnvironment { - public static let unimplemented = ContactEnvironment() + public static let unimplemented = ContactEnvironment( + messenger: .unimplemented, + db: .unimplemented, + mainQueue: .unimplemented, + bgQueue: .unimplemented + ) } #endif diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift index 2b403a21..a050747e 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift @@ -26,7 +26,9 @@ public struct ContactView: View { public struct ContactView_Previews: PreviewProvider { public static var previews: some View { ContactView(store: Store( - initialState: ContactState(), + initialState: ContactState( + id: "contact-id".data(using: .utf8)! + ), reducer: .empty, environment: () )) diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift index 425832e2..e8cb92c0 100644 --- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift @@ -120,7 +120,10 @@ public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSe return .none case .result(let id, action: .tapped): - state.contact = ContactState() + state.contact = ContactState( + id: id, + xxContact: state.results[id: id]?.xxContact + ) return .none case .binding(_), .result(_, _), .contact(_): diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift index e22cd95b..4711a132 100644 --- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift @@ -5,7 +5,9 @@ import XCTest final class ContactFeatureTests: XCTestCase { func testStart() { let store = TestStore( - initialState: ContactState(), + initialState: ContactState( + id: "contact-id".data(using: .utf8)! + ), reducer: contactReducer, environment: .unimplemented ) diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift index 285d359e..c457327c 100644 --- a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift +++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift @@ -104,14 +104,19 @@ final class UserSearchFeatureTests: XCTestCase { ) store.send(.result(id: "contact-id".data(using: .utf8)!, action: .tapped)) { - $0.contact = ContactState() + $0.contact = ContactState( + id: "contact-id".data(using: .utf8)!, + xxContact: .unimplemented("contact-data".data(using: .utf8)!) + ) } } func testDismissingContact() { let store = TestStore( initialState: UserSearchState( - contact: ContactState() + contact: ContactState( + id: "contact-id".data(using: .utf8)! + ) ), reducer: userSearchReducer, environment: .unimplemented -- GitLab From d0b2595b96d82eaabedfa33f3761b4e02deb4424 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 11:58:38 +0200 Subject: [PATCH 09/29] Add XXClient.Contact helpers --- Examples/xx-messenger/Package.swift | 1 + .../XXClientHelpers/XXContact+Helpers.swift | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 Examples/xx-messenger/Sources/AppCore/XXClientHelpers/XXContact+Helpers.swift diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index 95a87202..e991313f 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -53,6 +53,7 @@ let package = Package( name: "AppCore", dependencies: [ .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), + .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), .product(name: "XXDatabase", package: "client-ios-db"), .product(name: "XXModels", package: "client-ios-db"), ], diff --git a/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/XXContact+Helpers.swift b/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/XXContact+Helpers.swift new file mode 100644 index 00000000..d2c93683 --- /dev/null +++ b/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/XXContact+Helpers.swift @@ -0,0 +1,15 @@ +import XXClient + +extension Contact { + public var username: String? { + try? getFacts().first(where: { $0.type == 0 })?.fact + } + + public var email: String? { + try? getFacts().first(where: { $0.type == 1 })?.fact + } + + public var phone: String? { + try? getFacts().first(where: { $0.type == 2 })?.fact + } +} -- GitLab From aad544cd6ca3591117c33306ca6f4b1525bcf8d9 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 12:04:13 +0200 Subject: [PATCH 10/29] Fetch db contact in ContactFeature --- .../ContactFeature/ContactFeature.swift | 14 +++ .../Sources/ContactFeature/ContactView.swift | 115 +++++++++++++++++- .../ContactFeatureTests.swift | 30 +++++ 3 files changed, 158 insertions(+), 1 deletion(-) diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift index 80fbcc81..6624bb2c 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift @@ -24,6 +24,7 @@ public struct ContactState: Equatable { public enum ContactAction: Equatable { case start + case dbContactFetched(XXModels.Contact?) } public struct ContactEnvironment { @@ -58,8 +59,21 @@ extension ContactEnvironment { public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironment> { state, action, env in + enum DBFetchEffectID {} + switch action { case .start: + return try! env.db().fetchContactsPublisher(.init(id: [state.id])) + .assertNoFailure() + .map(\.first) + .map(ContactAction.dbContactFetched) + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + .cancellable(id: DBFetchEffectID.self, cancelInFlight: true) + + case .dbContactFetched(let contact): + state.dbContact = contact return .none } } diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift index a050747e..0d985c62 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift @@ -1,5 +1,8 @@ +import AppCore import ComposableArchitecture import SwiftUI +import XXClient +import XXModels public struct ContactView: View { public init(store: Store<ContactState, ContactAction>) { @@ -9,13 +12,123 @@ public struct ContactView: View { let store: Store<ContactState, ContactAction> struct ViewState: Equatable { - init(state: ContactState) {} + var dbContact: XXModels.Contact? + var xxContact: XXClient.Contact? + + init(state: ContactState) { + dbContact = state.dbContact + xxContact = state.xxContact + } } public var body: some View { WithViewStore(store.scope(state: ViewState.init)) { viewStore in Form { + Section { + if let dbContact = viewStore.dbContact { + Label(dbContact.username ?? "", systemImage: "person") + Label(dbContact.email ?? "", systemImage: "envelope") + Label(dbContact.phone ?? "", systemImage: "phone") + } else { + Text("Contact not saved locally") + } + } header: { + Text("Local data") + } + + Section { + Label(viewStore.xxContact?.username ?? "", systemImage: "person") + Label(viewStore.xxContact?.email ?? "", systemImage: "envelope") + Label(viewStore.xxContact?.phone ?? "", systemImage: "phone") + } header: { + Text("Facts") + } + + Section { + switch viewStore.dbContact?.authStatus { + case .none, .stranger: + HStack { + Text("Stranger") + Spacer() + Image(systemName: "person.fill.questionmark") + } + + case .requesting: + HStack { + Text("Sending auth request") + Spacer() + ProgressView() + } + + case .requested: + HStack { + Text("Request sent") + Spacer() + Image(systemName: "paperplane") + } + + case .requestFailed: + HStack { + Text("Sending request failed") + Spacer() + Image(systemName: "xmark.diamond.fill") + .foregroundColor(.red) + } + + case .verificationInProgress: + HStack { + Text("Verification is progress") + Spacer() + ProgressView() + } + + case .verified: + HStack { + Text("Verified") + Spacer() + Image(systemName: "person.fill.checkmark") + } + + case .verificationFailed: + HStack { + Text("Verification failed") + Spacer() + Image(systemName: "xmark.diamond.fill") + .foregroundColor(.red) + } + + case .confirming: + HStack { + Text("Confirming auth request") + Spacer() + ProgressView() + } + + case .confirmationFailed: + HStack { + Text("Confirmation failed") + Spacer() + Image(systemName: "xmark.diamond.fill") + .foregroundColor(.red) + } + + case .friend: + HStack { + Text("Friend") + Spacer() + Image(systemName: "person.fill.checkmark") + } + case .hidden: + HStack { + Text("Hidden") + Spacer() + Image(systemName: "eye.slash") + } + } + } header: { + Text("Auth status") + } } .navigationTitle("Contact") } diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift index 4711a132..5145acce 100644 --- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift @@ -1,5 +1,8 @@ +import Combine import ComposableArchitecture +import CustomDump import XCTest +import XXModels @testable import ContactFeature final class ContactFeatureTests: XCTestCase { @@ -12,6 +15,33 @@ final class ContactFeatureTests: XCTestCase { environment: .unimplemented ) + var dbDidFetchContacts: [XXModels.Contact.Query] = [] + let dbContactsPublisher = PassthroughSubject<[XXModels.Contact], Error>() + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.db.run = { + var db: Database = .failing + db.fetchContactsPublisher.run = { query in + dbDidFetchContacts.append(query) + return dbContactsPublisher.eraseToAnyPublisher() + } + return db + } + store.send(.start) + + XCTAssertNoDifference(dbDidFetchContacts, [ + .init(id: ["contact-id".data(using: .utf8)!]) + ]) + + let dbContact = XXModels.Contact(id: "contact-id".data(using: .utf8)!) + dbContactsPublisher.send([dbContact]) + + store.receive(.dbContactFetched(dbContact)) { + $0.dbContact = dbContact + } + + dbContactsPublisher.send(completion: .finished) } } -- GitLab From 4fbe3b68b073a6659083eb34c51eec2549d35f87 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 12:57:20 +0200 Subject: [PATCH 11/29] Update ContactFeature --- .../ContactFeature/ContactFeature.swift | 8 + .../Sources/ContactFeature/ContactView.swift | 222 ++++++++++-------- .../ContactFeatureTests.swift | 24 ++ 3 files changed, 157 insertions(+), 97 deletions(-) diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift index 6624bb2c..37388359 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift @@ -25,6 +25,8 @@ public struct ContactState: Equatable { public enum ContactAction: Equatable { case start case dbContactFetched(XXModels.Contact?) + case saveFactsTapped + case sendRequestTapped } public struct ContactEnvironment { @@ -75,5 +77,11 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm case .dbContactFetched(let contact): state.dbContact = contact return .none + + case .saveFactsTapped: + return .none + + case .sendRequestTapped: + return .none } } diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift index 0d985c62..ca5eb328 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift @@ -24,110 +24,138 @@ public struct ContactView: View { public var body: some View { WithViewStore(store.scope(state: ViewState.init)) { viewStore in Form { - Section { - if let dbContact = viewStore.dbContact { + if let xxContact = viewStore.xxContact { + Section { + Label(xxContact.username ?? "", systemImage: "person") + Label(xxContact.email ?? "", systemImage: "envelope") + Label(xxContact.phone ?? "", systemImage: "phone") + Button { + viewStore.send(.saveFactsTapped) + } label: { + if viewStore.dbContact == nil { + Text("Save contact") + } else { + Text("Update contact") + } + } + } header: { + Text("Facts") + } + } + + if let dbContact = viewStore.dbContact { + Section { Label(dbContact.username ?? "", systemImage: "person") Label(dbContact.email ?? "", systemImage: "envelope") Label(dbContact.phone ?? "", systemImage: "phone") - } else { - Text("Contact not saved locally") + } header: { + Text("Contact") } - } header: { - Text("Local data") - } - - Section { - Label(viewStore.xxContact?.username ?? "", systemImage: "person") - Label(viewStore.xxContact?.email ?? "", systemImage: "envelope") - Label(viewStore.xxContact?.phone ?? "", systemImage: "phone") - } header: { - Text("Facts") - } - - Section { - switch viewStore.dbContact?.authStatus { - case .none, .stranger: - HStack { - Text("Stranger") - Spacer() - Image(systemName: "person.fill.questionmark") - } - - case .requesting: - HStack { - Text("Sending auth request") - Spacer() - ProgressView() - } - - case .requested: - HStack { - Text("Request sent") - Spacer() - Image(systemName: "paperplane") - } - - case .requestFailed: - HStack { - Text("Sending request failed") - Spacer() - Image(systemName: "xmark.diamond.fill") - .foregroundColor(.red) - } - - case .verificationInProgress: - HStack { - Text("Verification is progress") - Spacer() - ProgressView() - } - - case .verified: - HStack { - Text("Verified") - Spacer() - Image(systemName: "person.fill.checkmark") - } - - case .verificationFailed: - HStack { - Text("Verification failed") - Spacer() - Image(systemName: "xmark.diamond.fill") - .foregroundColor(.red) - } - - case .confirming: - HStack { - Text("Confirming auth request") - Spacer() - ProgressView() - } - - case .confirmationFailed: - HStack { - Text("Confirmation failed") - Spacer() - Image(systemName: "xmark.diamond.fill") - .foregroundColor(.red) - } - - case .friend: - HStack { - Text("Friend") - Spacer() - Image(systemName: "person.fill.checkmark") - } - case .hidden: - HStack { - Text("Hidden") - Spacer() - Image(systemName: "eye.slash") + Section { + switch dbContact.authStatus { + case .stranger: + HStack { + Text("Stranger") + Spacer() + Image(systemName: "person.fill.questionmark") + } + Button { + viewStore.send(.sendRequestTapped) + } label: { + HStack { + Text("Send request") + Spacer() + Image(systemName: "chevron.forward") + } + } + + case .requesting: + HStack { + Text("Sending auth request") + Spacer() + ProgressView() + } + + case .requested: + HStack { + Text("Request sent") + Spacer() + Image(systemName: "chevron.forward") + } + + case .requestFailed: + HStack { + Text("Sending request failed") + Spacer() + Image(systemName: "xmark.diamond.fill") + .foregroundColor(.red) + } + Button { + viewStore.send(.sendRequestTapped) + } label: { + HStack { + Text("Resend request") + Spacer() + Image(systemName: "paperplane") + } + } + + case .verificationInProgress: + HStack { + Text("Verification is progress") + Spacer() + ProgressView() + } + + case .verified: + HStack { + Text("Verified") + Spacer() + Image(systemName: "person.fill.checkmark") + } + + case .verificationFailed: + HStack { + Text("Verification failed") + Spacer() + Image(systemName: "xmark.diamond.fill") + .foregroundColor(.red) + } + + case .confirming: + HStack { + Text("Confirming auth request") + Spacer() + ProgressView() + } + + case .confirmationFailed: + HStack { + Text("Confirmation failed") + Spacer() + Image(systemName: "xmark.diamond.fill") + .foregroundColor(.red) + } + + case .friend: + HStack { + Text("Friend") + Spacer() + Image(systemName: "person.fill.checkmark") + } + + case .hidden: + HStack { + Text("Hidden") + Spacer() + Image(systemName: "eye.slash") + } } + } header: { + Text("Auth status") } - } header: { - Text("Auth status") + .animation(.default, value: viewStore.dbContact?.authStatus) } } .navigationTitle("Contact") diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift index 5145acce..b0d3e8b6 100644 --- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift @@ -44,4 +44,28 @@ final class ContactFeatureTests: XCTestCase { dbContactsPublisher.send(completion: .finished) } + + func testSaveFacts() { + let store = TestStore( + initialState: ContactState( + id: "contact-id".data(using: .utf8)! + ), + reducer: contactReducer, + environment: .unimplemented + ) + + store.send(.saveFactsTapped) + } + + func testSendRequest() { + let store = TestStore( + initialState: ContactState( + id: "contact-id".data(using: .utf8)! + ), + reducer: contactReducer, + environment: .unimplemented + ) + + store.send(.sendRequestTapped) + } } -- GitLab From a05510e2255fc40f2652697420e9bb44b52f391e Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 13:14:16 +0200 Subject: [PATCH 12/29] Save contact --- .../ContactFeature/ContactFeature.swift | 13 ++++++- .../Sources/ContactFeature/ContactView.swift | 1 + .../ContactFeatureTests.swift | 39 ++++++++++++++++++- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift index 37388359..7bbb5f3c 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift @@ -79,7 +79,18 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm return .none case .saveFactsTapped: - return .none + guard let xxContact = state.xxContact else { return .none } + return .fireAndForget { [state] in + var dbContact = state.dbContact ?? XXModels.Contact(id: state.id) + dbContact.marshaled = xxContact.data + dbContact.username = xxContact.username + dbContact.email = xxContact.email + dbContact.phone = xxContact.phone + _ = try! env.db().saveContact(dbContact) + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() case .sendRequestTapped: return .none diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift index ca5eb328..6bf50150 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift @@ -159,6 +159,7 @@ public struct ContactView: View { } } .navigationTitle("Contact") + .task { viewStore.send(.start) } } } } diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift index b0d3e8b6..f14c9e39 100644 --- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift @@ -2,6 +2,7 @@ import Combine import ComposableArchitecture import CustomDump import XCTest +import XXClient import XXModels @testable import ContactFeature @@ -46,15 +47,51 @@ final class ContactFeatureTests: XCTestCase { } func testSaveFacts() { + let dbContact: XXModels.Contact = .init( + id: "contact-id".data(using: .utf8)! + ) + + var xxContact: XXClient.Contact = .unimplemented("contact-data".data(using: .utf8)!) + xxContact.getFactsFromContact.run = { _ in + [ + Fact(fact: "contact-username", type: 0), + Fact(fact: "contact-email", type: 1), + Fact(fact: "contact-phone", type: 2), + ] + } + let store = TestStore( initialState: ContactState( - id: "contact-id".data(using: .utf8)! + id: "contact-id".data(using: .utf8)!, + dbContact: dbContact, + xxContact: xxContact ), reducer: contactReducer, environment: .unimplemented ) + var dbDidSaveContact: [XXModels.Contact] = [] + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.db.run = { + var db: Database = .failing + db.saveContact.run = { contact in + dbDidSaveContact.append(contact) + return contact + } + return db + } + store.send(.saveFactsTapped) + + var expectedSavedContact = dbContact + expectedSavedContact.marshaled = xxContact.data + expectedSavedContact.username = "contact-username" + expectedSavedContact.email = "contact-email" + expectedSavedContact.phone = "contact-phone" + + XCTAssertNoDifference(dbDidSaveContact, [expectedSavedContact]) } func testSendRequest() { -- GitLab From b2b508d289d05d78eeb756ec3dc700f3b9626edd Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 13:26:26 +0200 Subject: [PATCH 13/29] Add Send Request screen --- Examples/xx-messenger/Package.swift | 1 + .../AppFeature/AppEnvironment+Live.swift | 5 ++- .../ContactFeature/ContactFeature.swift | 31 ++++++++++++++-- .../ContactSendRequestFeature.swift | 28 +++++++++++++++ .../ContactSendRequestView.swift | 36 +++++++++++++++++++ .../Sources/ContactFeature/ContactView.swift | 9 +++++ .../ContactFeatureTests.swift | 8 ++++- .../ContactSendRequestFeatureTests.swift | 15 ++++++++ 8 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestFeature.swift create mode 100644 Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestView.swift create mode 100644 Examples/xx-messenger/Tests/ContactFeatureTests/ContactSendRequestFeatureTests.swift diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index e991313f..79123e05 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -95,6 +95,7 @@ let package = Package( dependencies: [ .target(name: "AppCore"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + .product(name: "ComposablePresentation", package: "swift-composable-presentation"), .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), .product(name: "XXModels", package: "client-ios-db"), ], diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index 8e3f48dc..514f56bd 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -60,7 +60,10 @@ extension AppEnvironment { messenger: messenger, db: dbManager.getDB, mainQueue: mainQueue, - bgQueue: bgQueue + bgQueue: bgQueue, + sendRequest: { + ContactSendRequestEnvironment() + } ) } ) diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift index 7bbb5f3c..76715592 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift @@ -1,5 +1,6 @@ import AppCore import ComposableArchitecture +import ComposablePresentation import Foundation import XCTestDynamicOverlay import XXClient @@ -10,16 +11,19 @@ public struct ContactState: Equatable { public init( id: Data, dbContact: XXModels.Contact? = nil, - xxContact: XXClient.Contact? = nil + xxContact: XXClient.Contact? = nil, + sendRequest: ContactSendRequestState? = nil ) { self.id = id self.dbContact = dbContact self.xxContact = xxContact + self.sendRequest = sendRequest } public var id: Data public var dbContact: XXModels.Contact? public var xxContact: XXClient.Contact? + public var sendRequest: ContactSendRequestState? } public enum ContactAction: Equatable { @@ -27,6 +31,8 @@ public enum ContactAction: Equatable { case dbContactFetched(XXModels.Contact?) case saveFactsTapped case sendRequestTapped + case sendRequestDismissed + case sendRequest(ContactSendRequestAction) } public struct ContactEnvironment { @@ -34,18 +40,21 @@ public struct ContactEnvironment { messenger: Messenger, db: DBManagerGetDB, mainQueue: AnySchedulerOf<DispatchQueue>, - bgQueue: AnySchedulerOf<DispatchQueue> + bgQueue: AnySchedulerOf<DispatchQueue>, + sendRequest: @escaping () -> ContactSendRequestEnvironment ) { self.messenger = messenger self.db = db self.mainQueue = mainQueue self.bgQueue = bgQueue + self.sendRequest = sendRequest } public var messenger: Messenger public var db: DBManagerGetDB public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> + public var sendRequest: () -> ContactSendRequestEnvironment } #if DEBUG @@ -54,7 +63,8 @@ extension ContactEnvironment { messenger: .unimplemented, db: .unimplemented, mainQueue: .unimplemented, - bgQueue: .unimplemented + bgQueue: .unimplemented, + sendRequest: { .unimplemented } ) } #endif @@ -93,6 +103,21 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm .eraseToEffect() case .sendRequestTapped: + state.sendRequest = ContactSendRequestState() + return .none + + case .sendRequestDismissed: + state.sendRequest = nil + return .none + + case .sendRequest(_): return .none } } +.presenting( + contactSendRequestReducer, + state: .keyPath(\.sendRequest), + id: .notNil(), + action: /ContactAction.sendRequest, + environment: { $0.sendRequest() } +) diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestFeature.swift new file mode 100644 index 00000000..a0a474e5 --- /dev/null +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestFeature.swift @@ -0,0 +1,28 @@ +import ComposableArchitecture +import XCTestDynamicOverlay + +public struct ContactSendRequestState: Equatable { + public init() {} +} + +public enum ContactSendRequestAction: Equatable { + case start +} + +public struct ContactSendRequestEnvironment { + public init() {} +} + +#if DEBUG +extension ContactSendRequestEnvironment { + public static let unimplemented = ContactSendRequestEnvironment() +} +#endif + +public let contactSendRequestReducer = Reducer<ContactSendRequestState, ContactSendRequestAction, ContactSendRequestEnvironment> +{ state, action, env in + switch action { + case .start: + return .none + } +} diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestView.swift new file mode 100644 index 00000000..4f5048d3 --- /dev/null +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestView.swift @@ -0,0 +1,36 @@ +import ComposableArchitecture +import SwiftUI + +public struct ContactSendRequestView: View { + public init(store: Store<ContactSendRequestState, ContactSendRequestAction>) { + self.store = store + } + + let store: Store<ContactSendRequestState, ContactSendRequestAction> + + struct ViewState: Equatable { + init(state: ContactSendRequestState) {} + } + + public var body: some View { + WithViewStore(store.scope(state: ViewState.init)) { viewStore in + Form { + + } + .navigationTitle("Send Request") + .task { viewStore.send(.start) } + } + } +} + +#if DEBUG +public struct ContactSendRequestView_Previews: PreviewProvider { + public static var previews: some View { + ContactSendRequestView(store: Store( + initialState: ContactSendRequestState(), + reducer: .empty, + environment: () + )) + } +} +#endif diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift index 6bf50150..44a46c56 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift @@ -1,5 +1,6 @@ import AppCore import ComposableArchitecture +import ComposablePresentation import SwiftUI import XXClient import XXModels @@ -160,6 +161,14 @@ public struct ContactView: View { } .navigationTitle("Contact") .task { viewStore.send(.start) } + .background(NavigationLinkWithStore( + store.scope( + state: \.sendRequest, + action: ContactAction.sendRequest + ), + onDeactivate: { viewStore.send(.sendRequestDismissed) }, + destination: ContactSendRequestView.init(store:) + )) } } } diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift index f14c9e39..64d0d72a 100644 --- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift @@ -103,6 +103,12 @@ final class ContactFeatureTests: XCTestCase { environment: .unimplemented ) - store.send(.sendRequestTapped) + store.send(.sendRequestTapped) { + $0.sendRequest = ContactSendRequestState() + } + + store.send(.sendRequestDismissed) { + $0.sendRequest = nil + } } } diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactSendRequestFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactSendRequestFeatureTests.swift new file mode 100644 index 00000000..8d4eb040 --- /dev/null +++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactSendRequestFeatureTests.swift @@ -0,0 +1,15 @@ +import ComposableArchitecture +import XCTest +@testable import ContactFeature + +final class ContactSendRequestFeatureTests: XCTestCase { + func testStart() { + let store = TestStore( + initialState: ContactSendRequestState(), + reducer: contactSendRequestReducer, + environment: .unimplemented + ) + + store.send(.start) + } +} -- GitLab From 4f378901dfa92a0eca8fccd327e671504933c3f9 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 13:42:33 +0200 Subject: [PATCH 14/29] Extract SendRequestFeature to separate library --- .../xcschemes/SendRequestFeature.xcscheme | 78 +++++++++++++++++++ Examples/xx-messenger/Package.swift | 17 ++++ .../xcschemes/XXMessenger.xcscheme | 10 +++ .../AppFeature/AppEnvironment+Live.swift | 2 +- .../ContactFeature/ContactFeature.swift | 15 ++-- .../ContactSendRequestFeature.swift | 28 ------- .../Sources/ContactFeature/ContactView.swift | 3 +- .../SendRequestFeature.swift | 28 +++++++ .../SendRequestView.swift} | 14 ++-- .../ContactFeatureTests.swift | 3 +- .../ContactSendRequestFeatureTests.swift | 15 ---- .../SendRequestFeatureTests.swift | 15 ++++ 12 files changed, 168 insertions(+), 60 deletions(-) create mode 100644 Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/SendRequestFeature.xcscheme delete mode 100644 Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestFeature.swift create mode 100644 Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift rename Examples/xx-messenger/Sources/{ContactFeature/ContactSendRequestView.swift => SendRequestFeature/SendRequestView.swift} (52%) delete mode 100644 Examples/xx-messenger/Tests/ContactFeatureTests/ContactSendRequestFeatureTests.swift create mode 100644 Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift diff --git a/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/SendRequestFeature.xcscheme b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/SendRequestFeature.xcscheme new file mode 100644 index 00000000..2f70b385 --- /dev/null +++ b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/SendRequestFeature.xcscheme @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1340" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "SendRequestFeature" + BuildableName = "SendRequestFeature" + BlueprintName = "SendRequestFeature" + 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 = "SendRequestFeatureTests" + BuildableName = "SendRequestFeatureTests" + BlueprintName = "SendRequestFeatureTests" + 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 = "SendRequestFeature" + BuildableName = "SendRequestFeature" + BlueprintName = "SendRequestFeature" + 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 79123e05..bbd8afa7 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -24,6 +24,7 @@ let package = Package( .library(name: "HomeFeature", targets: ["HomeFeature"]), .library(name: "RegisterFeature", targets: ["RegisterFeature"]), .library(name: "RestoreFeature", targets: ["RestoreFeature"]), + .library(name: "SendRequestFeature", targets: ["SendRequestFeature"]), .library(name: "UserSearchFeature", targets: ["UserSearchFeature"]), .library(name: "WelcomeFeature", targets: ["WelcomeFeature"]), ], @@ -74,6 +75,7 @@ let package = Package( .target(name: "HomeFeature"), .target(name: "RegisterFeature"), .target(name: "RestoreFeature"), + .target(name: "SendRequestFeature"), .target(name: "UserSearchFeature"), .target(name: "WelcomeFeature"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), @@ -94,6 +96,7 @@ let package = Package( name: "ContactFeature", dependencies: [ .target(name: "AppCore"), + .target(name: "SendRequestFeature"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "ComposablePresentation", package: "swift-composable-presentation"), .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), @@ -158,6 +161,20 @@ let package = Package( ], swiftSettings: swiftSettings ), + .target( + name: "SendRequestFeature", + dependencies: [ + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + ], + swiftSettings: swiftSettings + ), + .testTarget( + name: "SendRequestFeatureTests", + dependencies: [ + .target(name: "SendRequestFeature"), + ], + swiftSettings: swiftSettings + ), .target( name: "UserSearchFeature", 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 0bf8b441..0d7cb106 100644 --- a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme +++ b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme @@ -89,6 +89,16 @@ ReferencedContainer = "container:.."> </BuildableReference> </TestableReference> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "SendRequestFeatureTests" + BuildableName = "SendRequestFeatureTests" + BlueprintName = "SendRequestFeatureTests" + ReferencedContainer = "container:.."> + </BuildableReference> + </TestableReference> <TestableReference skipped = "NO"> <BuildableReference diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index 514f56bd..a060ad48 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -62,7 +62,7 @@ extension AppEnvironment { mainQueue: mainQueue, bgQueue: bgQueue, sendRequest: { - ContactSendRequestEnvironment() + SendRequestEnvironment() } ) } diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift index 76715592..99b7b2bc 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift @@ -2,6 +2,7 @@ import AppCore import ComposableArchitecture import ComposablePresentation import Foundation +import SendRequestFeature import XCTestDynamicOverlay import XXClient import XXMessengerClient @@ -12,7 +13,7 @@ public struct ContactState: Equatable { id: Data, dbContact: XXModels.Contact? = nil, xxContact: XXClient.Contact? = nil, - sendRequest: ContactSendRequestState? = nil + sendRequest: SendRequestState? = nil ) { self.id = id self.dbContact = dbContact @@ -23,7 +24,7 @@ public struct ContactState: Equatable { public var id: Data public var dbContact: XXModels.Contact? public var xxContact: XXClient.Contact? - public var sendRequest: ContactSendRequestState? + public var sendRequest: SendRequestState? } public enum ContactAction: Equatable { @@ -32,7 +33,7 @@ public enum ContactAction: Equatable { case saveFactsTapped case sendRequestTapped case sendRequestDismissed - case sendRequest(ContactSendRequestAction) + case sendRequest(SendRequestAction) } public struct ContactEnvironment { @@ -41,7 +42,7 @@ public struct ContactEnvironment { db: DBManagerGetDB, mainQueue: AnySchedulerOf<DispatchQueue>, bgQueue: AnySchedulerOf<DispatchQueue>, - sendRequest: @escaping () -> ContactSendRequestEnvironment + sendRequest: @escaping () -> SendRequestEnvironment ) { self.messenger = messenger self.db = db @@ -54,7 +55,7 @@ public struct ContactEnvironment { public var db: DBManagerGetDB public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> - public var sendRequest: () -> ContactSendRequestEnvironment + public var sendRequest: () -> SendRequestEnvironment } #if DEBUG @@ -103,7 +104,7 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm .eraseToEffect() case .sendRequestTapped: - state.sendRequest = ContactSendRequestState() + state.sendRequest = SendRequestState() return .none case .sendRequestDismissed: @@ -115,7 +116,7 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm } } .presenting( - contactSendRequestReducer, + sendRequestReducer, state: .keyPath(\.sendRequest), id: .notNil(), action: /ContactAction.sendRequest, diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestFeature.swift deleted file mode 100644 index a0a474e5..00000000 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestFeature.swift +++ /dev/null @@ -1,28 +0,0 @@ -import ComposableArchitecture -import XCTestDynamicOverlay - -public struct ContactSendRequestState: Equatable { - public init() {} -} - -public enum ContactSendRequestAction: Equatable { - case start -} - -public struct ContactSendRequestEnvironment { - public init() {} -} - -#if DEBUG -extension ContactSendRequestEnvironment { - public static let unimplemented = ContactSendRequestEnvironment() -} -#endif - -public let contactSendRequestReducer = Reducer<ContactSendRequestState, ContactSendRequestAction, ContactSendRequestEnvironment> -{ state, action, env in - switch action { - case .start: - return .none - } -} diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift index 44a46c56..02f12d62 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift @@ -1,6 +1,7 @@ import AppCore import ComposableArchitecture import ComposablePresentation +import SendRequestFeature import SwiftUI import XXClient import XXModels @@ -167,7 +168,7 @@ public struct ContactView: View { action: ContactAction.sendRequest ), onDeactivate: { viewStore.send(.sendRequestDismissed) }, - destination: ContactSendRequestView.init(store:) + destination: SendRequestView.init(store:) )) } } diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift new file mode 100644 index 00000000..a6683c29 --- /dev/null +++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift @@ -0,0 +1,28 @@ +import ComposableArchitecture +import XCTestDynamicOverlay + +public struct SendRequestState: Equatable { + public init() {} +} + +public enum SendRequestAction: Equatable { + case start +} + +public struct SendRequestEnvironment { + public init() {} +} + +#if DEBUG +extension SendRequestEnvironment { + public static let unimplemented = SendRequestEnvironment() +} +#endif + +public let sendRequestReducer = Reducer<SendRequestState, SendRequestAction, SendRequestEnvironment> +{ state, action, env in + switch action { + case .start: + return .none + } +} diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestView.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift similarity index 52% rename from Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestView.swift rename to Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift index 4f5048d3..40042466 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestView.swift +++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift @@ -1,15 +1,15 @@ import ComposableArchitecture import SwiftUI -public struct ContactSendRequestView: View { - public init(store: Store<ContactSendRequestState, ContactSendRequestAction>) { +public struct SendRequestView: View { + public init(store: Store<SendRequestState, SendRequestAction>) { self.store = store } - let store: Store<ContactSendRequestState, ContactSendRequestAction> + let store: Store<SendRequestState, SendRequestAction> struct ViewState: Equatable { - init(state: ContactSendRequestState) {} + init(state: SendRequestState) {} } public var body: some View { @@ -24,10 +24,10 @@ public struct ContactSendRequestView: View { } #if DEBUG -public struct ContactSendRequestView_Previews: PreviewProvider { +public struct SendRequestView_Previews: PreviewProvider { public static var previews: some View { - ContactSendRequestView(store: Store( - initialState: ContactSendRequestState(), + SendRequestView(store: Store( + initialState: SendRequestState(), reducer: .empty, environment: () )) diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift index 64d0d72a..9f323870 100644 --- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift @@ -1,6 +1,7 @@ import Combine import ComposableArchitecture import CustomDump +import SendRequestFeature import XCTest import XXClient import XXModels @@ -104,7 +105,7 @@ final class ContactFeatureTests: XCTestCase { ) store.send(.sendRequestTapped) { - $0.sendRequest = ContactSendRequestState() + $0.sendRequest = SendRequestState() } store.send(.sendRequestDismissed) { diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactSendRequestFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactSendRequestFeatureTests.swift deleted file mode 100644 index 8d4eb040..00000000 --- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactSendRequestFeatureTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import ComposableArchitecture -import XCTest -@testable import ContactFeature - -final class ContactSendRequestFeatureTests: XCTestCase { - func testStart() { - let store = TestStore( - initialState: ContactSendRequestState(), - reducer: contactSendRequestReducer, - environment: .unimplemented - ) - - store.send(.start) - } -} diff --git a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift new file mode 100644 index 00000000..c64665d2 --- /dev/null +++ b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift @@ -0,0 +1,15 @@ +import ComposableArchitecture +import XCTest +@testable import SendRequestFeature + +final class SendRequestFeatureTests: XCTestCase { + func testStart() { + let store = TestStore( + initialState: SendRequestState(), + reducer: sendRequestReducer, + environment: .unimplemented + ) + + store.send(.start) + } +} -- GitLab From adce3dd232c6a63a08e21c6b74752109cd168018 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 14:21:42 +0200 Subject: [PATCH 15/29] Implement SendRequest UI --- Examples/xx-messenger/Package.swift | 4 + .../AppFeature/AppEnvironment+Live.swift | 1 + .../ContactFeature/ContactFeature.swift | 6 +- .../SendRequestFeature.swift | 38 +++++- .../SendRequestFeature/SendRequestView.swift | 129 +++++++++++++++++- .../ContactFeatureTests.swift | 40 +++++- .../SendRequestFeatureTests.swift | 4 +- 7 files changed, 209 insertions(+), 13 deletions(-) diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index bbd8afa7..7a0357bd 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -164,7 +164,11 @@ let package = Package( .target( name: "SendRequestFeature", dependencies: [ + .target(name: "AppCore"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), + .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 a060ad48..49ea8087 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -4,6 +4,7 @@ import Foundation import HomeFeature import RegisterFeature import RestoreFeature +import SendRequestFeature import UserSearchFeature import WelcomeFeature import XXMessengerClient diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift index 99b7b2bc..0bf0e29b 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift @@ -104,7 +104,11 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm .eraseToEffect() case .sendRequestTapped: - state.sendRequest = SendRequestState() + if let xxContact = state.xxContact { + state.sendRequest = SendRequestState(contact: xxContact) + } else if let marshaled = state.dbContact?.marshaled { + state.sendRequest = SendRequestState(contact: .live(marshaled)) + } return .none case .sendRequestDismissed: diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift index a6683c29..cefd80d3 100644 --- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift +++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift @@ -1,12 +1,40 @@ import ComposableArchitecture import XCTestDynamicOverlay +import XXClient +import XXModels public struct SendRequestState: Equatable { - public init() {} + public init( + contact: XXClient.Contact, + myContact: XXClient.Contact? = nil, + sendUsername: Bool = true, + sendEmail: Bool = true, + sendPhone: Bool = true, + isSending: Bool = false, + failure: String? = nil + ) { + self.contact = contact + self.myContact = myContact + self.sendUsername = sendUsername + self.sendEmail = sendEmail + self.sendPhone = sendPhone + self.isSending = isSending + self.failure = failure + } + + public var contact: XXClient.Contact + public var myContact: XXClient.Contact? + @BindableState public var sendUsername: Bool + @BindableState public var sendEmail: Bool + @BindableState public var sendPhone: Bool + public var isSending: Bool + public var failure: String? } -public enum SendRequestAction: Equatable { +public enum SendRequestAction: Equatable, BindableAction { case start + case sendTapped + case binding(BindingAction<SendRequestState>) } public struct SendRequestEnvironment { @@ -24,5 +52,11 @@ public let sendRequestReducer = Reducer<SendRequestState, SendRequestAction, Sen switch action { case .start: return .none + + case .sendTapped: + return .none + + case .binding(_): + return .none } } diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift index 40042466..f1bd8ff5 100644 --- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift +++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift @@ -1,5 +1,7 @@ +import AppCore import ComposableArchitecture import SwiftUI +import XXClient public struct SendRequestView: View { public init(store: Store<SendRequestState, SendRequestAction>) { @@ -9,13 +11,101 @@ public struct SendRequestView: View { let store: Store<SendRequestState, SendRequestAction> struct ViewState: Equatable { - init(state: SendRequestState) {} + var contact: XXClient.Contact + var myContact: XXClient.Contact? + var sendUsername: Bool + var sendEmail: Bool + var sendPhone: Bool + var isSending: Bool + var failure: String? + + init(state: SendRequestState) { + contact = state.contact + myContact = state.myContact + sendUsername = state.sendUsername + sendEmail = state.sendEmail + sendPhone = state.sendPhone + isSending = state.isSending + failure = state.failure + } } public var body: some View { WithViewStore(store.scope(state: ViewState.init)) { viewStore in Form { + Section { + HStack { + Label(viewStore.myContact?.username ?? "", systemImage: "person") + Spacer() + Toggle( + isOn: viewStore.binding( + get: \.sendUsername, + send: { SendRequestAction.set(\.$sendUsername, $0) } + ), + label: EmptyView.init + ) + } + + HStack { + Label(viewStore.myContact?.email ?? "", systemImage: "envelope") + Spacer() + Toggle( + isOn: viewStore.binding( + get: \.sendEmail, + send: { SendRequestAction.set(\.$sendEmail, $0) } + ), + label: EmptyView.init + ) + } + + HStack { + Label(viewStore.myContact?.phone ?? "", systemImage: "phone") + Spacer() + Toggle( + isOn: viewStore.binding( + get: \.sendPhone, + send: { SendRequestAction.set(\.$sendPhone, $0) } + ), + label: EmptyView.init + ) + } + } header: { + Text("My facts") + } + .disabled(viewStore.isSending) + Section { + Label(viewStore.contact.username ?? "", systemImage: "person") + Label(viewStore.contact.email ?? "", systemImage: "envelope") + Label(viewStore.contact.phone ?? "", systemImage: "phone") + } header: { + Text("Contact") + } + + Section { + Button { + viewStore.send(.sendTapped) + } label: { + HStack { + Text("Send request") + Spacer() + if viewStore.isSending { + ProgressView() + } else { + Image(systemName: "paperplane") + } + } + } + } + .disabled(viewStore.isSending) + + if let failure = viewStore.failure { + Section { + Text(failure) + } header: { + Text("Error") + } + } } .navigationTitle("Send Request") .task { viewStore.send(.start) } @@ -26,11 +116,38 @@ public struct SendRequestView: View { #if DEBUG public struct SendRequestView_Previews: PreviewProvider { public static var previews: some View { - SendRequestView(store: Store( - initialState: SendRequestState(), - reducer: .empty, - environment: () - )) + NavigationView { + SendRequestView(store: Store( + initialState: SendRequestState( + contact: { + var contact = XXClient.Contact.unimplemented("contact-data".data(using: .utf8)!) + contact.getFactsFromContact.run = { _ in + [ + Fact(fact: "contact-username", type: 0), + Fact(fact: "contact-email", type: 1), + Fact(fact: "contact-phone", type: 2), + ] + } + return contact + }(), + myContact: { + var contact = XXClient.Contact.unimplemented("my-data".data(using: .utf8)!) + contact.getFactsFromContact.run = { _ in + [ + Fact(fact: "my-username", type: 0), + Fact(fact: "my-email", type: 1), + Fact(fact: "my-phone", type: 2), + ] + } + return contact + }(), + isSending: true, + failure: "Something went wrong" + ), + reducer: .empty, + environment: () + )) + } } } #endif diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift index 9f323870..e629b3f0 100644 --- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift @@ -95,18 +95,52 @@ final class ContactFeatureTests: XCTestCase { XCTAssertNoDifference(dbDidSaveContact, [expectedSavedContact]) } - func testSendRequest() { + func testSendRequestWithDBContact() { + var dbContact = XXModels.Contact(id: "contact-id".data(using: .utf8)!) + dbContact.marshaled = "contact-data".data(using: .utf8)! + let store = TestStore( initialState: ContactState( - id: "contact-id".data(using: .utf8)! + id: dbContact.id, + dbContact: dbContact ), reducer: contactReducer, environment: .unimplemented ) store.send(.sendRequestTapped) { - $0.sendRequest = SendRequestState() + $0.sendRequest = SendRequestState(contact: .live(dbContact.marshaled!)) } + } + + func testSendRequestWithXXContact() { + let xxContact = XXClient.Contact.unimplemented("contact-id".data(using: .utf8)!) + + let store = TestStore( + initialState: ContactState( + id: "contact-id".data(using: .utf8)!, + xxContact: xxContact + ), + reducer: contactReducer, + environment: .unimplemented + ) + + store.send(.sendRequestTapped) { + $0.sendRequest = SendRequestState(contact: xxContact) + } + } + + func testSendRequestDismissed() { + let store = TestStore( + initialState: ContactState( + id: "contact-id".data(using: .utf8)!, + sendRequest: SendRequestState( + contact: .unimplemented("contact-id".data(using: .utf8)!) + ) + ), + reducer: contactReducer, + environment: .unimplemented + ) store.send(.sendRequestDismissed) { $0.sendRequest = nil diff --git a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift index c64665d2..a67a241c 100644 --- a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift +++ b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift @@ -5,7 +5,9 @@ import XCTest final class SendRequestFeatureTests: XCTestCase { func testStart() { let store = TestStore( - initialState: SendRequestState(), + initialState: SendRequestState( + contact: .unimplemented("contact-data".data(using: .utf8)!) + ), reducer: sendRequestReducer, environment: .unimplemented ) -- GitLab From 7a15a34db018b6c76d58cf308ce2738b2e6c35f7 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 14:53:12 +0200 Subject: [PATCH 16/29] Fix saving contact when registering --- Examples/xx-messenger/Package.swift | 1 + .../Sources/RegisterFeature/RegisterFeature.swift | 7 ++++++- .../Tests/RegisterFeatureTests/RegisterFeatureTests.swift | 7 +++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index 7a0357bd..77b97a0f 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -135,6 +135,7 @@ let package = Package( dependencies: [ .target(name: "AppCore"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), .product(name: "XXModels", package: "client-ios-db"), ], diff --git a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift index 8e1f3541..b55cae7d 100644 --- a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift +++ b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift @@ -2,6 +2,7 @@ import AppCore import ComposableArchitecture import Foundation import XCTestDynamicOverlay +import XXClient import XXMessengerClient import XXModels @@ -81,7 +82,11 @@ public let registerReducer = Reducer<RegisterState, RegisterAction, RegisterEnvi do { let db = try env.db() try env.messenger.register(username: username) - let contact = env.messenger.e2e()!.getContact() + var contact = try env.messenger.e2e.tryGet().getContact() + var facts: [Fact] = (try? contact.getFacts()) ?? [] + facts.removeAll(where: { $0.type == 0 }) + facts.append(Fact(fact: username, type: 0)) + try contact.setFacts(facts) try db.saveContact(Contact( id: try contact.getId(), marshaled: contact.data, diff --git a/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift b/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift index a2aad5bd..12addba7 100644 --- a/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift +++ b/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift @@ -16,6 +16,7 @@ final class RegisterFeatureTests: XCTestCase { let now = Date() let mainQueue = DispatchQueue.test let bgQueue = DispatchQueue.test + var didSetFactsOnContact: [[XXClient.Fact]] = [] var dbDidSaveContact: [XXModels.Contact] = [] var messengerDidRegisterUsername: [String] = [] @@ -30,6 +31,11 @@ final class RegisterFeatureTests: XCTestCase { 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 @@ -57,6 +63,7 @@ final class RegisterFeatureTests: XCTestCase { bgQueue.advance() XCTAssertNoDifference(messengerDidRegisterUsername, ["NewUser"]) + XCTAssertNoDifference(didSetFactsOnContact, [[Fact(fact: "NewUser", type: 0)]]) XCTAssertNoDifference(dbDidSaveContact, [ XXModels.Contact( id: "contact-id".data(using: .utf8)!, -- GitLab From f7ff8b5949868a7cfbf368433dbff212d8465b8b Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 15:03:00 +0200 Subject: [PATCH 17/29] Add DBManagerRemoveDB function --- .../Sources/AppCore/DBManager/DBManager.swift | 7 +++-- .../AppCore/DBManager/DBManagerRemoveDB.swift | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 Examples/xx-messenger/Sources/AppCore/DBManager/DBManagerRemoveDB.swift diff --git a/Examples/xx-messenger/Sources/AppCore/DBManager/DBManager.swift b/Examples/xx-messenger/Sources/AppCore/DBManager/DBManager.swift index 178e1078..f591dc1c 100644 --- a/Examples/xx-messenger/Sources/AppCore/DBManager/DBManager.swift +++ b/Examples/xx-messenger/Sources/AppCore/DBManager/DBManager.swift @@ -4,6 +4,7 @@ public struct DBManager { public var hasDB: DBManagerHasDB public var makeDB: DBManagerMakeDB public var getDB: DBManagerGetDB + public var removeDB: DBManagerRemoveDB } extension DBManager { @@ -17,7 +18,8 @@ extension DBManager { return DBManager( hasDB: .init { container.db != nil }, makeDB: .live(setDB: { container.db = $0 }), - getDB: .live(getDB: { container.db }) + getDB: .live(getDB: { container.db }), + removeDB: .live(getDB: { container.db }, unsetDB: { container.db = nil }) ) } } @@ -26,6 +28,7 @@ extension DBManager { public static let unimplemented = DBManager( hasDB: .unimplemented, makeDB: .unimplemented, - getDB: .unimplemented + getDB: .unimplemented, + removeDB: .unimplemented ) } diff --git a/Examples/xx-messenger/Sources/AppCore/DBManager/DBManagerRemoveDB.swift b/Examples/xx-messenger/Sources/AppCore/DBManager/DBManagerRemoveDB.swift new file mode 100644 index 00000000..69ab6e02 --- /dev/null +++ b/Examples/xx-messenger/Sources/AppCore/DBManager/DBManagerRemoveDB.swift @@ -0,0 +1,30 @@ +import Foundation +import XCTestDynamicOverlay +import XXDatabase +import XXModels + +public struct DBManagerRemoveDB { + public var run: () throws -> Void + + public func callAsFunction() throws -> Void { + try run() + } +} + +extension DBManagerRemoveDB { + public static func live( + getDB: @escaping () -> Database?, + unsetDB: @escaping () -> Void + ) -> DBManagerRemoveDB { + DBManagerRemoveDB { + try getDB()?.drop() + unsetDB() + } + } +} + +extension DBManagerRemoveDB { + public static let unimplemented = DBManagerRemoveDB( + run: XCTUnimplemented("\(Self.self)") + ) +} -- GitLab From 557dadbc709a4a3d5b8b0be20926de01b9f18f90 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 15:03:44 +0200 Subject: [PATCH 18/29] Remove db on account deletion --- .../Sources/AppFeature/AppEnvironment+Live.swift | 2 +- .../Sources/HomeFeature/HomeFeature.swift | 12 ++++++------ .../Tests/HomeFeatureTests/HomeFeatureTests.swift | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index 49ea8087..9b298c38 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -36,7 +36,7 @@ extension AppEnvironment { home: { HomeEnvironment( messenger: messenger, - db: dbManager.getDB, + dbManager: dbManager, mainQueue: mainQueue, bgQueue: bgQueue, register: { diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift index 51083ea0..712855ba 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift @@ -71,14 +71,14 @@ public enum HomeAction: Equatable { public struct HomeEnvironment { public init( messenger: Messenger, - db: DBManagerGetDB, + dbManager: DBManager, mainQueue: AnySchedulerOf<DispatchQueue>, bgQueue: AnySchedulerOf<DispatchQueue>, register: @escaping () -> RegisterEnvironment, userSearch: @escaping () -> UserSearchEnvironment ) { self.messenger = messenger - self.db = db + self.dbManager = dbManager self.mainQueue = mainQueue self.bgQueue = bgQueue self.register = register @@ -86,7 +86,7 @@ public struct HomeEnvironment { } public var messenger: Messenger - public var db: DBManagerGetDB + public var dbManager: DBManager public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> public var register: () -> RegisterEnvironment @@ -96,7 +96,7 @@ public struct HomeEnvironment { extension HomeEnvironment { public static let unimplemented = HomeEnvironment( messenger: .unimplemented, - db: .unimplemented, + dbManager: .unimplemented, mainQueue: .unimplemented, bgQueue: .unimplemented, register: { .unimplemented }, @@ -197,13 +197,13 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> return .result { do { let contactId = try env.messenger.e2e.tryGet().getContact().getId() - let contact = try env.db().fetchContacts(.init(id: [contactId])).first + let contact = try env.dbManager.getDB().fetchContacts(.init(id: [contactId])).first if let username = contact?.username { let ud = try env.messenger.ud.tryGet() try ud.permanentDeleteAccount(username: Fact(fact: username, type: 0)) } try env.messenger.destroy() - try env.db().drop() + try env.dbManager.removeDB() return .success(.deleteAccount(.success)) } catch { return .success(.deleteAccount(.failure(error as NSError))) diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift index aa309016..cbc261ce 100644 --- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift +++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift @@ -316,7 +316,7 @@ final class HomeFeatureTests: XCTestCase { var dbDidFetchContacts: [XXModels.Contact.Query] = [] var udDidPermanentDeleteAccount: [Fact] = [] var messengerDidDestroy = 0 - var dbDidDrop = 0 + var didRemoveDB = 0 store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate @@ -329,7 +329,7 @@ final class HomeFeatureTests: XCTestCase { } return e2e } - store.environment.db.run = { + store.environment.dbManager.getDB.run = { var db: Database = .failing db.fetchContacts.run = { query in dbDidFetchContacts.append(query) @@ -341,11 +341,11 @@ final class HomeFeatureTests: XCTestCase { ) ] } - db.drop.run = { - dbDidDrop += 1 - } return db } + store.environment.dbManager.removeDB.run = { + didRemoveDB += 1 + } store.environment.messenger.ud.get = { var ud: UserDiscovery = .unimplemented ud.permanentDeleteAccount.run = { usernameFact in @@ -372,7 +372,7 @@ final class HomeFeatureTests: XCTestCase { XCTAssertNoDifference(dbDidFetchContacts, [.init(id: ["contact-id".data(using: .utf8)!])]) XCTAssertNoDifference(udDidPermanentDeleteAccount, [Fact(fact: "MyUsername", type: 0)]) XCTAssertNoDifference(messengerDidDestroy, 1) - XCTAssertNoDifference(dbDidDrop, 1) + XCTAssertNoDifference(didRemoveDB, 1) store.receive(.deleteAccount(.success)) { $0.isDeletingAccount = false -- GitLab From 15798ff9a952591d190a899230c8bb4a560e4058 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 15:18:40 +0200 Subject: [PATCH 19/29] Fetch my facts in SendRequestFeature --- .../AppFeature/AppEnvironment+Live.swift | 7 +- .../SendRequestFeature.swift | 46 ++++++++++++- .../SendRequestFeatureTests.swift | 68 +++++++++++++++++++ 3 files changed, 118 insertions(+), 3 deletions(-) diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index 9b298c38..298cb9f6 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -63,7 +63,12 @@ extension AppEnvironment { mainQueue: mainQueue, bgQueue: bgQueue, sendRequest: { - SendRequestEnvironment() + SendRequestEnvironment( + messenger: messenger, + db: dbManager.getDB, + mainQueue: mainQueue, + bgQueue: bgQueue + ) } ) } diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift index cefd80d3..8740c744 100644 --- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift +++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift @@ -1,6 +1,9 @@ +import AppCore import ComposableArchitecture +import Foundation import XCTestDynamicOverlay import XXClient +import XXMessengerClient import XXModels public struct SendRequestState: Equatable { @@ -35,22 +38,60 @@ public enum SendRequestAction: Equatable, BindableAction { case start case sendTapped case binding(BindingAction<SendRequestState>) + case myContactFetched(XXClient.Contact?) } public struct SendRequestEnvironment { - public init() {} + public init( + messenger: Messenger, + db: DBManagerGetDB, + mainQueue: AnySchedulerOf<DispatchQueue>, + bgQueue: AnySchedulerOf<DispatchQueue> + ) { + self.messenger = messenger + self.db = db + self.mainQueue = mainQueue + self.bgQueue = bgQueue + } + + public var messenger: Messenger + public var db: DBManagerGetDB + public var mainQueue: AnySchedulerOf<DispatchQueue> + public var bgQueue: AnySchedulerOf<DispatchQueue> } #if DEBUG extension SendRequestEnvironment { - public static let unimplemented = SendRequestEnvironment() + public static let unimplemented = SendRequestEnvironment( + messenger: .unimplemented, + db: .unimplemented, + mainQueue: .unimplemented, + bgQueue: .unimplemented + ) } #endif 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) + + case .myContactFetched(let contact): + state.myContact = contact return .none case .sendTapped: @@ -60,3 +101,4 @@ public let sendRequestReducer = Reducer<SendRequestState, SendRequestAction, Sen return .none } } +.binding() diff --git a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift index a67a241c..a0a7750f 100644 --- a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift +++ b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift @@ -1,5 +1,8 @@ +import Combine import ComposableArchitecture import XCTest +import XXClient +import XXModels @testable import SendRequestFeature final class SendRequestFeatureTests: XCTestCase { @@ -12,6 +15,71 @@ final class SendRequestFeatureTests: XCTestCase { 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 + } + return e2e + } + store.environment.db.run = { + var db: Database = .failing + db.fetchContactsPublisher.run = { query in + dbDidFetchContacts.append(query) + return dbContactsPublisher.eraseToAnyPublisher() + } + return db + } + store.send(.start) + + XCTAssertNoDifference(dbDidFetchContacts, [.init(id: ["my-contact-id".data(using: .utf8)!])]) + + dbContactsPublisher.send([]) + + store.receive(.myContactFetched(nil)) + + var myDbContact = XXModels.Contact(id: "my-contact-id".data(using: .utf8)!) + myDbContact.marshaled = "my-contact-data".data(using: .utf8)! + dbContactsPublisher.send([myDbContact]) + + store.receive(.myContactFetched(.live("my-contact-data".data(using: .utf8)!))) { + $0.myContact = .live("my-contact-data".data(using: .utf8)!) + } + + dbContactsPublisher.send(completion: .finished) + } + + func testSendRequest() { + var myContact: XXClient.Contact = .unimplemented("my-contact-data".data(using: .utf8)!) + myContact.getFactsFromContact.run = { _ in + [ + Fact(fact: "my-username", type: 0), + Fact(fact: "my-email", type: 1), + Fact(fact: "my-phone", type: 2), + ] + } + + let store = TestStore( + initialState: SendRequestState( + contact: .unimplemented("contact-data".data(using: .utf8)!), + myContact: myContact + ), + reducer: sendRequestReducer, + environment: .unimplemented + ) + + store.send(.set(\.$sendPhone, false)) { + $0.sendPhone = false + } + + store.send(.sendTapped) } } -- GitLab From 4f55b8b49e9281e1a7b05fb7e0f4ef8c0f2337e9 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 15:37:11 +0200 Subject: [PATCH 20/29] Fix icon --- Examples/xx-messenger/Sources/ContactFeature/ContactView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift index 02f12d62..a5f07bc0 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift @@ -83,7 +83,7 @@ public struct ContactView: View { HStack { Text("Request sent") Spacer() - Image(systemName: "chevron.forward") + Image(systemName: "paperplane") } case .requestFailed: -- GitLab From b65e5aeac07a46effe203028ca824415642d8dac Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 15:50:03 +0200 Subject: [PATCH 21/29] Send auth request --- .../SendRequestFeature.swift | 47 +++++++ .../SendRequestFeatureTests.swift | 120 ++++++++++++++++-- 2 files changed, 157 insertions(+), 10 deletions(-) diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift index 8740c744..684e344a 100644 --- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift +++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift @@ -37,6 +37,8 @@ public struct SendRequestState: Equatable { public enum SendRequestAction: Equatable, BindableAction { case start case sendTapped + case sendSucceeded + case sendFailed(String) case binding(BindingAction<SendRequestState>) case myContactFetched(XXClient.Contact?) } @@ -95,6 +97,51 @@ public let sendRequestReducer = Reducer<SendRequestState, SendRequestAction, Sen return .none case .sendTapped: + state.isSending = true + state.failure = nil + return .result { [state] in + func updateAuthStatus(_ authStatus: XXModels.Contact.AuthStatus) throws { + try env.db().bulkUpdateContacts( + .init(id: [try state.contact.getId()]), + .init(authStatus: authStatus) + ) + } + do { + try updateAuthStatus(.requesting) + let myFacts = try state.myContact?.getFacts() ?? [] + var includedFacts: [Fact] = [] + if state.sendUsername, let fact = myFacts.first(where: { $0.type == 0 }) { + includedFacts.append(fact) + } + if state.sendEmail, let fact = myFacts.first(where: { $0.type == 1 }) { + includedFacts.append(fact) + } + if state.sendPhone, let fact = myFacts.first(where: { $0.type == 2 }) { + includedFacts.append(fact) + } + _ = try env.messenger.e2e.tryGet().requestAuthenticatedChannel( + partner: state.contact, + myFacts: includedFacts + ) + try updateAuthStatus(.requested) + return .success(.sendSucceeded) + } catch { + try? updateAuthStatus(.requestFailed) + return .success(.sendFailed(error.localizedDescription)) + } + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .sendSucceeded: + state.isSending = false + state.failure = nil + return .none + + case .sendFailed(let failure): + state.isSending = false + state.failure = failure return .none case .binding(_): diff --git a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift index a0a7750f..1dbe26be 100644 --- a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift +++ b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift @@ -58,28 +58,128 @@ final class SendRequestFeatureTests: XCTestCase { } func testSendRequest() { + var contact: XXClient.Contact = .unimplemented("contact-data".data(using: .utf8)!) + contact.getIdFromContact.run = { _ in "contact-id".data(using: .utf8)! } + var myContact: XXClient.Contact = .unimplemented("my-contact-data".data(using: .utf8)!) - myContact.getFactsFromContact.run = { _ in - [ - Fact(fact: "my-username", type: 0), - Fact(fact: "my-email", type: 1), - Fact(fact: "my-phone", type: 2), - ] + let myFacts = [ + Fact(fact: "my-username", type: 0), + Fact(fact: "my-email", type: 1), + Fact(fact: "my-phone", type: 2), + ] + myContact.getFactsFromContact.run = { _ in myFacts } + + let store = TestStore( + initialState: SendRequestState( + contact: contact, + myContact: myContact + ), + reducer: sendRequestReducer, + environment: .unimplemented + ) + + struct DidBulkUpdateContacts: Equatable { + var query: XXModels.Contact.Query + var assignments: XXModels.Contact.Assignments + } + struct DidRequestAuthChannel: Equatable { + var partner: XXClient.Contact + var myFacts: [XXClient.Fact] + } + + var didBulkUpdateContacts: [DidBulkUpdateContacts] = [] + var didRequestAuthChannel: [DidRequestAuthChannel] = [] + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.db.run = { + var db: Database = .failing + db.bulkUpdateContacts.run = { query, assignments in + didBulkUpdateContacts.append(.init(query: query, assignments: assignments)) + return 0 + } + return db + } + store.environment.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.requestAuthenticatedChannel.run = { partner, myFacts in + didRequestAuthChannel.append(.init(partner: partner, myFacts: myFacts)) + return 0 + } + return e2e } + store.send(.sendTapped) { + $0.isSending = true + } + + XCTAssertNoDifference(didBulkUpdateContacts, [ + .init( + query: .init(id: ["contact-id".data(using: .utf8)!]), + assignments: .init(authStatus: .requesting) + ), + .init( + query: .init(id: ["contact-id".data(using: .utf8)!]), + assignments: .init(authStatus: .requested) + ) + ]) + + XCTAssertNoDifference(didRequestAuthChannel, [ + .init( + partner: contact, + myFacts: myFacts + ) + ]) + + store.receive(.sendSucceeded) { + $0.isSending = false + } + } + + func testSendRequestFailure() { + var contact: XXClient.Contact = .unimplemented("contact-data".data(using: .utf8)!) + contact.getIdFromContact.run = { _ in "contact-id".data(using: .utf8)! } + + var myContact: XXClient.Contact = .unimplemented("my-contact-data".data(using: .utf8)!) + let myFacts = [ + Fact(fact: "my-username", type: 0), + Fact(fact: "my-email", type: 1), + Fact(fact: "my-phone", type: 2), + ] + myContact.getFactsFromContact.run = { _ in myFacts } + let store = TestStore( initialState: SendRequestState( - contact: .unimplemented("contact-data".data(using: .utf8)!), + contact: contact, myContact: myContact ), reducer: sendRequestReducer, environment: .unimplemented ) - store.send(.set(\.$sendPhone, false)) { - $0.sendPhone = false + struct Failure: Error {} + let failure = Failure() + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.db.run = { + var db: Database = .failing + db.bulkUpdateContacts.run = { _, _ in return 0 } + return db + } + store.environment.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.requestAuthenticatedChannel.run = { _, _ in throw failure } + return e2e } - store.send(.sendTapped) + store.send(.sendTapped) { + $0.isSending = true + } + + store.receive(.sendFailed(failure.localizedDescription)) { + $0.isSending = false + $0.failure = failure.localizedDescription + } } } -- GitLab From c7012099e74858107a8287ffcf81960f68674ac1 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 16:07:13 +0200 Subject: [PATCH 22/29] Update ContactView --- .../Sources/ContactFeature/ContactView.swift | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift index a5f07bc0..d14271f0 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift @@ -62,15 +62,6 @@ public struct ContactView: View { Spacer() Image(systemName: "person.fill.questionmark") } - Button { - viewStore.send(.sendRequestTapped) - } label: { - HStack { - Text("Send request") - Spacer() - Image(systemName: "chevron.forward") - } - } case .requesting: HStack { @@ -93,15 +84,6 @@ public struct ContactView: View { Image(systemName: "xmark.diamond.fill") .foregroundColor(.red) } - Button { - viewStore.send(.sendRequestTapped) - } label: { - HStack { - Text("Resend request") - Spacer() - Image(systemName: "paperplane") - } - } case .verificationInProgress: HStack { @@ -154,8 +136,17 @@ public struct ContactView: View { Image(systemName: "eye.slash") } } + Button { + viewStore.send(.sendRequestTapped) + } label: { + HStack { + Text("Send request") + Spacer() + Image(systemName: "chevron.forward") + } + } } header: { - Text("Auth status") + Text("Auth") } .animation(.default, value: viewStore.dbContact?.authStatus) } @@ -167,6 +158,7 @@ public struct ContactView: View { state: \.sendRequest, action: ContactAction.sendRequest ), + mapState: replayNonNil(), onDeactivate: { viewStore.send(.sendRequestDismissed) }, destination: SendRequestView.init(store:) )) -- GitLab From ab4f5b1ddaffaf9a2c90ba94776a8e4797d8adda Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 16:09:36 +0200 Subject: [PATCH 23/29] Dismiss SendRequest on success --- .../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 0bf0e29b..c75faa7b 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift @@ -115,6 +115,10 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm state.sendRequest = nil return .none + case .sendRequest(.sendSucceeded): + state.sendRequest = nil + return .none + case .sendRequest(_): return .none } diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift index e629b3f0..4d637ecf 100644 --- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift @@ -146,4 +146,21 @@ final class ContactFeatureTests: XCTestCase { $0.sendRequest = nil } } + + func testSendRequestSucceeded() { + let store = TestStore( + initialState: ContactState( + id: "contact-id".data(using: .utf8)!, + sendRequest: SendRequestState( + contact: .unimplemented("contact-id".data(using: .utf8)!) + ) + ), + reducer: contactReducer, + environment: .unimplemented + ) + + store.send(.sendRequest(.sendSucceeded)) { + $0.sendRequest = nil + } + } } -- GitLab From 5c6998723cb3a2f25f08fbdddfe86ca3367d306a Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 16:41:56 +0200 Subject: [PATCH 24/29] Update getFacts calls --- .../Sources/RegisterFeature/RegisterFeature.swift | 2 +- .../UserSearchFeature/UserSearchResultFeature.swift | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift index b55cae7d..fcb9c62e 100644 --- a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift +++ b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift @@ -83,7 +83,7 @@ public let registerReducer = Reducer<RegisterState, RegisterAction, RegisterEnvi let db = try env.db() try env.messenger.register(username: username) var contact = try env.messenger.e2e.tryGet().getContact() - var facts: [Fact] = (try? contact.getFacts()) ?? [] + var facts: [Fact] = try contact.getFacts() facts.removeAll(where: { $0.type == 0 }) facts.append(Fact(fact: username, type: 0)) try contact.setFacts(facts) diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift index ece68742..a331df70 100644 --- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift @@ -44,10 +44,9 @@ public let userSearchResultReducer = Reducer<UserSearchResultState, UserSearchRe { state, action, env in switch action { case .start: - let facts = (try? state.xxContact.getFacts()) ?? [] - state.username = facts.first(where: { $0.type == 0 })?.fact - state.email = facts.first(where: { $0.type == 1 })?.fact - state.phone = facts.first(where: { $0.type == 2 })?.fact + state.username = state.xxContact.username + state.email = state.xxContact.email + state.phone = state.xxContact.phone return .none case .tapped: -- GitLab From 83bdd9faa9b91b7aa335f40b922112acf8f8313a Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 16:57:56 +0200 Subject: [PATCH 25/29] Update SendRequestView --- .../SendRequestFeature/SendRequestView.swift | 68 ++++++++++--------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift index f1bd8ff5..159d10e8 100644 --- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift +++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift @@ -34,41 +34,44 @@ public struct SendRequestView: View { WithViewStore(store.scope(state: ViewState.init)) { viewStore in Form { Section { - HStack { - Label(viewStore.myContact?.username ?? "", systemImage: "person") - Spacer() - Toggle( - isOn: viewStore.binding( - get: \.sendUsername, - send: { SendRequestAction.set(\.$sendUsername, $0) } - ), - label: EmptyView.init - ) + Button { + viewStore.send(.set(\.$sendUsername, !viewStore.sendUsername)) + } label: { + HStack { + Label(viewStore.myContact?.username ?? "", systemImage: "person") + .tint(Color.primary) + Spacer() + Image(systemName: viewStore.sendUsername ? "checkmark.circle.fill" : "circle") + .foregroundColor(.accentColor) + } } + .animation(.default, value: viewStore.sendUsername) - HStack { - Label(viewStore.myContact?.email ?? "", systemImage: "envelope") - Spacer() - Toggle( - isOn: viewStore.binding( - get: \.sendEmail, - send: { SendRequestAction.set(\.$sendEmail, $0) } - ), - label: EmptyView.init - ) + Button { + viewStore.send(.set(\.$sendEmail, !viewStore.sendEmail)) + } label: { + HStack { + Label(viewStore.myContact?.email ?? "", systemImage: "envelope") + .tint(Color.primary) + Spacer() + Image(systemName: viewStore.sendEmail ? "checkmark.circle.fill" : "circle") + .foregroundColor(.accentColor) + } } + .animation(.default, value: viewStore.sendEmail) - HStack { - Label(viewStore.myContact?.phone ?? "", systemImage: "phone") - Spacer() - Toggle( - isOn: viewStore.binding( - get: \.sendPhone, - send: { SendRequestAction.set(\.$sendPhone, $0) } - ), - label: EmptyView.init - ) + Button { + viewStore.send(.set(\.$sendPhone, !viewStore.sendPhone)) + } label: { + HStack { + Label(viewStore.myContact?.phone ?? "", systemImage: "phone") + .tint(Color.primary) + Spacer() + Image(systemName: viewStore.sendPhone ? "checkmark.circle.fill" : "circle") + .foregroundColor(.accentColor) + } } + .animation(.default, value: viewStore.sendPhone) } header: { Text("My facts") } @@ -141,7 +144,10 @@ public struct SendRequestView_Previews: PreviewProvider { } return contact }(), - isSending: true, + sendUsername: true, + sendEmail: false, + sendPhone: true, + isSending: false, failure: "Something went wrong" ), reducer: .empty, -- GitLab From 16b258b9ff7fcb74ac619cc1d9cd2879fa1b78e5 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 22:29:58 +0200 Subject: [PATCH 26/29] Import selected facts to contact --- .../ContactFeature/ContactFeature.swift | 32 ++++++++++--- .../Sources/ContactFeature/ContactView.swift | 47 +++++++++++++++++-- .../ContactFeatureTests.swift | 4 +- 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift index c75faa7b..b8da32a4 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift @@ -13,27 +13,37 @@ public struct ContactState: Equatable { id: Data, dbContact: XXModels.Contact? = nil, xxContact: XXClient.Contact? = nil, + importUsername: Bool = true, + importEmail: Bool = true, + importPhone: Bool = true, sendRequest: SendRequestState? = nil ) { self.id = id self.dbContact = dbContact self.xxContact = xxContact + self.importUsername = importUsername + self.importEmail = importEmail + self.importPhone = importPhone self.sendRequest = sendRequest } public var id: Data public var dbContact: XXModels.Contact? public var xxContact: XXClient.Contact? + @BindableState public var importUsername: Bool + @BindableState public var importEmail: Bool + @BindableState public var importPhone: Bool public var sendRequest: SendRequestState? } -public enum ContactAction: Equatable { +public enum ContactAction: Equatable, BindableAction { case start case dbContactFetched(XXModels.Contact?) - case saveFactsTapped + case importFactsTapped case sendRequestTapped case sendRequestDismissed case sendRequest(SendRequestAction) + case binding(BindingAction<ContactState>) } public struct ContactEnvironment { @@ -89,14 +99,20 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm state.dbContact = contact return .none - case .saveFactsTapped: + case .importFactsTapped: guard let xxContact = state.xxContact else { return .none } return .fireAndForget { [state] in var dbContact = state.dbContact ?? XXModels.Contact(id: state.id) dbContact.marshaled = xxContact.data - dbContact.username = xxContact.username - dbContact.email = xxContact.email - dbContact.phone = xxContact.phone + if state.importUsername { + dbContact.username = xxContact.username + } + if state.importEmail { + dbContact.email = xxContact.email + } + if state.importPhone { + dbContact.phone = xxContact.phone + } _ = try! env.db().saveContact(dbContact) } .subscribe(on: env.bgQueue) @@ -121,8 +137,12 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm case .sendRequest(_): return .none + + case .binding(_): + return .none } } +.binding() .presenting( sendRequestReducer, state: .keyPath(\.sendRequest), diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift index d14271f0..a3b6ee08 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift @@ -16,10 +16,16 @@ public struct ContactView: View { struct ViewState: Equatable { var dbContact: XXModels.Contact? var xxContact: XXClient.Contact? + var importUsername: Bool + var importEmail: Bool + var importPhone: Bool init(state: ContactState) { dbContact = state.dbContact xxContact = state.xxContact + importUsername = state.importUsername + importEmail = state.importEmail + importPhone = state.importPhone } } @@ -28,11 +34,44 @@ public struct ContactView: View { Form { if let xxContact = viewStore.xxContact { Section { - Label(xxContact.username ?? "", systemImage: "person") - Label(xxContact.email ?? "", systemImage: "envelope") - Label(xxContact.phone ?? "", systemImage: "phone") Button { - viewStore.send(.saveFactsTapped) + viewStore.send(.set(\.$importUsername, !viewStore.importUsername)) + } label: { + HStack { + Label(xxContact.username ?? "", systemImage: "person") + .tint(Color.primary) + Spacer() + Image(systemName: viewStore.importUsername ? "checkmark.circle.fill" : "circle") + .foregroundColor(.accentColor) + } + } + + Button { + viewStore.send(.set(\.$importEmail, !viewStore.importEmail)) + } label: { + HStack { + Label(xxContact.email ?? "", systemImage: "envelope") + .tint(Color.primary) + Spacer() + Image(systemName: viewStore.importEmail ? "checkmark.circle.fill" : "circle") + .foregroundColor(.accentColor) + } + } + + Button { + viewStore.send(.set(\.$importPhone, !viewStore.importPhone)) + } label: { + HStack { + Label(xxContact.phone ?? "", systemImage: "phone") + .tint(Color.primary) + Spacer() + Image(systemName: viewStore.importPhone ? "checkmark.circle.fill" : "circle") + .foregroundColor(.accentColor) + } + } + + Button { + viewStore.send(.importFactsTapped) } label: { if viewStore.dbContact == nil { Text("Save contact") diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift index 4d637ecf..244cc900 100644 --- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift @@ -47,7 +47,7 @@ final class ContactFeatureTests: XCTestCase { dbContactsPublisher.send(completion: .finished) } - func testSaveFacts() { + func testImportFacts() { let dbContact: XXModels.Contact = .init( id: "contact-id".data(using: .utf8)! ) @@ -84,7 +84,7 @@ final class ContactFeatureTests: XCTestCase { return db } - store.send(.saveFactsTapped) + store.send(.importFactsTapped) var expectedSavedContact = dbContact expectedSavedContact.marshaled = xxContact.data -- GitLab From 1c098a7eff357d0179b0b687d63502836967bd46 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 23:24:18 +0200 Subject: [PATCH 27/29] Update presentation id in UserSearchFeature --- .../Sources/UserSearchFeature/UserSearchFeature.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift index e8cb92c0..1b10c1b3 100644 --- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift @@ -140,7 +140,7 @@ public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSe .presenting( contactReducer, state: .keyPath(\.contact), - id: .notNil(), // TODO: use Contact.ID + id: .keyPath(\.?.id), action: /UserSearchAction.contact, environment: { $0.contact() } ) -- GitLab From c727c9ddf76cdee2748b3e963f1e9232a88cea9b Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Wed, 7 Sep 2022 23:38:04 +0200 Subject: [PATCH 28/29] Refactor fact helpers --- .../AppCore/XXClientHelpers/FactHelpers.swift | 55 +++++++++++++++++++ .../XXClientHelpers/XXContact+Helpers.swift | 15 ----- .../ContactFeature/ContactFeature.swift | 6 +- .../Sources/ContactFeature/ContactView.swift | 18 ++++-- .../RegisterFeature/RegisterFeature.swift | 5 +- .../SendRequestFeature.swift | 6 +- .../SendRequestFeature/SendRequestView.swift | 28 ++++++---- .../UserSearchResultFeature.swift | 6 +- 8 files changed, 95 insertions(+), 44 deletions(-) create mode 100644 Examples/xx-messenger/Sources/AppCore/XXClientHelpers/FactHelpers.swift delete mode 100644 Examples/xx-messenger/Sources/AppCore/XXClientHelpers/XXContact+Helpers.swift diff --git a/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/FactHelpers.swift b/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/FactHelpers.swift new file mode 100644 index 00000000..83f04032 --- /dev/null +++ b/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/FactHelpers.swift @@ -0,0 +1,55 @@ +import XXClient + +// TODO: Move to XXClient library + +public enum FactType: Equatable { + case username + case email + case phone + case other(Int) + + public static let knownTypes: [Self] = [.username, .email, .phone] + + public init(rawValue: Int) { + if let known = FactType.knownTypes.first(where: { $0.rawValue == rawValue }) { + self = known + } else { + self = .other(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .username: return 0 + case .email: return 1 + case .phone: return 2 + case .other(let rawValue): return rawValue + } + } +} + +extension Array where Element == Fact { + public func get(_ type: FactType) -> Fact? { + first(where: { $0.type == type.rawValue }) + } + + public mutating func set(_ type: FactType, _ value: String?) { + removeAll(where: { $0.type == type.rawValue }) + if let value = value { + append(Fact(fact: value, type: type.rawValue)) + sort(by: { $0.type < $1.type }) + } + } +} + +extension Contact { + public func getFact(_ type: FactType) throws -> Fact? { + try getFacts().get(type) + } + + public mutating func setFact(_ type: FactType, _ value: String?) throws { + var facts = try getFacts() + facts.set(type, value) + try setFacts(facts) + } +} diff --git a/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/XXContact+Helpers.swift b/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/XXContact+Helpers.swift deleted file mode 100644 index d2c93683..00000000 --- a/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/XXContact+Helpers.swift +++ /dev/null @@ -1,15 +0,0 @@ -import XXClient - -extension Contact { - public var username: String? { - try? getFacts().first(where: { $0.type == 0 })?.fact - } - - public var email: String? { - try? getFacts().first(where: { $0.type == 1 })?.fact - } - - public var phone: String? { - try? getFacts().first(where: { $0.type == 2 })?.fact - } -} diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift index b8da32a4..8126b792 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift @@ -105,13 +105,13 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm var dbContact = state.dbContact ?? XXModels.Contact(id: state.id) dbContact.marshaled = xxContact.data if state.importUsername { - dbContact.username = xxContact.username + dbContact.username = try? xxContact.getFact(.username)?.fact } if state.importEmail { - dbContact.email = xxContact.email + dbContact.email = try? xxContact.getFact(.email)?.fact } if state.importPhone { - dbContact.phone = xxContact.phone + dbContact.phone = try? xxContact.getFact(.phone)?.fact } _ = try! env.db().saveContact(dbContact) } diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift index a3b6ee08..48743b07 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift @@ -15,14 +15,20 @@ public struct ContactView: View { struct ViewState: Equatable { var dbContact: XXModels.Contact? - var xxContact: XXClient.Contact? + var xxContactIsSet: Bool + var xxContactUsername: String? + var xxContactEmail: String? + var xxContactPhone: String? var importUsername: Bool var importEmail: Bool var importPhone: Bool init(state: ContactState) { dbContact = state.dbContact - xxContact = state.xxContact + xxContactIsSet = state.xxContact != nil + xxContactUsername = try? state.xxContact?.getFact(.username)?.fact + xxContactEmail = try? state.xxContact?.getFact(.email)?.fact + xxContactPhone = try? state.xxContact?.getFact(.phone)?.fact importUsername = state.importUsername importEmail = state.importEmail importPhone = state.importPhone @@ -32,13 +38,13 @@ public struct ContactView: View { public var body: some View { WithViewStore(store.scope(state: ViewState.init)) { viewStore in Form { - if let xxContact = viewStore.xxContact { + if viewStore.xxContactIsSet { Section { Button { viewStore.send(.set(\.$importUsername, !viewStore.importUsername)) } label: { HStack { - Label(xxContact.username ?? "", systemImage: "person") + Label(viewStore.xxContactUsername ?? "", systemImage: "person") .tint(Color.primary) Spacer() Image(systemName: viewStore.importUsername ? "checkmark.circle.fill" : "circle") @@ -50,7 +56,7 @@ public struct ContactView: View { viewStore.send(.set(\.$importEmail, !viewStore.importEmail)) } label: { HStack { - Label(xxContact.email ?? "", systemImage: "envelope") + Label(viewStore.xxContactEmail ?? "", systemImage: "envelope") .tint(Color.primary) Spacer() Image(systemName: viewStore.importEmail ? "checkmark.circle.fill" : "circle") @@ -62,7 +68,7 @@ public struct ContactView: View { viewStore.send(.set(\.$importPhone, !viewStore.importPhone)) } label: { HStack { - Label(xxContact.phone ?? "", systemImage: "phone") + Label(viewStore.xxContactPhone ?? "", systemImage: "phone") .tint(Color.primary) Spacer() Image(systemName: viewStore.importPhone ? "checkmark.circle.fill" : "circle") diff --git a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift index fcb9c62e..cb43c430 100644 --- a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift +++ b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift @@ -83,10 +83,7 @@ public let registerReducer = Reducer<RegisterState, RegisterAction, RegisterEnvi let db = try env.db() try env.messenger.register(username: username) var contact = try env.messenger.e2e.tryGet().getContact() - var facts: [Fact] = try contact.getFacts() - facts.removeAll(where: { $0.type == 0 }) - facts.append(Fact(fact: username, type: 0)) - try contact.setFacts(facts) + try contact.setFact(.username, username) try db.saveContact(Contact( id: try contact.getId(), marshaled: contact.data, diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift index 684e344a..f2625b91 100644 --- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift +++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift @@ -110,13 +110,13 @@ public let sendRequestReducer = Reducer<SendRequestState, SendRequestAction, Sen try updateAuthStatus(.requesting) let myFacts = try state.myContact?.getFacts() ?? [] var includedFacts: [Fact] = [] - if state.sendUsername, let fact = myFacts.first(where: { $0.type == 0 }) { + if state.sendUsername, let fact = myFacts.get(.username) { includedFacts.append(fact) } - if state.sendEmail, let fact = myFacts.first(where: { $0.type == 1 }) { + if state.sendEmail, let fact = myFacts.get(.email) { includedFacts.append(fact) } - if state.sendPhone, let fact = myFacts.first(where: { $0.type == 2 }) { + if state.sendPhone, let fact = myFacts.get(.phone) { includedFacts.append(fact) } _ = try env.messenger.e2e.tryGet().requestAuthenticatedChannel( diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift index 159d10e8..5f1cd7d5 100644 --- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift +++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift @@ -11,8 +11,12 @@ public struct SendRequestView: View { let store: Store<SendRequestState, SendRequestAction> struct ViewState: Equatable { - var contact: XXClient.Contact - var myContact: XXClient.Contact? + var contactUsername: String? + var contactEmail: String? + var contactPhone: String? + var myUsername: String? + var myEmail: String? + var myPhone: String? var sendUsername: Bool var sendEmail: Bool var sendPhone: Bool @@ -20,8 +24,12 @@ public struct SendRequestView: View { var failure: String? init(state: SendRequestState) { - contact = state.contact - myContact = state.myContact + contactUsername = try? state.contact.getFact(.username)?.fact + contactEmail = try? state.contact.getFact(.email)?.fact + contactPhone = try? state.contact.getFact(.phone)?.fact + myUsername = try? state.myContact?.getFact(.username)?.fact + myEmail = try? state.myContact?.getFact(.email)?.fact + myPhone = try? state.myContact?.getFact(.phone)?.fact sendUsername = state.sendUsername sendEmail = state.sendEmail sendPhone = state.sendPhone @@ -38,7 +46,7 @@ public struct SendRequestView: View { viewStore.send(.set(\.$sendUsername, !viewStore.sendUsername)) } label: { HStack { - Label(viewStore.myContact?.username ?? "", systemImage: "person") + Label(viewStore.myUsername ?? "", systemImage: "person") .tint(Color.primary) Spacer() Image(systemName: viewStore.sendUsername ? "checkmark.circle.fill" : "circle") @@ -51,7 +59,7 @@ public struct SendRequestView: View { viewStore.send(.set(\.$sendEmail, !viewStore.sendEmail)) } label: { HStack { - Label(viewStore.myContact?.email ?? "", systemImage: "envelope") + Label(viewStore.myEmail ?? "", systemImage: "envelope") .tint(Color.primary) Spacer() Image(systemName: viewStore.sendEmail ? "checkmark.circle.fill" : "circle") @@ -64,7 +72,7 @@ public struct SendRequestView: View { viewStore.send(.set(\.$sendPhone, !viewStore.sendPhone)) } label: { HStack { - Label(viewStore.myContact?.phone ?? "", systemImage: "phone") + Label(viewStore.myPhone ?? "", systemImage: "phone") .tint(Color.primary) Spacer() Image(systemName: viewStore.sendPhone ? "checkmark.circle.fill" : "circle") @@ -78,9 +86,9 @@ public struct SendRequestView: View { .disabled(viewStore.isSending) Section { - Label(viewStore.contact.username ?? "", systemImage: "person") - Label(viewStore.contact.email ?? "", systemImage: "envelope") - Label(viewStore.contact.phone ?? "", systemImage: "phone") + Label(viewStore.contactUsername ?? "", systemImage: "person") + Label(viewStore.contactEmail ?? "", systemImage: "envelope") + Label(viewStore.contactPhone ?? "", systemImage: "phone") } header: { Text("Contact") } diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift index a331df70..839e6886 100644 --- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift @@ -44,9 +44,9 @@ public let userSearchResultReducer = Reducer<UserSearchResultState, UserSearchRe { state, action, env in switch action { case .start: - state.username = state.xxContact.username - state.email = state.xxContact.email - state.phone = state.xxContact.phone + state.username = try? state.xxContact.getFact(.username)?.fact + state.email = try? state.xxContact.getFact(.email)?.fact + state.phone = try? state.xxContact.getFact(.phone)?.fact return .none case .tapped: -- GitLab From 7ddd7cf6febde59368f3f652e460632a4c53ffd7 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 8 Sep 2022 00:51:06 +0200 Subject: [PATCH 29/29] Remove FactHelpers from AppCore library --- .../AppCore/XXClientHelpers/FactHelpers.swift | 55 ------------------- 1 file changed, 55 deletions(-) delete mode 100644 Examples/xx-messenger/Sources/AppCore/XXClientHelpers/FactHelpers.swift diff --git a/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/FactHelpers.swift b/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/FactHelpers.swift deleted file mode 100644 index 83f04032..00000000 --- a/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/FactHelpers.swift +++ /dev/null @@ -1,55 +0,0 @@ -import XXClient - -// TODO: Move to XXClient library - -public enum FactType: Equatable { - case username - case email - case phone - case other(Int) - - public static let knownTypes: [Self] = [.username, .email, .phone] - - public init(rawValue: Int) { - if let known = FactType.knownTypes.first(where: { $0.rawValue == rawValue }) { - self = known - } else { - self = .other(rawValue) - } - } - - public var rawValue: Int { - switch self { - case .username: return 0 - case .email: return 1 - case .phone: return 2 - case .other(let rawValue): return rawValue - } - } -} - -extension Array where Element == Fact { - public func get(_ type: FactType) -> Fact? { - first(where: { $0.type == type.rawValue }) - } - - public mutating func set(_ type: FactType, _ value: String?) { - removeAll(where: { $0.type == type.rawValue }) - if let value = value { - append(Fact(fact: value, type: type.rawValue)) - sort(by: { $0.type < $1.type }) - } - } -} - -extension Contact { - public func getFact(_ type: FactType) throws -> Fact? { - try getFacts().get(type) - } - - public mutating func setFact(_ type: FactType, _ value: String?) throws { - var facts = try getFacts() - facts.set(type, value) - try setFacts(facts) - } -} -- GitLab