import Foundation import UIKit import Display import SwiftSignalKit import Postbox import TelegramCore import TelegramPresentationData import ItemListUI import OverlayStatusController import AccountContext import AlertUI import AuthorizationUI private final class ChangePhoneNumberCodeControllerArguments { let updateEntryText: (String) -> Void let next: () -> Void init(updateEntryText: @escaping (String) -> Void, next: @escaping () -> Void) { self.updateEntryText = updateEntryText self.next = next } } private enum ChangePhoneNumberCodeSection: Int32 { case code } private enum ChangePhoneNumberCodeTag: ItemListItemTag { case input func isEqual(to other: ItemListItemTag) -> Bool { if let other = other as? ChangePhoneNumberCodeTag { switch self { case .input: if case .input = other { return true } else { return false } } } else { return false } } } private enum ChangePhoneNumberCodeEntry: ItemListNodeEntry { case codeEntry(PresentationTheme, PresentationStrings, String, String) case codeInfo(PresentationTheme, String) var section: ItemListSectionId { return ChangePhoneNumberCodeSection.code.rawValue } var stableId: Int32 { switch self { case .codeEntry: return 1 case .codeInfo: return 2 } } static func ==(lhs: ChangePhoneNumberCodeEntry, rhs: ChangePhoneNumberCodeEntry) -> Bool { switch lhs { case let .codeEntry(lhsTheme, lhsStrings, lhsTitle, lhsText): if case let .codeEntry(rhsTheme, rhsStrings, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsTitle == rhsTitle, lhsText == rhsText { return true } else { return false } case let .codeInfo(lhsTheme, lhsText): if case let .codeInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } } } static func <(lhs: ChangePhoneNumberCodeEntry, rhs: ChangePhoneNumberCodeEntry) -> Bool { return lhs.stableId < rhs.stableId } func item(_ arguments: Any) -> ListViewItem { let arguments = arguments as! ChangePhoneNumberCodeControllerArguments switch self { case let .codeEntry(theme, strings, title, text): return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: title, textColor: .black), text: text, placeholder: "", type: .number, spacing: 10.0, tag: ChangePhoneNumberCodeTag.input, sectionId: self.section, textUpdated: { updatedText in arguments.updateEntryText(updatedText) }, action: { arguments.next() }) case let .codeInfo(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) } } } private struct ChangePhoneNumberCodeControllerState: Equatable { let codeText: String let checking: Bool init(codeText: String, checking: Bool) { self.codeText = codeText self.checking = checking } static func ==(lhs: ChangePhoneNumberCodeControllerState, rhs: ChangePhoneNumberCodeControllerState) -> Bool { if lhs.codeText != rhs.codeText { return false } if lhs.checking != rhs.checking { return false } return true } func withUpdatedCodeText(_ codeText: String) -> ChangePhoneNumberCodeControllerState { return ChangePhoneNumberCodeControllerState(codeText: codeText, checking: self.checking) } func withUpdatedChecking(_ checking: Bool) -> ChangePhoneNumberCodeControllerState { return ChangePhoneNumberCodeControllerState(codeText: self.codeText, checking: checking) } func withUpdatedNextMethodTimeout(_ nextMethodTimeout: Int32?) -> ChangePhoneNumberCodeControllerState { return ChangePhoneNumberCodeControllerState(codeText: self.codeText, checking: self.checking) } func withUpdatedCodeData(_ codeData: ChangeAccountPhoneNumberData) -> ChangePhoneNumberCodeControllerState { return ChangePhoneNumberCodeControllerState(codeText: self.codeText, checking: self.checking) } } private func changePhoneNumberCodeControllerEntries(presentationData: PresentationData, state: ChangePhoneNumberCodeControllerState, codeData: ChangeAccountPhoneNumberData, timeout: Int32?, strings: PresentationStrings) -> [ChangePhoneNumberCodeEntry] { var entries: [ChangePhoneNumberCodeEntry] = [] entries.append(.codeEntry(presentationData.theme, presentationData.strings, presentationData.strings.ChangePhoneNumberCode_CodePlaceholder, state.codeText)) var text = authorizationCurrentOptionText(codeData.type, strings: presentationData.strings, primaryColor: presentationData.theme.list.itemPrimaryTextColor, accentColor: presentationData.theme.list.itemAccentColor).string if let nextType = codeData.nextType { text += "\n\n" + authorizationNextOptionText(currentType: codeData.type, nextType: nextType, timeout: timeout, strings: presentationData.strings, primaryColor: .black, accentColor: .black).0.string } entries.append(.codeInfo(presentationData.theme, text)) return entries } private func timeoutSignal(codeData: ChangeAccountPhoneNumberData) -> Signal { if let _ = codeData.nextType, let timeout = codeData.timeout { return Signal { subscriber in let value = Atomic(value: timeout) subscriber.putNext(timeout) let timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { subscriber.putNext(value.modify { value in return max(0, value - 1) }) }, queue: Queue.mainQueue()) timer.start() return ActionDisposable { timer.invalidate() } } } else { return .single(nil) } } public protocol ChangePhoneNumberCodeController: class { func applyCode(_ code: Int) } private final class ChangePhoneNumberCodeControllerImpl: ItemListController, ChangePhoneNumberCodeController { private let applyCodeImpl: (Int) -> Void init(context: AccountContext, state: Signal<(ItemListControllerState, (ItemListNodeState, Any)), NoError>, applyCodeImpl: @escaping (Int) -> Void) { self.applyCodeImpl = applyCodeImpl let presentationData = context.sharedContext.currentPresentationData.with { $0 } super.init(theme: presentationData.theme, strings: presentationData.strings, updatedPresentationData: context.sharedContext.presentationData |> map { ($0.theme, $0.strings) }, state: state, tabBarItem: nil) } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } func applyCode(_ code: Int) { self.applyCodeImpl(code) } } func changePhoneNumberCodeController(context: AccountContext, phoneNumber: String, codeData: ChangeAccountPhoneNumberData) -> ViewController { let initialState = ChangePhoneNumberCodeControllerState(codeText: "", checking: false) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((ChangePhoneNumberCodeControllerState) -> ChangePhoneNumberCodeControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } var dismissImpl: (() -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? let actionsDisposable = DisposableSet() let changePhoneDisposable = MetaDisposable() actionsDisposable.add(changePhoneDisposable) let nextTypeDisposable = MetaDisposable() actionsDisposable.add(nextTypeDisposable) let currentDataPromise = Promise() currentDataPromise.set(.single(codeData)) let timeout = Promise() timeout.set(timeoutSignal(codeData: codeData)) let resendCode = currentDataPromise.get() |> mapToSignal { [weak currentDataPromise] data -> Signal in if let _ = data.nextType { return timeout.get() |> filter { $0 == 0 } |> take(1) |> mapToSignal { _ -> Signal in return Signal { subscriber in return requestNextChangeAccountPhoneNumberVerification(account: context.account, phoneNumber: phoneNumber, phoneCodeHash: data.hash).start(next: { next in currentDataPromise?.set(.single(next)) }, error: { error in }) } } } else { return .complete() } } nextTypeDisposable.set(resendCode.start()) let checkCode: () -> Void = { var code: String? updateState { state in if state.checking || state.codeText.isEmpty { return state } else { code = state.codeText return state.withUpdatedChecking(true) } } if let code = code { changePhoneDisposable.set((requestChangeAccountPhoneNumber(account: context.account, phoneNumber: phoneNumber, phoneCodeHash: codeData.hash, phoneCode: code) |> deliverOnMainQueue).start(error: { error in updateState { return $0.withUpdatedChecking(false) } let presentationData = context.sharedContext.currentPresentationData.with { $0 } let alertText: String switch error { case .generic: alertText = presentationData.strings.Login_UnknownError case .invalidCode: alertText = presentationData.strings.Login_InvalidCodeError case .codeExpired: alertText = presentationData.strings.Login_CodeExpiredError case .limitExceeded: alertText = presentationData.strings.Login_CodeFloodError } presentControllerImpl?(textAlertController(context: context, title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, completed: { updateState { return $0.withUpdatedChecking(false) } let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .success), nil) dismissImpl?() })) } } let arguments = ChangePhoneNumberCodeControllerArguments(updateEntryText: { updatedText in var initiateCheck = false updateState { state in if state.codeText.count < 5 && updatedText.count == 5 { initiateCheck = true } return state.withUpdatedCodeText(updatedText) } if initiateCheck { checkCode() } }, next: { checkCode() }) let signal = combineLatest(context.sharedContext.presentationData, statePromise.get() |> deliverOnMainQueue, currentDataPromise.get() |> deliverOnMainQueue, timeout.get() |> deliverOnMainQueue) |> deliverOnMainQueue |> map { presentationData, state, data, timeout -> (ItemListControllerState, (ItemListNodeState, Any)) in var rightNavigationButton: ItemListNavigationButton? if state.checking { rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else { var nextEnabled = true if state.codeText.isEmpty { nextEnabled = false } rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Next), style: .bold, enabled: nextEnabled, action: { checkCode() }) } let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(formatPhoneNumber(phoneNumber)), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let listState = ItemListNodeState(entries: changePhoneNumberCodeControllerEntries(presentationData: presentationData, state: state, codeData: data, timeout: timeout, strings: presentationData.strings), style: .blocks, focusItemTag: ChangePhoneNumberCodeTag.input, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } let controller = ChangePhoneNumberCodeControllerImpl(context: context, state: signal, applyCodeImpl: { code in updateState { state in return state.withUpdatedCodeText("\(code)") } checkCode() }) presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window(.root), with: p) } } dismissImpl = { [weak controller] in (controller?.navigationController as? NavigationController)?.popToRoot(animated: true) } return controller }