Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,135 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ImageBlur
import FastBlur
import GradientBackground
protocol PasscodeBackground {
var size: CGSize { get }
var backgroundImage: UIImage? { get }
var foregroundImage: UIImage? { get }
func makeBackgroundNode() -> ASDisplayNode?
func makeForegroundNode(backgroundNode: ASDisplayNode?) -> ASDisplayNode?
}
final class CustomPasscodeBackground: PasscodeBackground {
private let colors: [UIColor]
private let backgroundNode: GradientBackgroundNode
let inverted: Bool
public private(set) var size: CGSize
public private(set) var backgroundImage: UIImage? = nil
public private(set) var foregroundImage: UIImage? = nil
init(size: CGSize, colors: [UIColor], inverted: Bool) {
self.size = size
self.colors = colors
self.inverted = inverted
self.backgroundNode = createGradientBackgroundNode(colors: self.colors)
}
func makeBackgroundNode() -> ASDisplayNode? {
return self.backgroundNode
}
func makeForegroundNode(backgroundNode: ASDisplayNode?) -> ASDisplayNode? {
if self.inverted, let backgroundNode = backgroundNode as? GradientBackgroundNode {
return GradientBackgroundNode.CloneNode(parentNode: backgroundNode, isDimmed: true)
} else {
return nil
}
}
}
final class GradientPasscodeBackground: PasscodeBackground {
public private(set) var size: CGSize
public private(set) var backgroundImage: UIImage?
public private(set) var foregroundImage: UIImage?
init(size: CGSize, backgroundColors: (UIColor, UIColor), buttonColor: UIColor) {
self.size = size
self.backgroundImage = generateImage(CGSize(width: 8.0, height: size.height), contextGenerator: { size, context in
let gradientColors = [backgroundColors.1.cgColor, backgroundColors.0.cgColor] as CFArray
var locations: [CGFloat] = [0.0, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
})!
self.foregroundImage = generateImage(CGSize(width: 1.0, height: 1.0), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
if buttonColor != UIColor.clear {
context.setFillColor(buttonColor.cgColor)
} else {
context.setFillColor(UIColor.white.withAlphaComponent(0.5).cgColor)
}
context.fill(bounds)
})!
}
func makeBackgroundNode() -> ASDisplayNode? {
return nil
}
func makeForegroundNode(backgroundNode: ASDisplayNode?) -> ASDisplayNode? {
return nil
}
}
final class ImageBasedPasscodeBackground: PasscodeBackground {
public private(set) var size: CGSize
public private(set) var backgroundImage: UIImage?
public private(set) var foregroundImage: UIImage?
init(image: UIImage, size: CGSize) {
self.size = size
let contextSize = size.aspectFilled(CGSize(width: 320.0, height: 320.0))
let foregroundContext = DrawingContext(size: contextSize, scale: 1.0)!
let bounds = CGRect(origin: CGPoint(), size: contextSize)
let filledImageSize = image.size.aspectFilled(contextSize)
let filledImageRect = CGRect(origin: CGPoint(x: (contextSize.width - filledImageSize.width) / 2.0, y: (contextSize.height - filledImageSize.height) / 2.0), size: filledImageSize)
foregroundContext.withFlippedContext { c in
c.interpolationQuality = .medium
c.draw(image.cgImage!, in: filledImageRect)
}
telegramFastBlurMore(Int32(contextSize.width), Int32(contextSize.height), Int32(foregroundContext.bytesPerRow), foregroundContext.bytes)
telegramFastBlurMore(Int32(contextSize.width), Int32(contextSize.height), Int32(foregroundContext.bytesPerRow), foregroundContext.bytes)
telegramFastBlurMore(Int32(contextSize.width), Int32(contextSize.height), Int32(foregroundContext.bytesPerRow), foregroundContext.bytes)
telegramBrightenImage(Int32(contextSize.width), Int32(contextSize.height), Int32(foregroundContext.bytesPerRow), foregroundContext.bytes)
foregroundContext.withFlippedContext { c in
c.setFillColor(UIColor(white: 1.0, alpha: 0.1).cgColor)
c.fill(bounds)
}
self.foregroundImage = foregroundContext.generateImage()!
let backgroundContext = DrawingContext(size: contextSize, scale: 1.0)!
backgroundContext.withFlippedContext { c in
c.interpolationQuality = .medium
c.draw(image.cgImage!, in: filledImageRect)
}
telegramFastBlurMore(Int32(contextSize.width), Int32(contextSize.height), Int32(backgroundContext.bytesPerRow), backgroundContext.bytes)
telegramFastBlurMore(Int32(contextSize.width), Int32(contextSize.height), Int32(backgroundContext.bytesPerRow), backgroundContext.bytes)
telegramFastBlurMore(Int32(contextSize.width), Int32(contextSize.height), Int32(foregroundContext.bytesPerRow), backgroundContext.bytes)
backgroundContext.withFlippedContext { context in
context.setFillColor(UIColor(white: 0.0, alpha: 0.35).cgColor)
context.fill(bounds)
}
self.backgroundImage = backgroundContext.generateImage()!
}
func makeBackgroundNode() -> ASDisplayNode? {
return nil
}
func makeForegroundNode(backgroundNode: ASDisplayNode?) -> ASDisplayNode? {
return nil
}
}
@@ -0,0 +1,284 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import LocalAuth
import TelegramStringFormatting
public final class PasscodeEntryControllerPresentationArguments {
let animated: Bool
let fadeIn: Bool
let lockIconInitialFrame: () -> CGRect
let cancel: (() -> Void)?
let modalPresentation: Bool
public init(animated: Bool = true, fadeIn: Bool = false, lockIconInitialFrame: @escaping () -> CGRect = { return CGRect() }, cancel: (() -> Void)? = nil, modalPresentation: Bool = false) {
self.animated = animated
self.fadeIn = fadeIn
self.lockIconInitialFrame = lockIconInitialFrame
self.cancel = cancel
self.modalPresentation = modalPresentation
}
}
public enum PasscodeEntryControllerBiometricsMode {
case none
case enabled(Data?)
}
public final class PasscodeEntryController: ViewController {
private var controllerNode: PasscodeEntryControllerNode {
return self.displayNode as! PasscodeEntryControllerNode
}
private let applicationBindings: TelegramApplicationBindings
private let accountManager: AccountManager<TelegramAccountManagerTypes>
private var energyUsageSettings: EnergyUsageSettings?
private let appLockContext: AppLockContext
private let presentationDataSignal: Signal<PresentationData, NoError>
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let challengeData: PostboxAccessChallengeData
private let biometrics: PasscodeEntryControllerBiometricsMode
private let arguments: PasscodeEntryControllerPresentationArguments
public var presentationCompleted: (() -> Void)?
public var completed: (() -> Void)?
private let biometricsDisposable = MetaDisposable()
private var hasOngoingBiometricsRequest = false
private var skipNextBiometricsRequest = false
private var inBackground: Bool = false
private var inBackgroundDisposable: Disposable?
public init(applicationBindings: TelegramApplicationBindings, accountManager: AccountManager<TelegramAccountManagerTypes>, appLockContext: AppLockContext, presentationData: PresentationData, presentationDataSignal: Signal<PresentationData, NoError>, statusBarHost: StatusBarHost?, challengeData: PostboxAccessChallengeData, biometrics: PasscodeEntryControllerBiometricsMode, arguments: PasscodeEntryControllerPresentationArguments) {
self.applicationBindings = applicationBindings
self.accountManager = accountManager
self.appLockContext = appLockContext
self.presentationData = presentationData
self.presentationDataSignal = presentationDataSignal
self.challengeData = challengeData
self.biometrics = biometrics
self.arguments = arguments
super.init(navigationBarPresentationData: nil)
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.statusBar.updateStatusBarStyle(.White, animated: false)
self.presentationDataDisposable = (presentationDataSignal
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self, strongSelf.isNodeLoaded {
strongSelf.controllerNode.updatePresentationData(presentationData)
}
})
self.inBackgroundDisposable = (applicationBindings.applicationInForeground
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.inBackground = !value
if !value {
strongSelf.skipNextBiometricsRequest = false
}
})
}
deinit {
self.presentationDataDisposable?.dispose()
self.biometricsDisposable.dispose()
self.inBackgroundDisposable?.dispose()
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
let passcodeType: PasscodeEntryFieldType
switch self.challengeData {
case let .numericalPassword(value):
passcodeType = value.count == 6 ? .digits6 : .digits4
default:
passcodeType = .alphanumeric
}
let biometricsType: LocalAuthBiometricAuthentication?
if case let .enabled(data) = self.biometrics {
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
#if targetEnvironment(simulator)
biometricsType = .touchId
#else
if data == LocalAuth.evaluatedPolicyDomainState || (data == nil && !self.applicationBindings.isMainApp) {
biometricsType = LocalAuth.biometricAuthentication
} else {
biometricsType = nil
}
#endif
} else {
biometricsType = LocalAuth.biometricAuthentication
}
} else {
biometricsType = nil
}
self.displayNode = PasscodeEntryControllerNode(accountManager: self.accountManager, presentationData: self.presentationData, theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, passcodeType: passcodeType, biometricsType: biometricsType, arguments: self.arguments, modalPresentation: self.arguments.modalPresentation)
self.displayNodeDidLoad()
let _ = (self.appLockContext.invalidAttempts
|> deliverOnMainQueue).start(next: { [weak self] attempts in
guard let strongSelf = self else {
return
}
strongSelf.controllerNode.updateInvalidAttempts(attempts)
})
self.controllerNode.checkPasscode = { [weak self] passcode in
guard let strongSelf = self else {
return
}
var succeed = false
switch strongSelf.challengeData {
case .none:
succeed = true
case let .numericalPassword(code):
succeed = passcode == normalizeArabicNumeralString(code, type: .western)
case let .plaintextPassword(code):
succeed = passcode == code
}
if succeed {
if let completed = strongSelf.completed {
completed()
} else {
strongSelf.appLockContext.unlock()
}
let isMainApp = strongSelf.applicationBindings.isMainApp
let _ = updatePresentationPasscodeSettingsInteractively(accountManager: strongSelf.accountManager, { settings in
if isMainApp {
return settings.withUpdatedBiometricsDomainState(LocalAuth.evaluatedPolicyDomainState)
} else {
return settings.withUpdatedShareBiometricsDomainState(LocalAuth.evaluatedPolicyDomainState)
}
}).start()
} else {
strongSelf.appLockContext.failedUnlockAttempt()
strongSelf.controllerNode.animateError()
}
}
self.controllerNode.requestBiometrics = { [weak self] in
if let strongSelf = self {
strongSelf.requestBiometrics(force: true)
}
}
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.view.disablesInteractiveTransitionGestureRecognizer = true
self.controllerNode.activateInput()
if self.arguments.animated {
self.controllerNode.animateIn(iconFrame: self.arguments.lockIconInitialFrame(), completion: { [weak self] in
self?.presentationCompleted?()
})
} else {
self.controllerNode.initialAppearance(fadeIn: self.arguments.fadeIn)
self.presentationCompleted?()
}
}
public func ensureInputFocused() {
self.controllerNode.activateInput()
}
public func requestBiometrics(force: Bool = false) {
guard case let .enabled(data) = self.biometrics, let _ = LocalAuth.biometricAuthentication else {
return
}
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
if data == nil && self.applicationBindings.isMainApp {
return
}
}
if self.skipNextBiometricsRequest {
self.skipNextBiometricsRequest = false
if !force {
return
}
}
if self.hasOngoingBiometricsRequest {
if !force {
return
}
}
self.hasOngoingBiometricsRequest = true
self.biometricsDisposable.set((LocalAuth.auth(reason: self.presentationData.strings.EnterPasscode_TouchId) |> deliverOnMainQueue).start(next: { [weak self] result, evaluatedPolicyDomainState in
guard let strongSelf = self else {
return
}
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
if case let .enabled(storedDomainState) = strongSelf.biometrics, evaluatedPolicyDomainState != nil {
if !strongSelf.applicationBindings.isMainApp && storedDomainState == nil {
let _ = updatePresentationPasscodeSettingsInteractively(accountManager: strongSelf.accountManager, { settings in
return settings.withUpdatedShareBiometricsDomainState(LocalAuth.evaluatedPolicyDomainState)
}).start()
} else if storedDomainState != evaluatedPolicyDomainState {
strongSelf.controllerNode.hideBiometrics()
return
}
}
}
if result {
strongSelf.controllerNode.animateSuccess()
if let completed = strongSelf.completed {
Queue.mainQueue().after(1.5) {
completed()
}
strongSelf.hasOngoingBiometricsRequest = false
} else {
strongSelf.appLockContext.unlock()
strongSelf.hasOngoingBiometricsRequest = false
}
} else {
strongSelf.hasOngoingBiometricsRequest = false
strongSelf.skipNextBiometricsRequest = true
}
}))
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
public override func dismiss(completion: (() -> Void)? = nil) {
self.view.endEditing(true)
self.controllerNode.animateOut { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.view.endEditing(true)
strongSelf.presentingViewController?.dismiss(animated: false, completion: completion)
}
}
}
@@ -0,0 +1,628 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import AccountContext
import LocalAuth
import AppBundle
import PasscodeInputFieldNode
import MonotonicTime
import GradientBackground
import TelegramUIPreferences
private extension CGRect {
var center: CGPoint {
return CGPoint(x: self.midX, y: self.midY)
}
}
private let titleFont = Font.regular(20.0)
private let subtitleFont = Font.regular(15.0)
private let buttonFont = Font.regular(17.0)
final class PasscodeEntryControllerNode: ASDisplayNode {
private let accountManager: AccountManager<TelegramAccountManagerTypes>
private var presentationData: PresentationData
private var theme: PresentationTheme
private var strings: PresentationStrings
private var wallpaper: TelegramWallpaper
private let passcodeType: PasscodeEntryFieldType
private let biometricsType: LocalAuthBiometricAuthentication?
private let arguments: PasscodeEntryControllerPresentationArguments
private var background: PasscodeBackground?
private let modalPresentation: Bool
private let coverNode: ASDisplayNode
private var backgroundCustomNode: ASDisplayNode?
private let backgroundDimNode: ASDisplayNode
private let backgroundImageNode: ASImageNode
private let iconNode: PasscodeLockIconNode
private let titleNode: PasscodeEntryLabelNode
private let inputFieldNode: PasscodeInputFieldNode
private let subtitleNode: PasscodeEntryLabelNode
private let keyboardNode: PasscodeEntryKeyboardNode
private let cancelButtonNode: HighlightableButtonNode
private let deleteButtonNode: HighlightableButtonNode
private let biometricButtonNode: HighlightableButtonNode
private let effectView: UIVisualEffectView
private var invalidAttempts: AccessChallengeAttempts?
private var timer: SwiftSignalKit.Timer?
private let hapticFeedback = HapticFeedback()
private var validLayout: ContainerViewLayout?
var checkPasscode: ((String) -> Void)?
var requestBiometrics: (() -> Void)?
var energyUsageSettings: EnergyUsageSettings = .default
var energyUsageSettingsDisposable: Disposable?
init(accountManager: AccountManager<TelegramAccountManagerTypes>, presentationData: PresentationData, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, passcodeType: PasscodeEntryFieldType, biometricsType: LocalAuthBiometricAuthentication?, arguments: PasscodeEntryControllerPresentationArguments, modalPresentation: Bool) {
self.accountManager = accountManager
self.presentationData = presentationData
self.theme = theme
self.strings = strings
self.wallpaper = wallpaper
self.passcodeType = passcodeType
self.biometricsType = biometricsType
self.arguments = arguments
self.modalPresentation = modalPresentation
self.coverNode = ASDisplayNode()
self.coverNode.backgroundColor = .black
self.backgroundImageNode = ASImageNode()
self.backgroundImageNode.contentMode = .scaleToFill
self.backgroundDimNode = ASDisplayNode()
self.backgroundDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.15)
self.backgroundDimNode.isHidden = true
self.iconNode = PasscodeLockIconNode()
self.titleNode = PasscodeEntryLabelNode()
self.inputFieldNode = PasscodeInputFieldNode(color: .white, accentColor: .white, fieldType: passcodeType, keyboardAppearance: .dark, useCustomNumpad: true)
self.subtitleNode = PasscodeEntryLabelNode()
self.keyboardNode = PasscodeEntryKeyboardNode()
self.cancelButtonNode = HighlightableButtonNode()
self.deleteButtonNode = HighlightableButtonNode()
self.deleteButtonNode.hitTestSlop = UIEdgeInsets(top: -10.0, left: -16.0, bottom: -10.0, right: -16.0)
self.biometricButtonNode = HighlightableButtonNode()
self.effectView = UIVisualEffectView(effect: nil)
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.backgroundColor = .clear
self.iconNode.unlockedColor = theme.rootController.navigationBar.primaryTextColor
self.keyboardNode.charactedEntered = { [weak self] character in
if let strongSelf = self {
strongSelf.inputFieldNode.append(character)
if let gradientNode = strongSelf.backgroundCustomNode as? GradientBackgroundNode {
if strongSelf.energyUsageSettings.fullTranslucency {
gradientNode.animateEvent(transition: .animated(duration: 0.55, curve: .spring), extendAnimation: false, backwards: false, completion: {})
}
}
}
}
self.keyboardNode.backspace = { [weak self] in
if let strongSelf = self {
let _ = strongSelf.inputFieldNode.delete()
if let gradientNode = strongSelf.backgroundCustomNode as? GradientBackgroundNode {
if strongSelf.energyUsageSettings.fullTranslucency {
gradientNode.animateEvent(transition: .animated(duration: 0.55, curve: .spring), extendAnimation: false, backwards: true, completion: {})
}
}
}
}
self.inputFieldNode.complete = { [weak self] passcode in
guard let strongSelf = self else {
return
}
if strongSelf.shouldWaitBeforeNextAttempt() {
strongSelf.animateError()
} else {
strongSelf.checkPasscode?(passcode)
}
}
self.cancelButtonNode.setTitle(strings.Common_Cancel, with: buttonFont, with: .white, for: .normal)
self.cancelButtonNode.accessibilityLabel = strings.Common_Cancel
self.cancelButtonNode.accessibilityTraits = [.button]
self.deleteButtonNode.setTitle(strings.Common_Delete, with: buttonFont, with: .white, for: .normal)
self.deleteButtonNode.accessibilityLabel = strings.Common_Delete
self.deleteButtonNode.accessibilityTraits = [.button]
if let biometricsType = self.biometricsType {
switch biometricsType {
case .touchId:
self.biometricButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/PasscodeTouchId"), color: .white), for: .normal)
case .faceId:
self.biometricButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/PasscodeFaceId"), color: .white), for: .normal)
}
}
self.addSubnode(self.coverNode)
self.addSubnode(self.backgroundImageNode)
self.addSubnode(self.backgroundDimNode)
self.addSubnode(self.iconNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.inputFieldNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.keyboardNode)
self.addSubnode(self.deleteButtonNode)
self.addSubnode(self.biometricButtonNode)
if self.arguments.cancel != nil {
self.addSubnode(self.cancelButtonNode)
}
self.energyUsageSettingsDisposable = (accountManager.sharedData(keys: Set([ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]))
|> deliverOnMainQueue).start(next: { [weak self] sharedData in
guard let self else {
return
}
if let mediaAutoDownloadSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]?.get(MediaAutoDownloadSettings.self) {
if automaticEnergyUsageShouldBeOnNow(settings: mediaAutoDownloadSettings) {
self.energyUsageSettings = EnergyUsageSettings.powerSavingDefault
} else {
self.energyUsageSettings = mediaAutoDownloadSettings.energyUsageSettings
}
}
})
}
deinit {
self.energyUsageSettingsDisposable?.dispose()
}
override func didLoad() {
super.didLoad()
self.view.disablesInteractiveKeyboardGestureRecognizer = true
self.view.insertSubview(self.effectView, at: 0)
if self.arguments.cancel != nil {
self.cancelButtonNode.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
}
self.deleteButtonNode.addTarget(self, action: #selector(self.deletePressed), forControlEvents: .touchUpInside)
self.biometricButtonNode.addTarget(self, action: #selector(self.biometricsPressed), forControlEvents: .touchUpInside)
}
@objc private func cancelPressed() {
self.animateOut(down: true)
self.arguments.cancel?()
}
@objc private func deletePressed() {
self.hapticFeedback.tap()
let result = self.inputFieldNode.delete()
if result, let gradientNode = self.backgroundCustomNode as? GradientBackgroundNode {
if self.energyUsageSettings.fullTranslucency {
gradientNode.animateEvent(transition: .animated(duration: 0.55, curve: .spring), extendAnimation: false, backwards: true, completion: {})
}
}
}
@objc private func biometricsPressed() {
self.requestBiometrics?()
}
func activateInput() {
self.inputFieldNode.activateInput()
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.theme = presentationData.theme
self.strings = presentationData.strings
self.wallpaper = presentationData.chatWallpaper
self.deleteButtonNode.setTitle(self.strings.Common_Delete, with: buttonFont, with: .white, for: .normal)
if let validLayout = self.validLayout {
self.containerLayoutUpdated(validLayout, navigationBarHeight: 0.0, transition: .immediate)
}
}
func updateBackground() {
guard let validLayout = self.validLayout else {
return
}
let size = validLayout.size
if let background = self.background, background.size == size {
return
}
switch self.wallpaper {
case let .color(colorValue):
let color = UIColor(argb: colorValue)
let baseColor: UIColor
let lightness = color.lightness
if lightness < 0.1 || lightness > 0.9 {
baseColor = self.theme.chat.message.outgoing.bubble.withoutWallpaper.fill[0]
} else{
baseColor = color
}
let color1: UIColor
let color2: UIColor
let color3: UIColor
let color4: UIColor
if self.theme.overallDarkAppearance {
color1 = baseColor.withMultiplied(hue: 1.034, saturation: 0.819, brightness: 0.214)
color2 = baseColor.withMultiplied(hue: 1.029, saturation: 0.77, brightness: 0.132)
color3 = color1
color4 = color2
} else {
color1 = baseColor.withMultiplied(hue: 1.029, saturation: 0.312, brightness: 1.26)
color2 = baseColor.withMultiplied(hue: 1.034, saturation: 0.729, brightness: 0.942)
color3 = baseColor.withMultiplied(hue: 1.029, saturation: 0.729, brightness: 1.231)
color4 = baseColor.withMultiplied(hue: 1.034, saturation: 0.583, brightness: 1.043)
}
self.coverNode.backgroundColor = color3
self.background = CustomPasscodeBackground(size: size, colors: [color1, color2, color3, color4], inverted: false)
case let .gradient(gradient):
self.coverNode.backgroundColor = gradient.colors.first.flatMap { UIColor(rgb: $0) }
self.background = CustomPasscodeBackground(size: size, colors: gradient.colors.compactMap { UIColor(rgb: $0) }, inverted: (gradient.settings.intensity ?? 0) < 0)
case .image, .file:
if let image = chatControllerBackgroundImage(theme: self.theme, wallpaper: self.wallpaper, mediaBox: self.accountManager.mediaBox, composed: false, knockoutMode: false) {
self.coverNode.backgroundColor = .black
self.background = ImageBasedPasscodeBackground(image: image, size: size)
} else {
if case let .file(file) = self.wallpaper, !file.settings.colors.isEmpty {
self.coverNode.backgroundColor = file.settings.colors.last.flatMap { UIColor(rgb: $0) }
self.background = CustomPasscodeBackground(size: size, colors: file.settings.colors.compactMap { UIColor(rgb: $0) }, inverted: (file.settings.intensity ?? 0) < 0)
} else {
self.coverNode.backgroundColor = self.theme.passcode.backgroundColors.bottomColor
self.background = GradientPasscodeBackground(size: size, backgroundColors: self.theme.passcode.backgroundColors.colors, buttonColor: self.theme.passcode.buttonColor)
}
}
default:
self.coverNode.backgroundColor = self.theme.passcode.backgroundColors.bottomColor
self.background = GradientPasscodeBackground(size: size, backgroundColors: self.theme.passcode.backgroundColors.colors, buttonColor: self.theme.passcode.buttonColor)
}
if let background = self.background {
self.backgroundCustomNode?.removeFromSupernode()
self.backgroundCustomNode = nil
if let backgroundImage = background.backgroundImage {
self.backgroundImageNode.image = backgroundImage
self.backgroundDimNode.isHidden = true
} else if let customBackgroundNode = background.makeBackgroundNode() {
self.backgroundCustomNode = customBackgroundNode
self.insertSubnode(customBackgroundNode, aboveSubnode: self.backgroundImageNode)
if let background = background as? CustomPasscodeBackground, background.inverted {
self.backgroundDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.75)
} else {
self.backgroundDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.15)
}
self.backgroundDimNode.isHidden = false
}
self.keyboardNode.updateBackground(self.presentationData, background)
self.inputFieldNode.updateBackground(background)
}
}
private let waitInterval: Int32 = 60
private func shouldWaitBeforeNextAttempt() -> Bool {
if let attempts = self.invalidAttempts {
if attempts.count >= 6 {
var bootTimestamp: Int32 = 0
let uptime = getDeviceUptimeSeconds(&bootTimestamp)
if attempts.bootTimestamp != bootTimestamp {
return true
}
if uptime - attempts.uptime < waitInterval {
return true
} else {
return false
}
} else {
return false
}
} else {
return false
}
}
func updateInvalidAttempts(_ attempts: AccessChallengeAttempts?, animated: Bool = false) {
self.invalidAttempts = attempts
if let attempts = attempts {
var text = NSAttributedString(string: "")
if attempts.count >= 6 && self.shouldWaitBeforeNextAttempt() {
text = NSAttributedString(string: self.strings.PasscodeSettings_TryAgainIn1Minute, font: subtitleFont, textColor: .white)
self.timer?.invalidate()
let timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
if let strongSelf = self {
if !strongSelf.shouldWaitBeforeNextAttempt() {
strongSelf.updateInvalidAttempts(strongSelf.invalidAttempts, animated: true)
strongSelf.timer?.invalidate()
strongSelf.timer = nil
}
}
}, queue: Queue.mainQueue())
self.timer = timer
timer.start()
}
self.subtitleNode.setAttributedText(text, animation: animated ? .crossFade : .none, completion: {})
} else {
self.subtitleNode.setAttributedText(NSAttributedString(string: ""), animation: animated ? .crossFade : .none, completion: {})
}
}
func hideBiometrics() {
self.biometricButtonNode.layer.animateScale(from: 1.0, to: 0.00001, duration: 0.25, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, completion: { [weak self] _ in
self?.biometricButtonNode.isHidden = true
})
self.animateError()
}
func initialAppearance(fadeIn: Bool = false) {
if fadeIn {
let effect = self.theme.overallDarkAppearance ? UIBlurEffect(style: .dark) : UIBlurEffect(style: .light)
UIView.animate(withDuration: 0.3, animations: {
if #available(iOS 9.0, *) {
self.effectView.effect = effect
} else {
self.effectView.alpha = 1.0
}
})
self.backgroundImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
if let gradientNode = self.backgroundCustomNode as? GradientBackgroundNode {
gradientNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.backgroundDimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
if self.energyUsageSettings.fullTranslucency {
gradientNode.animateEvent(transition: .animated(duration: 1.0, curve: .spring), extendAnimation: true, backwards: false, completion: {})
}
}
}
self.titleNode.setAttributedText(NSAttributedString(string: self.strings.EnterPasscode_EnterPasscode, font: titleFont, textColor: .white), animation: .none)
}
func animateIn(iconFrame: CGRect, completion: @escaping () -> Void = {}) {
self.coverNode.isHidden = true
let effect = self.theme.overallDarkAppearance ? UIBlurEffect(style: .dark) : UIBlurEffect(style: .light)
UIView.animate(withDuration: 0.3, animations: {
if #available(iOS 9.0, *) {
self.effectView.effect = effect
} else {
self.effectView.alpha = 1.0
}
})
self.backgroundImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
if let gradientNode = self.backgroundCustomNode as? GradientBackgroundNode, self.energyUsageSettings.fullTranslucency {
gradientNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
if self.energyUsageSettings.fullTranslucency {
gradientNode.animateEvent(transition: .animated(duration: 0.35, curve: .spring), extendAnimation: false, backwards: false, completion: {})
}
self.backgroundDimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
if !iconFrame.isEmpty {
self.iconNode.animateIn(fromScale: 0.416)
self.iconNode.layer.animatePosition(from: iconFrame.center.offsetBy(dx: 6.0, dy: 6.0), to: self.iconNode.layer.position, duration: 0.45)
Queue.mainQueue().after(0.45) {
self.hapticFeedback.impact(.medium)
}
}
self.subtitleNode.isHidden = true
self.inputFieldNode.isHidden = true
self.keyboardNode.isHidden = true
self.cancelButtonNode.isHidden = true
self.deleteButtonNode.isHidden = true
self.biometricButtonNode.isHidden = true
self.titleNode.setAttributedText(NSAttributedString(string: self.strings.Passcode_AppLockedAlert.replacingOccurrences(of: "\n", with: " "), font: titleFont, textColor: .white), animation: .slideIn, completion: {
self.coverNode.isHidden = false
self.subtitleNode.isHidden = false
self.inputFieldNode.isHidden = false
self.keyboardNode.isHidden = false
self.cancelButtonNode.isHidden = false
self.deleteButtonNode.isHidden = false
self.biometricButtonNode.isHidden = false
self.subtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
if let gradientNode = self.backgroundCustomNode as? GradientBackgroundNode {
if self.energyUsageSettings.fullTranslucency {
gradientNode.animateEvent(transition: .animated(duration: 1.0, curve: .spring), extendAnimation: false, backwards: false, completion: {})
}
}
self.inputFieldNode.animateIn()
self.keyboardNode.animateIn()
var biometricDelay = 0.3
if case .alphanumeric = self.passcodeType {
biometricDelay = 0.0
} else {
self.cancelButtonNode.layer.animateScale(from: 0.0001, to: 1.0, duration: 0.25, delay: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
self.deleteButtonNode.layer.animateScale(from: 0.0001, to: 1.0, duration: 0.25, delay: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
}
self.biometricButtonNode.layer.animateScale(from: 0.0001, to: 1.0, duration: 0.25, delay: biometricDelay, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
Queue.mainQueue().after(1.5, {
self.titleNode.setAttributedText(NSAttributedString(string: self.strings.EnterPasscode_EnterPasscode, font: titleFont, textColor: .white), animation: .crossFade)
if let validLayout = self.validLayout {
self.containerLayoutUpdated(validLayout, navigationBarHeight: 0.0, transition: .animated(duration: 0.5, curve: .easeInOut))
}
})
completion()
})
}
func animateOut(down: Bool = false, completion: @escaping () -> Void = {}) {
self.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: down ? self.bounds.size.height : -self.bounds.size.height), duration: 0.2, removeOnCompletion: false, additive: true, completion: { _ in
completion()
})
}
func animateSuccess() {
self.iconNode.animateUnlock()
self.inputFieldNode.animateSuccess()
}
func animateError() {
self.inputFieldNode.reset()
self.inputFieldNode.layer.addShakeAnimation(amplitude: -30.0, duration: 0.5, count: 6, decay: true)
self.iconNode.layer.addShakeAnimation(amplitude: -8.0, duration: 0.5, count: 6, decay: true)
self.hapticFeedback.error()
if let gradientNode = self.backgroundCustomNode as? GradientBackgroundNode {
if self.energyUsageSettings.fullTranslucency {
gradientNode.animateEvent(transition: .animated(duration: 1.5, curve: .spring), extendAnimation: true, backwards: true, completion: {})
}
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
self.updateBackground()
let maxSide = max(layout.size.width, layout.size.height)
let coverSize = CGSize(width: maxSide, height: maxSide)
transition.updateFrame(node: self.coverNode, frame: CGRect(origin: CGPoint(x: round((layout.size.width - coverSize.width) / 2.0), y: round((layout.size.height - coverSize.height) / 2.0)), size: coverSize))
let bounds = CGRect(origin: CGPoint(), size: layout.size)
transition.updateFrame(node: self.backgroundImageNode, frame: bounds)
transition.updateFrame(node: self.backgroundDimNode, frame: bounds)
if let backgroundCustomNode = self.backgroundCustomNode {
transition.updateFrame(node: backgroundCustomNode, frame: bounds)
if let gradientBackgroundNode = backgroundCustomNode as? GradientBackgroundNode {
gradientBackgroundNode.updateLayout(size: bounds.size, transition: transition, extendAnimation: false, backwards: false, completion: {})
}
}
transition.updateFrame(view: self.effectView, frame: bounds)
switch self.passcodeType {
case .digits6, .digits4:
self.keyboardNode.alpha = 1.0
self.deleteButtonNode.alpha = 1.0
case .alphanumeric:
self.keyboardNode.alpha = 0.0
self.deleteButtonNode.alpha = 0.0
}
let isLandscape = layout.orientation == .landscape && layout.deviceMetrics.type != .tablet
let keyboardHidden = self.keyboardNode.alpha == 0.0
let layoutSize: CGSize
if isLandscape {
if keyboardHidden {
layoutSize = CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: layout.size.height)
} else {
layoutSize = CGSize(width: layout.size.width / 2.0, height: layout.size.height)
}
} else {
layoutSize = layout.size
}
if layout.size.width == 320.0 || (isLandscape && keyboardHidden) {
self.iconNode.alpha = 0.0
} else {
self.iconNode.alpha = 1.0
}
let passcodeLayout = PasscodeLayout(layout: layout, modalPresentation: self.modalPresentation)
let inputFieldOffset: CGFloat
if isLandscape {
let bottomInset = layout.inputHeight ?? 0.0
if !keyboardHidden || bottomInset == 0.0 {
inputFieldOffset = floor(layoutSize.height / 2.0 + 12.0)
} else {
inputFieldOffset = floor(layoutSize.height - bottomInset) / 2.0 - 40.0
}
} else {
inputFieldOffset = passcodeLayout.inputFieldOffset
}
let inputFieldFrame = self.inputFieldNode.updateLayout(size: layoutSize, topOffset: inputFieldOffset, transition: transition)
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left, y: 0.0), size: layoutSize))
let titleFrame: CGRect
if isLandscape {
let titleSize = self.titleNode.updateLayout(size: CGSize(width: layoutSize.width, height: layout.size.height), transition: transition)
titleFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: inputFieldFrame.minY - titleSize.height - 16.0), size: titleSize)
} else {
let titleSize = self.titleNode.updateLayout(size: layout.size, transition: transition)
titleFrame = CGRect(origin: CGPoint(x: 0.0, y: passcodeLayout.titleOffset), size: titleSize)
}
transition.updateFrame(node: self.titleNode, frame: titleFrame)
let iconSize = CGSize(width: 35.0, height: 37.0)
let iconFrame: CGRect
if isLandscape {
iconFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layoutSize.width - iconSize.width) / 2.0) + 6.0, y: titleFrame.minY - iconSize.height - 14.0), size: iconSize)
} else {
iconFrame = CGRect(origin: CGPoint(x: floor((layoutSize.width - iconSize.width) / 2.0) + 6.0, y: layout.insets(options: .statusBar).top + 15.0), size: iconSize)
}
transition.updateFrame(node: self.iconNode, frame: iconFrame)
var subtitleOffset = passcodeLayout.subtitleOffset
if case .alphanumeric = self.passcodeType {
subtitleOffset = 16.0
}
let subtitleSize = self.subtitleNode.updateLayout(size: layoutSize, transition: transition)
transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left, y: inputFieldFrame.maxY + subtitleOffset), size: subtitleSize))
let (keyboardFrame, keyboardButtonSize) = self.keyboardNode.updateLayout(layout: passcodeLayout, transition: transition)
transition.updateFrame(node: self.keyboardNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: layout.size))
let bottomInset = layout.inputHeight ?? 0.0
let cancelSize = self.cancelButtonNode.measure(layout.size)
var bottomButtonY = layout.size.height - layout.intrinsicInsets.bottom - cancelSize.height - passcodeLayout.keyboard.deleteOffset
var cancelX = floor(keyboardFrame.minX + keyboardButtonSize.width / 2.0 - cancelSize.width / 2.0)
var cancelY = bottomButtonY
if bottomInset > 0 && keyboardHidden {
cancelX = floor((layout.size.width - cancelSize.width) / 2.0)
cancelY = layout.size.height - bottomInset - cancelSize.height - 15.0 - layout.intrinsicInsets.bottom
} else if isLandscape {
bottomButtonY = keyboardFrame.maxY - keyboardButtonSize.height + floor((keyboardButtonSize.height - cancelSize.height) / 2.0)
cancelY = bottomButtonY
}
transition.updateFrame(node: self.cancelButtonNode, frame: CGRect(origin: CGPoint(x: cancelX, y: cancelY), size: cancelSize))
let deleteSize = self.deleteButtonNode.measure(layout.size)
transition.updateFrame(node: self.deleteButtonNode, frame: CGRect(origin: CGPoint(x: floor(keyboardFrame.maxX - keyboardButtonSize.width / 2.0 - deleteSize.width / 2.0), y: bottomButtonY), size: deleteSize))
if let biometricIcon = self.biometricButtonNode.image(for: .normal) {
var biometricX = layout.safeInsets.left + floor((layoutSize.width - biometricIcon.size.width) / 2.0)
var biometricY: CGFloat = 0.0
if isLandscape {
if bottomInset > 0 && keyboardHidden {
biometricX = cancelX + cancelSize.width + 64.0
}
biometricY = cancelY + floor((cancelSize.height - biometricIcon.size.height) / 2.0)
} else {
if bottomInset > 0 && keyboardHidden {
biometricY = inputFieldFrame.maxY + floor((layout.size.height - bottomInset - inputFieldFrame.maxY - biometricIcon.size.height) / 2.0)
} else {
biometricY = keyboardFrame.maxY + passcodeLayout.keyboard.biometricsOffset
}
}
transition.updateFrame(node: self.biometricButtonNode, frame: CGRect(origin: CGPoint(x: biometricX, y: biometricY), size: biometricIcon.size))
}
}
}
@@ -0,0 +1,373 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import GradientBackground
private let regularTitleFont = Font.regular(36.0)
private let regularSubtitleFont: UIFont = {
return UIFont.systemFont(ofSize: 10.0, weight: UIFont.Weight.bold)
}()
private let largeTitleFont = Font.regular(40.0)
private let largeSubtitleFont: UIFont = {
return UIFont.systemFont(ofSize: 12.0, weight: UIFont.Weight.bold)
}()
private func generateButtonImage(background: PasscodeBackground, frame: CGRect, title: String, subtitle: String, highlighted: Bool) -> UIImage? {
return generateImage(frame.size, contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let relativeFrame = CGRect(x: -frame.minX, y: frame.minY - background.size.height + frame.size.height
, width: background.size.width, height: background.size.height)
context.beginPath()
context.addEllipse(in: bounds)
context.clip()
context.setAlpha(0.8)
if let foregroundImage = background.foregroundImage {
context.draw(foregroundImage.cgImage!, in: relativeFrame)
}
if highlighted {
context.setFillColor(UIColor(white: 1.0, alpha: 0.65).cgColor)
context.fillEllipse(in: bounds)
}
context.setAlpha(1.0)
context.textMatrix = .identity
let titleFont: UIFont
let subtitleFont: UIFont
let titleOffset: CGFloat
let subtitleOffset: CGFloat
if size.width > 80.0 {
titleFont = largeTitleFont
subtitleFont = largeSubtitleFont
if subtitle.isEmpty {
titleOffset = -18.0
} else {
titleOffset = -11.0
}
subtitleOffset = -54.0
} else if size.width > 70.0 {
titleFont = regularTitleFont
subtitleFont = regularSubtitleFont
if subtitle.isEmpty {
titleOffset = -17.0
} else {
titleOffset = -10.0
}
subtitleOffset = -48.0
}
else {
titleFont = regularTitleFont
subtitleFont = regularSubtitleFont
if subtitle.isEmpty {
titleOffset = -11.0
} else {
titleOffset = -4.0
}
subtitleOffset = -41.0
}
let titlePath = CGMutablePath()
titlePath.addRect(bounds.offsetBy(dx: 0.0, dy: titleOffset))
let titleString = NSAttributedString(string: title, font: titleFont, textColor: .white, paragraphAlignment: .center)
let titleFramesetter = CTFramesetterCreateWithAttributedString(titleString as CFAttributedString)
let titleFrame = CTFramesetterCreateFrame(titleFramesetter, CFRangeMake(0, titleString.length), titlePath, nil)
CTFrameDraw(titleFrame, context)
if !subtitle.isEmpty {
let subtitlePath = CGMutablePath()
subtitlePath.addRect(bounds.offsetBy(dx: 0.0, dy: subtitleOffset))
let subtitleString = NSAttributedString(string: subtitle, font: subtitleFont, textColor: .white, paragraphAlignment: .center)
let subtitleFramesetter = CTFramesetterCreateWithAttributedString(subtitleString as CFAttributedString)
let subtitleFrame = CTFramesetterCreateFrame(subtitleFramesetter, CFRangeMake(0, subtitleString.length), subtitlePath, nil)
CTFrameDraw(subtitleFrame, context)
}
})
}
final class PasscodeEntryButtonNode: HighlightTrackingButtonNode {
private var presentationData: PresentationData
private var background: PasscodeBackground
let title: String
private let subtitle: String
private var currentImage: UIImage?
private var regularImage: UIImage?
private var highlightedImage: UIImage?
private var blurredBackgroundNode: NavigationBackgroundNode?
private var gradientBackgroundNode: GradientBackgroundNode.CloneNode?
private let backgroundNode: ASImageNode
var action: (() -> Void)?
var cancelAction: (() -> Void)?
init(presentationData: PresentationData, background: PasscodeBackground, title: String, subtitle: String) {
self.presentationData = presentationData
self.background = background
self.title = title
self.subtitle = subtitle
if let background = background as? CustomPasscodeBackground {
let blurredBackgroundColor = (background.inverted ? UIColor(rgb: 0xffffff, alpha: 0.1) : UIColor(rgb: 0x000000, alpha: 0.2), dateFillNeedsBlur(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper))
let blurredBackgroundNode = NavigationBackgroundNode(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1)
self.blurredBackgroundNode = blurredBackgroundNode
if isReduceTransparencyEnabled() {
blurredBackgroundNode.alpha = 0.1
}
}
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.isUserInteractionEnabled = false
super.init()
self.accessibilityLabel = title
self.accessibilityTraits = .keyboardKey
if let gradientBackgroundNode = self.gradientBackgroundNode {
self.addSubnode(gradientBackgroundNode)
}
if let blurredBackgroundNode = self.blurredBackgroundNode {
self.addSubnode(blurredBackgroundNode)
}
self.addSubnode(self.backgroundNode)
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
strongSelf.updateState(highlighted: highlighted)
}
}
self.addTarget(self, action: #selector(self.nop), forControlEvents: .touchUpInside)
}
@objc private func nop() {
}
override var frame: CGRect {
get {
return super.frame
}
set {
super.frame = newValue
self.updateGraphics()
}
}
func updateBackground(_ presentationData: PresentationData, _ background: PasscodeBackground) {
self.presentationData = presentationData
self.background = background
self.updateGraphics()
}
private func updateGraphics() {
self.regularImage = generateButtonImage(background: self.background, frame: self.frame, title: self.title, subtitle: self.subtitle, highlighted: false)
self.highlightedImage = generateButtonImage(background: self.background, frame: self.frame, title: self.title, subtitle: self.subtitle, highlighted: true)
self.updateState(highlighted: self.isHighlighted)
if let gradientBackgroundNode = self.gradientBackgroundNode {
let containerSize = self.background.size
let shiftedContentsRect = CGRect(origin: CGPoint(x: self.frame.minX / containerSize.width, y: self.frame.minY / containerSize.height), size: CGSize(width: self.frame.width / containerSize.width, height: self.frame.height / containerSize.height))
gradientBackgroundNode.layer.contentsRect = shiftedContentsRect
}
}
private func updateState(highlighted: Bool) {
let image = highlighted ? self.highlightedImage : self.regularImage
if self.currentImage !== image {
let currentContents = self.backgroundNode.layer.contents
self.backgroundNode.layer.removeAnimation(forKey: "contents")
if let currentContents = currentContents, let image = image {
self.backgroundNode.image = image
self.backgroundNode.layer.animate(from: currentContents as AnyObject, to: image.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: image === self.regularImage ? 0.45 : 0.05)
} else {
self.backgroundNode.image = image
}
self.currentImage = image
}
}
override func layout() {
super.layout()
if let gradientBackgroundNode = self.gradientBackgroundNode {
gradientBackgroundNode.frame = self.bounds
}
if let blurredBackgroundNode = self.blurredBackgroundNode {
blurredBackgroundNode.frame = self.bounds
blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, transition: .immediate)
}
self.backgroundNode.frame = self.bounds
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
self.action?()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
if let touchPosition = touches.first?.location(in: self.view), !self.view.bounds.contains(touchPosition) {
self.cancelAction?()
}
}
override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
self.cancelAction?()
}
}
private let buttonsData = [
("1", " "),
("2", "A B C"),
("3", "D E F"),
("4", "G H I"),
("5", "J K L"),
("6", "M N O"),
("7", "P Q R S"),
("8", "T U V"),
("9", "W X Y Z"),
("0", "")
]
final class PasscodeEntryKeyboardNode: ASDisplayNode {
private var presentationData: PresentationData?
private var background: PasscodeBackground?
var charactedEntered: ((String) -> Void)?
var backspace: (() -> Void)?
private func updateButtons() {
guard let presentationData = self.presentationData, let background = self.background else {
return
}
if let subnodes = self.subnodes, !subnodes.isEmpty {
for case let button as PasscodeEntryButtonNode in subnodes {
button.updateBackground(presentationData, background)
}
} else {
for (title, subtitle) in buttonsData {
let buttonNode = PasscodeEntryButtonNode(presentationData: presentationData, background: background, title: title, subtitle: subtitle)
buttonNode.action = { [weak self] in
self?.charactedEntered?(title)
}
buttonNode.cancelAction = { [weak self] in
self?.backspace?()
}
self.addSubnode(buttonNode)
}
}
}
func updateBackground(_ presentationData: PresentationData, _ background: PasscodeBackground) {
self.presentationData = presentationData
self.background = background
self.updateButtons()
}
func animateIn() {
if let subnodes = self.subnodes {
for i in 0 ..< subnodes.count {
let subnode = subnodes[i]
var delay: Double = 0.001
if i / 3 == 1 {
delay = 0.05
}
else if i / 3 == 2 {
delay = 0.1
}
else if i / 3 == 3 {
delay = 0.15
}
subnode.layer.animateScale(from: 0.0001, to: 1.0, duration: 0.25, delay: delay, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
}
}
}
func updateLayout(layout: PasscodeLayout, transition: ContainedViewLayoutTransition) -> (CGRect, CGSize) {
let origin: CGPoint
let buttonSize: CGFloat
let horizontalSecond: CGFloat
let horizontalThird: CGFloat
let verticalSecond: CGFloat
let verticalThird: CGFloat
let verticalFourth: CGFloat
let keyboardSize: CGSize
if layout.layout.orientation == .landscape && layout.layout.deviceMetrics.type != .tablet {
let horizontalSpacing: CGFloat = 20.0
let verticalSpacing: CGFloat = 12.0
buttonSize = 65.0
keyboardSize = CGSize(width: buttonSize * 3.0 + horizontalSpacing * 2.0, height: buttonSize * 4.0 + verticalSpacing * 3.0)
horizontalSecond = buttonSize + horizontalSpacing
horizontalThird = buttonSize * 2.0 + horizontalSpacing * 2.0
verticalSecond = buttonSize + verticalSpacing
verticalThird = buttonSize * 2.0 + verticalSpacing * 2.0
verticalFourth = buttonSize * 3.0 + verticalSpacing * 3.0
origin = CGPoint(x: floor(layout.layout.size.width / 2.0 + (layout.layout.size.width / 2.0 - keyboardSize.width) / 2.0) - layout.layout.safeInsets.right, y: floor((layout.layout.size.height - keyboardSize.height) / 2.0))
} else {
origin = CGPoint(x: floor((layout.layout.size.width - layout.keyboard.size.width) / 2.0), y: layout.keyboard.topOffset)
buttonSize = layout.keyboard.buttonSize
horizontalSecond = layout.keyboard.horizontalSecond
horizontalThird = layout.keyboard.horizontalThird
verticalSecond = layout.keyboard.verticalSecond
verticalThird = layout.keyboard.verticalThird
verticalFourth = layout.keyboard.verticalFourth
keyboardSize = layout.keyboard.size
}
if let subnodes = self.subnodes {
for i in 0 ..< subnodes.count {
var origin = origin
if i % 3 == 0 {
origin.x += 0.0
} else if (i % 3 == 1) {
origin.x += horizontalSecond
}
else {
origin.x += horizontalThird
}
if i / 3 == 0 {
origin.y += 0.0
}
else if i / 3 == 1 {
origin.y += verticalSecond
}
else if i / 3 == 2 {
origin.y += verticalThird
}
else if i / 3 == 3 {
origin.x += horizontalSecond
origin.y += verticalFourth
}
transition.updateFrame(node: subnodes[i], frame: CGRect(origin: origin, size: CGSize(width: buttonSize, height: buttonSize)))
}
}
return (CGRect(origin: origin, size: keyboardSize), CGSize(width: buttonSize, height: buttonSize))
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if let result = result, result.isDescendant(of: self.view) {
return result
}
return nil
}
}
@@ -0,0 +1,92 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
enum PasscodeEntryTitleAnimation {
case none
case slideIn
case crossFade
}
final class PasscodeEntryLabelNode: ASDisplayNode {
private let wrapperNode: ASDisplayNode
private let textNode: ImmediateTextNode
private var validLayout: CGSize?
override init() {
self.wrapperNode = ASDisplayNode()
self.wrapperNode.clipsToBounds = true
self.textNode = ImmediateTextNode()
self.textNode.isLayerBacked = false
self.textNode.textAlignment = .center
self.textNode.displaysAsynchronously = false
self.textNode.maximumNumberOfLines = 2
super.init()
self.addSubnode(self.wrapperNode)
self.wrapperNode.addSubnode(self.textNode)
}
func setAttributedText(_ text: NSAttributedString, animation: PasscodeEntryTitleAnimation = .none, completion: @escaping () -> Void = {}) {
switch animation {
case .none:
self.textNode.attributedText = text
completion()
if let size = self.validLayout {
let _ = self.updateLayout(size: size, transition: .immediate)
}
case .slideIn:
self.textNode.attributedText = text
if let size = self.validLayout {
let _ = self.updateLayout(size: size, transition: .immediate)
}
let offset = self.wrapperNode.bounds.width / 2.0
self.wrapperNode.layer.animatePosition(from: CGPoint(x: -offset, y: 0.0), to: CGPoint(), duration: 0.45, additive: true)
self.textNode.layer.animatePosition(from: CGPoint(x: offset * 2.0, y: 0.0), to: CGPoint(), duration: 0.45, additive: true, completion: { _ in
completion()
})
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
case .crossFade:
if let snapshotView = self.textNode.view.snapshotContentTree() {
snapshotView.frame = self.textNode.frame
self.textNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.textNode.view)
self.textNode.alpha = 0.0
self.textNode.attributedText = text
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
self.textNode.alpha = 1.0
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, completion: { _ in
completion()
})
if let size = self.validLayout {
let _ = self.updateLayout(size: size, transition: .immediate)
}
})
} else {
self.textNode.attributedText = text
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
completion()
if let size = self.validLayout {
let _ = self.updateLayout(size: size, transition: .immediate)
}
}
}
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
self.validLayout = size
let textSize = self.textNode.updateLayout(size)
let textFrame = CGRect(x: floor((size.width - textSize.width) / 2.0), y: 0.0, width: textSize.width, height: textSize.height)
transition.updateFrame(node: self.wrapperNode, frame: textFrame)
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(), size: textSize))
return CGSize(width: size.width, height: max(25.0, textSize.height))
}
}
@@ -0,0 +1,385 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
private let dotDiameter: CGFloat = 13.0
private let dotSpacing: CGFloat = 24.0
private let fieldHeight: CGFloat = 38.0
private func generateDotImage(color: UIColor, filled: Bool) -> UIImage? {
return generateImage(CGSize(width: dotDiameter, height: dotDiameter), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
if filled {
context.setFillColor(color.cgColor)
context.fillEllipse(in: bounds)
} else {
context.setStrokeColor(color.cgColor)
context.setLineWidth(1.0)
context.strokeEllipse(in: bounds.insetBy(dx: 0.5, dy: 0.5))
}
})
}
private func generateFieldBackgroundImage(backgroundImage: UIImage?, backgroundSize: CGSize?, frame: CGRect) -> UIImage? {
return generateImage(frame.size, contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let path = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height), cornerRadius: 6.0)
context.addPath(path.cgPath)
context.clip()
if let backgroundImage = backgroundImage, let backgroundSize = backgroundSize {
let relativeFrame = CGRect(x: -frame.minX, y: frame.minY - backgroundSize.height + frame.size.height
, width: backgroundSize.width, height: backgroundSize.height)
context.draw(backgroundImage.cgImage!, in: relativeFrame)
} else {
context.setFillColor(UIColor(rgb: 0xffffff, alpha: 1.0).cgColor)
context.fill(bounds)
}
context.setBlendMode(.clear)
context.setFillColor(UIColor.clear.cgColor)
let innerPath = UIBezierPath(roundedRect: CGRect(x: 1.0, y: 1.0, width: size.width - 2.0, height: size.height - 2.0), cornerRadius: 6.0)
context.addPath(innerPath.cgPath)
context.fillPath()
})
}
private let validDigitsSet: CharacterSet = {
return CharacterSet(charactersIn: "0".unicodeScalars.first! ... "9".unicodeScalars.first!)
}()
public enum PasscodeEntryFieldType {
case digits6
case digits4
case alphanumeric
public var maxLength: Int? {
switch self {
case .digits6:
return 6
case .digits4:
return 4
case .alphanumeric:
return nil
}
}
public var allowedCharacters: CharacterSet? {
switch self {
case .digits6, .digits4:
return validDigitsSet
case .alphanumeric:
return nil
}
}
public var keyboardType: UIKeyboardType {
switch self {
case .digits6, .digits4:
if #available(iOS 10.0, *) {
return .asciiCapableNumberPad
} else {
return .numberPad
}
case .alphanumeric:
return .default
}
}
}
private class PasscodeEntryInputView: UIView {
}
private class PasscodeEntryDotNode: ASImageNode {
private let regularImage: UIImage
private let filledImage: UIImage
private var currentImage: UIImage
init(color: UIColor) {
self.regularImage = generateDotImage(color: color, filled: false)!
self.filledImage = generateDotImage(color: color, filled: true)!
self.currentImage = self.regularImage
super.init()
self.image = self.currentImage
}
func updateState(filled: Bool, animated: Bool = false, delay: Double = 0.0) {
let image = filled ? self.filledImage : self.regularImage
if self.currentImage !== image {
let currentContents = self.layer.contents
self.layer.removeAnimation(forKey: "contents")
if let currentContents = currentContents, animated {
self.layer.animate(from: currentContents as AnyObject, to: image.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: image === self.regularImage ? 0.25 : 0.05, delay: delay, removeOnCompletion: false, completion: { finished in
if finished {
self.image = image
}
})
} else {
self.image = image
}
self.currentImage = image
}
}
}
public final class PasscodeInputFieldNode: ASDisplayNode, UITextFieldDelegate {
private var background: PasscodeBackground?
private var color: UIColor
private var accentColor: UIColor
private var fieldType: PasscodeEntryFieldType
private let useCustomNumpad: Bool
private let textFieldNode: TextFieldNode
private let borderNode: ASImageNode
private let dotNodes: [PasscodeEntryDotNode]
private var validLayout: (CGSize, CGFloat)?
public var complete: ((String) -> Void)?
public var text: String {
return self.textFieldNode.textField.text ?? ""
}
public var keyboardAppearance: UIKeyboardAppearance {
didSet {
self.textFieldNode.textField.keyboardAppearance = self.keyboardAppearance
}
}
public init(color: UIColor, accentColor: UIColor, fieldType: PasscodeEntryFieldType, keyboardAppearance: UIKeyboardAppearance, useCustomNumpad: Bool = false) {
self.color = color
self.accentColor = accentColor
self.fieldType = fieldType
self.keyboardAppearance = keyboardAppearance
self.useCustomNumpad = useCustomNumpad
self.textFieldNode = TextFieldNode()
self.borderNode = ASImageNode()
self.dotNodes = (0 ..< 6).map { _ in PasscodeEntryDotNode(color: color) }
super.init()
self.isUserInteractionEnabled = false
for node in self.dotNodes {
self.addSubnode(node)
}
self.addSubnode(self.textFieldNode)
self.addSubnode(self.borderNode)
}
override public func didLoad() {
super.didLoad()
self.textFieldNode.textField.isSecureTextEntry = true
self.textFieldNode.textField.textColor = self.color
self.textFieldNode.textField.delegate = self
self.textFieldNode.textField.returnKeyType = .done
self.textFieldNode.textField.tintColor = self.accentColor
self.textFieldNode.textField.keyboardAppearance = self.keyboardAppearance
self.textFieldNode.textField.keyboardType = self.fieldType.keyboardType
self.textFieldNode.textField.tintColor = self.accentColor
if self.useCustomNumpad {
switch self.fieldType {
case .digits6, .digits4:
self.textFieldNode.textField.inputView = PasscodeEntryInputView()
case .alphanumeric:
break
}
}
}
func updateFieldType(_ fieldType: PasscodeEntryFieldType, animated: Bool) {
self.fieldType = fieldType
self.textFieldNode.textField.keyboardType = self.fieldType.keyboardType
if let (size, topOffset) = self.validLayout {
let _ = self.updateLayout(size: size, topOffset: topOffset, transition: animated ? .animated(duration: 0.25, curve: .easeInOut) : .immediate)
}
}
func updateBackground(_ background: PasscodeBackground) {
self.background = background
if let (size, topOffset) = self.validLayout {
let _ = self.updateLayout(size: size, topOffset: topOffset, transition: .immediate)
}
}
public func activateInput() {
self.textFieldNode.textField.becomeFirstResponder()
}
func animateIn() {
switch self.fieldType {
case .digits6, .digits4:
for node in self.dotNodes {
node.layer.animateScale(from: 0.0001, to: 1.0, duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
}
case .alphanumeric:
self.textFieldNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
self.borderNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
}
}
func animateSuccess() {
switch self.fieldType {
case .digits6, .digits4:
var delay: Double = 0.0
for node in self.dotNodes {
node.updateState(filled: true, animated: true, delay: delay)
delay += 0.01
}
case .alphanumeric:
if (self.textFieldNode.textField.text ?? "").isEmpty {
self.textFieldNode.textField.text = "passwordpassword"
}
}
}
public func reset(animated: Bool = true) {
var delay: Double = 0.0
for node in self.dotNodes.reversed() {
if node.alpha < 1.0 {
continue
}
node.updateState(filled: false, animated: animated, delay: delay)
delay += 0.05
}
self.textFieldNode.textField.text = ""
}
func append(_ string: String) {
var text = (self.textFieldNode.textField.text ?? "") + string
let maxLength = self.fieldType.maxLength
if let maxLength = maxLength, text.count > maxLength {
return
}
self.textFieldNode.textField.text = text
text = self.textFieldNode.textField.text ?? "" + string
self.updateDots(count: text.count, animated: false)
if let maxLength = maxLength, text.count == maxLength {
Queue.mainQueue().after(0.2) {
self.complete?(text)
}
}
}
func delete() -> Bool {
var text = self.textFieldNode.textField.text ?? ""
guard !text.isEmpty else {
return false
}
text = String(text[text.startIndex ..< text.index(text.endIndex, offsetBy: -1)])
self.textFieldNode.textField.text = text
self.updateDots(count: text.count, animated: true)
return true
}
func updateDots(count: Int, animated: Bool) {
var i = -1
for node in self.dotNodes {
if node.alpha < 1.0 {
continue
}
i += 1
node.updateState(filled: i < count, animated: animated)
}
}
public func update(fieldType: PasscodeEntryFieldType) {
if fieldType != self.fieldType {
self.textFieldNode.textField.text = ""
}
self.fieldType = fieldType
if let (size, topOffset) = self.validLayout {
let _ = self.updateLayout(size: size, topOffset: topOffset, transition: .immediate)
}
}
public func updateLayout(size: CGSize, topOffset: CGFloat, transition: ContainedViewLayoutTransition) -> CGRect {
self.validLayout = (size, topOffset)
let fieldAlpha: CGFloat
switch self.fieldType {
case .digits6, .digits4:
fieldAlpha = 0.0
case .alphanumeric:
fieldAlpha = 1.0
}
transition.updateAlpha(node: self.textFieldNode, alpha: fieldAlpha)
transition.updateAlpha(node: self.borderNode, alpha: fieldAlpha)
let origin = CGPoint(x: floor((size.width - dotDiameter * 6 - dotSpacing * 5) / 2.0), y: topOffset)
for i in 0 ..< self.dotNodes.count {
let node = self.dotNodes[i]
let dotAlpha: CGFloat
switch self.fieldType {
case .digits6:
dotAlpha = 1.0
case .digits4:
dotAlpha = (i > 0 && i < self.dotNodes.count - 1) ? 1.0 : 0.0
case .alphanumeric:
dotAlpha = 0.0
}
transition.updateAlpha(node: node, alpha: dotAlpha)
let dotFrame = CGRect(x: origin.x + CGFloat(i) * (dotDiameter + dotSpacing), y: origin.y, width: dotDiameter, height: dotDiameter)
transition.updateFrame(node: node, frame: dotFrame)
}
var inset: CGFloat = 50.0
if !self.useCustomNumpad {
inset = 16.0
}
let fieldFrame = CGRect(x: inset, y: origin.y, width: size.width - inset * 2.0, height: fieldHeight)
transition.updateFrame(node: self.borderNode, frame: fieldFrame)
transition.updateFrame(node: self.textFieldNode, frame: fieldFrame.insetBy(dx: 13.0, dy: 0.0))
self.borderNode.image = generateFieldBackgroundImage(backgroundImage: self.background?.foregroundImage, backgroundSize: self.background?.size, frame: fieldFrame)
return fieldFrame
}
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let currentText = textField.text ?? ""
let text = (currentText as NSString).replacingCharacters(in: range, with: string)
if let maxLength = self.fieldType.maxLength, text.count > maxLength {
return false
}
if let allowedCharacters = self.fieldType.allowedCharacters, let _ = text.rangeOfCharacter(from: allowedCharacters.inverted) {
return false
}
self.updateDots(count: text.count, animated: text.count < currentText.count)
if string == "\n" {
Queue.mainQueue().after(0.2) {
self.complete?(currentText)
}
return false
}
if let maxLength = self.fieldType.maxLength, text.count == maxLength {
Queue.mainQueue().after(0.2) {
self.complete?(text)
}
}
return true
}
}
@@ -0,0 +1,184 @@
import Foundation
import UIKit
import Display
struct PasscodeKeyboardLayout {
var buttonSize: CGFloat
var horizontalSecond: CGFloat
var horizontalThird: CGFloat
var verticalSecond: CGFloat
var verticalThird: CGFloat
var verticalFourth: CGFloat
var size: CGSize
var topOffset: CGFloat
var biometricsOffset: CGFloat
var deleteOffset: CGFloat
fileprivate init(layout: ContainerViewLayout, modalPresentation: Bool) {
var modalOffset: CGFloat = 0.0
var modalBiometricsOffset: CGFloat = 0.0
if modalPresentation {
modalOffset -= 20.0
modalBiometricsOffset -= 8.0
}
switch layout.deviceMetrics {
case .iPhone4:
self.buttonSize = 75.0
self.horizontalSecond = 95.0
self.horizontalThird = 190.0
self.verticalSecond = 88.0
self.verticalThird = 176.0
self.verticalFourth = 264.0
self.size = CGSize(width: 265.0, height: 339.0)
self.topOffset = 122.0
self.biometricsOffset = 0.0
self.deleteOffset = 45.0
case .iPhone5:
self.buttonSize = 75.0
self.horizontalSecond = 95.0
self.horizontalThird = 190.0
self.verticalSecond = 88.0
self.verticalThird = 176.0
self.verticalFourth = 264.0
self.size = CGSize(width: 265.0, height: 339.0)
self.topOffset = 155.0
self.biometricsOffset = 23.0
self.deleteOffset = 20.0
case .iPhone6:
self.buttonSize = 75.0
self.horizontalSecond = 103.0
self.horizontalThird = 206.0
self.verticalSecond = 90.0
self.verticalThird = 180.0
self.verticalFourth = 270.0
self.size = CGSize(width: 281.0, height: 348.0)
self.topOffset = 221.0
self.biometricsOffset = 30.0
self.deleteOffset = 20.0
case .iPhone6Plus:
self.buttonSize = 85.0
self.horizontalSecond = 115.0
self.horizontalThird = 230.0
self.verticalSecond = 100.0
self.verticalThird = 200.0
self.verticalFourth = 300.0
self.size = CGSize(width: 315.0, height: 385.0)
self.topOffset = 226.0
self.biometricsOffset = 30.0
self.deleteOffset = 20.0
case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhoneAir:
self.buttonSize = 75.0
self.horizontalSecond = 103.0
self.horizontalThird = 206.0
self.verticalSecond = 91.0
self.verticalThird = 182.0
self.verticalFourth = 273.0
self.size = CGSize(width: 281.0, height: 348.0)
self.topOffset = 294.0
self.biometricsOffset = 30.0
self.deleteOffset = 20.0
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax:
self.buttonSize = 85.0
self.horizontalSecond = 115.0
self.horizontalThird = 230.0
self.verticalSecond = 100.0
self.verticalThird = 200.0
self.verticalFourth = 300.0
self.size = CGSize(width: 315.0, height: 385.0)
self.topOffset = 329.0
self.biometricsOffset = 30.0
self.deleteOffset = 20.0
case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen:
self.buttonSize = 81.0
self.horizontalSecond = 106.0
self.horizontalThird = 212.0
self.verticalSecond = 101.0
self.verticalThird = 202.0
self.verticalFourth = 303.0
self.size = CGSize(width: 293.0, height: 384.0)
self.topOffset = 120.0 + (layout.size.height - self.size.height - 120.0) / 2.0
self.biometricsOffset = 30.0
self.deleteOffset = 80.0
case .unknown:
self.buttonSize = 75.0
self.horizontalSecond = 95.0
self.horizontalThird = 190.0
self.verticalSecond = 88.0
self.verticalThird = 176.0
self.verticalFourth = 264.0
self.size = CGSize(width: 265.0, height: 339.0)
self.topOffset = 120.0 + (layout.size.height - self.size.height - 120.0) / 2.0
self.biometricsOffset = 30.0
self.deleteOffset = 20.0
}
self.topOffset += modalOffset * 2.0
self.biometricsOffset += modalBiometricsOffset
}
}
public struct PasscodeLayout {
var layout: ContainerViewLayout
var keyboard: PasscodeKeyboardLayout
var titleOffset: CGFloat
var subtitleOffset: CGFloat
var inputFieldOffset: CGFloat
init(layout: ContainerViewLayout, modalPresentation: Bool) {
self.layout = layout
var modalOffset: CGFloat = 0.0
if modalPresentation {
modalOffset -= 20.0
}
self.keyboard = PasscodeKeyboardLayout(layout: layout, modalPresentation: modalPresentation)
switch layout.deviceMetrics {
case .iPhone4:
self.titleOffset = 30.0
self.subtitleOffset = -13.0
self.inputFieldOffset = 70.0
case .iPhone5:
self.titleOffset = 50.0
self.subtitleOffset = -7.0
self.inputFieldOffset = 90.0
case .iPhone6:
self.titleOffset = 100.0
self.subtitleOffset = -3.0
self.inputFieldOffset = 144.0
case .iPhone6Plus:
self.titleOffset = 112.0
self.subtitleOffset = -6.0
self.inputFieldOffset = 156.0
case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhoneAir:
self.titleOffset = 162.0
self.subtitleOffset = 0.0
self.inputFieldOffset = 206.0
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax:
self.titleOffset = 180.0
self.subtitleOffset = 0.0
self.inputFieldOffset = 226.0
case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen:
self.titleOffset = self.keyboard.topOffset - 120.0
self.subtitleOffset = -2.0
self.inputFieldOffset = self.keyboard.topOffset - 76.0
case .unknown:
self.titleOffset = 100.0
self.subtitleOffset = 0.0
self.inputFieldOffset = 140.0
}
self.titleOffset += modalOffset
self.subtitleOffset += modalOffset
self.inputFieldOffset += modalOffset
}
public init(layout: ContainerViewLayout, titleOffset: CGFloat, subtitleOffset: CGFloat, inputFieldOffset: CGFloat, modalPresentation: Bool) {
self.layout = layout
self.keyboard = PasscodeKeyboardLayout(layout: layout, modalPresentation: modalPresentation)
self.titleOffset = titleOffset
self.subtitleOffset = subtitleOffset
self.inputFieldOffset = inputFieldOffset
}
}
@@ -0,0 +1,169 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import LegacyComponents
private final class PasscodeLockIconNodeParameters: NSObject {
let unlockedColor: UIColor
let lockedColor: UIColor
let progress: CGFloat
let fromScale: CGFloat
let keepLockedColor: Bool
init(unlockedColor: UIColor, lockedColor: UIColor, progress: CGFloat, fromScale: CGFloat, keepLockedColor: Bool) {
self.unlockedColor = unlockedColor
self.lockedColor = lockedColor
self.progress = progress
self.fromScale = fromScale
self.keepLockedColor = keepLockedColor
super.init()
}
}
final class PasscodeLockIconNode: ASDisplayNode {
var unlockedColor: UIColor = .black {
didSet {
self.setNeedsDisplay()
}
}
private var effectiveProgress: CGFloat = 1.0 {
didSet {
self.setNeedsDisplay()
}
}
private var fromScale: CGFloat = 1.0
private var keepLockedColor = false
override init() {
super.init()
self.isOpaque = false
self.backgroundColor = .clear
}
func animateIn(fromScale: CGFloat = 1.0) {
self.fromScale = fromScale
self.pop_removeAllAnimations()
let animation = POPBasicAnimation()
animation.property = (POPAnimatableProperty.property(withName: "progress", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! PasscodeLockIconNode).effectiveProgress
}
property?.writeBlock = { node, values in
(node as! PasscodeLockIconNode).effectiveProgress = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty)
animation.fromValue = 0.0 as NSNumber
animation.toValue = 1.0 as NSNumber
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.duration = 0.55
self.pop_add(animation, forKey: "progress")
}
func animateUnlock() {
self.fromScale = 1.0
self.keepLockedColor = true
self.pop_removeAllAnimations()
let animation = POPBasicAnimation()
animation.property = (POPAnimatableProperty.property(withName: "progress", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! PasscodeLockIconNode).effectiveProgress
}
property?.writeBlock = { node, values in
(node as! PasscodeLockIconNode).effectiveProgress = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty)
animation.fromValue = 1.0 as NSNumber
animation.toValue = 0.0 as NSNumber
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.duration = 0.75
self.pop_add(animation, forKey: "progress")
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return PasscodeLockIconNodeParameters(unlockedColor: self.unlockedColor, lockedColor: .white, progress: self.effectiveProgress, fromScale: self.fromScale, keepLockedColor: self.keepLockedColor)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
guard let parameters = parameters as? PasscodeLockIconNodeParameters else {
return
}
let progress = parameters.progress
let fromScale = parameters.fromScale
let lockSpan: CGFloat = parameters.keepLockedColor ? 0.5 : 0.85
let lockProgress = min(1.0, progress / lockSpan)
context.translateBy(x: bounds.width / 2.0, y: bounds.height / 2.0)
context.scaleBy(x: fromScale + (1.0 - fromScale) * lockProgress, y: fromScale + (1.0 - fromScale) * lockProgress)
context.translateBy(x: -bounds.width / 2.0, y: -bounds.height / 2.0)
let color = parameters.keepLockedColor ? parameters.lockedColor : parameters.unlockedColor.mixedWith(parameters.lockedColor, alpha: progress)
context.setStrokeColor(color.cgColor)
let lineWidth: CGFloat = 3.0
context.setLineWidth(lineWidth)
var topRect: CGRect
var topRadius: CGFloat
var offset: CGFloat = 0.0
if lockProgress < 0.5 {
topRect = CGRect(x: 19.0, y: lineWidth / 2.0 + 1.0, width: 14.0 * (0.5 - lockProgress) / 0.5, height: 22.0)
topRadius = 6.0 * (0.5 - lockProgress) * 2.0
} else {
let width = 14.0 * (lockProgress - 0.5) * 2.0
topRect = CGRect(x: 19.0 - width, y: lineWidth / 2.0 + 1.0, width: width, height: 22.0)
topRadius = 6.0 * (lockProgress - 0.5) * 2.0
}
if progress > lockSpan {
let innerProgress = (progress - lockSpan) / (1.0 - lockSpan)
if !parameters.keepLockedColor {
if innerProgress < 0.6 {
offset = 2.0 * min(1.0, innerProgress / 0.6)
} else {
offset = 2.0 * min(1.0, max(0.0, (1.0 - innerProgress) / 0.4))
}
}
topRect.origin.y += 4.0 * min(1.0, max(0.0, innerProgress / 0.6)) + offset
}
let topPath = UIBezierPath(roundedRect: topRect, cornerRadius: topRadius)
context.addPath(topPath.cgPath)
context.strokePath()
var clearRect: CGRect
if lockProgress < 0.5 {
clearRect = CGRect(x: topRect.minX + lineWidth, y: topRect.minY + 11.0, width: 14.0, height: 22.0)
} else {
clearRect = CGRect(x: topRect.maxX - 14.0 - lineWidth, y: topRect.minY + 11.0, width: 14.0, height: 22.0)
}
context.setBlendMode(.clear)
context.clear(clearRect)
context.setBlendMode(.normal)
context.setFillColor(color.cgColor)
let basePath = UIBezierPath(roundedRect: CGRect(x: 0.0, y: bounds.height - 21.0 + offset, width: 24.0, height: 19.0), cornerRadius: 3.5)
context.addPath(basePath.cgPath)
context.fillPath()
}
}
@@ -0,0 +1,152 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
public enum PasscodeSetupControllerMode {
case setup(change: Bool, PasscodeEntryFieldType)
case entry(PostboxAccessChallengeData)
}
public final class PasscodeSetupController: ViewController {
private var controllerNode: PasscodeSetupControllerNode {
return self.displayNode as! PasscodeSetupControllerNode
}
private let context: AccountContext
private var mode: PasscodeSetupControllerMode
public var complete: ((String, Bool) -> Void)?
public var check: ((String) -> Bool)?
private let hapticFeedback = HapticFeedback()
private var presentationData: PresentationData
private var nextAction: UIBarButtonItem?
public init(context: AccountContext, mode: PasscodeSetupControllerMode) {
self.context = context
self.mode = mode
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.nextAction = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed))
self.title = self.presentationData.strings.PasscodeSettings_Title
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = PasscodeSetupControllerNode(presentationData: self.presentationData, mode: self.mode)
self.displayNodeDidLoad()
self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
self.controllerNode.selectPasscodeMode = { [weak self] in
guard let strongSelf = self, case let .setup(change, type) = strongSelf.mode else {
return
}
let controller = ActionSheetController(presentationData: strongSelf.presentationData)
let dismissAction: () -> Void = { [weak controller] in
self?.controllerNode.activateInput()
controller?.dismissAnimated()
}
var items: [ActionSheetButtonItem] = []
if case .digits6 = type {
} else {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.PasscodeSettings_6DigitCode, action: { [weak self] in
if let strongSelf = self {
strongSelf.mode = .setup(change: change, .digits6)
strongSelf.controllerNode.updateMode(strongSelf.mode)
}
dismissAction()
}))
}
if case .digits4 = type {
} else {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.PasscodeSettings_4DigitCode, action: {
if let strongSelf = self {
strongSelf.mode = .setup(change: change, .digits4)
strongSelf.controllerNode.updateMode(strongSelf.mode)
}
dismissAction()
}))
}
if case .alphanumeric = type {
} else {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.PasscodeSettings_AlphanumericCode, action: {
if let strongSelf = self {
strongSelf.mode = .setup(change: change, .alphanumeric)
strongSelf.controllerNode.updateMode(strongSelf.mode)
}
dismissAction()
}))
}
controller.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
controller.dismissed = { _ in
self?.controllerNode.activateInput()
}
strongSelf.view.endEditing(true)
strongSelf.present(controller, in: .window(.root))
}
self.controllerNode.updateNextAction = { [weak self] visible in
guard let strongSelf = self else {
return
}
if visible {
strongSelf.navigationItem.rightBarButtonItem = strongSelf.nextAction
} else {
strongSelf.navigationItem.rightBarButtonItem = nil
}
}
self.controllerNode.complete = { [weak self] passcode, numerical in
if let strongSelf = self {
strongSelf.complete?(passcode, numerical)
}
}
self.controllerNode.checkPasscode = { [weak self] passcode in
return self?.check?(passcode) ?? false
}
}
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.controllerNode.activateInput()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.view.disablesInteractiveTransitionGestureRecognizer = true
self.controllerNode.activateInput()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
@objc private func nextPressed() {
self.controllerNode.activateNext()
}
}
@@ -0,0 +1,280 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
import PasscodeInputFieldNode
enum PasscodeSetupInitialState {
case createPasscode
case changePassword(current: String, hasRecoveryEmail: Bool, hasSecureValues: Bool)
}
enum PasscodeSetupStateKind: Int32 {
case enterPasscode
case confirmPasscode
}
private func generateFieldBackground(backgroundColor: UIColor, borderColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 1.0, height: 48.0), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.setFillColor(backgroundColor.cgColor)
context.fill(bounds)
context.setFillColor(borderColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: UIScreenPixel)))
context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: 1.0, height: UIScreenPixel)))
})
}
final class PasscodeSetupControllerNode: ASDisplayNode {
private var presentationData: PresentationData
private var mode: PasscodeSetupControllerMode
private let wrapperNode: ASDisplayNode
private let titleNode: ASTextNode
private let subtitleNode: ASTextNode
private let inputFieldNode: PasscodeInputFieldNode
private let inputFieldBackgroundNode: ASImageNode
private let modeButtonNode: HighlightableButtonNode
var previousPasscode: String?
var currentPasscode: String {
return self.inputFieldNode.text
}
var selectPasscodeMode: (() -> Void)?
var checkPasscode: ((String) -> Bool)?
var complete: ((String, Bool) -> Void)?
var updateNextAction: ((Bool) -> Void)?
private let hapticFeedback = HapticFeedback()
private var validLayout: (ContainerViewLayout, CGFloat)?
private var maxBottomInset: CGFloat?
init(presentationData: PresentationData, mode: PasscodeSetupControllerMode) {
self.presentationData = presentationData
self.mode = mode
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 {
case let .numericalPassword(value):
passcodeType = value.count == 6 ? .digits6 : .digits4
default:
passcodeType = .alphanumeric
}
case .setup:
passcodeType = .digits6
}
self.inputFieldNode = PasscodeInputFieldNode(color: self.presentationData.theme.list.itemPrimaryTextColor, accentColor: self.presentationData.theme.list.itemAccentColor, fieldType: passcodeType, keyboardAppearance: self.presentationData.theme.rootController.keyboardColor.keyboardAppearance)
self.inputFieldBackgroundNode = ASImageNode()
self.inputFieldBackgroundNode.alpha = passcodeType == .alphanumeric ? 1.0 : 0.0
self.inputFieldBackgroundNode.contentMode = .scaleToFill
self.inputFieldBackgroundNode.image = generateFieldBackground(backgroundColor: self.presentationData.theme.list.itemBlocksBackgroundColor, borderColor: self.presentationData.theme.list.itemBlocksSeparatorColor)
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.addSubnode(self.wrapperNode)
self.wrapperNode.addSubnode(self.titleNode)
self.wrapperNode.addSubnode(self.subtitleNode)
self.wrapperNode.addSubnode(self.inputFieldBackgroundNode)
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
self?.activateNext()
}
self.modeButtonNode.addTarget(self, action: #selector(self.modePressed), forControlEvents: .touchUpInside)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [.statusBar, .input])
if let maxBottomInset = self.maxBottomInset {
if maxBottomInset > insets.bottom {
insets.bottom = maxBottomInset
} else {
self.maxBottomInset = insets.bottom
}
} else {
self.maxBottomInset = insets.bottom
}
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))
transition.updateFrame(node: self.inputFieldBackgroundNode, frame: CGRect(x: 0.0, y: inputFieldFrame.minY - 6.0, width: layout.size.width, height: 48.0))
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
}
func updateMode(_ mode: PasscodeSetupControllerMode) {
self.mode = mode
self.inputFieldNode.reset()
if case let .setup(_, type) = mode {
self.inputFieldNode.updateFieldType(type, animated: true)
let fieldBackgroundAlpha: CGFloat
if case .alphanumeric = type {
fieldBackgroundAlpha = 1.0
self.updateNextAction?(true)
} else {
fieldBackgroundAlpha = 0.0
self.updateNextAction?(false)
}
let previousAlpha = self.inputFieldBackgroundNode.alpha
self.inputFieldBackgroundNode.alpha = fieldBackgroundAlpha
self.inputFieldBackgroundNode.layer.animateAlpha(from: previousAlpha, to: fieldBackgroundAlpha, duration: 0.25)
self.subtitleNode.isHidden = true
}
}
func activateNext() {
guard !self.currentPasscode.isEmpty else {
self.animateError()
return
}
switch self.mode {
case .entry:
if !(self.checkPasscode?(self.currentPasscode) ?? false) {
self.animateError()
}
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() {
self.inputFieldNode.activateInput()
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()
}
@objc func modePressed() {
self.selectPasscodeMode?()
}
}