Skip to content
Snippets Groups Projects
RestoreFeature.swift 4.71 KiB
Newer Older
Dariusz Rybicki's avatar
Dariusz Rybicki committed
import AppCore
import Combine
import ComposableArchitecture
import Foundation
import XCTestDynamicOverlay
import XXMessengerClient
Dariusz Rybicki's avatar
Dariusz Rybicki committed
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,
Dariusz Rybicki's avatar
Dariusz Rybicki committed
    db: DBManagerGetDB,
    loadData: URLDataLoader,
Dariusz Rybicki's avatar
Dariusz Rybicki committed
    now: @escaping () -> Date,
    mainQueue: AnySchedulerOf<DispatchQueue>,
    bgQueue: AnySchedulerOf<DispatchQueue>
  ) {
    self.messenger = messenger
Dariusz Rybicki's avatar
Dariusz Rybicki committed
    self.db = db
    self.loadData = loadData
Dariusz Rybicki's avatar
Dariusz Rybicki committed
    self.now = now
    self.mainQueue = mainQueue
    self.bgQueue = bgQueue
  }

  public var messenger: Messenger
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  public var db: DBManagerGetDB
  public var loadData: URLDataLoader
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  public var now: () -> Date
  public var mainQueue: AnySchedulerOf<DispatchQueue>
  public var bgQueue: AnySchedulerOf<DispatchQueue>
extension RestoreEnvironment {
  public static let unimplemented = RestoreEnvironment(
    messenger: .unimplemented,
Dariusz Rybicki's avatar
Dariusz Rybicki committed
    db: .unimplemented,
    loadData: .unimplemented,
Dariusz Rybicki's avatar
Dariusz Rybicki committed
    now: XCTUnimplemented("\(Self.self).now"),
    mainQueue: .unimplemented,
    bgQueue: .unimplemented
  )
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 {
Dariusz Rybicki's avatar
Dariusz Rybicki committed
        let result = try env.messenger.restoreBackup(
          backupData: backupData,
          backupPassphrase: backupPassphrase
        )
Dariusz Rybicki's avatar
Dariusz Rybicki committed
        let facts = try env.messenger.ud.tryGet().getFacts()
Dariusz Rybicki's avatar
Dariusz Rybicki committed
        try env.db().saveContact(Contact(
          id: try env.messenger.e2e.tryGet().getContact().getId(),
          username: facts.get(.username)?.value,
Dariusz Rybicki's avatar
Dariusz Rybicki committed
          email: facts.get(.email)?.value,
          phone: facts.get(.phone)?.value,
Dariusz Rybicki's avatar
Dariusz Rybicki committed
          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()