diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index b91c434087900d6e07683c90d27b931be723baf6..4ed6cfd8bbfbd9a1da66a1743410f1e623ed2648 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -69,6 +69,7 @@ let package = Package( dependencies: [ .target(name: "AppCore"), .target(name: "HomeFeature"), + .target(name: "RegisterFeature"), .target(name: "RestoreFeature"), .target(name: "WelcomeFeature"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), @@ -89,7 +90,9 @@ let package = Package( name: "HomeFeature", dependencies: [ .target(name: "AppCore"), + .target(name: "RegisterFeature"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + .product(name: "ComposablePresentation", package: "swift-composable-presentation"), .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), ], swiftSettings: swiftSettings diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index eec6f199e316c81d484d3518c26cdc7d64a7fb34..d3cc134a7dbca79b4d9e75472eb26697f08a455b 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -34,7 +34,14 @@ extension AppEnvironment { HomeEnvironment( messenger: messenger, mainQueue: mainQueue, - bgQueue: bgQueue + bgQueue: bgQueue, + register: { + RegisterEnvironment( + messenger: messenger, + mainQueue: mainQueue, + bgQueue: bgQueue + ) + } ) } ) diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift index 499bed7832eed00684d649bdb8e0f8b96363fc93..29abfb6ba9d977e0e87b0cb31bf7eca167b15886 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift @@ -1,37 +1,58 @@ +import Combine import ComposableArchitecture +import ComposablePresentation import Foundation +import RegisterFeature import XXClient import XXMessengerClient public struct HomeState: Equatable { - public init() {} + public init( + username: String? = nil, + failure: String? = nil, + register: RegisterState? = nil + ) { + self.username = username + self.failure = failure + self.register = register + } + + @BindableState public var username: String? + @BindableState public var failure: String? + @BindableState public var register: RegisterState? } -public enum HomeAction: Equatable { +public enum HomeAction: Equatable, BindableAction { case start + case binding(BindingAction<HomeState>) + case register(RegisterAction) } public struct HomeEnvironment { public init( messenger: Messenger, mainQueue: AnySchedulerOf<DispatchQueue>, - bgQueue: AnySchedulerOf<DispatchQueue> + bgQueue: AnySchedulerOf<DispatchQueue>, + register: @escaping () -> RegisterEnvironment ) { self.messenger = messenger self.mainQueue = mainQueue self.bgQueue = bgQueue + self.register = register } public var messenger: Messenger public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> + public var register: () -> RegisterEnvironment } extension HomeEnvironment { public static let unimplemented = HomeEnvironment( messenger: .unimplemented, mainQueue: .unimplemented, - bgQueue: .unimplemented + bgQueue: .unimplemented, + register: { .unimplemented } ) } @@ -39,6 +60,51 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> { state, action, env in switch action { case .start: + return .run { subscriber in + do { + try env.messenger.start() + + if env.messenger.isConnected() == false { + try env.messenger.connect() + } + + if env.messenger.isLoggedIn() == false { + if try env.messenger.isRegistered() == false { + subscriber.send(.set(\.$register, RegisterState())) + subscriber.send(completion: .finished) + return AnyCancellable {} + } + try env.messenger.logIn() + } + + if let contact = env.messenger.e2e()?.getContact(), + let facts = try? contact.getFacts(), + let username = facts.first(where: { $0.type == 0 })?.fact { + subscriber.send(.set(\.$username, username)) + } + } catch { + subscriber.send(.set(\.$failure, error.localizedDescription)) + } + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .register(.finished): + state.register = nil + return Effect(value: .start) + + case .binding(_), .register(_): return .none } } +.binding() +.presenting( + registerReducer, + state: .keyPath(\.register), + id: .notNil(), + action: /HomeAction.register, + environment: { $0.register() } +) diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift index 0bda29bd8c2ea48b125337dd5a487e63334794f0..e40bb1db3982e1673ca2c77d6b8b0e72fa697da9 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift @@ -1,4 +1,6 @@ import ComposableArchitecture +import ComposablePresentation +import RegisterFeature import SwiftUI public struct HomeView: View { @@ -9,19 +11,54 @@ public struct HomeView: View { let store: Store<HomeState, HomeAction> struct ViewState: Equatable { - init(state: HomeState) {} + var username: String? + var failure: String? + + init(state: HomeState) { + username = state.username + failure = state.failure + } } public var body: some View { WithViewStore(store.scope(state: ViewState.init)) { viewStore in NavigationView { Form { + if let username = viewStore.username { + Section { + Text(username) + } header: { + Text("Username") + } + } + if let failure = viewStore.failure { + Section { + Text(failure) + Button { + viewStore.send(.start) + } label: { + Text("Retry") + } + } header: { + Text("Error") + } + } } .navigationTitle("Home") } .navigationViewStyle(.stack) .task { viewStore.send(.start) } + .fullScreenCover( + store.scope( + state: \.register, + action: HomeAction.register + ), + onDismiss: { + viewStore.send(.set(\.$register, nil)) + }, + content: RegisterView.init(store:) + ) } } } diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift index 21a25bb641274dced7d7dd83ebc49f073ada4420..efdd867ff247317e7dd6dc379f38edfbc5ba7b3b 100644 --- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift +++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift @@ -1,16 +1,232 @@ import ComposableArchitecture +import RegisterFeature import XCTest +import XXClient +import XXMessengerClient @testable import HomeFeature -@MainActor final class HomeFeatureTests: XCTestCase { - func testStart() async throws { + func testStartUnregistered() { let store = TestStore( initialState: HomeState(), reducer: homeReducer, environment: .unimplemented ) - await store.send(.start) + let bgQueue = DispatchQueue.test + let mainQueue = DispatchQueue.test + var messengerDidStartWithTimeout: [Int] = [] + var messengerDidConnect = 0 + + store.environment.bgQueue = bgQueue.eraseToAnyScheduler() + store.environment.mainQueue = mainQueue.eraseToAnyScheduler() + store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } + store.environment.messenger.isConnected.run = { false } + store.environment.messenger.connect.run = { messengerDidConnect += 1 } + store.environment.messenger.isLoggedIn.run = { false } + store.environment.messenger.isRegistered.run = { false } + + store.send(.start) + + bgQueue.advance() + + XCTAssertNoDifference(messengerDidStartWithTimeout, [30_000]) + XCTAssertNoDifference(messengerDidConnect, 1) + + mainQueue.advance() + + store.receive(.set(\.$register, RegisterState())) { + $0.register = RegisterState() + } + } + + func testStartRegistered() { + let store = TestStore( + initialState: HomeState(), + reducer: homeReducer, + environment: .unimplemented + ) + + let username = "test_username" + let bgQueue = DispatchQueue.test + let mainQueue = DispatchQueue.test + var messengerDidStartWithTimeout: [Int] = [] + var messengerDidConnect = 0 + var messengerDidLogIn = 0 + + store.environment.bgQueue = bgQueue.eraseToAnyScheduler() + store.environment.mainQueue = mainQueue.eraseToAnyScheduler() + store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } + store.environment.messenger.isConnected.run = { false } + store.environment.messenger.connect.run = { messengerDidConnect += 1 } + store.environment.messenger.isLoggedIn.run = { false } + store.environment.messenger.isRegistered.run = { true } + store.environment.messenger.logIn.run = { messengerDidLogIn += 1 } + store.environment.messenger.e2e.get = { + var e2e = E2E.unimplemented + e2e.getContact.run = { + var contact = Contact.unimplemented(Data()) + contact.getFactsFromContact.run = { _ in [Fact(fact: username, type: 0)] } + return contact + } + return e2e + } + + store.send(.start) + + bgQueue.advance() + + XCTAssertNoDifference(messengerDidStartWithTimeout, [30_000]) + XCTAssertNoDifference(messengerDidConnect, 1) + XCTAssertNoDifference(messengerDidLogIn, 1) + + mainQueue.advance() + + store.receive(.set(\.$username, username)) { + $0.username = username + } + } + + func testRegisterFinished() { + let store = TestStore( + initialState: HomeState( + register: RegisterState() + ), + reducer: homeReducer, + environment: .unimplemented + ) + + let username = "test_username" + let bgQueue = DispatchQueue.test + let mainQueue = DispatchQueue.test + var messengerDidStartWithTimeout: [Int] = [] + var messengerDidLogIn = 0 + + store.environment.bgQueue = bgQueue.eraseToAnyScheduler() + store.environment.mainQueue = mainQueue.eraseToAnyScheduler() + store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } + store.environment.messenger.isConnected.run = { true } + store.environment.messenger.isLoggedIn.run = { false } + store.environment.messenger.isRegistered.run = { true } + store.environment.messenger.logIn.run = { messengerDidLogIn += 1 } + store.environment.messenger.e2e.get = { + var e2e = E2E.unimplemented + e2e.getContact.run = { + var contact = Contact.unimplemented(Data()) + contact.getFactsFromContact.run = { _ in [Fact(fact: username, type: 0)] } + return contact + } + return e2e + } + + store.send(.register(.finished)) { + $0.register = nil + } + + store.receive(.start) + + bgQueue.advance() + + XCTAssertNoDifference(messengerDidStartWithTimeout, [30_000]) + XCTAssertNoDifference(messengerDidLogIn, 1) + + mainQueue.advance() + + store.receive(.set(\.$username, username)) { + $0.username = username + } + } + + func testStartMessengerStartFailure() { + let store = TestStore( + initialState: HomeState(), + reducer: homeReducer, + environment: .unimplemented + ) + + struct Failure: Error {} + let error = Failure() + + store.environment.bgQueue = .immediate + store.environment.mainQueue = .immediate + store.environment.messenger.start.run = { _ in throw error } + + store.send(.start) + + store.receive(.set(\.$failure, error.localizedDescription)) { + $0.failure = error.localizedDescription + } + } + + func testStartMessengerConnectFailure() { + let store = TestStore( + initialState: HomeState(), + reducer: homeReducer, + environment: .unimplemented + ) + + struct Failure: Error {} + let error = Failure() + + store.environment.bgQueue = .immediate + store.environment.mainQueue = .immediate + store.environment.messenger.start.run = { _ in } + store.environment.messenger.isConnected.run = { false } + store.environment.messenger.connect.run = { throw error } + + store.send(.start) + + store.receive(.set(\.$failure, error.localizedDescription)) { + $0.failure = error.localizedDescription + } + } + + func testStartMessengerIsRegisteredFailure() { + let store = TestStore( + initialState: HomeState(), + reducer: homeReducer, + environment: .unimplemented + ) + + struct Failure: Error {} + let error = Failure() + + store.environment.bgQueue = .immediate + store.environment.mainQueue = .immediate + store.environment.messenger.start.run = { _ in } + store.environment.messenger.isConnected.run = { true } + store.environment.messenger.isLoggedIn.run = { false } + store.environment.messenger.isRegistered.run = { throw error } + + store.send(.start) + + store.receive(.set(\.$failure, error.localizedDescription)) { + $0.failure = error.localizedDescription + } + } + + func testStartMessengerLogInFailure() { + let store = TestStore( + initialState: HomeState(), + reducer: homeReducer, + environment: .unimplemented + ) + + struct Failure: Error {} + let error = Failure() + + store.environment.bgQueue = .immediate + store.environment.mainQueue = .immediate + store.environment.messenger.start.run = { _ in } + store.environment.messenger.isConnected.run = { true } + store.environment.messenger.isLoggedIn.run = { false } + store.environment.messenger.isRegistered.run = { true } + store.environment.messenger.logIn.run = { throw error } + + store.send(.start) + + store.receive(.set(\.$failure, error.localizedDescription)) { + $0.failure = error.localizedDescription + } } }