import Foundation import UIKit import Display import SwiftSignalKit import TelegramCore import TelegramPresentationData import ItemListUI import PresentationDataUtils import AccountContext import AlertUI import PresentationDataUtils import AuthorizationUtils 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(_, _, 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(_, strings, formattedPhoneNumber, nextOptionText): let stringAndRanges = strings.CancelResetAccount_TextSMS(formattedPhoneNumber) var result = "" result += stringAndRanges.string if let range = result.range(of: formattedPhoneNumber) { 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, formattedPhoneNumber: 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, formattedPhoneNumber, 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: AnyObject { 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(context: context, number: phoneNumber)).string, 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, formattedPhoneNumber: formatPhoneNumber(context: context, number: 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 }