Files
ghostgram/submodules/PasscodeUI/Sources/PasscodeSetupControllerNode.swift
T
ichmagmaus111andClaude Opus 5.5 0622057a4f Ghostgram: open-source release
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:57:09 +01:00

402 lines
20 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
import PasscodeInputFieldNode
import ComponentFlow
import WalletSendKeyboardComponent
enum PasscodeSetupInitialState {
case createPasscode
case changePassword(current: String, hasRecoveryEmail: Bool, hasSecureValues: Bool)
}
enum PasscodeSetupStateKind: Int32 {
case enterPasscode
case confirmPasscode
}
final class PasscodeSetupControllerNode: ASDisplayNode {
private var presentationData: PresentationData
private var mode: PasscodeSetupControllerMode
private let useCustomNumericKeyboard: Bool
private let keyboard = ComponentView<Empty>()
private var isCustomInputActive = true
private var previousDisplaysCustomKeyboard: Bool?
private var displaysCustomKeyboard: Bool {
guard self.useCustomNumericKeyboard else { return false }
switch self.mode {
case let .setup(_, type):
return type != .alphanumeric
case let .entry(challenge):
switch challenge.passcodeKind {
case .digits4, .digits6:
return true
default:
return false
}
}
}
private let wrapperNode: ASDisplayNode
private let titleNode: ASTextNode
private let subtitleNode: ASTextNode
private let inputFieldNode: PasscodeInputFieldNode
private let modeButtonNode: HighlightableButtonNode
var previousPasscode: String?
var currentPasscode: String {
return self.inputFieldNode.text
}
var selectPasscodeMode: ((HighlightableButtonNode) -> Void)?
var checkPasscode: ((String) -> Void)?
var complete: ((String, Bool) -> Void)?
var updateNextAction: ((Bool) -> Void)?
private let hapticFeedback = HapticFeedback()
private var authenticationInputEnabled = true
private var validLayout: (ContainerViewLayout, CGFloat)?
init(presentationData: PresentationData, mode: PasscodeSetupControllerMode, useCustomNumericKeyboard: Bool) {
self.presentationData = presentationData
self.mode = mode
self.useCustomNumericKeyboard = useCustomNumericKeyboard
self.wrapperNode = ASDisplayNode()
self.titleNode = ASTextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.subtitleNode = ASTextNode()
self.subtitleNode.isUserInteractionEnabled = false
self.subtitleNode.displaysAsynchronously = false
let passcodeType: PasscodeEntryFieldType
switch self.mode {
case let .entry(challenge):
switch challenge.passcodeKind {
case .digits4: passcodeType = .digits4
case .digits6: passcodeType = .digits6
default: passcodeType = .alphanumeric
}
case let .setup(_, type):
passcodeType = type
}
self.inputFieldNode = PasscodeInputFieldNode(color: self.presentationData.theme.list.itemPrimaryTextColor, accentColor: self.presentationData.theme.list.itemAccentColor, fieldType: passcodeType, keyboardAppearance: self.presentationData.theme.rootController.keyboardColor.keyboardAppearance, useCustomNumpad: self.useCustomNumericKeyboard && passcodeType != .alphanumeric, fieldBackgroundColor: self.presentationData.theme.list.itemBlocksBackgroundColor)
self.modeButtonNode = HighlightableButtonNode()
self.modeButtonNode.setTitle(self.presentationData.strings.PasscodeSettings_PasscodeOptions, with: Font.regular(17.0), with: self.presentationData.theme.list.itemAccentColor, for: .normal)
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
self.clipsToBounds = self.useCustomNumericKeyboard
self.addSubnode(self.wrapperNode)
self.wrapperNode.addSubnode(self.titleNode)
self.wrapperNode.addSubnode(self.subtitleNode)
self.wrapperNode.addSubnode(self.inputFieldNode)
self.wrapperNode.addSubnode(self.modeButtonNode)
let text: String
switch self.mode {
case .entry:
self.modeButtonNode.isHidden = true
self.modeButtonNode.isAccessibilityElement = false
text = self.presentationData.strings.EnterPasscode_EnterPasscode
case let .setup(change, _):
if change {
text = self.presentationData.strings.EnterPasscode_EnterNewPasscodeChange
} else {
text = self.presentationData.strings.EnterPasscode_EnterNewPasscodeNew
}
}
self.titleNode.attributedText = NSAttributedString(string: text, font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.inputFieldNode.complete = { [weak self] passcode in
guard let self, self.currentPasscode == passcode else { return }
self.activateNext()
}
self.modeButtonNode.addTarget(self, action: #selector(self.modePressed), forControlEvents: .touchUpInside)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
self.updateCustomKeyboard(layout: layout, transition: transition)
let standardInputHeight = layout.deviceMetrics.standardInputHeight(inLandscape: layout.orientation == .landscape)
var insets = layout.insets(options: [.statusBar])
if self.displaysCustomKeyboard {
insets.bottom = standardInputHeight + layout.additionalInsets.bottom
} else {
let keyboardHeight = max(layout.inputHeight ?? 0.0, standardInputHeight)
insets.bottom = max(insets.bottom, keyboardHeight)
}
self.wrapperNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
let inputFieldFrame = self.inputFieldNode.updateLayout(size: layout.size, topOffset: floor(insets.top + navigationBarHeight + (layout.size.height - navigationBarHeight - insets.top - insets.bottom - 24.0) / 2.0), transition: transition)
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(origin: CGPoint(), size: layout.size))
let titleSize = self.titleNode.measure(CGSize(width: layout.size.width - 28.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: inputFieldFrame.minY - titleSize.height - 20.0), size: titleSize))
let subtitleSize = self.subtitleNode.measure(CGSize(width: layout.size.width - 28.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - subtitleSize.width) / 2.0), y: inputFieldFrame.maxY + 20.0), size: subtitleSize))
transition.updateFrame(node: self.modeButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - 53.0), size: CGSize(width: layout.size.width, height: 44.0)))
}
override func didLoad() {
super.didLoad()
self.view.disablesInteractiveKeyboardGestureRecognizer = true
}
private func updateCustomKeyboard(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
let isVisible = self.displaysCustomKeyboard
let visibilityChanged = self.previousDisplaysCustomKeyboard.map { $0 != isVisible } ?? false
self.previousDisplaysCustomKeyboard = isVisible
guard isVisible || self.keyboard.view != nil else { return }
var minimumKeyboardHeight = layout.deviceMetrics.standardInputHeight(inLandscape: layout.orientation == .landscape) - 49.0
if UIDevice.current.userInterfaceIdiom == .pad {
minimumKeyboardHeight = min(minimumKeyboardHeight, 270.0)
}
let isEnabled = isVisible && self.authenticationInputEnabled && self.isCustomInputActive
let keyboardSize = self.keyboard.update(
transition: .immediate,
component: AnyComponent(WalletSendKeyboardComponent(
theme: self.presentationData.theme,
safeInsets: layout.safeInsets,
isLandscape: layout.size.width > layout.size.height && layout.metrics.widthClass == .compact,
minimumHeight: minimumKeyboardHeight,
mode: .numeric,
deleteTitle: self.presentationData.strings.Common_Delete,
isEnabled: isEnabled,
action: { [weak self] action in
guard let self, self.displaysCustomKeyboard, self.authenticationInputEnabled, self.isCustomInputActive, self.view.isUserInteractionEnabled else { return }
self.hapticFeedback.impact(.light)
switch action {
case let .insertText(text):
self.inputFieldNode.append(text)
case .deleteBackward:
let _ = self.inputFieldNode.delete()
}
}
)),
environment: {},
containerSize: layout.size
)
if let keyboardView = self.keyboard.view {
if keyboardView.superview == nil {
self.view.addSubview(keyboardView)
keyboardView.frame = CGRect(x: 0.0, y: layout.size.height, width: keyboardSize.width, height: keyboardSize.height)
}
if isVisible {
keyboardView.isHidden = false
}
keyboardView.isUserInteractionEnabled = isEnabled
keyboardView.accessibilityElementsHidden = !isEnabled
let keyboardFrame = CGRect(
x: 0.0,
y: isVisible ? layout.size.height - layout.additionalInsets.bottom - keyboardSize.height : layout.size.height,
width: keyboardSize.width,
height: keyboardSize.height
)
// Input activation and system keyboard updates can repeat the same target during the slide.
if keyboardView.frame != keyboardFrame {
var keyboardTransition = transition
if visibilityChanged || (!transition.isAnimated && keyboardView.layer.animation(forKey: "position") != nil) {
keyboardTransition = .animated(duration: 0.25, curve: .easeInOut)
}
keyboardTransition.updateFrame(view: keyboardView, frame: keyboardFrame, beginWithCurrentState: true, completion: { [weak self, weak keyboardView] completed in
guard completed, let self, let keyboardView,
!self.displaysCustomKeyboard, keyboardView.frame == keyboardFrame else { return }
keyboardView.isHidden = true
})
}
}
}
func deactivateCustomInput() {
guard self.useCustomNumericKeyboard else { return }
self.isCustomInputActive = false
self.inputFieldNode.isInputEnabled = false
self.inputFieldNode.cancelPendingCompletion()
if let validLayout = self.validLayout {
self.updateCustomKeyboard(layout: validLayout.0, transition: .immediate)
}
}
func updateInputEnabled(_ enabled: Bool) {
self.authenticationInputEnabled = enabled
self.inputFieldNode.isInputEnabled = enabled && (!self.useCustomNumericKeyboard || self.isCustomInputActive)
self.modeButtonNode.isUserInteractionEnabled = enabled
if let validLayout = self.validLayout {
self.updateCustomKeyboard(layout: validLayout.0, transition: .immediate)
}
}
func updateMode(_ mode: PasscodeSetupControllerMode) {
(self.keyboard.view as? WalletSendKeyboardComponent.View)?.cancelKeyPresses()
self.mode = mode
self.inputFieldNode.reset()
if case let .setup(_, type) = mode {
self.inputFieldNode.updateFieldType(type, animated: true, useCustomNumpad: self.displaysCustomKeyboard)
if case .alphanumeric = type {
self.updateNextAction?(true)
} else {
self.updateNextAction?(false)
}
self.subtitleNode.isHidden = true
}
if let validLayout = self.validLayout {
self.containerLayoutUpdated(validLayout.0, navigationBarHeight: validLayout.1, transition: .animated(duration: 0.25, curve: .easeInOut))
}
}
func activateNext() {
guard self.authenticationInputEnabled else { return }
if self.useCustomNumericKeyboard && !self.isCustomInputActive {
return
}
if case let .setup(_, type) = self.mode, let maxLength = type.maxLength, self.currentPasscode.count != maxLength {
return
}
(self.keyboard.view as? WalletSendKeyboardComponent.View)?.cancelKeyPresses()
guard !self.currentPasscode.isEmpty else {
self.animateError()
return
}
switch self.mode {
case .entry:
self.checkPasscode?(self.currentPasscode)
case .setup:
if let previousPasscode = self.previousPasscode {
if self.currentPasscode == previousPasscode {
var numerical = false
if case let .setup(_, type) = mode {
if case .alphanumeric = type {
} else {
numerical = true
}
}
self.complete?(self.currentPasscode, numerical)
} else {
self.previousPasscode = nil
if let snapshotView = self.wrapperNode.view.snapshotContentTree() {
snapshotView.frame = self.wrapperNode.frame
self.wrapperNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.wrapperNode.view)
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: self.wrapperNode.bounds.width, y: 0.0), duration: 0.25, removeOnCompletion: false, additive: true, completion : { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
self.wrapperNode.layer.animatePosition(from: CGPoint(x: -self.wrapperNode.bounds.width, y: 0.0), to: CGPoint(), duration: 0.25, additive: true)
self.inputFieldNode.reset(animated: false)
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.EnterPasscode_EnterNewPasscodeChange, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.subtitleNode.isHidden = false
self.subtitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.PasscodeSettings_DoNotMatch, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.modeButtonNode.isHidden = false
self.modeButtonNode.isAccessibilityElement = true
UIAccessibility.post(notification: UIAccessibility.Notification.announcement, argument: self.presentationData.strings.PasscodeSettings_DoNotMatch)
if let validLayout = self.validLayout {
self.containerLayoutUpdated(validLayout.0, navigationBarHeight: validLayout.1, transition: .immediate)
}
}
}
} else {
self.previousPasscode = self.currentPasscode
if let snapshotView = self.wrapperNode.view.snapshotContentTree() {
snapshotView.frame = self.wrapperNode.frame
self.wrapperNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.wrapperNode.view)
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -self.wrapperNode.bounds.width, y: 0.0), duration: 0.25, removeOnCompletion: false, additive: true, completion : { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
self.wrapperNode.layer.animatePosition(from: CGPoint(x: self.wrapperNode.bounds.width, y: 0.0), to: CGPoint(), duration: 0.25, additive: true)
self.inputFieldNode.reset(animated: false)
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.EnterPasscode_RepeatNewPasscode, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.subtitleNode.isHidden = true
self.modeButtonNode.isHidden = true
self.modeButtonNode.isAccessibilityElement = false
UIAccessibility.post(notification: UIAccessibility.Notification.announcement, argument: self.presentationData.strings.EnterPasscode_RepeatNewPasscode)
if let validLayout = self.validLayout {
self.containerLayoutUpdated(validLayout.0, navigationBarHeight: validLayout.1, transition: .immediate)
}
}
}
}
}
func activateInput() {
guard self.authenticationInputEnabled else { return }
self.isCustomInputActive = true
self.inputFieldNode.isInputEnabled = true
self.inputFieldNode.activateInput()
if let validLayout = self.validLayout {
self.updateCustomKeyboard(layout: validLayout.0, transition: .immediate)
}
UIAccessibility.post(notification: UIAccessibility.Notification.announcement, argument: self.titleNode.attributedText?.string)
}
func animateError() {
self.inputFieldNode.reset()
self.inputFieldNode.layer.addShakeAnimation(amplitude: -30.0, duration: 0.5, count: 6, decay: true)
self.hapticFeedback.error()
}
func updateAuthenticationState(_ state: SettingsPasscodeAuthentication.State) {
self.updateInputEnabled(state == .ready)
self.inputFieldNode.isUserInteractionEnabled = self.authenticationInputEnabled
let message: String?
switch state {
case .ready: message = ""
case .cooldown:
message = self.presentationData.strings.PasscodeSettings_TryAgainIn1Minute
self.inputFieldNode.reset(animated: false)
case .checking: message = nil
case .finished:
message = ""
self.inputFieldNode.reset(animated: false)
self.view.endEditing(true)
}
if let message {
self.subtitleNode.attributedText = NSAttributedString(string: message, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.subtitleNode.isHidden = message.isEmpty
if let validLayout = self.validLayout {
self.containerLayoutUpdated(validLayout.0, navigationBarHeight: validLayout.1, transition: .immediate)
}
}
}
@objc func modePressed() {
self.deactivateCustomInput()
self.selectPasscodeMode?(self.modeButtonNode)
}
}