mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
2023 lines
113 KiB
Swift
2023 lines
113 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
|
|
import TelegramAnimatedStickerNode
|
|
import ActivityIndicator
|
|
|
|
public enum TwoFactorDataInputMode {
|
|
public struct Recovery {
|
|
public enum Mode {
|
|
case notAuthorized(syncContacts: Bool)
|
|
case authorized
|
|
}
|
|
|
|
public var code: String
|
|
public var mode: Mode
|
|
|
|
public init(code: String, mode: Mode) {
|
|
self.code = code
|
|
self.mode = mode
|
|
}
|
|
}
|
|
|
|
public enum PasswordRecoveryEmailMode {
|
|
case notAuthorized(syncContacts: Bool)
|
|
case authorized
|
|
}
|
|
|
|
case password(phoneNumber: String?, doneText: String)
|
|
case passwordRecoveryEmail(emailPattern: String, mode: PasswordRecoveryEmailMode, doneText: String)
|
|
case passwordRecovery(recovery: Recovery, doneText: String)
|
|
case emailAddress(password: String, hint: String, doneText: String)
|
|
case updateEmailAddress(password: String, doneText: String)
|
|
case emailConfirmation(passwordAndHint: (String, String)?, emailPattern: String, codeLength: Int?, doneText: String)
|
|
case passwordHint(recovery: Recovery?, password: String, doneText: String)
|
|
case rememberPassword(doneText: String)
|
|
}
|
|
|
|
public final class TwoFactorDataInputScreen: ViewController {
|
|
private let sharedContext: SharedAccountContext
|
|
private let engine: SomeTelegramEngine
|
|
private var presentationData: PresentationData
|
|
private let mode: TwoFactorDataInputMode
|
|
private let stateUpdated: (SetupTwoStepVerificationStateUpdate) -> Void
|
|
private let actionDisposable = MetaDisposable()
|
|
|
|
private let forgotDataDisposable = MetaDisposable()
|
|
private let forgotDataPromise = Promise<TwoStepVerificationConfiguration?>(nil)
|
|
private var didSetupForgotData = false
|
|
|
|
public var passwordRecoveryFailed: (() -> Void)?
|
|
|
|
public var passwordRemembered: (() -> Void)?
|
|
public var twoStepAuthSettingsController: ((TwoStepVerificationConfiguration) -> ViewController)?
|
|
|
|
public init(sharedContext: SharedAccountContext, engine: SomeTelegramEngine, mode: TwoFactorDataInputMode, stateUpdated: @escaping (SetupTwoStepVerificationStateUpdate) -> Void, presentation: ViewControllerNavigationPresentation = .modalInLargeLayout) {
|
|
self.sharedContext = sharedContext
|
|
self.engine = engine
|
|
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 = presentation
|
|
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")
|
|
}
|
|
|
|
deinit {
|
|
self.actionDisposable.dispose()
|
|
self.forgotDataDisposable.dispose()
|
|
}
|
|
|
|
@objc private func backPressed() {
|
|
self.dismiss()
|
|
}
|
|
|
|
public override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
|
|
switch self.mode {
|
|
case .rememberPassword, .password, .passwordHint, .emailAddress:
|
|
(self.displayNode as? TwoFactorDataInputScreenNode)?.focus()
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
override public func loadDisplayNode() {
|
|
self.displayNode = TwoFactorDataInputScreenNode(sharedContext: self.sharedContext, presentationData: self.presentationData, mode: self.mode, action: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
switch strongSelf.mode {
|
|
case let .password(_, doneText):
|
|
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, engine: strongSelf.engine, mode: .passwordHint(recovery: nil, password: values[0], doneText: doneText), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation))
|
|
navigationController.setViewControllers(controllers, animated: true)
|
|
case let .passwordRecoveryEmail(_, mode, doneText):
|
|
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))
|
|
|
|
strongSelf.actionDisposable.set((strongSelf.engine.auth.checkPasswordRecoveryCode(code: text)
|
|
|> deliverOnMainQueue).start(error: { [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(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
|
}, completed: { [weak statusController] in
|
|
statusController?.dismiss()
|
|
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
let mappedMode: TwoFactorDataInputMode.Recovery.Mode
|
|
switch mode {
|
|
case .authorized:
|
|
mappedMode = .authorized
|
|
case let .notAuthorized(syncContacts):
|
|
mappedMode = .notAuthorized(syncContacts: syncContacts)
|
|
}
|
|
|
|
let setupController = TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .passwordRecovery(recovery: TwoFactorDataInputMode.Recovery(code: text, mode: mappedMode), doneText: doneText), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation)
|
|
|
|
guard let navigationController = strongSelf.navigationController as? NavigationController else {
|
|
return
|
|
}
|
|
navigationController.replaceController(strongSelf, with: setupController, animated: true)
|
|
}))
|
|
case let .passwordRecovery(recovery, doneText):
|
|
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
|
|
}
|
|
navigationController.replaceController(strongSelf, with: TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .passwordHint(recovery: recovery, password: values[0], doneText: doneText), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation), animated: true)
|
|
case let .emailAddress(password, hint, doneText):
|
|
guard let text = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !text.isEmpty else {
|
|
(strongSelf.displayNode as? TwoFactorDataInputScreenNode)?.onAction(success: false)
|
|
return
|
|
}
|
|
let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil))
|
|
strongSelf.present(statusController, in: .window(.root))
|
|
|
|
let _ = (strongSelf.engine.auth.updateTwoStepVerificationPassword(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, engine: strongSelf.engine, mode: .emailConfirmation(passwordAndHint: (password, hint), emailPattern: text, codeLength: pendingEmail.codeLength.flatMap(Int.init), doneText: doneText), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation))
|
|
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, engine: strongSelf.engine, mode: .done(doneText: doneText)))
|
|
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, doneText):
|
|
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))
|
|
|
|
switch strongSelf.engine {
|
|
case let .authorized(engine):
|
|
let _ = (engine.auth.updateTwoStepVerificationEmail(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, engine: strongSelf.engine, mode: .emailConfirmation(passwordAndHint: (password, ""), emailPattern: text, codeLength: pendingEmail.codeLength.flatMap(Int.init), doneText: doneText), 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, engine: strongSelf.engine, mode: .done(doneText: doneText)))
|
|
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 .unauthorized:
|
|
break
|
|
}
|
|
case let .emailConfirmation(_, _, _, doneText):
|
|
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))
|
|
|
|
switch strongSelf.engine {
|
|
case let .authorized(engine):
|
|
let _ = (engine.auth.confirmTwoStepRecoveryEmail(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, engine: strongSelf.engine, mode: .done(doneText: doneText)))
|
|
navigationController.setViewControllers(controllers, animated: true)
|
|
})
|
|
case .unauthorized:
|
|
break
|
|
}
|
|
case let .passwordHint(recovery, password, doneText):
|
|
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, engine: strongSelf.engine, mode: .emailAddress(password: password, hint: value, doneText: doneText), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation))
|
|
}
|
|
case .rememberPassword:
|
|
guard case let .authorized(engine) = strongSelf.engine else {
|
|
return
|
|
}
|
|
|
|
guard let value = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !value.isEmpty else {
|
|
return
|
|
}
|
|
|
|
(strongSelf.displayNode as? TwoFactorDataInputScreenNode)?.isLoading = true
|
|
|
|
let _ = (engine.auth.requestTwoStepVerifiationSettings(password: value)
|
|
|> deliverOnMainQueue).start(error: { [weak self] error in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
(strongSelf.displayNode as? TwoFactorDataInputScreenNode)?.isLoading = false
|
|
|
|
let presentationData = strongSelf.presentationData
|
|
let text: String?
|
|
switch error {
|
|
case .limitExceeded:
|
|
text = presentationData.strings.LoginPassword_FloodError
|
|
case .invalidPassword:
|
|
text = nil
|
|
case .generic:
|
|
text = presentationData.strings.Login_UnknownError
|
|
}
|
|
if let text = text {
|
|
strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
|
} else {
|
|
(strongSelf.displayNode as? TwoFactorDataInputScreenNode)?.onAction(success: false)
|
|
}
|
|
}, completed: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
(strongSelf.displayNode as? TwoFactorDataInputScreenNode)?.isLoading = false
|
|
strongSelf.passwordRemembered?()
|
|
|
|
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, engine: strongSelf.engine, mode: .remember))
|
|
navigationController.setViewControllers(controllers, animated: true)
|
|
})
|
|
}
|
|
}, skipAction: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
switch strongSelf.mode {
|
|
case let .emailAddress(password, hint, doneText):
|
|
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 _ = (strongSelf.engine.auth.updateTwoStepVerificationPassword(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, engine: strongSelf.engine, mode: .done(doneText: doneText)))
|
|
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, doneText):
|
|
if let recovery = recovery {
|
|
strongSelf.performRecovery(recovery: recovery, password: password, hint: "")
|
|
} else {
|
|
strongSelf.push(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .emailAddress(password: password, hint: "", doneText: doneText), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation))
|
|
}
|
|
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))
|
|
case let .rememberPassword(doneText):
|
|
guard case let .authorized(engine) = strongSelf.engine else {
|
|
return
|
|
}
|
|
|
|
strongSelf.view.endEditing(true)
|
|
|
|
let sharedContext = strongSelf.sharedContext
|
|
let presentationData = sharedContext.currentPresentationData.with { $0 }
|
|
let navigationController = strongSelf.navigationController as? NavigationController
|
|
|
|
if !strongSelf.didSetupForgotData {
|
|
strongSelf.didSetupForgotData = true
|
|
strongSelf.forgotDataPromise.set(engine.auth.twoStepVerificationConfiguration() |> map(Optional.init))
|
|
}
|
|
let disposable = strongSelf.forgotDataDisposable
|
|
|
|
disposable.set((strongSelf.forgotDataPromise.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { [weak self] configuration in
|
|
if let strongSelf = self, let configuration = configuration {
|
|
switch configuration {
|
|
case let .set(_, hasRecoveryEmail, _, _, pendingResetTimestamp):
|
|
if hasRecoveryEmail {
|
|
disposable.set((engine.auth.requestTwoStepVerificationPasswordRecoveryCode()
|
|
|> deliverOnMainQueue).start(next: { emailPattern in
|
|
var stateUpdated: ((SetupTwoStepVerificationStateUpdate) -> Void)?
|
|
let controller = TwoFactorDataInputScreen(sharedContext: sharedContext, engine: .authorized(engine), mode: .passwordRecoveryEmail(emailPattern: emailPattern, mode: .authorized, doneText: doneText), stateUpdated: { state in
|
|
stateUpdated?(state)
|
|
})
|
|
stateUpdated = { [weak controller] state in
|
|
controller?.view.endEditing(true)
|
|
controller?.dismiss()
|
|
|
|
switch state {
|
|
case .noPassword, .awaitingEmailConfirmation, .passwordSet:
|
|
controller?.dismiss()
|
|
|
|
navigationController?.filterController(strongSelf, animated: true)
|
|
case .pendingPasswordReset:
|
|
let _ = (engine.auth.twoStepVerificationConfiguration()
|
|
|> deliverOnMainQueue).start(next: { [weak self] configuration in
|
|
if let strongSelf = self {
|
|
if let navigationController = navigationController, let twoStepAuthSettingsController = strongSelf.twoStepAuthSettingsController?(configuration) {
|
|
var controllers = navigationController.viewControllers.filter { controller in
|
|
if controller is TwoFactorDataInputScreen {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
controllers.append(twoStepAuthSettingsController)
|
|
navigationController.setViewControllers(controllers, animated: true)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
strongSelf.push(controller)
|
|
}, error: { _ in
|
|
strongSelf.present(textAlertController(sharedContext: sharedContext, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
|
}))
|
|
} else {
|
|
if let pendingResetTimestamp = pendingResetTimestamp {
|
|
let remainingSeconds = pendingResetTimestamp - Int32(Date().timeIntervalSince1970)
|
|
if remainingSeconds <= 0 {
|
|
let _ = (engine.auth.requestTwoStepPasswordReset()
|
|
|> deliverOnMainQueue).start(next: { result in
|
|
switch result {
|
|
case .done, .waitingForReset:
|
|
let _ = (engine.auth.twoStepVerificationConfiguration()
|
|
|> deliverOnMainQueue).start(next: { [weak self] configuration in
|
|
if let strongSelf = self {
|
|
if let navigationController = navigationController, let twoStepAuthSettingsController = strongSelf.twoStepAuthSettingsController?(configuration) {
|
|
var controllers = navigationController.viewControllers.filter { controller in
|
|
if controller is TwoFactorDataInputScreen {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
controllers.append(twoStepAuthSettingsController)
|
|
navigationController.setViewControllers(controllers, animated: true)
|
|
}
|
|
}
|
|
})
|
|
case .declined:
|
|
break
|
|
case let .error(reason):
|
|
let text: String
|
|
switch reason {
|
|
case let .limitExceeded(retryAtTimestamp):
|
|
if let retryAtTimestamp = retryAtTimestamp {
|
|
let remainingSeconds = retryAtTimestamp - Int32(Date().timeIntervalSince1970)
|
|
text = presentationData.strings.TwoFactorSetup_ResetFloodWait(timeIntervalString(strings: presentationData.strings, value: remainingSeconds)).string
|
|
} else {
|
|
text = presentationData.strings.TwoStepAuth_FloodError
|
|
}
|
|
case .generic:
|
|
text = presentationData.strings.Login_UnknownError
|
|
}
|
|
strongSelf.present(textAlertController(sharedContext: sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
|
}
|
|
})
|
|
}
|
|
} else {
|
|
strongSelf.present(textAlertController(sharedContext: sharedContext, title: presentationData.strings.TwoStepAuth_RecoveryUnavailableResetTitle, text: presentationData.strings.TwoStepAuth_RecoveryUnavailableResetText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.TwoStepAuth_RecoveryUnavailableResetAction, action: {
|
|
let _ = (engine.auth.requestTwoStepPasswordReset()
|
|
|> deliverOnMainQueue).start(next: { result in
|
|
switch result {
|
|
case .done, .waitingForReset:
|
|
let _ = (engine.auth.twoStepVerificationConfiguration()
|
|
|> deliverOnMainQueue).start(next: { [weak self] configuration in
|
|
if let strongSelf = self {
|
|
if let navigationController = navigationController, let twoStepAuthSettingsController = strongSelf.twoStepAuthSettingsController?(configuration) {
|
|
var controllers = navigationController.viewControllers.filter { controller in
|
|
if controller is TwoFactorDataInputScreen {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
controllers.append(twoStepAuthSettingsController)
|
|
navigationController.setViewControllers(controllers, animated: true)
|
|
}
|
|
}
|
|
})
|
|
case .declined:
|
|
break
|
|
case let .error(reason):
|
|
let text: String
|
|
switch reason {
|
|
case let .limitExceeded(retryAtTimestamp):
|
|
if let retryAtTimestamp = retryAtTimestamp {
|
|
let remainingSeconds = retryAtTimestamp - Int32(Date().timeIntervalSince1970)
|
|
text = presentationData.strings.TwoFactorSetup_ResetFloodWait(timeIntervalString(strings: presentationData.strings, value: remainingSeconds)).string
|
|
} else {
|
|
text = presentationData.strings.TwoStepAuth_FloodError
|
|
}
|
|
case .generic:
|
|
text = presentationData.strings.Login_UnknownError
|
|
}
|
|
strongSelf.present(textAlertController(sharedContext: sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
|
}
|
|
})
|
|
})]), in: .window(.root))
|
|
}
|
|
}
|
|
case .notSet:
|
|
break
|
|
}
|
|
}
|
|
}))
|
|
default:
|
|
break
|
|
}
|
|
}, changeEmailAction: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
switch strongSelf.mode {
|
|
case let .emailConfirmation(passwordAndHint, _, _, doneText):
|
|
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, engine: strongSelf.engine, mode: .emailAddress(password: password, hint: hint, doneText: doneText), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation))
|
|
navigationController.setViewControllers(controllers, animated: true)
|
|
} else {
|
|
}
|
|
case .passwordRecoveryEmail:
|
|
switch strongSelf.engine {
|
|
case let .authorized(engine):
|
|
strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: strongSelf.presentationData.strings.TwoStepAuth_RecoveryUnavailableResetTitle, text: strongSelf.presentationData.strings.TwoStepAuth_RecoveryEmailResetText, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.TwoStepAuth_RecoveryUnavailableResetAction, action: {
|
|
let _ = (engine.auth.requestTwoStepPasswordReset()
|
|
|> deliverOnMainQueue).start(next: { result in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
switch result {
|
|
case .done, .waitingForReset:
|
|
strongSelf.stateUpdated(.pendingPasswordReset)
|
|
case .declined:
|
|
break
|
|
case let .error(reason):
|
|
let text: String
|
|
switch reason {
|
|
case let .limitExceeded(retryAtTimestamp):
|
|
if let retryAtTimestamp = retryAtTimestamp {
|
|
let remainingSeconds = retryAtTimestamp - Int32(Date().timeIntervalSince1970)
|
|
text = strongSelf.presentationData.strings.TwoFactorSetup_ResetFloodWait(timeIntervalString(strings: strongSelf.presentationData.strings, value: remainingSeconds)).string
|
|
} else {
|
|
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))
|
|
}
|
|
})
|
|
})]), in: .window(.root))
|
|
case .unauthorized:
|
|
strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_RecoveryFailed, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.passwordRecoveryFailed?()
|
|
})]), in: .window(.root))
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}, resendCodeAction: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
switch strongSelf.mode {
|
|
case .passwordRecoveryEmail:
|
|
let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil))
|
|
strongSelf.present(statusController, in: .window(.root))
|
|
|
|
let _ = (strongSelf.engine.auth.requestTwoStepVerificationPasswordRecoveryCode()
|
|
|> deliverOnMainQueue).start(error: { [weak statusController] error in
|
|
statusController?.dismiss()
|
|
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
let text: String
|
|
switch error {
|
|
case .limitExceeded:
|
|
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()
|
|
})
|
|
default:
|
|
guard case let .authorized(engine) = strongSelf.engine else {
|
|
return
|
|
}
|
|
let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil))
|
|
strongSelf.present(statusController, in: .window(.root))
|
|
|
|
let _ = (engine.auth.resendTwoStepRecoveryEmail()
|
|
|> 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))
|
|
|
|
switch self.engine {
|
|
case let .unauthorized(engine):
|
|
var syncContacts = false
|
|
switch recovery.mode {
|
|
case let .notAuthorized(syncContactsValue):
|
|
syncContacts = syncContactsValue
|
|
case .authorized:
|
|
break
|
|
}
|
|
let _ = (engine.auth.performPasswordRecovery(code: recovery.code, updatedPassword: password.isEmpty ? .none : .password(password: password, hint: hint, email: nil))
|
|
|> deliverOnMainQueue).start(next: { [weak self, weak statusController] recoveredAccountData in
|
|
statusController?.dismiss()
|
|
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
if password.isEmpty {
|
|
strongSelf.stateUpdated(.noPassword)
|
|
} else {
|
|
strongSelf.stateUpdated(.passwordSet(password: password, hasRecoveryEmail: true, hasSecureValues: false))
|
|
}
|
|
|
|
(strongSelf.navigationController as? NavigationController)?.replaceController(strongSelf, with: TwoFactorAuthSplashScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .recoveryDone(recoveredAccountData: recoveredAccountData, syncContacts: syncContacts, isPasswordSet: !password.isEmpty)), animated: true)
|
|
}, 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 statusController] in
|
|
statusController?.dismiss()
|
|
})
|
|
case let .authorized(engine):
|
|
let _ = (engine.auth.performPasswordRecovery(code: recovery.code, 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
|
|
}
|
|
|
|
if password.isEmpty {
|
|
strongSelf.stateUpdated(.noPassword)
|
|
} else {
|
|
strongSelf.stateUpdated(.passwordSet(password: password, hasRecoveryEmail: true, hasSecureValues: false))
|
|
}
|
|
strongSelf.dismiss()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum TwoFactorDataInputTextNodeType {
|
|
case password(phoneNumber: String?, 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 var shadowInputNode: TextFieldNode?
|
|
private let inputNode: TextFieldNode
|
|
private let hideButtonNode: HighlightableButtonNode
|
|
private let clearButtonNode: HighlightableButtonNode
|
|
|
|
private var activityIndicator: ActivityIndicator?
|
|
|
|
fileprivate var ignoreTextChanged: Bool = false
|
|
|
|
var isFocused: Bool {
|
|
get {
|
|
return self.inputNode.textField.isFirstResponder
|
|
}
|
|
set {
|
|
let _ = self.inputNode.textField.becomeFirstResponder()
|
|
}
|
|
}
|
|
|
|
var text: String {
|
|
get {
|
|
return self.inputNode.textField.text ?? ""
|
|
} set(value) {
|
|
self.inputNode.textField.text = value
|
|
self.textFieldChanged(self.inputNode.textField)
|
|
}
|
|
}
|
|
|
|
var isFailed: Bool = false {
|
|
didSet {
|
|
if self.isFailed != oldValue {
|
|
UIView.transition(with: self.view, duration: 0.2, options: [.transitionCrossDissolve, .curveEaseInOut]) {
|
|
self.inputNode.textField.textColor = self.isFailed ? self.theme.list.itemDestructiveColor : self.theme.list.freePlainInputField.primaryColor
|
|
self.hideButtonNode.setImage(generateTextHiddenImage(color: self.isFailed ? self.theme.list.itemDestructiveColor : self.theme.list.freePlainInputField.controlColor, on: !self.inputNode.textField.isSecureTextEntry), for: [])
|
|
self.backgroundNode.image = self.isFailed ? generateStretchableFilledCircleImage(diameter: 20.0, color: self.theme.list.itemDestructiveColor.withAlphaComponent(0.1)) : generateStretchableFilledCircleImage(diameter: 20.0, color: self.theme.list.freePlainInputField.backgroundColor)
|
|
} completion: { _ in
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var isLoading: Bool = false {
|
|
didSet {
|
|
if self.isLoading != oldValue {
|
|
if self.isLoading {
|
|
if self.activityIndicator == nil {
|
|
let activityIndicator = ActivityIndicator(type: .custom(self.theme.actionSheet.inputClearButtonColor, 24.0, 1.0, false))
|
|
self.activityIndicator = activityIndicator
|
|
self.addSubnode(activityIndicator)
|
|
if let size = self.validLayout {
|
|
self.updateLayout(size: size, transition: .immediate)
|
|
}
|
|
}
|
|
} else if let activityIndicator = self.activityIndicator {
|
|
self.activityIndicator = nil
|
|
activityIndicator.removeFromSupernode()
|
|
}
|
|
self.hideButtonNode.isHidden = self.isLoading
|
|
}
|
|
}
|
|
}
|
|
|
|
private var validLayout: CGSize?
|
|
|
|
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(phoneNumber, confirmation):
|
|
self.inputNode.textField.keyboardType = .default
|
|
self.inputNode.textField.isSecureTextEntry = true
|
|
if confirmation {
|
|
self.inputNode.textField.returnKeyType = .done
|
|
} else {
|
|
self.inputNode.textField.returnKeyType = .next
|
|
}
|
|
if #available(iOS 12.0, *) {
|
|
#if DEBUG
|
|
if !confirmation, let phoneNumber {
|
|
let shadowInputNode = TextFieldNode()
|
|
shadowInputNode.textField.font = Font.regular(17.0)
|
|
shadowInputNode.textField.textColor = theme.list.freePlainInputField.primaryColor
|
|
shadowInputNode.textField.attributedPlaceholder = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.list.freePlainInputField.placeholderColor)
|
|
self.shadowInputNode = shadowInputNode
|
|
|
|
|
|
shadowInputNode.textField.textContentType = .username
|
|
shadowInputNode.textField.text = phoneNumber
|
|
}
|
|
#endif
|
|
|
|
#if DEBUG
|
|
self.inputNode.textField.textContentType = .newPassword
|
|
self.inputNode.textField.passwordRules = UITextInputPasswordRules(descriptor: "minlength: 8;")
|
|
#endif
|
|
}
|
|
self.hideButtonNode.isHidden = confirmation
|
|
case .email:
|
|
self.inputNode.textField.keyboardType = .emailAddress
|
|
self.inputNode.textField.returnKeyType = .done
|
|
self.hideButtonNode.isHidden = true
|
|
|
|
if #available(iOS 12.0, *) {
|
|
self.inputNode.textField.textContentType = .emailAddress
|
|
}
|
|
|
|
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
|
|
}
|
|
case .code:
|
|
self.inputNode.textField.keyboardType = .numberPad
|
|
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
|
|
}
|
|
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)
|
|
if let shadowInputNode = self.shadowInputNode {
|
|
shadowInputNode.alpha = 0.001
|
|
self.addSubnode(shadowInputNode)
|
|
}
|
|
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 {
|
|
if self.isLoading {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
@objc private func textFieldChanged(_ textField: UITextField) {
|
|
if !self.ignoreTextChanged {
|
|
switch self.mode {
|
|
case .password:
|
|
if self.isFailed {
|
|
self.isFailed = false
|
|
}
|
|
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) {
|
|
self.validLayout = size
|
|
|
|
let leftInset: CGFloat = 16.0
|
|
let rightInset: CGFloat = 38.0
|
|
|
|
if let shadowInputNode = self.shadowInputNode {
|
|
transition.updateFrame(node: shadowInputNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: size.width - leftInset - rightInset, height: size.height)))
|
|
}
|
|
|
|
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)))
|
|
|
|
if let activityIndicator = self.activityIndicator {
|
|
let indicatorSize = activityIndicator.measure(CGSize(width: 24.0, height: 24.0))
|
|
transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: size.width - rightInset + 6.0, y: floorToScreenPixels((size.height - indicatorSize.height) / 2.0) + 1.0), size: indicatorSize))
|
|
}
|
|
}
|
|
|
|
func focus() {
|
|
self.inputNode.textField.becomeFirstResponder()
|
|
}
|
|
|
|
func updateTextHidden(_ value: Bool) {
|
|
self.hideButtonNode.setImage(generateTextHiddenImage(color: self.isFailed ? self.theme.list.itemDestructiveColor : self.theme.list.freePlainInputField.controlColor, 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, ASScrollViewDelegate {
|
|
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 validLayout: (ContainerViewLayout, CGFloat)?
|
|
|
|
var inputText: [String] {
|
|
return self.inputNodes.map { $0.text }
|
|
}
|
|
|
|
var isLoading: Bool = false {
|
|
didSet {
|
|
self.inputNodes.first?.isLoading = self.isLoading
|
|
}
|
|
}
|
|
|
|
private var skipIsHiddenUntilError = false
|
|
|
|
init(sharedContext: SharedAccountContext, 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, .passwordRecoveryEmail:
|
|
let animatedStickerNode = DefaultAnimatedStickerNodeImpl()
|
|
animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "TwoFactorSetupMail"), width: 272, height: 272, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
|
animatedStickerNode.visibility = true
|
|
self.animatedStickerNode = animatedStickerNode
|
|
case .passwordHint:
|
|
let animatedStickerNode = DefaultAnimatedStickerNodeImpl()
|
|
animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "TwoFactorSetupHint"), width: 272, height: 272, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
|
animatedStickerNode.visibility = true
|
|
self.animatedStickerNode = animatedStickerNode
|
|
case .rememberPassword:
|
|
let animatedStickerNode = DefaultAnimatedStickerNodeImpl()
|
|
animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "TwoFactorSetupRemember"), width: 272, height: 272, playbackMode: .count(3), 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 let .password(phoneNumber, _):
|
|
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(phoneNumber: phoneNumber, 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(phoneNumber: phoneNumber, 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(phoneNumber: nil, 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(phoneNumber: nil, 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
|
|
self.skipIsHiddenUntilError = true
|
|
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 .passwordRecoveryEmail(emailPattern, _, _):
|
|
title = presentationData.strings.TwoFactorSetup_EmailVerification_Title
|
|
let formattedString = presentationData.strings.TwoFactorSetup_EmailVerification_Text(emailPattern)
|
|
|
|
let string = NSMutableAttributedString()
|
|
string.append(NSAttributedString(string: formattedString.string, font: Font.regular(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor))
|
|
for range in formattedString.ranges {
|
|
string.addAttribute(.font, value: Font.semibold(16.0), range: range.range)
|
|
}
|
|
|
|
text = string
|
|
|
|
buttonText = presentationData.strings.TwoFactorSetup_EmailVerification_Action
|
|
skipActionText = ""
|
|
changeEmailActionText = presentationData.strings.TwoStepAuth_RecoveryEmailResetNoAccess
|
|
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 let .emailConfirmation(_, emailPattern, _, _):
|
|
title = presentationData.strings.TwoFactorSetup_EmailVerification_Title
|
|
let formattedString = presentationData.strings.TwoFactorSetup_EmailVerification_Text(emailPattern)
|
|
|
|
let string = NSMutableAttributedString()
|
|
string.append(NSAttributedString(string: formattedString.string, font: Font.regular(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor))
|
|
for range in formattedString.ranges {
|
|
string.addAttribute(.font, value: Font.semibold(16.0), range: 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)
|
|
}),
|
|
]
|
|
case .rememberPassword:
|
|
title = presentationData.strings.TwoFactorRemember_Title
|
|
text = NSAttributedString(string: presentationData.strings.TwoFactorRemember_Text, font: Font.regular(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
|
|
buttonText = presentationData.strings.TwoFactorRemember_CheckPassword
|
|
skipActionText = presentationData.strings.TwoFactorRemember_Forgot
|
|
changeEmailActionText = ""
|
|
resendCodeActionText = ""
|
|
inputNodes = [
|
|
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .password(phoneNumber: nil, confirmation: false), placeholder: presentationData.strings.TwoFactorRemember_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.skipIsHiddenUntilError
|
|
self.skipActionButtonNode.isHidden = skipActionText.isEmpty || self.skipIsHiddenUntilError
|
|
|
|
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.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: 11.0, gloss: false)
|
|
|
|
super.init()
|
|
|
|
if case .rememberPassword = mode {
|
|
self.buttonNode.alpha = 0.0
|
|
}
|
|
|
|
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, .passwordRecovery:
|
|
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 = { node, _ in
|
|
DispatchQueue.main.async {
|
|
updateAnimations()
|
|
}
|
|
}
|
|
updated = { [weak self] _ in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
switch strongSelf.mode {
|
|
case .emailAddress, .updateEmailAddress, .passwordRecovery:
|
|
let hasText = strongSelf.inputNodes.contains(where: { !$0.text.isEmpty })
|
|
strongSelf.buttonNode.isHidden = !hasText
|
|
strongSelf.skipActionTitleNode.isHidden = hasText || strongSelf.skipIsHiddenUntilError
|
|
strongSelf.skipActionButtonNode.isHidden = hasText || strongSelf.skipIsHiddenUntilError
|
|
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 .passwordRecoveryEmail:
|
|
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 text.count == 6 {
|
|
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:
|
|
break
|
|
case .rememberPassword:
|
|
let hasText = strongSelf.inputNodes.contains(where: { !$0.text.isEmpty })
|
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
|
transition.updateAlpha(node: strongSelf.buttonNode, alpha: hasText ? 1.0 : 0.0)
|
|
transition.updateAlpha(node: strongSelf.skipActionTitleNode, alpha: hasText ? 0.0 : 1.0)
|
|
strongSelf.skipActionButtonNode.isHidden = hasText
|
|
|
|
if strongSelf.textNode.attributedText?.string != strongSelf.presentationData.strings.TwoFactorRemember_Text {
|
|
if let snapshotView = strongSelf.textNode.view.snapshotContentTree() {
|
|
snapshotView.frame = strongSelf.textNode.view.frame
|
|
strongSelf.textNode.view.superview?.addSubview(snapshotView)
|
|
|
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
|
snapshotView?.removeFromSuperview()
|
|
})
|
|
|
|
strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
|
|
strongSelf.textNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.TwoFactorRemember_Text, font: Font.regular(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
|
|
if let (layout, navigationHeight) = strongSelf.validLayout {
|
|
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
updateAnimations()
|
|
}
|
|
toggleTextHidden = { [weak self] _ in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
switch strongSelf.mode {
|
|
case .password, .passwordRecovery, .rememberPassword:
|
|
textHidden = !textHidden
|
|
for node in strongSelf.inputNodes {
|
|
node.updateTextHidden(textHidden)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
updateAnimations()
|
|
}
|
|
self.inputNodes.first.flatMap { updated?($0) }
|
|
}
|
|
|
|
func focus() {
|
|
self.inputNodes.first?.isFocused = true
|
|
}
|
|
|
|
@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.wrappedScrollViewDelegate
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
}
|
|
|
|
func onAction(success: Bool) {
|
|
switch self.mode {
|
|
case .emailAddress:
|
|
if !success {
|
|
self.inputNodes.first?.layer.addShakeAnimation()
|
|
HapticFeedback().error()
|
|
|
|
if self.skipIsHiddenUntilError {
|
|
self.skipIsHiddenUntilError = false
|
|
self.skipActionTitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
self.skipActionTitleNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
|
self.skipActionTitleNode.isHidden = false
|
|
self.skipActionButtonNode.isHidden = false
|
|
}
|
|
}
|
|
case .rememberPassword:
|
|
if !success {
|
|
self.skipActionTitleNode.isHidden = false
|
|
self.skipActionButtonNode.isHidden = false
|
|
|
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
|
transition.updateAlpha(node: self.buttonNode, alpha: 0.0)
|
|
transition.updateAlpha(node: self.skipActionTitleNode, alpha: 1.0)
|
|
|
|
if let snapshotView = self.textNode.view.snapshotContentTree() {
|
|
snapshotView.frame = self.textNode.view.frame
|
|
self.textNode.view.superview?.addSubview(snapshotView)
|
|
|
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
|
snapshotView?.removeFromSuperview()
|
|
})
|
|
|
|
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
|
|
self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.TwoFactorRemember_WrongPassword, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemDestructiveColor)
|
|
self.inputNodes.first?.isFailed = true
|
|
|
|
if let (layout, navigationHeight) = self.validLayout {
|
|
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate)
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
self.validLayout = (layout, 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 * 2.0
|
|
|
|
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 case .rememberPassword = self.mode {
|
|
} else 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 - buttonFrame.minX * 2.0 - 32.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))
|
|
}
|
|
}
|
|
}
|
|
|