import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import Postbox import TelegramCore import SyncCore import TelegramPresentationData import AccountContext import LocalAuth import AppBundle import PasscodeInputFieldNode 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 context: AccountContext 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 statusBar: StatusBar private let backgroundNode: 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)? init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, passcodeType: PasscodeEntryFieldType, biometricsType: LocalAuthBiometricAuthentication?, arguments: PasscodeEntryControllerPresentationArguments, statusBar: StatusBar) { self.context = context self.theme = theme self.strings = strings self.wallpaper = wallpaper self.passcodeType = passcodeType self.biometricsType = biometricsType self.arguments = arguments self.statusBar = statusBar self.backgroundNode = ASImageNode() self.backgroundNode.contentMode = .scaleToFill 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.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 self?.inputFieldNode.append(character) } 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.deleteButtonNode.setTitle(strings.Common_Delete, with: buttonFont, with: .white, for: .normal) 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.backgroundNode) 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) } } override func didLoad() { super.didLoad() 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() self.inputFieldNode.delete() } @objc private func biometricsPressed() { self.requestBiometrics?() } func activateInput() { self.inputFieldNode.activateInput() } func updatePresentationData(_ 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 } var size = validLayout.size if case .compact = validLayout.metrics.widthClass, size.width > size.height { size = CGSize(width: size.height, height: size.width) } if let background = self.background, background.size == size { return } switch self.wallpaper { case .image, .file: if let image = chatControllerBackgroundImage(theme: self.theme, wallpaper: self.wallpaper, mediaBox: self.context.sharedContext.accountManager.mediaBox, composed: false, knockoutMode: false) { self.background = ImageBasedPasscodeBackground(image: image, size: size) } else { self.background = GradientPasscodeBackground(size: size, backgroundColors: self.theme.passcode.backgroundColors.colors, buttonColor: self.theme.passcode.buttonColor) } default: self.background = GradientPasscodeBackground(size: size, backgroundColors: self.theme.passcode.backgroundColors.colors, buttonColor: self.theme.passcode.buttonColor) } if let background = self.background { self.backgroundNode.image = background.backgroundImage self.keyboardNode.updateBackground(background) self.inputFieldNode.updateBackground(background.foregroundImage, size: background.size) } } private let waitInterval: Int32 = 60 private func shouldWaitBeforeNextAttempt() -> Bool { if let attempts = self.invalidAttempts { if attempts.count >= 6 { if Int32(CFAbsoluteTimeGetCurrent()) - attempts.timestamp < 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: Double(attempts.timestamp + waitInterval - Int32(CFAbsoluteTimeGetCurrent())), repeat: false, completion: { [weak self] in if let strongSelf = self { strongSelf.timer = nil strongSelf.updateInvalidAttempts(strongSelf.invalidAttempts, animated: true) } }, 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.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } self.titleNode.setAttributedText(NSAttributedString(string: self.strings.EnterPasscode_EnterPasscode, font: titleFont, textColor: .white), animation: .none) } func animateIn(iconFrame: CGRect, completion: @escaping () -> Void = {}) { 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.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) 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) } self.statusBar.layer.removeAnimation(forKey: "opacity") self.statusBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) 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.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) 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) }) completion() }) } func animateOut(down: Bool = false, completion: @escaping () -> Void = {}) { self.statusBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) 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() } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.validLayout = layout self.updateBackground() if layout.size.width == 320.0 { self.iconNode.alpha = 0.0 } let bounds = CGRect(origin: CGPoint(), size: layout.size) transition.updateFrame(node: self.backgroundNode, frame: bounds) transition.updateFrame(view: self.effectView, frame: bounds) let iconSize = CGSize(width: 35.0, height: 37.0) transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0) + 6.0, y: layout.insets(options: .statusBar).top + 15.0), size: iconSize)) let passcodeLayout = PasscodeLayout(layout: layout) let inputFieldFrame = self.inputFieldNode.updateLayout(layout: passcodeLayout.layout, topOffset: passcodeLayout.inputFieldOffset, transition: transition) transition.updateFrame(node: self.inputFieldNode, frame: CGRect(origin: CGPoint(), size: layout.size)) let titleSize = self.titleNode.updateLayout(layout: layout, transition: transition) transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: passcodeLayout.titleOffset), size: titleSize)) var subtitleOffset = passcodeLayout.subtitleOffset if case .alphanumeric = self.passcodeType { subtitleOffset = 16.0 } let subtitleSize = self.subtitleNode.updateLayout(layout: layout, transition: transition) transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, 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(), size: layout.size)) 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 bottomInset = layout.inputHeight ?? 0.0 let cancelSize = self.cancelButtonNode.measure(layout.size) var cancelY: CGFloat = layout.size.height - layout.intrinsicInsets.bottom - cancelSize.height - passcodeLayout.keyboard.deleteOffset if bottomInset > 0 && self.keyboardNode.alpha < 1.0 { cancelY = layout.size.height - bottomInset - cancelSize.height - 20.0 } transition.updateFrame(node: self.cancelButtonNode, frame: CGRect(origin: CGPoint(x: floor(keyboardFrame.minX + keyboardButtonSize.width / 2.0 - cancelSize.width / 2.0), 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: layout.size.height - layout.intrinsicInsets.bottom - deleteSize.height - passcodeLayout.keyboard.deleteOffset), size: deleteSize)) if let biometricIcon = self.biometricButtonNode.image(for: .normal) { var biometricY: CGFloat = 0.0 if bottomInset > 0 && self.keyboardNode.alpha < 1.0 { 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: floor((layout.size.width - biometricIcon.size.width) / 2.0), y: biometricY), size: biometricIcon.size)) } } }