Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,94 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import ProgressNavigationButtonNode
final class AuthorizationSequenceAwaitingAccountResetController: ViewController {
private var controllerNode: AuthorizationSequenceAwaitingAccountResetControllerNode {
return self.displayNode as! AuthorizationSequenceAwaitingAccountResetControllerNode
}
private let strings: PresentationStrings
private let theme: PresentationTheme
var logout: (() -> Void)?
var reset: (() -> Void)?
var protectedUntil: Int32?
var number: String?
var inProgress: Bool = false {
didSet {
if self.inProgress {
let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.theme.rootController.navigationBar.accentTextColor))
self.navigationItem.rightBarButtonItem = item
} else {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.logoutPressed))
}
}
}
init(strings: PresentationStrings, theme: PresentationTheme, back: @escaping () -> Void) {
self.strings = strings
self.theme = theme
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(theme), strings: NavigationBarStrings(presentationStrings: strings)))
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.statusBar.statusBarStyle = theme.intro.statusBarStyle.style
self.attemptNavigation = { _ in
return false
}
self.navigationBar?.backPressed = {
back()
}
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: " ", style: .plain, target: self, action: nil)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Settings_Logout, style: .plain, target: self, action: #selector(self.logoutPressed))
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = AuthorizationSequenceAwaitingAccountResetControllerNode(strings: self.strings, theme: self.theme)
self.displayNodeDidLoad()
self.controllerNode.reset = { [weak self] in
self?.reset?()
}
if let protectedUntil = self.protectedUntil, let number = self.number {
self.controllerNode.updateData(protectedUntil: protectedUntil, number: number)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
func updateData(protectedUntil: Int32, number: String) {
if self.protectedUntil != protectedUntil || self.number != number {
self.protectedUntil = protectedUntil
self.number = number
if self.isNodeLoaded {
self.controllerNode.updateData(protectedUntil: protectedUntil, number: number)
}
}
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
@objc func logoutPressed() {
self.logout?()
}
}
@@ -0,0 +1,176 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import AuthorizationUtils
private func timerValueString(days: Int32, hours: Int32, minutes: Int32, color: UIColor, strings: PresentationStrings) -> NSAttributedString {
var daysString = ""
if days > 0 {
daysString = strings.MessageTimer_Days(days) + " "
}
var hoursString = ""
if hours > 0 || days > 0 {
hoursString = strings.MessageTimer_Hours(hours) + " "
}
let minutesString = strings.MessageTimer_Minutes(minutes)
return NSAttributedString(string: daysString + hoursString + minutesString, font: Font.regular(21.0), textColor: color)
}
final class AuthorizationSequenceAwaitingAccountResetControllerNode: ASDisplayNode, UITextFieldDelegate {
private let strings: PresentationStrings
private let theme: PresentationTheme
private let titleNode: ASTextNode
private let noticeNode: ASTextNode
private let timerTitleNode: ASTextNode
private let timerValueNode: ASTextNode
private let resetNode: HighlightableButtonNode
private var layoutArguments: (ContainerViewLayout, CGFloat)?
var reset: (() -> Void)?
private var protectedUntil: Int32 = 0
private var timer: SwiftSignalKit.Timer?
init(strings: PresentationStrings, theme: PresentationTheme) {
self.strings = strings
self.theme = theme
self.titleNode = ASTextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.titleNode.attributedText = NSAttributedString(string: strings.Login_ResetAccountProtected_Title, font: Font.light(30.0), textColor: self.theme.list.itemPrimaryTextColor)
self.noticeNode = ASTextNode()
self.noticeNode.isUserInteractionEnabled = false
self.noticeNode.displaysAsynchronously = false
self.timerTitleNode = ASTextNode()
self.timerTitleNode.isUserInteractionEnabled = false
self.timerTitleNode.displaysAsynchronously = false
self.timerTitleNode.attributedText = NSAttributedString(string: strings.Login_ResetAccountProtected_TimerTitle, font: Font.regular(16.0), textColor: self.theme.list.itemPrimaryTextColor)
self.timerValueNode = ASTextNode()
self.timerValueNode.isUserInteractionEnabled = false
self.timerValueNode.displaysAsynchronously = false
self.resetNode = HighlightableButtonNode()
self.resetNode.setAttributedTitle(NSAttributedString(string: strings.Login_ResetAccountProtected_Reset, font: Font.regular(21.0), textColor: self.theme.list.itemAccentColor), for: [])
self.resetNode.setAttributedTitle(NSAttributedString(string: strings.Login_ResetAccountProtected_Reset, font: Font.regular(21.0), textColor: self.theme.list.itemPlaceholderTextColor), for: [.disabled])
self.resetNode.displaysAsynchronously = false
self.resetNode.isEnabled = false
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.backgroundColor = self.theme.list.plainBackgroundColor
self.addSubnode(self.titleNode)
self.addSubnode(self.noticeNode)
self.addSubnode(self.timerTitleNode)
self.addSubnode(self.timerValueNode)
self.addSubnode(self.resetNode)
self.resetNode.addTarget(self, action: #selector(self.resetPressed), forControlEvents: .touchUpInside)
}
deinit {
self.timer?.invalidate()
}
func updateData(protectedUntil: Int32, number: String) {
self.protectedUntil = protectedUntil
self.updateTimerValue()
self.noticeNode.attributedText = NSAttributedString(string: strings.Login_ResetAccountProtected_Text(number).string, font: Font.regular(16.0), textColor: self.theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
if let (layout, navigationHeight) = self.layoutArguments {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate)
}
if self.timer == nil {
let timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
self?.updateTimerValue()
}, queue: Queue.mainQueue())
self.timer = timer
timer.start()
}
}
private func updateTimerValue() {
let timerSeconds = max(0, self.protectedUntil - Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970))
let secondsInAMinute: Int32 = 60
let secondsInAnHour: Int32 = 60 * secondsInAMinute
let secondsInADay: Int32 = 24 * secondsInAnHour
let days = timerSeconds / secondsInADay
let hourSeconds = timerSeconds % secondsInADay
let hours = hourSeconds / secondsInAnHour
let minuteSeconds = hourSeconds % secondsInAnHour
var minutes = minuteSeconds / secondsInAMinute
if days == 0 && hours == 0 && minutes == 0 && timerSeconds > 0 {
minutes = 1
}
self.timerValueNode.attributedText = timerValueString(days: days, hours: hours, minutes: minutes, color: self.theme.list.itemPrimaryTextColor, strings: self.strings)
self.resetNode.isEnabled = timerSeconds <= 0
if let (layout, navigationHeight) = self.layoutArguments {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate)
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.layoutArguments = (layout, navigationBarHeight)
var insets = layout.insets(options: [.input])
insets.top = navigationBarHeight
if max(layout.size.width, layout.size.height) > 1023.0 {
self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_ResetAccountProtected_Title, font: Font.light(40.0), textColor: self.theme.list.itemPrimaryTextColor)
} else {
self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_ResetAccountProtected_Title, font: Font.light(30.0), textColor: self.theme.list.itemPrimaryTextColor)
}
let titleSize = self.titleNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude))
let noticeSize = self.noticeNode.measure(CGSize(width: layout.size.width - 28.0, height: CGFloat.greatestFiniteMagnitude))
let timerTitleSize = self.timerTitleNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude))
let timerValueSize = self.timerValueNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude))
let resetSize = self.resetNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude))
var items: [AuthorizationLayoutItem] = []
items.append(AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.noticeNode, size: noticeSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 20.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.timerTitleNode, size: timerTitleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 100.0, maxValue: 100.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.timerValueNode, size: timerValueSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.resetNode, size: resetSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
let _ = layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - 20.0)), items: items, transition: transition, failIfDoesNotFit: false)
}
@objc func resetPressed() {
self.reset?()
}
}
@@ -0,0 +1,287 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ProgressNavigationButtonNode
public final class AuthorizationSequenceCodeEntryController: ViewController {
private var controllerNode: AuthorizationSequenceCodeEntryControllerNode {
return self.displayNode as! AuthorizationSequenceCodeEntryControllerNode
}
private var validLayout: ContainerViewLayout?
private let strings: PresentationStrings
private let theme: PresentationTheme
public var loginWithCode: ((String) -> Void)?
public var signInWithApple: (() -> Void)?
public var openFragment: ((String) -> Void)?
var reset: (() -> Void)?
public var requestNextOption: (() -> Void)?
public var requestPreviousOption: (() -> Void)?
var resetEmail: (() -> Void)?
var retryResetEmail: (() -> Void)?
var data: (String, String?, SentAuthorizationCodeType, AuthorizationCodeNextType?, Int32?, SentAuthorizationCodeType?, Bool)?
var termsOfService: (UnauthorizedAccountTermsOfService, Bool)?
private let hapticFeedback = HapticFeedback()
private var appleSignInAllowed = false
public var inProgress: Bool = false {
didSet {
self.updateNavigationItems()
self.controllerNode.inProgress = self.inProgress
}
}
var isPrevious: Bool {
return self.data?.6 ?? false
}
public init(presentationData: PresentationData, back: @escaping () -> Void) {
self.strings = presentationData.strings
self.theme = presentationData.theme
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(theme), strings: NavigationBarStrings(presentationStrings: strings)))
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.hasActiveInput = true
self.statusBar.statusBarStyle = theme.intro.statusBarStyle.style
self.attemptNavigation = { _ in
return false
}
self.navigationBar?.backPressed = { [weak self] in
let text: String
let proceed: String
let stop: String
if let (_, _, type, _, _, _, _) = self?.data, case .email = type {
text = presentationData.strings.Login_CancelEmailVerification
proceed = presentationData.strings.Login_CancelEmailVerificationContinue
stop = presentationData.strings.Login_CancelEmailVerificationStop
} else {
text = presentationData.strings.Login_CancelPhoneVerification
proceed = presentationData.strings.Login_CancelPhoneVerificationContinue
stop = presentationData.strings.Login_CancelPhoneVerificationStop
}
self?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: proceed, action: {
}), TextAlertAction(type: .defaultAction, title: stop, action: {
back()
})]), in: .window(.root))
}
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = AuthorizationSequenceCodeEntryControllerNode(strings: self.strings, theme: self.theme)
self.displayNodeDidLoad()
self.controllerNode.view.disableAutomaticKeyboardHandling = [.forward, .backward]
self.controllerNode.loginWithCode = { [weak self] code in
self?.continueWithCode(code)
}
self.controllerNode.signInWithApple = { [weak self] in
self?.signInWithApple?()
}
self.controllerNode.openFragment = { [weak self] url in
self?.openFragment?(url)
}
self.controllerNode.requestNextOption = { [weak self] in
self?.requestNextOption?()
}
self.controllerNode.requestAnotherOption = { [weak self] in
self?.requestNextOption?()
}
self.controllerNode.requestPreviousOption = { [weak self] in
self?.requestPreviousOption?()
}
self.controllerNode.updateNextEnabled = { [weak self] value in
self?.navigationItem.rightBarButtonItem?.isEnabled = value
}
self.controllerNode.reset = { [weak self] in
self?.resetEmail?()
}
self.controllerNode.retryReset = { [weak self] in
self?.retryResetEmail?()
}
self.controllerNode.present = { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}
if let (number, email, codeType, nextType, timeout, previousCodeType, isPrevious) = self.data {
var appleSignInAllowed = false
if case let .email(_, _, _, _, appleSignInAllowedValue, _) = codeType {
appleSignInAllowed = appleSignInAllowedValue
}
self.controllerNode.updateData(number: number, email: email, codeType: codeType, nextType: nextType, timeout: timeout, appleSignInAllowed: appleSignInAllowed, previousCodeType: previousCodeType, isPrevious: isPrevious)
}
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let navigationController = self.navigationController as? NavigationController, let layout = self.validLayout {
addTemporaryKeyboardSnapshotView(navigationController: navigationController, layout: layout)
}
self.controllerNode.activateInput()
}
public func resetCode() {
self.controllerNode.resetCode()
}
public func animateSuccess() {
self.controllerNode.animateSuccess()
}
public func selectIncorrectPart() {
self.controllerNode.selectIncorrectPart()
}
public func animateError(text: String) {
self.hapticFeedback.error()
self.controllerNode.animateError(text: text)
}
func updateAppIsActive(_ isActive: Bool) {
self.controllerNode.updatePasteVisibility()
}
func updateNavigationItems() {
guard let layout = self.validLayout, layout.size.width < 360.0 else {
return
}
if self.inProgress {
let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.theme.rootController.navigationBar.accentTextColor))
self.navigationItem.rightBarButtonItem = item
} else {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed))
}
}
public func updateData(number: String, email: String?, codeType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, termsOfService: (UnauthorizedAccountTermsOfService, Bool)?, previousCodeType: SentAuthorizationCodeType?, isPrevious: Bool) {
self.termsOfService = termsOfService
if self.data?.0 != number || self.data?.1 != email || self.data?.2 != codeType || self.data?.3 != nextType || self.data?.4 != timeout || self.data?.5 != previousCodeType || self.data?.6 != isPrevious {
self.data = (number, email, codeType, nextType, timeout, previousCodeType, isPrevious)
var appleSignInAllowed = false
if case let .email(_, _, _, _, appleSignInAllowedValue, _) = codeType {
appleSignInAllowed = appleSignInAllowedValue
}
if self.isNodeLoaded {
self.controllerNode.updateData(number: number, email: email, codeType: codeType, nextType: nextType, timeout: timeout, appleSignInAllowed: appleSignInAllowed, previousCodeType: previousCodeType, isPrevious: isPrevious)
self.requestLayout(transition: .immediate)
}
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
let hadLayout = self.validLayout != nil
self.validLayout = layout
if !hadLayout {
self.updateNavigationItems()
if let navigationController = self.navigationController as? NavigationController {
addTemporaryKeyboardSnapshotView(navigationController: navigationController, layout: layout, local: true)
}
}
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
@objc private func nextPressed() {
guard let (_, _, type, _, _, _, _) = self.data else {
return
}
var minimalCodeLength = 1
switch type {
case let .otherSession(length):
minimalCodeLength = Int(length)
case let .sms(length):
minimalCodeLength = Int(length)
case let .call(length):
minimalCodeLength = Int(length)
case let .missedCall(_, length):
minimalCodeLength = Int(length)
case let .email(_, length, _, _, _, _):
minimalCodeLength = Int(length)
case let .fragment(_, length):
minimalCodeLength = Int(length)
case let .firebase(_, length):
minimalCodeLength = Int(length)
case .flashCall, .emailSetupRequired, .word, .phrase:
break
}
if self.controllerNode.currentCode.count < minimalCodeLength {
self.hapticFeedback.error()
self.controllerNode.animateError()
} else {
self.continueWithCode(self.controllerNode.currentCode)
}
}
private func continueWithCode(_ code: String) {
self.loginWithCode?(code)
}
public func applyConfirmationCode(_ code: Int) {
self.controllerNode.updateCode("\(code)")
}
}
func addTemporaryKeyboardSnapshotView(navigationController: NavigationController, layout: ContainerViewLayout, local: Bool = false) {
if case .compact = layout.metrics.widthClass, let statusBarHost = navigationController.statusBarHost {
if let keyboardView = statusBarHost.keyboardView {
keyboardView.layer.removeAllAnimations()
if let snapshotView = keyboardView.snapshotView(afterScreenUpdates: false) {
UIView.performWithoutAnimation {
snapshotView.frame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - snapshotView.frame.size.height), size: snapshotView.frame.size)
if local {
navigationController.view.addSubview(snapshotView)
} else if let keyboardWindow = statusBarHost.keyboardWindow {
keyboardWindow.addSubview(snapshotView)
}
Queue.mainQueue().after(local ? 0.8 : 0.7, {
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
})
}
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,178 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ProgressNavigationButtonNode
import AccountContext
public final class AuthorizationSequenceEmailEntryController: ViewController {
public enum Mode {
case setup
case change
}
private let mode: Mode
private let blocking: Bool
private var controllerNode: AuthorizationSequenceEmailEntryControllerNode {
return self.displayNode as! AuthorizationSequenceEmailEntryControllerNode
}
private var validLayout: ContainerViewLayout?
private let presentationData: PresentationData
public var proceedWithEmail: ((String) -> Void)?
public var signInWithApple: (() -> Void)?
private let hapticFeedback = HapticFeedback()
private var appleSignInAllowed = false
public var inProgress: Bool = false {
didSet {
self.updateNavigationItems()
self.controllerNode.inProgress = self.inProgress
}
}
public var authorization: Any?
public var authorizationDelegate: Any?
private var inBackground = false
private var inBackgroundDisposable: Disposable?
public init(context: AccountContext? = nil, presentationData: PresentationData, mode: Mode, blocking: Bool = false, back: @escaping () -> Void) {
self.presentationData = presentationData
self.mode = mode
self.blocking = blocking
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(presentationData.theme), strings: NavigationBarStrings(presentationStrings: presentationData.strings)))
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.hasActiveInput = true
self.statusBar.statusBarStyle = presentationData.theme.intro.statusBarStyle.style
self.attemptNavigation = { _ in
return false
}
self.navigationBar?.backPressed = {
back()
}
if self.blocking {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView())
}
if let context {
self.inBackgroundDisposable = (context.sharedContext.applicationBindings.applicationInForeground
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let self else {
return
}
let previousValue = self.inBackground
self.inBackground = value
if !value && previousValue {
let _ = (context.engine.notices.getServerProvidedSuggestions(reload: true)
|> deliverOnMainQueue).start(next: { [weak self] currentValues in
guard let self else {
return
}
if !currentValues.contains(.setupLoginEmail) && !currentValues.contains(.setupLoginEmailBlocking) {
self.dismiss()
}
})
}
})
}
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.inBackgroundDisposable?.dispose()
}
override public func loadDisplayNode() {
self.displayNode = AuthorizationSequenceEmailEntryControllerNode(strings: self.presentationData.strings, theme: self.presentationData.theme, mode: self.mode)
self.displayNodeDidLoad()
self.controllerNode.view.disableAutomaticKeyboardHandling = [.forward, .backward]
self.controllerNode.proceedWithEmail = { [weak self] _ in
self?.nextPressed()
}
self.controllerNode.signInWithApple = { [weak self] in
self?.signInWithApple?()
}
self.controllerNode.updateData(appleSignInAllowed: self.appleSignInAllowed)
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.controllerNode.activateInput()
}
func updateNavigationItems() {
guard let layout = self.validLayout, layout.size.width < 360.0 else {
return
}
if self.inProgress {
let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.rootController.navigationBar.accentTextColor))
self.navigationItem.rightBarButtonItem = item
} else {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed))
}
}
public func updateData(appleSignInAllowed: Bool) {
var appleSignInAllowed = appleSignInAllowed
if #available(iOS 13.0, *) {
} else {
appleSignInAllowed = false
}
if self.appleSignInAllowed != appleSignInAllowed {
self.appleSignInAllowed = appleSignInAllowed
if self.isNodeLoaded {
self.controllerNode.updateData(appleSignInAllowed: appleSignInAllowed)
}
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
let hadLayout = self.validLayout != nil
self.validLayout = layout
if !hadLayout {
self.updateNavigationItems()
}
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
@objc private func nextPressed() {
if self.controllerNode.currentEmail.isEmpty {
if self.appleSignInAllowed {
self.signInWithApple?()
} else {
self.hapticFeedback.error()
self.controllerNode.animateError()
}
} else {
self.proceedWithEmail?(self.controllerNode.currentEmail)
}
}
}
@@ -0,0 +1,296 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AuthorizationUtils
import AuthenticationServices
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import SolidRoundedButtonNode
final class AuthorizationDividerNode: ASDisplayNode {
private let titleNode: ImmediateTextNode
private let leftLineNode: ASDisplayNode
private let rightLineNode: ASDisplayNode
init(theme: PresentationTheme, strings: PresentationStrings) {
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 1
self.titleNode.attributedText = NSAttributedString(string: strings.Login_Or, font: Font.regular(17.0), textColor: theme.list.itemSecondaryTextColor)
self.leftLineNode = ASDisplayNode()
self.leftLineNode.backgroundColor = theme.list.itemSecondaryTextColor
self.rightLineNode = ASDisplayNode()
self.rightLineNode.backgroundColor = theme.list.itemSecondaryTextColor
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.leftLineNode)
self.addSubnode(self.rightLineNode)
}
func updateLayout(width: CGFloat) -> CGSize {
let lineSize = CGSize(width: 33.0, height: UIScreenPixel)
let spacing: CGFloat = 7.0
let titleSize = self.titleNode.updateLayout(CGSize(width: width - (lineSize.width + spacing) * 2.0, height: .greatestFiniteMagnitude))
let height: CGFloat = 40.0
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - lineSize.width) / 2.0), y: floor((height - titleSize.height) / 2.0)), size: titleSize)
self.titleNode.frame = titleFrame
self.leftLineNode.frame = CGRect(origin: CGPoint(x: titleFrame.minX - spacing - lineSize.width, y: floorToScreenPixels(height / 2.0)), size: lineSize)
self.rightLineNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + spacing, y: floorToScreenPixels(height / 2.0)), size: lineSize)
return CGSize(width: width, height: height)
}
}
final class AuthorizationSequenceEmailEntryControllerNode: ASDisplayNode, UITextFieldDelegate {
private let strings: PresentationStrings
private let theme: PresentationTheme
private let mode: AuthorizationSequenceEmailEntryController.Mode
private let animationNode: AnimatedStickerNode
private let titleNode: ASTextNode
private let noticeNode: ASTextNode
private let dividerNode: AuthorizationDividerNode
private var signInWithAppleButton: UIControl?
private let proceedNode: SolidRoundedButtonNode
private let codeField: TextFieldNode
private let codeSeparatorNode: ASDisplayNode
private var layoutArguments: (ContainerViewLayout, CGFloat)?
var currentEmail: String {
return self.codeField.textField.text ?? ""
}
var proceedWithEmail: ((String) -> Void)?
var signInWithApple: (() -> Void)?
private var appleSignInAllowed = false
var inProgress: Bool = false {
didSet {
self.codeField.alpha = self.inProgress ? 0.6 : 1.0
if self.inProgress != oldValue {
if self.inProgress {
self.proceedNode.transitionToProgress()
} else {
self.proceedNode.transitionFromProgress()
}
}
}
}
private let appearanceTimestamp = CACurrentMediaTime()
init(strings: PresentationStrings, theme: PresentationTheme, mode: AuthorizationSequenceEmailEntryController.Mode) {
self.strings = strings
self.theme = theme
self.mode = mode
self.animationNode = DefaultAnimatedStickerNodeImpl()
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "IntroMail"), width: 256, height: 256, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
self.titleNode = ASTextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.noticeNode = ASTextNode()
self.noticeNode.isUserInteractionEnabled = false
self.noticeNode.displaysAsynchronously = false
self.noticeNode.lineSpacing = 0.1
self.noticeNode.attributedText = NSAttributedString(string: self.strings.LoginEmail_Description, font: Font.regular(16.0), textColor: self.theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
if #available(iOS 13.0, *) {
self.signInWithAppleButton = ASAuthorizationAppleIDButton(authorizationButtonType: .signIn, authorizationButtonStyle: theme.overallDarkAppearance ? .white : .black)
(self.signInWithAppleButton as? ASAuthorizationAppleIDButton)?.cornerRadius = 11
}
self.proceedNode = SolidRoundedButtonNode(title: self.strings.Login_Continue, theme: SolidRoundedButtonTheme(theme: self.theme), height: 50.0, cornerRadius: 11.0)
self.proceedNode.progressType = .embedded
self.codeSeparatorNode = ASDisplayNode()
self.codeSeparatorNode.isLayerBacked = true
self.codeSeparatorNode.backgroundColor = self.theme.list.itemPlainSeparatorColor
self.codeField = TextFieldNode()
self.codeField.textField.font = Font.regular(20.0)
self.codeField.textField.textColor = self.theme.list.itemPrimaryTextColor
self.codeField.textField.textAlignment = .natural
self.codeField.textField.autocorrectionType = .no
self.codeField.textField.autocapitalizationType = .none
self.codeField.textField.keyboardType = .emailAddress
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.codeField.textField.textContentType = UITextContentType(rawValue: "")
}
self.codeField.textField.returnKeyType = .done
self.codeField.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
self.codeField.textField.disableAutomaticKeyboardHandling = [.forward, .backward]
self.codeField.textField.tintColor = self.theme.list.itemAccentColor
self.codeField.textField.placeholder = self.strings.Login_AddEmailPlaceholder
self.dividerNode = AuthorizationDividerNode(theme: self.theme, strings: self.strings)
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.backgroundColor = self.theme.list.plainBackgroundColor
self.codeField.textField.delegate = self
self.addSubnode(self.codeSeparatorNode)
self.addSubnode(self.codeField)
self.addSubnode(self.titleNode)
self.addSubnode(self.proceedNode)
self.addSubnode(self.noticeNode)
self.addSubnode(self.animationNode)
self.addSubnode(self.dividerNode)
self.codeField.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged)
self.proceedNode.pressed = { [weak self] in
self?.proceedPressed()
}
self.signInWithAppleButton?.addTarget(self, action: #selector(self.signInWithApplePressed), for: .touchUpInside)
}
override func didLoad() {
super.didLoad()
if let signInWithAppleButton = self.signInWithAppleButton {
self.view.addSubview(signInWithAppleButton)
}
}
@objc private func textDidChange() {
self.updateButtonsVisibility(transition: .animated(duration: 0.2, curve: .easeInOut))
}
private func updateButtonsVisibility(transition: ContainedViewLayoutTransition) {
if self.currentEmail.isEmpty && self.appleSignInAllowed {
transition.updateAlpha(node: self.proceedNode, alpha: 0.0)
transition.updateAlpha(node: self.dividerNode, alpha: 1.0)
if let signInWithAppleButton = self.signInWithAppleButton {
transition.updateAlpha(layer: signInWithAppleButton.layer, alpha: 1.0)
}
} else {
transition.updateAlpha(node: self.proceedNode, alpha: 1.0)
transition.updateAlpha(node: self.dividerNode, alpha: 0.0)
if let signInWithAppleButton = self.signInWithAppleButton {
transition.updateAlpha(layer: signInWithAppleButton.layer, alpha: 0.0)
}
}
}
func updateData(appleSignInAllowed: Bool) {
self.appleSignInAllowed = appleSignInAllowed
if let (layout, navigationHeight) = self.layoutArguments {
self.updateButtonsVisibility(transition: .immediate)
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate)
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let previousInputHeight = self.layoutArguments?.0.inputHeight ?? 0.0
let newInputHeight = layout.inputHeight ?? 0.0
self.layoutArguments = (layout, navigationBarHeight)
var layout = layout
if CACurrentMediaTime() - self.appearanceTimestamp < 2.0, newInputHeight < previousInputHeight {
layout = layout.withUpdatedInputHeight(previousInputHeight)
}
var insets = layout.insets(options: [])
insets.top = layout.statusBarHeight ?? 20.0
if let inputHeight = layout.inputHeight {
insets.bottom = max(inputHeight, insets.bottom)
}
let titleInset: CGFloat = layout.size.width > 320.0 ? 18.0 : 0.0
self.titleNode.attributedText = NSAttributedString(string: self.mode == .setup ? self.strings.LoginEmail_Title : self.strings.Login_EnterNewEmailTitle, font: Font.bold(28.0), textColor: self.theme.list.itemPrimaryTextColor)
let inset: CGFloat = 24.0
let maximumWidth: CGFloat = min(430.0, layout.size.width)
let animationSize = CGSize(width: 100.0, height: 100.0)
let titleSize = self.titleNode.measure(CGSize(width: maximumWidth, height: CGFloat.greatestFiniteMagnitude))
let noticeSize = self.noticeNode.measure(CGSize(width: maximumWidth - 80.0, height: CGFloat.greatestFiniteMagnitude))
let proceedHeight = self.proceedNode.updateLayout(width: maximumWidth - 48.0, transition: transition)
let proceedSize = CGSize(width: maximumWidth - 48.0, height: proceedHeight)
var items: [AuthorizationLayoutItem] = []
items.append(AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: titleInset, maxValue: titleInset), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.noticeNode, size: noticeSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 18.0, maxValue: 18.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.codeField, size: CGSize(width: maximumWidth - 88.0, height: 44.0), spacingBefore: AuthorizationLayoutItemSpacing(weight: 18.0, maxValue: 30.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.codeSeparatorNode, size: CGSize(width: maximumWidth - 48.0, height: UIScreenPixel), spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
if layout.size.width > 320.0 {
items.insert(AuthorizationLayoutItem(node: self.animationNode, size: animationSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), at: 0)
self.animationNode.updateLayout(size: animationSize)
self.proceedNode.isHidden = false
self.animationNode.isHidden = false
self.animationNode.visibility = true
} else {
insets.top = navigationBarHeight
self.proceedNode.isHidden = true
self.animationNode.isHidden = true
}
let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - proceedSize.width) / 2.0), y: layout.size.height - insets.bottom - proceedSize.height - inset), size: proceedSize)
transition.updateFrame(node: self.proceedNode, frame: buttonFrame)
let dividerSize = self.dividerNode.updateLayout(width: maximumWidth)
transition.updateFrame(node: self.dividerNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - dividerSize.width) / 2.0), y: buttonFrame.minY - dividerSize.height), size: dividerSize))
if let _ = self.signInWithAppleButton, self.appleSignInAllowed {
self.dividerNode.isHidden = false
} else {
self.dividerNode.isHidden = true
}
let _ = layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - 120.0)), items: items, transition: transition, failIfDoesNotFit: false)
if let signInWithAppleButton = self.signInWithAppleButton, self.appleSignInAllowed {
signInWithAppleButton.isHidden = false
transition.updateFrame(view: signInWithAppleButton, frame: self.proceedNode.frame)
} else {
self.signInWithAppleButton?.isHidden = true
}
}
func activateInput() {
self.codeField.textField.becomeFirstResponder()
}
func animateError() {
self.codeField.layer.addShakeAnimation()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
self.proceedWithEmail?(self.currentEmail)
return false
}
@objc func proceedPressed() {
self.proceedWithEmail?(self.currentEmail)
}
@objc func signInWithApplePressed() {
self.signInWithApple?()
}
}
@@ -0,0 +1,168 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import ProgressNavigationButtonNode
final class AuthorizationSequencePasswordEntryController: ViewController {
private var controllerNode: AuthorizationSequencePasswordEntryControllerNode {
return self.displayNode as! AuthorizationSequencePasswordEntryControllerNode
}
private var validLayout: ContainerViewLayout?
private let presentationData: PresentationData
var loginWithPassword: ((String) -> Void)?
var forgot: (() -> Void)?
var reset: (() -> Void)?
var hint: String?
var didForgotWithNoRecovery: Bool = false {
didSet {
if self.didForgotWithNoRecovery != oldValue {
if self.isNodeLoaded, let hint = self.hint {
self.controllerNode.updateData(hint: hint, didForgotWithNoRecovery: didForgotWithNoRecovery, suggestReset: self.suggestReset)
}
}
}
}
var suggestReset: Bool = false
private let hapticFeedback = HapticFeedback()
var inProgress: Bool = false {
didSet {
self.updateNavigationItems()
self.controllerNode.inProgress = self.inProgress
}
}
init(presentationData: PresentationData, back: @escaping () -> Void) {
self.presentationData = presentationData
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(presentationData.theme), strings: NavigationBarStrings(presentationStrings: presentationData.strings)))
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.hasActiveInput = true
self.statusBar.statusBarStyle = presentationData.theme.intro.statusBarStyle.style
self.attemptNavigation = { _ in
return false
}
self.navigationBar?.backPressed = {
back()
}
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = AuthorizationSequencePasswordEntryControllerNode(strings: self.presentationData.strings, theme: self.presentationData.theme)
self.displayNodeDidLoad()
self.controllerNode.view.disableAutomaticKeyboardHandling = [.forward, .backward]
self.controllerNode.loginWithCode = { [weak self] _ in
self?.nextPressed()
}
self.controllerNode.forgot = { [weak self] in
self?.forgotPressed()
}
self.controllerNode.reset = { [weak self] in
self?.resetPressed()
}
if let hint = self.hint {
self.controllerNode.updateData(hint: hint, didForgotWithNoRecovery: self.didForgotWithNoRecovery, suggestReset: self.suggestReset)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let navigationController = self.navigationController as? NavigationController, let layout = self.validLayout {
addTemporaryKeyboardSnapshotView(navigationController: navigationController, layout: layout)
}
self.controllerNode.activateInput()
}
func updateNavigationItems() {
guard let layout = self.validLayout, layout.size.width < 360.0 else {
return
}
if self.inProgress {
let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.rootController.navigationBar.accentTextColor))
self.navigationItem.rightBarButtonItem = item
} else {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed))
}
}
func updateData(hint: String, suggestReset: Bool) {
if self.hint != hint || self.suggestReset != suggestReset {
self.hint = hint
self.suggestReset = suggestReset
if self.isNodeLoaded {
self.controllerNode.updateData(hint: hint, didForgotWithNoRecovery: self.didForgotWithNoRecovery, suggestReset: self.suggestReset)
}
}
}
func passwordIsInvalid() {
if self.isNodeLoaded {
self.hapticFeedback.error()
self.controllerNode.passwordIsInvalid()
}
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
let hadLayout = self.validLayout != nil
self.validLayout = layout
if !hadLayout {
self.updateNavigationItems()
if let navigationController = self.navigationController as? NavigationController {
addTemporaryKeyboardSnapshotView(navigationController: navigationController, layout: layout, local: true)
}
}
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
@objc func nextPressed() {
if self.controllerNode.currentPassword.isEmpty {
self.hapticFeedback.error()
self.controllerNode.animateError()
} else {
self.loginWithPassword?(self.controllerNode.currentPassword)
}
}
func forgotPressed() {
/*if self.suggestReset {
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.TwoStepAuth_RecoveryFailed, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
} else*/ if self.didForgotWithNoRecovery {
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
} else {
self.forgot?()
}
}
func resetPressed() {
self.reset?()
}
}
@@ -0,0 +1,277 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import AuthorizationUtils
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import SolidRoundedButtonNode
final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UITextFieldDelegate {
private let strings: PresentationStrings
private let theme: PresentationTheme
private let animationNode: AnimatedStickerNode
private let titleNode: ASTextNode
private let titleActivateAreaNode: AccessibilityAreaNode
private let noticeNode: ASTextNode
private let noticeActivateAreaNode: AccessibilityAreaNode
private let forgotNode: HighlightableButtonNode
private let resetNode: HighlightableButtonNode
private let proceedNode: SolidRoundedButtonNode
private let codeField: TextFieldNode
private let codeSeparatorNode: ASDisplayNode
private var layoutArguments: (ContainerViewLayout, CGFloat)?
var currentPassword: String {
return self.codeField.textField.text ?? ""
}
var loginWithCode: ((String) -> Void)?
var forgot: (() -> Void)?
var reset: (() -> Void)?
var didForgotWithNoRecovery = false
var suggestReset = false
private var clearOnce: Bool = false
var inProgress: Bool = false {
didSet {
self.codeField.alpha = self.inProgress ? 0.6 : 1.0
if self.inProgress != oldValue {
if self.inProgress {
self.proceedNode.transitionToProgress()
} else {
self.proceedNode.transitionFromProgress()
}
}
}
}
private var timer: SwiftSignalKit.Timer?
private let appearanceTimestamp = CACurrentMediaTime()
init(strings: PresentationStrings, theme: PresentationTheme) {
self.strings = strings
self.theme = theme
self.animationNode = DefaultAnimatedStickerNodeImpl()
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "IntroPassword"), width: 256, height: 256, playbackMode: .still(.start), mode: .direct(cachePathPrefix: nil))
self.titleNode = ASTextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.titleNode.attributedText = NSAttributedString(string: strings.LoginPassword_Title, font: Font.semibold(28.0), textColor: self.theme.list.itemPrimaryTextColor)
self.titleActivateAreaNode = AccessibilityAreaNode()
self.titleActivateAreaNode.accessibilityTraits = .staticText
self.noticeNode = ASTextNode()
self.noticeNode.isUserInteractionEnabled = false
self.noticeNode.displaysAsynchronously = false
self.noticeNode.lineSpacing = 0.1
self.noticeNode.attributedText = NSAttributedString(string: strings.TwoStepAuth_EnterPasswordHelp, font: Font.regular(17.0), textColor: self.theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
self.noticeActivateAreaNode = AccessibilityAreaNode()
self.noticeActivateAreaNode.accessibilityTraits = .staticText
self.forgotNode = HighlightableButtonNode()
self.forgotNode.displaysAsynchronously = false
self.forgotNode.setAttributedTitle(NSAttributedString(string: self.strings.TwoStepAuth_EnterPasswordForgot, font: Font.regular(16.0), textColor: self.theme.list.itemAccentColor, paragraphAlignment: .center), for: [])
self.forgotNode.accessibilityLabel = self.strings.TwoStepAuth_EnterPasswordForgot
self.forgotNode.accessibilityTraits = [.button]
self.resetNode = HighlightableButtonNode()
self.resetNode.displaysAsynchronously = false
self.resetNode.setAttributedTitle(NSAttributedString(string: self.strings.LoginPassword_ResetAccount, font: Font.regular(16.0), textColor: self.theme.list.itemDestructiveColor, paragraphAlignment: .center), for: [])
self.codeSeparatorNode = ASDisplayNode()
self.codeSeparatorNode.isLayerBacked = true
self.codeSeparatorNode.backgroundColor = self.theme.list.itemPlainSeparatorColor
self.codeField = TextFieldNode()
self.codeField.textField.font = Font.regular(20.0)
self.codeField.textField.textColor = self.theme.list.itemPrimaryTextColor
self.codeField.textField.textAlignment = .natural
self.codeField.textField.isSecureTextEntry = true
self.codeField.textField.returnKeyType = .done
self.codeField.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
self.codeField.textField.disableAutomaticKeyboardHandling = [.forward, .backward]
self.codeField.textField.tintColor = self.theme.list.itemAccentColor
self.codeField.textField.accessibilityHint = self.strings.Login_VoiceOver_Password
self.proceedNode = SolidRoundedButtonNode(title: self.strings.Login_Continue, theme: SolidRoundedButtonTheme(theme: self.theme), height: 50.0, cornerRadius: 11.0)
self.proceedNode.progressType = .embedded
self.proceedNode.isEnabled = false
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.backgroundColor = self.theme.list.plainBackgroundColor
self.codeField.textField.delegate = self
self.codeField.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged)
self.addSubnode(self.codeSeparatorNode)
self.addSubnode(self.codeField)
self.addSubnode(self.titleNode)
self.addSubnode(self.titleActivateAreaNode)
self.addSubnode(self.forgotNode)
self.addSubnode(self.resetNode)
self.addSubnode(self.noticeNode)
self.addSubnode(self.noticeActivateAreaNode)
self.addSubnode(self.animationNode)
self.addSubnode(self.proceedNode)
self.forgotNode.addTarget(self, action: #selector(self.forgotPressed), forControlEvents: .touchUpInside)
self.resetNode.addTarget(self, action: #selector(self.resetPressed), forControlEvents: .touchUpInside)
self.proceedNode.pressed = { [weak self] in
if let strongSelf = self {
strongSelf.loginWithCode?(strongSelf.currentPassword)
}
}
self.timer = SwiftSignalKit.Timer(timeout: 7.5, repeat: true, completion: { [weak self] in
self?.animationNode.playOnce()
}, queue: Queue.mainQueue())
self.timer?.start()
}
deinit {
self.timer?.invalidate()
}
func updateData(hint: String, didForgotWithNoRecovery: Bool, suggestReset: Bool) {
self.didForgotWithNoRecovery = didForgotWithNoRecovery
self.suggestReset = suggestReset
self.codeField.textField.attributedPlaceholder = NSAttributedString(string: hint, font: Font.regular(20.0), textColor: self.theme.list.itemPlaceholderTextColor)
if let (layout, navigationHeight) = self.layoutArguments {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate)
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let previousInputHeight = self.layoutArguments?.0.inputHeight ?? 0.0
let newInputHeight = layout.inputHeight ?? 0.0
self.layoutArguments = (layout, navigationBarHeight)
var layout = layout
if CACurrentMediaTime() - self.appearanceTimestamp < 2.0, newInputHeight < previousInputHeight {
layout = layout.withUpdatedInputHeight(previousInputHeight)
}
let inset: CGFloat = 24.0
let maximumWidth: CGFloat = min(430.0, layout.size.width)
var insets = layout.insets(options: [])
insets.top = layout.statusBarHeight ?? 20.0
if let inputHeight = layout.inputHeight, !inputHeight.isZero {
insets.bottom = max(inputHeight, insets.bottom)
}
let titleInset: CGFloat = layout.size.width > 320.0 ? 18.0 : 0.0
let additionalBottomInset: CGFloat = layout.size.width > 320.0 ? 110.0 : 20.0
self.titleNode.attributedText = NSAttributedString(string: self.strings.LoginPassword_Title, font: Font.semibold(28.0), textColor: self.theme.list.itemPrimaryTextColor)
let animationSize = CGSize(width: 100.0, height: 100.0)
let titleSize = self.titleNode.measure(CGSize(width: maximumWidth, height: CGFloat.greatestFiniteMagnitude))
let noticeSize = self.noticeNode.measure(CGSize(width: maximumWidth - 28.0, height: CGFloat.greatestFiniteMagnitude))
let forgotSize = self.forgotNode.measure(CGSize(width: maximumWidth, height: CGFloat.greatestFiniteMagnitude))
let resetSize = self.resetNode.measure(CGSize(width: maximumWidth, height: CGFloat.greatestFiniteMagnitude))
let proceedHeight = self.proceedNode.updateLayout(width: maximumWidth - inset * 2.0, transition: transition)
let proceedSize = CGSize(width: maximumWidth - inset * 2.0, height: proceedHeight)
var items: [AuthorizationLayoutItem] = []
items.append(AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: titleInset, maxValue: titleInset), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.noticeNode, size: noticeSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 18.0, maxValue: 18.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.codeField, size: CGSize(width: maximumWidth - 80.0, height: 44.0), spacingBefore: AuthorizationLayoutItemSpacing(weight: 32.0, maxValue: 60.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.codeSeparatorNode, size: CGSize(width: maximumWidth - 48.0, height: UIScreenPixel), spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.forgotNode, size: forgotSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 48.0, maxValue: 100.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
if self.didForgotWithNoRecovery || self.suggestReset {
self.resetNode.isHidden = false
items.append(AuthorizationLayoutItem(node: self.resetNode, size: resetSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
} else {
self.resetNode.isHidden = true
}
if layout.size.width > 320.0 {
items.insert(AuthorizationLayoutItem(node: self.animationNode, size: animationSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), at: 0)
self.proceedNode.isHidden = false
self.animationNode.isHidden = false
self.animationNode.visibility = true
} else {
insets.top = navigationBarHeight
self.proceedNode.isHidden = true
self.animationNode.isHidden = true
}
transition.updateFrame(node: self.proceedNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - proceedSize.width) / 2.0), y: layout.size.height - insets.bottom - proceedSize.height - inset), size: proceedSize))
self.animationNode.updateLayout(size: animationSize)
let _ = layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - additionalBottomInset)), items: items, transition: transition, failIfDoesNotFit: false)
self.titleActivateAreaNode.accessibilityLabel = self.titleNode.attributedText?.string ?? ""
self.noticeActivateAreaNode.accessibilityLabel = self.noticeNode.attributedText?.string ?? ""
self.titleActivateAreaNode.frame = self.titleNode.frame
self.noticeActivateAreaNode.frame = self.noticeNode.frame
}
func activateInput() {
self.codeField.textField.becomeFirstResponder()
}
func animateError() {
self.codeField.layer.addShakeAnimation()
}
func passwordIsInvalid() {
self.clearOnce = true
}
@objc func textDidChange() {
self.proceedNode.isEnabled = !(self.codeField.textField.text ?? "").isEmpty
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if self.clearOnce {
self.clearOnce = false
if range.length > string.count {
textField.text = ""
return false
}
}
return true
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
self.loginWithCode?(self.currentPassword)
return false
}
@objc func forgotPressed() {
self.forgot?()
}
@objc func resetPressed() {
self.reset?()
}
}
@@ -0,0 +1,108 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import ProgressNavigationButtonNode
final class AuthorizationSequencePasswordRecoveryController: ViewController {
private var controllerNode: AuthorizationSequencePasswordRecoveryControllerNode {
return self.displayNode as! AuthorizationSequencePasswordRecoveryControllerNode
}
private let strings: PresentationStrings
private let theme: PresentationTheme
var recoverWithCode: ((String) -> Void)?
var noAccess: (() -> Void)?
var emailPattern: String?
private let hapticFeedback = HapticFeedback()
var inProgress: Bool = false {
didSet {
if self.inProgress {
let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.theme.rootController.navigationBar.accentTextColor))
self.navigationItem.rightBarButtonItem = item
} else {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed))
}
self.controllerNode.inProgress = self.inProgress
}
}
init(strings: PresentationStrings, theme: PresentationTheme, back: @escaping () -> Void) {
self.strings = strings
self.theme = theme
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(theme), strings: NavigationBarStrings(presentationStrings: strings)))
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.hasActiveInput = true
self.statusBar.statusBarStyle = theme.intro.statusBarStyle.style
self.attemptNavigation = { _ in
return false
}
self.navigationBar?.backPressed = {
back()
}
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed))
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = AuthorizationSequencePasswordRecoveryControllerNode(strings: self.strings, theme: self.theme)
self.displayNodeDidLoad()
self.controllerNode.recoverWithCode = { [weak self] _ in
self?.nextPressed()
}
self.controllerNode.noAccess = { [weak self] in
self?.noAccess?()
}
if let emailPattern = self.emailPattern {
self.controllerNode.updateData(emailPattern: emailPattern)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.controllerNode.activateInput()
}
func updateData(emailPattern: String) {
if self.emailPattern != emailPattern {
self.emailPattern = emailPattern
if self.isNodeLoaded {
self.controllerNode.updateData(emailPattern: emailPattern)
}
}
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
@objc func nextPressed() {
if self.controllerNode.currentCode.isEmpty {
hapticFeedback.error()
self.controllerNode.animateError()
} else {
self.recoverWithCode?(self.controllerNode.currentCode)
}
}
}
@@ -0,0 +1,139 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AuthorizationUtils
final class AuthorizationSequencePasswordRecoveryControllerNode: ASDisplayNode, UITextFieldDelegate {
private let strings: PresentationStrings
private let theme: PresentationTheme
private let titleNode: ASTextNode
private let noticeNode: ASTextNode
private let noAccessNode: HighlightableButtonNode
private let codeField: TextFieldNode
private let codeSeparatorNode: ASDisplayNode
private var layoutArguments: (ContainerViewLayout, CGFloat)?
var currentCode: String {
return self.codeField.textField.text ?? ""
}
var recoverWithCode: ((String) -> Void)?
var noAccess: (() -> Void)?
var inProgress: Bool = false {
didSet {
self.codeField.alpha = self.inProgress ? 0.6 : 1.0
}
}
init(strings: PresentationStrings, theme: PresentationTheme) {
self.strings = strings
self.theme = theme
self.titleNode = ASTextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.titleNode.attributedText = NSAttributedString(string: strings.TwoStepAuth_RecoveryTitle, font: Font.light(30.0), textColor: self.theme.list.itemPrimaryTextColor)
self.noticeNode = ASTextNode()
self.noticeNode.isUserInteractionEnabled = false
self.noticeNode.displaysAsynchronously = false
self.noticeNode.attributedText = NSAttributedString(string: strings.TwoStepAuth_RecoveryCodeHelp, font: Font.regular(17.0), textColor: self.theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
self.noticeNode.lineSpacing = 0.1
self.noAccessNode = HighlightableButtonNode()
self.noAccessNode.displaysAsynchronously = false
self.codeSeparatorNode = ASDisplayNode()
self.codeSeparatorNode.isLayerBacked = true
self.codeSeparatorNode.backgroundColor = self.theme.list.itemPlainSeparatorColor
self.codeField = TextFieldNode()
self.codeField.textField.font = Font.regular(20.0)
self.codeField.textField.textColor = self.theme.list.itemPrimaryTextColor
self.codeField.textField.textAlignment = .center
self.codeField.textField.attributedPlaceholder = NSAttributedString(string: self.strings.TwoStepAuth_RecoveryCode, font: Font.regular(20.0), textColor: self.theme.list.itemPlaceholderTextColor)
self.codeField.textField.returnKeyType = .done
self.codeField.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
self.codeField.textField.disableAutomaticKeyboardHandling = [.forward, .backward]
self.codeField.textField.tintColor = self.theme.list.itemAccentColor
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.backgroundColor = self.theme.list.plainBackgroundColor
self.codeField.textField.delegate = self
self.addSubnode(self.codeSeparatorNode)
self.addSubnode(self.codeField)
self.addSubnode(self.titleNode)
self.addSubnode(self.noAccessNode)
self.addSubnode(self.noticeNode)
self.noAccessNode.addTarget(self, action: #selector(self.noAccessPressed), forControlEvents: .touchUpInside)
}
func updateData(emailPattern: String) {
self.noAccessNode.setAttributedTitle(NSAttributedString(string: self.strings.TwoStepAuth_RecoveryEmailUnavailable(emailPattern).string, font: Font.regular(16.0), textColor: self.theme.list.itemAccentColor, paragraphAlignment: .center), for: [])
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.layoutArguments = (layout, navigationBarHeight)
var insets = layout.insets(options: [.input])
insets.top = navigationBarHeight
if max(layout.size.width, layout.size.height) > 1023.0 {
self.titleNode.attributedText = NSAttributedString(string: self.strings.TwoStepAuth_RecoveryTitle, font: Font.light(40.0), textColor: self.theme.list.itemPrimaryTextColor)
} else {
self.titleNode.attributedText = NSAttributedString(string: self.strings.TwoStepAuth_RecoveryTitle, font: Font.light(30.0), textColor: self.theme.list.itemPrimaryTextColor)
}
let titleSize = self.titleNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude))
let noticeSize = self.noticeNode.measure(CGSize(width: layout.size.width - 28.0, height: CGFloat.greatestFiniteMagnitude))
let noAccessSize = self.noAccessNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude))
var items: [AuthorizationLayoutItem] = []
items.append(AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.noticeNode, size: noticeSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.codeField, size: CGSize(width: layout.size.width - 88.0, height: 44.0), spacingBefore: AuthorizationLayoutItemSpacing(weight: 32.0, maxValue: 100.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.codeSeparatorNode, size: CGSize(width: layout.size.width - 88.0, height: UIScreenPixel), spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.noAccessNode, size: noAccessSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 48.0, maxValue: 100.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
let _ = layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - 20.0)), items: items, transition: transition, failIfDoesNotFit: false)
}
func activateInput() {
self.codeField.textField.becomeFirstResponder()
}
func animateError() {
self.codeField.layer.addShakeAnimation()
}
@objc func passwordFieldTextChanged(_ textField: UITextField) {
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
self.recoverWithCode?(self.currentCode)
return false
}
@objc func noAccessPressed() {
self.noAccess?()
}
}
@@ -0,0 +1,701 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import TelegramStringFormatting
import PresentationDataUtils
import ComponentFlow
import ViewControllerComponent
import MultilineTextComponent
import BalancedTextComponent
import BundleIconComponent
import ButtonComponent
import TextFormat
import InAppPurchaseManager
import ConfettiEffect
import PremiumCoinComponent
import Markdown
import CountrySelectionUI
import AccountContext
import AlertUI
import MessageUI
import CoreTelephony
import PhoneNumberFormat
import PlainButtonComponent
import StoreKit
import DeviceModel
final class AuthorizationSequencePaymentScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let sharedContext: SharedAccountContext
let engine: TelegramEngineUnauthorized
let inAppPurchaseManager: InAppPurchaseManager
let presentationData: PresentationData
let phoneNumber: String
let phoneCodeHash: String
let storeProduct: String
let supportEmailAddress: String
let supportEmailSubject: String
init(
sharedContext: SharedAccountContext,
engine: TelegramEngineUnauthorized,
inAppPurchaseManager: InAppPurchaseManager,
presentationData: PresentationData,
phoneNumber: String,
phoneCodeHash: String,
storeProduct: String,
supportEmailAddress: String,
supportEmailSubject: String
) {
self.sharedContext = sharedContext
self.engine = engine
self.inAppPurchaseManager = inAppPurchaseManager
self.presentationData = presentationData
self.phoneNumber = phoneNumber
self.phoneCodeHash = phoneCodeHash
self.storeProduct = storeProduct
self.supportEmailAddress = supportEmailAddress
self.supportEmailSubject = supportEmailSubject
}
static func ==(lhs: AuthorizationSequencePaymentScreenComponent, rhs: AuthorizationSequencePaymentScreenComponent) -> Bool {
if lhs.storeProduct != rhs.storeProduct {
return false
}
return true
}
final class View: UIView {
private let animation = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let list = ComponentView<Empty>()
private let check = ComponentView<Empty>()
private let button = ComponentView<Empty>()
private let helpButton = ComponentView<Empty>()
private var isUpdating: Bool = false
private var component: AuthorizationSequencePaymentScreenComponent?
private(set) weak var state: EmptyComponentState?
private var environment: EnvironmentType?
private var products: [InAppPurchaseManager.Product] = []
private var productsDisposable: Disposable?
private var inProgress = false
private var paymentDisposable = MetaDisposable()
override init(frame: CGRect) {
super.init(frame: frame)
self.disablesInteractiveKeyboardGestureRecognizer = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.paymentDisposable.dispose()
self.productsDisposable?.dispose()
}
private func proceed() {
guard let component = self.component, let storeProduct = self.products.first(where: { $0.id == component.storeProduct }), !self.inProgress else {
return
}
self.inProgress = true
self.state?.updated()
let (currency, amount) = storeProduct.priceCurrencyAndAmount
let purpose: AppStoreTransactionPurpose = .authCode(restore: false, phoneNumber: component.phoneNumber, phoneCodeHash: component.phoneCodeHash, currency: currency, amount: amount)
let _ = (component.engine.payments.canPurchasePremium(purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] available in
guard let self else {
return
}
let presentationData = component.presentationData
if available {
self.paymentDisposable.set((component.inAppPurchaseManager.buyProduct(storeProduct, quantity: 1, purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] status in
guard let self else {
return
}
self.inProgress = false
}, error: { [weak self] error in
guard let self, let controller = self.environment?.controller() else {
return
}
self.inProgress = false
self.state?.updated(transition: .immediate)
var errorText: String?
var errorCode: Int32?
switch error {
case .generic:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
errorCode = 1001
case .network:
errorText = presentationData.strings.Premium_Purchase_ErrorNetwork
errorCode = 1002
case .notAllowed:
errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed
errorCode = 1003
case .cantMakePayments:
errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments
errorCode = 1004
case .assignFailed:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
errorCode = 1005
case .tryLater:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
errorCode = 1006
case .cancelled:
break
}
if let errorText, let errorCode {
let theme = AlertControllerTheme(presentationData: presentationData)
let alertController = textAlertController(
alertContext: AlertControllerContext(theme: theme, themeSignal: .single(theme)),
title: nil,
text: errorText,
actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Login_PhoneNumberHelp, action: { [weak self] in
guard let self else {
return
}
self.displaySendEmail(error: errorText, errorCode: "\(errorCode)")
})
]
)
controller.present(alertController, in: .window(.root))
}
}))
} else {
self.inProgress = false
self.state?.updated(transition: .immediate)
}
})
}
private func displaySendEmail(error: String?, errorCode: String?) {
guard let component = self.component, let environment = self.environment, let controller = environment.controller() else {
return
}
let formattedNumber = "\(component.phoneNumber)"
let device = DeviceModel.currentModelCode()
let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown"
let systemVersion = UIDevice.current.systemVersion
let locale = Locale.current.identifier
let issue = error ?? "unknown"
let errorCode = errorCode ?? "unknown"
let body = environment.strings.Login_PhonePaidEmailText(
device,
systemVersion,
locale,
formattedNumber,
"1",
appVersion,
issue,
errorCode
).string
let presentationData = component.presentationData
AuthorizationSequenceController.presentEmailComposeController(address: component.supportEmailAddress, subject: environment.strings.Login_PhonePaidEmailSubject, body: body, from: controller, presentationData: presentationData)
}
func update(component: AuthorizationSequencePaymentScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[EnvironmentType.self].value
let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment
self.state = state
if self.component == nil {
self.productsDisposable = (component.inAppPurchaseManager.availableProducts
|> deliverOnMainQueue).start(next: { [weak self] products in
guard let self else {
return
}
self.products = products
if !self.isUpdating {
self.state?.updated()
}
})
}
self.component = component
if themeUpdated {
self.backgroundColor = environment.theme.list.plainBackgroundColor
}
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let helpButtonSize = self.helpButton.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.Login_PhoneNumberHelp, font: Font.regular(17.0), textColor: environment.theme.list.itemAccentColor))
)),
minSize: CGSize(width: 0.0, height: 44.0),
contentInsets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 8.0),
action: { [weak self] in
guard let self else {
return
}
self.displaySendEmail(error: nil, errorCode: nil)
},
animateScale: false,
animateContents: false
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 100.0)
)
let helpButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - 8.0 - helpButtonSize.width, y: environment.statusBarHeight), size: helpButtonSize)
if let helpButtonView = self.helpButton.view {
if helpButtonView.superview == nil {
self.addSubview(helpButtonView)
}
transition.setFrame(view: helpButtonView, frame: helpButtonFrame)
}
let animationSize = self.animation.update(
transition: transition,
component: AnyComponent(PremiumCoinComponent(
mode: .business,
isIntro: true,
isVisible: true,
hasIdleAnimations: true
)),
environment: {},
containerSize: CGSize(width: min(414.0, availableSize.width), height: 184.0)
)
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Login_Fee_Title, font: Font.bold(28.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)))
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
)
let textColor = environment.theme.list.itemPrimaryTextColor
let secondaryTextColor = environment.theme.list.itemSecondaryTextColor
let linkColor = environment.theme.list.itemAccentColor
var countryName: String = ""
if let (country, _) = AuthorizationSequenceCountrySelectionController.lookupCountryIdByNumber(component.phoneNumber, preferredCountries: [:]) {
countryName = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(country.id, strings: environment.strings) ?? country.name
}
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: "cost",
component: AnyComponent(ParagraphComponent(
title: environment.strings.Login_Fee_SmsCost_Title,
titleColor: textColor,
text: environment.strings.Login_Fee_SmsCost_Text(countryName).string,
textColor: secondaryTextColor,
iconName: "Premium/Authorization/Cost",
iconColor: linkColor
))
)
)
items.append(
AnyComponentWithIdentity(
id: "verification",
component: AnyComponent(ParagraphComponent(
title: environment.strings.Login_Fee_Verification_Title,
titleColor: textColor,
text: environment.strings.Login_Fee_Verification_Text,
textColor: secondaryTextColor,
iconName: "Premium/Authorization/Verification",
iconColor: linkColor
))
)
)
items.append(
AnyComponentWithIdentity(
id: "support",
component: AnyComponent(ParagraphComponent(
title: environment.strings.Login_Fee_Support_Title,
titleColor: textColor,
text: environment.strings.Login_Fee_Support_Text,
textColor: secondaryTextColor,
iconName: "Premium/Authorization/Support",
iconColor: linkColor,
action: { [weak self] in
guard let self, let controller = self.environment?.controller(), let product = self.products.first(where: { $0.id == component.storeProduct }) else {
return
}
let introController = component.sharedContext.makePremiumIntroController(
sharedContext: component.sharedContext,
engine: component.engine,
inAppPurchaseManager: component.inAppPurchaseManager,
source: .auth(product.price),
proceed: { [weak self] in
self?.proceed()
}
)
controller.push(introController)
}
))
)
)
let listSize = self.list.update(
transition: transition,
component: AnyComponent(List(items)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
)
let buttonHeight: CGFloat = 50.0
let bottomPanelPadding: CGFloat = 12.0
let titleSpacing: CGFloat = -24.0
let listSpacing: CGFloat = 12.0
let totalHeight = animationSize.height + titleSpacing + titleSize.height + listSpacing + listSize.height
var originY = floor((availableSize.height - buttonHeight - bottomPanelPadding * 2.0 - totalHeight) / 2.0)
if let animationView = self.animation.view {
if animationView.superview == nil {
self.addSubview(animationView)
}
animationView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - animationSize.width) / 2.0), y: originY), size: animationSize)
originY += animationSize.height + titleSpacing
}
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
titleView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: originY), size: titleSize)
originY += titleSize.height + listSpacing
}
if let listView = self.list.view {
if listView.superview == nil {
self.addSubview(listView)
}
listView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - listSize.width) / 2.0), y: originY), size: listSize)
}
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset
let priceString: String
if let product = self.products.first(where: { $0.id == component.storeProduct }) {
priceString = product.price
} else {
priceString = ""
}
let buttonString = environment.strings.Login_Fee_SignUp(priceString).string
let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
let buttonSize = self.button.update(
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: environment.theme.list.itemCheckColors.fillColor,
foreground: environment.theme.list.itemCheckColors.foregroundColor,
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable(buttonString),
component: AnyComponent(
VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Login_Fee_GetPremiumForAWeek, font: Font.medium(11.0), textColor: environment.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), paragraphAlignment: .center)))))
], spacing: 1.0)
)
),
isEnabled: true,
displaysProgress: self.inProgress,
action: { [weak self] in
self?.proceed()
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: buttonHeight)
)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - bottomPanelHeight + bottomPanelPadding), size: buttonSize)
}
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class AuthorizationSequencePaymentScreen: ViewControllerComponentContainer {
public init(
sharedContext: SharedAccountContext,
engine: TelegramEngineUnauthorized,
presentationData: PresentationData,
inAppPurchaseManager: InAppPurchaseManager,
phoneNumber: String,
phoneCodeHash: String,
storeProduct: String,
supportEmailAddress: String,
supportEmailSubject: String,
back: @escaping () -> Void
) {
super.init(component: AuthorizationSequencePaymentScreenComponent(
sharedContext: sharedContext,
engine: engine,
inAppPurchaseManager: inAppPurchaseManager,
presentationData: presentationData,
phoneNumber: phoneNumber,
phoneCodeHash: phoneCodeHash,
storeProduct: storeProduct,
supportEmailAddress: supportEmailAddress,
supportEmailSubject: supportEmailSubject
), navigationBarAppearance: .transparent, theme: .default, updatedPresentationData: (initial: presentationData, signal: .single(presentationData)))
loadServerCountryCodes(accountManager: sharedContext.accountManager, engine: engine, completion: { [weak self] in
if let strongSelf = self {
strongSelf.requestLayout(forceUpdate: true, transition: ContainedViewLayoutTransition.immediate)
}
})
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.attemptNavigation = { _ in
return false
}
self.navigationBar?.backPressed = {
back()
}
}
public override func loadDisplayNode() {
super.loadDisplayNode()
self.displayNode.view.disableAutomaticKeyboardHandling = [.forward, .backward]
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
@objc private func cancelPressed() {
self.dismiss()
}
}
private final class ParagraphComponent: CombinedComponent {
let title: String
let titleColor: UIColor
let text: String
let textColor: UIColor
let iconName: String
let iconColor: UIColor
let action: (() -> Void)?
public init(
title: String,
titleColor: UIColor,
text: String,
textColor: UIColor,
iconName: String,
iconColor: UIColor,
action: (() -> Void)? = nil
) {
self.title = title
self.titleColor = titleColor
self.text = text
self.textColor = textColor
self.iconName = iconName
self.iconColor = iconColor
self.action = action
}
static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.titleColor != rhs.titleColor {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.iconColor != rhs.iconColor {
return false
}
return true
}
final class State: ComponentState {
var cachedChevronImage: (UIImage, UIColor)?
}
func makeState() -> State {
return State()
}
static var body: Body {
let title = Child(MultilineTextComponent.self)
let text = Child(MultilineTextComponent.self)
let icon = Child(BundleIconComponent.self)
return { context in
let component = context.component
let state = context.state
let leftInset: CGFloat = 64.0
let rightInset: CGFloat = 32.0
let textSideInset: CGFloat = leftInset + 8.0
let spacing: CGFloat = 5.0
let textTopInset: CGFloat = 9.0
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let titleColor = component.titleColor
let textColor = component.textColor
let linkColor = component.iconColor
let titleMarkdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: boldTextFont, textColor: titleColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: titleColor),
link: MarkdownAttributeSet(font: boldTextFont, textColor: linkColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== linkColor {
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, linkColor)
}
let titleAttributedString = parseMarkdownIntoAttributedString(component.title, attributes: titleMarkdownAttributes).mutableCopy() as! NSMutableAttributedString
if let range = titleAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 {
titleAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: titleAttributedString.string))
}
let title = title.update(
component: MultilineTextComponent(
text: .plain(titleAttributedString),
horizontalAlignment: .center,
maximumNumberOfLines: 1,
highlightColor: linkColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { attributes, _ in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
component.action?()
}
}
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let textMarkdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: linkColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
let text = text.update(
component: MultilineTextComponent(
text: .markdown(text: component.text, attributes: textMarkdownAttributes),
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
highlightColor: linkColor.withAlphaComponent(0.1),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { attributes, _ in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
component.action?()
}
}
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height),
transition: .immediate
)
let icon = icon.update(
component: BundleIconComponent(
name: component.iconName,
tintColor: component.iconColor
),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
)
context.add(text
.position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0))
)
context.add(icon
.position(CGPoint(x: 47.0, y: textTopInset + 18.0))
)
return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 18.0)
}
}
}
@@ -0,0 +1,440 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import Postbox
import TelegramPresentationData
import ProgressNavigationButtonNode
import AccountContext
import CountrySelectionUI
import PhoneNumberFormat
import DebugSettingsUI
import MessageUI
import AuthenticationServices
public final class AuthorizationSequencePhoneEntryController: ViewController, MFMailComposeViewControllerDelegate, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
private var controllerNode: AuthorizationSequencePhoneEntryControllerNode {
return self.displayNode as! AuthorizationSequencePhoneEntryControllerNode
}
private var validLayout: ContainerViewLayout?
private let sharedContext: SharedAccountContext
private var account: UnauthorizedAccount?
private let apiId: Int32
private let apiHash: String
private let isTestingEnvironment: Bool
private let otherAccountPhoneNumbers: ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)])
private let network: Network
private let presentationData: PresentationData
private let openUrl: (String) -> Void
private let back: () -> Void
private var currentData: (Int32, String?, String)?
var codeNode: ASDisplayNode {
return self.controllerNode.codeNode
}
var numberNode: ASDisplayNode {
return self.controllerNode.numberNode
}
var buttonNode: ASDisplayNode {
return self.controllerNode.buttonNode
}
public var inProgress: Bool = false {
didSet {
self.updateNavigationItems()
self.controllerNode.inProgress = self.inProgress
self.confirmationController?.inProgress = self.inProgress
}
}
public var loginWithNumber: ((String, Bool) -> Void)?
public var loginWithPasskey: ((AuthorizationPasskeyData, Bool) -> Void)?
var accountUpdated: ((UnauthorizedAccount) -> Void)?
weak var confirmationController: PhoneConfirmationController?
private let termsDisposable = MetaDisposable()
private let hapticFeedback = HapticFeedback()
public init(sharedContext: SharedAccountContext, account: UnauthorizedAccount?, countriesConfiguration: CountriesConfiguration? = nil, apiId: Int32, apiHash: String, isTestingEnvironment: Bool, otherAccountPhoneNumbers: ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]), network: Network, presentationData: PresentationData, openUrl: @escaping (String) -> Void, back: @escaping () -> Void) {
self.sharedContext = sharedContext
self.account = account
self.apiId = apiId
self.apiHash = apiHash
self.isTestingEnvironment = isTestingEnvironment
self.otherAccountPhoneNumbers = otherAccountPhoneNumbers
self.network = network
self.presentationData = presentationData
self.openUrl = openUrl
self.back = back
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(presentationData.theme), strings: NavigationBarStrings(presentationStrings: presentationData.strings)))
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.hasActiveInput = true
self.statusBar.statusBarStyle = presentationData.theme.intro.statusBarStyle.style
self.attemptNavigation = { _ in
return false
}
self.navigationBar?.backPressed = {
back()
}
if !otherAccountPhoneNumbers.1.isEmpty {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
}
if let countriesConfiguration {
AuthorizationSequenceCountrySelectionController.setupCountryCodes(countries: countriesConfiguration.countries, codesByPrefix: countriesConfiguration.countriesByPrefix)
}
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.termsDisposable.dispose()
}
@objc private func cancelPressed() {
self.back()
}
func updateNavigationItems() {
guard let layout = self.validLayout, layout.size.width < 360.0 else {
return
}
if self.inProgress {
let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.rootController.navigationBar.accentTextColor))
self.navigationItem.rightBarButtonItem = item
} else {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed))
}
}
public func updateData(countryCode: Int32, countryName: String?, number: String) {
self.currentData = (countryCode, countryName, number)
if self.isNodeLoaded {
self.controllerNode.codeAndNumber = (countryCode, countryName, number)
}
}
private var shouldAnimateIn = false
private var transitionInArguments: (buttonFrame: CGRect, buttonTitle: String, animationSnapshot: UIView, textSnapshot: UIView)?
func animateWithSplashController(_ controller: AuthorizationSequenceSplashController) {
self.shouldAnimateIn = true
if let animationSnapshot = controller.animationSnapshot, let textSnapshot = controller.textSnaphot {
self.transitionInArguments = (controller.buttonFrame, controller.buttonTitle, animationSnapshot, textSnapshot)
}
}
override public func loadDisplayNode() {
self.displayNode = AuthorizationSequencePhoneEntryControllerNode(sharedContext: self.sharedContext, account: self.account, strings: self.presentationData.strings, theme: self.presentationData.theme, debugAction: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.view.endEditing(true)
self?.present(debugController(sharedContext: strongSelf.sharedContext, context: nil, modal: true), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, hasOtherAccounts: self.otherAccountPhoneNumbers.0 != nil)
self.controllerNode.accountUpdated = { [weak self] account in
guard let strongSelf = self else {
return
}
strongSelf.account = account
strongSelf.accountUpdated?(account)
}
self.controllerNode.retryPasskey = { [weak self] in
guard let self else {
return
}
self.loadAndPresentPasskey(force: true)
}
if let (code, name, number) = self.currentData {
self.controllerNode.codeAndNumber = (code, name, number)
}
self.displayNodeDidLoad()
self.controllerNode.view.disableAutomaticKeyboardHandling = [.forward, .backward]
self.controllerNode.selectCountryCode = { [weak self] in
if let strongSelf = self {
let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme)
controller.completeWithCountryCode = { code, name in
if let strongSelf = self, let currentData = strongSelf.currentData {
strongSelf.updateData(countryCode: Int32(code), countryName: name, number: currentData.2)
strongSelf.controllerNode.activateInput()
}
}
controller.dismissed = {
self?.controllerNode.activateInput()
}
strongSelf.push(controller)
}
}
self.controllerNode.checkPhone = { [weak self] in
self?.nextPressed()
}
if let account = self.account {
loadServerCountryCodes(accountManager: sharedContext.accountManager, engine: TelegramEngineUnauthorized(account: account), completion: { [weak self] in
if let strongSelf = self {
strongSelf.controllerNode.updateCountryCode()
}
})
} else {
self.controllerNode.updateCountryCode()
}
self.loadAndPresentPasskey(force: false)
}
private func loadAndPresentPasskey(force: Bool) {
if #available(iOS 16.0, *) {
Task { @MainActor [weak self] in
guard let self, let account = self.account else {
return
}
let decodeBase64: (String) -> Data? = { string in
var string = string.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
while string.count % 4 != 0 {
string.append("=")
}
return Data(base64Encoded: string)
}
let engine = TelegramEngineUnauthorized(account: account)
let passkeyDataString = await engine.auth.requestPasskeyLoginData(apiId: self.apiId, apiHash: self.apiHash).get()
guard let passkeyDataString, let passkeyData = passkeyDataString.data(using: .utf8) else {
return
}
guard let params = try? JSONSerialization.jsonObject(with: passkeyData) as? [String: Any] else {
return
}
guard let pkDict = params["publicKey"] as? [String: Any] else {
return
}
guard let relyingPartyIdentifier = pkDict["rpId"] as? String else {
return
}
guard let challengeBase64 = pkDict["challenge"] as? String else {
return
}
guard let challengeData = decodeBase64(challengeBase64) else {
return
}
let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: relyingPartyIdentifier)
let platformKeyRequest = platformProvider.createCredentialAssertionRequest(challenge: challengeData)
let authController = ASAuthorizationController(authorizationRequests: [platformKeyRequest])
authController.delegate = self
authController.presentationContextProvider = self
if force {
authController.performRequests()
} else {
authController.performRequests(options: [.preferImmediatelyAvailableCredentials])
}
}
}
}
public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
Task { @MainActor [weak self] in
guard let self, let account = self.account else {
return
}
let encodeBase64URL: (Data) -> String = { data in
var string = data.base64EncodedString()
string = string
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
string = string.replacingOccurrences(of: "=", with: "")
return string
}
if #available(iOS 17.0, *) {
if let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion {
guard let clientData = String(data: credential.rawClientDataJSON, encoding: .utf8) else {
return
}
guard let userHandle = String(data: credential.userID, encoding: .utf8) else {
return
}
let passkey = AuthorizationPasskeyData(
id: encodeBase64URL(credential.credentialID),
clientData: clientData,
authenticatorData: credential.rawAuthenticatorData,
signature: credential.signature,
userHandle: userHandle
)
self.loginWithPasskey?(passkey, self.controllerNode.syncContacts)
/*if let clientData = String(data: credential.rawClientDataJSON, encoding: .utf8), let attestationObject = credential.rawAttestationObject {
let passkey = await component.context.engine.auth.requestCreatePasskey(id: encodeBase64URL(credential.credentialID), clientData: clientData, attestationObject: attestationObject).get()
if let passkey {
if self.passkeysData == nil {
self.passkeysData = []
self.passkeysData?.insert(passkey, at: 0)
}
self.state?.updated(transition: .immediate)
}
}*/
let _ = account
let _ = credential
}
}
}
}
public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: any Error) {
if (error as NSError).domain == "com.apple.AuthenticationServices.AuthorizationError" && (error as NSError).code == 1001 {
self.controllerNode.updateDisplayPasskeyLoginOption()
if let validLayout = self.validLayout {
self.containerLayoutUpdated(validLayout, transition: .immediate)
}
}
}
public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
guard let windowScene = self.view.window?.windowScene else {
preconditionFailure()
}
return ASPresentationAnchor(windowScene: windowScene)
}
public func updateCountryCode() {
self.controllerNode.updateCountryCode()
}
private var animatingIn = false
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if self.shouldAnimateIn {
self.animatingIn = true
if let (buttonFrame, buttonTitle, animationSnapshot, textSnapshot) = self.transitionInArguments {
self.controllerNode.willAnimateIn(buttonFrame: buttonFrame, buttonTitle: buttonTitle, animationSnapshot: animationSnapshot, textSnapshot: textSnapshot)
}
Queue.mainQueue().justDispatch {
self.controllerNode.activateInput()
}
} else {
self.controllerNode.activateInput()
}
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.animatingIn {
self.controllerNode.activateInput()
}
}
override public func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if let confirmationController = self.confirmationController {
confirmationController.transitionOut()
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
let hadLayout = self.validLayout != nil
self.validLayout = layout
if !hadLayout {
self.updateNavigationItems()
}
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
if self.shouldAnimateIn, let inputHeight = layout.inputHeight, inputHeight > 0.0 {
if let (buttonFrame, buttonTitle, animationSnapshot, textSnapshot) = self.transitionInArguments {
self.shouldAnimateIn = false
self.controllerNode.animateIn(buttonFrame: buttonFrame, buttonTitle: buttonTitle, animationSnapshot: animationSnapshot, textSnapshot: textSnapshot)
}
}
}
public func dismissConfirmation() {
self.confirmationController?.dismissAnimated()
self.confirmationController = nil
}
@objc func nextPressed() {
guard self.confirmationController == nil else {
return
}
let (_, _, number) = self.controllerNode.codeAndNumber
if !number.isEmpty {
let logInNumber = cleanPhoneNumber(self.controllerNode.currentNumber, removePlus: true)
var existing: (String, AccountRecordId)?
for (number, id, isTestingEnvironment) in self.otherAccountPhoneNumbers.1 {
if isTestingEnvironment == self.isTestingEnvironment && cleanPhoneNumber(number, removePlus: true) == logInNumber {
existing = (number, id)
}
}
if let (_, id) = existing {
var actions: [TextAlertAction] = []
if let (current, _, _) = self.otherAccountPhoneNumbers.0, logInNumber != cleanPhoneNumber(current, removePlus: true) {
actions.append(TextAlertAction(type: .genericAction, title: self.presentationData.strings.Login_PhoneNumberAlreadyAuthorizedSwitch, action: { [weak self] in
self?.sharedContext.switchToAccount(id: id, fromSettingsController: nil, withChatListController: nil)
self?.back()
}))
}
actions.append(TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {}))
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Login_PhoneNumberAlreadyAuthorized, actions: actions), in: .window(.root))
} else {
if let validLayout = self.validLayout, validLayout.size.width > 320.0 {
let (code, formattedNumber) = self.controllerNode.formattedCodeAndNumber
let confirmationController = PhoneConfirmationController(theme: self.presentationData.theme, strings: self.presentationData.strings, code: code, number: formattedNumber, sourceController: self)
confirmationController.proceed = { [weak self] in
if let strongSelf = self {
strongSelf.loginWithNumber?(strongSelf.controllerNode.currentNumber, strongSelf.controllerNode.syncContacts)
}
}
(self.navigationController as? NavigationController)?.presentOverlay(controller: confirmationController, inGlobal: true, blockInteraction: true)
self.confirmationController = confirmationController
} else {
var actions: [TextAlertAction] = []
actions.append(TextAlertAction(type: .genericAction, title: self.presentationData.strings.Login_Edit, action: {}))
actions.append(TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Login_Yes, action: { [weak self] in
if let strongSelf = self {
strongSelf.loginWithNumber?(strongSelf.controllerNode.currentNumber, strongSelf.controllerNode.syncContacts)
}
}))
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: logInNumber, text: self.presentationData.strings.Login_PhoneNumberConfirmation, actions: actions), in: .window(.root))
}
}
} else {
self.hapticFeedback.error()
self.controllerNode.animateError()
}
}
public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true, completion: nil)
}
}
@@ -0,0 +1,292 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import LegacyComponents
import ProgressNavigationButtonNode
import ImageCompression
import LegacyMediaPickerUI
import Postbox
import TextFormat
import MoreButtonNode
import ContextUI
final class AuthorizationSequenceSignUpController: ViewController {
private var controllerNode: AuthorizationSequenceSignUpControllerNode {
return self.displayNode as! AuthorizationSequenceSignUpControllerNode
}
private var validLayout: ContainerViewLayout?
private let moreButtonNode: MoreButtonNode
private let presentationData: PresentationData
private let back: () -> Void
var initialName: (String, String) = ("", "")
private var termsOfService: UnauthorizedAccountTermsOfService?
var signUpWithName: ((String, String, Data?, Any?, TGVideoEditAdjustments?, Bool) -> Void)?
var openUrl: ((String) -> Void)?
var avatarAsset: Any?
var avatarAdjustments: TGVideoEditAdjustments?
var announceSignUp = true
private let hapticFeedback = HapticFeedback()
var inProgress: Bool = false {
didSet {
self.updateNavigationItems()
self.controllerNode.inProgress = self.inProgress
}
}
init(presentationData: PresentationData, back: @escaping () -> Void, displayCancel: Bool) {
self.presentationData = presentationData
self.back = back
self.moreButtonNode = MoreButtonNode(theme: self.presentationData.theme)
self.moreButtonNode.iconNode.enqueueState(.more, animated: false)
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(presentationData.theme), strings: NavigationBarStrings(presentationStrings: presentationData.strings)))
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.statusBar.statusBarStyle = presentationData.theme.intro.statusBarStyle.style
// self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed))
self.attemptNavigation = { _ in
return false
}
self.navigationBar?.backPressed = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_CancelSignUpConfirmation, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Login_CancelPhoneVerificationContinue, action: {
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Login_CancelPhoneVerificationStop, action: {
back()
})]), in: .window(.root))
}
if displayCancel {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
}
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode)
self.moreButtonNode.action = { [weak self] _, gesture in
if let strongSelf = self {
strongSelf.morePressed(node: strongSelf.moreButtonNode.contextSourceNode, gesture: gesture)
}
}
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func cancelPressed() {
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Login_CancelSignUpConfirmation, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Login_CancelPhoneVerificationContinue, action: {
}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Login_CancelPhoneVerificationStop, action: { [weak self] in
self?.back()
})]), in: .window(.root))
}
@objc private func moreButtonPressed() {
self.moreButtonNode.buttonPressed()
}
@objc private func morePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) {
let presentationData = self.presentationData
let announceSignUp = self.announceSignUp
var items: [ContextMenuItem] = []
let nop: ((ContextMenuActionItem.Action) -> Void)? = nil
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Login_Announce_Info, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: nop)))
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Login_Announce_Notify, icon: { theme in
if !announceSignUp {
return nil
}
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}, iconPosition: .left, action: { [weak self] _, a in
a(.default)
self?.announceSignUp = true
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Login_Announce_DontNotify, icon: { theme in
if announceSignUp {
return nil
}
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}, iconPosition: .left, action: { [weak self] _, a in
a(.default)
self?.announceSignUp = false
})))
let contextController = ContextController(presentationData: self.presentationData, source: .reference(AuthorizationContextReferenceContentSource(controller: self, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
self.present(contextController, in: .window(.root))
}
func updateNavigationItems() {
guard let layout = self.validLayout, layout.size.width < 360.0 else {
return
}
if self.inProgress {
let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.rootController.navigationBar.accentTextColor))
self.navigationItem.rightBarButtonItem = item
} else {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed))
}
}
override public func loadDisplayNode() {
let currentAvatarMixin = Atomic<NSObject?>(value: nil)
let theme = self.presentationData.theme
self.displayNode = AuthorizationSequenceSignUpControllerNode(theme: theme, strings: self.presentationData.strings, addPhoto: { [weak self] in
presentLegacyAvatarPicker(holder: currentAvatarMixin, signup: true, theme: theme, present: { c, a in
self?.view.endEditing(true)
self?.present(c, in: .window(.root), with: a)
}, openCurrent: nil, completion: { image in
self?.controllerNode.currentPhoto = image
self?.avatarAsset = nil
self?.avatarAdjustments = nil
}, videoCompletion: { image, asset, adjustments in
self?.controllerNode.currentPhoto = image
self?.avatarAsset = asset
self?.avatarAdjustments = adjustments
})
})
self.displayNodeDidLoad()
self.controllerNode.view.disableAutomaticKeyboardHandling = [.forward, .backward]
self.controllerNode.signUpWithName = { [weak self] _, _ in
self?.nextPressed()
}
self.controllerNode.openTermsOfService = { [weak self] in
guard let strongSelf = self, let termsOfService = strongSelf.termsOfService else {
return
}
strongSelf.view.endEditing(true)
let presentAlertImpl: () -> Void = {
guard let strongSelf = self else {
return
}
var dismissImpl: (() -> Void)?
let alertTheme = AlertControllerTheme(presentationData: strongSelf.presentationData)
let attributedText = stringWithAppliedEntities(termsOfService.text, entities: termsOfService.entities, baseColor: alertTheme.primaryColor, linkColor: alertTheme.accentColor, baseFont: Font.regular(13.0), linkFont: Font.regular(13.0), boldFont: Font.semibold(13.0), italicFont: Font.italic(13.0), boldItalicFont: Font.semiboldItalic(13.0), fixedFont: Font.regular(13.0), blockQuoteFont: Font.regular(13.0), message: nil)
let contentNode = TextAlertContentNode(theme: alertTheme, title: NSAttributedString(string: strongSelf.presentationData.strings.Login_TermsOfServiceHeader, font: Font.medium(17.0), textColor: alertTheme.primaryColor, paragraphAlignment: .center), text: attributedText, actions: [
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
dismissImpl?()
})
], actionLayout: .vertical, dismissOnOutsideTap: true)
contentNode.textAttributeAction = (NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), { value in
if let value = value as? String {
strongSelf.openUrl?(value)
}
})
let controller = AlertController(theme: alertTheme, contentNode: contentNode)
dismissImpl = { [weak controller] in
controller?.dismissAnimated()
}
strongSelf.view.endEditing(true)
strongSelf.present(controller, in: .window(.root))
}
presentAlertImpl()
}
self.controllerNode.updateData(firstName: self.initialName.0, lastName: self.initialName.1, hasTermsOfService: self.termsOfService != nil)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let navigationController = self.navigationController as? NavigationController, let layout = self.validLayout {
addTemporaryKeyboardSnapshotView(navigationController: navigationController, layout: layout)
}
self.controllerNode.activateInput()
}
func updateData(firstName: String, lastName: String, termsOfService: UnauthorizedAccountTermsOfService?) {
if self.isNodeLoaded {
if (firstName, lastName) != self.controllerNode.currentName || self.termsOfService != termsOfService {
self.termsOfService = termsOfService
self.controllerNode.updateData(firstName: firstName, lastName: lastName, hasTermsOfService: termsOfService != nil)
}
} else {
self.initialName = (firstName, lastName)
self.termsOfService = termsOfService
}
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
let hadLayout = self.validLayout != nil
self.validLayout = layout
if !hadLayout {
self.updateNavigationItems()
if let navigationController = self.navigationController as? NavigationController {
addTemporaryKeyboardSnapshotView(navigationController: navigationController, layout: layout, local: true)
}
}
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
@objc func nextPressed() {
let firstName = self.controllerNode.currentName.0.trimmingCharacters(in: .whitespacesAndNewlines)
let lastName = self.controllerNode.currentName.1.trimmingCharacters(in: .whitespacesAndNewlines)
var name: (String, String)?
if firstName.isEmpty && lastName.isEmpty {
self.hapticFeedback.error()
self.controllerNode.animateError()
return
} else if firstName.isEmpty && !lastName.isEmpty {
name = (lastName, "")
} else {
name = (firstName, lastName)
}
if let name = name {
self.signUpWithName?(name.0, name.1, self.controllerNode.currentPhoto.flatMap({ image in
let tempFile = TempBox.shared.tempFile(fileName: "file")
let result = compressImageToJPEG(image, quality: 0.7, tempFilePath: tempFile.path)
TempBox.shared.dispose(tempFile)
return result
}), self.avatarAsset, self.avatarAdjustments, self.announceSignUp)
}
}
}
private final class AuthorizationContextReferenceContentSource: ContextReferenceContentSource {
private let controller: ViewController
private let sourceNode: ContextReferenceContentNode
init(controller: ViewController, sourceNode: ContextReferenceContentNode) {
self.controller = controller
self.sourceNode = sourceNode
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
@@ -0,0 +1,299 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import TextFormat
import Markdown
import SolidRoundedButtonNode
import AuthorizationUtils
private func roundCorners(diameter: CGFloat) -> UIImage {
UIGraphicsBeginImageContextWithOptions(CGSize(width: diameter, height: diameter), false, 0.0)
let context = UIGraphicsGetCurrentContext()!
context.setBlendMode(.copy)
context.setFillColor(UIColor.black.cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: diameter, height: diameter)))
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: diameter, height: diameter)))
let image = UIGraphicsGetImageFromCurrentImageContext()!.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0))
UIGraphicsEndImageContext()
return image
}
final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFieldDelegate {
private let theme: PresentationTheme
private let strings: PresentationStrings
private let addPhoto: () -> Void
private let titleNode: ASTextNode
private let currentOptionNode: ASTextNode
private let termsNode: ImmediateTextNode
private let firstNameField: TextFieldNode
private let lastNameField: TextFieldNode
private let firstSeparatorNode: ASDisplayNode
private let lastSeparatorNode: ASDisplayNode
private let currentPhotoNode: ASImageNode
private let addPhotoButton: HighlightableButtonNode
private let proceedNode: SolidRoundedButtonNode
private var layoutArguments: (ContainerViewLayout, CGFloat)?
private let appearanceTimestamp = CACurrentMediaTime()
var currentName: (String, String) {
return (self.firstNameField.textField.text ?? "", self.lastNameField.textField.text ?? "")
}
var currentPhoto: UIImage? = nil {
didSet {
if let currentPhoto = self.currentPhoto {
self.currentPhotoNode.image = generateImage(CGSize(width: 110.0, height: 110.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.draw(currentPhoto.cgImage!, in: CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.destinationOut)
context.draw(roundCorners(diameter: size.width).cgImage!, in: CGRect(origin: CGPoint(), size: size))
})
} else {
self.currentPhotoNode.image = nil
}
}
}
var signUpWithName: ((String, String) -> Void)?
var openTermsOfService: (() -> Void)?
var inProgress: Bool = false {
didSet {
self.firstNameField.alpha = self.inProgress ? 0.6 : 1.0
self.lastNameField.alpha = self.inProgress ? 0.6 : 1.0
if self.inProgress != oldValue {
if self.inProgress {
self.proceedNode.transitionToProgress()
} else {
self.proceedNode.transitionFromProgress()
}
}
}
}
init(theme: PresentationTheme, strings: PresentationStrings, addPhoto: @escaping () -> Void) {
self.theme = theme
self.strings = strings
self.addPhoto = addPhoto
self.titleNode = ASTextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_InfoTitle, font: Font.semibold(28.0), textColor: theme.list.itemPrimaryTextColor)
self.currentOptionNode = ASTextNode()
self.currentOptionNode.isUserInteractionEnabled = false
self.currentOptionNode.displaysAsynchronously = false
self.currentOptionNode.attributedText = NSAttributedString(string: self.strings.Login_InfoHelp, font: Font.regular(16.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
self.termsNode = ImmediateTextNode()
self.termsNode.textAlignment = .center
self.termsNode.maximumNumberOfLines = 0
self.termsNode.displaysAsynchronously = false
let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.list.itemSecondaryTextColor)
let link = MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.list.itemAccentColor, additionalAttributes: [TelegramTextAttributes.URL: ""])
self.termsNode.attributedText = parseMarkdownIntoAttributedString(strings.Login_TermsOfServiceLabel.replacingOccurrences(of: "\n", with: " ").replacingOccurrences(of: "]", with: "]()"), attributes: MarkdownAttributes(body: body, bold: body, link: link, linkAttribute: { _ in nil }), textAlignment: .center)
self.firstSeparatorNode = ASDisplayNode()
self.firstSeparatorNode.isLayerBacked = true
self.firstSeparatorNode.backgroundColor = self.theme.list.itemPlainSeparatorColor
self.lastSeparatorNode = ASDisplayNode()
self.lastSeparatorNode.isLayerBacked = true
self.lastSeparatorNode.backgroundColor = self.theme.list.itemPlainSeparatorColor
self.firstNameField = TextFieldNode()
self.firstNameField.textField.font = Font.regular(20.0)
self.firstNameField.textField.textColor = self.theme.list.itemPrimaryTextColor
self.firstNameField.textField.textAlignment = .natural
self.firstNameField.textField.returnKeyType = .next
self.firstNameField.textField.attributedPlaceholder = NSAttributedString(string: self.strings.UserInfo_FirstNamePlaceholder, font: self.firstNameField.textField.font, textColor: self.theme.list.itemPlaceholderTextColor)
self.firstNameField.textField.autocapitalizationType = .words
self.firstNameField.textField.autocorrectionType = .no
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.firstNameField.textField.textContentType = .givenName
}
self.firstNameField.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.firstNameField.textField.tintColor = theme.list.itemAccentColor
self.lastNameField = TextFieldNode()
self.lastNameField.textField.font = Font.regular(20.0)
self.lastNameField.textField.textColor = self.theme.list.itemPrimaryTextColor
self.lastNameField.textField.textAlignment = .natural
self.lastNameField.textField.returnKeyType = .done
self.lastNameField.textField.attributedPlaceholder = NSAttributedString(string: strings.UserInfo_LastNamePlaceholder, font: self.lastNameField.textField.font, textColor: self.theme.list.itemPlaceholderTextColor)
self.lastNameField.textField.autocapitalizationType = .words
self.lastNameField.textField.autocorrectionType = .no
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.lastNameField.textField.textContentType = .familyName
}
self.lastNameField.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.lastNameField.textField.tintColor = theme.list.itemAccentColor
self.currentPhotoNode = ASImageNode()
self.currentPhotoNode.isUserInteractionEnabled = false
self.currentPhotoNode.displaysAsynchronously = false
self.currentPhotoNode.displayWithoutProcessing = true
self.addPhotoButton = HighlightableButtonNode()
self.addPhotoButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIconLarge"), color: self.theme.list.itemAccentColor), for: .normal)
self.addPhotoButton.setBackgroundImage(generateFilledCircleImage(diameter: 110.0, color: self.theme.list.itemAccentColor.withAlphaComponent(0.1), strokeColor: nil, strokeWidth: nil, backgroundColor: nil), for: .normal)
self.addPhotoButton.addSubnode(self.currentPhotoNode)
self.addPhotoButton.allowsGroupOpacity = true
self.proceedNode = SolidRoundedButtonNode(title: self.strings.Login_Continue, theme: SolidRoundedButtonTheme(theme: self.theme), height: 50.0, cornerRadius: 11.0)
self.proceedNode.progressType = .embedded
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.backgroundColor = self.theme.list.plainBackgroundColor
self.firstNameField.textField.delegate = self
self.lastNameField.textField.delegate = self
self.addSubnode(self.firstSeparatorNode)
self.addSubnode(self.lastSeparatorNode)
self.addSubnode(self.firstNameField)
self.addSubnode(self.lastNameField)
self.addSubnode(self.titleNode)
self.addSubnode(self.currentOptionNode)
self.addSubnode(self.termsNode)
self.termsNode.isHidden = true
self.addSubnode(self.addPhotoButton)
self.addSubnode(self.proceedNode)
self.addPhotoButton.addTarget(self, action: #selector(self.addPhotoPressed), forControlEvents: .touchUpInside)
self.termsNode.linkHighlightColor = self.theme.list.itemAccentColor.withAlphaComponent(0.2)
self.termsNode.highlightAttributeAction = { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
}
self.termsNode.tapAttributeAction = { [weak self] attributes, _ in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
self?.openTermsOfService?()
}
}
self.proceedNode.pressed = { [weak self] in
if let strongSelf = self {
let name = strongSelf.currentName
strongSelf.signUpWithName?(name.0, name.1)
}
}
}
func updateData(firstName: String, lastName: String, hasTermsOfService: Bool) {
self.termsNode.isHidden = !hasTermsOfService
self.firstNameField.textField.attributedText = NSAttributedString(string: firstName, font: Font.regular(20.0), textColor: self.theme.list.itemPlaceholderTextColor)
self.lastNameField.textField.attributedText = NSAttributedString(string: lastName, font: Font.regular(20.0), textColor: self.theme.list.itemPlaceholderTextColor)
if let (layout, navigationHeight) = self.layoutArguments {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate)
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let previousInputHeight = self.layoutArguments?.0.inputHeight ?? 0.0
let newInputHeight = layout.inputHeight ?? 0.0
self.layoutArguments = (layout, navigationBarHeight)
var layout = layout
if CACurrentMediaTime() - self.appearanceTimestamp < 2.0, newInputHeight < previousInputHeight {
layout = layout.withUpdatedInputHeight(previousInputHeight)
}
let maximumWidth: CGFloat = min(430.0, layout.size.width)
var insets = layout.insets(options: [.statusBar])
if let inputHeight = layout.inputHeight {
insets.bottom = max(inputHeight, layout.standardInputHeight)
}
let additionalBottomInset: CGFloat = layout.size.width > 320.0 ? 90.0 : 10.0
self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_InfoTitle, font: Font.semibold(28.0), textColor: self.theme.list.itemPrimaryTextColor)
let titleSize = self.titleNode.measure(CGSize(width: maximumWidth, height: CGFloat.greatestFiniteMagnitude))
let fieldHeight: CGFloat = 54.0
let sideInset: CGFloat = 24.0
let innerInset: CGFloat = 16.0
let noticeSize = self.currentOptionNode.measure(CGSize(width: maximumWidth - 28.0, height: CGFloat.greatestFiniteMagnitude))
let termsSize = self.termsNode.updateLayout(CGSize(width: maximumWidth - 28.0, height: CGFloat.greatestFiniteMagnitude))
let avatarSize: CGSize = CGSize(width: 110.0, height: 110.0)
var items: [AuthorizationLayoutItem] = []
items.append(AuthorizationLayoutItem(node: self.addPhotoButton, size: avatarSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 16.0, maxValue: 16.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
self.currentPhotoNode.frame = CGRect(origin: CGPoint(), size: avatarSize)
items.append(AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 18.0, maxValue: 18.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.currentOptionNode, size: noticeSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 20.0, maxValue: 20.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.firstNameField, size: CGSize(width: layout.size.width - (sideInset + innerInset) * 2.0, height: fieldHeight), spacingBefore: AuthorizationLayoutItemSpacing(weight: 32.0, maxValue: 60.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.firstSeparatorNode, size: CGSize(width: layout.size.width - sideInset * 2.0, height: UIScreenPixel), spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.lastNameField, size: CGSize(width: layout.size.width - (sideInset + innerInset) * 2.0, height: fieldHeight), spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.lastSeparatorNode, size: CGSize(width: layout.size.width - sideInset * 2.0, height: UIScreenPixel), spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
items.append(AuthorizationLayoutItem(node: self.termsNode, size: termsSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 48.0, maxValue: 100.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
if layout.size.width > 320.0 {
self.proceedNode.isHidden = false
let inset: CGFloat = 24.0
let proceedHeight = self.proceedNode.updateLayout(width: maximumWidth - 48.0, transition: transition)
let proceedSize = CGSize(width: maximumWidth - 48.0, height: proceedHeight)
transition.updateFrame(node: self.proceedNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - proceedSize.width) / 2.0), y: layout.size.height - insets.bottom - proceedSize.height - inset), size: proceedSize))
} else {
insets.top = navigationBarHeight
self.proceedNode.isHidden = true
}
let _ = layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - additionalBottomInset)), items: items, transition: transition, failIfDoesNotFit: false)
}
func activateInput() {
self.firstNameField.textField.becomeFirstResponder()
}
func animateError() {
if self.firstNameField.textField.text == nil || self.firstNameField.textField.text!.isEmpty {
self.firstNameField.layer.addShakeAnimation()
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if textField === self.firstNameField.textField {
self.lastNameField.textField.becomeFirstResponder()
} else {
let name = self.currentName
self.signUpWithName?(name.0, name.1)
}
return false
}
@objc private func addPhotoPressed() {
self.addPhoto()
}
}
@@ -0,0 +1,252 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SSignalKit
import SwiftSignalKit
import TelegramPresentationData
import LegacyComponents
import SolidRoundedButtonNode
import RMIntro
public final class AuthorizationSequenceSplashController: ViewController {
private var controllerNode: AuthorizationSequenceSplashControllerNode {
return self.displayNode as! AuthorizationSequenceSplashControllerNode
}
private let accountManager: AccountManager<TelegramAccountManagerTypes>
private let account: UnauthorizedAccount
private let theme: PresentationTheme
private let controller: RMIntroViewController
private var validLayout: ContainerViewLayout?
var nextPressed: ((PresentationStrings?) -> Void)?
private let suggestedLocalization = Promise<SuggestedLocalizationInfo?>()
private let activateLocalizationDisposable = MetaDisposable()
private let startButton: SolidRoundedButtonNode
init(accountManager: AccountManager<TelegramAccountManagerTypes>, account: UnauthorizedAccount, theme: PresentationTheme) {
self.accountManager = accountManager
self.account = account
self.theme = theme
self.suggestedLocalization.set(.single(nil)
|> then(TelegramEngineUnauthorized(account: self.account).localization.currentlySuggestedLocalization(extractKeys: ["Login.ContinueWithLocalization"])))
let suggestedLocalization = self.suggestedLocalization
let localizationSignal = SSignal(generator: { subscriber in
let disposable = suggestedLocalization.get().start(next: { localization in
guard let localization = localization else {
return
}
var continueWithLanguageString: String = "Continue"
for entry in localization.extractedEntries {
switch entry {
case let .string(key, value):
if key == "Login.ContinueWithLocalization" {
continueWithLanguageString = value
}
default:
break
}
}
if let available = localization.availableLocalizations.first, available.languageCode != "en" {
let value = TGSuggestedLocalization(info: TGAvailableLocalization(title: available.title, localizedTitle: available.localizedTitle, code: available.languageCode), continueWithLanguageString: continueWithLanguageString, chooseLanguageString: "Choose Language", chooseLanguageOtherString: "Choose Language", englishLanguageNameString: "English")
subscriber.putNext(value)
}
}, completed: {
subscriber.putCompletion()
})
return SBlockDisposable(block: {
disposable.dispose()
})
})
self.controller = RMIntroViewController(backgroundColor: theme.list.plainBackgroundColor, primaryColor: theme.list.itemPrimaryTextColor, buttonColor: theme.intro.startButtonColor, accentColor: theme.list.itemAccentColor, regularDotColor: theme.intro.dotColor, highlightedDotColor: theme.list.itemAccentColor, suggestedLocalizationSignal: localizationSignal)
self.startButton = SolidRoundedButtonNode(title: "Start Messaging", theme: SolidRoundedButtonTheme(theme: theme), height: 50.0, cornerRadius: 13.0, isShimmering: true)
super.init(navigationBarPresentationData: nil)
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.statusBar.statusBarStyle = theme.intro.statusBarStyle.style
self.controller.startMessaging = { [weak self] in
self?.activateLocalization("en")
}
self.controller.startMessagingInAlternativeLanguage = { [weak self] code in
if let code = code {
self?.activateLocalization(code)
}
}
self.startButton.pressed = { [weak self] in
self?.activateLocalization("en")
}
self.controller.createStartButton = { [weak self] width in
let _ = self?.startButton.updateLayout(width: width, transition: .immediate)
return self?.startButton.view
}
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.activateLocalizationDisposable.dispose()
}
public override func loadDisplayNode() {
self.displayNode = AuthorizationSequenceSplashControllerNode(theme: self.theme)
self.displayNodeDidLoad()
}
func animateIn() {
self.controller.animateIn()
}
var buttonFrame: CGRect {
return self.startButton.frame
}
var buttonTitle: String {
return self.startButton.title ?? ""
}
var animationSnapshot: UIView? {
return self.controller.createAnimationSnapshot()
}
var textSnaphot: UIView? {
return self.controller.createTextSnapshot()
}
private func addControllerIfNeeded() {
if !self.controller.isViewLoaded || self.controller.view.superview == nil {
self.displayNode.view.addSubview(self.controller.view)
if let layout = self.validLayout {
controller.view.frame = CGRect(origin: CGPoint(), size: layout.size)
}
self.controller.viewDidAppear(false)
}
}
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.addControllerIfNeeded()
self.controller.viewWillAppear(false)
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
controller.viewDidAppear(animated)
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
controller.viewWillDisappear(animated)
}
public override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
controller.viewDidDisappear(animated)
}
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.validLayout = layout
let controllerFrame = CGRect(origin: CGPoint(), size: layout.size)
self.controller.defaultFrame = controllerFrame
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition)
self.addControllerIfNeeded()
if case .immediate = transition {
self.controller.view.frame = controllerFrame
} else {
UIView.animate(withDuration: 0.3, animations: {
self.controller.view.frame = controllerFrame
})
}
}
private func activateLocalization(_ code: String) {
let currentCode = self.accountManager.transaction { transaction -> String in
if let current = transaction.getSharedData(SharedDataKeys.localizationSettings)?.get(LocalizationSettings.self) {
return current.primaryComponent.languageCode
} else {
return "en"
}
}
let suggestedCode = self.suggestedLocalization.get()
|> map { localization -> String? in
return localization?.availableLocalizations.first?.languageCode
}
let _ = (combineLatest(currentCode, suggestedCode)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] currentCode, suggestedCode in
guard let strongSelf = self else {
return
}
if let suggestedCode = suggestedCode {
_ = TelegramEngineUnauthorized(account: strongSelf.account).localization.markSuggestedLocalizationAsSeenInteractively(languageCode: suggestedCode).start()
}
if currentCode == code {
strongSelf.pressNext(strings: nil)
return
}
strongSelf.controller.isEnabled = false
strongSelf.startButton.alpha = 0.6
let accountManager = strongSelf.accountManager
strongSelf.activateLocalizationDisposable.set(TelegramEngineUnauthorized(account: strongSelf.account).localization.downloadAndApplyLocalization(accountManager: accountManager, languageCode: code).start(completed: {
let _ = (accountManager.transaction { transaction -> PresentationStrings? in
let localizationSettings: LocalizationSettings?
if let current = transaction.getSharedData(SharedDataKeys.localizationSettings)?.get(LocalizationSettings.self) {
localizationSettings = current
} else {
localizationSettings = nil
}
let stringsValue: PresentationStrings
if let localizationSettings = localizationSettings {
stringsValue = PresentationStrings(primaryComponent: PresentationStrings.Component(languageCode: localizationSettings.primaryComponent.languageCode, localizedName: localizationSettings.primaryComponent.localizedName, pluralizationRulesCode: localizationSettings.primaryComponent.customPluralizationCode, dict: dictFromLocalization(localizationSettings.primaryComponent.localization)), secondaryComponent: localizationSettings.secondaryComponent.flatMap({ PresentationStrings.Component(languageCode: $0.languageCode, localizedName: $0.localizedName, pluralizationRulesCode: $0.customPluralizationCode, dict: dictFromLocalization($0.localization)) }), groupingSeparator: "")
} else {
stringsValue = defaultPresentationStrings
}
return stringsValue
}
|> deliverOnMainQueue).start(next: { strings in
self?.controller.isEnabled = true
self?.startButton.alpha = 1.0
self?.pressNext(strings: strings)
})
}))
})
}
private func pressNext(strings: PresentationStrings?) {
if let navigationController = self.navigationController, navigationController.viewControllers.last === self {
self.nextPressed?(strings)
}
}
}
@@ -0,0 +1,21 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
final class AuthorizationSequenceSplashControllerNode: ASDisplayNode {
init(theme: PresentationTheme) {
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.backgroundColor = theme.list.plainBackgroundColor
self.view.disablesInteractiveTransitionGestureRecognizer = true
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
}
}