import Foundation import UIKit import AsyncDisplayKit import Display import TelegramCore import Postbox import SwiftSignalKit import CoreTelephony import TelegramPresentationData import AccountContext import AlertUI import PresentationDataUtils import CountrySelectionUI private func cleanPhoneNumber(_ text: String?) -> String { var cleanNumber = "" if let text = text { for c in text { if c >= "0" && c <= "9" { cleanNumber += String(c) } } } return cleanNumber } public final class SecureIdPlaintextFormParams { fileprivate let openCountrySelection: () -> Void fileprivate let updateTextField: (SecureIdPlaintextFormTextField, String) -> Void fileprivate let usePhone: (String) -> Void fileprivate let useEmailAddress: (String) -> Void fileprivate let save: () -> Void fileprivate init(openCountrySelection: @escaping () -> Void, updateTextField: @escaping (SecureIdPlaintextFormTextField, String) -> Void, usePhone: @escaping (String) -> Void, useEmailAddress: @escaping (String) -> Void, save: @escaping () -> Void) { self.openCountrySelection = openCountrySelection self.updateTextField = updateTextField self.usePhone = usePhone self.useEmailAddress = useEmailAddress self.save = save } } private struct PhoneInputState { var countryCode: String var number: String var countryId: String func isEqual(to: PhoneInputState) -> Bool { if self.countryCode != to.countryCode { return false } if self.number != to.number { return false } if self.countryId != to.countryId { return false } return true } } private struct PhoneVerifyState { let phone: String let payload: SecureIdPreparePhoneVerificationPayload var code: String func isEqual(to: PhoneVerifyState) -> Bool { if self.code != to.code { return false } return true } } private enum SecureIdPlaintextFormPhoneState { case input(PhoneInputState) case verify(PhoneVerifyState) func isEqual(to: SecureIdPlaintextFormPhoneState) -> Bool { switch self { case let .input(lhsInput): if case let .input(rhsInput) = to, lhsInput.isEqual(to: rhsInput) { return true } else { return false } case let .verify(lhsInput): if case let .verify(rhsInput) = to, lhsInput.isEqual(to: rhsInput) { return true } else { return false } } } func isComplete() -> Bool { switch self { case let .input(input): if input.countryCode.isEmpty { return false } if input.number.isEmpty { return false } return true case let .verify(verify): if verify.code.isEmpty { return false } return true } } } private struct EmailInputState { var email: String func isEqual(to: EmailInputState) -> Bool { if self.email != to.email { return false } return true } } private struct EmailVerifyState { let email: String let payload: SecureIdPrepareEmailVerificationPayload var code: String func isEqual(to: EmailVerifyState) -> Bool { if self.code != to.code { return false } return true } } private enum SecureIdPlaintextFormEmailState { case input(EmailInputState) case verify(EmailVerifyState) func isEqual(to: SecureIdPlaintextFormEmailState) -> Bool { switch self { case let .input(lhsInput): if case let .input(rhsInput) = to, lhsInput.isEqual(to: rhsInput) { return true } else { return false } case let .verify(lhsInput): if case let .verify(rhsInput) = to, lhsInput.isEqual(to: rhsInput) { return true } else { return false } } } func isComplete() -> Bool { switch self { case let .input(input): if input.email.isEmpty { return false } return true case let .verify(verify): if verify.code.isEmpty { return false } return true } } } private enum SecureIdPlaintextFormTextField { case countryCode case number case code case email } private enum SecureIdPlaintextFormDataState { case phone(SecureIdPlaintextFormPhoneState) case email(SecureIdPlaintextFormEmailState) mutating func updateTextField(type: SecureIdPlaintextFormTextField, value: String) { switch self { case let .phone(phone): switch phone { case var .input(input): switch type { case .countryCode: input.countryCode = value case .number: input.number = value default: break } self = .phone(.input(input)) case var .verify(verify): switch type { case .code: verify.code = value default: break } self = .phone(.verify(verify)) } case let .email(email): switch email { case var .input(input): switch type { case .email: input.email = value default: break } self = .email(.input(input)) case var .verify(verify): switch type { case .code: verify.code = value default: break } self = .email(.verify(verify)) } } } func isEqual(to: SecureIdPlaintextFormDataState) -> Bool { switch self { case let .phone(lhsValue): if case let .phone(rhsValue) = to, lhsValue.isEqual(to: rhsValue) { return true } else { return false } case let .email(lhsValue): if case let .email(rhsValue) = to, lhsValue.isEqual(to: rhsValue) { return true } else { return false } } } } private enum SecureIdPlaintextFormActionState { case none case saving case deleting } enum SecureIdPlaintextFormInputState { case nextAvailable case nextNotAvailable case saveAvailable case saveNotAvailable case inProgress } public struct SecureIdPlaintextFormInnerState: FormControllerInnerState { fileprivate let previousValue: SecureIdValue? fileprivate var data: SecureIdPlaintextFormDataState fileprivate var actionState: SecureIdPlaintextFormActionState public func isEqual(to: SecureIdPlaintextFormInnerState) -> Bool { if !self.data.isEqual(to: to.data) { return false } if self.actionState != to.actionState { return false } return true } public func entries() -> [FormControllerItemEntry] { switch self.data { case let .phone(phone): var result: [FormControllerItemEntry] = [] switch phone { case let .input(input): result.append(.spacer) if let value = self.previousValue, case let .phone(phone) = value { result.append(.entry(SecureIdPlaintextFormEntry.immediatelyAvailablePhone(phone.phone))) result.append(.entry(SecureIdPlaintextFormEntry.immediatelyAvailablePhoneInfo)) result.append(.spacer) result.append(.entry(SecureIdPlaintextFormEntry.numberInputHeader)) } result.append(.entry(SecureIdPlaintextFormEntry.numberInput(countryCode: input.countryCode, number: input.number))) result.append(.entry(SecureIdPlaintextFormEntry.numberInputInfo)) case let .verify(verify): result.append(.spacer) var codeLength: Int32 = 5 switch verify.payload.type { case let .sms(length): codeLength = length case let .call(length): codeLength = length case let .otherSession(length): codeLength = length default: break } result.append(.entry(SecureIdPlaintextFormEntry.numberCode(verify.code, codeLength))) result.append(.entry(SecureIdPlaintextFormEntry.numberVerifyInfo)) } return result case let .email(email): var result: [FormControllerItemEntry] = [] switch email { case let .input(input): result.append(.spacer) if let value = self.previousValue, case let .email(email) = value { result.append(.entry(SecureIdPlaintextFormEntry.immediatelyAvailableEmail(email.email))) result.append(.entry(SecureIdPlaintextFormEntry.immediatelyAvailableEmailInfo)) result.append(.spacer) result.append(.entry(SecureIdPlaintextFormEntry.emailInputHeader)) } result.append(.entry(SecureIdPlaintextFormEntry.emailAddress(input.email))) result.append(.entry(SecureIdPlaintextFormEntry.emailInputInfo)) case let .verify(verify): result.append(.spacer) result.append(.entry(SecureIdPlaintextFormEntry.numberCode(verify.code, verify.payload.length))) result.append(.entry(SecureIdPlaintextFormEntry.emailVerifyInfo(verify.email))) } return result } } func actionInputState() -> SecureIdPlaintextFormInputState { switch self.actionState { case .deleting, .saving: return .inProgress default: break } switch self.data { case let .phone(phone): switch phone { case .input: if !phone.isComplete() { return .nextNotAvailable } else { return .nextAvailable } case .verify: if !phone.isComplete() { return .saveNotAvailable } else { return .saveAvailable } } case let .email(email): switch email { case .input: if !email.isComplete() { return .nextNotAvailable } else { return .nextAvailable } case .verify: if !email.isComplete() { return .saveNotAvailable } else { return .saveAvailable } } } } } extension SecureIdPlaintextFormInnerState { init(type: SecureIdPlaintextFormType, immediatelyAvailableValue: SecureIdValue?) { switch type { case .phone: var countryId: String? = nil let networkInfo = CTTelephonyNetworkInfo() if let carrier = networkInfo.subscriberCellularProvider { countryId = carrier.isoCountryCode } if countryId == nil { countryId = (Locale.current as NSLocale).object(forKey: .countryCode) as? String } var countryCodeAndId: (Int32, String) = (1, "US") if let countryId = countryId { let normalizedId = countryId.uppercased() for (code, idAndName) in countryCodeToIdAndName { if idAndName.0 == normalizedId { countryCodeAndId = (Int32(code), idAndName.0.uppercased()) break } } } self.init(previousValue: immediatelyAvailableValue, data: .phone(.input(PhoneInputState(countryCode: "+\(countryCodeAndId.0)", number: "", countryId: countryCodeAndId.1))), actionState: .none) case .email: self.init(previousValue: immediatelyAvailableValue, data: .email(.input(EmailInputState(email: ""))), actionState: .none) } } } public enum SecureIdPlaintextFormEntryId: Hashable { case immediatelyAvailablePhone case immediatelyAvailablePhoneInfo case numberInputHeader case numberInput case numberInputInfo case numberCode case numberVerifyInfo case immediatelyAvailableEmail case immediatelyAvailableEmailInfo case emailInputHeader case emailAddress case emailInputInfo case emailCode case emailVerifyInfo } public enum SecureIdPlaintextFormEntry: FormControllerEntry { case immediatelyAvailablePhone(String) case immediatelyAvailablePhoneInfo case numberInputHeader case numberInput(countryCode: String, number: String) case numberInputInfo case numberCode(String, Int32) case numberVerifyInfo case immediatelyAvailableEmail(String) case immediatelyAvailableEmailInfo case emailInputHeader case emailAddress(String) case emailInputInfo case emailCode(String) case emailVerifyInfo(String) public var stableId: SecureIdPlaintextFormEntryId { switch self { case .immediatelyAvailablePhone: return .immediatelyAvailablePhone case .immediatelyAvailablePhoneInfo: return .immediatelyAvailablePhoneInfo case .numberInputHeader: return .numberInputHeader case .numberInput: return .numberInput case .numberInputInfo: return .numberInputInfo case .numberCode: return .numberCode case .numberVerifyInfo: return .numberVerifyInfo case .immediatelyAvailableEmail: return .immediatelyAvailableEmail case .immediatelyAvailableEmailInfo: return .immediatelyAvailableEmailInfo case .emailInputHeader: return .emailInputHeader case .emailAddress: return .emailAddress case .emailInputInfo: return .emailInputInfo case .emailCode: return .emailCode case .emailVerifyInfo: return .emailVerifyInfo } } public func isEqual(to: SecureIdPlaintextFormEntry) -> Bool { switch self { case let .immediatelyAvailablePhone(value): if case .immediatelyAvailablePhone(value) = to { return true } else { return false } case .immediatelyAvailablePhoneInfo: if case .immediatelyAvailablePhoneInfo = to { return true } else { return false } case .numberInputHeader: if case .numberInputHeader = to { return true } else { return false } case let .numberInput(countryCode, number): if case .numberInput(countryCode, number) = to { return true } else { return false } case .numberInputInfo: if case .numberInputInfo = to { return true } else { return false } case let .numberCode(code, length): if case .numberCode(code, length) = to { return true } else { return false } case .numberVerifyInfo: if case .numberVerifyInfo = to { return true } else { return false } case let .immediatelyAvailableEmail(value): if case .immediatelyAvailableEmail(value) = to { return true } else { return false } case .immediatelyAvailableEmailInfo: if case .immediatelyAvailableEmailInfo = to { return true } else { return false } case .emailInputHeader: if case .emailInputHeader = to { return true } else { return false } case let .emailAddress(code): if case .emailAddress(code) = to { return true } else { return false } case .emailInputInfo: if case .emailInputInfo = to { return true } else { return false } case let .emailCode(code): if case .emailCode(code) = to { return true } else { return false } case let .emailVerifyInfo(address): if case .emailVerifyInfo(address) = to { return true } else { return false } } } public func item(params: SecureIdPlaintextFormParams, strings: PresentationStrings) -> FormControllerItem { switch self { case let .immediatelyAvailablePhone(value): return FormControllerActionItem(type: .accent, title: strings.Passport_Phone_UseTelegramNumber(formatPhoneNumber(value)).0, activated: { params.usePhone(value) }) case .immediatelyAvailablePhoneInfo: return FormControllerTextItem(text: strings.Passport_Phone_UseTelegramNumberHelp) case .numberInputHeader: return FormControllerHeaderItem(text: strings.Passport_Phone_EnterOtherNumber) case let .numberInput(countryCode, number): var countryName = "" if let codeNumber = Int(countryCode), let codeId = AuthorizationSequenceCountrySelectionController.lookupCountryIdByCode(codeNumber) { countryName = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(codeId, strings: strings) ?? "" } return SecureIdValueFormPhoneItem(countryCode: countryCode, number: number, countryName: countryName, openCountrySelection: { params.openCountrySelection() }, updateCountryCode: { value in params.updateTextField(.countryCode, value) }, updateNumber: { value in params.updateTextField(.number, value) }) case .numberInputInfo: return FormControllerTextItem(text: strings.Passport_Phone_Help) case let .numberCode(code, length): return FormControllerTextInputItem(title: strings.ChangePhoneNumberCode_CodePlaceholder, text: code, placeholder: strings.ChangePhoneNumberCode_CodePlaceholder, type: .number, textUpdated: { value in params.updateTextField(.code, value) if value.count == length { params.save() } }, returnPressed: { }) case .numberVerifyInfo: return FormControllerTextItem(text: strings.ChangePhoneNumberCode_Help) case let .immediatelyAvailableEmail(value): return FormControllerActionItem(type: .accent, title: strings.Passport_Email_UseTelegramEmail(value).0, activated: { params.useEmailAddress(value) }) case .immediatelyAvailableEmailInfo: return FormControllerTextItem(text: strings.Passport_Email_UseTelegramEmailHelp) case .emailInputHeader: return FormControllerHeaderItem(text: strings.Passport_Email_EnterOtherEmail) case let .emailAddress(address): return FormControllerTextInputItem(title: strings.TwoStepAuth_Email, text: address, placeholder: strings.Passport_Email_EmailPlaceholder, type: .email, textUpdated: { value in params.updateTextField(.email, value) }, returnPressed: { params.save() }) case .emailInputInfo: return FormControllerTextItem(text: strings.Passport_Email_Help) case let .emailCode(code): return FormControllerTextInputItem(title: strings.TwoStepAuth_RecoveryCode, text: code, placeholder: strings.TwoStepAuth_RecoveryCode, type: .number, textUpdated: { value in params.updateTextField(.code, value) }, returnPressed: { }) case let .emailVerifyInfo(address): return FormControllerTextItem(text: strings.Passport_Email_CodeHelp(address).0) } } } public struct SecureIdPlaintextFormControllerNodeInitParams { let context: AccountContext let secureIdContext: SecureIdAccessContext } private enum SecureIdPlaintextFormNavigatonTransition { case none case push } public final class SecureIdPlaintextFormControllerNode: FormControllerNode { private var _itemParams: SecureIdPlaintextFormParams? override public var itemParams: SecureIdPlaintextFormParams { return self._itemParams! } private var theme: PresentationTheme private var strings: PresentationStrings private let context: AccountContext private let secureIdContext: SecureIdAccessContext var actionInputStateUpdated: ((SecureIdPlaintextFormInputState) -> Void)? var completedWithValue: ((SecureIdValueWithContext?) -> Void)? var dismiss: (() -> Void)? private let actionDisposable = MetaDisposable() required public init(initParams: SecureIdPlaintextFormControllerNodeInitParams, presentationData: PresentationData) { self.theme = presentationData.theme self.strings = presentationData.strings self.context = initParams.context self.secureIdContext = initParams.secureIdContext super.init(initParams: initParams, presentationData: presentationData) self._itemParams = SecureIdPlaintextFormParams(openCountrySelection: { [weak self] in guard let strongSelf = self else { return } let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.strings, theme: strongSelf.theme, displayCodes: true) controller.completeWithCountryCode = { code, _ in if let strongSelf = self, var innerState = strongSelf.innerState { innerState.data.updateTextField(type: .countryCode, value: "+\(code)") strongSelf.updateInnerState(transition: .immediate, with: innerState) } } strongSelf.view.endEditing(true) strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, updateTextField: { [weak self] type, value in guard let strongSelf = self else { return } guard var innerState = strongSelf.innerState else { return } innerState.data.updateTextField(type: type, value: value) strongSelf.updateInnerState(transition: .immediate, with: innerState) }, usePhone: { [weak self] value in self?.savePhone(value) }, useEmailAddress: { [weak self] value in self?.saveEmailAddress(value) }, save: { [weak self] in self?.save() }) } deinit { self.actionDisposable.dispose() } override func updateInnerState(transition: ContainedViewLayoutTransition, with innerState: SecureIdPlaintextFormInnerState) { let previousActionInputState = self.innerState?.actionInputState() super.updateInnerState(transition: transition, with: innerState) let actionInputState = innerState.actionInputState() if previousActionInputState != actionInputState { self.actionInputStateUpdated?(actionInputState) } } private func updateInnerState(transition: ContainedViewLayoutTransition, navigationTransition: SecureIdPlaintextFormNavigatonTransition, with innerState: SecureIdPlaintextFormInnerState) { if case .push = navigationTransition { if let snapshotView = self.scrollNode.view.snapshotContentTree() { snapshotView.frame = self.scrollNode.view.frame.offsetBy(dx: 0.0, dy: self.scrollNode.view.contentInset.top) self.scrollNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.scrollNode.view) snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -self.scrollNode.view.bounds.width, y: 0.0), duration: 0.25, removeOnCompletion: false, additive: true, completion : { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) self.scrollNode.view.layer.animatePosition(from: CGPoint(x: self.scrollNode.view.bounds.width, y: 0.0), to: CGPoint(), duration: 0.25, additive: true) } } self.updateInnerState(transition: transition, with: innerState) } func activateMainInput() { self.enumerateItemsAndEntries({ itemEntry, itemNode in switch itemEntry { case .emailAddress, .numberCode, .emailCode: if let inputNode = itemNode as? FormControllerTextInputItemNode { inputNode.activate() } return false case .numberInput: if let inputNode = itemNode as? SecureIdValueFormPhoneItemNode { inputNode.activate() } return false default: return true } }) } override func didAppear() { self.activateMainInput() } func save() { guard var innerState = self.innerState else { return } guard case .none = innerState.actionState else { return } switch innerState.data { case let .phone(phone): switch phone { case let .input(input): self.savePhone(input.countryCode + input.number) return case .verify: self.verifyPhoneCode() return } case let .email(email): switch email { case let .input(input): self.saveEmailAddress(input.email) return case let .verify(verify): guard case .saveAvailable = innerState.actionInputState() else { return } innerState.actionState = .saving self.updateInnerState(transition: .immediate, with: innerState) self.actionDisposable.set((secureIdCommitEmailVerification(postbox: self.context.account.postbox, network: self.context.account.network, context: self.secureIdContext, payload: verify.payload, code: verify.code) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { guard let innerState = strongSelf.innerState else { return } guard case .saving = innerState.actionState else { return } strongSelf.completedWithValue?(result) } }, error: { [weak self] error in if let strongSelf = self { guard var innerState = strongSelf.innerState else { return } guard case .saving = innerState.actionState else { return } innerState.actionState = .none strongSelf.updateInnerState(transition: .immediate, with: innerState) let errorText: String switch error { case .generic: errorText = strongSelf.strings.Login_UnknownError case .flood: errorText = strongSelf.strings.Login_CodeFloodError case .invalid: errorText = strongSelf.strings.Login_InvalidCodeError } strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), nil) } })) return } } } private func savePhone(_ value: String) { guard var innerState = self.innerState else { return } guard case .none = innerState.actionState else { return } innerState.actionState = .saving let inputPhone = cleanPhoneNumber(value) self.updateInnerState(transition: .immediate, with: innerState) self.actionDisposable.set((secureIdPreparePhoneVerification(network: self.context.account.network, value: SecureIdPhoneValue(phone: inputPhone)) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { guard var innerState = strongSelf.innerState else { return } guard case .saving = innerState.actionState else { return } innerState.actionState = .none innerState.data = .phone(.verify(PhoneVerifyState(phone: inputPhone, payload: result, code: ""))) strongSelf.updateInnerState(transition: .immediate, navigationTransition: .push, with: innerState) strongSelf.activateMainInput() } }, error: { [weak self] error in if let strongSelf = self { guard var innerState = strongSelf.innerState else { return } guard case .saving = innerState.actionState else { return } innerState.actionState = .none strongSelf.updateInnerState(transition: .immediate, with: innerState) let errorText: String switch error { case .generic: errorText = strongSelf.strings.Login_UnknownError case .flood: errorText = strongSelf.strings.Login_CodeFloodError } strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), nil) } })) } private func saveEmailAddress(_ value: String) { guard var innerState = self.innerState else { return } guard case .none = innerState.actionState else { return } innerState.actionState = .saving self.updateInnerState(transition: .immediate, with: innerState) self.actionDisposable.set((saveSecureIdValue(postbox: self.context.account.postbox, network: self.context.account.network, context: self.secureIdContext, value: .email(SecureIdEmailValue(email: value)), uploadedFiles: [:]) |> deliverOnMainQueue).start(next: { [weak self] result in guard let strongSelf = self else { return } strongSelf.completedWithValue?(result) }, error: { [weak self] _ in guard let strongSelf = self else { return } strongSelf.actionDisposable.set((secureIdPrepareEmailVerification(network: strongSelf.context.account.network, value: SecureIdEmailValue(email: value)) |> deliverOnMainQueue).start(next: { result in guard let strongSelf = self else { return } guard var innerState = strongSelf.innerState else { return } guard case .saving = innerState.actionState else { return } innerState.actionState = .none innerState.data = .email(.verify(EmailVerifyState(email: value, payload: result, code: ""))) strongSelf.updateInnerState(transition: .immediate, navigationTransition: .push, with: innerState) strongSelf.activateMainInput() }, error: { [weak self] error in guard let strongSelf = self else { return } guard var innerState = strongSelf.innerState else { return } guard case .saving = innerState.actionState else { return } innerState.actionState = .none strongSelf.updateInnerState(transition: .immediate, with: innerState) let errorText: String switch error { case .generic: errorText = strongSelf.strings.Login_UnknownError case .invalidEmail: errorText = strongSelf.strings.TwoStepAuth_EmailInvalid case .flood: errorText = strongSelf.strings.Login_CodeFloodError } strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), nil) })) })) } private func verifyPhoneCode() { guard var innerState = self.innerState else { return } guard case let .phone(phone) = innerState.data, case let .verify(verify) = phone else { return } guard case .saveAvailable = innerState.actionInputState() else { return } innerState.actionState = .saving self.updateInnerState(transition: .immediate, with: innerState) self.actionDisposable.set((secureIdCommitPhoneVerification(postbox: self.context.account.postbox, network: self.context.account.network, context: self.secureIdContext, payload: verify.payload, code: verify.code) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { guard let innerState = strongSelf.innerState else { return } guard case .saving = innerState.actionState else { return } strongSelf.completedWithValue?(result) } }, error: { [weak self] error in if let strongSelf = self { guard var innerState = strongSelf.innerState else { return } guard case .saving = innerState.actionState else { return } innerState.actionState = .none strongSelf.updateInnerState(transition: .immediate, with: innerState) let errorText: String switch error { case .generic: errorText = strongSelf.strings.Login_UnknownError case .flood: errorText = strongSelf.strings.Login_CodeFloodError case .invalid: errorText = strongSelf.strings.Login_InvalidCodeError } strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), nil) } })) } func applyPhoneCode(_ code: Int) { guard var innerState = self.innerState else { return } switch innerState.data { case let .phone(phone): switch phone { case var .verify(verify): let value = "\(code)" verify.code = value innerState.data = .phone(.verify(verify)) self.updateInnerState(transition: .immediate, with: innerState) self.verifyPhoneCode() default: break } default: break } } }