diff --git a/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ConfirmRequestFeature.xcscheme b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ConfirmRequestFeature.xcscheme new file mode 100644 index 0000000000000000000000000000000000000000..a3026c70355eab27e54fd62ebe8af6b508a45acd --- /dev/null +++ b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ConfirmRequestFeature.xcscheme @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1400" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "ConfirmRequestFeature" + BuildableName = "ConfirmRequestFeature" + BlueprintName = "ConfirmRequestFeature" + 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 = "ConfirmRequestFeatureTests" + BuildableName = "ConfirmRequestFeatureTests" + BlueprintName = "ConfirmRequestFeatureTests" + 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 = "ConfirmRequestFeature" + BuildableName = "ConfirmRequestFeature" + BlueprintName = "ConfirmRequestFeature" + 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 2736c69c437e636e8eabcc3eefda2a96b9f238fc..716b1482e5d2f1081a20b6e748e3707b31229b26 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -21,6 +21,7 @@ let package = Package( .library(name: "AppCore", targets: ["AppCore"]), .library(name: "AppFeature", targets: ["AppFeature"]), .library(name: "CheckContactAuthFeature", targets: ["CheckContactAuthFeature"]), + .library(name: "ConfirmRequestFeature", targets: ["ConfirmRequestFeature"]), .library(name: "ContactFeature", targets: ["ContactFeature"]), .library(name: "ContactsFeature", targets: ["ContactsFeature"]), .library(name: "HomeFeature", targets: ["HomeFeature"]), @@ -115,6 +116,22 @@ let package = Package( .target(name: "CheckContactAuthFeature"), ] ), + .target( + name: "ConfirmRequestFeature", + 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"), + ] + ), + .testTarget( + name: "ConfirmRequestFeatureTests", + dependencies: [ + .target(name: "ConfirmRequestFeature"), + ] + ), .target( name: "ContactFeature", 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 85d232415d82af42337d1fceea8eadb26369b000..0e5e54ad091bf310eb399f185b4c8680fed98e07 100644 --- a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme +++ b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme @@ -59,6 +59,16 @@ ReferencedContainer = "container:.."> </BuildableReference> </TestableReference> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "ConfirmRequestFeatureTests" + BuildableName = "ConfirmRequestFeatureTests" + BlueprintName = "ConfirmRequestFeatureTests" + ReferencedContainer = "container:.."> + </BuildableReference> + </TestableReference> <TestableReference skipped = "NO"> <BuildableReference diff --git a/Examples/xx-messenger/Sources/ConfirmRequestFeature/ConfirmRequestFeature.swift b/Examples/xx-messenger/Sources/ConfirmRequestFeature/ConfirmRequestFeature.swift new file mode 100644 index 0000000000000000000000000000000000000000..b18f9da947115a2f6ebe503182430090cbf3445a --- /dev/null +++ b/Examples/xx-messenger/Sources/ConfirmRequestFeature/ConfirmRequestFeature.swift @@ -0,0 +1,94 @@ +import AppCore +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct ConfirmRequestState: Equatable { + public enum Result: Equatable { + case success + case failure(String) + } + + public init( + contact: XXClient.Contact, + isConfirming: Bool = false, + result: Result? = nil + ) { + self.contact = contact + self.isConfirming = isConfirming + self.result = result + } + + public var contact: XXClient.Contact + public var isConfirming: Bool + public var result: Result? +} + +public enum ConfirmRequestAction: Equatable { + case confirmTapped + case didConfirm(ConfirmRequestState.Result) +} + +public struct ConfirmRequestEnvironment { + 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 ConfirmRequestEnvironment { + public static let unimplemented = ConfirmRequestEnvironment( + messenger: .unimplemented, + db: .unimplemented, + mainQueue: .unimplemented, + bgQueue: .unimplemented + ) +} +#endif + +public let confirmRequestReducer = Reducer<ConfirmRequestState, ConfirmRequestAction, ConfirmRequestEnvironment> +{ state, action, env in + switch action { + case .confirmTapped: + state.isConfirming = true + state.result = nil + return Effect.result { [state] in + do { + let e2e = try env.messenger.e2e.tryGet() + _ = try e2e.confirmReceivedRequest(partner: state.contact) + let contactId = try state.contact.getId() + try env.db().bulkUpdateContacts.callAsFunction( + .init(id: [contactId]), + .init(authStatus: .friend) + ) + return .success(.didConfirm(.success)) + } catch { + return .success(.didConfirm(.failure(error.localizedDescription))) + } + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .didConfirm(let result): + state.isConfirming = false + state.result = result + return .none + } +} diff --git a/Examples/xx-messenger/Sources/ConfirmRequestFeature/ConfirmRequestView.swift b/Examples/xx-messenger/Sources/ConfirmRequestFeature/ConfirmRequestView.swift new file mode 100644 index 0000000000000000000000000000000000000000..90ebc70e4490d51792db636a47d2e02b318f5820 --- /dev/null +++ b/Examples/xx-messenger/Sources/ConfirmRequestFeature/ConfirmRequestView.swift @@ -0,0 +1,95 @@ +import ComposableArchitecture +import SwiftUI + +public struct ConfirmRequestView: View { + public init(store: Store<ConfirmRequestState, ConfirmRequestAction>) { + self.store = store + } + + let store: Store<ConfirmRequestState, ConfirmRequestAction> + + struct ViewState: Equatable { + var username: String? + var email: String? + var phone: String? + var isConfirming: Bool + var result: ConfirmRequestState.Result? + + init(state: ConfirmRequestState) { + username = try? state.contact.getFact(.username)?.value + email = try? state.contact.getFact(.email)?.value + phone = try? state.contact.getFact(.phone)?.value + isConfirming = state.isConfirming + result = state.result + } + } + + public var body: some View { + WithViewStore(store, observe: ViewState.init) { viewStore in + Form { + Section { + Label(viewStore.username ?? "", systemImage: "person") + Label(viewStore.email ?? "", systemImage: "envelope") + Label(viewStore.phone ?? "", systemImage: "phone") + } header: { + Text("Facts") + } + + Section { + Button { + viewStore.send(.confirmTapped) + } label: { + HStack { + Text("Confirm") + Spacer() + if viewStore.isConfirming { + ProgressView() + } else { + Image(systemName: "checkmark") + } + } + } + .disabled(viewStore.isConfirming) + } + + if let result = viewStore.result { + Section { + HStack { + switch result { + case .success: + Text("Request confirmed") + Spacer() + Image(systemName: "person.fill.checkmark") + + case .failure(_): + Text("Confirming request failed") + Spacer() + Image(systemName: "xmark") + } + } + if case .failure(let failure) = result { + Text(failure) + } + } header: { + Text("Result") + } + } + } + .navigationTitle("Confirm request") + } + } +} + +#if DEBUG +public struct ConfirmRequestView_Previews: PreviewProvider { + public static var previews: some View { + ConfirmRequestView(store: Store( + initialState: ConfirmRequestState( + contact: .unimplemented("contact-data".data(using: .utf8)!) + ), + reducer: .empty, + environment: () + )) + } +} +#endif diff --git a/Examples/xx-messenger/Tests/ConfirmRequestFeatureTests/ConfirmRequestFeatureTests.swift b/Examples/xx-messenger/Tests/ConfirmRequestFeatureTests/ConfirmRequestFeatureTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..2686d64531d147aae03a6b9b6fe888ef811153a0 --- /dev/null +++ b/Examples/xx-messenger/Tests/ConfirmRequestFeatureTests/ConfirmRequestFeatureTests.swift @@ -0,0 +1,95 @@ +import ComposableArchitecture +import CustomDump +import XCTest +import XXClient +import XXModels +@testable import ConfirmRequestFeature + +final class ConfirmRequestFeatureTests: XCTestCase { + func testConfirm() { + var contact = XXClient.Contact.unimplemented("contact-data".data(using: .utf8)!) + let contactId = "contact-id".data(using: .utf8)! + contact.getIdFromContact.run = { _ in contactId } + + let store = TestStore( + initialState: ConfirmRequestState( + contact: contact + ), + reducer: confirmRequestReducer, + environment: .unimplemented + ) + + var didConfirmRequestFromContact: [XXClient.Contact] = [] + var didBulkUpdateContactsWithQuery: [XXModels.Contact.Query] = [] + var didBulkUpdateContactsWithAssignments: [XXModels.Contact.Assignments] = [] + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.confirmReceivedRequest.run = { contact in + didConfirmRequestFromContact.append(contact) + return 0 + } + return e2e + } + store.environment.db.run = { + var db: Database = .failing + db.bulkUpdateContacts.run = { query, assignments in + didBulkUpdateContactsWithQuery.append(query) + didBulkUpdateContactsWithAssignments.append(assignments) + return 0 + } + return db + } + + store.send(.confirmTapped) { + $0.isConfirming = true + $0.result = nil + } + + XCTAssertNoDifference(didConfirmRequestFromContact, [contact]) + XCTAssertNoDifference(didBulkUpdateContactsWithQuery, [.init(id: [contactId])]) + XCTAssertNoDifference(didBulkUpdateContactsWithAssignments, [.init(authStatus: .friend)]) + + store.receive(.didConfirm(.success)) { + $0.isConfirming = false + $0.result = .success + } + } + + func testConfirmFailure() { + var contact = XXClient.Contact.unimplemented("contact-data".data(using: .utf8)!) + let contactId = "contact-id".data(using: .utf8)! + contact.getIdFromContact.run = { _ in contactId } + + let store = TestStore( + initialState: ConfirmRequestState( + contact: contact + ), + reducer: confirmRequestReducer, + environment: .unimplemented + ) + + struct Failure: Error {} + let error = Failure() + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.confirmReceivedRequest.run = { _ in throw error } + return e2e + } + + store.send(.confirmTapped) { + $0.isConfirming = true + $0.result = nil + } + + store.receive(.didConfirm(.failure(error.localizedDescription))) { + $0.isConfirming = false + $0.result = .failure(error.localizedDescription) + } + } +}