Swiftgram/submodules/TelegramUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift
Peter Iakovlev e9a4a9347a Revert "Rename directories [skip ci]"
This reverts commit 789438a27450dcbdee6065ebf096198ed3b90fec
2020-03-01 10:06:51 +00:00

464 lines
23 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import SyncCore
import TelegramPresentationData
import PhoneInputNode
import CountrySelectionUI
import AuthorizationUI
import QrCode
import SwiftSignalKit
import Postbox
import AccountContext
private func emojiFlagForISOCountryCode(_ countryCode: NSString) -> String {
if countryCode.length != 2 {
return ""
}
let base: UInt32 = 127462 - 65
let first: UInt32 = base + UInt32(countryCode.character(at: 0))
let second: UInt32 = base + UInt32(countryCode.character(at: 1))
var data = Data()
data.count = 8
data.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt32>) -> Void in
bytes[0] = first
bytes[1] = second
}
return String(data: data, encoding: String.Encoding.utf32LittleEndian) ?? ""
}
private final class PhoneAndCountryNode: ASDisplayNode {
let strings: PresentationStrings
let countryButton: ASButtonNode
let phoneBackground: ASImageNode
let phoneInputNode: PhoneInputNode
var selectCountryCode: (() -> Void)?
var checkPhone: (() -> Void)?
init(strings: PresentationStrings, theme: PresentationTheme) {
self.strings = strings
let countryButtonBackground = generateImage(CGSize(width: 61.0, height: 67.0), rotatedContext: { size, context in
let arrowSize: CGFloat = 10.0
let lineWidth = UIScreenPixel
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(theme.list.itemPlainSeparatorColor.cgColor)
context.setLineWidth(lineWidth)
context.move(to: CGPoint(x: 15.0, y: lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width, y: lineWidth / 2.0))
context.strokePath()
context.move(to: CGPoint(x: size.width, y: size.height - arrowSize - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize, y: size.height - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize - arrowSize, y: size.height - arrowSize - lineWidth / 2.0))
context.addLine(to: CGPoint(x: 15.0, y: size.height - arrowSize - lineWidth / 2.0))
context.strokePath()
})?.stretchableImage(withLeftCapWidth: 61, topCapHeight: 1)
let countryButtonHighlightedBackground = generateImage(CGSize(width: 60.0, height: 67.0), rotatedContext: { size, context in
let arrowSize: CGFloat = 10.0
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(theme.list.itemHighlightedBackgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height - arrowSize)))
context.move(to: CGPoint(x: size.width, y: size.height - arrowSize))
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize, y: size.height))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize - arrowSize, y: size.height - arrowSize))
context.closePath()
context.fillPath()
})?.stretchableImage(withLeftCapWidth: 61, topCapHeight: 2)
let phoneInputBackground = generateImage(CGSize(width: 85.0, height: 57.0), rotatedContext: { size, context in
let lineWidth = UIScreenPixel
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(theme.list.itemPlainSeparatorColor.cgColor)
context.setLineWidth(lineWidth)
context.move(to: CGPoint(x: 15.0, y: size.height - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width, y: size.height - lineWidth / 2.0))
context.strokePath()
context.move(to: CGPoint(x: size.width - 2.0 + lineWidth / 2.0, y: size.height - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - 2.0 + lineWidth / 2.0, y: 0.0))
context.strokePath()
})?.stretchableImage(withLeftCapWidth: 84, topCapHeight: 2)
self.countryButton = ASButtonNode()
self.countryButton.displaysAsynchronously = false
self.countryButton.setBackgroundImage(countryButtonBackground, for: [])
self.countryButton.titleNode.maximumNumberOfLines = 1
self.countryButton.titleNode.truncationMode = .byTruncatingTail
self.countryButton.setBackgroundImage(countryButtonHighlightedBackground, for: .highlighted)
self.phoneBackground = ASImageNode()
self.phoneBackground.image = phoneInputBackground
self.phoneBackground.displaysAsynchronously = false
self.phoneBackground.displayWithoutProcessing = true
self.phoneBackground.isLayerBacked = true
self.phoneInputNode = PhoneInputNode()
super.init()
self.addSubnode(self.phoneBackground)
self.addSubnode(self.countryButton)
self.addSubnode(self.phoneInputNode)
self.phoneInputNode.countryCodeField.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.phoneInputNode.numberField.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.phoneInputNode.countryCodeField.textField.textColor = theme.list.itemPrimaryTextColor
self.phoneInputNode.numberField.textField.textColor = theme.list.itemPrimaryTextColor
self.phoneInputNode.countryCodeField.textField.tintColor = theme.list.itemAccentColor
self.phoneInputNode.numberField.textField.tintColor = theme.list.itemAccentColor
self.phoneInputNode.countryCodeField.textField.tintColor = theme.list.itemAccentColor
self.phoneInputNode.numberField.textField.tintColor = theme.list.itemAccentColor
self.phoneInputNode.countryCodeField.textField.disableAutomaticKeyboardHandling = [.forward]
self.phoneInputNode.numberField.textField.disableAutomaticKeyboardHandling = [.forward]
self.countryButton.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 15.0, bottom: 10.0, right: 0.0)
self.countryButton.contentHorizontalAlignment = .left
self.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: strings.Login_PhonePlaceholder, font: Font.regular(20.0), textColor: theme.list.itemPlaceholderTextColor)
self.countryButton.addTarget(self, action: #selector(self.countryPressed), forControlEvents: .touchUpInside)
self.phoneInputNode.countryCodeUpdated = { [weak self] code, name in
if let strongSelf = self {
if let code = Int(code), let name = name, let countryName = countryCodeAndIdToName[CountryCodeAndId(code: code, id: name)] {
let flagString = emojiFlagForISOCountryCode(name as NSString)
let localizedName: String = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(name, strings: strongSelf.strings) ?? countryName
strongSelf.countryButton.setTitle("\(flagString) \(localizedName)", with: Font.regular(20.0), with: theme.list.itemPrimaryTextColor, for: [])
} else if let code = Int(code), let (countryId, countryName) = countryCodeToIdAndName[code] {
let flagString = emojiFlagForISOCountryCode(countryId as NSString)
let localizedName: String = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(countryId, strings: strongSelf.strings) ?? countryName
strongSelf.countryButton.setTitle("\(flagString) \(localizedName)", with: Font.regular(20.0), with: theme.list.itemPrimaryTextColor, for: [])
} else {
strongSelf.countryButton.setTitle(strings.Login_SelectCountry_Title, with: Font.regular(20.0), with: theme.list.itemPlaceholderTextColor, for: [])
}
}
}
self.phoneInputNode.number = "+1"
self.phoneInputNode.returnAction = { [weak self] in
self?.checkPhone?()
}
}
@objc func countryPressed() {
self.selectCountryCode?()
}
override func layout() {
super.layout()
let size = self.bounds.size
self.countryButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 67.0))
self.phoneBackground.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height - 57.0), size: CGSize(width: size.width, height: 57.0))
let countryCodeFrame = CGRect(origin: CGPoint(x: 18.0, y: size.height - 57.0), size: CGSize(width: 60.0, height: 57.0))
let numberFrame = CGRect(origin: CGPoint(x: 96.0, y: size.height - 57.0), size: CGSize(width: size.width - 96.0 - 8.0, height: 57.0))
let phoneInputFrame = countryCodeFrame.union(numberFrame)
self.phoneInputNode.frame = phoneInputFrame
self.phoneInputNode.countryCodeField.frame = countryCodeFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY)
self.phoneInputNode.numberField.frame = numberFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY)
}
}
private final class ContactSyncNode: ASDisplayNode {
private let titleNode: ImmediateTextNode
let switchNode: SwitchNode
init(theme: PresentationTheme, strings: PresentationStrings) {
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 1
self.titleNode.attributedText = NSAttributedString(string: strings.Privacy_ContactsSync, font: Font.regular(17.0), textColor: theme.list.itemPrimaryTextColor)
self.switchNode = SwitchNode()
self.switchNode.frameColor = theme.list.itemSwitchColors.frameColor
self.switchNode.contentColor = theme.list.itemSwitchColors.contentColor
self.switchNode.handleColor = theme.list.itemSwitchColors.handleColor
self.switchNode.isOn = true
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.switchNode)
}
func updateLayout(width: CGFloat) -> CGSize {
let switchSize = CGSize(width: 51.0, height: 31.0)
let titleSize = self.titleNode.updateLayout(CGSize(width: width - switchSize.width - 16.0 * 2.0 - 8.0, height: .greatestFiniteMagnitude))
let height: CGFloat = 40.0
self.titleNode.frame = CGRect(origin: CGPoint(x: 16.0, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
self.switchNode.frame = CGRect(origin: CGPoint(x: width - 16.0 - switchSize.width, y: floor((height - switchSize.height) / 2.0)), size: switchSize)
return CGSize(width: width, height: height)
}
}
final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
private let sharedContext: SharedAccountContext
private var account: UnauthorizedAccount
private let strings: PresentationStrings
private let theme: PresentationTheme
private let hasOtherAccounts: Bool
private let titleNode: ASTextNode
private let noticeNode: ASTextNode
private let phoneAndCountryNode: PhoneAndCountryNode
private let contactSyncNode: ContactSyncNode
private var qrNode: ASImageNode?
private let exportTokenDisposable = MetaDisposable()
private let tokenEventsDisposable = MetaDisposable()
var accountUpdated: ((UnauthorizedAccount) -> Void)?
private let debugAction: () -> Void
var currentNumber: String {
return self.phoneAndCountryNode.phoneInputNode.number
}
var codeAndNumber: (Int32?, String?, String) {
get {
return self.phoneAndCountryNode.phoneInputNode.codeAndNumber
} set(value) {
self.phoneAndCountryNode.phoneInputNode.codeAndNumber = value
}
}
var syncContacts: Bool {
get {
if self.hasOtherAccounts {
return self.contactSyncNode.switchNode.isOn
} else {
return true
}
}
}
var selectCountryCode: (() -> Void)?
var checkPhone: (() -> Void)?
var inProgress: Bool = false {
didSet {
self.phoneAndCountryNode.phoneInputNode.enableEditing = !self.inProgress
self.phoneAndCountryNode.phoneInputNode.alpha = self.inProgress ? 0.6 : 1.0
self.phoneAndCountryNode.countryButton.isEnabled = !self.inProgress
}
}
init(sharedContext: SharedAccountContext, account: UnauthorizedAccount, strings: PresentationStrings, theme: PresentationTheme, debugAction: @escaping () -> Void, hasOtherAccounts: Bool) {
self.sharedContext = sharedContext
self.account = account
self.strings = strings
self.theme = theme
self.debugAction = debugAction
self.hasOtherAccounts = hasOtherAccounts
self.titleNode = ASTextNode()
self.titleNode.isUserInteractionEnabled = true
self.titleNode.displaysAsynchronously = false
self.titleNode.attributedText = NSAttributedString(string: strings.Login_PhoneTitle, font: Font.light(30.0), textColor: theme.list.itemPrimaryTextColor)
self.noticeNode = ASTextNode()
self.noticeNode.maximumNumberOfLines = 0
self.noticeNode.isUserInteractionEnabled = true
self.noticeNode.displaysAsynchronously = false
self.noticeNode.attributedText = NSAttributedString(string: strings.Login_PhoneAndCountryHelp, font: Font.regular(16.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
self.contactSyncNode = ContactSyncNode(theme: theme, strings: strings)
self.phoneAndCountryNode = PhoneAndCountryNode(strings: strings, theme: theme)
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.backgroundColor = theme.list.plainBackgroundColor
self.addSubnode(self.titleNode)
self.addSubnode(self.noticeNode)
self.addSubnode(self.phoneAndCountryNode)
self.addSubnode(self.contactSyncNode)
self.contactSyncNode.isHidden = true
self.phoneAndCountryNode.selectCountryCode = { [weak self] in
self?.selectCountryCode?()
}
self.phoneAndCountryNode.checkPhone = { [weak self] in
self?.checkPhone?()
}
self.tokenEventsDisposable.set((account.updateLoginTokenEvents
|> deliverOnMainQueue).start(next: { [weak self] _ in
self?.refreshQrToken()
}))
}
deinit {
self.exportTokenDisposable.dispose()
self.tokenEventsDisposable.dispose()
}
override func didLoad() {
super.didLoad()
self.titleNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.debugTap(_:))))
#if DEBUG
self.noticeNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.debugQrTap(_:))))
#endif
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
var insets = layout.insets(options: [])
insets.top = navigationBarHeight
if let inputHeight = layout.inputHeight, !inputHeight.isZero {
insets.bottom += max(inputHeight, layout.standardInputHeight)
}
if max(layout.size.width, layout.size.height) > 1023.0 {
self.titleNode.attributedText = NSAttributedString(string: strings.Login_PhoneTitle, font: Font.light(40.0), textColor: self.theme.list.itemPrimaryTextColor)
} else {
self.titleNode.attributedText = NSAttributedString(string: strings.Login_PhoneTitle, 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: min(274.0, layout.size.width - 28.0), height: CGFloat.greatestFiniteMagnitude))
var items: [AuthorizationLayoutItem] = [
AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)),
AuthorizationLayoutItem(node: self.noticeNode, size: noticeSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 18.0, maxValue: 18.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)),
AuthorizationLayoutItem(node: self.phoneAndCountryNode, size: CGSize(width: layout.size.width, height: 115.0), spacingBefore: AuthorizationLayoutItemSpacing(weight: 44.0, maxValue: 44.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))
]
let contactSyncSize = self.contactSyncNode.updateLayout(width: layout.size.width)
if self.hasOtherAccounts {
self.contactSyncNode.isHidden = false
items.append(AuthorizationLayoutItem(node: self.contactSyncNode, size: contactSyncSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 16.0, maxValue: 16.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
} else {
self.contactSyncNode.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 - 10.0)), items: items, transition: transition, failIfDoesNotFit: false)
}
func activateInput() {
self.phoneAndCountryNode.phoneInputNode.numberField.textField.becomeFirstResponder()
}
func animateError() {
self.phoneAndCountryNode.phoneInputNode.countryCodeField.layer.addShakeAnimation()
self.phoneAndCountryNode.phoneInputNode.numberField.layer.addShakeAnimation()
}
private var debugTapCounter: (Double, Int) = (0.0, 0)
@objc private func debugTap(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
let timestamp = CACurrentMediaTime()
if self.debugTapCounter.0 < timestamp - 0.4 {
self.debugTapCounter.0 = timestamp
self.debugTapCounter.1 = 0
}
if self.debugTapCounter.0 >= timestamp - 0.4 {
self.debugTapCounter.0 = timestamp
self.debugTapCounter.1 += 1
}
if self.debugTapCounter.1 >= 10 {
self.debugTapCounter.1 = 0
self.debugAction()
}
}
}
@objc private func debugQrTap(_ recognizer: UITapGestureRecognizer) {
if self.qrNode == nil {
let qrNode = ASImageNode()
qrNode.frame = CGRect(origin: CGPoint(x: 16.0, y: 64.0 + 16.0), size: CGSize(width: 200.0, height: 200.0))
self.qrNode = qrNode
self.addSubnode(qrNode)
self.refreshQrToken()
}
}
private func refreshQrToken() {
let sharedContext = self.sharedContext
let account = self.account
let tokenSignal = sharedContext.activeAccounts
|> castError(ExportAuthTransferTokenError.self)
|> take(1)
|> mapToSignal { activeAccountsAndInfo -> Signal<ExportAuthTransferTokenResult, ExportAuthTransferTokenError> in
let (primary, activeAccounts, _) = activeAccountsAndInfo
var activeProductionUserIds = activeAccounts.map({ $0.1 }).filter({ !$0.testingEnvironment }).map({ $0.peerId.id })
var activeTestingUserIds = activeAccounts.map({ $0.1 }).filter({ $0.testingEnvironment }).map({ $0.peerId.id })
let allProductionUserIds = activeProductionUserIds
let allTestingUserIds = activeTestingUserIds
return exportAuthTransferToken(accountManager: sharedContext.accountManager, account: account, otherAccountUserIds: account.testingEnvironment ? allTestingUserIds : allProductionUserIds, syncContacts: true)
}
self.exportTokenDisposable.set((tokenSignal
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else {
return
}
switch result {
case let .displayToken(token):
var tokenString = token.value.base64EncodedString()
print("export token \(tokenString)")
tokenString = tokenString.replacingOccurrences(of: "+", with: "-")
tokenString = tokenString.replacingOccurrences(of: "/", with: "_")
let urlString = "tg://login?token=\(tokenString)"
let _ = (qrCode(string: urlString, color: .black, backgroundColor: .white, icon: .none)
|> deliverOnMainQueue).start(next: { _, generate in
guard let strongSelf = self else {
return
}
let context = generate(TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(width: 200.0, height: 200.0), boundingSize: CGSize(width: 200.0, height: 200.0), intrinsicInsets: UIEdgeInsets()))
if let image = context?.generateImage() {
strongSelf.qrNode?.image = image
}
})
let timestamp = Int32(Date().timeIntervalSince1970)
let timeout = max(5, token.validUntil - timestamp)
strongSelf.exportTokenDisposable.set((Signal<Never, NoError>.complete()
|> delay(Double(timeout), queue: .mainQueue())).start(completed: {
guard let strongSelf = self else {
return
}
strongSelf.refreshQrToken()
}))
case let .changeAccountAndRetry(account):
strongSelf.exportTokenDisposable.set(nil)
strongSelf.account = account
strongSelf.accountUpdated?(account)
strongSelf.tokenEventsDisposable.set((account.updateLoginTokenEvents
|> deliverOnMainQueue).start(next: { _ in
self?.refreshQrToken()
}))
strongSelf.refreshQrToken()
case .loggedIn, .passwordRequested:
strongSelf.exportTokenDisposable.set(nil)
}
}))
}
}