Swiftgram/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift
2024-04-16 17:21:40 +04:00

1032 lines
56 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import TextFormat
import AuthenticationServices
import CodeInputView
import PhoneNumberFormat
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import SolidRoundedButtonNode
import AuthorizationUtils
import TelegramStringFormatting
import TextNodeWithEntities
final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextFieldDelegate {
private let strings: PresentationStrings
private let theme: PresentationTheme
private let animationNode: AnimatedStickerNode
private let titleNode: ImmediateTextNode
private let titleActivateAreaNode: AccessibilityAreaNode
private let titleIconNode: ASImageNode
private let currentOptionNode: ImmediateTextNodeWithEntities
private let currentOptionActivateAreaNode: AccessibilityAreaNode
private let currentOptionInfoNode: ASTextNode
private let currentOptionInfoActivateAreaNode: AccessibilityAreaNode
private let nextOptionTitleNode: ImmediateTextNode
private let nextOptionButtonNode: HighlightableButtonNode
private let nextOptionArrowNode: ASImageNode
private let resetTextNode: ImmediateTextNode
private let resetNode: HighlightableButtonNode
private let dividerNode: AuthorizationDividerNode
private var signInWithAppleButton: UIControl?
private let proceedNode: SolidRoundedButtonNode
private let textField: TextFieldNode
private let textSeparatorNode: ASDisplayNode
private let pasteButton: HighlightableButtonNode
private let codeInputView: CodeInputView
private let errorTextNode: ImmediateTextNode
private let hintButtonNode: HighlightTrackingButtonNode
private let hintTextNode: ImmediateTextNode
private let hintArrowNode: ASImageNode
private var codeType: SentAuthorizationCodeType?
private let countdownDisposable = MetaDisposable()
private var currentTimeoutTime: Int32?
private var layoutArguments: (ContainerViewLayout, CGFloat)?
private var appleSignInAllowed = false
private var hasPreviousCode = false
private var previousIsPhrase = false
var phoneNumber: String = "" {
didSet {
if self.phoneNumber != oldValue {
if let (layout, navigationHeight) = self.layoutArguments {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate)
}
}
}
}
var email: String?
var currentCode: String {
switch self.codeType {
case .word, .phrase:
return self.textField.textField.text ?? ""
default:
return self.codeInputView.text
}
}
var loginWithCode: ((String) -> Void)?
var signInWithApple: (() -> Void)?
var openFragment: ((String) -> Void)?
var present: (ViewController, Any?) -> Void = { _, _ in }
var requestNextOption: (() -> Void)?
var requestAnotherOption: (() -> Void)?
var requestPreviousOption: (() -> Void)?
var updateNextEnabled: ((Bool) -> Void)?
var reset: (() -> Void)?
var retryReset: (() -> Void)?
var inProgress: Bool = false {
didSet {
self.codeInputView.alpha = self.inProgress ? 0.6 : 1.0
switch self.codeType {
case .word, .phrase:
if self.inProgress != oldValue {
if self.inProgress {
self.proceedNode.transitionToProgress()
} else {
self.proceedNode.transitionFromProgress()
}
}
default:
break
}
}
}
private let appearanceTimestamp = CACurrentMediaTime()
init(strings: PresentationStrings, theme: PresentationTheme) {
self.strings = strings
self.theme = theme
self.animationNode = DefaultAnimatedStickerNodeImpl()
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 0
self.titleNode.textAlignment = .center
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.titleActivateAreaNode = AccessibilityAreaNode()
self.titleActivateAreaNode.accessibilityTraits = .staticText
self.titleIconNode = ASImageNode()
self.titleIconNode.isLayerBacked = true
self.titleIconNode.displayWithoutProcessing = true
self.titleIconNode.displaysAsynchronously = false
self.currentOptionNode = ImmediateTextNodeWithEntities()
self.currentOptionNode.balancedTextLayout = true
self.currentOptionNode.isUserInteractionEnabled = false
self.currentOptionNode.displaysAsynchronously = false
self.currentOptionNode.lineSpacing = 0.1
self.currentOptionNode.maximumNumberOfLines = 0
self.currentOptionNode.spoilerColor = self.theme.list.itemSecondaryTextColor
self.currentOptionActivateAreaNode = AccessibilityAreaNode()
self.currentOptionActivateAreaNode.accessibilityTraits = .staticText
self.currentOptionInfoNode = ASTextNode()
self.currentOptionInfoNode.isUserInteractionEnabled = false
self.currentOptionInfoNode.displaysAsynchronously = false
self.currentOptionInfoActivateAreaNode = AccessibilityAreaNode()
self.currentOptionInfoActivateAreaNode.accessibilityTraits = .staticText
self.nextOptionTitleNode = ImmediateTextNode()
self.nextOptionButtonNode = HighlightableButtonNode()
self.nextOptionButtonNode.displaysAsynchronously = false
let (nextOptionText, nextOptionActive) = authorizationNextOptionText(currentType: .sms(length: 5), nextType: .call, timeout: 60, strings: self.strings, primaryColor: self.theme.list.itemPrimaryTextColor, accentColor: self.theme.list.itemAccentColor)
self.nextOptionTitleNode.attributedText = nextOptionText
self.nextOptionButtonNode.isUserInteractionEnabled = nextOptionActive
self.nextOptionButtonNode.accessibilityLabel = nextOptionText.string
if nextOptionActive {
self.nextOptionButtonNode.accessibilityTraits = [.button]
} else {
self.nextOptionButtonNode.accessibilityTraits = [.button, .notEnabled]
}
self.nextOptionButtonNode.addSubnode(self.nextOptionTitleNode)
self.nextOptionArrowNode = ASImageNode()
self.nextOptionArrowNode.displaysAsynchronously = false
self.nextOptionArrowNode.isUserInteractionEnabled = false
self.codeInputView = CodeInputView()
self.codeInputView.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
self.codeInputView.textField.returnKeyType = .done
self.codeInputView.textField.disableAutomaticKeyboardHandling = [.forward, .backward]
if #available(iOSApplicationExtension 12.0, iOS 12.0, *) {
self.codeInputView.textField.textContentType = .oneTimeCode
}
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.codeInputView.textField.keyboardType = .asciiCapableNumberPad
} else {
self.codeInputView.textField.keyboardType = .numberPad
}
self.textSeparatorNode = ASDisplayNode()
self.textSeparatorNode.isLayerBacked = true
self.textSeparatorNode.backgroundColor = self.theme.list.itemPlainSeparatorColor
self.textField = TextFieldNode()
self.textField.textField.font = Font.regular(20.0)
self.textField.textField.textColor = self.theme.list.itemPrimaryTextColor
self.textField.textField.textAlignment = .natural
self.textField.textField.autocorrectionType = .yes
self.textField.textField.autocorrectionType = .no
self.textField.textField.spellCheckingType = .yes
self.textField.textField.spellCheckingType = .no
self.textField.textField.autocapitalizationType = .none
self.textField.textField.keyboardType = .default
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.textField.textField.textContentType = UITextContentType(rawValue: "")
}
self.textField.textField.returnKeyType = .default
self.textField.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
self.textField.textField.disableAutomaticKeyboardHandling = [.forward, .backward]
self.textField.textField.tintColor = self.theme.list.itemAccentColor
self.pasteButton = HighlightableButtonNode()
self.errorTextNode = ImmediateTextNode()
self.errorTextNode.alpha = 0.0
self.errorTextNode.displaysAsynchronously = false
self.errorTextNode.textAlignment = .center
self.errorTextNode.isUserInteractionEnabled = false
self.hintButtonNode = HighlightableButtonNode()
self.hintButtonNode.alpha = 0.0
self.hintButtonNode.isUserInteractionEnabled = false
self.hintTextNode = ImmediateTextNode()
self.hintTextNode.displaysAsynchronously = false
self.hintTextNode.textAlignment = .center
self.hintTextNode.isUserInteractionEnabled = false
self.hintArrowNode = ASImageNode()
self.hintArrowNode.displaysAsynchronously = false
self.resetNode = HighlightableButtonNode()
self.resetNode.displaysAsynchronously = false
self.resetNode.setAttributedTitle(NSAttributedString(string: self.strings.Login_Email_CantAccess, font: Font.regular(17.0), textColor: self.theme.list.itemAccentColor, paragraphAlignment: .center), for: [])
self.resetNode.isHidden = true
self.resetTextNode = ImmediateTextNode()
self.resetTextNode.maximumNumberOfLines = 1
self.resetTextNode.textAlignment = .center
self.resetTextNode.isUserInteractionEnabled = false
self.resetTextNode.displaysAsynchronously = false
self.resetTextNode.isHidden = true
self.dividerNode = AuthorizationDividerNode(theme: self.theme, strings: self.strings)
if #available(iOS 13.0, *) {
self.signInWithAppleButton = ASAuthorizationAppleIDButton(authorizationButtonType: .signIn, authorizationButtonStyle: theme.overallDarkAppearance ? .white : .black)
self.signInWithAppleButton?.isHidden = true
(self.signInWithAppleButton as? ASAuthorizationAppleIDButton)?.cornerRadius = 11
}
self.proceedNode = SolidRoundedButtonNode(title: self.strings.Login_Continue, theme: SolidRoundedButtonTheme(theme: self.theme), height: 50.0, cornerRadius: 11.0, gloss: false)
self.proceedNode.progressType = .embedded
self.proceedNode.isHidden = true
self.proceedNode.iconSpacing = 4.0
self.proceedNode.animationSize = CGSize(width: 36.0, height: 36.0)
self.proceedNode.isEnabled = false
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.backgroundColor = self.theme.list.plainBackgroundColor
self.textField.textField.delegate = self
self.addSubnode(self.codeInputView)
self.addSubnode(self.textSeparatorNode)
self.addSubnode(self.textField)
self.addSubnode(self.pasteButton)
self.addSubnode(self.titleNode)
self.addSubnode(self.titleActivateAreaNode)
self.addSubnode(self.titleIconNode)
self.addSubnode(self.currentOptionNode)
self.addSubnode(self.currentOptionActivateAreaNode)
self.addSubnode(self.currentOptionInfoNode)
self.addSubnode(self.nextOptionButtonNode)
self.nextOptionButtonNode.addSubnode(self.nextOptionArrowNode)
self.addSubnode(self.animationNode)
self.addSubnode(self.resetNode)
self.addSubnode(self.resetTextNode)
self.addSubnode(self.dividerNode)
self.addSubnode(self.errorTextNode)
self.addSubnode(self.hintButtonNode)
self.hintButtonNode.addSubnode(self.hintTextNode)
self.hintButtonNode.addSubnode(self.hintArrowNode)
self.addSubnode(self.proceedNode)
self.codeInputView.updated = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.codeChanged(text: strongSelf.codeInputView.text)
}
self.textField.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged)
self.codeInputView.longPressed = { [weak self] in
guard let strongSelf = self else {
return
}
if let code = UIPasteboard.general.string, let codeLength = strongSelf.requiredCodeLength, code.count == Int(codeLength) {
let code = normalizeArabicNumeralString(code, type: .western)
guard code.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789").inverted) == nil else {
return
}
let controller = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: strongSelf.strings.Common_Paste, accessibilityLabel: strongSelf.strings.Common_Paste), action: { [weak self] in
self?.updateCode(code)
})])
strongSelf.present(
controller,
ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
if let strongSelf = self {
return (strongSelf, strongSelf.codeInputView.frame.offsetBy(dx: 0.0, dy: -8.0), strongSelf, strongSelf.bounds)
} else {
return nil
}
})
)
}
}
self.nextOptionButtonNode.addTarget(self, action: #selector(self.nextOptionNodePressed), forControlEvents: .touchUpInside)
self.proceedNode.pressed = { [weak self] in
self?.proceedPressed()
}
self.signInWithAppleButton?.addTarget(self, action: #selector(self.signInWithApplePressed), for: .touchUpInside)
self.resetNode.addTarget(self, action: #selector(self.resetPressed), forControlEvents: .touchUpInside)
self.pasteButton.setTitle(strings.Login_Paste, with: Font.medium(13.0), with: theme.list.itemAccentColor, for: .normal)
self.pasteButton.setBackgroundImage(generateStretchableFilledCircleImage(radius: 12.0, color: theme.list.itemAccentColor.withAlphaComponent(0.1)), for: .normal)
self.pasteButton.addTarget(self, action: #selector(self.pastePressed), forControlEvents: .touchUpInside)
self.hintButtonNode.addTarget(self, action: #selector(self.previousOptionNodePressed), forControlEvents: .touchUpInside)
self.hintArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: theme.list.itemAccentColor)
self.hintArrowNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
self.nextOptionArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: theme.list.itemAccentColor)
self.updatePasteVisibility()
}
deinit {
self.countdownDisposable.dispose()
}
override func didLoad() {
super.didLoad()
if let signInWithAppleButton = self.signInWithAppleButton {
self.view.addSubview(signInWithAppleButton)
}
}
@objc private func pastePressed() {
if let text = UIPasteboard.general.string, !text.isEmpty {
if checkValidity(text: text) {
self.textField.textField.text = text
}
}
}
func updatePasteVisibility() {
self.pasteButton.isHidden = !UIPasteboard.general.hasStrings
}
func updateCode(_ code: String) {
self.codeInputView.text = code
self.codeChanged(text: code)
if let codeLength = self.requiredCodeLength, code.count == Int(codeLength) {
self.loginWithCode?(code)
}
}
var requiredCodeLength: Int32? {
if let codeType = self.codeType {
switch codeType {
case let .call(length):
return length
case let .otherSession(length):
return length
case let .missedCall(_, length):
return length
case let .sms(length):
return length
case let .fragment(_, length):
return length
default:
return nil
}
} else {
return nil
}
}
func resetCode() {
self.codeInputView.text = ""
}
func updateData(number: String, email: String?, codeType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, appleSignInAllowed: Bool, hasPreviousCode: Bool, previousIsPhrase: Bool) {
self.codeType = codeType
self.phoneNumber = number
self.email = email
self.hasPreviousCode = hasPreviousCode
self.previousIsPhrase = previousIsPhrase
var appleSignInAllowed = appleSignInAllowed
if #available(iOS 13.0, *) {
} else {
appleSignInAllowed = false
}
self.appleSignInAllowed = appleSignInAllowed
self.currentOptionNode.attributedText = authorizationCurrentOptionText(codeType, phoneNumber: self.phoneNumber, email: self.email, strings: self.strings, primaryColor: self.theme.list.itemPrimaryTextColor, accentColor: self.theme.list.itemAccentColor)
self.currentOptionActivateAreaNode.accessibilityLabel = self.currentOptionNode.attributedText?.string ?? ""
if case .missedCall = codeType {
self.currentOptionInfoNode.attributedText = NSAttributedString(string: self.strings.Login_CodePhonePatternInfoText, font: Font.regular(17.0), textColor: self.theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
self.currentOptionInfoActivateAreaNode.accessibilityLabel = self.currentOptionInfoNode.attributedText?.string ?? ""
if self.currentOptionInfoActivateAreaNode.supernode == nil {
self.addSubnode(self.currentOptionInfoActivateAreaNode)
}
} else {
self.currentOptionInfoNode.attributedText = NSAttributedString(string: "", font: Font.regular(17.0), textColor: self.theme.list.itemPrimaryTextColor)
if self.currentOptionInfoActivateAreaNode.supernode != nil {
self.currentOptionInfoActivateAreaNode.removeFromSupernode()
}
}
if let timeout = timeout {
#if DEBUG
let timeout = min(timeout, 5)
#endif
self.currentTimeoutTime = timeout
let disposable = ((Signal<Int, NoError>.single(1) |> delay(1.0, queue: Queue.mainQueue())) |> restart).startStrict(next: { [weak self] _ in
if let strongSelf = self {
if let currentTimeoutTime = strongSelf.currentTimeoutTime, currentTimeoutTime > 0 {
strongSelf.currentTimeoutTime = currentTimeoutTime - 1
let (nextOptionText, nextOptionActive) = authorizationNextOptionText(currentType: codeType, nextType: nextType, returnToCode: hasPreviousCode, timeout: strongSelf.currentTimeoutTime, strings: strongSelf.strings, primaryColor: strongSelf.theme.list.itemPrimaryTextColor, accentColor: strongSelf.theme.list.itemAccentColor)
strongSelf.nextOptionTitleNode.attributedText = nextOptionText
strongSelf.nextOptionButtonNode.isUserInteractionEnabled = nextOptionActive
strongSelf.nextOptionButtonNode.accessibilityLabel = nextOptionText.string
if nextOptionActive {
strongSelf.nextOptionButtonNode.accessibilityTraits = [.button]
} else {
strongSelf.nextOptionButtonNode.accessibilityTraits = [.button, .notEnabled]
}
if let layoutArguments = strongSelf.layoutArguments {
strongSelf.containerLayoutUpdated(layoutArguments.0, navigationBarHeight: layoutArguments.1, transition: .immediate)
}
/*if currentTimeoutTime == 1 {
strongSelf.requestNextOption?()
}*/
}
}
})
self.countdownDisposable.set(disposable)
} else if case let .email(_, _, _, pendingDate, _, _) = codeType, let pendingDate {
let disposable = ((Signal<Int, NoError>.single(1) |> delay(1.0, queue: Queue.mainQueue())) |> restart).startStrict(next: { [weak self] _ in
if let strongSelf = self {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let interval = pendingDate - currentTime
if interval <= 0 {
strongSelf.countdownDisposable.set(nil)
Queue.mainQueue().after(2.0) {
strongSelf.retryReset?()
}
}
if let layoutArguments = strongSelf.layoutArguments {
strongSelf.containerLayoutUpdated(layoutArguments.0, navigationBarHeight: layoutArguments.1, transition: .immediate)
}
}
})
self.countdownDisposable.set(disposable)
} else {
self.currentTimeoutTime = nil
self.countdownDisposable.set(nil)
}
let (nextOptionText, nextOptionActive) = authorizationNextOptionText(currentType: codeType, nextType: nextType, returnToCode: hasPreviousCode, timeout: self.currentTimeoutTime, strings: self.strings, primaryColor: self.theme.list.itemPrimaryTextColor, accentColor: self.theme.list.itemAccentColor)
self.nextOptionTitleNode.attributedText = nextOptionText
self.nextOptionButtonNode.isUserInteractionEnabled = nextOptionActive
self.nextOptionButtonNode.accessibilityLabel = nextOptionText.string
if nextOptionActive {
self.nextOptionButtonNode.accessibilityTraits = [.button]
} else {
self.nextOptionButtonNode.accessibilityTraits = [.button, .notEnabled]
}
switch codeType {
case .word, .phrase:
self.nextOptionArrowNode.isHidden = !hasPreviousCode
default:
self.nextOptionArrowNode.isHidden = true
}
if let layoutArguments = self.layoutArguments {
self.containerLayoutUpdated(layoutArguments.0, navigationBarHeight: layoutArguments.1, transition: .immediate)
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let previousInputHeight = self.layoutArguments?.0.inputHeight ?? 0.0
let newInputHeight = layout.inputHeight ?? 0.0
self.layoutArguments = (layout, navigationBarHeight)
var layout = layout
if CACurrentMediaTime() - self.appearanceTimestamp < 2.0, newInputHeight < previousInputHeight {
layout = layout.withUpdatedInputHeight(previousInputHeight)
}
let maximumWidth: CGFloat = min(430.0, layout.size.width)
let inset: CGFloat = 24.0
var insets = layout.insets(options: [])
insets.top = layout.statusBarHeight ?? 20.0
var animationName = "IntroMessage"
var animationPlaybackMode: AnimatedStickerPlaybackMode = .once
var textFieldPlaceholder = ""
if let codeType = self.codeType {
switch codeType {
case .missedCall:
self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_EnterMissingDigits, font: Font.semibold(28.0), textColor: self.theme.list.itemPrimaryTextColor)
case .email:
self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_EnterCodeEmailTitle, font: Font.semibold(28.0), textColor: self.theme.list.itemPrimaryTextColor)
animationName = "IntroLetter"
case .sms:
self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_EnterCodeSMSTitle, font: Font.semibold(28.0), textColor: self.theme.list.itemPrimaryTextColor)
case .fragment:
self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_EnterCodeFragmentTitle, font: Font.semibold(28.0), textColor: self.theme.list.itemPrimaryTextColor)
self.proceedNode.title = self.strings.Login_OpenFragment
self.proceedNode.updateTheme(SolidRoundedButtonTheme(backgroundColor: UIColor(rgb: 0x37475a), foregroundColor: .white))
self.proceedNode.isEnabled = true
animationName = "IntroFragment"
animationPlaybackMode = .count(3)
self.proceedNode.animation = "anim_fragment"
case .word:
self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_EnterWordTitle, font: Font.semibold(28.0), textColor: self.theme.list.itemPrimaryTextColor)
textFieldPlaceholder = self.strings.Login_EnterWordPlaceholder
case .phrase:
self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_EnterPhraseTitle, font: Font.semibold(28.0), textColor: self.theme.list.itemPrimaryTextColor)
textFieldPlaceholder = self.strings.Login_EnterPhrasePlaceholder
default:
self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_EnterCodeTelegramTitle, font: Font.semibold(28.0), textColor: self.theme.list.itemPrimaryTextColor)
}
} else {
self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_EnterCodeTelegramTitle, font: Font.semibold(40.0), textColor: self.theme.list.itemPrimaryTextColor)
}
self.textField.textField.placeholder = textFieldPlaceholder
self.titleActivateAreaNode.accessibilityLabel = self.titleNode.attributedText?.string ?? ""
if let inputHeight = layout.inputHeight {
switch self.codeType {
case .email, .fragment:
insets.bottom = max(inputHeight, insets.bottom)
case .word, .phrase:
insets.bottom = max(inputHeight, layout.standardKeyboardHeight)
default:
insets.bottom = max(inputHeight, layout.standardInputHeight)
}
}
if !self.animationNode.visibility {
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 256, height: 256, playbackMode: animationPlaybackMode, mode: .direct(cachePathPrefix: nil))
self.animationNode.visibility = true
}
let animationSize = CGSize(width: 100.0, height: 100.0)
let titleSize = self.titleNode.updateLayout(CGSize(width: maximumWidth, height: CGFloat.greatestFiniteMagnitude))
let currentOptionSize = self.currentOptionNode.updateLayout(CGSize(width: maximumWidth - 48.0, height: CGFloat.greatestFiniteMagnitude))
let currentOptionInfoSize = self.currentOptionInfoNode.measure(CGSize(width: maximumWidth - 48.0, height: CGFloat.greatestFiniteMagnitude))
let nextOptionSize = self.nextOptionTitleNode.updateLayout(CGSize(width: maximumWidth, height: CGFloat.greatestFiniteMagnitude))
let proceedHeight = self.proceedNode.updateLayout(width: maximumWidth - inset * 2.0, transition: transition)
let proceedSize = CGSize(width: maximumWidth - inset * 2.0, height: proceedHeight)
let codeLength: Int
var codePrefix: String = ""
switch self.codeType {
case .flashCall:
codeLength = 6
case let .call(length):
codeLength = Int(length)
case let .otherSession(length):
codeLength = Int(length)
case let .missedCall(prefix, length):
if prefix.hasPrefix("+") {
codePrefix = prefix
} else {
codePrefix = InteractivePhoneFormatter().updateText("+" + prefix).1
}
codeLength = Int(length)
case let .sms(length):
codeLength = Int(length)
case let .email(_, length, _, _, _, _):
codeLength = Int(length)
case let .fragment(_, length):
codeLength = Int(length)
case let .firebase(_, length):
codeLength = Int(length)
case .emailSetupRequired:
codeLength = 6
case .word, .phrase:
codeLength = 0
case .none:
codeLength = 6
}
let codeFieldSize = self.codeInputView.update(
theme: CodeInputView.Theme(
inactiveBorder: self.theme.list.itemPlainSeparatorColor.argb,
activeBorder: self.theme.list.itemAccentColor.argb,
succeedBorder: self.theme.list.itemDisclosureActions.constructive.fillColor.argb,
failedBorder: self.theme.list.itemDestructiveColor.argb,
foreground: self.theme.list.itemPrimaryTextColor.argb,
isDark: self.theme.overallDarkAppearance
),
prefix: codePrefix,
count: codeLength,
width: maximumWidth - 28.0,
compact: layout.size.width <= 320.0 || (layout.size.width <= 375.0 && codeLength > 5)
)
var items: [AuthorizationLayoutItem] = []
if layout.size.width > 320.0 {
items.append(AuthorizationLayoutItem(node: self.animationNode, size: animationSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
self.animationNode.updateLayout(size: animationSize)
self.animationNode.isHidden = false
self.animationNode.visibility = true
} else {
insets.top = navigationBarHeight
self.animationNode.isHidden = true
}
var additionalBottomInset: CGFloat = 20.0
if let codeType = self.codeType {
switch codeType {
case .otherSession:
items.append(AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 18.0, maxValue: 18.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.currentOptionNode, size: currentOptionSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.codeInputView, size: codeFieldSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 30.0, maxValue: 30.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.nextOptionButtonNode, size: nextOptionSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 50.0, maxValue: 120.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
case .missedCall:
self.titleIconNode.isHidden = false
if self.titleIconNode.image == nil {
self.titleIconNode.image = generateImage(CGSize(width: 72.0, height: 72.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(theme.list.itemAccentColor.cgColor)
let _ = try? drawSvgPath(context, path: "M42,10.5 C41.1716,10.5 40.5,11.1716 40.5,12 C40.5,12.8284 41.1716,13.5 42,13.5 L51.3787,13.5 L36,28.8787 L19.0607,11.9393 C18.4749,11.3536 17.5251,11.3536 16.9393,11.9393 C16.3536,12.5251 16.3536,13.4749 16.9393,14.0607 L34.9393,32.0607 C35.5251,32.6464 36.4749,32.6464 37.0607,32.0607 L53.5,15.6213 L53.5,25 C53.5,25.8284 54.1716,26.5 55,26.5 C55.8284,26.5 56.5,25.8284 56.5,25 L56.5,12 C56.5,11.1716 55.8284,10.5 55,10.5 L42,10.5 Z ")
context.setFillColor(theme.list.itemPrimaryTextColor.cgColor)
let _ = try? drawSvgPath(context, path: "M35.9832,37.4038 C46.3353,37.4066 56.7252,39.7842 62.0325,45.0915 C64.3893,47.4483 65.7444,50.3613 65.6897,53.8677 C65.6717,56.0012 64.9858,57.8376 63.8173,59.0061 C62.8158,60.0076 61.4987,60.5082 59.9403,60.248 L51.6994,58.3061 C49.2077,57.719 47.3333,55.6605 46.9816,53.1249 L46.264,47.9528 C46.2639,47.5446 46.1154,47.2478 45.8742,47.0065 C45.6515,46.7838 45.3175,46.6353 45.0206,46.5239 C43.3508,45.9298 39.7701,45.5763 35.9855,45.5753 C32.2194,45.5557 28.6389,45.9815 26.9694,46.5005 C26.6726,46.6117 26.3387,46.76 26.079,47.0197 C25.8194,47.2793 25.6525,47.5947 25.6526,48.0028 L24.9872,53.09 C24.6524,55.6494 22.7664,57.7335 20.253,58.3214 L11.8346,60.2905 C10.2949,60.5684 9.1074,60.0486 8.2166,59.1579 C6.9733,57.9145 6.3791,55.9107 6.3229,53.9628 C6.1921,50.4193 7.4343,47.5069 9.8639,45.0773 C15.1684,39.7728 25.6683,37.401 35.9832,37.4038 Z ")
})
}
items.append(AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 18.0, maxValue: 18.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.currentOptionNode, size: currentOptionSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.codeInputView, size: codeFieldSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 40.0, maxValue: 100.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.currentOptionInfoNode, size: currentOptionInfoSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 60.0, maxValue: 100.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.nextOptionButtonNode, size: nextOptionSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 50.0, maxValue: 120.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
default:
items.append(AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 18.0, maxValue: 18.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.currentOptionNode, size: currentOptionSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 18.0, maxValue: 18.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
var canReset = false
var pendingDate: Int32?
if case let .email(_, _, resetPeriod, pendingDateValue, _, setup) = codeType, !setup {
if resetPeriod != nil {
canReset = true
} else if pendingDateValue != nil {
pendingDate = pendingDateValue
}
}
switch codeType {
case .word, .phrase:
self.codeInputView.isHidden = true
self.textField.isHidden = false
self.textSeparatorNode.isHidden = false
items.append(AuthorizationLayoutItem(node: self.textField, size: CGSize(width: maximumWidth - 88.0, height: 44.0), spacingBefore: AuthorizationLayoutItemSpacing(weight: 18.0, maxValue: 30.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.textSeparatorNode, size: CGSize(width: maximumWidth - 48.0, height: UIScreenPixel), spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
default:
self.codeInputView.isHidden = false
self.textField.isHidden = true
self.textSeparatorNode.isHidden = true
items.append(AuthorizationLayoutItem(node: self.codeInputView, size: codeFieldSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 30.0, maxValue: 30.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: canReset || pendingDate != nil ? 0.0 : 104.0, maxValue: canReset ? 0.0 : 104.0)))
}
if canReset {
self.resetNode.setAttributedTitle(NSAttributedString(string: self.strings.Login_Email_CantAccess, font: Font.regular(17.0), textColor: self.theme.list.itemAccentColor, paragraphAlignment: .center), for: [])
let resetSize = self.resetNode.measure(CGSize(width: maximumWidth, height: CGFloat.greatestFiniteMagnitude))
self.resetTextNode.isHidden = true
self.resetNode.isHidden = false
items.append(AuthorizationLayoutItem(node: self.resetNode, size: resetSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 36.0, maxValue: 36.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 104.0, maxValue: 104.0)))
} else if let pendingDate {
self.resetNode.setAttributedTitle(NSAttributedString(string: self.strings.Login_Email_ResetNowViaSMS, font: Font.regular(17.0), textColor: self.theme.list.itemAccentColor, paragraphAlignment: .center), for: [])
let resetSize = self.resetNode.measure(CGSize(width: maximumWidth, height: CGFloat.greatestFiniteMagnitude))
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let resetText: String
let interval = pendingDate - currentTime
if interval <= 0 {
resetText = self.strings.Login_Email_ResetingNow
} else if interval < 60 * 60 * 24 {
let minutes = interval / 60
let seconds = interval % 60
let timeString = String(format: "%d:%.02d", Int(minutes), Int(seconds))
resetText = self.strings.Login_Email_ElapsedTime(timeString).string
} else {
resetText = unmuteIntervalString(strings: self.strings, value: interval)
}
self.resetTextNode.attributedText = NSAttributedString(string: self.strings.Login_Email_WillBeResetIn(resetText).string, font: Font.regular(16.0), textColor: self.theme.list.itemSecondaryTextColor, paragraphAlignment: .center)
let resetTextSize = self.resetTextNode.updateLayout(CGSize(width: maximumWidth, height: CGFloat.greatestFiniteMagnitude))
if !self.resetNode.isHidden && self.resetTextNode.isHidden {
self.resetTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
self.resetTextNode.isHidden = false
items.append(AuthorizationLayoutItem(node: self.resetTextNode, size: resetTextSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 36.0, maxValue: 36.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
self.resetNode.isHidden = false
items.append(AuthorizationLayoutItem(node: self.resetNode, size: resetSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 20.0, maxValue: 20.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 104.0, maxValue: 104.0)))
} else {
self.resetTextNode.isHidden = true
self.resetNode.isHidden = true
}
let inset: CGFloat = 24.0
if case .fragment = codeType {
self.proceedNode.isHidden = false
let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - proceedSize.width) / 2.0), y: layout.size.height - insets.bottom - proceedSize.height - inset), size: proceedSize)
transition.updateFrame(node: self.proceedNode, frame: buttonFrame)
} else if self.appleSignInAllowed, let signInWithAppleButton = self.signInWithAppleButton {
additionalBottomInset = 80.0
self.nextOptionButtonNode.isHidden = true
signInWithAppleButton.isHidden = false
self.proceedNode.isHidden = true
let buttonSize = CGSize(width: layout.size.width - inset * 2.0, height: 50.0)
transition.updateFrame(view: signInWithAppleButton, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - buttonSize.width) / 2.0), y: layout.size.height - insets.bottom - buttonSize.height - inset), size: buttonSize))
let dividerSize = self.dividerNode.updateLayout(width: layout.size.width)
transition.updateFrame(node: self.dividerNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - dividerSize.width) / 2.0), y: layout.size.height - insets.bottom - buttonSize.height - inset - dividerSize.height), size: dividerSize))
} else {
self.signInWithAppleButton?.isHidden = true
self.dividerNode.isHidden = true
switch codeType {
case .word, .phrase:
additionalBottomInset = 100.0
self.nextOptionButtonNode.isHidden = false
items.append(AuthorizationLayoutItem(node: self.nextOptionButtonNode, size: nextOptionSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 50.0, maxValue: 120.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
if layout.size.width > 320.0 {
self.proceedNode.isHidden = false
} else {
self.proceedNode.isHidden = true
}
let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - proceedSize.width) / 2.0), y: layout.size.height - insets.bottom - proceedSize.height - inset), size: proceedSize)
transition.updateFrame(node: self.proceedNode, frame: buttonFrame)
case .email:
self.nextOptionButtonNode.isHidden = true
self.proceedNode.isHidden = true
default:
self.nextOptionButtonNode.isHidden = false
self.proceedNode.isHidden = true
items.append(AuthorizationLayoutItem(node: self.nextOptionButtonNode, size: nextOptionSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 50.0, maxValue: 120.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
}
}
}
} else {
self.titleIconNode.isHidden = true
items.append(AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.currentOptionNode, size: currentOptionSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.codeInputView, size: codeFieldSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 40.0, maxValue: 100.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.nextOptionButtonNode, size: nextOptionSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 50.0, maxValue: 120.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
}
let _ = layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - additionalBottomInset)), items: items, transition: transition, failIfDoesNotFit: false)
if let codeType = self.codeType {
let yOffset: CGFloat = layout.size.width > 320.0 ? 18.0 : 5.0
if case .phrase = codeType {
self.hintButtonNode.alpha = 1.0
self.hintButtonNode.isUserInteractionEnabled = false
self.hintTextNode.attributedText = NSAttributedString(string: self.strings.Login_EnterPhraseHint, font: Font.regular(13.0), textColor: self.theme.list.itemSecondaryTextColor, paragraphAlignment: .center)
let hintTextSize = self.hintTextNode.updateLayout(CGSize(width: layout.size.width - 48.0, height: .greatestFiniteMagnitude))
transition.updateFrame(node: self.hintButtonNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - hintTextSize.width) / 2.0), y: self.textField.frame.maxY + yOffset), size: hintTextSize))
self.hintTextNode.frame = CGRect(origin: .zero, size: hintTextSize)
let pasteSize = self.pasteButton.measure(layout.size)
let pasteButtonSize = CGSize(width: pasteSize.width + 16.0, height: 24.0)
transition.updateFrame(node: self.pasteButton, frame: CGRect(origin: CGPoint(x: layout.size.width - 40.0 - pasteButtonSize.width, y: self.textField.frame.midY - pasteButtonSize.height / 2.0), size: pasteButtonSize))
self.hintArrowNode.isHidden = true
} else if case .word = codeType {
self.hintButtonNode.alpha = 0.0
self.hintButtonNode.isUserInteractionEnabled = false
self.hintArrowNode.isHidden = true
} else if self.hasPreviousCode {
self.hintButtonNode.alpha = 1.0
self.hintButtonNode.isUserInteractionEnabled = true
self.hintTextNode.attributedText = NSAttributedString(string: self.previousIsPhrase ? self.strings.Login_BackToPhrase : self.strings.Login_BackToWord, font: Font.regular(13.0), textColor: self.theme.list.itemAccentColor, paragraphAlignment: .center)
let hintTextSize = self.hintTextNode.updateLayout(CGSize(width: layout.size.width - 48.0, height: .greatestFiniteMagnitude))
transition.updateFrame(node: self.hintButtonNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - hintTextSize.width) / 2.0), y: self.codeInputView.frame.maxY + yOffset + 6.0), size: hintTextSize))
self.hintTextNode.frame = CGRect(origin: .zero, size: hintTextSize)
if let icon = self.hintArrowNode.image {
self.hintArrowNode.frame = CGRect(origin: CGPoint(x: self.hintTextNode.frame.minX - icon.size.width - 5.0, y: self.hintTextNode.frame.midY - icon.size.height / 2.0), size: icon.size)
}
self.hintArrowNode.isHidden = false
} else {
self.hintButtonNode.alpha = 0.0
self.hintButtonNode.isUserInteractionEnabled = false
self.hintArrowNode.isHidden = true
}
} else {
self.hintButtonNode.alpha = 0.0
self.hintButtonNode.isUserInteractionEnabled = false
self.hintArrowNode.isHidden = true
}
self.nextOptionTitleNode.frame = self.nextOptionButtonNode.bounds
if let icon = self.nextOptionArrowNode.image {
self.nextOptionArrowNode.frame = CGRect(origin: CGPoint(x: self.nextOptionTitleNode.frame.maxX + 7.0, y: self.nextOptionTitleNode.frame.midY - icon.size.height / 2.0), size: icon.size)
}
self.titleActivateAreaNode.frame = self.titleNode.frame
self.currentOptionActivateAreaNode.frame = self.currentOptionNode.frame
self.currentOptionInfoActivateAreaNode.frame = self.currentOptionInfoNode.frame
}
func activateInput() {
switch self.codeType {
case .word, .phrase:
self.textField.textField.becomeFirstResponder()
default:
let _ = self.codeInputView.becomeFirstResponder()
}
}
func animateError() {
switch self.codeType {
case .word, .phrase:
self.textField.layer.addShakeAnimation()
default:
self.codeInputView.layer.addShakeAnimation()
}
}
func animateError(text: String) {
let errorOriginY: CGFloat
let errorOriginOffset: CGFloat
switch self.codeType {
case .word, .phrase:
self.textField.layer.addShakeAnimation()
let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .easeInOut)
transition.updateBackgroundColor(node: self.textSeparatorNode, color: self.theme.list.itemDestructiveColor)
errorOriginY = self.textField.frame.maxY
errorOriginOffset = 5.0
default:
self.codeInputView.animateError()
self.codeInputView.layer.addShakeAnimation(amplitude: -30.0, duration: 0.5, count: 6, decay: true)
errorOriginY = self.codeInputView.frame.maxY
errorOriginOffset = 11.0
}
self.errorTextNode.attributedText = NSAttributedString(string: text, font: Font.regular(13.0), textColor: self.theme.list.itemDestructiveColor, paragraphAlignment: .center)
if let (layout, _) = self.layoutArguments {
let errorTextSize = self.errorTextNode.updateLayout(CGSize(width: layout.size.width - 48.0, height: .greatestFiniteMagnitude))
let yOffset: CGFloat = layout.size.width > 320.0 ? errorOriginOffset + 13.0 : errorOriginOffset
self.errorTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - errorTextSize.width) / 2.0), y: errorOriginY + yOffset), size: errorTextSize)
}
self.errorTextNode.alpha = 1.0
self.errorTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
let previousHintAlpha = self.hintButtonNode.alpha
self.hintButtonNode.alpha = 0.0
self.hintButtonNode.layer.animateAlpha(from: previousHintAlpha, to: 0.0, duration: 0.1)
Queue.mainQueue().after(1.6) {
self.errorTextNode.alpha = 0.0
self.errorTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15)
self.hintButtonNode.alpha = previousHintAlpha
self.hintButtonNode.layer.animateAlpha(from: 0.0, to: previousHintAlpha, duration: 0.1)
let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .easeInOut)
transition.updateBackgroundColor(node: self.textSeparatorNode, color: self.theme.list.itemPlainSeparatorColor)
}
}
func animateSuccess() {
self.codeInputView.animateSuccess()
let values: [NSNumber] = [1.0, 1.1, 1.0]
self.codeInputView.layer.animateKeyframes(values: values, duration: 0.4, keyPath: "transform.scale")
}
@objc private func textDidChange() {
let text = self.textField.textField.text ?? ""
self.proceedNode.isEnabled = !text.isEmpty
self.updateNextEnabled?(!text.isEmpty)
}
private func codeChanged(text: String) {
self.updateNextEnabled?(!text.isEmpty)
if let codeType = self.codeType {
var codeLength: Int32?
switch codeType {
case let .call(length):
codeLength = length
case let .otherSession(length):
codeLength = length
case let .missedCall(_, length):
codeLength = length
case let .sms(length):
codeLength = length
case let .email(_, length, _, _, _, _):
codeLength = length
case let .fragment(_, length):
codeLength = length
case let .firebase(_, length):
codeLength = length
default:
break
}
if let codeLength = codeLength, text.count == Int(codeLength) {
self.loginWithCode?(text)
}
}
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if self.inProgress {
return false
}
var updated = textField.text ?? ""
updated.replaceSubrange(updated.index(updated.startIndex, offsetBy: range.lowerBound) ..< updated.index(updated.startIndex, offsetBy: range.upperBound), with: string)
return checkValidity(text: updated)
}
func checkValidity(text: String) -> Bool {
if let codeType = self.codeType {
switch codeType {
case let .word(startsWith):
if let startsWith, startsWith.count == 1, !text.isEmpty && !text.hasPrefix(startsWith) {
if self.errorTextNode.alpha.isZero {
self.animateError(text: self.strings.Login_WrongPhraseError)
}
return false
}
case let .phrase(startsWith):
if let startsWith, !text.isEmpty {
let firstWord = text.components(separatedBy: " ").first ?? ""
if !firstWord.isEmpty && !startsWith.hasPrefix(firstWord) {
if self.errorTextNode.alpha.isZero {
self.animateError(text: self.strings.Login_WrongPhraseError)
}
return false
}
}
default:
break
}
}
return true
}
@objc func nextOptionNodePressed() {
self.requestAnotherOption?()
}
@objc func previousOptionNodePressed() {
self.requestPreviousOption?()
}
@objc func proceedPressed() {
switch self.codeType {
case let .fragment(url, _):
self.openFragment?(url)
case .word, .phrase:
if let text = self.textField.textField.text, !text.isEmpty {
self.loginWithCode?(text)
}
default:
break
}
}
@objc func signInWithApplePressed() {
self.signInWithApple?()
}
@objc func resetPressed() {
self.reset?()
}
}