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 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 case passwordRecoveryEmail(emailPattern: String, mode: PasswordRecoveryEmailMode) 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 engine: SomeTelegramEngine private var presentationData: PresentationData private let mode: TwoFactorDataInputMode private let stateUpdated: (SetupTwoStepVerificationStateUpdate) -> Void private let actionDisposable = MetaDisposable() public var passwordRecoveryFailed: (() -> Void)? 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() } @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, engine: strongSelf.engine, mode: .passwordHint(recovery: nil, password: values[0]), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation)) navigationController.setViewControllers(controllers, animated: true) case let .passwordRecoveryEmail(_, mode): 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(TwoFactorDataInputMode.Recovery(code: text, mode: mappedMode)), 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): 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]), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation), 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 _ = (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)), 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)) 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)) 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)), 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)) 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 .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)) 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)) navigationController.setViewControllers(controllers, animated: true) }) case .unauthorized: break } 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, engine: strongSelf.engine, mode: .emailAddress(password: password, hint: value), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation)) } } }, 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 _ = (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)) 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, engine: strongSelf.engine, mode: .emailAddress(password: password, hint: ""), 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)) 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, engine: strongSelf.engine, mode: .emailAddress(password: password, hint: hint), 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): break } }) })]), 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)), 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 self, 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(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, .passwordRecoveryEmail: 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 .passwordRecoveryEmail(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.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 (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.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, .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.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 .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 } updateAnimations() } toggleTextHidden = { [weak self] _ in guard let strongSelf = self else { return } switch strongSelf.mode { case .password, .passwordRecovery: 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)) } } }