Swiftgram/submodules/SettingsUI/Sources/Privacy and Security/ConfirmPhoneNumberController.swift
2022-08-28 15:07:00 +02:00

343 lines
14 KiB
Swift

import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
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, phoneNumber, nextOptionText):
let formattedNumber = formatPhoneNumber(phoneNumber)
let stringAndRanges = strings.CancelResetAccount_TextSMS(formattedNumber)
var result = ""
result += stringAndRanges.string
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<Int32?, NoError> {
if let _ = codeData.nextType, let timeout = codeData.timeout {
return Signal { subscriber in
let value = Atomic<Int32>(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<CancelAccountResetData>()
currentDataPromise.set(.single(codeData))
let timeout = Promise<Int32?>()
timeout.set(currentDataPromise.get()
|> mapToSignal(timeoutSignal))
let resendCode = currentDataPromise.get()
|> mapToSignal { [weak currentDataPromise] data -> Signal<Void, NoError> in
if let _ = data.nextType {
return timeout.get()
|> filter { $0 == 0 }
|> take(1)
|> mapToSignal { _ -> Signal<Void, NoError> 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)).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, 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
}