Skip to content
Snippets Groups Projects
MyContactFeature.swift 10.4 KiB
Newer Older
Dariusz Rybicki's avatar
Dariusz Rybicki committed
import AppCore
import ComposableArchitecture
Dariusz Rybicki's avatar
Dariusz Rybicki committed
import Foundation
import XCTestDynamicOverlay
Dariusz Rybicki's avatar
Dariusz Rybicki committed
import XXClient
import XXMessengerClient
import XXModels

public struct MyContactState: Equatable {
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  public enum Field: String, Hashable {
    case email
Dariusz Rybicki's avatar
Dariusz Rybicki committed
    case phone
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  }

  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
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  ) {
    self.contact = contact
    self.focusedField = focusedField
    self.email = email
    self.emailConfirmationID = emailConfirmationID
    self.emailConfirmationCode = emailConfirmationCode
    self.isRegisteringEmail = isRegisteringEmail
    self.isConfirmingEmail = isConfirmingEmail
    self.isUnregisteringEmail = isUnregisteringEmail
Dariusz Rybicki's avatar
Dariusz Rybicki committed
    self.phone = phone
    self.phoneConfirmationID = phoneConfirmationID
    self.phoneConfirmationCode = phoneConfirmationCode
    self.isRegisteringPhone = isRegisteringPhone
    self.isConfirmingPhone = isConfirmingPhone
    self.isUnregisteringPhone = isUnregisteringPhone
    self.isLoadingFacts = isLoadingFacts
    self.alert = alert
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  }

  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
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  @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>?
Dariusz Rybicki's avatar
Dariusz Rybicki committed
public enum MyContactAction: Equatable, BindableAction {
  case start
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  case contactFetched(XXModels.Contact?)
  case registerEmailTapped
  case confirmEmailTapped
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  case unregisterEmailTapped
  case registerPhoneTapped
  case confirmPhoneTapped
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  case unregisterPhoneTapped
  case loadFactsTapped
  case didFail(String)
  case alertDismissed
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  case binding(BindingAction<MyContactState>)
}

public struct MyContactEnvironment {
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  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 {
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  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
Dariusz Rybicki's avatar
Dariusz Rybicki committed
  enum DBFetchEffectID {}

  switch action {
  case .start:
Dariusz Rybicki's avatar
Dariusz Rybicki committed
    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()
Dariusz Rybicki's avatar
Dariusz Rybicki committed

  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()
Dariusz Rybicki's avatar
Dariusz Rybicki committed

  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()
Dariusz Rybicki's avatar
Dariusz Rybicki committed

  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()
Dariusz Rybicki's avatar
Dariusz Rybicki committed

  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

Dariusz Rybicki's avatar
Dariusz Rybicki committed
  case .binding(_):
    return .none
  }
}
Dariusz Rybicki's avatar
Dariusz Rybicki committed
.binding()