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 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, 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.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() } )