diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index 2b2d10391ee64f2b70f06b607c67437e78f3d55a..e49750ca16702861be964a5991472e4ff186b9fb 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -38,7 +38,7 @@ let package = Package( ), .package( url: "https://github.com/pointfreeco/swift-composable-architecture.git", - .upToNextMajor(from: "0.40.2") + .upToNextMajor(from: "0.43.0") ), .package( url: "https://git.xx.network/elixxir/client-ios-db.git", @@ -46,15 +46,15 @@ let package = Package( ), .package( url: "https://github.com/darrarski/swift-composable-presentation.git", - .upToNextMajor(from: "0.5.3") + .upToNextMajor(from: "0.6.0") ), .package( url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git", - .upToNextMajor(from: "0.4.1") + .upToNextMajor(from: "0.5.0") ), .package( url: "https://github.com/pointfreeco/swift-custom-dump.git", - .upToNextMajor(from: "0.5.2") + .upToNextMajor(from: "0.6.0") ), .package( url: "https://github.com/apple/swift-log.git", @@ -62,13 +62,14 @@ let package = Package( ), .package( url: "https://github.com/kean/Pulse.git", - .upToNextMajor(from: "2.1.2") + .upToNextMajor(from: "2.1.3") ), ], targets: [ .target( name: "AppCore", dependencies: [ + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "Logging", package: "swift-log"), .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), @@ -127,6 +128,7 @@ let package = Package( .target( name: "BackupFeature", dependencies: [ + .target(name: "AppCore"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), ], @@ -376,6 +378,7 @@ let package = Package( .target( name: "UserSearchFeature", dependencies: [ + .target(name: "AppCore"), .target(name: "ContactFeature"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "ComposablePresentation", package: "swift-composable-presentation"), diff --git a/Examples/xx-messenger/Sources/AppCore/AppDependecies.swift b/Examples/xx-messenger/Sources/AppCore/AppDependecies.swift new file mode 100644 index 0000000000000000000000000000000000000000..fcb72dc0ee649c5e096f6397396bbaad4e1d6194 --- /dev/null +++ b/Examples/xx-messenger/Sources/AppCore/AppDependecies.swift @@ -0,0 +1,96 @@ +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay +import XXMessengerClient + +public struct AppDependencies { + public var dbManager: DBManager + public var messenger: Messenger + public var authHandler: AuthCallbackHandler + public var backupStorage: BackupStorage + public var mainQueue: AnySchedulerOf<DispatchQueue> + public var bgQueue: AnySchedulerOf<DispatchQueue> + public var now: () -> Date + public var sendMessage: SendMessage + public var sendImage: SendImage + public var messageListener: MessageListenerHandler + public var receiveFileHandler: ReceiveFileHandler + public var log: Logger + public var loadData: URLDataLoader +} + +extension AppDependencies { + public static func live() -> AppDependencies { + let dbManager = DBManager.live() + let messengerEnv = MessengerEnvironment.live() + let messenger = Messenger.live(messengerEnv) + let now: () -> Date = Date.init + + return AppDependencies( + dbManager: dbManager, + messenger: messenger, + authHandler: .live( + messenger: messenger, + handleRequest: .live(db: dbManager.getDB, now: now), + handleConfirm: .live(db: dbManager.getDB), + handleReset: .live(db: dbManager.getDB) + ), + backupStorage: .onDisk(), + mainQueue: DispatchQueue.main.eraseToAnyScheduler(), + bgQueue: DispatchQueue.global(qos: .background).eraseToAnyScheduler(), + now: now, + sendMessage: .live( + messenger: messenger, + db: dbManager.getDB, + now: now + ), + sendImage: .live( + messenger: messenger, + db: dbManager.getDB, + now: now + ), + messageListener: .live( + messenger: messenger, + db: dbManager.getDB + ), + receiveFileHandler: .live( + messenger: messenger, + db: dbManager.getDB, + now: now + ), + log: .live(), + loadData: .live + ) + } + + public static let unimplemented = AppDependencies( + dbManager: .unimplemented, + messenger: .unimplemented, + authHandler: .unimplemented, + backupStorage: .unimplemented, + mainQueue: .unimplemented, + bgQueue: .unimplemented, + now: XCTestDynamicOverlay.unimplemented( + "\(Self.self)", + placeholder: Date(timeIntervalSince1970: 0) + ), + sendMessage: .unimplemented, + sendImage: .unimplemented, + messageListener: .unimplemented, + receiveFileHandler: .unimplemented, + log: .unimplemented, + loadData: .unimplemented + ) +} + +private enum AppDependenciesKey: DependencyKey { + static let liveValue: AppDependencies = .live() + static let testValue: AppDependencies = .unimplemented +} + +extension DependencyValues { + public var app: AppDependencies { + get { self[AppDependenciesKey.self] } + set { self[AppDependenciesKey.self] = newValue } + } +} diff --git a/Examples/xx-messenger/Sources/RestoreFeature/URLDataLoader.swift b/Examples/xx-messenger/Sources/AppCore/URLDataLoader/URLDataLoader.swift similarity index 100% rename from Examples/xx-messenger/Sources/RestoreFeature/URLDataLoader.swift rename to Examples/xx-messenger/Sources/AppCore/URLDataLoader/URLDataLoader.swift diff --git a/Examples/xx-messenger/Sources/AppFeature/App.swift b/Examples/xx-messenger/Sources/AppFeature/App.swift index 7c3f0e82b5b00ee3aa4ff601b50556dbc8a148d2..c395f2ad0b8ac415bdea844055d5f85ca7ad9672 100644 --- a/Examples/xx-messenger/Sources/AppFeature/App.swift +++ b/Examples/xx-messenger/Sources/AppFeature/App.swift @@ -7,15 +7,17 @@ import SwiftUI struct App: SwiftUI.App { init() { LoggingSystem.bootstrap(PersistentLogHandler.init) + ViewStore(store.stateless).send(.setupLogging) } + let store = Store( + initialState: AppComponent.State(), + reducer: AppComponent() + ) + var body: some Scene { WindowGroup { - AppView(store: Store( - initialState: AppState(), - reducer: appReducer, - environment: .live() - )) + AppView(store: store) } } } diff --git a/Examples/xx-messenger/Sources/AppFeature/AppComponent.swift b/Examples/xx-messenger/Sources/AppFeature/AppComponent.swift new file mode 100644 index 0000000000000000000000000000000000000000..d092a69e57ac781372297df8de4cdcce02a43d36 --- /dev/null +++ b/Examples/xx-messenger/Sources/AppFeature/AppComponent.swift @@ -0,0 +1,159 @@ +import AppCore +import Combine +import ComposableArchitecture +import ComposablePresentation +import Foundation +import HomeFeature +import RestoreFeature +import WelcomeFeature +import XXClient +import XXMessengerClient + +struct AppComponent: ReducerProtocol { + struct State: Equatable { + enum Screen: Equatable { + case loading + case welcome(WelcomeComponent.State) + case restore(RestoreComponent.State) + case home(HomeComponent.State) + case failure(String) + } + + @BindableState var screen: Screen = .loading + } + + enum Action: Equatable, BindableAction { + case setupLogging + case start + case stop + case binding(BindingAction<State>) + case welcome(WelcomeComponent.Action) + case restore(RestoreComponent.Action) + case home(HomeComponent.Action) + } + + @Dependency(\.app.dbManager) var dbManager: DBManager + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.authHandler) var authHandler: AuthCallbackHandler + @Dependency(\.app.messageListener) var messageListener: MessageListenerHandler + @Dependency(\.app.receiveFileHandler) var receiveFileHandler: ReceiveFileHandler + @Dependency(\.app.backupStorage) var backupStorage: BackupStorage + @Dependency(\.app.log) var log: Logger + @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + + var body: some ReducerProtocol<State, Action> { + BindingReducer() + Reduce { state, action in + enum EffectId {} + + let dbManager = self.dbManager + let messenger = self.messenger + let authHandler = self.authHandler + let messageListener = self.messageListener + let receiveFileHandler = self.receiveFileHandler + let backupStorage = self.backupStorage + let log = self.log + + switch action { + case .setupLogging: + _ = try! messenger.setLogLevel(.debug) + messenger.startLogging() + return .none + + case .start, .welcome(.finished), .restore(.finished), .home(.deleteAccount(.success)): + state.screen = .loading + return Effect.run { subscriber in + var cancellables: [XXClient.Cancellable] = [] + + do { + if dbManager.hasDB() == false { + try dbManager.makeDB() + } + + cancellables.append(authHandler(onError: { error in + log(.error(error as NSError)) + })) + cancellables.append(messageListener(onError: { error in + log(.error(error as NSError)) + })) + cancellables.append(receiveFileHandler(onError: { error in + log(.error(error as NSError)) + })) + + cancellables.append(messenger.registerBackupCallback(.init { data in + try? backupStorage.store(data) + })) + + let isLoaded = messenger.isLoaded() + let isCreated = messenger.isCreated() + + if !isLoaded, !isCreated { + subscriber.send(.set(\.$screen, .welcome(WelcomeComponent.State()))) + } else if !isLoaded { + try messenger.load() + subscriber.send(.set(\.$screen, .home(HomeComponent.State()))) + } else { + subscriber.send(.set(\.$screen, .home(HomeComponent.State()))) + } + } catch { + subscriber.send(.set(\.$screen, .failure(error.localizedDescription))) + } + + return AnyCancellable { cancellables.forEach { $0.cancel() } } + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + .cancellable(id: EffectId.self, cancelInFlight: true) + + case .stop: + return .cancel(id: EffectId.self) + + case .welcome(.restoreTapped): + state.screen = .restore(RestoreComponent.State()) + return .none + + case .welcome(.failed(let failure)): + state.screen = .failure(failure) + return .none + + case .binding(_), .welcome(_), .restore(_), .home(_): + return .none + } + } + .presenting( + state: .keyPath(\.screen.asWelcome), + id: .notNil(), + action: /Action.welcome, + presented: { WelcomeComponent() } + ) + .presenting( + state: .keyPath(\.screen.asRestore), + id: .notNil(), + action: /Action.restore, + presented: { RestoreComponent() } + ) + .presenting( + state: .keyPath(\.screen.asHome), + id: .notNil(), + action: /Action.home, + presented: { HomeComponent() } + ) + } +} + +extension AppComponent.State.Screen { + var asWelcome: WelcomeComponent.State? { + get { (/AppComponent.State.Screen.welcome).extract(from: self) } + set { if let newValue = newValue { self = .welcome(newValue) } } + } + var asRestore: RestoreComponent.State? { + get { (/AppComponent.State.Screen.restore).extract(from: self) } + set { if let state = newValue { self = .restore(state) } } + } + var asHome: HomeComponent.State? { + get { (/AppComponent.State.Screen.home).extract(from: self) } + set { if let newValue = newValue { self = .home(newValue) } } + } +} diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift deleted file mode 100644 index 2b1f9964c4b900bee5596eb7fee1e1f59ef4f7c8..0000000000000000000000000000000000000000 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ /dev/null @@ -1,199 +0,0 @@ -import AppCore -import BackupFeature -import ChatFeature -import CheckContactAuthFeature -import ConfirmRequestFeature -import ContactFeature -import ContactLookupFeature -import ContactsFeature -import Foundation -import HomeFeature -import MyContactFeature -import RegisterFeature -import ResetAuthFeature -import RestoreFeature -import SendRequestFeature -import UserSearchFeature -import VerifyContactFeature -import WelcomeFeature -import XXMessengerClient -import XXModels - -extension AppEnvironment { - static func live() -> AppEnvironment { - let dbManager = DBManager.live() - let messengerEnv = MessengerEnvironment.live() - let messenger = Messenger.live(messengerEnv) - let authHandler = AuthCallbackHandler.live( - messenger: messenger, - handleRequest: .live(db: dbManager.getDB, now: Date.init), - handleConfirm: .live(db: dbManager.getDB), - handleReset: .live(db: dbManager.getDB) - ) - let backupStorage = BackupStorage.onDisk() - let mainQueue = DispatchQueue.main.eraseToAnyScheduler() - let bgQueue = DispatchQueue.global(qos: .background).eraseToAnyScheduler() - - defer { - _ = try! messenger.setLogLevel(.debug) - messenger.startLogging() - } - - let contactEnvironment = ContactEnvironment( - messenger: messenger, - db: dbManager.getDB, - mainQueue: mainQueue, - bgQueue: bgQueue, - lookup: { - ContactLookupEnvironment( - messenger: messenger, - mainQueue: mainQueue, - bgQueue: bgQueue - ) - }, - sendRequest: { - SendRequestEnvironment( - messenger: messenger, - db: dbManager.getDB, - mainQueue: mainQueue, - bgQueue: bgQueue - ) - }, - verifyContact: { - VerifyContactEnvironment( - messenger: messenger, - db: dbManager.getDB, - mainQueue: mainQueue, - bgQueue: bgQueue - ) - }, - confirmRequest: { - ConfirmRequestEnvironment( - messenger: messenger, - db: dbManager.getDB, - mainQueue: mainQueue, - bgQueue: bgQueue - ) - }, - checkAuth: { - CheckContactAuthEnvironment( - messenger: messenger, - db: dbManager.getDB, - mainQueue: mainQueue, - bgQueue: bgQueue - ) - }, - resetAuth: { - ResetAuthEnvironment( - messenger: messenger, - mainQueue: mainQueue, - bgQueue: bgQueue - ) - }, - chat: { - ChatEnvironment( - messenger: messenger, - db: dbManager.getDB, - sendMessage: .live( - messenger: messenger, - db: dbManager.getDB, - now: Date.init - ), - sendImage: .live( - messenger: messenger, - db: dbManager.getDB, - now: Date.init - ), - mainQueue: mainQueue, - bgQueue: bgQueue - ) - } - ) - - return AppEnvironment( - dbManager: dbManager, - messenger: messenger, - authHandler: authHandler, - messageListener: .live( - messenger: messenger, - db: dbManager.getDB - ), - receiveFileHandler: .live( - messenger: messenger, - db: dbManager.getDB, - now: Date.init - ), - backupStorage: backupStorage, - log: .live(), - mainQueue: mainQueue, - bgQueue: bgQueue, - welcome: { - WelcomeEnvironment( - messenger: messenger, - mainQueue: mainQueue, - bgQueue: bgQueue - ) - }, - restore: { - RestoreEnvironment( - messenger: messenger, - db: dbManager.getDB, - loadData: .live, - now: Date.init, - mainQueue: mainQueue, - bgQueue: bgQueue - ) - }, - home: { - HomeEnvironment( - messenger: messenger, - dbManager: dbManager, - mainQueue: mainQueue, - bgQueue: bgQueue, - register: { - RegisterEnvironment( - messenger: messenger, - db: dbManager.getDB, - now: Date.init, - mainQueue: mainQueue, - bgQueue: bgQueue - ) - }, - contacts: { - ContactsEnvironment( - messenger: messenger, - db: dbManager.getDB, - mainQueue: mainQueue, - bgQueue: bgQueue, - contact: { contactEnvironment }, - myContact: { - MyContactEnvironment( - messenger: messenger, - db: dbManager.getDB, - mainQueue: mainQueue, - bgQueue: bgQueue - ) - } - ) - }, - userSearch: { - UserSearchEnvironment( - messenger: messenger, - mainQueue: mainQueue, - bgQueue: bgQueue, - contact: { contactEnvironment } - ) - }, - backup: { - BackupEnvironment( - messenger: messenger, - backupStorage: backupStorage, - mainQueue: mainQueue, - bgQueue: bgQueue - ) - } - ) - } - ) - } -} diff --git a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift b/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift deleted file mode 100644 index 56f692c6c1b1cb83f010ec479fb2d82058f4b25e..0000000000000000000000000000000000000000 --- a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift +++ /dev/null @@ -1,169 +0,0 @@ -import AppCore -import Combine -import ComposableArchitecture -import ComposablePresentation -import Foundation -import HomeFeature -import RestoreFeature -import WelcomeFeature -import XXClient -import XXMessengerClient - -struct AppState: Equatable { - enum Screen: Equatable { - case loading - case welcome(WelcomeState) - case restore(RestoreState) - case home(HomeState) - case failure(String) - } - - @BindableState var screen: Screen = .loading -} - -extension AppState.Screen { - var asWelcome: WelcomeState? { - get { (/AppState.Screen.welcome).extract(from: self) } - set { if let newValue = newValue { self = .welcome(newValue) } } - } - var asRestore: RestoreState? { - get { (/AppState.Screen.restore).extract(from: self) } - set { if let state = newValue { self = .restore(state) } } - } - var asHome: HomeState? { - get { (/AppState.Screen.home).extract(from: self) } - set { if let newValue = newValue { self = .home(newValue) } } - } -} - -enum AppAction: Equatable, BindableAction { - case start - case stop - case binding(BindingAction<AppState>) - case welcome(WelcomeAction) - case restore(RestoreAction) - case home(HomeAction) -} - -struct AppEnvironment { - var dbManager: DBManager - var messenger: Messenger - var authHandler: AuthCallbackHandler - var messageListener: MessageListenerHandler - var receiveFileHandler: ReceiveFileHandler - var backupStorage: BackupStorage - var log: Logger - var mainQueue: AnySchedulerOf<DispatchQueue> - var bgQueue: AnySchedulerOf<DispatchQueue> - var welcome: () -> WelcomeEnvironment - var restore: () -> RestoreEnvironment - var home: () -> HomeEnvironment -} - -#if DEBUG -extension AppEnvironment { - static let unimplemented = AppEnvironment( - dbManager: .unimplemented, - messenger: .unimplemented, - authHandler: .unimplemented, - messageListener: .unimplemented, - receiveFileHandler: .unimplemented, - backupStorage: .unimplemented, - log: .unimplemented, - mainQueue: .unimplemented, - bgQueue: .unimplemented, - welcome: { .unimplemented }, - restore: { .unimplemented }, - home: { .unimplemented } - ) -} -#endif - -let appReducer = Reducer<AppState, AppAction, AppEnvironment> -{ state, action, env in - enum EffectId {} - - switch action { - case .start, .welcome(.finished), .restore(.finished), .home(.deleteAccount(.success)): - state.screen = .loading - return Effect.run { subscriber in - var cancellables: [XXClient.Cancellable] = [] - - do { - if env.dbManager.hasDB() == false { - try env.dbManager.makeDB() - } - - cancellables.append(env.authHandler(onError: { error in - env.log(.error(error as NSError)) - })) - cancellables.append(env.messageListener(onError: { error in - env.log(.error(error as NSError)) - })) - cancellables.append(env.receiveFileHandler(onError: { error in - env.log(.error(error as NSError)) - })) - - cancellables.append(env.messenger.registerBackupCallback(.init { data in - try? env.backupStorage.store(data) - })) - - let isLoaded = env.messenger.isLoaded() - let isCreated = env.messenger.isCreated() - - if !isLoaded, !isCreated { - subscriber.send(.set(\.$screen, .welcome(WelcomeState()))) - } else if !isLoaded { - try env.messenger.load() - subscriber.send(.set(\.$screen, .home(HomeState()))) - } else { - subscriber.send(.set(\.$screen, .home(HomeState()))) - } - } catch { - subscriber.send(.set(\.$screen, .failure(error.localizedDescription))) - } - - return AnyCancellable { cancellables.forEach { $0.cancel() } } - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - .cancellable(id: EffectId.self, cancelInFlight: true) - - case .stop: - return .cancel(id: EffectId.self) - - case .welcome(.restoreTapped): - state.screen = .restore(RestoreState()) - return .none - - case .welcome(.failed(let failure)): - state.screen = .failure(failure) - return .none - - case .binding(_), .welcome(_), .restore(_), .home(_): - return .none - } -} -.binding() -.presenting( - welcomeReducer, - state: .keyPath(\.screen.asWelcome), - id: .notNil(), - action: /AppAction.welcome, - environment: { $0.welcome() } -) -.presenting( - restoreReducer, - state: .keyPath(\.screen.asRestore), - id: .notNil(), - action: /AppAction.restore, - environment: { $0.restore() } -) -.presenting( - homeReducer, - state: .keyPath(\.screen.asHome), - id: .notNil(), - action: /AppAction.home, - environment: { $0.home() } -) diff --git a/Examples/xx-messenger/Sources/AppFeature/AppView.swift b/Examples/xx-messenger/Sources/AppFeature/AppView.swift index 57983b1dd0b826321f3dac8c120637736ff64b60..e0ffb3425c2f4cfcc96499384789676d8b42ae4b 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppView.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppView.swift @@ -6,7 +6,7 @@ import SwiftUI import WelcomeFeature struct AppView: View { - let store: Store<AppState, AppAction> + let store: StoreOf<AppComponent> @State var isPresentingPulse = false enum ViewState: Equatable { @@ -16,7 +16,7 @@ struct AppView: View { case home case failure(String) - init(_ state: AppState) { + init(_ state: AppComponent.State) { switch state.screen { case .loading: self = .loading case .welcome(_): self = .welcome @@ -42,8 +42,8 @@ struct AppView: View { case .welcome: IfLetStore( store.scope( - state: { (/AppState.Screen.welcome).extract(from: $0.screen) }, - action: AppAction.welcome + state: { (/AppComponent.State.Screen.welcome).extract(from: $0.screen) }, + action: AppComponent.Action.welcome ), then: { store in WelcomeView(store: store) @@ -58,8 +58,8 @@ struct AppView: View { case .restore: IfLetStore( store.scope( - state: { (/AppState.Screen.restore).extract(from: $0.screen) }, - action: AppAction.restore + state: { (/AppComponent.State.Screen.restore).extract(from: $0.screen) }, + action: AppComponent.Action.restore ), then: { store in RestoreView(store: store) @@ -74,8 +74,8 @@ struct AppView: View { case .home: IfLetStore( store.scope( - state: { (/AppState.Screen.home).extract(from: $0.screen) }, - action: AppAction.home + state: { (/AppComponent.State.Screen.home).extract(from: $0.screen) }, + action: AppComponent.Action.home ), then: { store in HomeView(store: store) @@ -137,9 +137,8 @@ struct AppView: View { struct AppView_Previews: PreviewProvider { static var previews: some View { AppView(store: Store( - initialState: AppState(), - reducer: .empty, - environment: () + initialState: AppComponent.State(), + reducer: EmptyReducer() )) } } diff --git a/Examples/xx-messenger/Sources/BackupFeature/Alerts.swift b/Examples/xx-messenger/Sources/BackupFeature/Alerts.swift index 2bd95f770ff5ce1be9e808284d1d50f3d111af1d..e261adeba5b042c931817ffa7f0d4a6d849a2671 100644 --- a/Examples/xx-messenger/Sources/BackupFeature/Alerts.swift +++ b/Examples/xx-messenger/Sources/BackupFeature/Alerts.swift @@ -1,8 +1,8 @@ import ComposableArchitecture -extension AlertState where Action == BackupAction { - public static func error(_ error: Error) -> AlertState<BackupAction> { - AlertState( +extension AlertState where Action == BackupComponent.Action { + public static func error(_ error: Error) -> AlertState<BackupComponent.Action> { + AlertState<BackupComponent.Action>( title: TextState("Error"), message: TextState(error.localizedDescription) ) diff --git a/Examples/xx-messenger/Sources/BackupFeature/BackupComponent.swift b/Examples/xx-messenger/Sources/BackupFeature/BackupComponent.swift new file mode 100644 index 0000000000000000000000000000000000000000..af3e3c34a5728a73a084c307b918fab128cce7b8 --- /dev/null +++ b/Examples/xx-messenger/Sources/BackupFeature/BackupComponent.swift @@ -0,0 +1,209 @@ +import AppCore +import Combine +import ComposableArchitecture +import Foundation +import XXClient +import XXMessengerClient + +public struct BackupComponent: ReducerProtocol { + public struct State: Equatable { + public enum Field: String, Hashable { + case passphrase + } + + public enum Error: String, Swift.Error, Equatable { + case contactUsernameMissing + } + + public init( + isRunning: Bool = false, + isStarting: Bool = false, + isResuming: Bool = false, + isStopping: Bool = false, + backup: BackupStorage.Backup? = nil, + alert: AlertState<Action>? = nil, + focusedField: Field? = nil, + passphrase: String = "", + isExporting: Bool = false, + exportData: Data? = nil + ) { + self.isRunning = isRunning + self.isStarting = isStarting + self.isResuming = isResuming + self.isStopping = isStopping + self.backup = backup + self.alert = alert + self.focusedField = focusedField + self.passphrase = passphrase + self.isExporting = isExporting + self.exportData = exportData + } + + public var isRunning: Bool + public var isStarting: Bool + public var isResuming: Bool + public var isStopping: Bool + public var backup: BackupStorage.Backup? + public var alert: AlertState<Action>? + @BindableState public var focusedField: Field? + @BindableState public var passphrase: String + @BindableState public var isExporting: Bool + public var exportData: Data? + } + + public enum Action: Equatable, BindableAction { + case task + case cancelTask + case startTapped + case resumeTapped + case stopTapped + case exportTapped + case alertDismissed + case backupUpdated(BackupStorage.Backup?) + case didStart(failure: NSError?) + case didResume(failure: NSError?) + case didStop(failure: NSError?) + case didExport(failure: NSError?) + case binding(BindingAction<State>) + } + + public init() {} + + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.backupStorage) var backupStorage: BackupStorage + @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + + public var body: some ReducerProtocol<State, Action> { + BindingReducer() + Reduce { state, action in + enum TaskEffectId {} + + switch action { + case .task: + state.isRunning = messenger.isBackupRunning() + return Effect.run { subscriber in + subscriber.send(.backupUpdated(backupStorage.stored())) + let cancellable = backupStorage.observe { backup in + subscriber.send(.backupUpdated(backup)) + } + return AnyCancellable { cancellable.cancel() } + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + .cancellable(id: TaskEffectId.self, cancelInFlight: true) + + case .cancelTask: + return .cancel(id: TaskEffectId.self) + + case .startTapped: + state.isStarting = true + state.focusedField = nil + return Effect.run { [state] subscriber in + do { + let contact = try messenger.myContact(includeFacts: .types([.username])) + guard let username = try contact.getFact(.username)?.value else { + throw State.Error.contactUsernameMissing + } + try messenger.startBackup( + password: state.passphrase, + params: BackupParams(username: username) + ) + subscriber.send(.didStart(failure: nil)) + } catch { + subscriber.send(.didStart(failure: error as NSError)) + } + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .resumeTapped: + state.isResuming = true + return Effect.run { subscriber in + do { + try messenger.resumeBackup() + subscriber.send(.didResume(failure: nil)) + } catch { + subscriber.send(.didResume(failure: error as NSError)) + } + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .stopTapped: + state.isStopping = true + return Effect.run { subscriber in + do { + try messenger.stopBackup() + try backupStorage.remove() + subscriber.send(.didStop(failure: nil)) + } catch { + subscriber.send(.didStop(failure: error as NSError)) + } + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .exportTapped: + state.isExporting = true + state.exportData = state.backup?.data + return .none + + case .alertDismissed: + state.alert = nil + return .none + + case .backupUpdated(let backup): + state.backup = backup + return .none + + case .didStart(let failure): + state.isRunning = messenger.isBackupRunning() + state.isStarting = false + if let failure { + state.alert = .error(failure) + } else { + state.passphrase = "" + } + return .none + + case .didResume(let failure): + state.isRunning = messenger.isBackupRunning() + state.isResuming = false + if let failure { + state.alert = .error(failure) + } + return .none + + case .didStop(let failure): + state.isRunning = messenger.isBackupRunning() + state.isStopping = false + if let failure { + state.alert = .error(failure) + } + return .none + + case .didExport(let failure): + state.isExporting = false + state.exportData = nil + if let failure { + state.alert = .error(failure) + } + return .none + + case .binding(_): + return .none + } + } + } +} diff --git a/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift b/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift deleted file mode 100644 index 8010f17d1d0727c4621841dd124a93dcda0e0b5d..0000000000000000000000000000000000000000 --- a/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift +++ /dev/null @@ -1,228 +0,0 @@ -import Combine -import ComposableArchitecture -import Foundation -import XXClient -import XXMessengerClient - -public struct BackupState: Equatable { - public enum Field: String, Hashable { - case passphrase - } - - public enum Error: String, Swift.Error, Equatable { - case contactUsernameMissing - } - - public init( - isRunning: Bool = false, - isStarting: Bool = false, - isResuming: Bool = false, - isStopping: Bool = false, - backup: BackupStorage.Backup? = nil, - alert: AlertState<BackupAction>? = nil, - focusedField: Field? = nil, - passphrase: String = "", - isExporting: Bool = false, - exportData: Data? = nil - ) { - self.isRunning = isRunning - self.isStarting = isStarting - self.isResuming = isResuming - self.isStopping = isStopping - self.backup = backup - self.alert = alert - self.focusedField = focusedField - self.passphrase = passphrase - self.isExporting = isExporting - self.exportData = exportData - } - - public var isRunning: Bool - public var isStarting: Bool - public var isResuming: Bool - public var isStopping: Bool - public var backup: BackupStorage.Backup? - public var alert: AlertState<BackupAction>? - @BindableState public var focusedField: Field? - @BindableState public var passphrase: String - @BindableState public var isExporting: Bool - public var exportData: Data? -} - -public enum BackupAction: Equatable, BindableAction { - case task - case cancelTask - case startTapped - case resumeTapped - case stopTapped - case exportTapped - case alertDismissed - case backupUpdated(BackupStorage.Backup?) - case didStart(failure: NSError?) - case didResume(failure: NSError?) - case didStop(failure: NSError?) - case didExport(failure: NSError?) - case binding(BindingAction<BackupState>) -} - -public struct BackupEnvironment { - public init( - messenger: Messenger, - backupStorage: BackupStorage, - mainQueue: AnySchedulerOf<DispatchQueue>, - bgQueue: AnySchedulerOf<DispatchQueue> - ) { - self.messenger = messenger - self.backupStorage = backupStorage - self.mainQueue = mainQueue - self.bgQueue = bgQueue - } - - public var messenger: Messenger - public var backupStorage: BackupStorage - public var mainQueue: AnySchedulerOf<DispatchQueue> - public var bgQueue: AnySchedulerOf<DispatchQueue> -} - -#if DEBUG -extension BackupEnvironment { - public static let unimplemented = BackupEnvironment( - messenger: .unimplemented, - backupStorage: .unimplemented, - mainQueue: .unimplemented, - bgQueue: .unimplemented - ) -} -#endif - -public let backupReducer = Reducer<BackupState, BackupAction, BackupEnvironment> -{ state, action, env in - enum TaskEffectId {} - - switch action { - case .task: - state.isRunning = env.messenger.isBackupRunning() - return Effect.run { subscriber in - subscriber.send(.backupUpdated(env.backupStorage.stored())) - let cancellable = env.backupStorage.observe { backup in - subscriber.send(.backupUpdated(backup)) - } - return AnyCancellable { cancellable.cancel() } - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - .cancellable(id: TaskEffectId.self, cancelInFlight: true) - - case .cancelTask: - return .cancel(id: TaskEffectId.self) - - case .startTapped: - state.isStarting = true - state.focusedField = nil - return Effect.run { [state] subscriber in - do { - let contact = try env.messenger.myContact(includeFacts: .types([.username])) - guard let username = try contact.getFact(.username)?.value else { - throw BackupState.Error.contactUsernameMissing - } - try env.messenger.startBackup( - password: state.passphrase, - params: BackupParams(username: username) - ) - subscriber.send(.didStart(failure: nil)) - } catch { - subscriber.send(.didStart(failure: error as NSError)) - } - subscriber.send(completion: .finished) - return AnyCancellable {} - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .resumeTapped: - state.isResuming = true - return Effect.run { subscriber in - do { - try env.messenger.resumeBackup() - subscriber.send(.didResume(failure: nil)) - } catch { - subscriber.send(.didResume(failure: error as NSError)) - } - subscriber.send(completion: .finished) - return AnyCancellable {} - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .stopTapped: - state.isStopping = true - return Effect.run { subscriber in - do { - try env.messenger.stopBackup() - try env.backupStorage.remove() - subscriber.send(.didStop(failure: nil)) - } catch { - subscriber.send(.didStop(failure: error as NSError)) - } - subscriber.send(completion: .finished) - return AnyCancellable {} - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .exportTapped: - state.isExporting = true - state.exportData = state.backup?.data - return .none - - case .alertDismissed: - state.alert = nil - return .none - - case .backupUpdated(let backup): - state.backup = backup - return .none - - case .didStart(let failure): - state.isRunning = env.messenger.isBackupRunning() - state.isStarting = false - if let failure { - state.alert = .error(failure) - } else { - state.passphrase = "" - } - return .none - - case .didResume(let failure): - state.isRunning = env.messenger.isBackupRunning() - state.isResuming = false - if let failure { - state.alert = .error(failure) - } - return .none - - case .didStop(let failure): - state.isRunning = env.messenger.isBackupRunning() - state.isStopping = false - if let failure { - state.alert = .error(failure) - } - return .none - - case .didExport(let failure): - state.isExporting = false - state.exportData = nil - if let failure { - state.alert = .error(failure) - } - return .none - - case .binding(_): - return .none - } -} -.binding() diff --git a/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift b/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift index 89510b2fbf2c993afa1438fd62408dc925d1c30e..78805e66acd18a8200d461f803243673d509bdac 100644 --- a/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift +++ b/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift @@ -3,12 +3,12 @@ import SwiftUI import UniformTypeIdentifiers public struct BackupView: View { - public init(store: Store<BackupState, BackupAction>) { + public init(store: StoreOf<BackupComponent>) { self.store = store } - let store: Store<BackupState, BackupAction> - @FocusState var focusedField: BackupState.Field? + let store: StoreOf<BackupComponent> + @FocusState var focusedField: BackupComponent.State.Field? struct ViewState: Equatable { struct Backup: Equatable { @@ -16,7 +16,7 @@ public struct BackupView: View { var size: Int } - init(state: BackupState) { + init(state: BackupComponent.State) { isRunning = state.isRunning isStarting = state.isStarting isResuming = state.isResuming @@ -36,7 +36,7 @@ public struct BackupView: View { var isStopping: Bool var isLoading: Bool { isStarting || isResuming || isStopping } var backup: Backup? - var focusedField: BackupState.Field? + var focusedField: BackupComponent.State.Field? var passphrase: String var isExporting: Bool var exportData: Data? @@ -67,7 +67,7 @@ public struct BackupView: View { } @ViewBuilder func newBackupSection( - _ viewStore: ViewStore<ViewState, BackupAction> + _ viewStore: ViewStore<ViewState, BackupComponent.Action> ) -> some View { Section { SecureField( @@ -103,7 +103,7 @@ public struct BackupView: View { } @ViewBuilder func backupSection( - _ viewStore: ViewStore<ViewState, BackupAction> + _ viewStore: ViewStore<ViewState, BackupComponent.Action> ) -> some View { Section { backupView(viewStore) @@ -115,7 +115,7 @@ public struct BackupView: View { } @ViewBuilder func backupView( - _ viewStore: ViewStore<ViewState, BackupAction> + _ viewStore: ViewStore<ViewState, BackupComponent.Action> ) -> some View { if let backup = viewStore.backup { HStack { @@ -165,7 +165,7 @@ public struct BackupView: View { } @ViewBuilder func stopView( - _ viewStore: ViewStore<ViewState, BackupAction> + _ viewStore: ViewStore<ViewState, BackupComponent.Action> ) -> some View { if viewStore.isRunning { Button { @@ -185,7 +185,7 @@ public struct BackupView: View { } @ViewBuilder func resumeView( - _ viewStore: ViewStore<ViewState, BackupAction> + _ viewStore: ViewStore<ViewState, BackupComponent.Action> ) -> some View { if !viewStore.isRunning, viewStore.backup != nil { Button { @@ -240,9 +240,8 @@ public struct BackupView_Previews: PreviewProvider { public static var previews: some View { NavigationView { BackupView(store: Store( - initialState: BackupState(), - reducer: .empty, - environment: () + initialState: BackupComponent.State(), + reducer: EmptyReducer() )) } } diff --git a/Examples/xx-messenger/Sources/ChatFeature/ChatComponent.swift b/Examples/xx-messenger/Sources/ChatFeature/ChatComponent.swift new file mode 100644 index 0000000000000000000000000000000000000000..6c71789dbbb201122cfda709999ac36984363b55 --- /dev/null +++ b/Examples/xx-messenger/Sources/ChatFeature/ChatComponent.swift @@ -0,0 +1,208 @@ +import AppCore +import Combine +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct ChatComponent: ReducerProtocol { + public struct State: Equatable, Identifiable { + public enum ID: Equatable, Hashable { + case contact(Data) + } + + public struct Message: Equatable, Identifiable { + public init( + id: Int64, + date: Date, + senderId: Data, + text: String, + status: XXModels.Message.Status, + fileTransfer: XXModels.FileTransfer? = nil + ) { + self.id = id + self.date = date + self.senderId = senderId + self.text = text + self.status = status + self.fileTransfer = fileTransfer + } + + public var id: Int64 + public var date: Date + public var senderId: Data + public var text: String + public var status: XXModels.Message.Status + public var fileTransfer: XXModels.FileTransfer? + } + + public init( + id: ID, + myContactId: Data? = nil, + messages: IdentifiedArrayOf<Message> = [], + failure: String? = nil, + sendFailure: String? = nil, + text: String = "" + ) { + self.id = id + self.myContactId = myContactId + self.messages = messages + self.failure = failure + self.sendFailure = sendFailure + self.text = text + } + + public var id: ID + public var myContactId: Data? + public var messages: IdentifiedArrayOf<Message> + public var failure: String? + public var sendFailure: String? + @BindableState public var text: String + } + + public enum Action: Equatable, BindableAction { + case start + case didFetchMessages(IdentifiedArrayOf<State.Message>) + case sendTapped + case sendFailed(String) + case imagePicked(Data) + case dismissSendFailureTapped + case binding(BindingAction<State>) + } + + public init() {} + + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB + @Dependency(\.app.sendMessage) var sendMessage: SendMessage + @Dependency(\.app.sendImage) var sendImage: SendImage + @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + + public var body: some ReducerProtocol<State, Action> { + BindingReducer() + Reduce { state, action in + enum FetchEffectId {} + + switch action { + case .start: + state.failure = nil + do { + let myContactId = try messenger.e2e.tryGet().getContact().getId() + state.myContactId = myContactId + let queryChat: XXModels.Message.Query.Chat + let receivedFileTransfersQuery: XXModels.FileTransfer.Query + let sentFileTransfersQuery: XXModels.FileTransfer.Query + switch state.id { + case .contact(let contactId): + queryChat = .direct(myContactId, contactId) + receivedFileTransfersQuery = .init( + contactId: contactId, + isIncoming: true + ) + sentFileTransfersQuery = .init( + contactId: myContactId, + isIncoming: false + ) + } + let messagesQuery = XXModels.Message.Query(chat: queryChat) + return Publishers.CombineLatest3( + try db().fetchMessagesPublisher(messagesQuery), + try db().fetchFileTransfersPublisher(receivedFileTransfersQuery), + try db().fetchFileTransfersPublisher(sentFileTransfersQuery) + ) + .map { messages, receivedFileTransfers, sentFileTransfers in + (messages, receivedFileTransfers + sentFileTransfers) + } + .assertNoFailure() + .map { messages, fileTransfers in + messages.compactMap { message in + guard let id = message.id else { return nil } + return State.Message( + id: id, + date: message.date, + senderId: message.senderId, + text: message.text, + status: message.status, + fileTransfer: fileTransfers.first { $0.id == message.fileTransferId } + ) + } + } + .removeDuplicates() + .map { IdentifiedArrayOf<State.Message>(uniqueElements: $0) } + .map(Action.didFetchMessages) + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + .cancellable(id: FetchEffectId.self, cancelInFlight: true) + } catch { + state.failure = error.localizedDescription + return .none + } + + case .didFetchMessages(let messages): + state.messages = messages + return .none + + case .sendTapped: + let text = state.text + let chatId = state.id + state.text = "" + return Effect.run { subscriber in + switch chatId { + case .contact(let recipientId): + sendMessage( + text: text, + to: recipientId, + onError: { error in + subscriber.send(.sendFailed(error.localizedDescription)) + }, + completion: { + subscriber.send(completion: .finished) + } + ) + } + return AnyCancellable {} + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .sendFailed(let failure): + state.sendFailure = failure + return .none + + case .imagePicked(let data): + let chatId = state.id + return Effect.run { subscriber in + switch chatId { + case .contact(let recipientId): + sendImage( + data, + to: recipientId, + onError: { error in + subscriber.send(.sendFailed(error.localizedDescription)) + }, + completion: { + subscriber.send(completion: .finished) + } + ) + } + return AnyCancellable {} + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .dismissSendFailureTapped: + state.sendFailure = nil + return .none + + case .binding(_): + return .none + } + } + } +} diff --git a/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift b/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift deleted file mode 100644 index 3538bc2536f23e493a298f2761fd3614711f77da..0000000000000000000000000000000000000000 --- a/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift +++ /dev/null @@ -1,234 +0,0 @@ -import AppCore -import Combine -import ComposableArchitecture -import Foundation -import XCTestDynamicOverlay -import XXClient -import XXMessengerClient -import XXModels - -public struct ChatState: Equatable, Identifiable { - public enum ID: Equatable, Hashable { - case contact(Data) - } - - public struct Message: Equatable, Identifiable { - public init( - id: Int64, - date: Date, - senderId: Data, - text: String, - status: XXModels.Message.Status, - fileTransfer: XXModels.FileTransfer? = nil - ) { - self.id = id - self.date = date - self.senderId = senderId - self.text = text - self.status = status - self.fileTransfer = fileTransfer - } - - public var id: Int64 - public var date: Date - public var senderId: Data - public var text: String - public var status: XXModels.Message.Status - public var fileTransfer: XXModels.FileTransfer? - } - - public init( - id: ID, - myContactId: Data? = nil, - messages: IdentifiedArrayOf<Message> = [], - failure: String? = nil, - sendFailure: String? = nil, - text: String = "" - ) { - self.id = id - self.myContactId = myContactId - self.messages = messages - self.failure = failure - self.sendFailure = sendFailure - self.text = text - } - - public var id: ID - public var myContactId: Data? - public var messages: IdentifiedArrayOf<Message> - public var failure: String? - public var sendFailure: String? - @BindableState public var text: String -} - -public enum ChatAction: Equatable, BindableAction { - case start - case didFetchMessages(IdentifiedArrayOf<ChatState.Message>) - case sendTapped - case sendFailed(String) - case imagePicked(Data) - case dismissSendFailureTapped - case binding(BindingAction<ChatState>) -} - -public struct ChatEnvironment { - public init( - messenger: Messenger, - db: DBManagerGetDB, - sendMessage: SendMessage, - sendImage: SendImage, - mainQueue: AnySchedulerOf<DispatchQueue>, - bgQueue: AnySchedulerOf<DispatchQueue> - ) { - self.messenger = messenger - self.db = db - self.sendMessage = sendMessage - self.sendImage = sendImage - self.mainQueue = mainQueue - self.bgQueue = bgQueue - } - - public var messenger: Messenger - public var db: DBManagerGetDB - public var sendMessage: SendMessage - public var sendImage: SendImage - public var mainQueue: AnySchedulerOf<DispatchQueue> - public var bgQueue: AnySchedulerOf<DispatchQueue> -} - -#if DEBUG -extension ChatEnvironment { - public static let unimplemented = ChatEnvironment( - messenger: .unimplemented, - db: .unimplemented, - sendMessage: .unimplemented, - sendImage: .unimplemented, - mainQueue: .unimplemented, - bgQueue: .unimplemented - ) -} -#endif - -public let chatReducer = Reducer<ChatState, ChatAction, ChatEnvironment> -{ state, action, env in - enum FetchEffectId {} - - switch action { - case .start: - state.failure = nil - do { - let myContactId = try env.messenger.e2e.tryGet().getContact().getId() - state.myContactId = myContactId - let queryChat: XXModels.Message.Query.Chat - let receivedFileTransfersQuery: XXModels.FileTransfer.Query - let sentFileTransfersQuery: XXModels.FileTransfer.Query - switch state.id { - case .contact(let contactId): - queryChat = .direct(myContactId, contactId) - receivedFileTransfersQuery = .init( - contactId: contactId, - isIncoming: true - ) - sentFileTransfersQuery = .init( - contactId: myContactId, - isIncoming: false - ) - } - let messagesQuery = XXModels.Message.Query(chat: queryChat) - return Publishers.CombineLatest3( - try env.db().fetchMessagesPublisher(messagesQuery), - try env.db().fetchFileTransfersPublisher(receivedFileTransfersQuery), - try env.db().fetchFileTransfersPublisher(sentFileTransfersQuery) - ) - .map { messages, receivedFileTransfers, sentFileTransfers in - (messages, receivedFileTransfers + sentFileTransfers) - } - .assertNoFailure() - .map { messages, fileTransfers in - messages.compactMap { message in - guard let id = message.id else { return nil } - return ChatState.Message( - id: id, - date: message.date, - senderId: message.senderId, - text: message.text, - status: message.status, - fileTransfer: fileTransfers.first { $0.id == message.fileTransferId } - ) - } - } - .removeDuplicates() - .map { IdentifiedArrayOf<ChatState.Message>(uniqueElements: $0) } - .map(ChatAction.didFetchMessages) - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - .cancellable(id: FetchEffectId.self, cancelInFlight: true) - } catch { - state.failure = error.localizedDescription - return .none - } - - case .didFetchMessages(let messages): - state.messages = messages - return .none - - case .sendTapped: - let text = state.text - let chatId = state.id - state.text = "" - return Effect.run { subscriber in - switch chatId { - case .contact(let recipientId): - env.sendMessage( - text: text, - to: recipientId, - onError: { error in - subscriber.send(.sendFailed(error.localizedDescription)) - }, - completion: { - subscriber.send(completion: .finished) - } - ) - } - return AnyCancellable {} - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .sendFailed(let failure): - state.sendFailure = failure - return .none - - case .imagePicked(let data): - let chatId = state.id - return Effect.run { subscriber in - switch chatId { - case .contact(let recipientId): - env.sendImage( - data, - to: recipientId, - onError: { error in - subscriber.send(.sendFailed(error.localizedDescription)) - }, - completion: { - subscriber.send(completion: .finished) - } - ) - } - return AnyCancellable {} - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .dismissSendFailureTapped: - state.sendFailure = nil - return .none - - case .binding(_): - return .none - } -} -.binding() diff --git a/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift b/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift index 6058b61bc86fe9c54f25483e7de2a05c5a35dc9d..7ac107f479b1bf8dee80c566043997caa7644cb0 100644 --- a/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift +++ b/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift @@ -3,21 +3,21 @@ import ComposableArchitecture import SwiftUI public struct ChatView: View { - public init(store: Store<ChatState, ChatAction>) { + public init(store: StoreOf<ChatComponent>) { self.store = store } - let store: Store<ChatState, ChatAction> + let store: StoreOf<ChatComponent> @State var isPresentingImagePicker = false struct ViewState: Equatable { var myContactId: Data? - var messages: IdentifiedArrayOf<ChatState.Message> + var messages: IdentifiedArrayOf<ChatComponent.State.Message> var failure: String? var sendFailure: String? var text: String - init(state: ChatState) { + init(state: ChatComponent.State) { myContactId = state.myContactId messages = state.messages failure = state.failure @@ -84,7 +84,7 @@ public struct ChatView: View { HStack { TextField("Text", text: viewStore.binding( get: \.text, - send: { ChatAction.set(\.$text, $0) } + send: { ChatComponent.Action.set(\.$text, $0) } )) .textFieldStyle(.roundedBorder) @@ -122,7 +122,7 @@ public struct ChatView: View { } struct MessageView: View { - var message: ChatState.Message + var message: ChatComponent.State.Message var myContactId: Data? var alignment: Alignment { @@ -199,7 +199,7 @@ public struct ChatView_Previews: PreviewProvider { public static var previews: some View { NavigationView { ChatView(store: Store( - initialState: ChatState( + initialState: ChatComponent.State( id: .contact("contact-id".data(using: .utf8)!), myContactId: "my-contact-id".data(using: .utf8)!, messages: [ @@ -262,8 +262,7 @@ public struct ChatView_Previews: PreviewProvider { failure: "Something went wrong when fetching messages from database.", sendFailure: "Something went wrong when sending message." ), - reducer: .empty, - environment: () + reducer: EmptyReducer() )) } } diff --git a/Examples/xx-messenger/Sources/CheckContactAuthFeature/CheckContactAuthComponent.swift b/Examples/xx-messenger/Sources/CheckContactAuthFeature/CheckContactAuthComponent.swift new file mode 100644 index 0000000000000000000000000000000000000000..5c62fa8f2a7c9ba928d19fa5f29adac127d791d4 --- /dev/null +++ b/Examples/xx-messenger/Sources/CheckContactAuthFeature/CheckContactAuthComponent.swift @@ -0,0 +1,72 @@ +import AppCore +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct CheckContactAuthComponent: ReducerProtocol { + public struct State: Equatable { + public enum Result: Equatable { + case success(Bool) + case failure(String) + } + + public init( + contact: XXClient.Contact, + isChecking: Bool = false, + result: Result? = nil + ) { + self.contact = contact + self.isChecking = isChecking + self.result = result + } + + public var contact: XXClient.Contact + public var isChecking: Bool + public var result: Result? + } + + public enum Action: Equatable { + case checkTapped + case didCheck(State.Result) + } + + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB + @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + + public init() {} + + public func reduce(into state: inout State, action: Action) -> EffectTask<Action> { + switch action { + case .checkTapped: + state.isChecking = true + state.result = nil + return Effect.result { [state] in + do { + let e2e = try messenger.e2e.tryGet() + let contactId = try state.contact.getId() + let result = try e2e.hasAuthenticatedChannel(partnerId: contactId) + try db().bulkUpdateContacts.callAsFunction( + .init(id: [contactId]), + .init(authStatus: result ? .friend : .stranger) + ) + return .success(.didCheck(.success(result))) + } catch { + return .success(.didCheck(.failure(error.localizedDescription))) + } + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .didCheck(let result): + state.isChecking = false + state.result = result + return .none + } + } +} diff --git a/Examples/xx-messenger/Sources/CheckContactAuthFeature/CheckContactAuthFeature.swift b/Examples/xx-messenger/Sources/CheckContactAuthFeature/CheckContactAuthFeature.swift deleted file mode 100644 index 1f768be8d1cba4f20c1f2db65253788358a66023..0000000000000000000000000000000000000000 --- a/Examples/xx-messenger/Sources/CheckContactAuthFeature/CheckContactAuthFeature.swift +++ /dev/null @@ -1,94 +0,0 @@ -import AppCore -import ComposableArchitecture -import Foundation -import XCTestDynamicOverlay -import XXClient -import XXMessengerClient -import XXModels - -public struct CheckContactAuthState: Equatable { - public enum Result: Equatable { - case success(Bool) - case failure(String) - } - - public init( - contact: XXClient.Contact, - isChecking: Bool = false, - result: Result? = nil - ) { - self.contact = contact - self.isChecking = isChecking - self.result = result - } - - public var contact: XXClient.Contact - public var isChecking: Bool - public var result: Result? -} - -public enum CheckContactAuthAction: Equatable { - case checkTapped - case didCheck(CheckContactAuthState.Result) -} - -public struct CheckContactAuthEnvironment { - 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 CheckContactAuthEnvironment { - public static let unimplemented = CheckContactAuthEnvironment( - messenger: .unimplemented, - db: .unimplemented, - mainQueue: .unimplemented, - bgQueue: .unimplemented - ) -} -#endif - -public let checkContactAuthReducer = Reducer<CheckContactAuthState, CheckContactAuthAction, CheckContactAuthEnvironment> -{ state, action, env in - switch action { - case .checkTapped: - state.isChecking = true - state.result = nil - return Effect.result { [state] in - do { - let e2e = try env.messenger.e2e.tryGet() - let contactId = try state.contact.getId() - let result = try e2e.hasAuthenticatedChannel(partnerId: contactId) - try env.db().bulkUpdateContacts.callAsFunction( - .init(id: [contactId]), - .init(authStatus: result ? .friend : .stranger) - ) - return .success(.didCheck(.success(result))) - } catch { - return .success(.didCheck(.failure(error.localizedDescription))) - } - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .didCheck(let result): - state.isChecking = false - state.result = result - return .none - } -} diff --git a/Examples/xx-messenger/Sources/CheckContactAuthFeature/CheckContactAuthView.swift b/Examples/xx-messenger/Sources/CheckContactAuthFeature/CheckContactAuthView.swift index dd2e7894e256c67f4a1ddcf421877dcbfbac63cc..b4b75bbc7dde04a75549d805009b302e4dd6c9b0 100644 --- a/Examples/xx-messenger/Sources/CheckContactAuthFeature/CheckContactAuthView.swift +++ b/Examples/xx-messenger/Sources/CheckContactAuthFeature/CheckContactAuthView.swift @@ -3,20 +3,20 @@ import SwiftUI import XXClient public struct CheckContactAuthView: View { - public init(store: Store<CheckContactAuthState, CheckContactAuthAction>) { + public init(store: StoreOf<CheckContactAuthComponent>) { self.store = store } - let store: Store<CheckContactAuthState, CheckContactAuthAction> + let store: StoreOf<CheckContactAuthComponent> struct ViewState: Equatable { var username: String? var email: String? var phone: String? var isChecking: Bool - var result: CheckContactAuthState.Result? + var result: CheckContactAuthComponent.State.Result? - init(state: CheckContactAuthState) { + init(state: CheckContactAuthComponent.State) { username = try? state.contact.getFact(.username)?.value email = try? state.contact.getFact(.email)?.value phone = try? state.contact.getFact(.phone)?.value @@ -90,11 +90,10 @@ public struct CheckContactAuthView: View { public struct CheckContactAuthView_Previews: PreviewProvider { public static var previews: some View { CheckContactAuthView(store: Store( - initialState: CheckContactAuthState( + initialState: CheckContactAuthComponent.State( contact: .unimplemented("contact-data".data(using: .utf8)!) ), - reducer: .empty, - environment: () + reducer: EmptyReducer() )) } } diff --git a/Examples/xx-messenger/Sources/ConfirmRequestFeature/ConfirmRequestComponent.swift b/Examples/xx-messenger/Sources/ConfirmRequestFeature/ConfirmRequestComponent.swift new file mode 100644 index 0000000000000000000000000000000000000000..3a67c2e9d018cded645299e1853f78def0c8c806 --- /dev/null +++ b/Examples/xx-messenger/Sources/ConfirmRequestFeature/ConfirmRequestComponent.swift @@ -0,0 +1,76 @@ +import AppCore +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct ConfirmRequestComponent: ReducerProtocol { + public struct State: 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 Action: Equatable { + case confirmTapped + case didConfirm(State.Result) + } + + public init() {} + + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB + @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + + public func reduce(into state: inout State, action: Action) -> EffectTask<Action> { + switch action { + case .confirmTapped: + state.isConfirming = true + state.result = nil + return Effect.result { [state] in + func updateStatus(_ status: XXModels.Contact.AuthStatus) throws { + try db().bulkUpdateContacts.callAsFunction( + .init(id: [try state.contact.getId()]), + .init(authStatus: status) + ) + } + do { + try updateStatus(.confirming) + let e2e = try messenger.e2e.tryGet() + _ = try e2e.confirmReceivedRequest(partner: state.contact) + try updateStatus(.friend) + return .success(.didConfirm(.success)) + } catch { + try? updateStatus(.confirmationFailed) + return .success(.didConfirm(.failure(error.localizedDescription))) + } + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .didConfirm(let result): + state.isConfirming = false + state.result = result + return .none + } + } +} diff --git a/Examples/xx-messenger/Sources/ConfirmRequestFeature/ConfirmRequestFeature.swift b/Examples/xx-messenger/Sources/ConfirmRequestFeature/ConfirmRequestFeature.swift deleted file mode 100644 index 7cc40da0b7e268ce6ca78bb1b57b07e00784b148..0000000000000000000000000000000000000000 --- a/Examples/xx-messenger/Sources/ConfirmRequestFeature/ConfirmRequestFeature.swift +++ /dev/null @@ -1,98 +0,0 @@ -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 - func updateStatus(_ status: XXModels.Contact.AuthStatus) throws { - try env.db().bulkUpdateContacts.callAsFunction( - .init(id: [try state.contact.getId()]), - .init(authStatus: status) - ) - } - do { - try updateStatus(.confirming) - let e2e = try env.messenger.e2e.tryGet() - _ = try e2e.confirmReceivedRequest(partner: state.contact) - try updateStatus(.friend) - return .success(.didConfirm(.success)) - } catch { - try? updateStatus(.confirmationFailed) - 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 index 90ebc70e4490d51792db636a47d2e02b318f5820..69f9d03ec076105c84956a9db41ed4dfe5153265 100644 --- a/Examples/xx-messenger/Sources/ConfirmRequestFeature/ConfirmRequestView.swift +++ b/Examples/xx-messenger/Sources/ConfirmRequestFeature/ConfirmRequestView.swift @@ -2,20 +2,20 @@ import ComposableArchitecture import SwiftUI public struct ConfirmRequestView: View { - public init(store: Store<ConfirmRequestState, ConfirmRequestAction>) { + public init(store: StoreOf<ConfirmRequestComponent>) { self.store = store } - let store: Store<ConfirmRequestState, ConfirmRequestAction> + let store: StoreOf<ConfirmRequestComponent> struct ViewState: Equatable { var username: String? var email: String? var phone: String? var isConfirming: Bool - var result: ConfirmRequestState.Result? + var result: ConfirmRequestComponent.State.Result? - init(state: ConfirmRequestState) { + init(state: ConfirmRequestComponent.State) { username = try? state.contact.getFact(.username)?.value email = try? state.contact.getFact(.email)?.value phone = try? state.contact.getFact(.phone)?.value @@ -84,11 +84,10 @@ public struct ConfirmRequestView: View { public struct ConfirmRequestView_Previews: PreviewProvider { public static var previews: some View { ConfirmRequestView(store: Store( - initialState: ConfirmRequestState( + initialState: ConfirmRequestComponent.State( contact: .unimplemented("contact-data".data(using: .utf8)!) ), - reducer: .empty, - environment: () + reducer: EmptyReducer() )) } } diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactComponent.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactComponent.swift new file mode 100644 index 0000000000000000000000000000000000000000..160c671feac5b1e7bf16a221259364e7549f50c1 --- /dev/null +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactComponent.swift @@ -0,0 +1,273 @@ +import AppCore +import ChatFeature +import CheckContactAuthFeature +import ComposableArchitecture +import ComposablePresentation +import ConfirmRequestFeature +import ContactLookupFeature +import Foundation +import ResetAuthFeature +import SendRequestFeature +import VerifyContactFeature +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct ContactComponent: ReducerProtocol { + public struct State: Equatable { + public init( + id: Data, + dbContact: XXModels.Contact? = nil, + xxContact: XXClient.Contact? = nil, + importUsername: Bool = true, + importEmail: Bool = true, + importPhone: Bool = true, + lookup: ContactLookupComponent.State? = nil, + sendRequest: SendRequestComponent.State? = nil, + verifyContact: VerifyContactComponent.State? = nil, + confirmRequest: ConfirmRequestComponent.State? = nil, + checkAuth: CheckContactAuthComponent.State? = nil, + resetAuth: ResetAuthComponent.State? = nil, + chat: ChatComponent.State? = nil + ) { + self.id = id + self.dbContact = dbContact + self.xxContact = xxContact + self.importUsername = importUsername + self.importEmail = importEmail + self.importPhone = importPhone + self.lookup = lookup + self.sendRequest = sendRequest + self.verifyContact = verifyContact + self.confirmRequest = confirmRequest + self.checkAuth = checkAuth + self.resetAuth = resetAuth + self.chat = chat + } + + 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 lookup: ContactLookupComponent.State? + public var sendRequest: SendRequestComponent.State? + public var verifyContact: VerifyContactComponent.State? + public var confirmRequest: ConfirmRequestComponent.State? + public var checkAuth: CheckContactAuthComponent.State? + public var resetAuth: ResetAuthComponent.State? + public var chat: ChatComponent.State? + } + + public enum Action: Equatable, BindableAction { + case start + case dbContactFetched(XXModels.Contact?) + case importFactsTapped + case lookupTapped + case lookupDismissed + case lookup(ContactLookupComponent.Action) + case sendRequestTapped + case sendRequestDismissed + case sendRequest(SendRequestComponent.Action) + case verifyContactTapped + case verifyContactDismissed + case verifyContact(VerifyContactComponent.Action) + case checkAuthTapped + case checkAuthDismissed + case checkAuth(CheckContactAuthComponent.Action) + case confirmRequestTapped + case confirmRequestDismissed + case confirmRequest(ConfirmRequestComponent.Action) + case resetAuthTapped + case resetAuthDismissed + case resetAuth(ResetAuthComponent.Action) + case chatTapped + case chatDismissed + case chat(ChatComponent.Action) + case binding(BindingAction<State>) + } + + public init() {} + + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB + @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + + public var body: some ReducerProtocol<State, Action> { + BindingReducer() + Reduce { state, action in + enum DBFetchEffectID {} + + switch action { + case .start: + return try! db().fetchContactsPublisher(.init(id: [state.id])) + .assertNoFailure() + .map(\.first) + .map(Action.dbContactFetched) + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + .cancellable(id: DBFetchEffectID.self, cancelInFlight: true) + + case .dbContactFetched(let contact): + state.dbContact = contact + return .none + + 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 + if state.importUsername { + dbContact.username = try? xxContact.getFact(.username)?.value + } + if state.importEmail { + dbContact.email = try? xxContact.getFact(.email)?.value + } + if state.importPhone { + dbContact.phone = try? xxContact.getFact(.phone)?.value + } + _ = try! db().saveContact(dbContact) + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .lookupTapped: + state.lookup = ContactLookupComponent.State(id: state.id) + return .none + + case .lookupDismissed: + state.lookup = nil + return .none + + case .lookup(.didLookup(let xxContact)): + state.xxContact = xxContact + state.lookup = nil + return .none + + case .sendRequestTapped: + if let xxContact = state.xxContact { + state.sendRequest = SendRequestComponent.State(contact: xxContact) + } else if let marshaled = state.dbContact?.marshaled { + state.sendRequest = SendRequestComponent.State(contact: .live(marshaled)) + } + return .none + + case .sendRequestDismissed: + state.sendRequest = nil + return .none + + case .sendRequest(.sendSucceeded): + state.sendRequest = nil + return .none + + case .verifyContactTapped: + if let marshaled = state.dbContact?.marshaled { + state.verifyContact = VerifyContactComponent.State( + contact: .live(marshaled) + ) + } + return .none + + case .verifyContactDismissed: + state.verifyContact = nil + return .none + + case .checkAuthTapped: + if let marshaled = state.dbContact?.marshaled { + state.checkAuth = CheckContactAuthComponent.State( + contact: .live(marshaled) + ) + } + return .none + + case .checkAuthDismissed: + state.checkAuth = nil + return .none + + case .confirmRequestTapped: + if let marshaled = state.dbContact?.marshaled { + state.confirmRequest = ConfirmRequestComponent.State( + contact: .live(marshaled) + ) + } + return .none + + case .confirmRequestDismissed: + state.confirmRequest = nil + return .none + + case .chatTapped: + state.chat = ChatComponent.State(id: .contact(state.id)) + return .none + + case .chatDismissed: + state.chat = nil + return .none + + case .resetAuthTapped: + if let marshaled = state.dbContact?.marshaled { + state.resetAuth = ResetAuthComponent.State( + partner: .live(marshaled) + ) + } + return .none + + case .resetAuthDismissed: + state.resetAuth = nil + return .none + + case .binding(_), .lookup(_), .sendRequest(_), + .verifyContact(_), .confirmRequest(_), + .checkAuth(_), .resetAuth(_), .chat(_): + return .none + } + } + .presenting( + state: .keyPath(\.lookup), + id: .notNil(), + action: /ContactComponent.Action.lookup, + presented: { ContactLookupComponent() } + ) + .presenting( + state: .keyPath(\.sendRequest), + id: .notNil(), + action: /ContactComponent.Action.sendRequest, + presented: { SendRequestComponent() } + ) + .presenting( + state: .keyPath(\.verifyContact), + id: .notNil(), + action: /ContactComponent.Action.verifyContact, + presented: { VerifyContactComponent() } + ) + .presenting( + state: .keyPath(\.confirmRequest), + id: .notNil(), + action: /ContactComponent.Action.confirmRequest, + presented: { ConfirmRequestComponent() } + ) + .presenting( + state: .keyPath(\.checkAuth), + id: .notNil(), + action: /ContactComponent.Action.checkAuth, + presented: { CheckContactAuthComponent() } + ) + .presenting( + state: .keyPath(\.resetAuth), + id: .notNil(), + action: /ContactComponent.Action.resetAuth, + presented: { ResetAuthComponent() } + ) + .presenting( + state: .keyPath(\.chat), + id: .keyPath(\.?.id), + action: /ContactComponent.Action.chat, + presented: { ChatComponent() } + ) + } +} diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift deleted file mode 100644 index be66fc8dfc8f59f7f2e20e1a1899a23da10106f6..0000000000000000000000000000000000000000 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift +++ /dev/null @@ -1,328 +0,0 @@ -import AppCore -import ChatFeature -import CheckContactAuthFeature -import ComposableArchitecture -import ComposablePresentation -import ConfirmRequestFeature -import ContactLookupFeature -import Foundation -import ResetAuthFeature -import SendRequestFeature -import VerifyContactFeature -import XCTestDynamicOverlay -import XXClient -import XXMessengerClient -import XXModels - -public struct ContactState: Equatable { - public init( - id: Data, - dbContact: XXModels.Contact? = nil, - xxContact: XXClient.Contact? = nil, - importUsername: Bool = true, - importEmail: Bool = true, - importPhone: Bool = true, - lookup: ContactLookupState? = nil, - sendRequest: SendRequestState? = nil, - verifyContact: VerifyContactState? = nil, - confirmRequest: ConfirmRequestState? = nil, - checkAuth: CheckContactAuthState? = nil, - resetAuth: ResetAuthState? = nil, - chat: ChatState? = nil - ) { - self.id = id - self.dbContact = dbContact - self.xxContact = xxContact - self.importUsername = importUsername - self.importEmail = importEmail - self.importPhone = importPhone - self.lookup = lookup - self.sendRequest = sendRequest - self.verifyContact = verifyContact - self.confirmRequest = confirmRequest - self.checkAuth = checkAuth - self.resetAuth = resetAuth - self.chat = chat - } - - 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 lookup: ContactLookupState? - public var sendRequest: SendRequestState? - public var verifyContact: VerifyContactState? - public var confirmRequest: ConfirmRequestState? - public var checkAuth: CheckContactAuthState? - public var resetAuth: ResetAuthState? - public var chat: ChatState? -} - -public enum ContactAction: Equatable, BindableAction { - case start - case dbContactFetched(XXModels.Contact?) - case importFactsTapped - case lookupTapped - case lookupDismissed - case lookup(ContactLookupAction) - case sendRequestTapped - case sendRequestDismissed - case sendRequest(SendRequestAction) - case verifyContactTapped - case verifyContactDismissed - case verifyContact(VerifyContactAction) - case checkAuthTapped - case checkAuthDismissed - case checkAuth(CheckContactAuthAction) - case confirmRequestTapped - case confirmRequestDismissed - case confirmRequest(ConfirmRequestAction) - case resetAuthTapped - case resetAuthDismissed - case resetAuth(ResetAuthAction) - case chatTapped - case chatDismissed - case chat(ChatAction) - case binding(BindingAction<ContactState>) -} - -public struct ContactEnvironment { - public init( - messenger: Messenger, - db: DBManagerGetDB, - mainQueue: AnySchedulerOf<DispatchQueue>, - bgQueue: AnySchedulerOf<DispatchQueue>, - lookup: @escaping () -> ContactLookupEnvironment, - sendRequest: @escaping () -> SendRequestEnvironment, - verifyContact: @escaping () -> VerifyContactEnvironment, - confirmRequest: @escaping () -> ConfirmRequestEnvironment, - checkAuth: @escaping () -> CheckContactAuthEnvironment, - resetAuth: @escaping () -> ResetAuthEnvironment, - chat: @escaping () -> ChatEnvironment - ) { - self.messenger = messenger - self.db = db - self.mainQueue = mainQueue - self.bgQueue = bgQueue - self.lookup = lookup - self.sendRequest = sendRequest - self.verifyContact = verifyContact - self.confirmRequest = confirmRequest - self.checkAuth = checkAuth - self.resetAuth = resetAuth - self.chat = chat - } - - public var messenger: Messenger - public var db: DBManagerGetDB - public var mainQueue: AnySchedulerOf<DispatchQueue> - public var bgQueue: AnySchedulerOf<DispatchQueue> - public var lookup: () -> ContactLookupEnvironment - public var sendRequest: () -> SendRequestEnvironment - public var verifyContact: () -> VerifyContactEnvironment - public var confirmRequest: () -> ConfirmRequestEnvironment - public var checkAuth: () -> CheckContactAuthEnvironment - public var resetAuth: () -> ResetAuthEnvironment - public var chat: () -> ChatEnvironment -} - -#if DEBUG -extension ContactEnvironment { - public static let unimplemented = ContactEnvironment( - messenger: .unimplemented, - db: .unimplemented, - mainQueue: .unimplemented, - bgQueue: .unimplemented, - lookup: { .unimplemented }, - sendRequest: { .unimplemented }, - verifyContact: { .unimplemented }, - confirmRequest: { .unimplemented }, - checkAuth: { .unimplemented }, - resetAuth: { .unimplemented }, - chat: { .unimplemented } - ) -} -#endif - -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 - - 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 - if state.importUsername { - dbContact.username = try? xxContact.getFact(.username)?.value - } - if state.importEmail { - dbContact.email = try? xxContact.getFact(.email)?.value - } - if state.importPhone { - dbContact.phone = try? xxContact.getFact(.phone)?.value - } - _ = try! env.db().saveContact(dbContact) - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .lookupTapped: - state.lookup = ContactLookupState(id: state.id) - return .none - - case .lookupDismissed: - state.lookup = nil - return .none - - case .lookup(.didLookup(let xxContact)): - state.xxContact = xxContact - state.lookup = nil - return .none - - case .sendRequestTapped: - 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: - state.sendRequest = nil - return .none - - case .sendRequest(.sendSucceeded): - state.sendRequest = nil - return .none - - case .verifyContactTapped: - if let marshaled = state.dbContact?.marshaled { - state.verifyContact = VerifyContactState( - contact: .live(marshaled) - ) - } - return .none - - case .verifyContactDismissed: - state.verifyContact = nil - return .none - - case .checkAuthTapped: - if let marshaled = state.dbContact?.marshaled { - state.checkAuth = CheckContactAuthState( - contact: .live(marshaled) - ) - } - return .none - - case .checkAuthDismissed: - state.checkAuth = nil - return .none - - case .confirmRequestTapped: - if let marshaled = state.dbContact?.marshaled { - state.confirmRequest = ConfirmRequestState( - contact: .live(marshaled) - ) - } - return .none - - case .confirmRequestDismissed: - state.confirmRequest = nil - return .none - - case .chatTapped: - state.chat = ChatState(id: .contact(state.id)) - return .none - - case .chatDismissed: - state.chat = nil - return .none - - case .resetAuthTapped: - if let marshaled = state.dbContact?.marshaled { - state.resetAuth = ResetAuthState( - partner: .live(marshaled) - ) - } - return .none - - case .resetAuthDismissed: - state.resetAuth = nil - return .none - - case .binding(_), .lookup(_), .sendRequest(_), - .verifyContact(_), .confirmRequest(_), - .checkAuth(_), .resetAuth(_), .chat(_): - return .none - } -} -.binding() -.presenting( - contactLookupReducer, - state: .keyPath(\.lookup), - id: .notNil(), - action: /ContactAction.lookup, - environment: { $0.lookup() } -) -.presenting( - sendRequestReducer, - state: .keyPath(\.sendRequest), - id: .notNil(), - action: /ContactAction.sendRequest, - environment: { $0.sendRequest() } -) -.presenting( - verifyContactReducer, - state: .keyPath(\.verifyContact), - id: .notNil(), - action: /ContactAction.verifyContact, - environment: { $0.verifyContact() } -) -.presenting( - confirmRequestReducer, - state: .keyPath(\.confirmRequest), - id: .notNil(), - action: /ContactAction.confirmRequest, - environment: { $0.confirmRequest() } -) -.presenting( - checkContactAuthReducer, - state: .keyPath(\.checkAuth), - id: .notNil(), - action: /ContactAction.checkAuth, - environment: { $0.checkAuth() } -) -.presenting( - resetAuthReducer, - state: .keyPath(\.resetAuth), - id: .notNil(), - action: /ContactAction.resetAuth, - environment: { $0.resetAuth() } -) -.presenting( - chatReducer, - state: .keyPath(\.chat), - id: .keyPath(\.?.id), - action: /ContactAction.chat, - environment: { $0.chat() } -) diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift index 7da763e03748536aabb0cc99698c2edd0ee0fba4..1cbf0e91bdf20a090838fd770861a9120825afba 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift @@ -13,11 +13,11 @@ import XXClient import XXModels public struct ContactView: View { - public init(store: Store<ContactState, ContactAction>) { + public init(store: StoreOf<ContactComponent>) { self.store = store } - let store: Store<ContactState, ContactAction> + let store: StoreOf<ContactComponent> struct ViewState: Equatable { var dbContact: XXModels.Contact? @@ -35,7 +35,7 @@ public struct ContactView: View { var canCheckAuthorization: Bool var canResetAuthorization: Bool - init(state: ContactState) { + init(state: ContactComponent.State) { dbContact = state.dbContact xxContactIsSet = state.xxContact != nil xxContactUsername = try? state.xxContact?.getFact(.username)?.value @@ -217,7 +217,7 @@ public struct ContactView: View { .background(NavigationLinkWithStore( store.scope( state: \.lookup, - action: ContactAction.lookup + action: ContactComponent.Action.lookup ), mapState: replayNonNil(), onDeactivate: { viewStore.send(.lookupDismissed) }, @@ -226,7 +226,7 @@ public struct ContactView: View { .background(NavigationLinkWithStore( store.scope( state: \.sendRequest, - action: ContactAction.sendRequest + action: ContactComponent.Action.sendRequest ), mapState: replayNonNil(), onDeactivate: { viewStore.send(.sendRequestDismissed) }, @@ -235,7 +235,7 @@ public struct ContactView: View { .background(NavigationLinkWithStore( store.scope( state: \.verifyContact, - action: ContactAction.verifyContact + action: ContactComponent.Action.verifyContact ), onDeactivate: { viewStore.send(.verifyContactDismissed) }, destination: VerifyContactView.init(store:) @@ -243,7 +243,7 @@ public struct ContactView: View { .background(NavigationLinkWithStore( store.scope( state: \.confirmRequest, - action: ContactAction.confirmRequest + action: ContactComponent.Action.confirmRequest ), onDeactivate: { viewStore.send(.confirmRequestDismissed) }, destination: ConfirmRequestView.init(store:) @@ -251,7 +251,7 @@ public struct ContactView: View { .background(NavigationLinkWithStore( store.scope( state: \.checkAuth, - action: ContactAction.checkAuth + action: ContactComponent.Action.checkAuth ), onDeactivate: { viewStore.send(.checkAuthDismissed) }, destination: CheckContactAuthView.init(store:) @@ -259,7 +259,7 @@ public struct ContactView: View { .background(NavigationLinkWithStore( store.scope( state: \.resetAuth, - action: ContactAction.resetAuth + action: ContactComponent.Action.resetAuth ), onDeactivate: { viewStore.send(.resetAuthDismissed) }, destination: ResetAuthView.init(store:) @@ -267,7 +267,7 @@ public struct ContactView: View { .background(NavigationLinkWithStore( store.scope( state: \.chat, - action: ContactAction.chat + action: ContactComponent.Action.chat ), onDeactivate: { viewStore.send(.chatDismissed) }, destination: ChatView.init(store:) @@ -280,11 +280,10 @@ public struct ContactView: View { public struct ContactView_Previews: PreviewProvider { public static var previews: some View { ContactView(store: Store( - initialState: ContactState( + initialState: ContactComponent.State( id: "contact-id".data(using: .utf8)! ), - reducer: .empty, - environment: () + reducer: EmptyReducer() )) } } diff --git a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupComponent.swift b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupComponent.swift new file mode 100644 index 0000000000000000000000000000000000000000..3369159037a2e12d8ad4846bad9effc8da3e0e11 --- /dev/null +++ b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupComponent.swift @@ -0,0 +1,64 @@ +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient + +public struct ContactLookupComponent: ReducerProtocol { + public struct State: Equatable { + public init( + id: Data, + isLookingUp: Bool = false, + failure: String? = nil + ) { + self.id = id + self.isLookingUp = isLookingUp + self.failure = failure + } + + public var id: Data + public var isLookingUp: Bool + public var failure: String? + } + + public enum Action: Equatable { + case lookupTapped + case didLookup(XXClient.Contact) + case didFail(NSError) + } + + public init() {} + + @Dependency(\.app.messenger) var messenger + @Dependency(\.app.mainQueue) var mainQueue + @Dependency(\.app.bgQueue) var bgQueue + + public func reduce(into state: inout State, action: Action) -> EffectTask<Action> { + switch action { + case .lookupTapped: + state.isLookingUp = true + state.failure = nil + return Effect.result { [state] in + do { + let contact = try messenger.lookupContact(id: state.id) + return .success(.didLookup(contact)) + } catch { + return .success(.didFail(error as NSError)) + } + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .didLookup(_): + state.isLookingUp = false + state.failure = nil + return .none + + case .didFail(let error): + state.isLookingUp = false + state.failure = error.localizedDescription + return .none + } + } +} diff --git a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift deleted file mode 100644 index 0b6f92dd46b351f6d10f78ef4b50961dad3e1fae..0000000000000000000000000000000000000000 --- a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift +++ /dev/null @@ -1,83 +0,0 @@ -import ComposableArchitecture -import Foundation -import XCTestDynamicOverlay -import XXClient -import XXMessengerClient - -public struct ContactLookupState: Equatable { - public init( - id: Data, - isLookingUp: Bool = false, - failure: String? = nil - ) { - self.id = id - self.isLookingUp = isLookingUp - self.failure = failure - } - - public var id: Data - public var isLookingUp: Bool - public var failure: String? -} - -public enum ContactLookupAction: Equatable { - case lookupTapped - case didLookup(XXClient.Contact) - case didFail(NSError) -} - -public struct ContactLookupEnvironment { - public init( - messenger: Messenger, - mainQueue: AnySchedulerOf<DispatchQueue>, - bgQueue: AnySchedulerOf<DispatchQueue> - ) { - self.messenger = messenger - self.mainQueue = mainQueue - self.bgQueue = bgQueue - } - - public var messenger: Messenger - public var mainQueue: AnySchedulerOf<DispatchQueue> - public var bgQueue: AnySchedulerOf<DispatchQueue> -} - -#if DEBUG -extension ContactLookupEnvironment { - public static let unimplemented = ContactLookupEnvironment( - messenger: .unimplemented, - mainQueue: .unimplemented, - bgQueue: .unimplemented - ) -} -#endif - -public let contactLookupReducer = Reducer<ContactLookupState, ContactLookupAction, ContactLookupEnvironment> -{ state, action, env in - switch action { - case .lookupTapped: - state.isLookingUp = true - state.failure = nil - return Effect.result { [state] in - do { - let contact = try env.messenger.lookupContact(id: state.id) - return .success(.didLookup(contact)) - } catch { - return .success(.didFail(error as NSError)) - } - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .didLookup(_): - state.isLookingUp = false - state.failure = nil - return .none - - case .didFail(let error): - state.isLookingUp = false - state.failure = error.localizedDescription - return .none - } -} diff --git a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift index 6ce83eda6ba5dacfcf15c2fca4e8c202087ef42d..c4850fab42272c7ceba3cf626e625653ec04b4d3 100644 --- a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift +++ b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift @@ -3,14 +3,14 @@ import ComposableArchitecture import SwiftUI public struct ContactLookupView: View { - public init(store: Store<ContactLookupState, ContactLookupAction>) { + public init(store: StoreOf<ContactLookupComponent>) { self.store = store } - let store: Store<ContactLookupState, ContactLookupAction> + let store: StoreOf<ContactLookupComponent> struct ViewState: Equatable { - init(state: ContactLookupState) { + init(state: ContactLookupComponent.State) { id = state.id isLookingUp = state.isLookingUp failure = state.failure @@ -64,11 +64,10 @@ public struct ContactLookupView_Previews: PreviewProvider { public static var previews: some View { NavigationView { ContactLookupView(store: Store( - initialState: ContactLookupState( + initialState: ContactLookupComponent.State( id: "1234".data(using: .utf8)! ), - reducer: .empty, - environment: () + reducer: EmptyReducer() )) } } diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsComponent.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsComponent.swift new file mode 100644 index 0000000000000000000000000000000000000000..a0ce4ed860b1dd45596fd70039aa1344b8deed76 --- /dev/null +++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsComponent.swift @@ -0,0 +1,105 @@ +import AppCore +import ComposableArchitecture +import ComposablePresentation +import ContactFeature +import Foundation +import MyContactFeature +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct ContactsComponent: ReducerProtocol { + public struct State: Equatable { + public init( + myId: Data? = nil, + contacts: IdentifiedArrayOf<XXModels.Contact> = [], + contact: ContactComponent.State? = nil, + myContact: MyContactComponent.State? = nil + ) { + self.myId = myId + self.contacts = contacts + self.contact = contact + self.myContact = myContact + } + + public var myId: Data? + public var contacts: IdentifiedArrayOf<XXModels.Contact> + public var contact: ContactComponent.State? + public var myContact: MyContactComponent.State? + } + + public enum Action: Equatable { + case start + case didFetchContacts([XXModels.Contact]) + case contactSelected(XXModels.Contact) + case contactDismissed + case contact(ContactComponent.Action) + case myContactSelected + case myContactDismissed + case myContact(MyContactComponent.Action) + } + + public init() {} + + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB + @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + + public var body: some ReducerProtocol<State, Action> { + Reduce { state, action in + switch action { + case .start: + state.myId = try? messenger.e2e.tryGet().getContact().getId() + return Effect + .catching { try db() } + .flatMap { $0.fetchContactsPublisher(.init()) } + .assertNoFailure() + .map(Action.didFetchContacts) + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .didFetchContacts(var contacts): + if let myId = state.myId, + let myIndex = contacts.firstIndex(where: { $0.id == myId }) { + contacts.move(fromOffsets: [myIndex], toOffset: contacts.startIndex) + } + state.contacts = IdentifiedArray(uniqueElements: contacts) + return .none + + case .contactSelected(let contact): + state.contact = ContactComponent.State(id: contact.id, dbContact: contact) + return .none + + case .contactDismissed: + state.contact = nil + return .none + + case .myContactSelected: + state.myContact = MyContactComponent.State() + return .none + + case .myContactDismissed: + state.myContact = nil + return .none + + case .contact(_), .myContact(_): + return .none + } + } + .presenting( + state: .keyPath(\.contact), + id: .keyPath(\.?.id), + action: /Action.contact, + presented: { ContactComponent() } + ) + .presenting( + state: .keyPath(\.myContact), + id: .notNil(), + action: /Action.myContact, + presented: { MyContactComponent() } + ) + } +} diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift deleted file mode 100644 index 680a231ec8cc9e6e13487a849a65941bda7a7428..0000000000000000000000000000000000000000 --- a/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift +++ /dev/null @@ -1,135 +0,0 @@ -import AppCore -import ComposableArchitecture -import ComposablePresentation -import ContactFeature -import Foundation -import MyContactFeature -import XCTestDynamicOverlay -import XXClient -import XXMessengerClient -import XXModels - -public struct ContactsState: Equatable { - public init( - myId: Data? = nil, - contacts: IdentifiedArrayOf<XXModels.Contact> = [], - contact: ContactState? = nil, - myContact: MyContactState? = nil - ) { - self.myId = myId - self.contacts = contacts - self.contact = contact - self.myContact = myContact - } - - public var myId: Data? - public var contacts: IdentifiedArrayOf<XXModels.Contact> - public var contact: ContactState? - public var myContact: MyContactState? -} - -public enum ContactsAction: Equatable { - case start - case didFetchContacts([XXModels.Contact]) - case contactSelected(XXModels.Contact) - case contactDismissed - case contact(ContactAction) - case myContactSelected - case myContactDismissed - case myContact(MyContactAction) -} - -public struct ContactsEnvironment { - public init( - messenger: Messenger, - db: DBManagerGetDB, - mainQueue: AnySchedulerOf<DispatchQueue>, - bgQueue: AnySchedulerOf<DispatchQueue>, - contact: @escaping () -> ContactEnvironment, - myContact: @escaping () -> MyContactEnvironment - ) { - self.messenger = messenger - self.db = db - self.mainQueue = mainQueue - self.bgQueue = bgQueue - self.contact = contact - self.myContact = myContact - } - - public var messenger: Messenger - public var db: DBManagerGetDB - public var mainQueue: AnySchedulerOf<DispatchQueue> - public var bgQueue: AnySchedulerOf<DispatchQueue> - public var contact: () -> ContactEnvironment - public var myContact: () -> MyContactEnvironment -} - -#if DEBUG -extension ContactsEnvironment { - public static let unimplemented = ContactsEnvironment( - messenger: .unimplemented, - db: .unimplemented, - mainQueue: .unimplemented, - bgQueue: .unimplemented, - contact: { .unimplemented }, - myContact: { .unimplemented } - ) -} -#endif - -public let contactsReducer = Reducer<ContactsState, ContactsAction, ContactsEnvironment> -{ state, action, env in - switch action { - case .start: - state.myId = try? env.messenger.e2e.tryGet().getContact().getId() - return Effect - .catching { try env.db() } - .flatMap { $0.fetchContactsPublisher(.init()) } - .assertNoFailure() - .map(ContactsAction.didFetchContacts) - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .didFetchContacts(var contacts): - if let myId = state.myId, - let myIndex = contacts.firstIndex(where: { $0.id == myId }) { - contacts.move(fromOffsets: [myIndex], toOffset: contacts.startIndex) - } - state.contacts = IdentifiedArray(uniqueElements: contacts) - return .none - - case .contactSelected(let contact): - state.contact = ContactState(id: contact.id, dbContact: contact) - return .none - - case .contactDismissed: - state.contact = nil - return .none - - case .myContactSelected: - state.myContact = MyContactState() - return .none - - case .myContactDismissed: - state.myContact = nil - return .none - - case .contact(_), .myContact(_): - return .none - } -} -.presenting( - contactReducer, - state: .keyPath(\.contact), - id: .keyPath(\.?.id), - action: /ContactsAction.contact, - environment: { $0.contact() } -) -.presenting( - myContactReducer, - state: .keyPath(\.myContact), - id: .notNil(), - action: /ContactsAction.myContact, - environment: { $0.myContact() } -) diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift index e09725d92c84feeb7f8cb7da5dbc38cf9d998087..9813bc54bc84cf3191925e3b6db60e7ebefd9629 100644 --- a/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift +++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift @@ -7,17 +7,17 @@ import SwiftUI import XXModels public struct ContactsView: View { - public init(store: Store<ContactsState, ContactsAction>) { + public init(store: StoreOf<ContactsComponent>) { self.store = store } - let store: Store<ContactsState, ContactsAction> + let store: StoreOf<ContactsComponent> struct ViewState: Equatable { var myId: Data? var contacts: IdentifiedArrayOf<XXModels.Contact> - init(state: ContactsState) { + init(state: ContactsComponent.State) { myId = state.myId contacts = state.contacts } @@ -74,7 +74,7 @@ public struct ContactsView: View { .background(NavigationLinkWithStore( store.scope( state: \.contact, - action: ContactsAction.contact + action: ContactsComponent.Action.contact ), onDeactivate: { viewStore.send(.contactDismissed) }, destination: ContactView.init(store:) @@ -82,7 +82,7 @@ public struct ContactsView: View { .background(NavigationLinkWithStore( store.scope( state: \.myContact, - action: ContactsAction.myContact + action: ContactsComponent.Action.myContact ), onDeactivate: { viewStore.send(.myContactDismissed) }, destination: MyContactView.init(store:) @@ -96,7 +96,7 @@ public struct ContactsView_Previews: PreviewProvider { public static var previews: some View { NavigationView { ContactsView(store: Store( - initialState: ContactsState( + initialState: ContactsComponent.State( contacts: [ .init( id: "1".data(using: .utf8)!, @@ -115,8 +115,7 @@ public struct ContactsView_Previews: PreviewProvider { ), ] ), - reducer: .empty, - environment: () + reducer: EmptyReducer() )) } } diff --git a/Examples/xx-messenger/Sources/HomeFeature/Alerts.swift b/Examples/xx-messenger/Sources/HomeFeature/Alerts.swift index 3b9b9a3d00ee09ec4dd23ddc24fec105f94946fe..36b604362e0f07c67b0a1e626a519241f692271c 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/Alerts.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/Alerts.swift @@ -1,8 +1,8 @@ import ComposableArchitecture extension AlertState { - public static func confirmAccountDeletion() -> AlertState<HomeAction> { - AlertState<HomeAction>( + public static func confirmAccountDeletion() -> AlertState<HomeComponent.Action> { + AlertState<HomeComponent.Action>( title: TextState("Delete Account"), message: TextState("This will permanently delete your account and can't be undone."), buttons: [ @@ -12,8 +12,8 @@ extension AlertState { ) } - public static func accountDeletionFailed(_ error: Error) -> AlertState<HomeAction> { - AlertState<HomeAction>( + public static func accountDeletionFailed(_ error: Error) -> AlertState<HomeComponent.Action> { + AlertState<HomeComponent.Action>( title: TextState("Error"), message: TextState(error.localizedDescription), buttons: [] diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeComponent.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeComponent.swift new file mode 100644 index 0000000000000000000000000000000000000000..18212bf0ca916cffff86f5a4ee6cb09f4d220359 --- /dev/null +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeComponent.swift @@ -0,0 +1,292 @@ +import AppCore +import BackupFeature +import Combine +import ComposableArchitecture +import ComposablePresentation +import ContactsFeature +import Foundation +import RegisterFeature +import UserSearchFeature +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct HomeComponent: ReducerProtocol { + public struct State: Equatable { + public init( + failure: String? = nil, + isNetworkHealthy: Bool? = nil, + networkNodesReport: NodeRegistrationReport? = nil, + isDeletingAccount: Bool = false, + alert: AlertState<Action>? = nil, + register: RegisterComponent.State? = nil, + contacts: ContactsComponent.State? = nil, + userSearch: UserSearchComponent.State? = nil, + backup: BackupComponent.State? = nil + ) { + self.failure = failure + self.isNetworkHealthy = isNetworkHealthy + self.isDeletingAccount = isDeletingAccount + self.alert = alert + self.register = register + self.contacts = contacts + self.userSearch = userSearch + self.backup = backup + } + + public var failure: String? + public var isNetworkHealthy: Bool? + public var networkNodesReport: NodeRegistrationReport? + public var isDeletingAccount: Bool + public var alert: AlertState<Action>? + public var register: RegisterComponent.State? + public var contacts: ContactsComponent.State? + public var userSearch: UserSearchComponent.State? + public var backup: BackupComponent.State? + } + + public enum Action: Equatable { + public enum Messenger: Equatable { + case start + case didStartRegistered + case didStartUnregistered + case failure(NSError) + } + + public enum NetworkMonitor: Equatable { + case start + case stop + case health(Bool) + case nodes(NodeRegistrationReport) + } + + public enum DeleteAccount: Equatable { + case buttonTapped + case confirmed + case success + case failure(NSError) + } + + case messenger(Messenger) + case networkMonitor(NetworkMonitor) + case deleteAccount(DeleteAccount) + case didDismissAlert + case didDismissRegister + case userSearchButtonTapped + case didDismissUserSearch + case contactsButtonTapped + case didDismissContacts + case backupButtonTapped + case didDismissBackup + case register(RegisterComponent.Action) + case contacts(ContactsComponent.Action) + case userSearch(UserSearchComponent.Action) + case backup(BackupComponent.Action) + } + + public init() {} + + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.dbManager) var dbManager: DBManager + @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + + public var body: some ReducerProtocol<State, Action> { + Reduce { state, action in + enum NetworkHealthEffectId {} + enum NetworkNodesEffectId {} + + let messenger = self.messenger + + switch action { + case .messenger(.start): + return .merge( + Effect(value: .networkMonitor(.stop)), + Effect.result { + do { + try messenger.start() + + if messenger.isConnected() == false { + try messenger.connect() + } + + if messenger.isListeningForMessages() == false { + try messenger.listenForMessages() + } + + if messenger.isFileTransferRunning() == false { + try messenger.startFileTransfer() + } + + if messenger.isLoggedIn() == false { + if try messenger.isRegistered() == false { + return .success(.messenger(.didStartUnregistered)) + } + try messenger.logIn() + } + + if !messenger.isBackupRunning() { + try? messenger.resumeBackup() + } + + return .success(.messenger(.didStartRegistered)) + } catch { + return .success(.messenger(.failure(error as NSError))) + } + } + ) + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .messenger(.didStartUnregistered): + state.register = RegisterComponent.State() + return .none + + case .messenger(.didStartRegistered): + return Effect(value: .networkMonitor(.start)) + + case .messenger(.failure(let error)): + state.failure = error.localizedDescription + return .none + + case .networkMonitor(.start): + return .merge( + Effect.run { subscriber in + let callback = HealthCallback { isHealthy in + subscriber.send(.networkMonitor(.health(isHealthy))) + } + let cancellable = messenger.cMix()?.addHealthCallback(callback) + return AnyCancellable { cancellable?.cancel() } + } + .cancellable(id: NetworkHealthEffectId.self, cancelInFlight: true), + Effect.timer( + id: NetworkNodesEffectId.self, + every: .seconds(2), + on: bgQueue + ) + .compactMap { _ in try? messenger.cMix()?.getNodeRegistrationStatus() } + .map { Action.networkMonitor(.nodes($0)) } + .eraseToEffect() + ) + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .networkMonitor(.stop): + state.isNetworkHealthy = nil + state.networkNodesReport = nil + return .merge( + .cancel(id: NetworkHealthEffectId.self), + .cancel(id: NetworkNodesEffectId.self) + ) + + case .networkMonitor(.health(let isHealthy)): + state.isNetworkHealthy = isHealthy + return .none + + case .networkMonitor(.nodes(let report)): + state.networkNodesReport = report + return .none + + case .deleteAccount(.buttonTapped): + state.alert = .confirmAccountDeletion() + return .none + + case .deleteAccount(.confirmed): + state.isDeletingAccount = true + return .result { + do { + let contactId = try messenger.e2e.tryGet().getContact().getId() + let contact = try dbManager.getDB().fetchContacts(.init(id: [contactId])).first + if let username = contact?.username { + let ud = try messenger.ud.tryGet() + try ud.permanentDeleteAccount(username: Fact(type: .username, value: username)) + } + try messenger.destroy() + try dbManager.removeDB() + return .success(.deleteAccount(.success)) + } catch { + return .success(.deleteAccount(.failure(error as NSError))) + } + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .deleteAccount(.success): + state.isDeletingAccount = false + return .none + + case .deleteAccount(.failure(let error)): + state.isDeletingAccount = false + state.alert = .accountDeletionFailed(error) + return .none + + case .didDismissAlert: + state.alert = nil + return .none + + case .didDismissRegister: + state.register = nil + return .none + + case .userSearchButtonTapped: + state.userSearch = UserSearchComponent.State() + return .none + + case .didDismissUserSearch: + state.userSearch = nil + return .none + + case .contactsButtonTapped: + state.contacts = ContactsComponent.State() + return .none + + case .didDismissContacts: + state.contacts = nil + return .none + + case .register(.finished): + state.register = nil + return Effect(value: .messenger(.start)) + + case .backupButtonTapped: + state.backup = BackupComponent.State() + return .none + + case .didDismissBackup: + state.backup = nil + return .none + + case .register(_), .contacts(_), .userSearch(_), .backup(_): + return .none + } + } + .presenting( + state: .keyPath(\.register), + id: .notNil(), + action: /Action.register, + presented: { RegisterComponent() } + ) + .presenting( + state: .keyPath(\.contacts), + id: .notNil(), + action: /Action.contacts, + presented: { ContactsComponent() } + ) + .presenting( + state: .keyPath(\.userSearch), + id: .notNil(), + action: /Action.userSearch, + presented: { UserSearchComponent() } + ) + .presenting( + state: .keyPath(\.backup), + id: .notNil(), + action: /Action.backup, + presented: { BackupComponent() } + ) + } +} diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift deleted file mode 100644 index 6433dd330faac74045cad472798cd8c06a79cb40..0000000000000000000000000000000000000000 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift +++ /dev/null @@ -1,330 +0,0 @@ -import AppCore -import BackupFeature -import Combine -import ComposableArchitecture -import ComposablePresentation -import ContactsFeature -import Foundation -import RegisterFeature -import UserSearchFeature -import XCTestDynamicOverlay -import XXClient -import XXMessengerClient -import XXModels - -public struct HomeState: Equatable { - public init( - failure: String? = nil, - isNetworkHealthy: Bool? = nil, - networkNodesReport: NodeRegistrationReport? = nil, - isDeletingAccount: Bool = false, - alert: AlertState<HomeAction>? = nil, - register: RegisterState? = nil, - contacts: ContactsState? = nil, - userSearch: UserSearchState? = nil, - backup: BackupState? = nil - ) { - self.failure = failure - self.isNetworkHealthy = isNetworkHealthy - self.isDeletingAccount = isDeletingAccount - self.alert = alert - self.register = register - self.contacts = contacts - self.userSearch = userSearch - self.backup = backup - } - - public var failure: String? - public var isNetworkHealthy: Bool? - public var networkNodesReport: NodeRegistrationReport? - public var isDeletingAccount: Bool - public var alert: AlertState<HomeAction>? - public var register: RegisterState? - public var contacts: ContactsState? - public var userSearch: UserSearchState? - public var backup: BackupState? -} - -public enum HomeAction: Equatable { - public enum Messenger: Equatable { - case start - case didStartRegistered - case didStartUnregistered - case failure(NSError) - } - - public enum NetworkMonitor: Equatable { - case start - case stop - case health(Bool) - case nodes(NodeRegistrationReport) - } - - public enum DeleteAccount: Equatable { - case buttonTapped - case confirmed - case success - case failure(NSError) - } - - case messenger(Messenger) - case networkMonitor(NetworkMonitor) - case deleteAccount(DeleteAccount) - case didDismissAlert - case didDismissRegister - case userSearchButtonTapped - case didDismissUserSearch - case contactsButtonTapped - case didDismissContacts - case backupButtonTapped - case didDismissBackup - case register(RegisterAction) - case contacts(ContactsAction) - case userSearch(UserSearchAction) - case backup(BackupAction) -} - -public struct HomeEnvironment { - public init( - messenger: Messenger, - dbManager: DBManager, - mainQueue: AnySchedulerOf<DispatchQueue>, - bgQueue: AnySchedulerOf<DispatchQueue>, - register: @escaping () -> RegisterEnvironment, - contacts: @escaping () -> ContactsEnvironment, - userSearch: @escaping () -> UserSearchEnvironment, - backup: @escaping () -> BackupEnvironment - ) { - self.messenger = messenger - self.dbManager = dbManager - self.mainQueue = mainQueue - self.bgQueue = bgQueue - self.register = register - self.contacts = contacts - self.userSearch = userSearch - self.backup = backup - } - - public var messenger: Messenger - public var dbManager: DBManager - public var mainQueue: AnySchedulerOf<DispatchQueue> - public var bgQueue: AnySchedulerOf<DispatchQueue> - public var register: () -> RegisterEnvironment - public var contacts: () -> ContactsEnvironment - public var userSearch: () -> UserSearchEnvironment - public var backup: () -> BackupEnvironment -} - -#if DEBUG -extension HomeEnvironment { - public static let unimplemented = HomeEnvironment( - messenger: .unimplemented, - dbManager: .unimplemented, - mainQueue: .unimplemented, - bgQueue: .unimplemented, - register: { .unimplemented }, - contacts: { .unimplemented }, - userSearch: { .unimplemented }, - backup: { .unimplemented } - ) -} -#endif - -public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> -{ state, action, env in - enum NetworkHealthEffectId {} - enum NetworkNodesEffectId {} - - switch action { - case .messenger(.start): - return .merge( - Effect(value: .networkMonitor(.stop)), - Effect.result { - do { - try env.messenger.start() - - if env.messenger.isConnected() == false { - try env.messenger.connect() - } - - if env.messenger.isListeningForMessages() == false { - try env.messenger.listenForMessages() - } - - if env.messenger.isFileTransferRunning() == false { - try env.messenger.startFileTransfer() - } - - if env.messenger.isLoggedIn() == false { - if try env.messenger.isRegistered() == false { - return .success(.messenger(.didStartUnregistered)) - } - try env.messenger.logIn() - } - - if !env.messenger.isBackupRunning() { - try? env.messenger.resumeBackup() - } - - return .success(.messenger(.didStartRegistered)) - } catch { - return .success(.messenger(.failure(error as NSError))) - } - } - ) - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .messenger(.didStartUnregistered): - state.register = RegisterState() - return .none - - case .messenger(.didStartRegistered): - return Effect(value: .networkMonitor(.start)) - - case .messenger(.failure(let error)): - state.failure = error.localizedDescription - return .none - - case .networkMonitor(.start): - return .merge( - Effect.run { subscriber in - let callback = HealthCallback { isHealthy in - subscriber.send(.networkMonitor(.health(isHealthy))) - } - let cancellable = env.messenger.cMix()?.addHealthCallback(callback) - return AnyCancellable { cancellable?.cancel() } - } - .cancellable(id: NetworkHealthEffectId.self, cancelInFlight: true), - Effect.timer( - id: NetworkNodesEffectId.self, - every: .seconds(2), - on: env.bgQueue - ) - .compactMap { _ in try? env.messenger.cMix()?.getNodeRegistrationStatus() } - .map { HomeAction.networkMonitor(.nodes($0)) } - .eraseToEffect() - ) - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .networkMonitor(.stop): - state.isNetworkHealthy = nil - state.networkNodesReport = nil - return .merge( - .cancel(id: NetworkHealthEffectId.self), - .cancel(id: NetworkNodesEffectId.self) - ) - - case .networkMonitor(.health(let isHealthy)): - state.isNetworkHealthy = isHealthy - return .none - - case .networkMonitor(.nodes(let report)): - state.networkNodesReport = report - return .none - - case .deleteAccount(.buttonTapped): - state.alert = .confirmAccountDeletion() - return .none - - case .deleteAccount(.confirmed): - state.isDeletingAccount = true - return .result { - do { - let contactId = try env.messenger.e2e.tryGet().getContact().getId() - 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(type: .username, value: username)) - } - try env.messenger.destroy() - try env.dbManager.removeDB() - return .success(.deleteAccount(.success)) - } catch { - return .success(.deleteAccount(.failure(error as NSError))) - } - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .deleteAccount(.success): - state.isDeletingAccount = false - return .none - - case .deleteAccount(.failure(let error)): - state.isDeletingAccount = false - state.alert = .accountDeletionFailed(error) - return .none - - case .didDismissAlert: - state.alert = nil - return .none - - case .didDismissRegister: - state.register = nil - return .none - - case .userSearchButtonTapped: - state.userSearch = UserSearchState() - return .none - - case .didDismissUserSearch: - state.userSearch = nil - return .none - - case .contactsButtonTapped: - state.contacts = ContactsState() - return .none - - case .didDismissContacts: - state.contacts = nil - return .none - - case .register(.finished): - state.register = nil - return Effect(value: .messenger(.start)) - - case .backupButtonTapped: - state.backup = BackupState() - return .none - - case .didDismissBackup: - state.backup = nil - return .none - - case .register(_), .contacts(_), .userSearch(_), .backup(_): - return .none - } -} -.presenting( - registerReducer, - state: .keyPath(\.register), - id: .notNil(), - action: /HomeAction.register, - environment: { $0.register() } -) -.presenting( - contactsReducer, - state: .keyPath(\.contacts), - id: .notNil(), - action: /HomeAction.contacts, - environment: { $0.contacts() } -) -.presenting( - userSearchReducer, - state: .keyPath(\.userSearch), - id: .notNil(), - action: /HomeAction.userSearch, - environment: { $0.userSearch() } -) -.presenting( - backupReducer, - state: .keyPath(\.backup), - id: .notNil(), - action: /HomeAction.backup, - environment: { $0.backup() } -) diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift index 8a1775d6a84ffdfc6b5ac1b2b816a8ee686b039c..d505100474cdd330d28afc81e126bbb0daf853b0 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift @@ -9,11 +9,11 @@ import UserSearchFeature import XXClient public struct HomeView: View { - public init(store: Store<HomeState, HomeAction>) { + public init(store: StoreOf<HomeComponent>) { self.store = store } - let store: Store<HomeState, HomeAction> + let store: StoreOf<HomeComponent> struct ViewState: Equatable { var failure: String? @@ -21,7 +21,7 @@ public struct HomeView: View { var networkNodesReport: NodeRegistrationReport? var isDeletingAccount: Bool - init(state: HomeState) { + init(state: HomeComponent.State) { failure = state.failure isNetworkHealthy = state.isNetworkHealthy isDeletingAccount = state.isDeletingAccount @@ -148,12 +148,12 @@ public struct HomeView: View { .navigationTitle("Home") .alert( store.scope(state: \.alert), - dismiss: HomeAction.didDismissAlert + dismiss: HomeComponent.Action.didDismissAlert ) .background(NavigationLinkWithStore( store.scope( state: \.contacts, - action: HomeAction.contacts + action: HomeComponent.Action.contacts ), onDeactivate: { viewStore.send(.didDismissContacts) @@ -163,7 +163,7 @@ public struct HomeView: View { .background(NavigationLinkWithStore( store.scope( state: \.userSearch, - action: HomeAction.userSearch + action: HomeComponent.Action.userSearch ), onDeactivate: { viewStore.send(.didDismissUserSearch) @@ -173,7 +173,7 @@ public struct HomeView: View { .background(NavigationLinkWithStore( store.scope( state: \.backup, - action: HomeAction.backup + action: HomeComponent.Action.backup ), onDeactivate: { viewStore.send(.didDismissBackup) @@ -186,7 +186,7 @@ public struct HomeView: View { .fullScreenCover( store.scope( state: \.register, - action: HomeAction.register + action: HomeComponent.Action.register ), onDismiss: { viewStore.send(.didDismissRegister) @@ -201,9 +201,8 @@ public struct HomeView: View { public struct HomeView_Previews: PreviewProvider { public static var previews: some View { HomeView(store: Store( - initialState: HomeState(), - reducer: .empty, - environment: () + initialState: HomeComponent.State(), + reducer: EmptyReducer() )) } } diff --git a/Examples/xx-messenger/Sources/MyContactFeature/Alerts.swift b/Examples/xx-messenger/Sources/MyContactFeature/Alerts.swift index 321139aec18ce7ae51b9be8097878a9133798236..f8693f17cc18c8f13c4412b1f30c8aac1a3c3d62 100644 --- a/Examples/xx-messenger/Sources/MyContactFeature/Alerts.swift +++ b/Examples/xx-messenger/Sources/MyContactFeature/Alerts.swift @@ -1,8 +1,8 @@ import ComposableArchitecture extension AlertState { - public static func error(_ message: String) -> AlertState<MyContactAction> { - AlertState<MyContactAction>( + public static func error(_ message: String) -> AlertState<MyContactComponent.Action> { + AlertState<MyContactComponent.Action>( title: TextState("Error"), message: TextState(message), buttons: [] diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactComponent.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactComponent.swift new file mode 100644 index 0000000000000000000000000000000000000000..377e824b807d50e056bfd5b4826585e8fde6f6b9 --- /dev/null +++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactComponent.swift @@ -0,0 +1,297 @@ +import AppCore +import Combine +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct MyContactComponent: ReducerProtocol { + public struct State: Equatable { + public enum Field: String, Hashable { + case email + case emailCode + case phone + case phoneCode + } + + public init( + contact: XXModels.Contact? = nil, + focusedField: Field? = nil, + email: String = "", + emailConfirmationID: String? = nil, + emailConfirmationCode: String = "", + isRegisteringEmail: Bool = false, + isConfirmingEmail: Bool = false, + isUnregisteringEmail: Bool = false, + phone: String = "", + phoneConfirmationID: String? = nil, + phoneConfirmationCode: String = "", + isRegisteringPhone: Bool = false, + isConfirmingPhone: Bool = false, + isUnregisteringPhone: Bool = false, + isLoadingFacts: Bool = false, + alert: AlertState<Action>? = nil + ) { + self.contact = contact + self.focusedField = focusedField + self.email = email + self.emailConfirmationID = emailConfirmationID + self.emailConfirmationCode = emailConfirmationCode + self.isRegisteringEmail = isRegisteringEmail + self.isConfirmingEmail = isConfirmingEmail + self.isUnregisteringEmail = isUnregisteringEmail + self.phone = phone + self.phoneConfirmationID = phoneConfirmationID + self.phoneConfirmationCode = phoneConfirmationCode + self.isRegisteringPhone = isRegisteringPhone + self.isConfirmingPhone = isConfirmingPhone + self.isUnregisteringPhone = isUnregisteringPhone + self.isLoadingFacts = isLoadingFacts + self.alert = alert + } + + public var contact: XXModels.Contact? + @BindableState public var focusedField: Field? + @BindableState public var email: String + @BindableState public var emailConfirmationID: String? + @BindableState public var emailConfirmationCode: String + @BindableState public var isRegisteringEmail: Bool + @BindableState public var isConfirmingEmail: Bool + @BindableState public var isUnregisteringEmail: Bool + @BindableState public var phone: String + @BindableState public var phoneConfirmationID: String? + @BindableState public var phoneConfirmationCode: String + @BindableState public var isRegisteringPhone: Bool + @BindableState public var isConfirmingPhone: Bool + @BindableState public var isUnregisteringPhone: Bool + @BindableState public var isLoadingFacts: Bool + public var alert: AlertState<Action>? + } + + public enum Action: Equatable, BindableAction { + case start + case contactFetched(XXModels.Contact?) + case registerEmailTapped + case confirmEmailTapped + case unregisterEmailTapped + case registerPhoneTapped + case confirmPhoneTapped + case unregisterPhoneTapped + case loadFactsTapped + case didFail(String) + case alertDismissed + case binding(BindingAction<State>) + } + + public init() {} + + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB + @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + + public var body: some ReducerProtocol<State, Action> { + BindingReducer() + Reduce { state, action in + enum DBFetchEffectID {} + let db = self.db + + switch action { + case .start: + return Effect + .catching { try messenger.e2e.tryGet().getContact().getId() } + .tryMap { try db().fetchContactsPublisher(.init(id: [$0])) } + .flatMap { $0 } + .assertNoFailure() + .map(\.first) + .map(Action.contactFetched) + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + .cancellable(id: DBFetchEffectID.self, cancelInFlight: true) + + case .contactFetched(let contact): + state.contact = contact + return .none + + case .registerEmailTapped: + state.focusedField = nil + state.isRegisteringEmail = true + return Effect.run { [state] subscriber in + do { + let ud = try messenger.ud.tryGet() + let fact = Fact(type: .email, value: state.email) + let confirmationID = try ud.sendRegisterFact(fact) + subscriber.send(.set(\.$emailConfirmationID, confirmationID)) + } catch { + subscriber.send(.didFail(error.localizedDescription)) + } + subscriber.send(.set(\.$isRegisteringEmail, false)) + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .confirmEmailTapped: + guard let confirmationID = state.emailConfirmationID else { return .none } + state.focusedField = nil + state.isConfirmingEmail = true + return Effect.run { [state] subscriber in + do { + let ud = try messenger.ud.tryGet() + try ud.confirmFact(confirmationId: confirmationID, code: state.emailConfirmationCode) + let contactId = try messenger.e2e.tryGet().getContact().getId() + if var dbContact = try db().fetchContacts(.init(id: [contactId])).first { + dbContact.email = state.email + try db().saveContact(dbContact) + } + subscriber.send(.set(\.$email, "")) + subscriber.send(.set(\.$emailConfirmationID, nil)) + subscriber.send(.set(\.$emailConfirmationCode, "")) + } catch { + subscriber.send(.didFail(error.localizedDescription)) + } + subscriber.send(.set(\.$isConfirmingEmail, false)) + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .unregisterEmailTapped: + guard let email = state.contact?.email else { return .none } + state.isUnregisteringEmail = true + return Effect.run { subscriber in + do { + let ud: UserDiscovery = try messenger.ud.tryGet() + let fact = Fact(type: .email, value: email) + try ud.removeFact(fact) + let contactId = try messenger.e2e.tryGet().getContact().getId() + if var dbContact = try db().fetchContacts(.init(id: [contactId])).first { + dbContact.email = nil + try db().saveContact(dbContact) + } + } catch { + subscriber.send(.didFail(error.localizedDescription)) + } + subscriber.send(.set(\.$isUnregisteringEmail, false)) + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .registerPhoneTapped: + state.focusedField = nil + state.isRegisteringPhone = true + return Effect.run { [state] subscriber in + do { + let ud = try messenger.ud.tryGet() + let fact = Fact(type: .phone, value: state.phone) + let confirmationID = try ud.sendRegisterFact(fact) + subscriber.send(.set(\.$phoneConfirmationID, confirmationID)) + } catch { + subscriber.send(.didFail(error.localizedDescription)) + } + subscriber.send(.set(\.$isRegisteringPhone, false)) + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .confirmPhoneTapped: + guard let confirmationID = state.phoneConfirmationID else { return .none } + state.focusedField = nil + state.isConfirmingPhone = true + return Effect.run { [state] subscriber in + do { + let ud = try messenger.ud.tryGet() + try ud.confirmFact(confirmationId: confirmationID, code: state.phoneConfirmationCode) + let contactId = try messenger.e2e.tryGet().getContact().getId() + if var dbContact = try db().fetchContacts(.init(id: [contactId])).first { + dbContact.phone = state.phone + try db().saveContact(dbContact) + } + subscriber.send(.set(\.$phone, "")) + subscriber.send(.set(\.$phoneConfirmationID, nil)) + subscriber.send(.set(\.$phoneConfirmationCode, "")) + } catch { + subscriber.send(.didFail(error.localizedDescription)) + } + subscriber.send(.set(\.$isConfirmingPhone, false)) + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .unregisterPhoneTapped: + guard let phone = state.contact?.phone else { return .none } + state.isUnregisteringPhone = true + return Effect.run { subscriber in + do { + let ud: UserDiscovery = try messenger.ud.tryGet() + let fact = Fact(type: .phone, value: phone) + try ud.removeFact(fact) + let contactId = try messenger.e2e.tryGet().getContact().getId() + if var dbContact = try db().fetchContacts(.init(id: [contactId])).first { + dbContact.phone = nil + try db().saveContact(dbContact) + } + } catch { + subscriber.send(.didFail(error.localizedDescription)) + } + subscriber.send(.set(\.$isUnregisteringPhone, false)) + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .loadFactsTapped: + state.isLoadingFacts = true + return Effect.run { subscriber in + do { + let contactId = try messenger.e2e.tryGet().getContact().getId() + if var dbContact = try db().fetchContacts(.init(id: [contactId])).first { + let facts = try messenger.ud.tryGet().getFacts() + dbContact.username = facts.get(.username)?.value + dbContact.email = facts.get(.email)?.value + dbContact.phone = facts.get(.phone)?.value + try db().saveContact(dbContact) + } + } catch { + subscriber.send(.didFail(error.localizedDescription)) + } + subscriber.send(.set(\.$isLoadingFacts, false)) + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .didFail(let failure): + state.alert = .error(failure) + return .none + + case .alertDismissed: + state.alert = nil + return .none + + case .binding(_): + return .none + } + } + } +} diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift deleted file mode 100644 index 434a1aca3c7cdc74b340c54a22558fa67a90a0eb..0000000000000000000000000000000000000000 --- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift +++ /dev/null @@ -1,316 +0,0 @@ -import AppCore -import Combine -import ComposableArchitecture -import Foundation -import XCTestDynamicOverlay -import XXClient -import XXMessengerClient -import XXModels - -public struct MyContactState: Equatable { - public enum Field: String, Hashable { - case email - case emailCode - case phone - case phoneCode - } - - public init( - contact: XXModels.Contact? = nil, - focusedField: Field? = nil, - email: String = "", - emailConfirmationID: String? = nil, - emailConfirmationCode: String = "", - isRegisteringEmail: Bool = false, - isConfirmingEmail: Bool = false, - isUnregisteringEmail: Bool = false, - phone: String = "", - phoneConfirmationID: String? = nil, - phoneConfirmationCode: String = "", - isRegisteringPhone: Bool = false, - isConfirmingPhone: Bool = false, - isUnregisteringPhone: Bool = false, - isLoadingFacts: Bool = false, - alert: AlertState<MyContactAction>? = nil - ) { - self.contact = contact - self.focusedField = focusedField - self.email = email - self.emailConfirmationID = emailConfirmationID - self.emailConfirmationCode = emailConfirmationCode - self.isRegisteringEmail = isRegisteringEmail - self.isConfirmingEmail = isConfirmingEmail - self.isUnregisteringEmail = isUnregisteringEmail - self.phone = phone - self.phoneConfirmationID = phoneConfirmationID - self.phoneConfirmationCode = phoneConfirmationCode - self.isRegisteringPhone = isRegisteringPhone - self.isConfirmingPhone = isConfirmingPhone - self.isUnregisteringPhone = isUnregisteringPhone - self.isLoadingFacts = isLoadingFacts - self.alert = alert - } - - public var contact: XXModels.Contact? - @BindableState public var focusedField: Field? - @BindableState public var email: String - @BindableState public var emailConfirmationID: String? - @BindableState public var emailConfirmationCode: String - @BindableState public var isRegisteringEmail: Bool - @BindableState public var isConfirmingEmail: Bool - @BindableState public var isUnregisteringEmail: Bool - @BindableState public var phone: String - @BindableState public var phoneConfirmationID: String? - @BindableState public var phoneConfirmationCode: String - @BindableState public var isRegisteringPhone: Bool - @BindableState public var isConfirmingPhone: Bool - @BindableState public var isUnregisteringPhone: Bool - @BindableState public var isLoadingFacts: Bool - public var alert: AlertState<MyContactAction>? -} - -public enum MyContactAction: Equatable, BindableAction { - case start - case contactFetched(XXModels.Contact?) - case registerEmailTapped - case confirmEmailTapped - case unregisterEmailTapped - case registerPhoneTapped - case confirmPhoneTapped - case unregisterPhoneTapped - case loadFactsTapped - case didFail(String) - case alertDismissed - case binding(BindingAction<MyContactState>) -} - -public struct MyContactEnvironment { - 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 MyContactEnvironment { - public static let unimplemented = MyContactEnvironment( - messenger: .unimplemented, - db: .unimplemented, - mainQueue: .unimplemented, - bgQueue: .unimplemented - ) -} -#endif - -public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContactEnvironment> -{ 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(MyContactAction.contactFetched) - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - .cancellable(id: DBFetchEffectID.self, cancelInFlight: true) - - case .contactFetched(let contact): - state.contact = contact - return .none - - case .registerEmailTapped: - state.focusedField = nil - state.isRegisteringEmail = true - return Effect.run { [state] subscriber in - do { - let ud = try env.messenger.ud.tryGet() - let fact = Fact(type: .email, value: state.email) - let confirmationID = try ud.sendRegisterFact(fact) - subscriber.send(.set(\.$emailConfirmationID, confirmationID)) - } catch { - subscriber.send(.didFail(error.localizedDescription)) - } - subscriber.send(.set(\.$isRegisteringEmail, false)) - subscriber.send(completion: .finished) - return AnyCancellable {} - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .confirmEmailTapped: - guard let confirmationID = state.emailConfirmationID else { return .none } - state.focusedField = nil - state.isConfirmingEmail = true - return Effect.run { [state] subscriber in - do { - let ud = try env.messenger.ud.tryGet() - try ud.confirmFact(confirmationId: confirmationID, code: state.emailConfirmationCode) - let contactId = try env.messenger.e2e.tryGet().getContact().getId() - if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first { - dbContact.email = state.email - try env.db().saveContact(dbContact) - } - subscriber.send(.set(\.$email, "")) - subscriber.send(.set(\.$emailConfirmationID, nil)) - subscriber.send(.set(\.$emailConfirmationCode, "")) - } catch { - subscriber.send(.didFail(error.localizedDescription)) - } - subscriber.send(.set(\.$isConfirmingEmail, false)) - subscriber.send(completion: .finished) - return AnyCancellable {} - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .unregisterEmailTapped: - guard let email = state.contact?.email else { return .none } - state.isUnregisteringEmail = true - return Effect.run { [state] subscriber in - do { - let ud: UserDiscovery = try env.messenger.ud.tryGet() - let fact = Fact(type: .email, value: email) - try ud.removeFact(fact) - let contactId = try env.messenger.e2e.tryGet().getContact().getId() - if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first { - dbContact.email = nil - try env.db().saveContact(dbContact) - } - } catch { - subscriber.send(.didFail(error.localizedDescription)) - } - subscriber.send(.set(\.$isUnregisteringEmail, false)) - subscriber.send(completion: .finished) - return AnyCancellable {} - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .registerPhoneTapped: - state.focusedField = nil - state.isRegisteringPhone = true - return Effect.run { [state] subscriber in - do { - let ud = try env.messenger.ud.tryGet() - let fact = Fact(type: .phone, value: state.phone) - let confirmationID = try ud.sendRegisterFact(fact) - subscriber.send(.set(\.$phoneConfirmationID, confirmationID)) - } catch { - subscriber.send(.didFail(error.localizedDescription)) - } - subscriber.send(.set(\.$isRegisteringPhone, false)) - subscriber.send(completion: .finished) - return AnyCancellable {} - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .confirmPhoneTapped: - guard let confirmationID = state.phoneConfirmationID else { return .none } - state.focusedField = nil - state.isConfirmingPhone = true - return Effect.run { [state] subscriber in - do { - let ud = try env.messenger.ud.tryGet() - try ud.confirmFact(confirmationId: confirmationID, code: state.phoneConfirmationCode) - let contactId = try env.messenger.e2e.tryGet().getContact().getId() - if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first { - dbContact.phone = state.phone - try env.db().saveContact(dbContact) - } - subscriber.send(.set(\.$phone, "")) - subscriber.send(.set(\.$phoneConfirmationID, nil)) - subscriber.send(.set(\.$phoneConfirmationCode, "")) - } catch { - subscriber.send(.didFail(error.localizedDescription)) - } - subscriber.send(.set(\.$isConfirmingPhone, false)) - subscriber.send(completion: .finished) - return AnyCancellable {} - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .unregisterPhoneTapped: - guard let phone = state.contact?.phone else { return .none } - state.isUnregisteringPhone = true - return Effect.run { [state] subscriber in - do { - let ud: UserDiscovery = try env.messenger.ud.tryGet() - let fact = Fact(type: .phone, value: phone) - try ud.removeFact(fact) - let contactId = try env.messenger.e2e.tryGet().getContact().getId() - if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first { - dbContact.phone = nil - try env.db().saveContact(dbContact) - } - } catch { - subscriber.send(.didFail(error.localizedDescription)) - } - subscriber.send(.set(\.$isUnregisteringPhone, false)) - subscriber.send(completion: .finished) - return AnyCancellable {} - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .loadFactsTapped: - state.isLoadingFacts = true - return Effect.run { subscriber in - do { - let contactId = try env.messenger.e2e.tryGet().getContact().getId() - if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first { - let facts = try env.messenger.ud.tryGet().getFacts() - dbContact.username = facts.get(.username)?.value - dbContact.email = facts.get(.email)?.value - dbContact.phone = facts.get(.phone)?.value - try env.db().saveContact(dbContact) - } - } catch { - subscriber.send(.didFail(error.localizedDescription)) - } - subscriber.send(.set(\.$isLoadingFacts, false)) - subscriber.send(completion: .finished) - return AnyCancellable {} - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .didFail(let failure): - state.alert = .error(failure) - return .none - - case .alertDismissed: - state.alert = nil - return .none - - case .binding(_): - return .none - } -} -.binding() diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift index d32a6f68848e89ad7eeb4a9bf5f25b543d379f40..b5cd127ed53e7037a278e476147a53ae0b42ebc7 100644 --- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift +++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift @@ -4,15 +4,15 @@ import SwiftUI import XXModels public struct MyContactView: View { - public init(store: Store<MyContactState, MyContactAction>) { + public init(store: StoreOf<MyContactComponent>) { self.store = store } - let store: Store<MyContactState, MyContactAction> - @FocusState var focusedField: MyContactState.Field? + let store: StoreOf<MyContactComponent> + @FocusState var focusedField: MyContactComponent.State.Field? struct ViewState: Equatable { - init(state: MyContactState) { + init(state: MyContactComponent.State) { contact = state.contact focusedField = state.focusedField email = state.email @@ -31,7 +31,7 @@ public struct MyContactView: View { } var contact: XXModels.Contact? - var focusedField: MyContactState.Field? + var focusedField: MyContactComponent.State.Field? var email: String var emailConfirmation: Bool var emailCode: String @@ -86,7 +86,7 @@ public struct MyContactView: View { TextField( text: viewStore.binding( get: \.email, - send: { MyContactAction.set(\.$email, $0) } + send: { MyContactComponent.Action.set(\.$email, $0) } ), prompt: Text("Enter email"), label: { Text("Email") } @@ -99,7 +99,7 @@ public struct MyContactView: View { TextField( text: viewStore.binding( get: \.emailCode, - send: { MyContactAction.set(\.$emailConfirmationCode, $0) } + send: { MyContactComponent.Action.set(\.$emailConfirmationCode, $0) } ), prompt: Text("Enter confirmation code"), label: { Text("Confirmation code") } @@ -163,7 +163,7 @@ public struct MyContactView: View { TextField( text: viewStore.binding( get: \.phone, - send: { MyContactAction.set(\.$phone, $0) } + send: { MyContactComponent.Action.set(\.$phone, $0) } ), prompt: Text("Enter phone"), label: { Text("Phone") } @@ -176,7 +176,7 @@ public struct MyContactView: View { TextField( text: viewStore.binding( get: \.phoneCode, - send: { MyContactAction.set(\.$phoneConfirmationCode, $0) } + send: { MyContactComponent.Action.set(\.$phoneConfirmationCode, $0) } ), prompt: Text("Enter confirmation code"), label: { Text("Confirmation code") } @@ -250,9 +250,8 @@ public struct MyContactView_Previews: PreviewProvider { public static var previews: some View { NavigationView { MyContactView(store: Store( - initialState: MyContactState(), - reducer: .empty, - environment: () + initialState: MyContactComponent.State(), + reducer: EmptyReducer() )) } } diff --git a/Examples/xx-messenger/Sources/RegisterFeature/RegisterComponent.swift b/Examples/xx-messenger/Sources/RegisterFeature/RegisterComponent.swift new file mode 100644 index 0000000000000000000000000000000000000000..062080af6b86f9243ecbf2644bc7df63ecf60a17 --- /dev/null +++ b/Examples/xx-messenger/Sources/RegisterFeature/RegisterComponent.swift @@ -0,0 +1,104 @@ +import AppCore +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct RegisterComponent: ReducerProtocol { + public struct State: Equatable { + public enum Error: Swift.Error, Equatable { + case usernameMismatch(registering: String, registered: String?) + } + + public enum Field: String, Hashable { + case username + } + + public init( + focusedField: Field? = nil, + username: String = "", + isRegistering: Bool = false, + failure: String? = nil + ) { + self.focusedField = focusedField + self.username = username + self.isRegistering = isRegistering + self.failure = failure + } + + @BindableState public var focusedField: Field? + @BindableState public var username: String + public var isRegistering: Bool + public var failure: String? + } + + public enum Action: Equatable, BindableAction { + case registerTapped + case failed(String) + case finished + case binding(BindingAction<State>) + } + + public init() {} + + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB + @Dependency(\.app.now) var now: () -> Date + @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + + public var body: some ReducerProtocol<State, Action> { + BindingReducer() + Reduce { state, action in + switch action { + case .binding(_): + return .none + + case .registerTapped: + state.focusedField = nil + state.isRegistering = true + state.failure = nil + return .future { [username = state.username] fulfill in + do { + let db = try db() + try messenger.register(username: username) + let contact = try messenger.myContact() + let facts = try contact.getFacts() + try db.saveContact(Contact( + id: try contact.getId(), + marshaled: contact.data, + username: facts.get(.username)?.value, + email: facts.get(.email)?.value, + phone: facts.get(.phone)?.value, + createdAt: now() + )) + guard facts.get(.username)?.value == username else { + throw State.Error.usernameMismatch( + registering: username, + registered: facts.get(.username)?.value + ) + } + fulfill(.success(.finished)) + } + catch { + fulfill(.success(.failed(error.localizedDescription))) + } + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .failed(let failure): + state.isRegistering = false + state.failure = failure + return .none + + case .finished: + state.isRegistering = false + return .none + } + } + } +} diff --git a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift deleted file mode 100644 index f8fdabefea7d859b3153c6716133d3351bff2fdd..0000000000000000000000000000000000000000 --- a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift +++ /dev/null @@ -1,127 +0,0 @@ -import AppCore -import ComposableArchitecture -import Foundation -import XCTestDynamicOverlay -import XXClient -import XXMessengerClient -import XXModels - -public struct RegisterState: Equatable { - public enum Error: Swift.Error, Equatable { - case usernameMismatch(registering: String, registered: String?) - } - - public enum Field: String, Hashable { - case username - } - - public init( - focusedField: Field? = nil, - username: String = "", - isRegistering: Bool = false, - failure: String? = nil - ) { - self.focusedField = focusedField - self.username = username - self.isRegistering = isRegistering - self.failure = failure - } - - @BindableState public var focusedField: Field? - @BindableState public var username: String - public var isRegistering: Bool - public var failure: String? -} - -public enum RegisterAction: Equatable, BindableAction { - case registerTapped - case failed(String) - case finished - case binding(BindingAction<RegisterState>) -} - -public struct RegisterEnvironment { - public init( - messenger: Messenger, - db: DBManagerGetDB, - now: @escaping () -> Date, - mainQueue: AnySchedulerOf<DispatchQueue>, - bgQueue: AnySchedulerOf<DispatchQueue> - ) { - self.messenger = messenger - self.db = db - self.now = now - self.mainQueue = mainQueue - self.bgQueue = bgQueue - } - - public var messenger: Messenger - public var db: DBManagerGetDB - public var now: () -> Date - public var mainQueue: AnySchedulerOf<DispatchQueue> - public var bgQueue: AnySchedulerOf<DispatchQueue> -} - -#if DEBUG -extension RegisterEnvironment { - public static let unimplemented = RegisterEnvironment( - messenger: .unimplemented, - db: .unimplemented, - now: XCTUnimplemented("\(Self.self).now"), - mainQueue: .unimplemented, - bgQueue: .unimplemented - ) -} -#endif - -public let registerReducer = Reducer<RegisterState, RegisterAction, RegisterEnvironment> -{ state, action, env in - switch action { - case .binding(_): - return .none - - case .registerTapped: - state.focusedField = nil - state.isRegistering = true - state.failure = nil - return .future { [username = state.username] fulfill in - do { - let db = try env.db() - try env.messenger.register(username: username) - let contact = try env.messenger.myContact() - let facts = try contact.getFacts() - try db.saveContact(Contact( - id: try contact.getId(), - marshaled: contact.data, - username: facts.get(.username)?.value, - email: facts.get(.email)?.value, - phone: facts.get(.phone)?.value, - createdAt: env.now() - )) - guard facts.get(.username)?.value == username else { - throw RegisterState.Error.usernameMismatch( - registering: username, - registered: facts.get(.username)?.value - ) - } - fulfill(.success(.finished)) - } - catch { - fulfill(.success(.failed(error.localizedDescription))) - } - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .failed(let failure): - state.isRegistering = false - state.failure = failure - return .none - - case .finished: - state.isRegistering = false - return .none - } -} -.binding() diff --git a/Examples/xx-messenger/Sources/RegisterFeature/RegisterView.swift b/Examples/xx-messenger/Sources/RegisterFeature/RegisterView.swift index 4b168c2765f7a2f67afad164362807e0daa5a819..a41e16ab9648176d3efb5eda5f616e66f9ae05cf 100644 --- a/Examples/xx-messenger/Sources/RegisterFeature/RegisterView.swift +++ b/Examples/xx-messenger/Sources/RegisterFeature/RegisterView.swift @@ -2,22 +2,22 @@ import ComposableArchitecture import SwiftUI public struct RegisterView: View { - public init(store: Store<RegisterState, RegisterAction>) { + public init(store: StoreOf<RegisterComponent>) { self.store = store } - let store: Store<RegisterState, RegisterAction> - @FocusState var focusedField: RegisterState.Field? + let store: StoreOf<RegisterComponent> + @FocusState var focusedField: RegisterComponent.State.Field? struct ViewState: Equatable { - init(_ state: RegisterState) { + init(_ state: RegisterComponent.State) { focusedField = state.focusedField username = state.username isRegistering = state.isRegistering failure = state.failure } - var focusedField: RegisterState.Field? + var focusedField: RegisterComponent.State.Field? var username: String var isRegistering: Bool var failure: String? @@ -31,7 +31,7 @@ public struct RegisterView: View { TextField( text: viewStore.binding( get: \.username, - send: { RegisterAction.set(\.$username, $0) } + send: { RegisterComponent.Action.set(\.$username, $0) } ), prompt: Text("Enter username"), label: { Text("Username") } @@ -78,9 +78,8 @@ public struct RegisterView: View { public struct RegisterView_Previews: PreviewProvider { public static var previews: some View { RegisterView(store: Store( - initialState: RegisterState(), - reducer: .empty, - environment: () + initialState: RegisterComponent.State(), + reducer: EmptyReducer() )) } } diff --git a/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthComponent.swift b/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthComponent.swift new file mode 100644 index 0000000000000000000000000000000000000000..629c150dc271792e1b1b88bc9733a364955a8985 --- /dev/null +++ b/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthComponent.swift @@ -0,0 +1,71 @@ +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient + +public struct ResetAuthComponent: ReducerProtocol { + public struct State: Equatable { + public init( + partner: Contact, + isResetting: Bool = false, + failure: String? = nil, + didReset: Bool = false + ) { + self.partner = partner + self.isResetting = isResetting + self.failure = failure + self.didReset = didReset + } + + public var partner: Contact + public var isResetting: Bool + public var failure: String? + public var didReset: Bool + } + + public enum Action: Equatable { + case resetTapped + case didReset + case didFail(String) + } + + public init() {} + + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + + public func reduce(into state: inout State, action: Action) -> EffectTask<Action> { + switch action { + case .resetTapped: + state.isResetting = true + state.didReset = false + state.failure = nil + return Effect.result { [state] in + do { + let e2e = try messenger.e2e.tryGet() + _ = try e2e.resetAuthenticatedChannel(partner: state.partner) + return .success(.didReset) + } catch { + return .success(.didFail(error.localizedDescription)) + } + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .didReset: + state.isResetting = false + state.didReset = true + state.failure = nil + return .none + + case .didFail(let failure): + state.isResetting = false + state.didReset = false + state.failure = failure + return .none + } + } +} diff --git a/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthFeature.swift b/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthFeature.swift deleted file mode 100644 index d4acb74002902f9b9a82da2981f78cd3312deb9e..0000000000000000000000000000000000000000 --- a/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthFeature.swift +++ /dev/null @@ -1,90 +0,0 @@ -import ComposableArchitecture -import Foundation -import XCTestDynamicOverlay -import XXClient -import XXMessengerClient - -public struct ResetAuthState: Equatable { - public init( - partner: Contact, - isResetting: Bool = false, - failure: String? = nil, - didReset: Bool = false - ) { - self.partner = partner - self.isResetting = isResetting - self.failure = failure - self.didReset = didReset - } - - public var partner: Contact - public var isResetting: Bool - public var failure: String? - public var didReset: Bool -} - -public enum ResetAuthAction: Equatable { - case resetTapped - case didReset - case didFail(String) -} - -public struct ResetAuthEnvironment { - public init( - messenger: Messenger, - mainQueue: AnySchedulerOf<DispatchQueue>, - bgQueue: AnySchedulerOf<DispatchQueue> - ) { - self.messenger = messenger - self.mainQueue = mainQueue - self.bgQueue = bgQueue - } - - public var messenger: Messenger - public var mainQueue: AnySchedulerOf<DispatchQueue> - public var bgQueue: AnySchedulerOf<DispatchQueue> -} - -#if DEBUG -extension ResetAuthEnvironment { - public static let unimplemented = ResetAuthEnvironment( - messenger: .unimplemented, - mainQueue: .unimplemented, - bgQueue: .unimplemented - ) -} -#endif - -public let resetAuthReducer = Reducer<ResetAuthState, ResetAuthAction, ResetAuthEnvironment> -{ state, action, env in - switch action { - case .resetTapped: - state.isResetting = true - state.didReset = false - state.failure = nil - return Effect.result { [state] in - do { - let e2e = try env.messenger.e2e.tryGet() - _ = try e2e.resetAuthenticatedChannel(partner: state.partner) - return .success(.didReset) - } catch { - return .success(.didFail(error.localizedDescription)) - } - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .didReset: - state.isResetting = false - state.didReset = true - state.failure = nil - return .none - - case .didFail(let failure): - state.isResetting = false - state.didReset = false - state.failure = failure - return .none - } -} diff --git a/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthView.swift b/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthView.swift index 7b384efb74d841b4edf04a61cf55aa02a7c0d725..cc15b34a75c475c41c5cedacf0bd11181877791d 100644 --- a/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthView.swift +++ b/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthView.swift @@ -3,14 +3,14 @@ import ComposableArchitecture import SwiftUI public struct ResetAuthView: View { - public init(store: Store<ResetAuthState, ResetAuthAction>) { + public init(store: StoreOf<ResetAuthComponent>) { self.store = store } - let store: Store<ResetAuthState, ResetAuthAction> + let store: StoreOf<ResetAuthComponent> struct ViewState: Equatable { - init(state: ResetAuthState) { + init(state: ResetAuthComponent.State) { contactID = try? state.partner.getId() isResetting = state.isResetting failure = state.failure @@ -68,11 +68,10 @@ public struct ResetAuthView_Previews: PreviewProvider { public static var previews: some View { NavigationView { ResetAuthView(store: Store( - initialState: ResetAuthState( + initialState: ResetAuthComponent.State( partner: .unimplemented(Data()) ), - reducer: .empty, - environment: () + reducer: EmptyReducer() )) } } diff --git a/Examples/xx-messenger/Sources/RestoreFeature/RestoreComponent.swift b/Examples/xx-messenger/Sources/RestoreFeature/RestoreComponent.swift new file mode 100644 index 0000000000000000000000000000000000000000..5a7ef06de236b757cfc82ebbab07b1829ba45822 --- /dev/null +++ b/Examples/xx-messenger/Sources/RestoreFeature/RestoreComponent.swift @@ -0,0 +1,155 @@ +import AppCore +import Combine +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay +import XXMessengerClient +import XXModels + +public struct RestoreComponent: ReducerProtocol { + public struct State: Equatable { + public enum Field: String, Hashable { + case passphrase + } + + public struct File: Equatable { + public init(name: String, data: Data) { + self.name = name + self.data = data + } + + public var name: String + public var data: Data + } + + public init( + file: File? = nil, + fileImportFailure: String? = nil, + restoreFailures: [String] = [], + focusedField: Field? = nil, + isImportingFile: Bool = false, + passphrase: String = "", + isRestoring: Bool = false + ) { + self.file = file + self.fileImportFailure = fileImportFailure + self.restoreFailures = restoreFailures + self.focusedField = focusedField + self.isImportingFile = isImportingFile + self.passphrase = passphrase + self.isRestoring = isRestoring + } + + public var file: File? + public var fileImportFailure: String? + public var restoreFailures: [String] + @BindableState public var focusedField: Field? + @BindableState public var isImportingFile: Bool + @BindableState public var passphrase: String + @BindableState public var isRestoring: Bool + } + + public enum Action: Equatable, BindableAction { + case importFileTapped + case fileImport(Result<URL, NSError>) + case restoreTapped + case finished + case failed([NSError]) + case binding(BindingAction<State>) + } + + public init() {} + + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB + @Dependency(\.app.loadData) var loadData: URLDataLoader + @Dependency(\.app.now) var now: () -> Date + @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + + public var body: some ReducerProtocol<State, Action> { + BindingReducer() + Reduce { state, action in + switch action { + case .importFileTapped: + state.isImportingFile = true + state.fileImportFailure = nil + return .none + + case .fileImport(.success(let url)): + state.isImportingFile = false + do { + state.file = .init( + name: url.lastPathComponent, + data: try loadData(url) + ) + state.fileImportFailure = nil + } catch { + state.file = nil + state.fileImportFailure = error.localizedDescription + } + return .none + + case .fileImport(.failure(let error)): + state.isImportingFile = false + state.file = nil + state.fileImportFailure = error.localizedDescription + return .none + + case .restoreTapped: + guard let backupData = state.file?.data, backupData.count > 0 else { return .none } + let backupPassphrase = state.passphrase + state.isRestoring = true + state.restoreFailures = [] + return Effect.result { + do { + let result = try messenger.restoreBackup( + backupData: backupData, + backupPassphrase: backupPassphrase + ) + let facts = try messenger.ud.tryGet().getFacts() + try db().saveContact(Contact( + id: try messenger.e2e.tryGet().getContact().getId(), + username: facts.get(.username)?.value, + email: facts.get(.email)?.value, + phone: facts.get(.phone)?.value, + createdAt: now() + )) + try result.restoredContacts.forEach { contactId in + if try db().fetchContacts(.init(id: [contactId])).isEmpty { + try db().saveContact(Contact( + id: contactId, + createdAt: now() + )) + } + } + return .success(.finished) + } catch { + var errors = [error as NSError] + do { + try messenger.destroy() + } catch { + errors.append(error as NSError) + } + return .success(.failed(errors)) + } + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .finished: + state.isRestoring = false + return .none + + case .failed(let errors): + state.isRestoring = false + state.restoreFailures = errors.map(\.localizedDescription) + return .none + + case .binding(_): + return .none + } + } + } +} diff --git a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift deleted file mode 100644 index 6b3d61d340a7932b6367f71894a0efd707ae81d2..0000000000000000000000000000000000000000 --- a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift +++ /dev/null @@ -1,181 +0,0 @@ -import AppCore -import Combine -import ComposableArchitecture -import Foundation -import XCTestDynamicOverlay -import XXMessengerClient -import XXModels - -public struct RestoreState: Equatable { - public enum Field: String, Hashable { - case passphrase - } - - public struct File: Equatable { - public init(name: String, data: Data) { - self.name = name - self.data = data - } - - public var name: String - public var data: Data - } - - public init( - file: File? = nil, - fileImportFailure: String? = nil, - restoreFailures: [String] = [], - focusedField: Field? = nil, - isImportingFile: Bool = false, - passphrase: String = "", - isRestoring: Bool = false - ) { - self.file = file - self.fileImportFailure = fileImportFailure - self.restoreFailures = restoreFailures - self.focusedField = focusedField - self.isImportingFile = isImportingFile - self.passphrase = passphrase - self.isRestoring = isRestoring - } - - public var file: File? - public var fileImportFailure: String? - public var restoreFailures: [String] - @BindableState public var focusedField: Field? - @BindableState public var isImportingFile: Bool - @BindableState public var passphrase: String - @BindableState public var isRestoring: Bool -} - -public enum RestoreAction: Equatable, BindableAction { - case importFileTapped - case fileImport(Result<URL, NSError>) - case restoreTapped - case finished - case failed([NSError]) - case binding(BindingAction<RestoreState>) -} - -public struct RestoreEnvironment { - public init( - messenger: Messenger, - db: DBManagerGetDB, - loadData: URLDataLoader, - now: @escaping () -> Date, - mainQueue: AnySchedulerOf<DispatchQueue>, - bgQueue: AnySchedulerOf<DispatchQueue> - ) { - self.messenger = messenger - self.db = db - self.loadData = loadData - self.now = now - self.mainQueue = mainQueue - self.bgQueue = bgQueue - } - - public var messenger: Messenger - public var db: DBManagerGetDB - public var loadData: URLDataLoader - public var now: () -> Date - public var mainQueue: AnySchedulerOf<DispatchQueue> - public var bgQueue: AnySchedulerOf<DispatchQueue> -} - -#if DEBUG -extension RestoreEnvironment { - public static let unimplemented = RestoreEnvironment( - messenger: .unimplemented, - db: .unimplemented, - loadData: .unimplemented, - now: XCTUnimplemented("\(Self.self).now"), - mainQueue: .unimplemented, - bgQueue: .unimplemented - ) -} -#endif - -public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironment> -{ state, action, env in - switch action { - case .importFileTapped: - state.isImportingFile = true - state.fileImportFailure = nil - return .none - - case .fileImport(.success(let url)): - state.isImportingFile = false - do { - state.file = .init( - name: url.lastPathComponent, - data: try env.loadData(url) - ) - state.fileImportFailure = nil - } catch { - state.file = nil - state.fileImportFailure = error.localizedDescription - } - return .none - - case .fileImport(.failure(let error)): - state.isImportingFile = false - state.file = nil - state.fileImportFailure = error.localizedDescription - return .none - - case .restoreTapped: - guard let backupData = state.file?.data, backupData.count > 0 else { return .none } - let backupPassphrase = state.passphrase - state.isRestoring = true - state.restoreFailures = [] - return Effect.result { - do { - let result = try env.messenger.restoreBackup( - backupData: backupData, - backupPassphrase: backupPassphrase - ) - let facts = try env.messenger.ud.tryGet().getFacts() - try env.db().saveContact(Contact( - id: try env.messenger.e2e.tryGet().getContact().getId(), - username: facts.get(.username)?.value, - email: facts.get(.email)?.value, - phone: facts.get(.phone)?.value, - createdAt: env.now() - )) - try result.restoredContacts.forEach { contactId in - if try env.db().fetchContacts(.init(id: [contactId])).isEmpty { - try env.db().saveContact(Contact( - id: contactId, - createdAt: env.now() - )) - } - } - return .success(.finished) - } catch { - var errors = [error as NSError] - do { - try env.messenger.destroy() - } catch { - errors.append(error as NSError) - } - return .success(.failed(errors)) - } - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .finished: - state.isRestoring = false - return .none - - case .failed(let errors): - state.isRestoring = false - state.restoreFailures = errors.map(\.localizedDescription) - return .none - - case .binding(_): - return .none - } -} -.binding() diff --git a/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift b/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift index 281f3f061e4a3ff5ccc1913bb45e45a03cff6576..503b868d44a587b19fb89728b2e9c7e713606301 100644 --- a/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift +++ b/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift @@ -2,12 +2,12 @@ import ComposableArchitecture import SwiftUI public struct RestoreView: View { - public init(store: Store<RestoreState, RestoreAction>) { + public init(store: StoreOf<RestoreComponent>) { self.store = store } - let store: Store<RestoreState, RestoreAction> - @FocusState var focusedField: RestoreState.Field? + let store: StoreOf<RestoreComponent> + @FocusState var focusedField: RestoreComponent.State.Field? struct ViewState: Equatable { struct File: Equatable { @@ -19,11 +19,11 @@ public struct RestoreView: View { var isImportingFile: Bool var passphrase: String var isRestoring: Bool - var focusedField: RestoreState.Field? + var focusedField: RestoreComponent.State.Field? var fileImportFailure: String? var restoreFailures: [String] - init(state: RestoreState) { + init(state: RestoreComponent.State) { file = state.file.map { .init(name: $0.name, size: $0.data.count) } isImportingFile = state.isImportingFile passphrase = state.passphrase @@ -61,7 +61,7 @@ public struct RestoreView: View { } } - @ViewBuilder func fileSection(_ viewStore: ViewStore<ViewState, RestoreAction>) -> some View { + @ViewBuilder func fileSection(_ viewStore: ViewStore<ViewState, RestoreComponent.Action>) -> some View { Section { if let file = viewStore.file { HStack(alignment: .bottom) { @@ -100,7 +100,7 @@ public struct RestoreView: View { } } - @ViewBuilder func restoreSection(_ viewStore: ViewStore<ViewState, RestoreAction>) -> some View { + @ViewBuilder func restoreSection(_ viewStore: ViewStore<ViewState, RestoreComponent.Action>) -> some View { Section { SecureField("Passphrase", text: viewStore.binding( get: \.passphrase, @@ -152,7 +152,7 @@ public struct RestoreView: View { public struct RestoreView_Previews: PreviewProvider { public static var previews: some View { RestoreView(store: Store( - initialState: RestoreState( + initialState: RestoreComponent.State( file: .init(name: "preview", data: Data()), fileImportFailure: nil, restoreFailures: [ @@ -165,8 +165,7 @@ public struct RestoreView_Previews: PreviewProvider { passphrase: "", isRestoring: true ), - reducer: .empty, - environment: () + reducer: EmptyReducer() )) } } diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestComponent.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestComponent.swift new file mode 100644 index 0000000000000000000000000000000000000000..f5f2e8afec9e1bf0ef52a40165221c21c538a914 --- /dev/null +++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestComponent.swift @@ -0,0 +1,138 @@ +import AppCore +import Combine +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct SendRequestComponent: ReducerProtocol { + public struct State: Equatable { + 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 Action: Equatable, BindableAction { + case start + case sendTapped + case sendSucceeded + case sendFailed(String) + case binding(BindingAction<State>) + case myContactFetched(XXClient.Contact) + case myContactFetchFailed(NSError) + } + + public init() {} + + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB + @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + + public var body: some ReducerProtocol<State, Action> { + BindingReducer() + Reduce { state, action in + switch action { + case .start: + return Effect.run { subscriber in + do { + let contact = try messenger.myContact() + subscriber.send(.myContactFetched(contact)) + } catch { + subscriber.send(.myContactFetchFailed(error as NSError)) + } + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .receive(on: mainQueue) + .subscribe(on: bgQueue) + .eraseToEffect() + + case .myContactFetched(let contact): + state.myContact = contact + state.failure = nil + return .none + + case .myContactFetchFailed(let failure): + state.myContact = nil + state.failure = failure.localizedDescription + return .none + + case .sendTapped: + state.isSending = true + state.failure = nil + return .result { [state] in + func updateAuthStatus(_ authStatus: XXModels.Contact.AuthStatus) throws { + try 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.get(.username) { + includedFacts.append(fact) + } + if state.sendEmail, let fact = myFacts.get(.email) { + includedFacts.append(fact) + } + if state.sendPhone, let fact = myFacts.get(.phone) { + includedFacts.append(fact) + } + _ = try 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: bgQueue) + .receive(on: 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(_): + return .none + } + } + } +} diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift deleted file mode 100644 index 18075179aedc62daff46847052e2bc0071f76b0a..0000000000000000000000000000000000000000 --- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift +++ /dev/null @@ -1,158 +0,0 @@ -import AppCore -import Combine -import ComposableArchitecture -import Foundation -import XCTestDynamicOverlay -import XXClient -import XXMessengerClient -import XXModels - -public struct SendRequestState: Equatable { - 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, BindableAction { - case start - case sendTapped - case sendSucceeded - case sendFailed(String) - case binding(BindingAction<SendRequestState>) - case myContactFetched(XXClient.Contact) - case myContactFetchFailed(NSError) -} - -public struct SendRequestEnvironment { - 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( - messenger: .unimplemented, - db: .unimplemented, - mainQueue: .unimplemented, - bgQueue: .unimplemented - ) -} -#endif - -public let sendRequestReducer = Reducer<SendRequestState, SendRequestAction, SendRequestEnvironment> -{ state, action, env in - switch action { - case .start: - return Effect.run { subscriber in - do { - let contact = try env.messenger.myContact() - subscriber.send(.myContactFetched(contact)) - } catch { - subscriber.send(.myContactFetchFailed(error as NSError)) - } - subscriber.send(completion: .finished) - return AnyCancellable {} - } - .receive(on: env.mainQueue) - .subscribe(on: env.bgQueue) - .eraseToEffect() - - case .myContactFetched(let contact): - state.myContact = contact - state.failure = nil - return .none - - case .myContactFetchFailed(let failure): - state.myContact = nil - state.failure = failure.localizedDescription - return .none - - case .sendTapped: - 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.get(.username) { - includedFacts.append(fact) - } - if state.sendEmail, let fact = myFacts.get(.email) { - includedFacts.append(fact) - } - if state.sendPhone, let fact = myFacts.get(.phone) { - 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(_): - return .none - } -} -.binding() diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift index 809d45be72436067d4e04c35bdef1995f7ed5a9d..90294a80303f1746d659aa8e3789e1b09e696275 100644 --- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift +++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift @@ -4,11 +4,11 @@ import SwiftUI import XXClient public struct SendRequestView: View { - public init(store: Store<SendRequestState, SendRequestAction>) { + public init(store: StoreOf<SendRequestComponent>) { self.store = store } - let store: Store<SendRequestState, SendRequestAction> + let store: StoreOf<SendRequestComponent> struct ViewState: Equatable { var contactUsername: String? @@ -23,7 +23,7 @@ public struct SendRequestView: View { var isSending: Bool var failure: String? - init(state: SendRequestState) { + init(state: SendRequestComponent.State) { contactUsername = try? state.contact.getFact(.username)?.value contactEmail = try? state.contact.getFact(.email)?.value contactPhone = try? state.contact.getFact(.phone)?.value @@ -129,7 +129,7 @@ public struct SendRequestView_Previews: PreviewProvider { public static var previews: some View { NavigationView { SendRequestView(store: Store( - initialState: SendRequestState( + initialState: SendRequestComponent.State( contact: { var contact = XXClient.Contact.unimplemented("contact-data".data(using: .utf8)!) contact.getFactsFromContact.run = { _ in @@ -158,8 +158,7 @@ public struct SendRequestView_Previews: PreviewProvider { isSending: false, failure: "Something went wrong" ), - reducer: .empty, - environment: () + reducer: EmptyReducer() )) } } diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchComponent.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchComponent.swift new file mode 100644 index 0000000000000000000000000000000000000000..c274d15b4dd368656619491c8a05ecadd3f2bb32 --- /dev/null +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchComponent.swift @@ -0,0 +1,146 @@ +import ComposableArchitecture +import ComposablePresentation +import ContactFeature +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient + +public struct UserSearchComponent: ReducerProtocol { + public struct State: Equatable { + public enum Field: String, Hashable { + case username + case email + case phone + } + + public struct Result: Equatable, Identifiable { + public init( + id: Data, + xxContact: XXClient.Contact, + username: String? = nil, + email: String? = nil, + phone: String? = nil + ) { + self.id = id + self.xxContact = xxContact + self.username = username + self.email = email + self.phone = phone + } + + public var id: Data + public var xxContact: XXClient.Contact + public var username: String? + public var email: String? + public var phone: String? + + public var hasFacts: Bool { + username != nil || email != nil || phone != nil + } + } + + public init( + focusedField: Field? = nil, + query: MessengerSearchContacts.Query = .init(), + isSearching: Bool = false, + failure: String? = nil, + results: IdentifiedArrayOf<Result> = [], + contact: ContactComponent.State? = nil + ) { + self.focusedField = focusedField + self.query = query + self.isSearching = isSearching + self.failure = failure + self.results = results + self.contact = contact + } + + @BindableState public var focusedField: Field? + @BindableState public var query: MessengerSearchContacts.Query + public var isSearching: Bool + public var failure: String? + public var results: IdentifiedArrayOf<Result> + public var contact: ContactComponent.State? + } + + public enum Action: Equatable, BindableAction { + case searchTapped + case didFail(String) + case didSucceed([Contact]) + case didDismissContact + case resultTapped(id: Data) + case binding(BindingAction<State>) + case contact(ContactComponent.Action) + } + + public init() {} + + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + + public var body: some ReducerProtocol<State, Action> { + BindingReducer() + Reduce { state, action in + switch action { + case .searchTapped: + state.focusedField = nil + state.isSearching = true + state.results = [] + state.failure = nil + return .result { [query = state.query] in + do { + return .success(.didSucceed(try messenger.searchContacts(query: query))) + } catch { + return .success(.didFail(error.localizedDescription)) + } + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .didSucceed(let contacts): + state.isSearching = false + state.failure = nil + state.results = IdentifiedArray(uniqueElements: contacts.compactMap { contact in + guard let id = try? contact.getId() else { return nil } + return State.Result( + id: id, + xxContact: contact, + username: try? contact.getFact(.username)?.value, + email: try? contact.getFact(.email)?.value, + phone: try? contact.getFact(.phone)?.value + ) + }) + return .none + + case .didFail(let failure): + state.isSearching = false + state.failure = failure + state.results = [] + return .none + + case .didDismissContact: + state.contact = nil + return .none + + case .resultTapped(let id): + state.contact = ContactComponent.State( + id: id, + xxContact: state.results[id: id]?.xxContact + ) + return .none + + case .binding(_), .contact(_): + return .none + } + } + .presenting( + state: .keyPath(\.contact), + id: .keyPath(\.?.id), + action: /Action.contact, + presented: { ContactComponent() } + ) + } +} diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift deleted file mode 100644 index f39353a78db32d57a1429d8f8a0b6e5fb46260e0..0000000000000000000000000000000000000000 --- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift +++ /dev/null @@ -1,168 +0,0 @@ -import ComposableArchitecture -import ComposablePresentation -import ContactFeature -import Foundation -import XCTestDynamicOverlay -import XXClient -import XXMessengerClient - -public struct UserSearchState: Equatable { - public enum Field: String, Hashable { - case username - case email - case phone - } - - public struct Result: Equatable, Identifiable { - public init( - id: Data, - xxContact: XXClient.Contact, - username: String? = nil, - email: String? = nil, - phone: String? = nil - ) { - self.id = id - self.xxContact = xxContact - self.username = username - self.email = email - self.phone = phone - } - - public var id: Data - public var xxContact: XXClient.Contact - public var username: String? - public var email: String? - public var phone: String? - - public var hasFacts: Bool { - username != nil || email != nil || phone != nil - } - } - - public init( - focusedField: Field? = nil, - query: MessengerSearchContacts.Query = .init(), - isSearching: Bool = false, - failure: String? = nil, - results: IdentifiedArrayOf<Result> = [], - 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? - @BindableState public var query: MessengerSearchContacts.Query - public var isSearching: Bool - public var failure: String? - public var results: IdentifiedArrayOf<Result> - public var contact: ContactState? -} - -public enum UserSearchAction: Equatable, BindableAction { - case searchTapped - case didFail(String) - case didSucceed([Contact]) - case didDismissContact - case resultTapped(id: Data) - case binding(BindingAction<UserSearchState>) - case contact(ContactAction) -} - -public struct UserSearchEnvironment { - public init( - messenger: Messenger, - mainQueue: AnySchedulerOf<DispatchQueue>, - bgQueue: AnySchedulerOf<DispatchQueue>, - contact: @escaping () -> ContactEnvironment - ) { - self.messenger = messenger - self.mainQueue = mainQueue - self.bgQueue = bgQueue - self.contact = contact - } - - public var messenger: Messenger - public var mainQueue: AnySchedulerOf<DispatchQueue> - public var bgQueue: AnySchedulerOf<DispatchQueue> - public var contact: () -> ContactEnvironment -} - -#if DEBUG -extension UserSearchEnvironment { - public static let unimplemented = UserSearchEnvironment( - messenger: .unimplemented, - mainQueue: .unimplemented, - bgQueue: .unimplemented, - contact: { .unimplemented } - ) -} -#endif - -public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSearchEnvironment> -{ state, action, env in - switch action { - case .searchTapped: - state.focusedField = nil - state.isSearching = true - state.results = [] - state.failure = nil - return .result { [query = state.query] in - do { - return .success(.didSucceed(try env.messenger.searchContacts(query: query))) - } catch { - return .success(.didFail(error.localizedDescription)) - } - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .didSucceed(let contacts): - state.isSearching = false - state.failure = nil - state.results = IdentifiedArray(uniqueElements: contacts.compactMap { contact in - guard let id = try? contact.getId() else { return nil } - return UserSearchState.Result( - id: id, - xxContact: contact, - username: try? contact.getFact(.username)?.value, - email: try? contact.getFact(.email)?.value, - phone: try? contact.getFact(.phone)?.value - ) - }) - return .none - - case .didFail(let failure): - state.isSearching = false - state.failure = failure - state.results = [] - return .none - - case .didDismissContact: - state.contact = nil - return .none - - case .resultTapped(let id): - state.contact = ContactState( - id: id, - xxContact: state.results[id: id]?.xxContact - ) - return .none - - case .binding(_), .contact(_): - return .none - } -} -.binding() -.presenting( - contactReducer, - state: .keyPath(\.contact), - id: .keyPath(\.?.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 328ff98c2e0f79907da76927ce254322022ea07a..ea5f2ad74eb15cd65c2935e1e206ecb3515d5ccc 100644 --- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift @@ -5,21 +5,21 @@ import SwiftUI import XXMessengerClient public struct UserSearchView: View { - public init(store: Store<UserSearchState, UserSearchAction>) { + public init(store: StoreOf<UserSearchComponent>) { self.store = store } - let store: Store<UserSearchState, UserSearchAction> - @FocusState var focusedField: UserSearchState.Field? + let store: StoreOf<UserSearchComponent> + @FocusState var focusedField: UserSearchComponent.State.Field? struct ViewState: Equatable { - var focusedField: UserSearchState.Field? + var focusedField: UserSearchComponent.State.Field? var query: MessengerSearchContacts.Query var isSearching: Bool var failure: String? - var results: IdentifiedArrayOf<UserSearchState.Result> + var results: IdentifiedArrayOf<UserSearchComponent.State.Result> - init(state: UserSearchState) { + init(state: UserSearchComponent.State) { focusedField = state.focusedField query = state.query isSearching = state.isSearching @@ -35,7 +35,7 @@ public struct UserSearchView: View { TextField( text: viewStore.binding( get: { $0.query.username ?? "" }, - send: { UserSearchAction.set(\.$query.username, $0.isEmpty ? nil : $0) } + send: { UserSearchComponent.Action.set(\.$query.username, $0.isEmpty ? nil : $0) } ), prompt: Text("Enter username"), label: { Text("Username") } @@ -45,7 +45,7 @@ public struct UserSearchView: View { TextField( text: viewStore.binding( get: { $0.query.email ?? "" }, - send: { UserSearchAction.set(\.$query.email, $0.isEmpty ? nil : $0) } + send: { UserSearchComponent.Action.set(\.$query.email, $0.isEmpty ? nil : $0) } ), prompt: Text("Enter email"), label: { Text("Email") } @@ -55,7 +55,7 @@ public struct UserSearchView: View { TextField( text: viewStore.binding( get: { $0.query.phone ?? "" }, - send: { UserSearchAction.set(\.$query.phone, $0.isEmpty ? nil : $0) } + send: { UserSearchComponent.Action.set(\.$query.phone, $0.isEmpty ? nil : $0) } ), prompt: Text("Enter phone"), label: { Text("Phone") } @@ -124,7 +124,7 @@ public struct UserSearchView: View { .background(NavigationLinkWithStore( store.scope( state: \.contact, - action: UserSearchAction.contact + action: UserSearchComponent.Action.contact ), onDeactivate: { viewStore.send(.didDismissContact) }, destination: ContactView.init(store:) @@ -137,9 +137,8 @@ public struct UserSearchView: View { public struct UserSearchView_Previews: PreviewProvider { public static var previews: some View { UserSearchView(store: Store( - initialState: UserSearchState(), - reducer: .empty, - environment: () + initialState: UserSearchComponent.State(), + reducer: EmptyReducer() )) } } diff --git a/Examples/xx-messenger/Sources/VerifyContactFeature/VerifyContactComponent.swift b/Examples/xx-messenger/Sources/VerifyContactFeature/VerifyContactComponent.swift new file mode 100644 index 0000000000000000000000000000000000000000..09fdb1585659abe9c77b873dd6917dcb17d75034 --- /dev/null +++ b/Examples/xx-messenger/Sources/VerifyContactFeature/VerifyContactComponent.swift @@ -0,0 +1,75 @@ +import AppCore +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct VerifyContactComponent: ReducerProtocol { + public struct State: Equatable { + public enum Result: Equatable { + case success(Bool) + case failure(String) + } + + public init( + contact: XXClient.Contact, + isVerifying: Bool = false, + result: Result? = nil + ) { + self.contact = contact + self.isVerifying = isVerifying + self.result = result + } + + public var contact: XXClient.Contact + public var isVerifying: Bool + public var result: Result? + } + + public enum Action: Equatable { + case verifyTapped + case didVerify(State.Result) + } + + public init() {} + + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB + @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + + public func reduce(into state: inout State, action: Action) -> EffectTask<Action> { + switch action { + case .verifyTapped: + state.isVerifying = true + state.result = nil + return Effect.result { [state] in + func updateStatus(_ status: XXModels.Contact.AuthStatus) throws { + try db().bulkUpdateContacts.callAsFunction( + .init(id: [try state.contact.getId()]), + .init(authStatus: status) + ) + } + do { + try updateStatus(.verificationInProgress) + let result = try messenger.verifyContact(state.contact) + try updateStatus(result ? .verified : .verificationFailed) + return .success(.didVerify(.success(result))) + } catch { + try? updateStatus(.verificationFailed) + return .success(.didVerify(.failure(error.localizedDescription))) + } + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .didVerify(let result): + state.isVerifying = false + state.result = result + return .none + } + } +} diff --git a/Examples/xx-messenger/Sources/VerifyContactFeature/VerifyContactFeature.swift b/Examples/xx-messenger/Sources/VerifyContactFeature/VerifyContactFeature.swift deleted file mode 100644 index 1663d155f59e9c8d7eb4b542de4255021620be4c..0000000000000000000000000000000000000000 --- a/Examples/xx-messenger/Sources/VerifyContactFeature/VerifyContactFeature.swift +++ /dev/null @@ -1,97 +0,0 @@ -import AppCore -import ComposableArchitecture -import Foundation -import XCTestDynamicOverlay -import XXClient -import XXMessengerClient -import XXModels - -public struct VerifyContactState: Equatable { - public enum Result: Equatable { - case success(Bool) - case failure(String) - } - - public init( - contact: XXClient.Contact, - isVerifying: Bool = false, - result: Result? = nil - ) { - self.contact = contact - self.isVerifying = isVerifying - self.result = result - } - - public var contact: XXClient.Contact - public var isVerifying: Bool - public var result: Result? -} - -public enum VerifyContactAction: Equatable { - case verifyTapped - case didVerify(VerifyContactState.Result) -} - -public struct VerifyContactEnvironment { - 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 VerifyContactEnvironment { - public static let unimplemented = VerifyContactEnvironment( - messenger: .unimplemented, - db: .unimplemented, - mainQueue: .unimplemented, - bgQueue: .unimplemented - ) -} -#endif - -public let verifyContactReducer = Reducer<VerifyContactState, VerifyContactAction, VerifyContactEnvironment> -{ state, action, env in - switch action { - case .verifyTapped: - state.isVerifying = true - state.result = nil - return Effect.result { [state] in - func updateStatus(_ status: XXModels.Contact.AuthStatus) throws { - try env.db().bulkUpdateContacts.callAsFunction( - .init(id: [try state.contact.getId()]), - .init(authStatus: status) - ) - } - do { - try updateStatus(.verificationInProgress) - let result = try env.messenger.verifyContact(state.contact) - try updateStatus(result ? .verified : .verificationFailed) - return .success(.didVerify(.success(result))) - } catch { - try? updateStatus(.verificationFailed) - return .success(.didVerify(.failure(error.localizedDescription))) - } - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .didVerify(let result): - state.isVerifying = false - state.result = result - return .none - } -} diff --git a/Examples/xx-messenger/Sources/VerifyContactFeature/VerifyContactView.swift b/Examples/xx-messenger/Sources/VerifyContactFeature/VerifyContactView.swift index 32b81ab789a5408cb46f34574cca6bf7e12144c0..d2cb428c3a8189d4ddbcd6f7948573ad68f9963e 100644 --- a/Examples/xx-messenger/Sources/VerifyContactFeature/VerifyContactView.swift +++ b/Examples/xx-messenger/Sources/VerifyContactFeature/VerifyContactView.swift @@ -2,20 +2,20 @@ import ComposableArchitecture import SwiftUI public struct VerifyContactView: View { - public init(store: Store<VerifyContactState, VerifyContactAction>) { + public init(store: StoreOf<VerifyContactComponent>) { self.store = store } - let store: Store<VerifyContactState, VerifyContactAction> + let store: StoreOf<VerifyContactComponent> struct ViewState: Equatable { var username: String? var email: String? var phone: String? var isVerifying: Bool - var result: VerifyContactState.Result? + var result: VerifyContactComponent.State.Result? - init(state: VerifyContactState) { + init(state: VerifyContactComponent.State) { username = try? state.contact.getFact(.username)?.value email = try? state.contact.getFact(.email)?.value phone = try? state.contact.getFact(.phone)?.value @@ -89,11 +89,10 @@ public struct VerifyContactView: View { public struct VerifyContactView_Previews: PreviewProvider { public static var previews: some View { VerifyContactView(store: Store( - initialState: VerifyContactState( + initialState: VerifyContactComponent.State( contact: .unimplemented("contact-data".data(using: .utf8)!) ), - reducer: .empty, - environment: () + reducer: EmptyReducer() )) } } diff --git a/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeComponent.swift b/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeComponent.swift new file mode 100644 index 0000000000000000000000000000000000000000..55af67030deb51647f697d58252537ea6242af8a --- /dev/null +++ b/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeComponent.swift @@ -0,0 +1,64 @@ +import ComposableArchitecture +import SwiftUI +import XXMessengerClient + +public struct WelcomeComponent: ReducerProtocol { + public struct State: Equatable { + public init( + isCreatingCMix: Bool = false, + failure: String? = nil + ) { + self.isCreatingAccount = isCreatingCMix + self.failure = failure + } + + public var isCreatingAccount: Bool + public var failure: String? + } + + public enum Action: Equatable { + case newAccountTapped + case restoreTapped + case finished + case failed(String) + } + + public init() {} + + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + + public func reduce(into state: inout State, action: Action) -> EffectTask<Action> { + switch action { + case .newAccountTapped: + state.isCreatingAccount = true + state.failure = nil + return .future { fulfill in + do { + try messenger.create() + fulfill(.success(.finished)) + } + catch { + fulfill(.success(.failed(error.localizedDescription))) + } + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .restoreTapped: + return .none + + case .finished: + state.isCreatingAccount = false + state.failure = nil + return .none + + case .failed(let failure): + state.isCreatingAccount = false + state.failure = failure + return .none + } + } +} diff --git a/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeFeature.swift b/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeFeature.swift deleted file mode 100644 index 66e9ef1b3492c1d8277fac263a9d76523769030c..0000000000000000000000000000000000000000 --- a/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeFeature.swift +++ /dev/null @@ -1,83 +0,0 @@ -import ComposableArchitecture -import SwiftUI -import XXMessengerClient - -public struct WelcomeState: Equatable { - public init( - isCreatingCMix: Bool = false, - failure: String? = nil - ) { - self.isCreatingAccount = isCreatingCMix - self.failure = failure - } - - public var isCreatingAccount: Bool - public var failure: String? -} - -public enum WelcomeAction: Equatable { - case newAccountTapped - case restoreTapped - case finished - case failed(String) -} - -public struct WelcomeEnvironment { - public init( - messenger: Messenger, - mainQueue: AnySchedulerOf<DispatchQueue>, - bgQueue: AnySchedulerOf<DispatchQueue> - ) { - self.messenger = messenger - self.mainQueue = mainQueue - self.bgQueue = bgQueue - } - - public var messenger: Messenger - public var mainQueue: AnySchedulerOf<DispatchQueue> - public var bgQueue: AnySchedulerOf<DispatchQueue> -} - -#if DEBUG -extension WelcomeEnvironment { - public static let unimplemented = WelcomeEnvironment( - messenger: .unimplemented, - mainQueue: .unimplemented, - bgQueue: .unimplemented - ) -} -#endif - -public let welcomeReducer = Reducer<WelcomeState, WelcomeAction, WelcomeEnvironment> -{ state, action, env in - switch action { - case .newAccountTapped: - state.isCreatingAccount = true - state.failure = nil - return .future { fulfill in - do { - try env.messenger.create() - fulfill(.success(.finished)) - } - catch { - fulfill(.success(.failed(error.localizedDescription))) - } - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .restoreTapped: - return .none - - case .finished: - state.isCreatingAccount = false - state.failure = nil - return .none - - case .failed(let failure): - state.isCreatingAccount = false - state.failure = failure - return .none - } -} diff --git a/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeView.swift b/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeView.swift index 64396fe1b6392267dc61efae6a8a73997698cd0c..0b4597e1de1bb3b803f1e1f2d76e8d7cc09022fd 100644 --- a/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeView.swift +++ b/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeView.swift @@ -3,14 +3,14 @@ import ComposableArchitecture import SwiftUI public struct WelcomeView: View { - public init(store: Store<WelcomeState, WelcomeAction>) { + public init(store: StoreOf<WelcomeComponent>) { self.store = store } - let store: Store<WelcomeState, WelcomeAction> + let store: StoreOf<WelcomeComponent> struct ViewState: Equatable { - init(_ state: WelcomeState) { + init(_ state: WelcomeComponent.State) { isCreatingAccount = state.isCreatingAccount failure = state.failure } @@ -69,9 +69,8 @@ public struct WelcomeView: View { public struct WelcomeView_Previews: PreviewProvider { public static var previews: some View { WelcomeView(store: Store( - initialState: WelcomeState(), - reducer: .empty, - environment: () + initialState: WelcomeComponent.State(), + reducer: EmptyReducer() )) } } diff --git a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift b/Examples/xx-messenger/Tests/AppFeatureTests/AppComponentTests.swift similarity index 56% rename from Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift rename to Examples/xx-messenger/Tests/AppFeatureTests/AppComponentTests.swift index 5ec16fc51cbb5f6b7ca0ce23696860b4460a8df3..7533666ecc857876698f0478e688257cb2c501e6 100644 --- a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift +++ b/Examples/xx-messenger/Tests/AppFeatureTests/AppComponentTests.swift @@ -8,37 +8,59 @@ import XCTest import XXClient @testable import AppFeature -final class AppFeatureTests: XCTestCase { +final class AppComponentTests: XCTestCase { + func testSetupLogging() { + var actions: [Action] = [] + + let store = TestStore( + initialState: AppComponent.State(), + reducer: AppComponent() + ) + store.dependencies.app.messenger.setLogLevel.run = { level in + actions.append(.didSetLogLevel(level)) + return true + } + store.dependencies.app.messenger.startLogging.run = { + actions.append(.didStartLogging) + } + + store.send(.setupLogging) + + XCTAssertNoDifference(actions, [ + .didSetLogLevel(.debug), + .didStartLogging, + ]) + } + func testStartWithoutMessengerCreated() { var actions: [Action]! let store = TestStore( - initialState: AppState(), - reducer: appReducer, - environment: .unimplemented + initialState: AppComponent.State(), + reducer: AppComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.dbManager.hasDB.run = { false } - store.environment.messenger.isLoaded.run = { false } - store.environment.messenger.isCreated.run = { false } - store.environment.dbManager.makeDB.run = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.dbManager.hasDB.run = { false } + store.dependencies.app.messenger.isLoaded.run = { false } + store.dependencies.app.messenger.isCreated.run = { false } + store.dependencies.app.dbManager.makeDB.run = { actions.append(.didMakeDB) } - store.environment.authHandler.run = { _ in + store.dependencies.app.authHandler.run = { _ in actions.append(.didStartAuthHandler) return Cancellable {} } - store.environment.messageListener.run = { _ in + store.dependencies.app.messageListener.run = { _ in actions.append(.didStartMessageListener) return Cancellable {} } - store.environment.receiveFileHandler.run = { _ in + store.dependencies.app.receiveFileHandler.run = { _ in actions.append(.didStartReceiveFileHandler) return Cancellable {} } - store.environment.messenger.registerBackupCallback.run = { _ in + store.dependencies.app.messenger.registerBackupCallback.run = { _ in actions.append(.didRegisterBackupCallback) return Cancellable {} } @@ -46,8 +68,8 @@ final class AppFeatureTests: XCTestCase { actions = [] store.send(.start) - store.receive(.set(\.$screen, .welcome(WelcomeState()))) { - $0.screen = .welcome(WelcomeState()) + store.receive(.set(\.$screen, .welcome(WelcomeComponent.State()))) { + $0.screen = .welcome(WelcomeComponent.State()) } XCTAssertNoDifference(actions, [ .didMakeDB, @@ -64,35 +86,34 @@ final class AppFeatureTests: XCTestCase { var actions: [Action]! let store = TestStore( - initialState: AppState(), - reducer: appReducer, - environment: .unimplemented + initialState: AppComponent.State(), + reducer: AppComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.dbManager.hasDB.run = { false } - store.environment.messenger.isLoaded.run = { false } - store.environment.messenger.isCreated.run = { true } - store.environment.dbManager.makeDB.run = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.dbManager.hasDB.run = { false } + store.dependencies.app.messenger.isLoaded.run = { false } + store.dependencies.app.messenger.isCreated.run = { true } + store.dependencies.app.dbManager.makeDB.run = { actions.append(.didMakeDB) } - store.environment.messenger.load.run = { + store.dependencies.app.messenger.load.run = { actions.append(.didLoadMessenger) } - store.environment.authHandler.run = { _ in + store.dependencies.app.authHandler.run = { _ in actions.append(.didStartAuthHandler) return Cancellable {} } - store.environment.messageListener.run = { _ in + store.dependencies.app.messageListener.run = { _ in actions.append(.didStartMessageListener) return Cancellable {} } - store.environment.receiveFileHandler.run = { _ in + store.dependencies.app.receiveFileHandler.run = { _ in actions.append(.didStartReceiveFileHandler) return Cancellable {} } - store.environment.messenger.registerBackupCallback.run = { _ in + store.dependencies.app.messenger.registerBackupCallback.run = { _ in actions.append(.didRegisterBackupCallback) return Cancellable {} } @@ -100,8 +121,8 @@ final class AppFeatureTests: XCTestCase { actions = [] store.send(.start) - store.receive(.set(\.$screen, .home(HomeState()))) { - $0.screen = .home(HomeState()) + store.receive(.set(\.$screen, .home(HomeComponent.State()))) { + $0.screen = .home(HomeComponent.State()) } XCTAssertNoDifference(actions, [ .didMakeDB, @@ -119,34 +140,33 @@ final class AppFeatureTests: XCTestCase { var actions: [Action]! let store = TestStore( - initialState: AppState( - screen: .welcome(WelcomeState()) + initialState: AppComponent.State( + screen: .welcome(WelcomeComponent.State()) ), - reducer: appReducer, - environment: .unimplemented + reducer: AppComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.dbManager.hasDB.run = { true } - store.environment.messenger.isLoaded.run = { false } - store.environment.messenger.isCreated.run = { true } - store.environment.messenger.load.run = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.dbManager.hasDB.run = { true } + store.dependencies.app.messenger.isLoaded.run = { false } + store.dependencies.app.messenger.isCreated.run = { true } + store.dependencies.app.messenger.load.run = { actions.append(.didLoadMessenger) } - store.environment.authHandler.run = { _ in + store.dependencies.app.authHandler.run = { _ in actions.append(.didStartAuthHandler) return Cancellable {} } - store.environment.messageListener.run = { _ in + store.dependencies.app.messageListener.run = { _ in actions.append(.didStartMessageListener) return Cancellable {} } - store.environment.receiveFileHandler.run = { _ in + store.dependencies.app.receiveFileHandler.run = { _ in actions.append(.didStartReceiveFileHandler) return Cancellable {} } - store.environment.messenger.registerBackupCallback.run = { _ in + store.dependencies.app.messenger.registerBackupCallback.run = { _ in actions.append(.didRegisterBackupCallback) return Cancellable {} } @@ -156,8 +176,8 @@ final class AppFeatureTests: XCTestCase { $0.screen = .loading } - store.receive(.set(\.$screen, .home(HomeState()))) { - $0.screen = .home(HomeState()) + store.receive(.set(\.$screen, .home(HomeComponent.State()))) { + $0.screen = .home(HomeComponent.State()) } XCTAssertNoDifference(actions, [ .didStartAuthHandler, @@ -174,34 +194,33 @@ final class AppFeatureTests: XCTestCase { var actions: [Action]! let store = TestStore( - initialState: AppState( - screen: .restore(RestoreState()) + initialState: AppComponent.State( + screen: .restore(RestoreComponent.State()) ), - reducer: appReducer, - environment: .unimplemented + reducer: AppComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.dbManager.hasDB.run = { true } - store.environment.messenger.isLoaded.run = { false } - store.environment.messenger.isCreated.run = { true } - store.environment.messenger.load.run = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.dbManager.hasDB.run = { true } + store.dependencies.app.messenger.isLoaded.run = { false } + store.dependencies.app.messenger.isCreated.run = { true } + store.dependencies.app.messenger.load.run = { actions.append(.didLoadMessenger) } - store.environment.authHandler.run = { _ in + store.dependencies.app.authHandler.run = { _ in actions.append(.didStartAuthHandler) return Cancellable {} } - store.environment.messageListener.run = { _ in + store.dependencies.app.messageListener.run = { _ in actions.append(.didStartMessageListener) return Cancellable {} } - store.environment.receiveFileHandler.run = { _ in + store.dependencies.app.receiveFileHandler.run = { _ in actions.append(.didStartReceiveFileHandler) return Cancellable {} } - store.environment.messenger.registerBackupCallback.run = { _ in + store.dependencies.app.messenger.registerBackupCallback.run = { _ in actions.append(.didRegisterBackupCallback) return Cancellable {} } @@ -211,8 +230,8 @@ final class AppFeatureTests: XCTestCase { $0.screen = .loading } - store.receive(.set(\.$screen, .home(HomeState()))) { - $0.screen = .home(HomeState()) + store.receive(.set(\.$screen, .home(HomeComponent.State()))) { + $0.screen = .home(HomeComponent.State()) } XCTAssertNoDifference(actions, [ .didStartAuthHandler, @@ -229,31 +248,30 @@ final class AppFeatureTests: XCTestCase { var actions: [Action]! let store = TestStore( - initialState: AppState( - screen: .home(HomeState()) + initialState: AppComponent.State( + screen: .home(HomeComponent.State()) ), - reducer: appReducer, - environment: .unimplemented + reducer: AppComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.dbManager.hasDB.run = { true } - store.environment.messenger.isLoaded.run = { false } - store.environment.messenger.isCreated.run = { false } - store.environment.authHandler.run = { _ in + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.dbManager.hasDB.run = { true } + store.dependencies.app.messenger.isLoaded.run = { false } + store.dependencies.app.messenger.isCreated.run = { false } + store.dependencies.app.authHandler.run = { _ in actions.append(.didStartAuthHandler) return Cancellable {} } - store.environment.messageListener.run = { _ in + store.dependencies.app.messageListener.run = { _ in actions.append(.didStartMessageListener) return Cancellable {} } - store.environment.receiveFileHandler.run = { _ in + store.dependencies.app.receiveFileHandler.run = { _ in actions.append(.didStartReceiveFileHandler) return Cancellable {} } - store.environment.messenger.registerBackupCallback.run = { _ in + store.dependencies.app.messenger.registerBackupCallback.run = { _ in actions.append(.didRegisterBackupCallback) return Cancellable {} } @@ -263,8 +281,8 @@ final class AppFeatureTests: XCTestCase { $0.screen = .loading } - store.receive(.set(\.$screen, .welcome(WelcomeState()))) { - $0.screen = .welcome(WelcomeState()) + store.receive(.set(\.$screen, .welcome(WelcomeComponent.State()))) { + $0.screen = .welcome(WelcomeComponent.State()) } XCTAssertNoDifference(actions, [ .didStartAuthHandler, @@ -278,15 +296,14 @@ final class AppFeatureTests: XCTestCase { func testWelcomeRestoreTapped() { let store = TestStore( - initialState: AppState( - screen: .welcome(WelcomeState()) + initialState: AppComponent.State( + screen: .welcome(WelcomeComponent.State()) ), - reducer: appReducer, - environment: .unimplemented + reducer: AppComponent() ) store.send(.welcome(.restoreTapped)) { - $0.screen = .restore(RestoreState()) + $0.screen = .restore(RestoreComponent.State()) } } @@ -294,11 +311,10 @@ final class AppFeatureTests: XCTestCase { let failure = "Something went wrong" let store = TestStore( - initialState: AppState( - screen: .welcome(WelcomeState()) + initialState: AppComponent.State( + screen: .welcome(WelcomeComponent.State()) ), - reducer: appReducer, - environment: .unimplemented + reducer: AppComponent() ) store.send(.welcome(.failed(failure))) { @@ -311,15 +327,14 @@ final class AppFeatureTests: XCTestCase { let error = Failure() let store = TestStore( - initialState: AppState(), - reducer: appReducer, - environment: .unimplemented + initialState: AppComponent.State(), + reducer: AppComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.dbManager.hasDB.run = { false } - store.environment.dbManager.makeDB.run = { throw error } + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.dbManager.hasDB.run = { false } + store.dependencies.app.dbManager.makeDB.run = { throw error } store.send(.start) @@ -337,30 +352,29 @@ final class AppFeatureTests: XCTestCase { var actions: [Action]! let store = TestStore( - initialState: AppState(), - reducer: appReducer, - environment: .unimplemented + initialState: AppComponent.State(), + reducer: AppComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.dbManager.hasDB.run = { true } - store.environment.messenger.isLoaded.run = { false } - store.environment.messenger.isCreated.run = { true } - store.environment.messenger.load.run = { throw error } - store.environment.authHandler.run = { _ in + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.dbManager.hasDB.run = { true } + store.dependencies.app.messenger.isLoaded.run = { false } + store.dependencies.app.messenger.isCreated.run = { true } + store.dependencies.app.messenger.load.run = { throw error } + store.dependencies.app.authHandler.run = { _ in actions.append(.didStartAuthHandler) return Cancellable {} } - store.environment.messageListener.run = { _ in + store.dependencies.app.messageListener.run = { _ in actions.append(.didStartMessageListener) return Cancellable {} } - store.environment.receiveFileHandler.run = { _ in + store.dependencies.app.receiveFileHandler.run = { _ in actions.append(.didStartReceiveFileHandler) return Cancellable {} } - store.environment.messenger.registerBackupCallback.run = { _ in + store.dependencies.app.messenger.registerBackupCallback.run = { _ in actions.append(.didRegisterBackupCallback) return Cancellable {} } @@ -390,56 +404,55 @@ final class AppFeatureTests: XCTestCase { var backupCallback: [UpdateBackupFunc] = [] let store = TestStore( - initialState: AppState(), - reducer: appReducer, - environment: .unimplemented + initialState: AppComponent.State(), + reducer: AppComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.dbManager.hasDB.run = { true } - store.environment.messenger.isLoaded.run = { true } - store.environment.messenger.isCreated.run = { true } - store.environment.authHandler.run = { onError in + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.dbManager.hasDB.run = { true } + store.dependencies.app.messenger.isLoaded.run = { true } + store.dependencies.app.messenger.isCreated.run = { true } + store.dependencies.app.authHandler.run = { onError in authHandlerOnError.append(onError) actions.append(.didStartAuthHandler) return Cancellable { actions.append(.didCancelAuthHandler) } } - store.environment.messageListener.run = { onError in + store.dependencies.app.messageListener.run = { onError in messageListenerOnError.append(onError) actions.append(.didStartMessageListener) return Cancellable { actions.append(.didCancelMessageListener) } } - store.environment.receiveFileHandler.run = { onError in + store.dependencies.app.receiveFileHandler.run = { onError in fileHandlerOnError.append(onError) actions.append(.didStartReceiveFileHandler) return Cancellable { actions.append(.didCancelReceiveFileHandler) } } - store.environment.messenger.registerBackupCallback.run = { callback in + store.dependencies.app.messenger.registerBackupCallback.run = { callback in backupCallback.append(callback) actions.append(.didRegisterBackupCallback) return Cancellable { actions.append(.didCancelBackupCallback) } } - store.environment.log.run = { msg, _, _, _ in + store.dependencies.app.log.run = { msg, _, _, _ in actions.append(.didLog(msg)) } - store.environment.backupStorage.store = { data in + store.dependencies.app.backupStorage.store = { data in actions.append(.didStoreBackup(data)) } actions = [] store.send(.start) - store.receive(.set(\.$screen, .home(HomeState()))) { - $0.screen = .home(HomeState()) + store.receive(.set(\.$screen, .home(HomeComponent.State()))) { + $0.screen = .home(HomeComponent.State()) } XCTAssertNoDifference(actions, [ .didStartAuthHandler, @@ -453,8 +466,8 @@ final class AppFeatureTests: XCTestCase { $0.screen = .loading } - store.receive(.set(\.$screen, .home(HomeState()))) { - $0.screen = .home(HomeState()) + store.receive(.set(\.$screen, .home(HomeComponent.State()))) { + $0.screen = .home(HomeComponent.State()) } XCTAssertNoDifference(actions, [ .didCancelAuthHandler, @@ -524,4 +537,6 @@ private enum Action: Equatable { case didCancelBackupCallback case didLog(Logger.Message) case didStoreBackup(Data) + case didSetLogLevel(LogLevel) + case didStartLogging } diff --git a/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift b/Examples/xx-messenger/Tests/BackupFeatureTests/BackupComponentTests.swift similarity index 70% rename from Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift rename to Examples/xx-messenger/Tests/BackupFeatureTests/BackupComponentTests.swift index d0c69eb87e2efea29f635fce84656b04d2e35cba..e409a1b9241f2a342f77fc3dca23174ca06c1c15 100644 --- a/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift +++ b/Examples/xx-messenger/Tests/BackupFeatureTests/BackupComponentTests.swift @@ -14,19 +14,18 @@ final class BackupFeatureTests: XCTestCase { ) let store = TestStore( - initialState: BackupState(), - reducer: backupReducer, - environment: .unimplemented + initialState: BackupComponent.State(), + reducer: BackupComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.isBackupRunning.run = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.isBackupRunning.run = { isBackupRunning.removeFirst() } - store.environment.backupStorage.stored = { + store.dependencies.app.backupStorage.stored = { storedBackup } - store.environment.backupStorage.observe = { + store.dependencies.app.backupStorage.observe = { let id = UUID() observers[id] = $0 return Cancellable { observers[id] = nil } @@ -68,22 +67,21 @@ final class BackupFeatureTests: XCTestCase { let passphrase = "backup-password" let store = TestStore( - initialState: BackupState(), - reducer: backupReducer, - environment: .unimplemented + initialState: BackupComponent.State(), + reducer: BackupComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.myContact.run = { includeFacts in + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.myContact.run = { includeFacts in actions.append(.didGetMyContact(includingFacts: includeFacts)) var contact = Contact.unimplemented("contact-data".data(using: .utf8)!) contact.getFactsFromContact.run = { _ in [Fact(type: .username, value: username)] } return contact } - store.environment.messenger.startBackup.run = { passphrase, params in + store.dependencies.app.messenger.startBackup.run = { passphrase, params in actions.append(.didStartBackup(passphrase: passphrase, params: params)) } - store.environment.messenger.isBackupRunning.run = { + store.dependencies.app.messenger.isBackupRunning.run = { isBackupRunning.removeFirst() } @@ -124,20 +122,19 @@ final class BackupFeatureTests: XCTestCase { var isBackupRunning: [Bool] = [false] let store = TestStore( - initialState: BackupState( + initialState: BackupComponent.State( passphrase: "1234" ), - reducer: backupReducer, - environment: .unimplemented + reducer: BackupComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.myContact.run = { _ in + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.myContact.run = { _ in var contact = Contact.unimplemented("contact-data".data(using: .utf8)!) contact.getFactsFromContact.run = { _ in [] } return contact } - store.environment.messenger.isBackupRunning.run = { + store.dependencies.app.messenger.isBackupRunning.run = { isBackupRunning.removeFirst() } @@ -145,7 +142,7 @@ final class BackupFeatureTests: XCTestCase { $0.isStarting = true } - let failure = BackupState.Error.contactUsernameMissing + let failure = BackupComponent.State.Error.contactUsernameMissing store.receive(.didStart(failure: failure as NSError)) { $0.isRunning = false $0.isStarting = false @@ -159,16 +156,15 @@ final class BackupFeatureTests: XCTestCase { var isBackupRunning: [Bool] = [false] let store = TestStore( - initialState: BackupState( + initialState: BackupComponent.State( passphrase: "1234" ), - reducer: backupReducer, - environment: .unimplemented + reducer: BackupComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.myContact.run = { _ in throw failure } - store.environment.messenger.isBackupRunning.run = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.myContact.run = { _ in throw failure } + store.dependencies.app.messenger.isBackupRunning.run = { isBackupRunning.removeFirst() } @@ -189,23 +185,22 @@ final class BackupFeatureTests: XCTestCase { var isBackupRunning: [Bool] = [false] let store = TestStore( - initialState: BackupState( + initialState: BackupComponent.State( passphrase: "1234" ), - reducer: backupReducer, - environment: .unimplemented + reducer: BackupComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.myContact.run = { _ in + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.myContact.run = { _ in var contact = Contact.unimplemented("data".data(using: .utf8)!) contact.getFactsFromContact.run = { _ in [Fact(type: .username, value: "username")] } return contact } - store.environment.messenger.startBackup.run = { _, _ in + store.dependencies.app.messenger.startBackup.run = { _, _ in throw failure } - store.environment.messenger.isBackupRunning.run = { + store.dependencies.app.messenger.isBackupRunning.run = { isBackupRunning.removeFirst() } @@ -225,16 +220,15 @@ final class BackupFeatureTests: XCTestCase { var isBackupRunning: [Bool] = [true] let store = TestStore( - initialState: BackupState(), - reducer: backupReducer, - environment: .unimplemented + initialState: BackupComponent.State(), + reducer: BackupComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.resumeBackup.run = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.resumeBackup.run = { actions.append(.didResumeBackup) } - store.environment.messenger.isBackupRunning.run = { + store.dependencies.app.messenger.isBackupRunning.run = { isBackupRunning.removeFirst() } @@ -260,16 +254,15 @@ final class BackupFeatureTests: XCTestCase { var isBackupRunning: [Bool] = [false] let store = TestStore( - initialState: BackupState(), - reducer: backupReducer, - environment: .unimplemented + initialState: BackupComponent.State(), + reducer: BackupComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.resumeBackup.run = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.resumeBackup.run = { throw failure } - store.environment.messenger.isBackupRunning.run = { + store.dependencies.app.messenger.isBackupRunning.run = { isBackupRunning.removeFirst() } @@ -289,19 +282,18 @@ final class BackupFeatureTests: XCTestCase { var isBackupRunning: [Bool] = [false] let store = TestStore( - initialState: BackupState(), - reducer: backupReducer, - environment: .unimplemented + initialState: BackupComponent.State(), + reducer: BackupComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.stopBackup.run = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.stopBackup.run = { actions.append(.didStopBackup) } - store.environment.messenger.isBackupRunning.run = { + store.dependencies.app.messenger.isBackupRunning.run = { isBackupRunning.removeFirst() } - store.environment.backupStorage.remove = { + store.dependencies.app.backupStorage.remove = { actions.append(.didRemoveBackup) } @@ -330,16 +322,15 @@ final class BackupFeatureTests: XCTestCase { var isBackupRunning: [Bool] = [true] let store = TestStore( - initialState: BackupState(), - reducer: backupReducer, - environment: .unimplemented + initialState: BackupComponent.State(), + reducer: BackupComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.stopBackup.run = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.stopBackup.run = { throw failure } - store.environment.messenger.isBackupRunning.run = { + store.dependencies.app.messenger.isBackupRunning.run = { isBackupRunning.removeFirst() } @@ -356,11 +347,10 @@ final class BackupFeatureTests: XCTestCase { func testAlertDismissed() { let store = TestStore( - initialState: BackupState( + initialState: BackupComponent.State( alert: .error(NSError(domain: "test", code: 0)) ), - reducer: backupReducer, - environment: .unimplemented + reducer: BackupComponent() ) store.send(.alertDismissed) { @@ -372,14 +362,13 @@ final class BackupFeatureTests: XCTestCase { let backupData = "backup-data".data(using: .utf8)! let store = TestStore( - initialState: BackupState( + initialState: BackupComponent.State( backup: .init( date: Date(), data: backupData ) ), - reducer: backupReducer, - environment: .unimplemented + reducer: BackupComponent() ) store.send(.exportTapped) { diff --git a/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift b/Examples/xx-messenger/Tests/ChatFeatureTests/ChatComponentTests.swift similarity index 79% rename from Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift rename to Examples/xx-messenger/Tests/ChatFeatureTests/ChatComponentTests.swift index 01d0ed8faa3c37b0a2005ace0b7c98d13b2e6590..c8f5952864c6f77f04ec42b53b5875a317ee37a4 100644 --- a/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ChatFeatureTests/ChatComponentTests.swift @@ -8,15 +8,14 @@ import XXMessengerClient import XXModels @testable import ChatFeature -final class ChatFeatureTests: XCTestCase { +final class ChatComponentTests: XCTestCase { func testStart() { let contactId = "contact-id".data(using: .utf8)! let myContactId = "my-contact-id".data(using: .utf8)! let store = TestStore( - initialState: ChatState(id: .contact(contactId)), - reducer: chatReducer, - environment: .unimplemented + initialState: ChatComponent.State(id: .contact(contactId)), + reducer: ChatComponent() ) var didFetchMessagesWithQuery: [XXModels.Message.Query] = [] @@ -24,9 +23,9 @@ final class ChatFeatureTests: XCTestCase { var didFetchFileTransfersWithQuery: [XXModels.FileTransfer.Query] = [] let fileTransfersPublisher = PassthroughSubject<[XXModels.FileTransfer], Error>() - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.e2e.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.getContact.run = { var contact: XXClient.Contact = .unimplemented(Data()) @@ -35,7 +34,7 @@ final class ChatFeatureTests: XCTestCase { } return e2e } - store.environment.db.run = { + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.fetchMessagesPublisher.run = { query in didFetchMessagesWithQuery.append(query) @@ -113,7 +112,7 @@ final class ChatFeatureTests: XCTestCase { sentFileTransfer, ]) - let expectedMessages = IdentifiedArrayOf<ChatState.Message>(uniqueElements: [ + let expectedMessages = IdentifiedArrayOf<ChatComponent.State.Message>(uniqueElements: [ .init( id: 1, date: Date(timeIntervalSince1970: 1), @@ -142,17 +141,16 @@ final class ChatFeatureTests: XCTestCase { func testStartFailure() { let store = TestStore( - initialState: ChatState(id: .contact("contact-id".data(using: .utf8)!)), - reducer: chatReducer, - environment: .unimplemented + initialState: ChatComponent.State(id: .contact("contact-id".data(using: .utf8)!)), + reducer: ChatComponent() ) struct Failure: Error {} let error = Failure() - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.e2e.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.getContact.run = { var contact: XXClient.Contact = .unimplemented(Data()) @@ -176,14 +174,13 @@ final class ChatFeatureTests: XCTestCase { var sendMessageCompletion: SendMessage.Completion? let store = TestStore( - initialState: ChatState(id: .contact("contact-id".data(using: .utf8)!)), - reducer: chatReducer, - environment: .unimplemented + initialState: ChatComponent.State(id: .contact("contact-id".data(using: .utf8)!)), + reducer: ChatComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.sendMessage.run = { text, recipientId, _, completion in + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.sendMessage.run = { text, recipientId, _, completion in didSendMessageWithParams.append(.init(text: text, recipientId: recipientId)) sendMessageCompletion = completion } @@ -208,17 +205,16 @@ final class ChatFeatureTests: XCTestCase { var sendMessageCompletion: SendMessage.Completion? let store = TestStore( - initialState: ChatState( + initialState: ChatComponent.State( id: .contact("contact-id".data(using: .utf8)!), text: "Hello" ), - reducer: chatReducer, - environment: .unimplemented + reducer: ChatComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.sendMessage.run = { _, _, onError, completion in + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.sendMessage.run = { _, _, onError, completion in sendMessageOnError = onError sendMessageCompletion = completion } @@ -250,14 +246,13 @@ final class ChatFeatureTests: XCTestCase { var sendImageCompletion: SendImage.Completion? let store = TestStore( - initialState: ChatState(id: .contact("contact-id".data(using: .utf8)!)), - reducer: chatReducer, - environment: .unimplemented + initialState: ChatComponent.State(id: .contact("contact-id".data(using: .utf8)!)), + reducer: ChatComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.sendImage.run = { image, recipientId, _, completion in + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.sendImage.run = { image, recipientId, _, completion in didSendImageWithParams.append(.init(image: image, recipientId: recipientId)) sendImageCompletion = completion } @@ -277,16 +272,15 @@ final class ChatFeatureTests: XCTestCase { var sendImageCompletion: SendImage.Completion? let store = TestStore( - initialState: ChatState( + initialState: ChatComponent.State( id: .contact("contact-id".data(using: .utf8)!) ), - reducer: chatReducer, - environment: .unimplemented + reducer: ChatComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.sendImage.run = { _, _, onError, completion in + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.sendImage.run = { _, _, onError, completion in sendImageOnError = onError sendImageCompletion = completion } diff --git a/Examples/xx-messenger/Tests/CheckContactAuthFeatureTests/CheckContactAuthFeatureTests.swift b/Examples/xx-messenger/Tests/CheckContactAuthFeatureTests/CheckContactAuthComponentTests.swift similarity index 80% rename from Examples/xx-messenger/Tests/CheckContactAuthFeatureTests/CheckContactAuthFeatureTests.swift rename to Examples/xx-messenger/Tests/CheckContactAuthFeatureTests/CheckContactAuthComponentTests.swift index a13645f2f29d6fd2ec3fe87353105cf3adf8df86..78031cb58485f6b81f7dee35fbf5e375c9934afa 100644 --- a/Examples/xx-messenger/Tests/CheckContactAuthFeatureTests/CheckContactAuthFeatureTests.swift +++ b/Examples/xx-messenger/Tests/CheckContactAuthFeatureTests/CheckContactAuthComponentTests.swift @@ -5,27 +5,26 @@ import XXClient import XXModels @testable import CheckContactAuthFeature -final class CheckContactAuthFeatureTests: XCTestCase { +final class CheckContactAuthComponentTests: XCTestCase { func testCheck() { 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: CheckContactAuthState( + initialState: CheckContactAuthComponent.State( contact: contact ), - reducer: checkContactAuthReducer, - environment: .unimplemented + reducer: CheckContactAuthComponent() ) var didCheckPartnerId: [Data] = [] var didBulkUpdateContactsWithQuery: [XXModels.Contact.Query] = [] var didBulkUpdateContactsWithAssignments: [XXModels.Contact.Assignments] = [] - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.e2e.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.hasAuthenticatedChannel.run = { partnerId in didCheckPartnerId.append(partnerId) @@ -33,7 +32,7 @@ final class CheckContactAuthFeatureTests: XCTestCase { } return e2e } - store.environment.db.run = { + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.bulkUpdateContacts.run = { query, assignments in didBulkUpdateContactsWithQuery.append(query) @@ -64,20 +63,19 @@ final class CheckContactAuthFeatureTests: XCTestCase { contact.getIdFromContact.run = { _ in contactId } let store = TestStore( - initialState: CheckContactAuthState( + initialState: CheckContactAuthComponent.State( contact: contact ), - reducer: checkContactAuthReducer, - environment: .unimplemented + reducer: CheckContactAuthComponent() ) var didCheckPartnerId: [Data] = [] var didBulkUpdateContactsWithQuery: [XXModels.Contact.Query] = [] var didBulkUpdateContactsWithAssignments: [XXModels.Contact.Assignments] = [] - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.e2e.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.hasAuthenticatedChannel.run = { partnerId in didCheckPartnerId.append(partnerId) @@ -85,7 +83,7 @@ final class CheckContactAuthFeatureTests: XCTestCase { } return e2e } - store.environment.db.run = { + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.bulkUpdateContacts.run = { query, assignments in didBulkUpdateContactsWithQuery.append(query) @@ -116,19 +114,18 @@ final class CheckContactAuthFeatureTests: XCTestCase { contact.getIdFromContact.run = { _ in contactId } let store = TestStore( - initialState: CheckContactAuthState( + initialState: CheckContactAuthComponent.State( contact: contact ), - reducer: checkContactAuthReducer, - environment: .unimplemented + reducer: CheckContactAuthComponent() ) struct Failure: Error {} let error = Failure() - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.e2e.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.hasAuthenticatedChannel.run = { _ in throw error } return e2e diff --git a/Examples/xx-messenger/Tests/ConfirmRequestFeatureTests/ConfirmRequestFeatureTests.swift b/Examples/xx-messenger/Tests/ConfirmRequestFeatureTests/ConfirmRequestComponentTests.swift similarity index 82% rename from Examples/xx-messenger/Tests/ConfirmRequestFeatureTests/ConfirmRequestFeatureTests.swift rename to Examples/xx-messenger/Tests/ConfirmRequestFeatureTests/ConfirmRequestComponentTests.swift index bc84224a8671219aeb7dbed33f5f8552b3302d59..263363f14b47b76f72c4945932c3e003ab200702 100644 --- a/Examples/xx-messenger/Tests/ConfirmRequestFeatureTests/ConfirmRequestFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ConfirmRequestFeatureTests/ConfirmRequestComponentTests.swift @@ -5,27 +5,26 @@ import XXClient import XXModels @testable import ConfirmRequestFeature -final class ConfirmRequestFeatureTests: XCTestCase { +final class ConfirmRequestComponentTests: 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( + initialState: ConfirmRequestComponent.State( contact: contact ), - reducer: confirmRequestReducer, - environment: .unimplemented + reducer: ConfirmRequestComponent() ) 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 = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.confirmReceivedRequest.run = { contact in didConfirmRequestFromContact.append(contact) @@ -33,7 +32,7 @@ final class ConfirmRequestFeatureTests: XCTestCase { } return e2e } - store.environment.db.run = { + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.bulkUpdateContacts.run = { query, assignments in didBulkUpdateContactsWithQuery.append(query) @@ -70,11 +69,10 @@ final class ConfirmRequestFeatureTests: XCTestCase { contact.getIdFromContact.run = { _ in contactId } let store = TestStore( - initialState: ConfirmRequestState( + initialState: ConfirmRequestComponent.State( contact: contact ), - reducer: confirmRequestReducer, - environment: .unimplemented + reducer: ConfirmRequestComponent() ) struct Failure: Error {} @@ -83,14 +81,14 @@ final class ConfirmRequestFeatureTests: XCTestCase { var didBulkUpdateContactsWithQuery: [XXModels.Contact.Query] = [] var didBulkUpdateContactsWithAssignments: [XXModels.Contact.Assignments] = [] - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.e2e.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.confirmReceivedRequest.run = { _ in throw error } return e2e } - store.environment.db.run = { + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.bulkUpdateContacts.run = { query, assignments in didBulkUpdateContactsWithQuery.append(query) diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactComponentTests.swift similarity index 70% rename from Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift rename to Examples/xx-messenger/Tests/ContactFeatureTests/ContactComponentTests.swift index eba41b7ab57d1dddfc0827747760a9bce224bdab..04caf49dbcb5205ee5dd081398983c72a6694011 100644 --- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactComponentTests.swift @@ -16,19 +16,18 @@ import XXModels final class ContactFeatureTests: XCTestCase { func testStart() { let store = TestStore( - initialState: ContactState( + initialState: ContactComponent.State( id: "contact-id".data(using: .utf8)! ), - reducer: contactReducer, - environment: .unimplemented + reducer: ContactComponent() ) var dbDidFetchContacts: [XXModels.Contact.Query] = [] let dbContactsPublisher = PassthroughSubject<[XXModels.Contact], Error>() - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.db.run = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.fetchContactsPublisher.run = { query in dbDidFetchContacts.append(query) @@ -68,20 +67,19 @@ final class ContactFeatureTests: XCTestCase { } let store = TestStore( - initialState: ContactState( + initialState: ContactComponent.State( id: "contact-id".data(using: .utf8)!, dbContact: dbContact, xxContact: xxContact ), - reducer: contactReducer, - environment: .unimplemented + reducer: ContactComponent() ) var dbDidSaveContact: [XXModels.Contact] = [] - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.db.run = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.saveContact.run = { contact in dbDidSaveContact.append(contact) @@ -104,27 +102,25 @@ final class ContactFeatureTests: XCTestCase { func testLookupTapped() { let contactId = "contact-id".data(using: .utf8)! let store = TestStore( - initialState: ContactState( + initialState: ContactComponent.State( id: contactId ), - reducer: contactReducer, - environment: .unimplemented + reducer: ContactComponent() ) store.send(.lookupTapped) { - $0.lookup = ContactLookupState(id: contactId) + $0.lookup = ContactLookupComponent.State(id: contactId) } } func testLookupDismissed() { let contactId = "contact-id".data(using: .utf8)! let store = TestStore( - initialState: ContactState( + initialState: ContactComponent.State( id: contactId, - lookup: ContactLookupState(id: contactId) + lookup: ContactLookupComponent.State(id: contactId) ), - reducer: contactReducer, - environment: .unimplemented + reducer: ContactComponent() ) store.send(.lookupDismissed) { @@ -136,12 +132,11 @@ final class ContactFeatureTests: XCTestCase { let contactId = "contact-id".data(using: .utf8)! let contact = Contact.unimplemented("contact-data".data(using: .utf8)!) let store = TestStore( - initialState: ContactState( + initialState: ContactComponent.State( id: contactId, - lookup: ContactLookupState(id: contactId) + lookup: ContactLookupComponent.State(id: contactId) ), - reducer: contactReducer, - environment: .unimplemented + reducer: ContactComponent() ) store.send(.lookup(.didLookup(contact))) { @@ -155,16 +150,15 @@ final class ContactFeatureTests: XCTestCase { dbContact.marshaled = "contact-data".data(using: .utf8)! let store = TestStore( - initialState: ContactState( + initialState: ContactComponent.State( id: dbContact.id, dbContact: dbContact ), - reducer: contactReducer, - environment: .unimplemented + reducer: ContactComponent() ) store.send(.sendRequestTapped) { - $0.sendRequest = SendRequestState(contact: .live(dbContact.marshaled!)) + $0.sendRequest = SendRequestComponent.State(contact: .live(dbContact.marshaled!)) } } @@ -172,29 +166,27 @@ final class ContactFeatureTests: XCTestCase { let xxContact = XXClient.Contact.unimplemented("contact-id".data(using: .utf8)!) let store = TestStore( - initialState: ContactState( + initialState: ContactComponent.State( id: "contact-id".data(using: .utf8)!, xxContact: xxContact ), - reducer: contactReducer, - environment: .unimplemented + reducer: ContactComponent() ) store.send(.sendRequestTapped) { - $0.sendRequest = SendRequestState(contact: xxContact) + $0.sendRequest = SendRequestComponent.State(contact: xxContact) } } func testSendRequestDismissed() { let store = TestStore( - initialState: ContactState( + initialState: ContactComponent.State( id: "contact-id".data(using: .utf8)!, - sendRequest: SendRequestState( + sendRequest: SendRequestComponent.State( contact: .unimplemented("contact-id".data(using: .utf8)!) ) ), - reducer: contactReducer, - environment: .unimplemented + reducer: ContactComponent() ) store.send(.sendRequestDismissed) { @@ -204,14 +196,13 @@ final class ContactFeatureTests: XCTestCase { func testSendRequestSucceeded() { let store = TestStore( - initialState: ContactState( + initialState: ContactComponent.State( id: "contact-id".data(using: .utf8)!, - sendRequest: SendRequestState( + sendRequest: SendRequestComponent.State( contact: .unimplemented("contact-id".data(using: .utf8)!) ) ), - reducer: contactReducer, - environment: .unimplemented + reducer: ContactComponent() ) store.send(.sendRequest(.sendSucceeded)) { @@ -222,19 +213,18 @@ final class ContactFeatureTests: XCTestCase { func testVerifyContactTapped() { let contactData = "contact-data".data(using: .utf8)! let store = TestStore( - initialState: ContactState( + initialState: ContactComponent.State( id: Data(), dbContact: XXModels.Contact( id: Data(), marshaled: contactData ) ), - reducer: contactReducer, - environment: .unimplemented + reducer: ContactComponent() ) store.send(.verifyContactTapped) { - $0.verifyContact = VerifyContactState( + $0.verifyContact = VerifyContactComponent.State( contact: .unimplemented(contactData) ) } @@ -242,14 +232,13 @@ final class ContactFeatureTests: XCTestCase { func testVerifyContactDismissed() { let store = TestStore( - initialState: ContactState( + initialState: ContactComponent.State( id: "contact-id".data(using: .utf8)!, - verifyContact: VerifyContactState( + verifyContact: VerifyContactComponent.State( contact: .unimplemented("contact-data".data(using: .utf8)!) ) ), - reducer: contactReducer, - environment: .unimplemented + reducer: ContactComponent() ) store.send(.verifyContactDismissed) { @@ -260,19 +249,18 @@ final class ContactFeatureTests: XCTestCase { func testCheckAuthTapped() { let contactData = "contact-data".data(using: .utf8)! let store = TestStore( - initialState: ContactState( + initialState: ContactComponent.State( id: Data(), dbContact: XXModels.Contact( id: Data(), marshaled: contactData ) ), - reducer: contactReducer, - environment: .unimplemented + reducer: ContactComponent() ) store.send(.checkAuthTapped) { - $0.checkAuth = CheckContactAuthState( + $0.checkAuth = CheckContactAuthComponent.State( contact: .unimplemented(contactData) ) } @@ -280,14 +268,13 @@ final class ContactFeatureTests: XCTestCase { func testCheckAuthDismissed() { let store = TestStore( - initialState: ContactState( + initialState: ContactComponent.State( id: "contact-id".data(using: .utf8)!, - checkAuth: CheckContactAuthState( + checkAuth: CheckContactAuthComponent.State( contact: .unimplemented("contact-data".data(using: .utf8)!) ) ), - reducer: contactReducer, - environment: .unimplemented + reducer: ContactComponent() ) store.send(.checkAuthDismissed) { @@ -298,19 +285,18 @@ final class ContactFeatureTests: XCTestCase { func testResetAuthTapped() { let contactData = "contact-data".data(using: .utf8)! let store = TestStore( - initialState: ContactState( + initialState: ContactComponent.State( id: Data(), dbContact: XXModels.Contact( id: Data(), marshaled: contactData ) ), - reducer: contactReducer, - environment: .unimplemented + reducer: ContactComponent() ) store.send(.resetAuthTapped) { - $0.resetAuth = ResetAuthState( + $0.resetAuth = ResetAuthComponent.State( partner: .unimplemented(contactData) ) } @@ -318,14 +304,13 @@ final class ContactFeatureTests: XCTestCase { func testResetAuthDismissed() { let store = TestStore( - initialState: ContactState( + initialState: ContactComponent.State( id: Data(), - resetAuth: ResetAuthState( + resetAuth: ResetAuthComponent.State( partner: .unimplemented(Data()) ) ), - reducer: contactReducer, - environment: .unimplemented + reducer: ContactComponent() ) store.send(.resetAuthDismissed) { @@ -336,19 +321,18 @@ final class ContactFeatureTests: XCTestCase { func testConfirmRequestTapped() { let contactData = "contact-data".data(using: .utf8)! let store = TestStore( - initialState: ContactState( + initialState: ContactComponent.State( id: Data(), dbContact: XXModels.Contact( id: Data(), marshaled: contactData ) ), - reducer: contactReducer, - environment: .unimplemented + reducer: ContactComponent() ) store.send(.confirmRequestTapped) { - $0.confirmRequest = ConfirmRequestState( + $0.confirmRequest = ConfirmRequestComponent.State( contact: .unimplemented(contactData) ) } @@ -356,14 +340,13 @@ final class ContactFeatureTests: XCTestCase { func testConfirmRequestDismissed() { let store = TestStore( - initialState: ContactState( + initialState: ContactComponent.State( id: "contact-id".data(using: .utf8)!, - confirmRequest: ConfirmRequestState( + confirmRequest: ConfirmRequestComponent.State( contact: .unimplemented("contact-data".data(using: .utf8)!) ) ), - reducer: contactReducer, - environment: .unimplemented + reducer: ContactComponent() ) store.send(.confirmRequestDismissed) { @@ -374,26 +357,24 @@ final class ContactFeatureTests: XCTestCase { func testChatTapped() { let contactId = "contact-id".data(using: .utf8)! let store = TestStore( - initialState: ContactState( + initialState: ContactComponent.State( id: contactId ), - reducer: contactReducer, - environment: .unimplemented + reducer: ContactComponent() ) store.send(.chatTapped) { - $0.chat = ChatState(id: .contact(contactId)) + $0.chat = ChatComponent.State(id: .contact(contactId)) } } func testChatDismissed() { let store = TestStore( - initialState: ContactState( + initialState: ContactComponent.State( id: "contact-id".data(using: .utf8)!, - chat: ChatState(id: .contact("contact-id".data(using: .utf8)!)) + chat: ChatComponent.State(id: .contact("contact-id".data(using: .utf8)!)) ), - reducer: contactReducer, - environment: .unimplemented + reducer: ContactComponent() ) store.send(.chatDismissed) { diff --git a/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift b/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupComponentTests.swift similarity index 62% rename from Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift rename to Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupComponentTests.swift index 76dde8d07350e240ccf86acdbef175834851b56f..26f2a0f49f319f88211bc6e50d04be6f4474cf57 100644 --- a/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupComponentTests.swift @@ -3,20 +3,19 @@ import XCTest import XXClient @testable import ContactLookupFeature -final class ContactLookupFeatureTests: XCTestCase { +final class ContactLookupComponentTests: XCTestCase { func testLookup() { let id: Data = "1234".data(using: .utf8)! var didLookupId: [Data] = [] let lookedUpContact = Contact.unimplemented("123data".data(using: .utf8)!) let store = TestStore( - initialState: ContactLookupState(id: id), - reducer: contactLookupReducer, - environment: .unimplemented + initialState: ContactLookupComponent.State(id: id), + reducer: ContactLookupComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.lookupContact.run = { id in + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.lookupContact.run = { id in didLookupId.append(id) return lookedUpContact } @@ -39,13 +38,12 @@ final class ContactLookupFeatureTests: XCTestCase { let failure = NSError(domain: "test", code: 0) let store = TestStore( - initialState: ContactLookupState(id: id), - reducer: contactLookupReducer, - environment: .unimplemented + initialState: ContactLookupComponent.State(id: id), + reducer: ContactLookupComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.lookupContact.run = { _ in throw failure } + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.lookupContact.run = { _ in throw failure } store.send(.lookupTapped) { $0.isLookingUp = true diff --git a/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsComponentTests.swift similarity index 70% rename from Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift rename to Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsComponentTests.swift index 9b3ac0d080ad8e23b8359ab8b3cf073433bed7d8..e0732e698e2756cf77de98ec25d47eea9f375757 100644 --- a/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsComponentTests.swift @@ -9,21 +9,20 @@ import XXMessengerClient import XXModels @testable import ContactsFeature -final class ContactsFeatureTests: XCTestCase { +final class ContactsComponentTests: XCTestCase { func testStart() { let store = TestStore( - initialState: ContactsState(), - reducer: contactsReducer, - environment: .unimplemented + initialState: ContactsComponent.State(), + reducer: ContactsComponent() ) let myId = "2".data(using: .utf8)! var didFetchContacts: [XXModels.Contact.Query] = [] let contactsPublisher = PassthroughSubject<[XXModels.Contact], Error>() - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.e2e.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.getContact.run = { var contact: XXClient.Contact = .unimplemented(Data()) @@ -32,7 +31,7 @@ final class ContactsFeatureTests: XCTestCase { } return e2e } - store.environment.db.run = { + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.fetchContactsPublisher.run = { query in didFetchContacts.append(query) @@ -67,28 +66,26 @@ final class ContactsFeatureTests: XCTestCase { func testSelectContact() { let store = TestStore( - initialState: ContactsState(), - reducer: contactsReducer, - environment: .unimplemented + initialState: ContactsComponent.State(), + reducer: ContactsComponent() ) let contact = XXModels.Contact(id: "id".data(using: .utf8)!) store.send(.contactSelected(contact)) { - $0.contact = ContactState(id: contact.id, dbContact: contact) + $0.contact = ContactComponent.State(id: contact.id, dbContact: contact) } } func testDismissContact() { let store = TestStore( - initialState: ContactsState( - contact: ContactState( + initialState: ContactsComponent.State( + contact: ContactComponent.State( id: "id".data(using: .utf8)!, dbContact: Contact(id: "id".data(using: .utf8)!) ) ), - reducer: contactsReducer, - environment: .unimplemented + reducer: ContactsComponent() ) store.send(.contactDismissed) { @@ -98,23 +95,21 @@ final class ContactsFeatureTests: XCTestCase { func testSelectMyContact() { let store = TestStore( - initialState: ContactsState(), - reducer: contactsReducer, - environment: .unimplemented + initialState: ContactsComponent.State(), + reducer: ContactsComponent() ) store.send(.myContactSelected) { - $0.myContact = MyContactState() + $0.myContact = MyContactComponent.State() } } func testDismissMyContact() { let store = TestStore( - initialState: ContactsState( - myContact: MyContactState() + initialState: ContactsComponent.State( + myContact: MyContactComponent.State() ), - reducer: contactsReducer, - environment: .unimplemented + reducer: ContactsComponent() ) store.send(.myContactDismissed) { diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeComponentTests.swift similarity index 59% rename from Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift rename to Examples/xx-messenger/Tests/HomeFeatureTests/HomeComponentTests.swift index 6c23ff8b718191771ce8dbf30ef4777cfd7303f9..3bd49a4283b9b4e535c40fb8c9279a5482aa3d16 100644 --- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift +++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeComponentTests.swift @@ -11,12 +11,11 @@ import XXMessengerClient import XXModels @testable import HomeFeature -final class HomeFeatureTests: XCTestCase { +final class HomeComponentTests: XCTestCase { func testMessengerStartUnregistered() { let store = TestStore( - initialState: HomeState(), - reducer: homeReducer, - environment: .unimplemented + initialState: HomeComponent.State(), + reducer: HomeComponent() ) var messengerDidStartWithTimeout: [Int] = [] @@ -24,17 +23,17 @@ final class HomeFeatureTests: XCTestCase { var messengerDidListenForMessages = 0 var messengerDidStartFileTransfer = 0 - store.environment.bgQueue = .immediate - store.environment.mainQueue = .immediate - store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } - store.environment.messenger.isConnected.run = { false } - store.environment.messenger.connect.run = { messengerDidConnect += 1 } - store.environment.messenger.isListeningForMessages.run = { false } - store.environment.messenger.listenForMessages.run = { messengerDidListenForMessages += 1 } - store.environment.messenger.isFileTransferRunning.run = { false } - store.environment.messenger.startFileTransfer.run = { messengerDidStartFileTransfer += 1 } - store.environment.messenger.isLoggedIn.run = { false } - store.environment.messenger.isRegistered.run = { false } + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.messenger.start.run = { messengerDidStartWithTimeout.append($0) } + store.dependencies.app.messenger.isConnected.run = { false } + store.dependencies.app.messenger.connect.run = { messengerDidConnect += 1 } + store.dependencies.app.messenger.isListeningForMessages.run = { false } + store.dependencies.app.messenger.listenForMessages.run = { messengerDidListenForMessages += 1 } + store.dependencies.app.messenger.isFileTransferRunning.run = { false } + store.dependencies.app.messenger.startFileTransfer.run = { messengerDidStartFileTransfer += 1 } + store.dependencies.app.messenger.isLoggedIn.run = { false } + store.dependencies.app.messenger.isRegistered.run = { false } store.send(.messenger(.start)) @@ -45,15 +44,14 @@ final class HomeFeatureTests: XCTestCase { store.receive(.networkMonitor(.stop)) store.receive(.messenger(.didStartUnregistered)) { - $0.register = RegisterState() + $0.register = RegisterComponent.State() } } func testMessengerStartRegistered() { let store = TestStore( - initialState: HomeState(), - reducer: homeReducer, - environment: .unimplemented + initialState: HomeComponent.State(), + reducer: HomeComponent() ) var messengerDidStartWithTimeout: [Int] = [] @@ -63,21 +61,21 @@ final class HomeFeatureTests: XCTestCase { var messengerDidLogIn = 0 var messengerDidResumeBackup = 0 - store.environment.bgQueue = .immediate - store.environment.mainQueue = .immediate - store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } - store.environment.messenger.isConnected.run = { false } - store.environment.messenger.connect.run = { messengerDidConnect += 1 } - store.environment.messenger.isListeningForMessages.run = { false } - store.environment.messenger.listenForMessages.run = { messengerDidListenForMessages += 1 } - store.environment.messenger.isFileTransferRunning.run = { false } - store.environment.messenger.startFileTransfer.run = { messengerDidStartFileTransfer += 1 } - store.environment.messenger.isLoggedIn.run = { false } - store.environment.messenger.isRegistered.run = { true } - store.environment.messenger.logIn.run = { messengerDidLogIn += 1 } - store.environment.messenger.isBackupRunning.run = { false } - store.environment.messenger.resumeBackup.run = { messengerDidResumeBackup += 1 } - store.environment.messenger.cMix.get = { + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.messenger.start.run = { messengerDidStartWithTimeout.append($0) } + store.dependencies.app.messenger.isConnected.run = { false } + store.dependencies.app.messenger.connect.run = { messengerDidConnect += 1 } + store.dependencies.app.messenger.isListeningForMessages.run = { false } + store.dependencies.app.messenger.listenForMessages.run = { messengerDidListenForMessages += 1 } + store.dependencies.app.messenger.isFileTransferRunning.run = { false } + store.dependencies.app.messenger.startFileTransfer.run = { messengerDidStartFileTransfer += 1 } + store.dependencies.app.messenger.isLoggedIn.run = { false } + store.dependencies.app.messenger.isRegistered.run = { true } + store.dependencies.app.messenger.logIn.run = { messengerDidLogIn += 1 } + store.dependencies.app.messenger.isBackupRunning.run = { false } + store.dependencies.app.messenger.resumeBackup.run = { messengerDidResumeBackup += 1 } + store.dependencies.app.messenger.cMix.get = { var cMix: CMix = .unimplemented cMix.addHealthCallback.run = { _ in Cancellable {} } cMix.getNodeRegistrationStatus.run = { @@ -105,27 +103,26 @@ final class HomeFeatureTests: XCTestCase { func testRegisterFinished() { let store = TestStore( - initialState: HomeState( - register: RegisterState() + initialState: HomeComponent.State( + register: RegisterComponent.State() ), - reducer: homeReducer, - environment: .unimplemented + reducer: HomeComponent() ) var messengerDidStartWithTimeout: [Int] = [] var messengerDidLogIn = 0 - store.environment.bgQueue = .immediate - store.environment.mainQueue = .immediate - store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } - store.environment.messenger.isConnected.run = { true } - store.environment.messenger.isListeningForMessages.run = { true } - store.environment.messenger.isFileTransferRunning.run = { true } - store.environment.messenger.isLoggedIn.run = { false } - store.environment.messenger.isRegistered.run = { true } - store.environment.messenger.logIn.run = { messengerDidLogIn += 1 } - store.environment.messenger.isBackupRunning.run = { true } - store.environment.messenger.cMix.get = { + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.messenger.start.run = { messengerDidStartWithTimeout.append($0) } + store.dependencies.app.messenger.isConnected.run = { true } + store.dependencies.app.messenger.isListeningForMessages.run = { true } + store.dependencies.app.messenger.isFileTransferRunning.run = { true } + store.dependencies.app.messenger.isLoggedIn.run = { false } + store.dependencies.app.messenger.isRegistered.run = { true } + store.dependencies.app.messenger.logIn.run = { messengerDidLogIn += 1 } + store.dependencies.app.messenger.isBackupRunning.run = { true } + store.dependencies.app.messenger.cMix.get = { var cMix: CMix = .unimplemented cMix.addHealthCallback.run = { _ in Cancellable {} } cMix.getNodeRegistrationStatus.run = { @@ -153,17 +150,16 @@ final class HomeFeatureTests: XCTestCase { func testMessengerStartFailure() { let store = TestStore( - initialState: HomeState(), - reducer: homeReducer, - environment: .unimplemented + initialState: HomeComponent.State(), + reducer: HomeComponent() ) struct Failure: Error {} let error = Failure() - store.environment.bgQueue = .immediate - store.environment.mainQueue = .immediate - store.environment.messenger.start.run = { _ in throw error } + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.messenger.start.run = { _ in throw error } store.send(.messenger(.start)) @@ -175,19 +171,18 @@ final class HomeFeatureTests: XCTestCase { func testMessengerStartConnectFailure() { let store = TestStore( - initialState: HomeState(), - reducer: homeReducer, - environment: .unimplemented + initialState: HomeComponent.State(), + reducer: HomeComponent() ) 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.dependencies.app.bgQueue = .immediate + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.messenger.start.run = { _ in } + store.dependencies.app.messenger.isConnected.run = { false } + store.dependencies.app.messenger.connect.run = { throw error } store.send(.messenger(.start)) @@ -199,22 +194,21 @@ final class HomeFeatureTests: XCTestCase { func testMessengerStartIsRegisteredFailure() { let store = TestStore( - initialState: HomeState(), - reducer: homeReducer, - environment: .unimplemented + initialState: HomeComponent.State(), + reducer: HomeComponent() ) 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.isListeningForMessages.run = { true } - store.environment.messenger.isFileTransferRunning.run = { true } - store.environment.messenger.isLoggedIn.run = { false } - store.environment.messenger.isRegistered.run = { throw error } + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.messenger.start.run = { _ in } + store.dependencies.app.messenger.isConnected.run = { true } + store.dependencies.app.messenger.isListeningForMessages.run = { true } + store.dependencies.app.messenger.isFileTransferRunning.run = { true } + store.dependencies.app.messenger.isLoggedIn.run = { false } + store.dependencies.app.messenger.isRegistered.run = { throw error } store.send(.messenger(.start)) @@ -226,23 +220,22 @@ final class HomeFeatureTests: XCTestCase { func testMessengerStartLogInFailure() { let store = TestStore( - initialState: HomeState(), - reducer: homeReducer, - environment: .unimplemented + initialState: HomeComponent.State(), + reducer: HomeComponent() ) 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.isListeningForMessages.run = { true } - store.environment.messenger.isFileTransferRunning.run = { true } - store.environment.messenger.isLoggedIn.run = { false } - store.environment.messenger.isRegistered.run = { true } - store.environment.messenger.logIn.run = { throw error } + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.messenger.start.run = { _ in } + store.dependencies.app.messenger.isConnected.run = { true } + store.dependencies.app.messenger.isListeningForMessages.run = { true } + store.dependencies.app.messenger.isFileTransferRunning.run = { true } + store.dependencies.app.messenger.isLoggedIn.run = { false } + store.dependencies.app.messenger.isRegistered.run = { true } + store.dependencies.app.messenger.logIn.run = { throw error } store.send(.messenger(.start)) @@ -254,9 +247,8 @@ final class HomeFeatureTests: XCTestCase { func testNetworkMonitorStart() { let store = TestStore( - initialState: HomeState(), - reducer: homeReducer, - environment: .unimplemented + initialState: HomeComponent.State(), + reducer: HomeComponent() ) let bgQueue = DispatchQueue.test @@ -271,9 +263,9 @@ final class HomeFeatureTests: XCTestCase { .init(registered: 2, total: 12), ] - store.environment.bgQueue = bgQueue.eraseToAnyScheduler() - store.environment.mainQueue = mainQueue.eraseToAnyScheduler() - store.environment.messenger.cMix.get = { + store.dependencies.app.bgQueue = bgQueue.eraseToAnyScheduler() + store.dependencies.app.mainQueue = mainQueue.eraseToAnyScheduler() + store.dependencies.app.messenger.cMix.get = { var cMix: CMix = .unimplemented cMix.addHealthCallback.run = { callback in cMixDidAddHealthCallback.append(callback) @@ -339,9 +331,8 @@ final class HomeFeatureTests: XCTestCase { func testAccountDeletion() { let store = TestStore( - initialState: HomeState(), - reducer: homeReducer, - environment: .unimplemented + initialState: HomeComponent.State(), + reducer: HomeComponent() ) var dbDidFetchContacts: [XXModels.Contact.Query] = [] @@ -349,9 +340,9 @@ final class HomeFeatureTests: XCTestCase { var messengerDidDestroy = 0 var didRemoveDB = 0 - store.environment.bgQueue = .immediate - store.environment.mainQueue = .immediate - store.environment.messenger.e2e.get = { + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.getContact.run = { var contact = Contact.unimplemented("contact-data".data(using: .utf8)!) @@ -360,7 +351,7 @@ final class HomeFeatureTests: XCTestCase { } return e2e } - store.environment.dbManager.getDB.run = { + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.fetchContacts.run = { query in dbDidFetchContacts.append(query) @@ -374,17 +365,17 @@ final class HomeFeatureTests: XCTestCase { } return db } - store.environment.dbManager.removeDB.run = { + store.dependencies.app.dbManager.removeDB.run = { didRemoveDB += 1 } - store.environment.messenger.ud.get = { + store.dependencies.app.messenger.ud.get = { var ud: UserDiscovery = .unimplemented ud.permanentDeleteAccount.run = { usernameFact in udDidPermanentDeleteAccount.append(usernameFact) } return ud } - store.environment.messenger.destroy.run = { + store.dependencies.app.messenger.destroy.run = { messengerDidDestroy += 1 } @@ -412,17 +403,16 @@ final class HomeFeatureTests: XCTestCase { func testAccountDeletionFailure() { let store = TestStore( - initialState: HomeState(), - reducer: homeReducer, - environment: .unimplemented + initialState: HomeComponent.State(), + reducer: HomeComponent() ) struct Failure: Error {} let error = Failure() - store.environment.bgQueue = .immediate - store.environment.mainQueue = .immediate - store.environment.messenger.e2e.get = { + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.getContact.run = { var contact = Contact.unimplemented("contact-data".data(using: .utf8)!) @@ -444,11 +434,10 @@ final class HomeFeatureTests: XCTestCase { func testDidDismissAlert() { let store = TestStore( - initialState: HomeState( + initialState: HomeComponent.State( alert: AlertState(title: TextState("")) ), - reducer: homeReducer, - environment: .unimplemented + reducer: HomeComponent() ) store.send(.didDismissAlert) { @@ -458,11 +447,10 @@ final class HomeFeatureTests: XCTestCase { func testDidDismissRegister() { let store = TestStore( - initialState: HomeState( - register: RegisterState() + initialState: HomeComponent.State( + register: RegisterComponent.State() ), - reducer: homeReducer, - environment: .unimplemented + reducer: HomeComponent() ) store.send(.didDismissRegister) { @@ -472,23 +460,21 @@ final class HomeFeatureTests: XCTestCase { func testUserSearchButtonTapped() { let store = TestStore( - initialState: HomeState(), - reducer: homeReducer, - environment: .unimplemented + initialState: HomeComponent.State(), + reducer: HomeComponent() ) store.send(.userSearchButtonTapped) { - $0.userSearch = UserSearchState() + $0.userSearch = UserSearchComponent.State() } } func testDidDismissUserSearch() { let store = TestStore( - initialState: HomeState( - userSearch: UserSearchState() + initialState: HomeComponent.State( + userSearch: UserSearchComponent.State() ), - reducer: homeReducer, - environment: .unimplemented + reducer: HomeComponent() ) store.send(.didDismissUserSearch) { @@ -498,23 +484,21 @@ final class HomeFeatureTests: XCTestCase { func testContactsButtonTapped() { let store = TestStore( - initialState: HomeState(), - reducer: homeReducer, - environment: .unimplemented + initialState: HomeComponent.State(), + reducer: HomeComponent() ) store.send(.contactsButtonTapped) { - $0.contacts = ContactsState() + $0.contacts = ContactsComponent.State() } } func testDidDismissContacts() { let store = TestStore( - initialState: HomeState( - contacts: ContactsState() + initialState: HomeComponent.State( + contacts: ContactsComponent.State() ), - reducer: homeReducer, - environment: .unimplemented + reducer: HomeComponent() ) store.send(.didDismissContacts) { @@ -524,23 +508,21 @@ final class HomeFeatureTests: XCTestCase { func testBackupButtonTapped() { let store = TestStore( - initialState: HomeState(), - reducer: homeReducer, - environment: .unimplemented + initialState: HomeComponent.State(), + reducer: HomeComponent() ) store.send(.backupButtonTapped) { - $0.backup = BackupState() + $0.backup = BackupComponent.State() } } func testDidDismissBackup() { let store = TestStore( - initialState: HomeState( - backup: BackupState() + initialState: HomeComponent.State( + backup: BackupComponent.State() ), - reducer: homeReducer, - environment: .unimplemented + reducer: HomeComponent() ) store.send(.didDismissBackup) { diff --git a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactComponentTests.swift similarity index 80% rename from Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift rename to Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactComponentTests.swift index 830e97156363b151fbff21276169d8dfc2095720..358db1c2bd3a6633072249d10d6efacf245366f3 100644 --- a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift +++ b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactComponentTests.swift @@ -7,22 +7,21 @@ import XXMessengerClient import XXModels @testable import MyContactFeature -final class MyContactFeatureTests: XCTestCase { +final class MyContactComponentTests: XCTestCase { func testStart() { let contactId = "contact-id".data(using: .utf8)! let store = TestStore( - initialState: MyContactState(), - reducer: myContactReducer, - environment: .unimplemented + initialState: MyContactComponent.State(), + reducer: MyContactComponent() ) var dbDidFetchContacts: [XXModels.Contact.Query] = [] let dbContactsPublisher = PassthroughSubject<[XXModels.Contact], Error>() - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.e2e.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.getContact.run = { var contact: XXClient.Contact = .unimplemented(Data()) @@ -31,7 +30,7 @@ final class MyContactFeatureTests: XCTestCase { } return e2e } - store.environment.db.run = { + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.fetchContactsPublisher.run = { query in dbDidFetchContacts.append(query) @@ -65,14 +64,13 @@ final class MyContactFeatureTests: XCTestCase { var didSendRegisterFact: [Fact] = [] let store = TestStore( - initialState: MyContactState(), - reducer: myContactReducer, - environment: .unimplemented + initialState: MyContactComponent.State(), + reducer: MyContactComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.ud.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.ud.get = { var ud: UserDiscovery = .unimplemented ud.sendRegisterFact.run = { fact in didSendRegisterFact.append(fact) @@ -110,14 +108,13 @@ final class MyContactFeatureTests: XCTestCase { let failure = Failure() let store = TestStore( - initialState: MyContactState(), - reducer: myContactReducer, - environment: .unimplemented + initialState: MyContactComponent.State(), + reducer: MyContactComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.ud.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.ud.get = { var ud: UserDiscovery = .unimplemented ud.sendRegisterFact.run = { _ in throw failure } return ud @@ -149,17 +146,16 @@ final class MyContactFeatureTests: XCTestCase { var didSaveContact: [XXModels.Contact] = [] let store = TestStore( - initialState: MyContactState( + initialState: MyContactComponent.State( email: email, emailConfirmationID: confirmationID ), - reducer: myContactReducer, - environment: .unimplemented + reducer: MyContactComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.ud.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.ud.get = { var ud: UserDiscovery = .unimplemented ud.confirmFact.run = { id, code in didConfirmWithID.append(id) @@ -167,7 +163,7 @@ final class MyContactFeatureTests: XCTestCase { } return ud } - store.environment.messenger.e2e.get = { + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.getContact.run = { var contact: XXClient.Contact = .unimplemented(Data()) @@ -176,7 +172,7 @@ final class MyContactFeatureTests: XCTestCase { } return e2e } - store.environment.db.run = { + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.fetchContacts.run = { query in didFetchContacts.append(query) @@ -228,16 +224,15 @@ final class MyContactFeatureTests: XCTestCase { let failure = Failure() let store = TestStore( - initialState: MyContactState( + initialState: MyContactComponent.State( emailConfirmationID: "123" ), - reducer: myContactReducer, - environment: .unimplemented + reducer: MyContactComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.ud.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.ud.get = { var ud: UserDiscovery = .unimplemented ud.confirmFact.run = { _, _ in throw failure } return ud @@ -266,21 +261,20 @@ final class MyContactFeatureTests: XCTestCase { var didSaveContact: [XXModels.Contact] = [] let store = TestStore( - initialState: MyContactState( + initialState: MyContactComponent.State( contact: dbContact ), - reducer: myContactReducer, - environment: .unimplemented + reducer: MyContactComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.ud.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.ud.get = { var ud: UserDiscovery = .unimplemented ud.removeFact.run = { didRemoveFact.append($0) } return ud } - store.environment.messenger.e2e.get = { + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.getContact.run = { var contact: XXClient.Contact = .unimplemented(Data()) @@ -289,7 +283,7 @@ final class MyContactFeatureTests: XCTestCase { } return e2e } - store.environment.db.run = { + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.fetchContacts.run = { query in didFetchContacts.append(query) @@ -322,16 +316,15 @@ final class MyContactFeatureTests: XCTestCase { let failure = Failure() let store = TestStore( - initialState: MyContactState( + initialState: MyContactComponent.State( contact: .init(id: Data(), email: "test@email.com") ), - reducer: myContactReducer, - environment: .unimplemented + reducer: MyContactComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.ud.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.ud.get = { var ud: UserDiscovery = .unimplemented ud.removeFact.run = { _ in throw failure } return ud @@ -357,14 +350,13 @@ final class MyContactFeatureTests: XCTestCase { var didSendRegisterFact: [Fact] = [] let store = TestStore( - initialState: MyContactState(), - reducer: myContactReducer, - environment: .unimplemented + initialState: MyContactComponent.State(), + reducer: MyContactComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.ud.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.ud.get = { var ud: UserDiscovery = .unimplemented ud.sendRegisterFact.run = { fact in didSendRegisterFact.append(fact) @@ -402,14 +394,13 @@ final class MyContactFeatureTests: XCTestCase { let failure = Failure() let store = TestStore( - initialState: MyContactState(), - reducer: myContactReducer, - environment: .unimplemented + initialState: MyContactComponent.State(), + reducer: MyContactComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.ud.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.ud.get = { var ud: UserDiscovery = .unimplemented ud.sendRegisterFact.run = { _ in throw failure } return ud @@ -441,17 +432,16 @@ final class MyContactFeatureTests: XCTestCase { var didSaveContact: [XXModels.Contact] = [] let store = TestStore( - initialState: MyContactState( + initialState: MyContactComponent.State( phone: phone, phoneConfirmationID: confirmationID ), - reducer: myContactReducer, - environment: .unimplemented + reducer: MyContactComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.ud.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.ud.get = { var ud: UserDiscovery = .unimplemented ud.confirmFact.run = { id, code in didConfirmWithID.append(id) @@ -459,7 +449,7 @@ final class MyContactFeatureTests: XCTestCase { } return ud } - store.environment.messenger.e2e.get = { + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.getContact.run = { var contact: XXClient.Contact = .unimplemented(Data()) @@ -468,7 +458,7 @@ final class MyContactFeatureTests: XCTestCase { } return e2e } - store.environment.db.run = { + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.fetchContacts.run = { query in didFetchContacts.append(query) @@ -520,16 +510,15 @@ final class MyContactFeatureTests: XCTestCase { let failure = Failure() let store = TestStore( - initialState: MyContactState( + initialState: MyContactComponent.State( phoneConfirmationID: "123" ), - reducer: myContactReducer, - environment: .unimplemented + reducer: MyContactComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.ud.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.ud.get = { var ud: UserDiscovery = .unimplemented ud.confirmFact.run = { _, _ in throw failure } return ud @@ -558,21 +547,20 @@ final class MyContactFeatureTests: XCTestCase { var didSaveContact: [XXModels.Contact] = [] let store = TestStore( - initialState: MyContactState( + initialState: MyContactComponent.State( contact: dbContact ), - reducer: myContactReducer, - environment: .unimplemented + reducer: MyContactComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.ud.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.ud.get = { var ud: UserDiscovery = .unimplemented ud.removeFact.run = { didRemoveFact.append($0) } return ud } - store.environment.messenger.e2e.get = { + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.getContact.run = { var contact: XXClient.Contact = .unimplemented(Data()) @@ -581,7 +569,7 @@ final class MyContactFeatureTests: XCTestCase { } return e2e } - store.environment.db.run = { + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.fetchContacts.run = { query in didFetchContacts.append(query) @@ -614,16 +602,15 @@ final class MyContactFeatureTests: XCTestCase { let failure = Failure() let store = TestStore( - initialState: MyContactState( + initialState: MyContactComponent.State( contact: .init(id: Data(), phone: "+123456789") ), - reducer: myContactReducer, - environment: .unimplemented + reducer: MyContactComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.ud.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.ud.get = { var ud: UserDiscovery = .unimplemented ud.removeFact.run = { _ in throw failure } return ud @@ -653,14 +640,13 @@ final class MyContactFeatureTests: XCTestCase { var didSaveContact: [XXModels.Contact] = [] let store = TestStore( - initialState: MyContactState(), - reducer: myContactReducer, - environment: .unimplemented + initialState: MyContactComponent.State(), + reducer: MyContactComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.e2e.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.getContact.run = { var contact: XXClient.Contact = .unimplemented(Data()) @@ -669,7 +655,7 @@ final class MyContactFeatureTests: XCTestCase { } return e2e } - store.environment.messenger.ud.get = { + store.dependencies.app.messenger.ud.get = { var ud: UserDiscovery = .unimplemented ud.getFacts.run = { [ @@ -680,7 +666,7 @@ final class MyContactFeatureTests: XCTestCase { } return ud } - store.environment.db.run = { + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.fetchContacts.run = { query in didFetchContacts.append(query) @@ -714,14 +700,13 @@ final class MyContactFeatureTests: XCTestCase { let failure = Failure() let store = TestStore( - initialState: MyContactState(), - reducer: myContactReducer, - environment: .unimplemented + initialState: MyContactComponent.State(), + reducer: MyContactComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.e2e.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.getContact.run = { var contact: XXClient.Contact = .unimplemented(Data()) @@ -746,9 +731,8 @@ final class MyContactFeatureTests: XCTestCase { func testErrorAlert() { let store = TestStore( - initialState: MyContactState(), - reducer: myContactReducer, - environment: .unimplemented + initialState: MyContactComponent.State(), + reducer: MyContactComponent() ) let failure = "Something went wrong" diff --git a/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift b/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterComponentTests.swift similarity index 76% rename from Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift rename to Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterComponentTests.swift index e0ffc5ef4fe9a3bc5f9d9f91d288025cd1065c85..0be4647e1ffa87e38cec2d505e472548ad4a81b8 100644 --- a/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift +++ b/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterComponentTests.swift @@ -6,7 +6,7 @@ import XXMessengerClient import XXModels @testable import RegisterFeature -final class RegisterFeatureTests: XCTestCase { +final class RegisterComponentTests: XCTestCase { func testRegister() throws { let now = Date() let username = "registering-username" @@ -26,24 +26,23 @@ final class RegisterFeatureTests: XCTestCase { var dbDidSaveContact: [XXModels.Contact] = [] let store = TestStore( - initialState: RegisterState(), - reducer: registerReducer, - environment: .unimplemented + initialState: RegisterComponent.State(), + reducer: RegisterComponent() ) - store.environment.now = { now } - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.register.run = { username in + store.dependencies.app.now = { now } + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.register.run = { username in messengerDidRegisterUsername.append(username) } - store.environment.messenger.myContact.run = { includeFacts in + store.dependencies.app.messenger.myContact.run = { includeFacts in didGetMyContact.append(includeFacts) var contact = XXClient.Contact.unimplemented(myContactData) contact.getIdFromContact.run = { _ in myContactId } contact.getFactsFromContact.run = { _ in myContactFacts } return contact } - store.environment.db.run = { + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.saveContact.run = { contact in dbDidSaveContact.append(contact) @@ -87,17 +86,16 @@ final class RegisterFeatureTests: XCTestCase { let error = Error() let store = TestStore( - initialState: RegisterState(), - reducer: registerReducer, - environment: .unimplemented + initialState: RegisterComponent.State(), + reducer: RegisterComponent() ) let mainQueue = DispatchQueue.test let bgQueue = DispatchQueue.test - store.environment.mainQueue = mainQueue.eraseToAnyScheduler() - store.environment.bgQueue = bgQueue.eraseToAnyScheduler() - store.environment.db.run = { throw error } + store.dependencies.app.mainQueue = mainQueue.eraseToAnyScheduler() + store.dependencies.app.bgQueue = bgQueue.eraseToAnyScheduler() + store.dependencies.app.dbManager.getDB.run = { throw error } store.send(.registerTapped) { $0.isRegistering = true @@ -117,18 +115,17 @@ final class RegisterFeatureTests: XCTestCase { let error = Error() let store = TestStore( - initialState: RegisterState(), - reducer: registerReducer, - environment: .unimplemented + initialState: RegisterComponent.State(), + reducer: RegisterComponent() ) let mainQueue = DispatchQueue.test let bgQueue = DispatchQueue.test - store.environment.mainQueue = mainQueue.eraseToAnyScheduler() - store.environment.bgQueue = bgQueue.eraseToAnyScheduler() - store.environment.db.run = { .unimplemented } - store.environment.messenger.register.run = { _ in throw error } + store.dependencies.app.mainQueue = mainQueue.eraseToAnyScheduler() + store.dependencies.app.bgQueue = bgQueue.eraseToAnyScheduler() + store.dependencies.app.dbManager.getDB.run = { .unimplemented } + store.dependencies.app.messenger.register.run = { _ in throw error } store.send(.registerTapped) { $0.isRegistering = true @@ -162,26 +159,25 @@ final class RegisterFeatureTests: XCTestCase { var dbDidSaveContact: [XXModels.Contact] = [] let store = TestStore( - initialState: RegisterState( + initialState: RegisterComponent.State( username: username ), - reducer: registerReducer, - environment: .unimplemented + reducer: RegisterComponent() ) - store.environment.now = { now } - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.register.run = { username in + store.dependencies.app.now = { now } + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.register.run = { username in messengerDidRegisterUsername.append(username) } - store.environment.messenger.myContact.run = { includeFacts in + store.dependencies.app.messenger.myContact.run = { includeFacts in didGetMyContact.append(includeFacts) var contact = XXClient.Contact.unimplemented(myContactData) contact.getIdFromContact.run = { _ in myContactId } contact.getFactsFromContact.run = { _ in myContactFacts } return contact } - store.environment.db.run = { + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.saveContact.run = { contact in dbDidSaveContact.append(contact) @@ -207,7 +203,7 @@ final class RegisterFeatureTests: XCTestCase { ) ]) - let failure = RegisterState.Error.usernameMismatch( + let failure = RegisterComponent.State.Error.usernameMismatch( registering: username, registered: myContactUsername ) diff --git a/Examples/xx-messenger/Tests/ResetAuthFeatureTests/ResetAuthFeatureTests.swift b/Examples/xx-messenger/Tests/ResetAuthFeatureTests/ResetAuthComponentTests.swift similarity index 71% rename from Examples/xx-messenger/Tests/ResetAuthFeatureTests/ResetAuthFeatureTests.swift rename to Examples/xx-messenger/Tests/ResetAuthFeatureTests/ResetAuthComponentTests.swift index 46b5dc9e402d5c2cb71a796790b205732876a658..03ec62686846d64a50ede08c39bc25255cc64558 100644 --- a/Examples/xx-messenger/Tests/ResetAuthFeatureTests/ResetAuthFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ResetAuthFeatureTests/ResetAuthComponentTests.swift @@ -4,7 +4,7 @@ import XCTest import XXClient @testable import ResetAuthFeature -final class ResetAuthFeatureTests: XCTestCase { +final class ResetAuthComponentTests: XCTestCase { func testReset() { let partnerData = "contact-data".data(using: .utf8)! let partner = Contact.unimplemented(partnerData) @@ -12,15 +12,14 @@ final class ResetAuthFeatureTests: XCTestCase { var didResetAuthChannel: [Contact] = [] let store = TestStore( - initialState: ResetAuthState( + initialState: ResetAuthComponent.State( partner: partner ), - reducer: resetAuthReducer, - environment: .unimplemented + reducer: ResetAuthComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.e2e.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.resetAuthenticatedChannel.run = { contact in didResetAuthChannel.append(contact) @@ -46,15 +45,14 @@ final class ResetAuthFeatureTests: XCTestCase { let failure = Failure() let store = TestStore( - initialState: ResetAuthState( + initialState: ResetAuthComponent.State( partner: .unimplemented(Data()) ), - reducer: resetAuthReducer, - environment: .unimplemented + reducer: ResetAuthComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.e2e.get = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.resetAuthenticatedChannel.run = { _ in throw failure } return e2e diff --git a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreComponentTests.swift similarity index 79% rename from Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift rename to Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreComponentTests.swift index 716d7650255b277cad37fb40ad5a73c4aee5bace..8a15fb747fb0124bd972593a8a81233d38c4c89c 100644 --- a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift +++ b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreComponentTests.swift @@ -6,19 +6,18 @@ import XXMessengerClient import XXModels @testable import RestoreFeature -final class RestoreFeatureTests: XCTestCase { +final class RestoreComponentTests: XCTestCase { func testFileImport() { let fileURL = URL(string: "file-url")! var didLoadDataFromURL: [URL] = [] let dataFromURL = "data-from-url".data(using: .utf8)! let store = TestStore( - initialState: RestoreState(), - reducer: restoreReducer, - environment: .unimplemented + initialState: RestoreComponent.State(), + reducer: RestoreComponent() ) - store.environment.loadData.load = { url in + store.dependencies.app.loadData.load = { url in didLoadDataFromURL.append(url) return dataFromURL } @@ -41,11 +40,10 @@ final class RestoreFeatureTests: XCTestCase { let failure = Failure() let store = TestStore( - initialState: RestoreState( + initialState: RestoreComponent.State( isImportingFile: true ), - reducer: restoreReducer, - environment: .unimplemented + reducer: RestoreComponent() ) store.send(.fileImport(.failure(failure as NSError))) { @@ -60,14 +58,13 @@ final class RestoreFeatureTests: XCTestCase { let failure = Failure() let store = TestStore( - initialState: RestoreState( + initialState: RestoreComponent.State( isImportingFile: true ), - reducer: restoreReducer, - environment: .unimplemented + reducer: RestoreComponent() ) - store.environment.loadData.load = { _ in throw failure } + store.dependencies.app.loadData.load = { _ in throw failure } store.send(.fileImport(.success(URL(string: "test")!))) { $0.isImportingFile = false @@ -102,23 +99,22 @@ final class RestoreFeatureTests: XCTestCase { var didSaveContact: [XXModels.Contact] = [] let store = TestStore( - initialState: RestoreState( + initialState: RestoreComponent.State( file: .init(name: "file-name", data: backupData) ), - reducer: restoreReducer, - environment: .unimplemented + reducer: RestoreComponent() ) - store.environment.bgQueue = .immediate - store.environment.mainQueue = .immediate - store.environment.now = { now } - store.environment.messenger.restoreBackup.run = { data, passphrase in + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.now = { now } + store.dependencies.app.messenger.restoreBackup.run = { data, passphrase in didRestoreWithData.append(data) didRestoreWithPassphrase.append(passphrase) udFacts = restoredFacts return restoreResult } - store.environment.messenger.e2e.get = { + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.getContact.run = { var contact: XXClient.Contact = .unimplemented(Data()) @@ -127,12 +123,12 @@ final class RestoreFeatureTests: XCTestCase { } return e2e } - store.environment.messenger.ud.get = { + store.dependencies.app.messenger.ud.get = { var ud: UserDiscovery = .unimplemented ud.getFacts.run = { udFacts } return ud } - store.environment.db.run = { + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.fetchContacts.run = { query in didFetchContacts.append(query) @@ -189,11 +185,10 @@ final class RestoreFeatureTests: XCTestCase { func testRestoreWithoutFile() { let store = TestStore( - initialState: RestoreState( + initialState: RestoreComponent.State( file: nil ), - reducer: restoreReducer, - environment: .unimplemented + reducer: RestoreComponent() ) store.send(.restoreTapped) @@ -206,17 +201,16 @@ final class RestoreFeatureTests: XCTestCase { } let store = TestStore( - initialState: RestoreState( + initialState: RestoreComponent.State( file: .init(name: "name", data: "data".data(using: .utf8)!) ), - reducer: restoreReducer, - environment: .unimplemented + reducer: RestoreComponent() ) - store.environment.bgQueue = .immediate - store.environment.mainQueue = .immediate - store.environment.messenger.restoreBackup.run = { _, _ in throw Failure.restore } - store.environment.messenger.destroy.run = { throw Failure.destroy } + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.messenger.restoreBackup.run = { _, _ in throw Failure.restore } + store.dependencies.app.messenger.destroy.run = { throw Failure.destroy } store.send(.restoreTapped) { $0.isRestoring = true diff --git a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestComponentTests.swift similarity index 78% rename from Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift rename to Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestComponentTests.swift index 4c68abab06a609346b47b4fe472d5f377417c2f5..ee2b668f779afcfd1ede6c6e0c42799d3a0be8b8 100644 --- a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift +++ b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestComponentTests.swift @@ -7,22 +7,21 @@ import XXMessengerClient import XXModels @testable import SendRequestFeature -final class SendRequestFeatureTests: XCTestCase { +final class SendRequestComponentTests: XCTestCase { func testStart() { let myContact = XXClient.Contact.unimplemented("my-contact-data".data(using: .utf8)!) var didGetMyContact: [MessengerMyContact.IncludeFacts?] = [] let store = TestStore( - initialState: SendRequestState( + initialState: SendRequestComponent.State( contact: .unimplemented("contact-data".data(using: .utf8)!) ), - reducer: sendRequestReducer, - environment: .unimplemented + reducer: SendRequestComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.myContact.run = { includeFacts in + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.myContact.run = { includeFacts in didGetMyContact.append(includeFacts) return myContact } @@ -39,15 +38,14 @@ final class SendRequestFeatureTests: XCTestCase { let failure = Failure() let store = TestStore( - initialState: SendRequestState( + initialState: SendRequestComponent.State( contact: .unimplemented("contact-data".data(using: .utf8)!) ), - reducer: sendRequestReducer, - environment: .unimplemented + reducer: SendRequestComponent() ) - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.myContact.run = { _ in throw failure } + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.myContact.run = { _ in throw failure } store.send(.start) @@ -70,12 +68,11 @@ final class SendRequestFeatureTests: XCTestCase { myContact.getFactsFromContact.run = { _ in myFacts } let store = TestStore( - initialState: SendRequestState( + initialState: SendRequestComponent.State( contact: contact, myContact: myContact ), - reducer: sendRequestReducer, - environment: .unimplemented + reducer: SendRequestComponent() ) struct DidBulkUpdateContacts: Equatable { @@ -90,9 +87,9 @@ final class SendRequestFeatureTests: XCTestCase { var didBulkUpdateContacts: [DidBulkUpdateContacts] = [] var didRequestAuthChannel: [DidRequestAuthChannel] = [] - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.db.run = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.bulkUpdateContacts.run = { query, assignments in didBulkUpdateContacts.append(.init(query: query, assignments: assignments)) @@ -100,7 +97,7 @@ final class SendRequestFeatureTests: XCTestCase { } return db } - store.environment.messenger.e2e.get = { + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.requestAuthenticatedChannel.run = { partner, myFacts in didRequestAuthChannel.append(.init(partner: partner, myFacts: myFacts)) @@ -149,25 +146,24 @@ final class SendRequestFeatureTests: XCTestCase { myContact.getFactsFromContact.run = { _ in myFacts } let store = TestStore( - initialState: SendRequestState( + initialState: SendRequestComponent.State( contact: contact, myContact: myContact ), - reducer: sendRequestReducer, - environment: .unimplemented + reducer: SendRequestComponent() ) struct Failure: Error {} let failure = Failure() - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.db.run = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.bulkUpdateContacts.run = { _, _ in return 0 } return db } - store.environment.messenger.e2e.get = { + store.dependencies.app.messenger.e2e.get = { var e2e: E2E = .unimplemented e2e.requestAuthenticatedChannel.run = { _, _ in throw failure } return e2e diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchComponentTests.swift similarity index 81% rename from Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift rename to Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchComponentTests.swift index 33f1edb9d61d7186503b63f90c1f73f11251185f..97ca5faef6c7fbf65470e9416df77ca25ca9bab1 100644 --- a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift +++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchComponentTests.swift @@ -6,12 +6,11 @@ import XXClient import XXMessengerClient @testable import UserSearchFeature -final class UserSearchFeatureTests: XCTestCase { +final class UserSearchComponentTests: XCTestCase { func testSearch() { let store = TestStore( - initialState: UserSearchState(), - reducer: userSearchReducer, - environment: .unimplemented + initialState: UserSearchComponent.State(), + reducer: UserSearchComponent() ) var didSearchWithQuery: [MessengerSearchContacts.Query] = [] @@ -43,9 +42,9 @@ final class UserSearchFeatureTests: XCTestCase { contact4.getFactsFromContact.run = { _ in throw GetFactsFromContactError() } let contacts = [contact1, contact2, contact3, contact4] - store.environment.bgQueue = .immediate - store.environment.mainQueue = .immediate - store.environment.messenger.searchContacts.run = { query in + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.messenger.searchContacts.run = { query in didSearchWithQuery.append(query) return contacts } @@ -93,17 +92,16 @@ final class UserSearchFeatureTests: XCTestCase { func testSearchFailure() { let store = TestStore( - initialState: UserSearchState(), - reducer: userSearchReducer, - environment: .unimplemented + initialState: UserSearchComponent.State(), + reducer: UserSearchComponent() ) struct Failure: Error {} let failure = Failure() - store.environment.bgQueue = .immediate - store.environment.mainQueue = .immediate - store.environment.messenger.searchContacts.run = { _ in throw failure } + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.messenger.searchContacts.run = { _ in throw failure } store.send(.searchTapped) { $0.focusedField = nil @@ -121,7 +119,7 @@ final class UserSearchFeatureTests: XCTestCase { func testResultTapped() { let store = TestStore( - initialState: UserSearchState( + initialState: UserSearchComponent.State( results: [ .init( id: "contact-id".data(using: .utf8)!, @@ -129,12 +127,11 @@ final class UserSearchFeatureTests: XCTestCase { ) ] ), - reducer: userSearchReducer, - environment: .unimplemented + reducer: UserSearchComponent() ) store.send(.resultTapped(id: "contact-id".data(using: .utf8)!)) { - $0.contact = ContactState( + $0.contact = ContactComponent.State( id: "contact-id".data(using: .utf8)!, xxContact: .unimplemented("contact-data".data(using: .utf8)!) ) @@ -143,13 +140,12 @@ final class UserSearchFeatureTests: XCTestCase { func testDismissingContact() { let store = TestStore( - initialState: UserSearchState( - contact: ContactState( + initialState: UserSearchComponent.State( + contact: ContactComponent.State( id: "contact-id".data(using: .utf8)! ) ), - reducer: userSearchReducer, - environment: .unimplemented + reducer: UserSearchComponent() ) store.send(.didDismissContact) { diff --git a/Examples/xx-messenger/Tests/VerifyContactFeatureTests/VerifyContactFeatureTests.swift b/Examples/xx-messenger/Tests/VerifyContactFeatureTests/VerifyContactComponentTests.swift similarity index 80% rename from Examples/xx-messenger/Tests/VerifyContactFeatureTests/VerifyContactFeatureTests.swift rename to Examples/xx-messenger/Tests/VerifyContactFeatureTests/VerifyContactComponentTests.swift index ceaa61cc3f215234aea66381b4c7dc17965bb1a4..c979ceaa572be2bcf9e4c0326763cf7f285db150 100644 --- a/Examples/xx-messenger/Tests/VerifyContactFeatureTests/VerifyContactFeatureTests.swift +++ b/Examples/xx-messenger/Tests/VerifyContactFeatureTests/VerifyContactComponentTests.swift @@ -5,31 +5,30 @@ import XXClient import XXModels @testable import VerifyContactFeature -final class VerifyContactFeatureTests: XCTestCase { +final class VerifyContactComponentTests: XCTestCase { func testVerify() { 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: VerifyContactState( + initialState: VerifyContactComponent.State( contact: contact ), - reducer: verifyContactReducer, - environment: .unimplemented + reducer: VerifyContactComponent() ) var didVerifyContact: [XXClient.Contact] = [] var didBulkUpdateContactsWithQuery: [XXModels.Contact.Query] = [] var didBulkUpdateContactsWithAssignments: [XXModels.Contact.Assignments] = [] - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.verifyContact.run = { contact in + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.verifyContact.run = { contact in didVerifyContact.append(contact) return true } - store.environment.db.run = { + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.bulkUpdateContacts.run = { query, assignments in didBulkUpdateContactsWithQuery.append(query) @@ -66,24 +65,23 @@ final class VerifyContactFeatureTests: XCTestCase { contact.getIdFromContact.run = { _ in contactId } let store = TestStore( - initialState: VerifyContactState( + initialState: VerifyContactComponent.State( contact: contact ), - reducer: verifyContactReducer, - environment: .unimplemented + reducer: VerifyContactComponent() ) var didVerifyContact: [XXClient.Contact] = [] var didBulkUpdateContactsWithQuery: [XXModels.Contact.Query] = [] var didBulkUpdateContactsWithAssignments: [XXModels.Contact.Assignments] = [] - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.verifyContact.run = { contact in + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.verifyContact.run = { contact in didVerifyContact.append(contact) return false } - store.environment.db.run = { + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.bulkUpdateContacts.run = { query, assignments in didBulkUpdateContactsWithQuery.append(query) @@ -120,11 +118,10 @@ final class VerifyContactFeatureTests: XCTestCase { contact.getIdFromContact.run = { _ in contactId } let store = TestStore( - initialState: VerifyContactState( + initialState: VerifyContactComponent.State( contact: contact ), - reducer: verifyContactReducer, - environment: .unimplemented + reducer: VerifyContactComponent() ) struct Failure: Error {} @@ -133,10 +130,10 @@ final class VerifyContactFeatureTests: XCTestCase { var didBulkUpdateContactsWithQuery: [XXModels.Contact.Query] = [] var didBulkUpdateContactsWithAssignments: [XXModels.Contact.Assignments] = [] - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.verifyContact.run = { _ in throw error } - store.environment.db.run = { + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.verifyContact.run = { _ in throw error } + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.bulkUpdateContacts.run = { query, assignments in didBulkUpdateContactsWithQuery.append(query) diff --git a/Examples/xx-messenger/Tests/WelcomeFeatureTests/WelcomeFeatureTests.swift b/Examples/xx-messenger/Tests/WelcomeFeatureTests/WelcomeComponentTests.swift similarity index 60% rename from Examples/xx-messenger/Tests/WelcomeFeatureTests/WelcomeFeatureTests.swift rename to Examples/xx-messenger/Tests/WelcomeFeatureTests/WelcomeComponentTests.swift index c6f23b7a6c885a0b8728796f6e3fd41f3a018aa1..4d2391d38cbfba47a3cdfcda70539b3f1a1bce16 100644 --- a/Examples/xx-messenger/Tests/WelcomeFeatureTests/WelcomeFeatureTests.swift +++ b/Examples/xx-messenger/Tests/WelcomeFeatureTests/WelcomeComponentTests.swift @@ -2,8 +2,7 @@ import ComposableArchitecture import XCTest @testable import WelcomeFeature -@MainActor -final class WelcomeFeatureTests: XCTestCase { +final class WelcomeComponentTests: XCTestCase { func testNewAccountTapped() { let mainQueue = DispatchQueue.test let bgQueue = DispatchQueue.test @@ -11,14 +10,13 @@ final class WelcomeFeatureTests: XCTestCase { var didCreateMessenger = 0 let store = TestStore( - initialState: WelcomeState(), - reducer: welcomeReducer, - environment: .unimplemented + initialState: WelcomeComponent.State(), + reducer: WelcomeComponent() ) - store.environment.mainQueue = mainQueue.eraseToAnyScheduler() - store.environment.bgQueue = bgQueue.eraseToAnyScheduler() - store.environment.messenger.create.run = { didCreateMessenger += 1 } + store.dependencies.app.mainQueue = mainQueue.eraseToAnyScheduler() + store.dependencies.app.bgQueue = bgQueue.eraseToAnyScheduler() + store.dependencies.app.messenger.create.run = { didCreateMessenger += 1 } store.send(.newAccountTapped) { $0.isCreatingAccount = true @@ -44,14 +42,13 @@ final class WelcomeFeatureTests: XCTestCase { let bgQueue = DispatchQueue.test let store = TestStore( - initialState: WelcomeState(), - reducer: welcomeReducer, - environment: .unimplemented + initialState: WelcomeComponent.State(), + reducer: WelcomeComponent() ) - store.environment.mainQueue = mainQueue.eraseToAnyScheduler() - store.environment.bgQueue = bgQueue.eraseToAnyScheduler() - store.environment.messenger.create.run = { throw failure } + store.dependencies.app.mainQueue = mainQueue.eraseToAnyScheduler() + store.dependencies.app.bgQueue = bgQueue.eraseToAnyScheduler() + store.dependencies.app.messenger.create.run = { throw failure } store.send(.newAccountTapped) { $0.isCreatingAccount = true @@ -69,9 +66,8 @@ final class WelcomeFeatureTests: XCTestCase { func testRestore() { let store = TestStore( - initialState: WelcomeState(), - reducer: welcomeReducer, - environment: .unimplemented + initialState: WelcomeComponent.State(), + reducer: WelcomeComponent() ) store.send(.restoreTapped) diff --git a/Examples/xx-messenger/XXMessenger.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/xx-messenger/XXMessenger.xcworkspace/xcshareddata/swiftpm/Package.resolved index 482d554fe8adf685c8dc7ce64c3f7d09ccbf25e0..4626e31d8eb62969ebd67d9f70afd2f184787bd0 100644 --- a/Examples/xx-messenger/XXMessenger.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/xx-messenger/XXMessenger.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/groue/GRDB.swift", "state" : { - "revision" : "0ac435744a4c67c4ec23a4a671c0d53ce1fee7c6", - "version" : "6.0.0" + "revision" : "13e1f4d7c2896a6a9293102f664e5311e017ffb2", + "version" : "6.1.0" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture.git", "state" : { - "revision" : "5c476994eaa79af8e466041f6de1ab116f37c528", - "version" : "0.42.0" + "revision" : "5bd450a8ac6a802f82d485bac219cbfacffa69fb", + "version" : "0.43.0" } }, { @@ -77,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/darrarski/swift-composable-presentation.git", "state" : { - "revision" : "bdb7df9476cf29e8379fc50aa03848dd6c8033d9", - "version" : "0.5.3" + "revision" : "f69eb0c9a82832f67dfd5dace98e6d0e8d748b0f", + "version" : "0.6.0" } }, { @@ -113,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay.git", "state" : { - "revision" : "30314f1ece684dd60679d598a9b89107557b67d9", - "version" : "0.4.1" + "revision" : "16e6409ee82e1b81390bdffbf217b9c08ab32784", + "version" : "0.5.0" } } ], diff --git a/Package.resolved b/Package.resolved index eb44c7c2804b6d4b64f6ad27cdde3a8f42b6710b..d7760fa278975208f6d8594eac3dbd5c29784cbe 100644 --- a/Package.resolved +++ b/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay.git", "state" : { - "revision" : "30314f1ece684dd60679d598a9b89107557b67d9", - "version" : "0.4.1" + "revision" : "16e6409ee82e1b81390bdffbf217b9c08ab32784", + "version" : "0.5.0" } } ], diff --git a/Package.swift b/Package.swift index cd25ca17a71b46a3f2dfe1d01374d552fb429475..b1d78d2adb57ef25fa0056fae812cc04b853cf7e 100644 --- a/Package.swift +++ b/Package.swift @@ -21,11 +21,11 @@ let package = Package( dependencies: [ .package( url: "https://github.com/pointfreeco/swift-custom-dump.git", - .upToNextMajor(from: "0.5.2") + .upToNextMajor(from: "0.6.0") ), .package( url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git", - .upToNextMajor(from: "0.4.1") + .upToNextMajor(from: "0.5.0") ), .package( url: "https://github.com/kishikawakatsumi/KeychainAccess.git",