Swiftgram/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift
2019-10-18 21:52:41 +04:00

1131 lines
56 KiB
Swift

import Foundation
import UIKit
import AppBundle
import AsyncDisplayKit
import Display
import SolidRoundedButtonNode
import SwiftSignalKit
import OverlayStatusController
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import TelegramCore
public enum TwoFactorDataInputMode {
case password
case emailAddress(password: String)
case emailConfirmation(password: String?, emailPattern: String)
case passwordHint(password: String)
}
private let animationIdle = ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle",
intro: nil,
loop: ManagedAnimationTrack(frameRange: 0 ..< 1),
outro: nil
)
private let animationTracking = ManagedAnimationItem(name: "TwoFactorSetupMonkeyTracking",
intro: nil,
loop: ManagedAnimationTrack(frameRange: 0 ..< Int.max),
outro: nil
)
private let animationHide = ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose",
intro: ManagedAnimationTrack(frameRange: 0 ..< 41),
loop: ManagedAnimationTrack(frameRange: 40 ..< 41),
outro: ManagedAnimationTrack(frameRange: 60 ..< 99)
)
private let animationHideNoOutro = ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose",
intro: ManagedAnimationTrack(frameRange: 0 ..< 41),
loop: ManagedAnimationTrack(frameRange: 40 ..< 41),
outro: nil
)
private let animationHideNoIntro = ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose",
intro: nil,
loop: ManagedAnimationTrack(frameRange: 40 ..< 41),
outro: ManagedAnimationTrack(frameRange: 60 ..< 99)
)
private let animationHideOutro = ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose",
intro: nil,
loop: nil,
outro: ManagedAnimationTrack(frameRange: 60 ..< 99)
)
private let animationPeek = ManagedAnimationItem(name: "TwoFactorSetupMonkeyPeek",
intro: ManagedAnimationTrack(frameRange: 0 ..< 14),
loop: ManagedAnimationTrack(frameRange: 13 ..< 14),
outro: ManagedAnimationTrack(frameRange: 14 ..< 34)
)
private let animationMail = ManagedAnimationItem(name: "TwoFactorSetupMail",
intro: ManagedAnimationTrack(frameRange: 0 ..< Int.max),
loop: ManagedAnimationTrack(frameRange: Int.max - 1 ..< Int.max),
outro: nil
)
private let animationHint = ManagedAnimationItem(name: "TwoFactorSetupHint",
intro: ManagedAnimationTrack(frameRange: 0 ..< Int.max),
loop: ManagedAnimationTrack(frameRange: Int.max - 1 ..< Int.max),
outro: nil
)
public final class TwoFactorDataInputScreen: ViewController {
private let context: AccountContext
private var presentationData: PresentationData
private let mode: TwoFactorDataInputMode
public init(context: AccountContext, mode: TwoFactorDataInputMode) {
self.context = context
self.mode = mode
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
let defaultTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme)
let navigationBarTheme = NavigationBarTheme(buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: defaultTheme.badgeBackgroundColor, badgeStrokeColor: defaultTheme.badgeStrokeColor, badgeTextColor: defaultTheme.badgeTextColor)
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Wallet_Navigation_Back, close: self.presentationData.strings.Wallet_Navigation_Close)))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.navigationPresentation = .modalInLargeLayout
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.navigationBar?.intrinsicCanTransitionInline = false
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Wallet_Navigation_Back, style: .plain, target: nil, action: nil)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func backPressed() {
self.dismiss()
}
override public func loadDisplayNode() {
self.displayNode = TwoFactorDataInputScreenNode(presentationData: self.presentationData, mode: self.mode, action: { [weak self] in
guard let strongSelf = self else {
return
}
switch strongSelf.mode {
case .password:
let values = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText
if values.count != 2 {
return
}
if values[0] != values[1] {
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_SetupPasswordConfirmFailed, actions: [
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})
]), in: .window(.root))
return
}
if values[0].isEmpty {
return
}
guard let navigationController = strongSelf.navigationController as? NavigationController else {
return
}
var controllers = navigationController.viewControllers.filter { controller in
if controller is TwoFactorAuthSplashScreen {
return false
}
if controller is TwoFactorDataInputScreen {
return false
}
return true
}
controllers.append(TwoFactorDataInputScreen(context: strongSelf.context, mode: .passwordHint(password: values[0])))
navigationController.setViewControllers(controllers, animated: true)
case let .emailAddress(password):
guard let text = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !text.isEmpty else {
return
}
let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil))
strongSelf.present(statusController, in: .window(.root))
let _ = (updateTwoStepVerificationEmail(network: strongSelf.context.account.network, currentPassword: password, updatedEmail: text)
|> deliverOnMainQueue).start(next: { [weak statusController] result in
statusController?.dismiss()
guard let strongSelf = self else {
return
}
switch result {
case .none:
break
case let .password(password, pendingEmail):
if let pendingEmail = pendingEmail {
guard let navigationController = strongSelf.navigationController as? NavigationController else {
return
}
var controllers = navigationController.viewControllers.filter { controller in
if controller is TwoFactorAuthSplashScreen {
return false
}
if controller is TwoFactorDataInputScreen {
return false
}
return true
}
controllers.append(TwoFactorDataInputScreen(context: strongSelf.context, mode: .emailConfirmation(password: password, emailPattern: text)))
navigationController.setViewControllers(controllers, animated: true)
} else {
guard let navigationController = strongSelf.navigationController as? NavigationController else {
return
}
var controllers = navigationController.viewControllers.filter { controller in
if controller is TwoFactorAuthSplashScreen {
return false
}
if controller is TwoFactorDataInputScreen {
return false
}
return true
}
controllers.append(TwoFactorAuthSplashScreen(context: strongSelf.context, mode: .done))
navigationController.setViewControllers(controllers, animated: true)
}
}
}, error: { [weak statusController] error in
statusController?.dismiss()
guard let strongSelf = self else {
return
}
let presentationData = strongSelf.presentationData
let alertText: String
switch error {
case .generic:
alertText = presentationData.strings.Login_UnknownError
case .invalidEmail:
alertText = presentationData.strings.TwoStepAuth_EmailInvalid
}
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
})
case .emailConfirmation:
guard let text = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !text.isEmpty else {
return
}
let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil))
strongSelf.present(statusController, in: .window(.root))
let _ = (confirmTwoStepRecoveryEmail(network: strongSelf.context.account.network, code: text)
|> deliverOnMainQueue).start(error: { [weak statusController] error in
statusController?.dismiss()
guard let strongSelf = self else {
return
}
let presentationData = strongSelf.presentationData
let text: String
switch error {
case .invalidEmail:
text = presentationData.strings.TwoStepAuth_EmailInvalid
case .invalidCode:
text = presentationData.strings.Login_InvalidCodeError
case .expired:
text = presentationData.strings.TwoStepAuth_EmailCodeExpired
case .flood:
text = presentationData.strings.TwoStepAuth_FloodError
case .generic:
text = presentationData.strings.Login_UnknownError
}
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}, completed: { [weak statusController] in
statusController?.dismiss()
guard let strongSelf = self else {
return
}
guard let navigationController = strongSelf.navigationController as? NavigationController else {
return
}
var controllers = navigationController.viewControllers.filter { controller in
if controller is TwoFactorAuthSplashScreen {
return false
}
if controller is TwoFactorDataInputScreen {
return false
}
return true
}
controllers.append(TwoFactorAuthSplashScreen(context: strongSelf.context, mode: .done))
navigationController.setViewControllers(controllers, animated: true)
})
case let .passwordHint(password):
guard let value = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !value.isEmpty else {
return
}
strongSelf.setPassword(password: password, hint: value)
}
}, skipAction: { [weak self] in
guard let strongSelf = self else {
return
}
switch strongSelf.mode {
case .emailAddress:
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationTitle, text: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationText, actions: [
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationSkip, action: {
guard let strongSelf = self else {
return
}
guard let navigationController = strongSelf.navigationController as? NavigationController else {
return
}
var controllers = navigationController.viewControllers.filter { controller in
if controller is TwoFactorAuthSplashScreen {
return false
}
if controller is TwoFactorDataInputScreen {
return false
}
return true
}
controllers.append(TwoFactorAuthSplashScreen(context: strongSelf.context, mode: .done))
navigationController.setViewControllers(controllers, animated: true)
}),
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})
]), in: .window(.root))
case let .passwordHint(password):
strongSelf.setPassword(password: password, hint: "")
default:
break
}
}, changeEmailAction: { [weak self] in
guard let strongSelf = self else {
return
}
switch strongSelf.mode {
case let .emailConfirmation(password, _):
if let password = password {
guard let navigationController = strongSelf.navigationController as? NavigationController else {
return
}
var controllers = navigationController.viewControllers.filter { controller in
if controller is TwoFactorAuthSplashScreen {
return false
}
if controller is TwoFactorDataInputScreen {
return false
}
return true
}
controllers.append(TwoFactorDataInputScreen(context: strongSelf.context, mode: .emailAddress(password: password)))
navigationController.setViewControllers(controllers, animated: true)
} else {
}
default:
break
}
}, resendCodeAction: { [weak self] in
guard let strongSelf = self else {
return
}
let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil))
strongSelf.present(statusController, in: .window(.root))
let _ = (resendTwoStepRecoveryEmail(network: strongSelf.context.account.network)
|> deliverOnMainQueue).start(error: { [weak statusController] error in
statusController?.dismiss()
guard let strongSelf = self else {
return
}
let text: String
switch error {
case .flood:
text = strongSelf.presentationData.strings.TwoStepAuth_FloodError
case .generic:
text = strongSelf.presentationData.strings.Login_UnknownError
}
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}, completed: { [weak statusController] in
statusController?.dismiss()
})
})
self.displayNodeDidLoad()
}
private func setPassword(password: String, hint: String) {
let statusController = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: nil))
self.present(statusController, in: .window(.root))
let _ = (updateTwoStepVerificationPassword(network: self.context.account.network, currentPassword: nil, updatedPassword: .password(password: password, hint: hint, email: nil))
|> deliverOnMainQueue).start(next: { [weak self, weak statusController] _ in
statusController?.dismiss()
guard let strongSelf = self else {
return
}
guard let navigationController = strongSelf.navigationController as? NavigationController else {
return
}
var controllers = navigationController.viewControllers.filter { controller in
if controller is TwoFactorAuthSplashScreen {
return false
}
if controller is TwoFactorDataInputScreen {
return false
}
return true
}
controllers.append(TwoFactorDataInputScreen(context: strongSelf.context, mode: .emailAddress(password: password)))
navigationController.setViewControllers(controllers, animated: true)
}, error: { [weak self, weak statusController] _ in
statusController?.dismiss()
guard let strongSelf = self else {
return
}
})
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
(self.displayNode as! TwoFactorDataInputScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationHeight, transition: transition)
}
}
private enum TwoFactorDataInputTextNodeType {
case password(confirmation: Bool)
case email
case code
case hint
}
private func generateClearImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 16.0, height: 16.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.setStrokeColor(UIColor.clear.cgColor)
context.setLineCap(.round)
context.setLineWidth(1.66)
context.move(to: CGPoint(x: 5.5, y: 5.5))
context.addLine(to: CGPoint(x: 10.5, y: 10.5))
context.strokePath()
context.move(to: CGPoint(x: size.width - 5.5, y: 5.5))
context.addLine(to: CGPoint(x: size.width - 10.5, y: 10.5))
context.strokePath()
})
}
private func generateTextHiddenImage(color: UIColor, on: Bool) -> UIImage? {
return generateImage(CGSize(width: 20.0, height: 18.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
guard let image = generateTintedImage(image: UIImage(bundleImageName: "PasswordSetup/TextHidden"), color: color) else {
return
}
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - size.width) / 2.0), y: floor((size.height - size.height) / 2.0)), size: size))
if !on {
context.setLineCap(.round)
context.setBlendMode(.copy)
context.setStrokeColor(UIColor.clear.cgColor)
context.setLineWidth(4.0)
context.move(to: CGPoint(x: 2.0, y: 3.0))
context.addLine(to: CGPoint(x: 18.0, y: 17.0))
context.strokePath()
context.setBlendMode(.normal)
context.setStrokeColor(color.cgColor)
context.setLineWidth(1.5)
context.move(to: CGPoint(x: 2.0, y: 3.0))
context.addLine(to: CGPoint(x: 18.0, y: 17.0))
context.strokePath()
}
})
}
private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelegate {
private let theme: PresentationTheme
let mode: TwoFactorDataInputTextNodeType
private let focused: (TwoFactorDataInputTextNode) -> Void
private let next: (TwoFactorDataInputTextNode) -> Void
private let updated: (TwoFactorDataInputTextNode) -> Void
private let toggleTextHidden: (TwoFactorDataInputTextNode) -> Void
private let backgroundNode: ASImageNode
private let inputNode: TextFieldNode
private let hideButtonNode: HighlightableButtonNode
private let clearButtonNode: HighlightableButtonNode
var text: String {
get {
return self.inputNode.textField.text ?? ""
} set(value) {
self.inputNode.textField.text = value
self.textFieldChanged(self.inputNode.textField)
}
}
init(theme: PresentationTheme, mode: TwoFactorDataInputTextNodeType, placeholder: String, focused: @escaping (TwoFactorDataInputTextNode) -> Void, next: @escaping (TwoFactorDataInputTextNode) -> Void, updated: @escaping (TwoFactorDataInputTextNode) -> Void, toggleTextHidden: @escaping (TwoFactorDataInputTextNode) -> Void) {
self.theme = theme
self.mode = mode
self.focused = focused
self.next = next
self.updated = updated
self.toggleTextHidden = toggleTextHidden
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: theme.actionSheet.inputBackgroundColor)
self.inputNode = TextFieldNode()
self.inputNode.textField.font = Font.regular(17.0)
self.inputNode.textField.textColor = theme.actionSheet.inputTextColor
self.inputNode.textField.attributedPlaceholder = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.actionSheet.inputPlaceholderColor)
self.hideButtonNode = HighlightableButtonNode()
switch mode {
case let .password(confirmation):
self.inputNode.textField.keyboardType = .default
self.inputNode.textField.isSecureTextEntry = true
if confirmation {
self.inputNode.textField.returnKeyType = .done
} else {
self.inputNode.textField.returnKeyType = .next
}
self.hideButtonNode.isHidden = confirmation
case .email:
self.inputNode.textField.keyboardType = .emailAddress
self.inputNode.textField.returnKeyType = .done
self.hideButtonNode.isHidden = true
case .code:
self.inputNode.textField.keyboardType = .numberPad
self.inputNode.textField.returnKeyType = .done
self.hideButtonNode.isHidden = true
case .hint:
self.inputNode.textField.keyboardType = .asciiCapable
self.inputNode.textField.returnKeyType = .done
self.hideButtonNode.isHidden = true
}
self.inputNode.textField.autocorrectionType = .no
self.inputNode.textField.autocapitalizationType = .none
self.inputNode.textField.spellCheckingType = .no
if #available(iOS 11.0, *) {
self.inputNode.textField.smartQuotesType = .no
self.inputNode.textField.smartDashesType = .no
self.inputNode.textField.smartInsertDeleteType = .no
}
self.inputNode.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.hideButtonNode.setImage(generateTextHiddenImage(color: theme.actionSheet.inputClearButtonColor, on: false), for: [])
self.clearButtonNode = HighlightableButtonNode()
self.clearButtonNode.setImage(generateClearImage(color: theme.actionSheet.inputClearButtonColor), for: [])
self.clearButtonNode.isHidden = true
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.inputNode)
self.addSubnode(self.hideButtonNode)
self.inputNode.textField.delegate = self
self.inputNode.textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged)
self.hideButtonNode.addTarget(self, action: #selector(self.hidePressed), forControlEvents: .touchUpInside)
}
func textFieldDidBeginEditing(_ textField: UITextField) {
let text = self.text
let isEmpty = text.isEmpty
self.focused(self)
}
func textFieldDidEndEditing(_ textField: UITextField) {
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
self.next(self)
return false
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return true
}
@objc private func textFieldChanged(_ textField: UITextField) {
switch self.mode {
case .password:
break
default:
self.clearButtonNode.isHidden = self.text.isEmpty
}
self.updated(self)
}
@objc private func hidePressed() {
switch self.mode {
case .password:
self.toggleTextHidden(self)
default:
break
}
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
let leftInset: CGFloat = 16.0
let rightInset: CGFloat = 38.0
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
transition.updateFrame(node: self.inputNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: size.width - leftInset - rightInset, height: size.height)))
transition.updateFrame(node: self.hideButtonNode, frame: CGRect(origin: CGPoint(x: size.width - rightInset - 4.0, y: 0.0), size: CGSize(width: rightInset + 4.0, height: size.height)))
transition.updateFrame(node: self.clearButtonNode, frame: CGRect(origin: CGPoint(x: size.width - rightInset - 4.0, y: 0.0), size: CGSize(width: rightInset + 4.0, height: size.height)))
}
func focus() {
self.inputNode.textField.becomeFirstResponder()
}
func updateTextHidden(_ value: Bool) {
self.hideButtonNode.setImage(generateTextHiddenImage(color: self.theme.actionSheet.inputClearButtonColor, on: !value), for: [])
self.inputNode.textField.isSecureTextEntry = value
}
}
private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIScrollViewDelegate {
private var presentationData: PresentationData
private let mode: TwoFactorDataInputMode
private let action: () -> Void
private let skipAction: () -> Void
private let changeEmailAction: () -> Void
private let resendCodeAction: () -> Void
private let navigationBackgroundNode: ASDisplayNode
private let navigationSeparatorNode: ASDisplayNode
private let scrollNode: ASScrollNode
private let animationNode: ManagedAnimationNode
private let titleNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let skipActionTitleNode: ImmediateTextNode
private let skipActionButtonNode: HighlightTrackingButtonNode
private let changeEmailActionTitleNode: ImmediateTextNode
private let changeEmailActionButtonNode: HighlightTrackingButtonNode
private let resendCodeActionTitleNode: ImmediateTextNode
private let resendCodeActionButtonNode: HighlightTrackingButtonNode
private let inputNodes: [TwoFactorDataInputTextNode]
private let buttonNode: SolidRoundedButtonNode
private var navigationHeight: CGFloat?
var inputText: [String] {
return self.inputNodes.map { $0.text }
}
init(presentationData: PresentationData, mode: TwoFactorDataInputMode, action: @escaping () -> Void, skipAction: @escaping () -> Void, changeEmailAction: @escaping () -> Void, resendCodeAction: @escaping () -> Void) {
self.presentationData = presentationData
self.mode = mode
self.action = action
self.skipAction = skipAction
self.changeEmailAction = changeEmailAction
self.resendCodeAction = resendCodeAction
self.navigationBackgroundNode = ASDisplayNode()
self.navigationBackgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor
self.navigationBackgroundNode.alpha = 0.0
self.navigationSeparatorNode = ASDisplayNode()
self.navigationSeparatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.scrollNode = ASScrollNode()
self.scrollNode.canCancelAllTouchesInViews = true
self.animationNode = ManagedAnimationNode(size: CGSize(width: 136.0, height: 136.0))
let title: String
let text: NSAttributedString
let buttonText: String
let skipActionText: String
let changeEmailActionText: String
let resendCodeActionText: String
var inputNodes: [TwoFactorDataInputTextNode] = []
var next: ((TwoFactorDataInputTextNode) -> Void)?
var focused: ((TwoFactorDataInputTextNode) -> Void)?
var updated: ((TwoFactorDataInputTextNode) -> Void)?
var toggleTextHidden: ((TwoFactorDataInputTextNode) -> Void)?
switch mode {
case .password:
self.animationNode.switchTo(animationIdle)
title = presentationData.strings.TwoFactorSetup_Password_Title
text = NSAttributedString(string: "", font: Font.regular(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
buttonText = presentationData.strings.TwoFactorSetup_Password_Action
skipActionText = ""
changeEmailActionText = ""
resendCodeActionText = ""
inputNodes = [
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .password(confirmation: false), placeholder: presentationData.strings.TwoFactorSetup_Password_PlaceholderPassword, focused: { node in
focused?(node)
}, next: { node in
next?(node)
}, updated: { node in
updated?(node)
}, toggleTextHidden: { node in
toggleTextHidden?(node)
}),
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .password(confirmation: true), placeholder: presentationData.strings.TwoFactorSetup_Password_PlaceholderConfirmPassword, focused: { node in
focused?(node)
}, next: { node in
next?(node)
}, updated: { node in
updated?(node)
}, toggleTextHidden: { node in
toggleTextHidden?(node)
})
]
case .emailAddress:
self.animationNode.switchTo(animationTracking)
title = presentationData.strings.TwoFactorSetup_Email_Title
text = NSAttributedString(string: presentationData.strings.TwoFactorSetup_Email_Text, font: Font.regular(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
buttonText = presentationData.strings.TwoFactorSetup_Email_Action
skipActionText = presentationData.strings.TwoFactorSetup_Email_SkipAction
changeEmailActionText = ""
resendCodeActionText = ""
inputNodes = [
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .email, placeholder: presentationData.strings.TwoFactorSetup_Email_Placeholder, focused: { node in
focused?(node)
}, next: { node in
next?(node)
}, updated: { node in
updated?(node)
}, toggleTextHidden: { node in
toggleTextHidden?(node)
}),
]
case let .emailConfirmation(_, emailPattern):
self.animationNode.switchTo(animationMail)
title = presentationData.strings.TwoFactorSetup_EmailVerification_Title
let (rawText, ranges) = presentationData.strings.TwoFactorSetup_EmailVerification_Text(emailPattern)
let string = NSMutableAttributedString()
string.append(NSAttributedString(string: rawText, font: Font.regular(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor))
for (_, range) in ranges {
string.addAttribute(.font, value: Font.semibold(16.0), range: range)
}
text = string
buttonText = presentationData.strings.TwoFactorSetup_EmailVerification_Action
skipActionText = ""
changeEmailActionText = presentationData.strings.TwoFactorSetup_EmailVerification_ChangeAction
resendCodeActionText = presentationData.strings.TwoFactorSetup_EmailVerification_ResendAction
inputNodes = [
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .code, placeholder: presentationData.strings.TwoFactorSetup_EmailVerification_Placeholder, focused: { node in
focused?(node)
}, next: { node in
next?(node)
}, updated: { node in
updated?(node)
}, toggleTextHidden: { node in
toggleTextHidden?(node)
}),
]
case .passwordHint:
self.animationNode.switchTo(animationHint)
title = presentationData.strings.TwoFactorSetup_Hint_Title
text = NSAttributedString(string: presentationData.strings.TwoFactorSetup_Hint_Text, font: Font.regular(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
buttonText = presentationData.strings.TwoFactorSetup_Hint_Action
skipActionText = presentationData.strings.TwoFactorSetup_Hint_SkipAction
changeEmailActionText = ""
resendCodeActionText = ""
inputNodes = [
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .hint, placeholder: presentationData.strings.TwoFactorSetup_Hint_Placeholder, focused: { node in
focused?(node)
}, next: { node in
next?(node)
}, updated: { node in
updated?(node)
}, toggleTextHidden: { node in
toggleTextHidden?(node)
}),
]
}
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(28.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.titleNode.maximumNumberOfLines = 0
self.titleNode.textAlignment = .center
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.attributedText = text
self.textNode.maximumNumberOfLines = 0
self.textNode.lineSpacing = 0.1
self.textNode.textAlignment = .center
self.skipActionTitleNode = ImmediateTextNode()
self.skipActionTitleNode.isUserInteractionEnabled = false
self.skipActionTitleNode.displaysAsynchronously = false
self.skipActionTitleNode.attributedText = NSAttributedString(string: skipActionText, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemAccentColor)
self.skipActionButtonNode = HighlightTrackingButtonNode()
self.skipActionTitleNode.isHidden = skipActionText.isEmpty
self.skipActionButtonNode.isHidden = skipActionText.isEmpty
self.changeEmailActionTitleNode = ImmediateTextNode()
self.changeEmailActionTitleNode.isUserInteractionEnabled = false
self.changeEmailActionTitleNode.displaysAsynchronously = false
self.changeEmailActionTitleNode.attributedText = NSAttributedString(string: changeEmailActionText, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemAccentColor)
self.changeEmailActionButtonNode = HighlightTrackingButtonNode()
self.changeEmailActionButtonNode.isHidden = changeEmailActionText.isEmpty
self.changeEmailActionButtonNode.isHidden = changeEmailActionText.isEmpty
self.resendCodeActionTitleNode = ImmediateTextNode()
self.resendCodeActionTitleNode.isUserInteractionEnabled = false
self.resendCodeActionTitleNode.displaysAsynchronously = false
self.resendCodeActionTitleNode.attributedText = NSAttributedString(string: resendCodeActionText, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemAccentColor)
self.resendCodeActionButtonNode = HighlightTrackingButtonNode()
self.resendCodeActionTitleNode.isHidden = resendCodeActionText.isEmpty
self.resendCodeActionButtonNode.isHidden = resendCodeActionText.isEmpty
self.inputNodes = inputNodes
self.buttonNode = SolidRoundedButtonNode(title: buttonText, theme: SolidRoundedButtonTheme(backgroundColor: self.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor), height: 50.0, cornerRadius: 10.0, gloss: false)
super.init()
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.addSubnode(self.scrollNode)
self.scrollNode.addSubnode(self.animationNode)
self.scrollNode.addSubnode(self.titleNode)
self.scrollNode.addSubnode(self.textNode)
self.scrollNode.addSubnode(self.skipActionTitleNode)
self.scrollNode.addSubnode(self.skipActionButtonNode)
self.scrollNode.addSubnode(self.changeEmailActionTitleNode)
self.scrollNode.addSubnode(self.changeEmailActionButtonNode)
self.scrollNode.addSubnode(self.resendCodeActionTitleNode)
self.scrollNode.addSubnode(self.resendCodeActionButtonNode)
self.scrollNode.addSubnode(self.buttonNode)
for (inputNode) in self.inputNodes {
self.scrollNode.addSubnode(inputNode)
}
self.navigationBackgroundNode.addSubnode(self.navigationSeparatorNode)
self.addSubnode(self.navigationBackgroundNode)
self.buttonNode.pressed = {
action()
}
self.skipActionButtonNode.highligthedChanged = { [weak self] highlighted in
guard let strongSelf = self else {
return
}
if highlighted {
strongSelf.skipActionTitleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.skipActionTitleNode.alpha = 0.4
} else {
strongSelf.skipActionTitleNode.alpha = 1.0
strongSelf.skipActionTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
self.skipActionButtonNode.addTarget(self, action: #selector(self.skipActionPressed), forControlEvents: .touchUpInside)
self.changeEmailActionButtonNode.highligthedChanged = { [weak self] highlighted in
guard let strongSelf = self else {
return
}
if highlighted {
strongSelf.changeEmailActionTitleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.changeEmailActionTitleNode.alpha = 0.4
} else {
strongSelf.changeEmailActionTitleNode.alpha = 1.0
strongSelf.changeEmailActionTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
self.changeEmailActionButtonNode.addTarget(self, action: #selector(self.changeEmailActionPressed), forControlEvents: .touchUpInside)
self.resendCodeActionButtonNode.highligthedChanged = { [weak self] highlighted in
guard let strongSelf = self else {
return
}
if highlighted {
strongSelf.resendCodeActionTitleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.resendCodeActionTitleNode.alpha = 0.4
} else {
strongSelf.resendCodeActionTitleNode.alpha = 1.0
strongSelf.resendCodeActionTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
self.resendCodeActionButtonNode.addTarget(self, action: #selector(self.resendCodeActionPressed), forControlEvents: .touchUpInside)
next = { [weak self] node in
guard let strongSelf = self else {
return
}
}
focused = { [weak self] node in
DispatchQueue.main.async {
guard let strongSelf = self else {
return
}
}
}
var textHidden = true
let updateAnimations: () -> Void = { [weak self] in
guard let strongSelf = self else {
return
}
let hasText = strongSelf.inputNodes.contains(where: { !$0.text.isEmpty })
switch strongSelf.mode {
case .password:
if !hasText {
if strongSelf.animationNode.currentItemName == animationPeek.name {
strongSelf.animationNode.switchTo(animationHideOutro)
strongSelf.animationNode.switchTo(animationIdle)
} else {
strongSelf.animationNode.switchTo(animationIdle)
}
} else if textHidden {
if strongSelf.animationNode.currentItemName == animationPeek.name {
strongSelf.animationNode.switchTo(animationHideNoIntro)
} else {
strongSelf.animationNode.switchTo(animationHide)
}
} else {
if strongSelf.animationNode.currentItemName != animationPeek.name {
if strongSelf.animationNode.currentItemName == animationHide.name {
strongSelf.animationNode.switchTo(animationPeek, noOutro: true)
} else if strongSelf.animationNode.currentItemName == animationIdle.name {
strongSelf.animationNode.switchTo(animationHideNoOutro)
strongSelf.animationNode.switchTo(animationPeek)
} else {
strongSelf.animationNode.switchTo(animationPeek, noOutro: strongSelf.animationNode.currentItemName == animationHide.name)
}
}
}
case .emailAddress:
let textLength = strongSelf.inputNodes[0].text.count
let maxWidth = strongSelf.inputNodes[0].bounds.width
if textLength == 0 || maxWidth.isZero {
strongSelf.animationNode.trackTo(frameIndex: 0)
} else {
let textNode = ImmediateTextNode()
textNode.attributedText = NSAttributedString(string: strongSelf.inputNodes[0].text, font: Font.regular(17.0), textColor: .black)
let textSize = textNode.updateLayout(CGSize(width: 1000.0, height: 100.0))
let maxTextLength = 20
let lowerBound = 14
let upperBound = 160
var trackingOffset = textSize.width / maxWidth
trackingOffset = max(0.0, min(1.0, trackingOffset))
let frameIndex = lowerBound + Int(trackingOffset * CGFloat(upperBound - lowerBound))
strongSelf.animationNode.trackTo(frameIndex: frameIndex)
}
default:
break
}
}
updated = { [weak self] _ in
guard let strongSelf = self else {
return
}
switch strongSelf.mode {
case .emailAddress:
let hasText = strongSelf.inputNodes.contains(where: { !$0.text.isEmpty })
strongSelf.buttonNode.isHidden = !hasText
strongSelf.skipActionTitleNode.isHidden = hasText
strongSelf.skipActionButtonNode.isHidden = hasText
case .emailConfirmation:
let hasText = strongSelf.inputNodes.contains(where: { !$0.text.isEmpty })
strongSelf.buttonNode.isHidden = !hasText
strongSelf.changeEmailActionTitleNode.isHidden = hasText
strongSelf.changeEmailActionButtonNode.isHidden = hasText
strongSelf.resendCodeActionTitleNode.isHidden = hasText
strongSelf.resendCodeActionButtonNode.isHidden = hasText
case .passwordHint:
let hasText = strongSelf.inputNodes.contains(where: { !$0.text.isEmpty })
strongSelf.buttonNode.isHidden = !hasText
strongSelf.skipActionTitleNode.isHidden = hasText
strongSelf.skipActionButtonNode.isHidden = hasText
case .password:
break
}
updateAnimations()
}
toggleTextHidden = { [weak self] _ in
guard let strongSelf = self else {
return
}
switch strongSelf.mode {
case .password:
textHidden = !textHidden
for node in strongSelf.inputNodes {
node.updateTextHidden(textHidden)
}
default:
break
}
updateAnimations()
}
self.inputNodes.first.flatMap { updated?($0) }
}
@objc private func skipActionPressed() {
self.skipAction()
}
@objc private func changeEmailActionPressed() {
self.changeEmailAction()
}
@objc private func resendCodeActionPressed() {
self.resendCodeAction()
}
override func didLoad() {
super.didLoad()
self.scrollNode.view.keyboardDismissMode = .none
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.canCancelContentTouches = true
//self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true
self.scrollNode.view.alwaysBounceVertical = false
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.showsHorizontalScrollIndicator = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.scrollNode.view.delegate = self
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
}
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.navigationHeight = navigationHeight
let contentAreaSize = layout.size
let availableAreaSize = CGSize(width: layout.size.width, height: layout.size.height - layout.insets(options: [.input]).bottom)
let sideInset: CGFloat = 32.0
let buttonSideInset: CGFloat = 48.0
let iconSpacing: CGFloat = 2.0
let titleSpacing: CGFloat = 19.0
let titleInputSpacing: CGFloat = 26.0
let textSpacing: CGFloat = 30.0
let buttonHeight: CGFloat = 50.0
let buttonSpacing: CGFloat = 20.0
let rowSpacing: CGFloat = 20.0
transition.updateFrame(node: self.navigationBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: contentAreaSize.width, height: navigationHeight)))
transition.updateFrame(node: self.navigationSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: contentAreaSize.width, height: UIScreenPixel)))
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: contentAreaSize))
let iconSize: CGSize = self.animationNode.intrinsicSize
let titleSize = self.titleNode.updateLayout(CGSize(width: contentAreaSize.width - sideInset * 2.0, height: contentAreaSize.height))
let textSize = self.textNode.updateLayout(CGSize(width: contentAreaSize.width - sideInset * 2.0, height: contentAreaSize.height))
let skipActionSize = self.skipActionTitleNode.updateLayout(CGSize(width: contentAreaSize.width - sideInset * 2.0, height: contentAreaSize.height))
let changeEmailActionSize = self.changeEmailActionTitleNode.updateLayout(CGSize(width: contentAreaSize.width - sideInset * 2.0, height: contentAreaSize.height))
let resendCodeActionSize = self.resendCodeActionTitleNode.updateLayout(CGSize(width: contentAreaSize.width - sideInset * 2.0, height: contentAreaSize.height))
var calculatedContentHeight = iconSize.height + iconSpacing + titleSize.height
if textSize.width.isZero {
calculatedContentHeight += titleInputSpacing
} else {
calculatedContentHeight += titleSpacing + textSize.height + textSpacing
}
for i in 0 ..< self.inputNodes.count {
if i != 0 {
calculatedContentHeight += rowSpacing
}
calculatedContentHeight += 50.0
}
calculatedContentHeight += buttonHeight + buttonSpacing
var contentHeight: CGFloat = 0.0
let insets = layout.insets(options: [.input])
let areaHeight = layout.size.height - insets.top - insets.bottom
let contentVerticalOrigin = max(layout.statusBarHeight ?? 0.0, floor((areaHeight - calculatedContentHeight) / 2.0))
let iconFrame = CGRect(origin: CGPoint(x: floor((contentAreaSize.width - iconSize.width) / 2.0), y: contentVerticalOrigin), size: iconSize)
transition.updateFrame(node: self.animationNode, frame: iconFrame)
let titleFrame = CGRect(origin: CGPoint(x: floor((contentAreaSize.width - titleSize.width) / 2.0), y: iconFrame.maxY + iconSpacing), size: titleSize)
transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame)
let textFrame: CGRect
if textSize.width.isZero {
textFrame = CGRect(origin: CGPoint(x: floor((contentAreaSize.width - textSize.width) / 2.0), y: titleFrame.maxY), size: textSize)
} else {
textFrame = CGRect(origin: CGPoint(x: floor((contentAreaSize.width - textSize.width) / 2.0), y: titleFrame.maxY + titleSpacing), size: textSize)
}
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
contentHeight = textFrame.maxY
if textSize.width.isZero {
contentHeight += titleInputSpacing
} else {
contentHeight += textSpacing
}
let rowWidth = contentAreaSize.width - buttonSideInset * 2.0
for i in 0 ..< self.inputNodes.count {
let inputNode = self.inputNodes[i]
if i != 0 {
contentHeight += rowSpacing
}
let inputNodeSize = CGSize(width: rowWidth, height: 50.0)
transition.updateFrame(node: inputNode, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: contentHeight), size: inputNodeSize))
inputNode.updateLayout(size: inputNodeSize, transition: transition)
contentHeight += inputNodeSize.height
}
let minimalBottomInset: CGFloat = 74.0
let buttonBottomInset = layout.intrinsicInsets.bottom + minimalBottomInset
let bottomInset = layout.intrinsicInsets.bottom + buttonSpacing
let buttonWidth = contentAreaSize.width - buttonSideInset * 2.0
let maxButtonY = min(areaHeight - buttonSpacing, layout.size.height - buttonBottomInset) - buttonHeight
let buttonFrame = CGRect(origin: CGPoint(x: floor((contentAreaSize.width - buttonWidth) / 2.0), y: max(contentHeight + buttonSpacing, maxButtonY)), size: CGSize(width: buttonWidth, height: buttonHeight))
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition)
transition.updateFrame(node: self.skipActionButtonNode, frame: buttonFrame)
transition.updateFrame(node: self.skipActionTitleNode, frame: CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - skipActionSize.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - skipActionSize.height) / 2.0)), size: skipActionSize))
transition.updateFrame(node: self.changeEmailActionButtonNode, frame: CGRect(origin: CGPoint(x: buttonFrame.minX, y: buttonFrame.minY), size: CGSize(width: floor(buttonFrame.width / 2.0), height: buttonFrame.height)))
transition.updateFrame(node: self.resendCodeActionButtonNode, frame: CGRect(origin: CGPoint(x: buttonFrame.maxX - floor(buttonFrame.width / 2.0), y: buttonFrame.minY), size: CGSize(width: floor(buttonFrame.width / 2.0), height: buttonFrame.height)))
transition.updateFrame(node: self.changeEmailActionTitleNode, frame: CGRect(origin: CGPoint(x: buttonFrame.minX, y: buttonFrame.minY + floor((buttonFrame.height - changeEmailActionSize.height) / 2.0)), size: changeEmailActionSize))
transition.updateFrame(node: self.resendCodeActionTitleNode, frame: CGRect(origin: CGPoint(x: buttonFrame.maxX - resendCodeActionSize.width, y: buttonFrame.minY + floor((buttonFrame.height - resendCodeActionSize.height) / 2.0)), size: resendCodeActionSize))
transition.animateView {
self.scrollNode.view.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: layout.insets(options: [.input]).bottom, right: 0.0)
self.scrollNode.view.contentSize = CGSize(width: contentAreaSize.width, height: max(availableAreaSize.height, buttonFrame.maxY + bottomInset))
}
}
}