Swiftgram/submodules/PasscodeUI/Sources/PasscodeEntryControllerNode.swift
2019-09-21 02:59:11 +03:00

399 lines
18 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
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))
}
}
}