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) -> 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 let font = Font.with(size: 20.0, design: .monospace, traits: []) 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: []) strongSelf.phoneInputNode.mask = AuthorizationSequenceCountrySelectionController.lookupPatternByCode(code).flatMap { NSAttributedString(string: $0, font: font, textColor: theme.list.itemPlaceholderTextColor) } } 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: []) strongSelf.phoneInputNode.mask = AuthorizationSequenceCountrySelectionController.lookupPatternByCode(code).flatMap { NSAttributedString(string: $0, font: font, textColor: theme.list.itemPlaceholderTextColor) } } else { strongSelf.countryButton.setTitle(strings.Login_SelectCountry_Title, with: Font.regular(20.0), with: theme.list.itemPlaceholderTextColor, for: []) strongSelf.phoneInputNode.mask = nil } } } 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 placeholderFrame = numberFrame.offsetBy(dx: -1.0, dy: 16.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) self.phoneInputNode.placeholderNode.frame = placeholderFrame.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 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.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) } })) } }