Skip to content
Snippets Groups Projects
AppFeature.swift 4.26 KiB
Newer Older
import AppCore
import Combine
import ComposableArchitecture
import ComposablePresentation
import Foundation
import HomeFeature
import RestoreFeature
import WelcomeFeature
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 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
}

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 }
  )
}

let appReducer = Reducer<AppState, AppAction, AppEnvironment>
{ state, action, env in
  switch action {
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  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
Dariusz Rybicki's avatar
Dariusz Rybicki committed
          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() }
)