import Foundation import UIKit import Display import SwiftSignalKit import Postbox import TelegramCore import SyncCore import TelegramPresentationData import ItemListUI import PresentationDataUtils import AccountContext import AlertUI import PresentationDataUtils import AuthorizationUI import PhoneNumberFormat private final class ConfirmPhoneNumberCodeControllerArguments { let updateEntryText: (String) -> Void let next: () -> Void init(updateEntryText: @escaping (String) -> Void, next: @escaping () -> Void) { self.updateEntryText = updateEntryText self.next = next } } private enum ConfirmPhoneNumberCodeSection: Int32 { case code } private enum ConfirmPhoneNumberCodeTag: ItemListItemTag { case input func isEqual(to other: ItemListItemTag) -> Bool { if let other = other as? ConfirmPhoneNumberCodeTag { switch self { case .input: if case .input = other { return true } else { return false } } } else { return false } } } private enum ConfirmPhoneNumberCodeEntry: ItemListNodeEntry { case codeEntry(PresentationTheme, PresentationStrings, String, String) case codeInfo(PresentationTheme, PresentationStrings, String, String) var section: ItemListSectionId { return ConfirmPhoneNumberCodeSection.code.rawValue } var stableId: Int32 { switch self { case .codeEntry: return 1 case .codeInfo: return 2 } } static func ==(lhs: ConfirmPhoneNumberCodeEntry, rhs: ConfirmPhoneNumberCodeEntry) -> 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, lhsStrings, lhsPhoneNumber, lhsText): if case let .codeInfo(rhsTheme, rhsStrings, rhsPhoneNumber, rhsText) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPhoneNumber == rhsPhoneNumber, lhsText == rhsText { return true } else { return false } } } static func <(lhs: ConfirmPhoneNumberCodeEntry, rhs: ConfirmPhoneNumberCodeEntry) -> Bool { return lhs.stableId < rhs.stableId } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ConfirmPhoneNumberCodeControllerArguments switch self { case let .codeEntry(theme, strings, title, text): return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: title, textColor: .black), text: text, placeholder: "", type: .number, spacing: 10.0, tag: ConfirmPhoneNumberCodeTag.input, sectionId: self.section, textUpdated: { updatedText in arguments.updateEntryText(updatedText) }, action: { arguments.next() }) case let .codeInfo(theme, strings, phoneNumber, nextOptionText): let formattedNumber = formatPhoneNumber(phoneNumber) let stringAndRanges = strings.CancelResetAccount_TextSMS(formattedNumber) var result = "" result += stringAndRanges.0 if let range = result.range(of: formattedNumber) { result.insert("*", at: range.upperBound) result.insert("*", at: range.upperBound) result.insert("*", at: range.lowerBound) result.insert("*", at: range.lowerBound) } if !nextOptionText.isEmpty { result += "\n\n" + nextOptionText } return ItemListTextItem(presentationData: presentationData, text: .markdown(result), sectionId: self.section) } } } private struct ConfirmPhoneNumberCodeControllerState: Equatable { var codeText: String var checking: Bool init(codeText: String, checking: Bool) { self.codeText = codeText self.checking = checking } } private func confirmPhoneNumberCodeControllerEntries(presentationData: PresentationData, state: ConfirmPhoneNumberCodeControllerState, phoneNumber: String, codeData: CancelAccountResetData, timeout: Int32?, strings: PresentationStrings, theme: PresentationTheme) -> [ConfirmPhoneNumberCodeEntry] { var entries: [ConfirmPhoneNumberCodeEntry] = [] entries.append(.codeEntry(presentationData.theme, presentationData.strings, presentationData.strings.ChangePhoneNumberCode_CodePlaceholder, state.codeText)) var text = "" if let nextType = codeData.nextType { text += authorizationNextOptionText(currentType: codeData.type, nextType: nextType, timeout: timeout, strings: presentationData.strings, primaryColor: .black, accentColor: .black).0.string } entries.append(.codeInfo(presentationData.theme, presentationData.strings, phoneNumber, text)) return entries } private func timeoutSignal(codeData: CancelAccountResetData) -> 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) } } protocol ConfirmPhoneNumberCodeController: class { func applyCode(_ code: Int) } private final class ConfirmPhoneNumberCodeControllerImpl: ItemListController, ConfirmPhoneNumberCodeController { 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(presentationData: ItemListPresentationData(presentationData), updatedPresentationData: context.sharedContext.presentationData |> map(ItemListPresentationData.init(_:)), state: state, tabBarItem: nil) } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } func applyCode(_ code: Int) { self.applyCodeImpl(code) } } public func confirmPhoneNumberCodeController(context: AccountContext, phoneNumber: String, codeData: CancelAccountResetData) -> ViewController { let initialState = ConfirmPhoneNumberCodeControllerState(codeText: "", checking: false) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((ConfirmPhoneNumberCodeControllerState) -> ConfirmPhoneNumberCodeControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } var dismissImpl: (() -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? let actionsDisposable = DisposableSet() let confirmPhoneDisposable = MetaDisposable() actionsDisposable.add(confirmPhoneDisposable) let nextTypeDisposable = MetaDisposable() actionsDisposable.add(nextTypeDisposable) let currentDataPromise = Promise() currentDataPromise.set(.single(codeData)) let timeout = Promise() timeout.set(currentDataPromise.get() |> mapToSignal(timeoutSignal)) 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 context.engine.auth.requestNextCancelAccountResetOption(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 var state = state if state.checking || state.codeText.isEmpty { return state } else { code = state.codeText state.checking = true return state } } if let code = code { confirmPhoneDisposable.set((context.engine.auth.requestCancelAccountReset(phoneCodeHash: codeData.hash, phoneCode: code) |> deliverOnMainQueue).start(error: { error in updateState { state in var state = state state.checking = false return state } 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 { state in var state = state state.checking = false return state } let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.CancelResetAccount_Success(formatPhoneNumber(phoneNumber)).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) dismissImpl?() })) } } let arguments = ConfirmPhoneNumberCodeControllerArguments(updateEntryText: { updatedText in var initiateCheck = false updateState { state in var state = state if state.codeText.count < 5 && updatedText.count == 5 { initiateCheck = true } state.codeText = updatedText return state } 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 let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) 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(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.CancelResetAccount_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: confirmPhoneNumberCodeControllerEntries(presentationData: presentationData, state: state, phoneNumber: phoneNumber, codeData: data, timeout: timeout, strings: presentationData.strings, theme: presentationData.theme), style: .blocks, focusItemTag: ConfirmPhoneNumberCodeTag.input, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } let controller = ConfirmPhoneNumberCodeControllerImpl(context: context, state: signal, applyCodeImpl: { code in updateState { state in var state = state state.codeText = "\(code)" return state } checkCode() }) presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window(.root), with: p) } } dismissImpl = { [weak controller] in controller?.view.endEditing(true) controller?.dismiss() } return controller }