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