mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1353 lines
71 KiB
Swift
1353 lines
71 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AppBundle
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import SolidRoundedButtonNode
|
|
import SwiftSignalKit
|
|
import OverlayStatusController
|
|
import AccountContext
|
|
import TelegramPresentationData
|
|
import PresentationDataUtils
|
|
import TelegramCore
|
|
import AnimatedStickerNode
|
|
|
|
public enum TwoFactorDataInputMode {
|
|
public struct Recovery {
|
|
public var code: String
|
|
public var syncContacts: Bool
|
|
public var account: UnauthorizedAccount
|
|
|
|
public init(code: String, syncContacts: Bool, account: UnauthorizedAccount) {
|
|
self.code = code
|
|
self.syncContacts = syncContacts
|
|
self.account = account
|
|
}
|
|
}
|
|
|
|
case password
|
|
case passwordRecovery(Recovery)
|
|
case emailAddress(password: String, hint: String)
|
|
case updateEmailAddress(password: String)
|
|
case emailConfirmation(passwordAndHint: (String, String)?, emailPattern: String, codeLength: Int?)
|
|
case passwordHint(recovery: Recovery?, password: String)
|
|
}
|
|
|
|
public final class TwoFactorDataInputScreen: ViewController {
|
|
private let sharedContext: SharedAccountContext
|
|
private let network: Network
|
|
private var presentationData: PresentationData
|
|
private let mode: TwoFactorDataInputMode
|
|
private let stateUpdated: (SetupTwoStepVerificationStateUpdate) -> Void
|
|
|
|
public init(sharedContext: SharedAccountContext, network: Network, mode: TwoFactorDataInputMode, stateUpdated: @escaping (SetupTwoStepVerificationStateUpdate) -> Void) {
|
|
self.sharedContext = sharedContext
|
|
self.network = network
|
|
self.mode = mode
|
|
self.stateUpdated = stateUpdated
|
|
|
|
self.presentationData = self.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
let defaultTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme)
|
|
let navigationBarTheme = NavigationBarTheme(buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: defaultTheme.badgeBackgroundColor, badgeStrokeColor: defaultTheme.badgeStrokeColor, badgeTextColor: defaultTheme.badgeTextColor)
|
|
|
|
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Common_Back, close: self.presentationData.strings.Common_Close)))
|
|
|
|
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
|
self.navigationPresentation = .modalInLargeLayout
|
|
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
|
self.navigationBar?.intrinsicCanTransitionInline = false
|
|
|
|
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
|
}
|
|
|
|
required init(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
@objc private func backPressed() {
|
|
self.dismiss()
|
|
}
|
|
|
|
override public func loadDisplayNode() {
|
|
self.displayNode = TwoFactorDataInputScreenNode(presentationData: self.presentationData, mode: self.mode, action: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
switch strongSelf.mode {
|
|
case .password:
|
|
let values = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText
|
|
if values.count != 2 {
|
|
return
|
|
}
|
|
if values[0] != values[1] {
|
|
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_SetupPasswordConfirmFailed, actions: [
|
|
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})
|
|
]), in: .window(.root))
|
|
return
|
|
}
|
|
if values[0].isEmpty {
|
|
return
|
|
}
|
|
guard let navigationController = strongSelf.navigationController as? NavigationController else {
|
|
return
|
|
}
|
|
var controllers = navigationController.viewControllers.filter { controller in
|
|
if controller is TwoFactorAuthSplashScreen {
|
|
return false
|
|
}
|
|
if controller is TwoFactorDataInputScreen && controller !== strongSelf {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
controllers.append(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, network: strongSelf.network, mode: .passwordHint(recovery: nil, password: values[0]), stateUpdated: strongSelf.stateUpdated))
|
|
navigationController.setViewControllers(controllers, animated: true)
|
|
case let .passwordRecovery(recovery):
|
|
let values = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText
|
|
if values.count != 2 {
|
|
return
|
|
}
|
|
if values[0] != values[1] {
|
|
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_SetupPasswordConfirmFailed, actions: [
|
|
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})
|
|
]), in: .window(.root))
|
|
return
|
|
}
|
|
if values[0].isEmpty {
|
|
return
|
|
}
|
|
guard let navigationController = strongSelf.navigationController as? NavigationController else {
|
|
return
|
|
}
|
|
var controllers = navigationController.viewControllers.filter { controller in
|
|
if controller is TwoFactorAuthSplashScreen {
|
|
return false
|
|
}
|
|
if controller is TwoFactorDataInputScreen && controller !== strongSelf {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
controllers.append(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, network: strongSelf.network, mode: .passwordHint(recovery: recovery, password: values[0]), stateUpdated: strongSelf.stateUpdated))
|
|
navigationController.setViewControllers(controllers, animated: true)
|
|
case let .emailAddress(password, hint):
|
|
guard let text = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !text.isEmpty else {
|
|
return
|
|
}
|
|
let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil))
|
|
strongSelf.present(statusController, in: .window(.root))
|
|
|
|
let _ = (updateTwoStepVerificationPassword(network: strongSelf.network, currentPassword: "", updatedPassword: .password(password: password, hint: hint, email: text))
|
|
|> deliverOnMainQueue).start(next: { [weak statusController] result in
|
|
statusController?.dismiss()
|
|
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
switch result {
|
|
case .none:
|
|
break
|
|
case let .password(_, pendingEmail):
|
|
if let pendingEmail = pendingEmail {
|
|
guard let navigationController = strongSelf.navigationController as? NavigationController else {
|
|
return
|
|
}
|
|
var controllers = navigationController.viewControllers.filter { controller in
|
|
if controller is TwoFactorAuthSplashScreen {
|
|
return false
|
|
}
|
|
if controller is TwoFactorDataInputScreen {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
controllers.append(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, network: strongSelf.network, mode: .emailConfirmation(passwordAndHint: (password, hint), emailPattern: text, codeLength: pendingEmail.codeLength.flatMap(Int.init)), stateUpdated: strongSelf.stateUpdated))
|
|
navigationController.setViewControllers(controllers, animated: true)
|
|
} else {
|
|
guard let navigationController = strongSelf.navigationController as? NavigationController else {
|
|
return
|
|
}
|
|
var controllers = navigationController.viewControllers.filter { controller in
|
|
if controller is TwoFactorAuthSplashScreen {
|
|
return false
|
|
}
|
|
if controller is TwoFactorDataInputScreen {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
controllers.append(TwoFactorAuthSplashScreen(sharedContext: strongSelf.sharedContext, network: strongSelf.network, mode: .done))
|
|
navigationController.setViewControllers(controllers, animated: true)
|
|
}
|
|
}
|
|
}, error: { [weak statusController] error in
|
|
statusController?.dismiss()
|
|
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
let presentationData = strongSelf.presentationData
|
|
let alertText: String
|
|
switch error {
|
|
case .generic:
|
|
alertText = presentationData.strings.Login_UnknownError
|
|
case .invalidEmail:
|
|
alertText = presentationData.strings.TwoStepAuth_EmailInvalid
|
|
}
|
|
strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
|
})
|
|
case let .updateEmailAddress(password):
|
|
guard let text = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !text.isEmpty else {
|
|
return
|
|
}
|
|
let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil))
|
|
strongSelf.present(statusController, in: .window(.root))
|
|
|
|
let _ = (updateTwoStepVerificationEmail(network: strongSelf.network, currentPassword: password, updatedEmail: text)
|
|
|> deliverOnMainQueue).start(next: { [weak statusController] result in
|
|
statusController?.dismiss()
|
|
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
switch result {
|
|
case .none:
|
|
break
|
|
case let .password(_, pendingEmail):
|
|
if let pendingEmail = pendingEmail {
|
|
guard let navigationController = strongSelf.navigationController as? NavigationController else {
|
|
return
|
|
}
|
|
var controllers = navigationController.viewControllers.filter { controller in
|
|
if controller is TwoFactorAuthSplashScreen {
|
|
return false
|
|
}
|
|
if controller is TwoFactorDataInputScreen {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
controllers.append(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, network: strongSelf.network, mode: .emailConfirmation(passwordAndHint: (password, ""), emailPattern: text, codeLength: pendingEmail.codeLength.flatMap(Int.init)), stateUpdated: strongSelf.stateUpdated))
|
|
navigationController.setViewControllers(controllers, animated: true)
|
|
} else {
|
|
guard let navigationController = strongSelf.navigationController as? NavigationController else {
|
|
return
|
|
}
|
|
var controllers = navigationController.viewControllers.filter { controller in
|
|
if controller is TwoFactorAuthSplashScreen {
|
|
return false
|
|
}
|
|
if controller is TwoFactorDataInputScreen {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
controllers.append(TwoFactorAuthSplashScreen(sharedContext: strongSelf.sharedContext, network: strongSelf.network, mode: .done))
|
|
navigationController.setViewControllers(controllers, animated: true)
|
|
}
|
|
}
|
|
}, error: { [weak statusController] error in
|
|
statusController?.dismiss()
|
|
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
let presentationData = strongSelf.presentationData
|
|
let alertText: String
|
|
switch error {
|
|
case .generic:
|
|
alertText = presentationData.strings.Login_UnknownError
|
|
case .invalidEmail:
|
|
alertText = presentationData.strings.TwoStepAuth_EmailInvalid
|
|
}
|
|
strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
|
})
|
|
case .emailConfirmation:
|
|
guard let text = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !text.isEmpty else {
|
|
return
|
|
}
|
|
let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil))
|
|
strongSelf.present(statusController, in: .window(.root))
|
|
|
|
let _ = (confirmTwoStepRecoveryEmail(network: strongSelf.network, code: text)
|
|
|> deliverOnMainQueue).start(error: { [weak statusController] error in
|
|
statusController?.dismiss()
|
|
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
let presentationData = strongSelf.presentationData
|
|
let text: String
|
|
switch error {
|
|
case .invalidEmail:
|
|
text = presentationData.strings.TwoStepAuth_EmailInvalid
|
|
case .invalidCode:
|
|
text = presentationData.strings.Login_InvalidCodeError
|
|
case .expired:
|
|
text = presentationData.strings.TwoStepAuth_EmailCodeExpired
|
|
case .flood:
|
|
text = presentationData.strings.TwoStepAuth_FloodError
|
|
case .generic:
|
|
text = presentationData.strings.Login_UnknownError
|
|
}
|
|
strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
|
}, completed: { [weak statusController] in
|
|
statusController?.dismiss()
|
|
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
guard let navigationController = strongSelf.navigationController as? NavigationController else {
|
|
return
|
|
}
|
|
var controllers = navigationController.viewControllers.filter { controller in
|
|
if controller is TwoFactorAuthSplashScreen {
|
|
return false
|
|
}
|
|
if controller is TwoFactorDataInputScreen {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
controllers.append(TwoFactorAuthSplashScreen(sharedContext: strongSelf.sharedContext, network: strongSelf.network, mode: .done))
|
|
navigationController.setViewControllers(controllers, animated: true)
|
|
})
|
|
case let .passwordHint(recovery, password):
|
|
guard let value = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !value.isEmpty else {
|
|
return
|
|
}
|
|
|
|
if let recovery = recovery {
|
|
strongSelf.performRecovery(recovery: recovery, password: password, hint: value)
|
|
} else {
|
|
strongSelf.push(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, network: strongSelf.network, mode: .emailAddress(password: password, hint: value), stateUpdated: strongSelf.stateUpdated))
|
|
}
|
|
}
|
|
}, skipAction: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
switch strongSelf.mode {
|
|
case let .emailAddress(password, hint):
|
|
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationTitle, text: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationText, actions: [
|
|
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationSkip, action: {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil))
|
|
strongSelf.present(statusController, in: .window(.root))
|
|
|
|
let _ = (updateTwoStepVerificationPassword(network: strongSelf.network, currentPassword: "", updatedPassword: .password(password: password, hint: hint, email: nil))
|
|
|> deliverOnMainQueue).start(next: { [weak statusController] result in
|
|
statusController?.dismiss()
|
|
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
switch result {
|
|
case .none:
|
|
break
|
|
case .password:
|
|
guard let navigationController = strongSelf.navigationController as? NavigationController else {
|
|
return
|
|
}
|
|
var controllers = navigationController.viewControllers.filter { controller in
|
|
if controller is TwoFactorAuthSplashScreen {
|
|
return false
|
|
}
|
|
if controller is TwoFactorDataInputScreen {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
controllers.append(TwoFactorAuthSplashScreen(sharedContext: strongSelf.sharedContext, network: strongSelf.network, mode: .done))
|
|
navigationController.setViewControllers(controllers, animated: true)
|
|
}
|
|
}, error: { [weak statusController] error in
|
|
statusController?.dismiss()
|
|
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
let presentationData = strongSelf.presentationData
|
|
let alertText: String
|
|
switch error {
|
|
case .generic:
|
|
alertText = presentationData.strings.Login_UnknownError
|
|
case .invalidEmail:
|
|
alertText = presentationData.strings.TwoStepAuth_EmailInvalid
|
|
}
|
|
strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
|
})
|
|
}),
|
|
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})
|
|
]), in: .window(.root))
|
|
case let .passwordHint(recovery, password):
|
|
if let recovery = recovery {
|
|
strongSelf.performRecovery(recovery: recovery, password: password, hint: "")
|
|
} else {
|
|
strongSelf.push(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, network: strongSelf.network, mode: .emailAddress(password: password, hint: ""), stateUpdated: strongSelf.stateUpdated))
|
|
}
|
|
case let .passwordRecovery(recovery):
|
|
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.TwoFactorSetup_PasswordRecovery_SkipAlertTitle, text: strongSelf.presentationData.strings.TwoFactorSetup_PasswordRecovery_SkipAlertText, actions: [
|
|
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.TwoFactorSetup_PasswordRecovery_SkipAlertAction, action: {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.performRecovery(recovery: recovery, password: "", hint: "")
|
|
}),
|
|
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})
|
|
]), in: .window(.root))
|
|
default:
|
|
break
|
|
}
|
|
}, changeEmailAction: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
switch strongSelf.mode {
|
|
case let .emailConfirmation(passwordAndHint, _, _):
|
|
if let (password, hint) = passwordAndHint {
|
|
guard let navigationController = strongSelf.navigationController as? NavigationController else {
|
|
return
|
|
}
|
|
var controllers = navigationController.viewControllers.filter { controller in
|
|
if controller is TwoFactorAuthSplashScreen {
|
|
return false
|
|
}
|
|
if controller is TwoFactorDataInputScreen {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
controllers.append(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, network: strongSelf.network, mode: .emailAddress(password: password, hint: hint), stateUpdated: strongSelf.stateUpdated))
|
|
navigationController.setViewControllers(controllers, animated: true)
|
|
} else {
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}, resendCodeAction: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil))
|
|
strongSelf.present(statusController, in: .window(.root))
|
|
|
|
let _ = (resendTwoStepRecoveryEmail(network: strongSelf.network)
|
|
|> deliverOnMainQueue).start(error: { [weak statusController] error in
|
|
statusController?.dismiss()
|
|
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
let text: String
|
|
switch error {
|
|
case .flood:
|
|
text = strongSelf.presentationData.strings.TwoStepAuth_FloodError
|
|
case .generic:
|
|
text = strongSelf.presentationData.strings.Login_UnknownError
|
|
}
|
|
strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
|
}, completed: { [weak statusController] in
|
|
statusController?.dismiss()
|
|
})
|
|
})
|
|
|
|
self.displayNodeDidLoad()
|
|
}
|
|
|
|
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
|
super.containerLayoutUpdated(layout, transition: transition)
|
|
|
|
(self.displayNode as! TwoFactorDataInputScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
|
|
}
|
|
|
|
private func performRecovery(recovery: TwoFactorDataInputMode.Recovery, password: String, hint: String) {
|
|
/*let statusController = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: nil))
|
|
self.present(statusController, in: .window(.root))
|
|
|
|
let _ = (performPasswordRecovery(accountManager: self.sharedContext.accountManager, account: recovery.account, code: recovery.code, syncContacts: recovery.syncContacts, updatedPassword: password.isEmpty ? .none : .password(password: password, hint: hint, email: nil))
|
|
|> deliverOnMainQueue).start(error: { [weak self, weak statusController] error in
|
|
statusController?.dismiss()
|
|
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
let text: String
|
|
switch error {
|
|
case .limitExceeded:
|
|
text = strongSelf.presentationData.strings.LoginPassword_FloodError
|
|
case .invalidCode:
|
|
text = strongSelf.presentationData.strings.Login_InvalidCodeError
|
|
case .expired:
|
|
text = strongSelf.presentationData.strings.Login_CodeExpiredError
|
|
case .generic:
|
|
text = strongSelf.presentationData.strings.Login_UnknownError
|
|
}
|
|
|
|
strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
|
}, completed: { [weak self, weak statusController] in
|
|
statusController?.dismiss()
|
|
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
strongSelf.dismiss()
|
|
})*/
|
|
}
|
|
}
|
|
|
|
private enum TwoFactorDataInputTextNodeType {
|
|
case password(confirmation: Bool)
|
|
case email
|
|
case code
|
|
case hint
|
|
}
|
|
|
|
private func generateClearImage(color: UIColor) -> UIImage? {
|
|
return generateImage(CGSize(width: 16.0, height: 16.0), rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
context.setFillColor(color.cgColor)
|
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
|
context.setBlendMode(.copy)
|
|
context.setStrokeColor(UIColor.clear.cgColor)
|
|
context.setLineCap(.round)
|
|
context.setLineWidth(1.66)
|
|
context.move(to: CGPoint(x: 5.5, y: 5.5))
|
|
context.addLine(to: CGPoint(x: 10.5, y: 10.5))
|
|
context.strokePath()
|
|
context.move(to: CGPoint(x: size.width - 5.5, y: 5.5))
|
|
context.addLine(to: CGPoint(x: size.width - 10.5, y: 10.5))
|
|
context.strokePath()
|
|
})
|
|
}
|
|
|
|
private func generateTextHiddenImage(color: UIColor, on: Bool) -> UIImage? {
|
|
return generateImage(CGSize(width: 20.0, height: 18.0), rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
guard let image = generateTintedImage(image: UIImage(bundleImageName: "PasswordSetup/TextHidden"), color: color) else {
|
|
return
|
|
}
|
|
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - size.width) / 2.0), y: floor((size.height - size.height) / 2.0)), size: size))
|
|
if !on {
|
|
context.setLineCap(.round)
|
|
|
|
context.setBlendMode(.copy)
|
|
context.setStrokeColor(UIColor.clear.cgColor)
|
|
context.setLineWidth(4.0)
|
|
context.move(to: CGPoint(x: 2.0, y: 3.0))
|
|
context.addLine(to: CGPoint(x: 18.0, y: 17.0))
|
|
context.strokePath()
|
|
|
|
context.setBlendMode(.normal)
|
|
context.setStrokeColor(color.cgColor)
|
|
context.setLineWidth(1.5)
|
|
context.move(to: CGPoint(x: 2.0, y: 3.0))
|
|
context.addLine(to: CGPoint(x: 18.0, y: 17.0))
|
|
context.strokePath()
|
|
}
|
|
})
|
|
}
|
|
|
|
private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelegate {
|
|
private let theme: PresentationTheme
|
|
let mode: TwoFactorDataInputTextNodeType
|
|
private let focusUpdated: (TwoFactorDataInputTextNode, Bool) -> Void
|
|
private let next: (TwoFactorDataInputTextNode) -> Void
|
|
private let updated: (TwoFactorDataInputTextNode) -> Void
|
|
private let toggleTextHidden: (TwoFactorDataInputTextNode) -> Void
|
|
|
|
private let backgroundNode: ASImageNode
|
|
private let inputNode: TextFieldNode
|
|
private let hideButtonNode: HighlightableButtonNode
|
|
private let clearButtonNode: HighlightableButtonNode
|
|
|
|
fileprivate var ignoreTextChanged: Bool = false
|
|
|
|
var isFocused: Bool {
|
|
return self.inputNode.textField.isFirstResponder
|
|
}
|
|
|
|
var text: String {
|
|
get {
|
|
return self.inputNode.textField.text ?? ""
|
|
} set(value) {
|
|
self.inputNode.textField.text = value
|
|
self.textFieldChanged(self.inputNode.textField)
|
|
}
|
|
}
|
|
|
|
init(theme: PresentationTheme, mode: TwoFactorDataInputTextNodeType, placeholder: String, focusUpdated: @escaping (TwoFactorDataInputTextNode, Bool) -> Void, next: @escaping (TwoFactorDataInputTextNode) -> Void, updated: @escaping (TwoFactorDataInputTextNode) -> Void, toggleTextHidden: @escaping (TwoFactorDataInputTextNode) -> Void) {
|
|
self.theme = theme
|
|
self.mode = mode
|
|
self.focusUpdated = focusUpdated
|
|
self.next = next
|
|
self.updated = updated
|
|
self.toggleTextHidden = toggleTextHidden
|
|
|
|
self.backgroundNode = ASImageNode()
|
|
self.backgroundNode.displaysAsynchronously = false
|
|
self.backgroundNode.displayWithoutProcessing = true
|
|
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: theme.list.freePlainInputField.backgroundColor)
|
|
|
|
self.inputNode = TextFieldNode()
|
|
self.inputNode.textField.font = Font.regular(17.0)
|
|
self.inputNode.textField.textColor = theme.list.freePlainInputField.primaryColor
|
|
self.inputNode.textField.attributedPlaceholder = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.list.freePlainInputField.placeholderColor)
|
|
|
|
self.hideButtonNode = HighlightableButtonNode()
|
|
|
|
switch mode {
|
|
case let .password(confirmation):
|
|
self.inputNode.textField.keyboardType = .default
|
|
self.inputNode.textField.isSecureTextEntry = true
|
|
if confirmation {
|
|
self.inputNode.textField.returnKeyType = .done
|
|
} else {
|
|
self.inputNode.textField.returnKeyType = .next
|
|
}
|
|
self.hideButtonNode.isHidden = confirmation
|
|
case .email:
|
|
self.inputNode.textField.keyboardType = .emailAddress
|
|
self.inputNode.textField.returnKeyType = .done
|
|
self.hideButtonNode.isHidden = true
|
|
case .code:
|
|
self.inputNode.textField.keyboardType = .numberPad
|
|
self.inputNode.textField.returnKeyType = .done
|
|
self.hideButtonNode.isHidden = true
|
|
case .hint:
|
|
self.inputNode.textField.keyboardType = .asciiCapable
|
|
self.inputNode.textField.returnKeyType = .done
|
|
self.hideButtonNode.isHidden = true
|
|
}
|
|
|
|
self.inputNode.textField.autocorrectionType = .no
|
|
self.inputNode.textField.autocapitalizationType = .none
|
|
self.inputNode.textField.spellCheckingType = .no
|
|
if #available(iOS 11.0, *) {
|
|
self.inputNode.textField.smartQuotesType = .no
|
|
self.inputNode.textField.smartDashesType = .no
|
|
self.inputNode.textField.smartInsertDeleteType = .no
|
|
}
|
|
self.inputNode.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
|
|
|
|
self.hideButtonNode.setImage(generateTextHiddenImage(color: theme.list.freePlainInputField.controlColor, on: false), for: [])
|
|
|
|
self.clearButtonNode = HighlightableButtonNode()
|
|
self.clearButtonNode.setImage(generateClearImage(color: theme.list.freePlainInputField.controlColor), for: [])
|
|
self.clearButtonNode.isHidden = true
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.backgroundNode)
|
|
self.addSubnode(self.inputNode)
|
|
self.addSubnode(self.hideButtonNode)
|
|
|
|
self.inputNode.textField.delegate = self
|
|
self.inputNode.textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged)
|
|
|
|
self.hideButtonNode.addTarget(self, action: #selector(self.hidePressed), forControlEvents: .touchUpInside)
|
|
}
|
|
|
|
func textFieldDidBeginEditing(_ textField: UITextField) {
|
|
let text = self.text
|
|
|
|
if self.inputNode.textField.isSecureTextEntry {
|
|
let previousIgnoreTextChanged = self.ignoreTextChanged
|
|
self.ignoreTextChanged = true
|
|
self.inputNode.textField.text = ""
|
|
self.inputNode.textField.insertText(text + " ")
|
|
self.inputNode.textField.deleteBackward()
|
|
self.ignoreTextChanged = previousIgnoreTextChanged
|
|
}
|
|
|
|
self.focusUpdated(self, true)
|
|
}
|
|
|
|
func textFieldDidEndEditing(_ textField: UITextField) {
|
|
self.focusUpdated(self, false)
|
|
}
|
|
|
|
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
|
self.next(self)
|
|
return false
|
|
}
|
|
|
|
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
|
return true
|
|
}
|
|
|
|
@objc private func textFieldChanged(_ textField: UITextField) {
|
|
if !self.ignoreTextChanged {
|
|
switch self.mode {
|
|
case .password:
|
|
break
|
|
default:
|
|
self.clearButtonNode.isHidden = self.text.isEmpty
|
|
}
|
|
self.updated(self)
|
|
}
|
|
}
|
|
|
|
@objc private func hidePressed() {
|
|
switch self.mode {
|
|
case .password:
|
|
self.toggleTextHidden(self)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
|
let leftInset: CGFloat = 16.0
|
|
let rightInset: CGFloat = 38.0
|
|
|
|
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
|
|
transition.updateFrame(node: self.inputNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: size.width - leftInset - rightInset, height: size.height)))
|
|
transition.updateFrame(node: self.hideButtonNode, frame: CGRect(origin: CGPoint(x: size.width - rightInset - 4.0, y: 0.0), size: CGSize(width: rightInset + 4.0, height: size.height)))
|
|
transition.updateFrame(node: self.clearButtonNode, frame: CGRect(origin: CGPoint(x: size.width - rightInset - 4.0, y: 0.0), size: CGSize(width: rightInset + 4.0, height: size.height)))
|
|
}
|
|
|
|
func focus() {
|
|
self.inputNode.textField.becomeFirstResponder()
|
|
}
|
|
|
|
func updateTextHidden(_ value: Bool) {
|
|
self.hideButtonNode.setImage(generateTextHiddenImage(color: self.theme.actionSheet.inputClearButtonColor, on: !value), for: [])
|
|
let text = self.inputNode.textField.text ?? ""
|
|
self.inputNode.textField.isSecureTextEntry = value
|
|
if value {
|
|
if self.inputNode.textField.isFirstResponder {
|
|
let previousIgnoreTextChanged = self.ignoreTextChanged
|
|
self.ignoreTextChanged = true
|
|
self.inputNode.textField.text = ""
|
|
self.inputNode.textField.becomeFirstResponder()
|
|
self.inputNode.textField.insertText(text + " ")
|
|
self.inputNode.textField.deleteBackward()
|
|
self.ignoreTextChanged = previousIgnoreTextChanged
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIScrollViewDelegate {
|
|
private var presentationData: PresentationData
|
|
private let mode: TwoFactorDataInputMode
|
|
private let action: () -> Void
|
|
private let skipAction: () -> Void
|
|
private let changeEmailAction: () -> Void
|
|
private let resendCodeAction: () -> Void
|
|
|
|
private let navigationBackgroundNode: ASDisplayNode
|
|
private let navigationSeparatorNode: ASDisplayNode
|
|
private let scrollNode: ASScrollNode
|
|
private var animatedStickerNode: AnimatedStickerNode?
|
|
private var monkeyNode: ManagedMonkeyAnimationNode?
|
|
private let titleNode: ImmediateTextNode
|
|
private let textNode: ImmediateTextNode
|
|
private let skipActionTitleNode: ImmediateTextNode
|
|
private let skipActionButtonNode: HighlightTrackingButtonNode
|
|
private let changeEmailActionTitleNode: ImmediateTextNode
|
|
private let changeEmailActionButtonNode: HighlightTrackingButtonNode
|
|
private let resendCodeActionTitleNode: ImmediateTextNode
|
|
private let resendCodeActionButtonNode: HighlightTrackingButtonNode
|
|
private let inputNodes: [TwoFactorDataInputTextNode]
|
|
private let buttonNode: SolidRoundedButtonNode
|
|
|
|
private var navigationHeight: CGFloat?
|
|
|
|
var inputText: [String] {
|
|
return self.inputNodes.map { $0.text }
|
|
}
|
|
|
|
init(presentationData: PresentationData, mode: TwoFactorDataInputMode, action: @escaping () -> Void, skipAction: @escaping () -> Void, changeEmailAction: @escaping () -> Void, resendCodeAction: @escaping () -> Void) {
|
|
self.presentationData = presentationData
|
|
self.mode = mode
|
|
self.action = action
|
|
self.skipAction = skipAction
|
|
self.changeEmailAction = changeEmailAction
|
|
self.resendCodeAction = resendCodeAction
|
|
|
|
self.navigationBackgroundNode = ASDisplayNode()
|
|
self.navigationBackgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor
|
|
self.navigationBackgroundNode.alpha = 0.0
|
|
self.navigationSeparatorNode = ASDisplayNode()
|
|
self.navigationSeparatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
|
|
|
|
self.scrollNode = ASScrollNode()
|
|
self.scrollNode.canCancelAllTouchesInViews = true
|
|
|
|
switch mode {
|
|
case .password, .passwordRecovery, .emailAddress, .updateEmailAddress:
|
|
self.monkeyNode = ManagedMonkeyAnimationNode()
|
|
case .emailConfirmation:
|
|
if let path = getAppBundle().path(forResource: "TwoFactorSetupMail", ofType: "tgs") {
|
|
let animatedStickerNode = AnimatedStickerNode()
|
|
animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 272, height: 272, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
|
animatedStickerNode.visibility = true
|
|
self.animatedStickerNode = animatedStickerNode
|
|
}
|
|
case .passwordHint:
|
|
if let path = getAppBundle().path(forResource: "TwoFactorSetupHint", ofType: "tgs") {
|
|
let animatedStickerNode = AnimatedStickerNode()
|
|
animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 272, height: 272, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
|
animatedStickerNode.visibility = true
|
|
self.animatedStickerNode = animatedStickerNode
|
|
}
|
|
}
|
|
|
|
let title: String
|
|
let text: NSAttributedString
|
|
let buttonText: String
|
|
let skipActionText: String
|
|
let changeEmailActionText: String
|
|
let resendCodeActionText: String
|
|
|
|
var inputNodes: [TwoFactorDataInputTextNode] = []
|
|
var next: ((TwoFactorDataInputTextNode) -> Void)?
|
|
var focusUpdated: ((TwoFactorDataInputTextNode, Bool) -> Void)?
|
|
var updated: ((TwoFactorDataInputTextNode) -> Void)?
|
|
var toggleTextHidden: ((TwoFactorDataInputTextNode) -> Void)?
|
|
|
|
switch mode {
|
|
case .password:
|
|
title = presentationData.strings.TwoFactorSetup_Password_Title
|
|
text = NSAttributedString(string: "", font: Font.regular(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
|
|
buttonText = presentationData.strings.TwoFactorSetup_Password_Action
|
|
skipActionText = ""
|
|
changeEmailActionText = ""
|
|
resendCodeActionText = ""
|
|
inputNodes = [
|
|
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .password(confirmation: false), placeholder: presentationData.strings.TwoFactorSetup_Password_PlaceholderPassword, focusUpdated: { node, focused in
|
|
focusUpdated?(node, focused)
|
|
}, next: { node in
|
|
next?(node)
|
|
}, updated: { node in
|
|
updated?(node)
|
|
}, toggleTextHidden: { node in
|
|
toggleTextHidden?(node)
|
|
}),
|
|
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .password(confirmation: true), placeholder: presentationData.strings.TwoFactorSetup_Password_PlaceholderConfirmPassword, focusUpdated: { node, focused in
|
|
focusUpdated?(node, focused)
|
|
}, next: { node in
|
|
next?(node)
|
|
}, updated: { node in
|
|
updated?(node)
|
|
}, toggleTextHidden: { node in
|
|
toggleTextHidden?(node)
|
|
})
|
|
]
|
|
case .passwordRecovery:
|
|
title = presentationData.strings.TwoFactorSetup_PasswordRecovery_Title
|
|
text = NSAttributedString(string: presentationData.strings.TwoFactorSetup_PasswordRecovery_Text, font: Font.regular(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
|
|
buttonText = presentationData.strings.TwoFactorSetup_PasswordRecovery_Action
|
|
skipActionText = presentationData.strings.TwoFactorSetup_PasswordRecovery_Skip
|
|
changeEmailActionText = ""
|
|
resendCodeActionText = ""
|
|
inputNodes = [
|
|
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .password(confirmation: false), placeholder: presentationData.strings.TwoFactorSetup_PasswordRecovery_PlaceholderPassword, focusUpdated: { node, focused in
|
|
focusUpdated?(node, focused)
|
|
}, next: { node in
|
|
next?(node)
|
|
}, updated: { node in
|
|
updated?(node)
|
|
}, toggleTextHidden: { node in
|
|
toggleTextHidden?(node)
|
|
}),
|
|
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .password(confirmation: true), placeholder: presentationData.strings.TwoFactorSetup_PasswordRecovery_PlaceholderConfirmPassword, focusUpdated: { node, focused in
|
|
focusUpdated?(node, focused)
|
|
}, next: { node in
|
|
next?(node)
|
|
}, updated: { node in
|
|
updated?(node)
|
|
}, toggleTextHidden: { node in
|
|
toggleTextHidden?(node)
|
|
})
|
|
]
|
|
case .emailAddress, .updateEmailAddress:
|
|
title = presentationData.strings.TwoFactorSetup_Email_Title
|
|
text = NSAttributedString(string: presentationData.strings.TwoFactorSetup_Email_Text, font: Font.regular(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
|
|
buttonText = presentationData.strings.TwoFactorSetup_Email_Action
|
|
skipActionText = presentationData.strings.TwoFactorSetup_Email_SkipAction
|
|
changeEmailActionText = ""
|
|
resendCodeActionText = ""
|
|
inputNodes = [
|
|
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .email, placeholder: presentationData.strings.TwoFactorSetup_Email_Placeholder, focusUpdated: { node, focused in
|
|
focusUpdated?(node, focused)
|
|
}, next: { node in
|
|
next?(node)
|
|
}, updated: { node in
|
|
updated?(node)
|
|
}, toggleTextHidden: { node in
|
|
toggleTextHidden?(node)
|
|
}),
|
|
]
|
|
case let .emailConfirmation(_, emailPattern, _):
|
|
title = presentationData.strings.TwoFactorSetup_EmailVerification_Title
|
|
let (rawText, ranges) = presentationData.strings.TwoFactorSetup_EmailVerification_Text(emailPattern)
|
|
|
|
let string = NSMutableAttributedString()
|
|
string.append(NSAttributedString(string: rawText, font: Font.regular(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor))
|
|
for (_, range) in ranges {
|
|
string.addAttribute(.font, value: Font.semibold(16.0), range: range)
|
|
}
|
|
|
|
text = string
|
|
|
|
buttonText = presentationData.strings.TwoFactorSetup_EmailVerification_Action
|
|
skipActionText = ""
|
|
changeEmailActionText = presentationData.strings.TwoFactorSetup_EmailVerification_ChangeAction
|
|
resendCodeActionText = presentationData.strings.TwoFactorSetup_EmailVerification_ResendAction
|
|
inputNodes = [
|
|
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .code, placeholder: presentationData.strings.TwoFactorSetup_EmailVerification_Placeholder, focusUpdated: { node, focused in
|
|
focusUpdated?(node, focused)
|
|
}, next: { node in
|
|
next?(node)
|
|
}, updated: { node in
|
|
updated?(node)
|
|
}, toggleTextHidden: { node in
|
|
toggleTextHidden?(node)
|
|
}),
|
|
]
|
|
case .passwordHint:
|
|
title = presentationData.strings.TwoFactorSetup_Hint_Title
|
|
|
|
text = NSAttributedString(string: presentationData.strings.TwoFactorSetup_Hint_Text, font: Font.regular(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
|
|
|
|
buttonText = presentationData.strings.TwoFactorSetup_Hint_Action
|
|
skipActionText = presentationData.strings.TwoFactorSetup_Hint_SkipAction
|
|
changeEmailActionText = ""
|
|
resendCodeActionText = ""
|
|
inputNodes = [
|
|
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .hint, placeholder: presentationData.strings.TwoFactorSetup_Hint_Placeholder, focusUpdated: { node, focused in
|
|
focusUpdated?(node, focused)
|
|
}, next: { node in
|
|
next?(node)
|
|
}, updated: { node in
|
|
updated?(node)
|
|
}, toggleTextHidden: { node in
|
|
toggleTextHidden?(node)
|
|
}),
|
|
]
|
|
}
|
|
|
|
self.titleNode = ImmediateTextNode()
|
|
self.titleNode.displaysAsynchronously = false
|
|
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(28.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
|
self.titleNode.maximumNumberOfLines = 0
|
|
self.titleNode.textAlignment = .center
|
|
|
|
self.textNode = ImmediateTextNode()
|
|
self.textNode.displaysAsynchronously = false
|
|
self.textNode.attributedText = text
|
|
self.textNode.maximumNumberOfLines = 0
|
|
self.textNode.lineSpacing = 0.1
|
|
self.textNode.textAlignment = .center
|
|
|
|
self.skipActionTitleNode = ImmediateTextNode()
|
|
self.skipActionTitleNode.isUserInteractionEnabled = false
|
|
self.skipActionTitleNode.displaysAsynchronously = false
|
|
self.skipActionTitleNode.attributedText = NSAttributedString(string: skipActionText, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemAccentColor)
|
|
self.skipActionButtonNode = HighlightTrackingButtonNode()
|
|
self.skipActionTitleNode.isHidden = skipActionText.isEmpty
|
|
self.skipActionButtonNode.isHidden = skipActionText.isEmpty
|
|
|
|
self.changeEmailActionTitleNode = ImmediateTextNode()
|
|
self.changeEmailActionTitleNode.isUserInteractionEnabled = false
|
|
self.changeEmailActionTitleNode.displaysAsynchronously = false
|
|
self.changeEmailActionTitleNode.attributedText = NSAttributedString(string: changeEmailActionText, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemAccentColor)
|
|
self.changeEmailActionButtonNode = HighlightTrackingButtonNode()
|
|
self.changeEmailActionButtonNode.isHidden = changeEmailActionText.isEmpty
|
|
self.changeEmailActionButtonNode.isHidden = changeEmailActionText.isEmpty
|
|
|
|
self.resendCodeActionTitleNode = ImmediateTextNode()
|
|
self.resendCodeActionTitleNode.isUserInteractionEnabled = false
|
|
self.resendCodeActionTitleNode.displaysAsynchronously = false
|
|
self.resendCodeActionTitleNode.attributedText = NSAttributedString(string: resendCodeActionText, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemAccentColor)
|
|
self.resendCodeActionButtonNode = HighlightTrackingButtonNode()
|
|
self.resendCodeActionTitleNode.isHidden = resendCodeActionText.isEmpty
|
|
self.resendCodeActionButtonNode.isHidden = resendCodeActionText.isEmpty
|
|
|
|
self.inputNodes = inputNodes
|
|
|
|
self.buttonNode = SolidRoundedButtonNode(title: buttonText, theme: SolidRoundedButtonTheme(backgroundColor: self.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor), height: 50.0, cornerRadius: 10.0, gloss: false)
|
|
|
|
super.init()
|
|
|
|
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
|
|
|
self.addSubnode(self.scrollNode)
|
|
|
|
self.animatedStickerNode.flatMap(self.scrollNode.addSubnode)
|
|
self.monkeyNode.flatMap(self.scrollNode.addSubnode)
|
|
self.scrollNode.addSubnode(self.titleNode)
|
|
self.scrollNode.addSubnode(self.textNode)
|
|
self.scrollNode.addSubnode(self.skipActionTitleNode)
|
|
self.scrollNode.addSubnode(self.skipActionButtonNode)
|
|
self.scrollNode.addSubnode(self.changeEmailActionTitleNode)
|
|
self.scrollNode.addSubnode(self.changeEmailActionButtonNode)
|
|
self.scrollNode.addSubnode(self.resendCodeActionTitleNode)
|
|
self.scrollNode.addSubnode(self.resendCodeActionButtonNode)
|
|
self.scrollNode.addSubnode(self.buttonNode)
|
|
|
|
for (inputNode) in self.inputNodes {
|
|
self.scrollNode.addSubnode(inputNode)
|
|
}
|
|
|
|
self.navigationBackgroundNode.addSubnode(self.navigationSeparatorNode)
|
|
self.addSubnode(self.navigationBackgroundNode)
|
|
|
|
self.buttonNode.pressed = {
|
|
action()
|
|
}
|
|
|
|
self.skipActionButtonNode.highligthedChanged = { [weak self] highlighted in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if highlighted {
|
|
strongSelf.skipActionTitleNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.skipActionTitleNode.alpha = 0.4
|
|
} else {
|
|
strongSelf.skipActionTitleNode.alpha = 1.0
|
|
strongSelf.skipActionTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
self.skipActionButtonNode.addTarget(self, action: #selector(self.skipActionPressed), forControlEvents: .touchUpInside)
|
|
|
|
self.changeEmailActionButtonNode.highligthedChanged = { [weak self] highlighted in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if highlighted {
|
|
strongSelf.changeEmailActionTitleNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.changeEmailActionTitleNode.alpha = 0.4
|
|
} else {
|
|
strongSelf.changeEmailActionTitleNode.alpha = 1.0
|
|
strongSelf.changeEmailActionTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
self.changeEmailActionButtonNode.addTarget(self, action: #selector(self.changeEmailActionPressed), forControlEvents: .touchUpInside)
|
|
|
|
self.resendCodeActionButtonNode.highligthedChanged = { [weak self] highlighted in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if highlighted {
|
|
strongSelf.resendCodeActionTitleNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.resendCodeActionTitleNode.alpha = 0.4
|
|
} else {
|
|
strongSelf.resendCodeActionTitleNode.alpha = 1.0
|
|
strongSelf.resendCodeActionTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
self.resendCodeActionButtonNode.addTarget(self, action: #selector(self.resendCodeActionPressed), forControlEvents: .touchUpInside)
|
|
|
|
next = { [weak self] node in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if let index = strongSelf.inputNodes.firstIndex(where: { $0 === node }) {
|
|
if index == strongSelf.inputNodes.count - 1 {
|
|
strongSelf.action()
|
|
} else if strongSelf.buttonNode.isUserInteractionEnabled {
|
|
strongSelf.inputNodes[index + 1].focus()
|
|
}
|
|
}
|
|
}
|
|
var textHidden = true
|
|
let updateAnimations: () -> Void = { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
switch strongSelf.mode {
|
|
case .password:
|
|
if strongSelf.inputNodes[1].isFocused {
|
|
let maxWidth = strongSelf.inputNodes[1].bounds.width
|
|
|
|
let textNode = ImmediateTextNode()
|
|
textNode.attributedText = NSAttributedString(string: strongSelf.inputNodes[1].text, font: Font.regular(17.0), textColor: .black)
|
|
let textSize = textNode.updateLayout(CGSize(width: 1000.0, height: 100.0))
|
|
|
|
var trackingOffset = textSize.width / maxWidth
|
|
trackingOffset = max(0.0, min(1.0, trackingOffset))
|
|
strongSelf.monkeyNode?.setState(.tracking(trackingOffset))
|
|
} else if strongSelf.inputNodes[0].isFocused {
|
|
let hasText = !strongSelf.inputNodes[0].text.isEmpty
|
|
if !hasText {
|
|
strongSelf.monkeyNode?.setState(.idle(.still))
|
|
} else if textHidden {
|
|
strongSelf.monkeyNode?.setState(.eyesClosed)
|
|
} else {
|
|
strongSelf.monkeyNode?.setState(.peeking)
|
|
}
|
|
} else {
|
|
strongSelf.monkeyNode?.setState(.idle(.still))
|
|
}
|
|
case .emailAddress:
|
|
if strongSelf.inputNodes[0].isFocused {
|
|
let maxWidth = strongSelf.inputNodes[0].bounds.width
|
|
|
|
let textNode = ImmediateTextNode()
|
|
textNode.attributedText = NSAttributedString(string: strongSelf.inputNodes[0].text, font: Font.regular(17.0), textColor: .black)
|
|
let textSize = textNode.updateLayout(CGSize(width: 1000.0, height: 100.0))
|
|
|
|
var trackingOffset = textSize.width / maxWidth
|
|
trackingOffset = max(0.0, min(1.0, trackingOffset))
|
|
strongSelf.monkeyNode?.setState(.tracking(trackingOffset))
|
|
} else {
|
|
strongSelf.monkeyNode?.setState(.idle(.still))
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
focusUpdated = { [weak self] node, _ in
|
|
DispatchQueue.main.async {
|
|
updateAnimations()
|
|
}
|
|
}
|
|
updated = { [weak self] _ in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
switch strongSelf.mode {
|
|
case .emailAddress, .updateEmailAddress:
|
|
let hasText = strongSelf.inputNodes.contains(where: { !$0.text.isEmpty })
|
|
strongSelf.buttonNode.isHidden = !hasText
|
|
strongSelf.skipActionTitleNode.isHidden = hasText
|
|
strongSelf.skipActionButtonNode.isHidden = hasText
|
|
case let .emailConfirmation(_, _, codeLength):
|
|
let text = strongSelf.inputNodes[0].text
|
|
let hasText = !text.isEmpty
|
|
strongSelf.buttonNode.isHidden = !hasText
|
|
strongSelf.changeEmailActionTitleNode.isHidden = hasText
|
|
strongSelf.changeEmailActionButtonNode.isHidden = hasText
|
|
strongSelf.resendCodeActionTitleNode.isHidden = hasText
|
|
strongSelf.resendCodeActionButtonNode.isHidden = hasText
|
|
|
|
if let codeLength = codeLength, text.count == codeLength {
|
|
action()
|
|
}
|
|
case .passwordHint:
|
|
let hasText = strongSelf.inputNodes.contains(where: { !$0.text.isEmpty })
|
|
strongSelf.buttonNode.isHidden = !hasText
|
|
strongSelf.skipActionTitleNode.isHidden = hasText
|
|
strongSelf.skipActionButtonNode.isHidden = hasText
|
|
case .password, .passwordRecovery:
|
|
break
|
|
}
|
|
updateAnimations()
|
|
}
|
|
toggleTextHidden = { [weak self] _ in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
switch strongSelf.mode {
|
|
case .password:
|
|
textHidden = !textHidden
|
|
for node in strongSelf.inputNodes {
|
|
node.updateTextHidden(textHidden)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
updateAnimations()
|
|
}
|
|
self.inputNodes.first.flatMap { updated?($0) }
|
|
}
|
|
|
|
@objc private func skipActionPressed() {
|
|
self.skipAction()
|
|
}
|
|
|
|
@objc private func changeEmailActionPressed() {
|
|
self.changeEmailAction()
|
|
}
|
|
|
|
@objc private func resendCodeActionPressed() {
|
|
self.resendCodeAction()
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.scrollNode.view.keyboardDismissMode = .none
|
|
self.scrollNode.view.delaysContentTouches = false
|
|
self.scrollNode.view.canCancelContentTouches = true
|
|
//self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true
|
|
self.scrollNode.view.alwaysBounceVertical = false
|
|
self.scrollNode.view.showsVerticalScrollIndicator = false
|
|
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
|
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
|
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
|
}
|
|
self.scrollNode.view.delegate = self
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
}
|
|
|
|
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
self.navigationHeight = navigationHeight
|
|
|
|
let contentAreaSize = layout.size
|
|
let availableAreaSize = CGSize(width: layout.size.width, height: layout.size.height - layout.insets(options: [.input]).bottom)
|
|
|
|
let sideInset: CGFloat = 32.0
|
|
let buttonSideInset: CGFloat = 48.0
|
|
let iconSpacing: CGFloat
|
|
switch self.mode {
|
|
case .passwordHint, .emailConfirmation:
|
|
iconSpacing = 6.0
|
|
default:
|
|
iconSpacing = 2.0
|
|
}
|
|
let titleSpacing: CGFloat = 19.0
|
|
let titleInputSpacing: CGFloat = 26.0
|
|
let textSpacing: CGFloat = 30.0
|
|
let buttonHeight: CGFloat = 50.0
|
|
let buttonSpacing: CGFloat = 20.0
|
|
let rowSpacing: CGFloat = 20.0
|
|
|
|
transition.updateFrame(node: self.navigationBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: contentAreaSize.width, height: navigationHeight)))
|
|
transition.updateFrame(node: self.navigationSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: contentAreaSize.width, height: UIScreenPixel)))
|
|
|
|
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: contentAreaSize))
|
|
|
|
let iconSize: CGSize
|
|
if let _ = self.animatedStickerNode {
|
|
iconSize = CGSize(width: 136.0, height: 136.0)
|
|
} else if let monkeyNode = self.monkeyNode {
|
|
iconSize = monkeyNode.intrinsicSize
|
|
} else {
|
|
iconSize = CGSize(width: 100.0, height: 100.0)
|
|
}
|
|
|
|
let titleSize = self.titleNode.updateLayout(CGSize(width: contentAreaSize.width - sideInset * 2.0, height: contentAreaSize.height))
|
|
let textSize = self.textNode.updateLayout(CGSize(width: contentAreaSize.width - sideInset * 2.0, height: contentAreaSize.height))
|
|
let skipActionSize = self.skipActionTitleNode.updateLayout(CGSize(width: contentAreaSize.width - sideInset * 2.0, height: contentAreaSize.height))
|
|
let changeEmailActionSize = self.changeEmailActionTitleNode.updateLayout(CGSize(width: contentAreaSize.width - sideInset * 2.0, height: contentAreaSize.height))
|
|
let resendCodeActionSize = self.resendCodeActionTitleNode.updateLayout(CGSize(width: contentAreaSize.width - sideInset * 2.0, height: contentAreaSize.height))
|
|
|
|
var calculatedContentHeight = iconSize.height + iconSpacing + titleSize.height
|
|
if textSize.width.isZero {
|
|
calculatedContentHeight += titleInputSpacing
|
|
} else {
|
|
calculatedContentHeight += titleSpacing + textSize.height + textSpacing
|
|
}
|
|
for i in 0 ..< self.inputNodes.count {
|
|
if i != 0 {
|
|
calculatedContentHeight += rowSpacing
|
|
}
|
|
calculatedContentHeight += 50.0
|
|
}
|
|
calculatedContentHeight += buttonHeight + buttonSpacing
|
|
|
|
var contentHeight: CGFloat = 0.0
|
|
|
|
let insets = layout.insets(options: [.input])
|
|
let areaHeight = layout.size.height - insets.top - insets.bottom
|
|
let contentVerticalOrigin = max(layout.statusBarHeight ?? 0.0, floor((areaHeight - calculatedContentHeight) / 2.0))
|
|
|
|
let iconFrame = CGRect(origin: CGPoint(x: floor((contentAreaSize.width - iconSize.width) / 2.0), y: contentVerticalOrigin), size: iconSize)
|
|
if let animatedStickerNode = self.animatedStickerNode {
|
|
animatedStickerNode.updateLayout(size: iconFrame.size)
|
|
transition.updateFrame(node: animatedStickerNode, frame: iconFrame)
|
|
} else if let monkeyNode = self.monkeyNode {
|
|
transition.updateFrame(node: monkeyNode, frame: iconFrame)
|
|
}
|
|
let titleFrame = CGRect(origin: CGPoint(x: floor((contentAreaSize.width - titleSize.width) / 2.0), y: iconFrame.maxY + iconSpacing), size: titleSize)
|
|
transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame)
|
|
let textFrame: CGRect
|
|
if textSize.width.isZero {
|
|
textFrame = CGRect(origin: CGPoint(x: floor((contentAreaSize.width - textSize.width) / 2.0), y: titleFrame.maxY), size: textSize)
|
|
} else {
|
|
textFrame = CGRect(origin: CGPoint(x: floor((contentAreaSize.width - textSize.width) / 2.0), y: titleFrame.maxY + titleSpacing), size: textSize)
|
|
}
|
|
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
|
|
|
|
contentHeight = textFrame.maxY
|
|
if textSize.width.isZero {
|
|
contentHeight += titleInputSpacing
|
|
} else {
|
|
contentHeight += textSpacing
|
|
}
|
|
|
|
let rowWidth = contentAreaSize.width - buttonSideInset * 2.0
|
|
|
|
for i in 0 ..< self.inputNodes.count {
|
|
let inputNode = self.inputNodes[i]
|
|
if i != 0 {
|
|
contentHeight += rowSpacing
|
|
}
|
|
let inputNodeSize = CGSize(width: rowWidth, height: 50.0)
|
|
transition.updateFrame(node: inputNode, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: contentHeight), size: inputNodeSize))
|
|
inputNode.updateLayout(size: inputNodeSize, transition: transition)
|
|
contentHeight += inputNodeSize.height
|
|
}
|
|
|
|
let minimalBottomInset: CGFloat = 74.0
|
|
let buttonBottomInset = layout.intrinsicInsets.bottom + minimalBottomInset
|
|
let bottomInset = layout.intrinsicInsets.bottom + buttonSpacing
|
|
|
|
let buttonWidth = contentAreaSize.width - buttonSideInset * 2.0
|
|
|
|
let maxButtonY = min(areaHeight - buttonSpacing, layout.size.height - buttonBottomInset) - buttonHeight
|
|
|
|
let buttonFrame = CGRect(origin: CGPoint(x: floor((contentAreaSize.width - buttonWidth) / 2.0), y: max(contentHeight + buttonSpacing, maxButtonY)), size: CGSize(width: buttonWidth, height: buttonHeight))
|
|
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
|
|
let _ = self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition)
|
|
|
|
var skipButtonFrame = buttonFrame
|
|
if !self.buttonNode.isHidden {
|
|
skipButtonFrame.origin.y += skipButtonFrame.height
|
|
}
|
|
|
|
transition.updateFrame(node: self.skipActionButtonNode, frame: skipButtonFrame)
|
|
transition.updateFrame(node: self.skipActionTitleNode, frame: CGRect(origin: CGPoint(x: skipButtonFrame.minX + floor((skipButtonFrame.width - skipActionSize.width) / 2.0), y: skipButtonFrame.minY + floor((skipButtonFrame.height - skipActionSize.height) / 2.0)), size: skipActionSize))
|
|
|
|
let changeEmailActionFrame: CGRect
|
|
let changeEmailActionButtonFrame: CGRect
|
|
let resendCodeActionFrame: CGRect
|
|
let resendCodeActionButtonFrame: CGRect
|
|
if changeEmailActionSize.width + resendCodeActionSize.width > layout.size.width - 24.0 {
|
|
changeEmailActionButtonFrame = CGRect(origin: CGPoint(x: buttonFrame.minX, y: buttonFrame.minY), size: CGSize(width: buttonFrame.width, height: buttonFrame.height))
|
|
changeEmailActionFrame = CGRect(origin: CGPoint(x: changeEmailActionButtonFrame.minX + floor((changeEmailActionButtonFrame.width - changeEmailActionSize.width) / 2.0), y: changeEmailActionButtonFrame.minY + floor((changeEmailActionButtonFrame.height - changeEmailActionSize.height) / 2.0)), size: changeEmailActionSize)
|
|
resendCodeActionButtonFrame = CGRect(origin: CGPoint(x: buttonFrame.minX, y: buttonFrame.maxY), size: CGSize(width: buttonFrame.width, height: buttonFrame.height))
|
|
resendCodeActionFrame = CGRect(origin: CGPoint(x: resendCodeActionButtonFrame.minX + floor((resendCodeActionButtonFrame.width - resendCodeActionSize.width) / 2.0), y: resendCodeActionButtonFrame.minY + floor((resendCodeActionButtonFrame.height - resendCodeActionSize.height) / 2.0)), size: resendCodeActionSize)
|
|
} else {
|
|
changeEmailActionButtonFrame = CGRect(origin: CGPoint(x: buttonFrame.minX, y: buttonFrame.minY), size: CGSize(width: floor(buttonFrame.width / 2.0), height: buttonFrame.height))
|
|
changeEmailActionFrame = CGRect(origin: CGPoint(x: changeEmailActionButtonFrame.minX, y: changeEmailActionButtonFrame.minY + floor((changeEmailActionButtonFrame.height - changeEmailActionSize.height) / 2.0)), size: changeEmailActionSize)
|
|
resendCodeActionButtonFrame = CGRect(origin: CGPoint(x: buttonFrame.maxX - floor(buttonFrame.width / 2.0), y: buttonFrame.minY), size: CGSize(width: floor(buttonFrame.width / 2.0), height: buttonFrame.height))
|
|
resendCodeActionFrame = CGRect(origin: CGPoint(x: resendCodeActionButtonFrame.maxX - resendCodeActionSize.width, y: resendCodeActionButtonFrame.minY + floor((resendCodeActionButtonFrame.height - resendCodeActionSize.height) / 2.0)), size: resendCodeActionSize)
|
|
}
|
|
|
|
transition.updateFrame(node: self.changeEmailActionButtonNode, frame: changeEmailActionButtonFrame)
|
|
transition.updateFrame(node: self.resendCodeActionButtonNode, frame: resendCodeActionButtonFrame)
|
|
|
|
transition.updateFrame(node: self.changeEmailActionTitleNode, frame: changeEmailActionFrame)
|
|
transition.updateFrame(node: self.resendCodeActionTitleNode, frame: resendCodeActionFrame)
|
|
|
|
transition.animateView {
|
|
self.scrollNode.view.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: layout.insets(options: [.input]).bottom, right: 0.0)
|
|
self.scrollNode.view.contentSize = CGSize(width: contentAreaSize.width, height: max(availableAreaSize.height, skipButtonFrame.maxY + bottomInset))
|
|
}
|
|
}
|
|
}
|
|
|