mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
448 lines
21 KiB
Swift
448 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
|
|
|
|
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 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) {
|
|
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.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 {
|
|
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()
|
|
|
|
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)
|
|
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))
|
|
}
|
|
}
|
|
}
|