Swiftgram/submodules/PasscodeUI/Sources/PasscodeEntryControllerNode.swift
2019-11-04 19:44:25 +04:00

462 lines
21 KiB
Swift

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
import MonotonicTime
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
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 modalPresentation: Bool
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(accountManager: AccountManager, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, passcodeType: PasscodeEntryFieldType, biometricsType: LocalAuthBiometricAuthentication?, arguments: PasscodeEntryControllerPresentationArguments, statusBar: StatusBar, modalPresentation: Bool) {
self.accountManager = accountManager
self.theme = theme
self.strings = strings
self.wallpaper = wallpaper
self.passcodeType = passcodeType
self.biometricsType = biometricsType
self.arguments = arguments
self.statusBar = statusBar
self.modalPresentation = modalPresentation
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 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.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 {
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.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()
let bounds = CGRect(origin: CGPoint(), size: layout.size)
transition.updateFrame(node: self.backgroundNode, frame: bounds)
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
}
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))
}
}
}