import Foundation import UIKit import AppBundle import AccountContext import SwiftSignalKit import TelegramPresentationData import AsyncDisplayKit import Display import Postbox import TelegramCore import SolidRoundedButtonNode import UndoUI import AlertUI import AnimationUI public enum WalletWordDisplayScreenMode { case check case export } public final class WalletWordDisplayScreen: ViewController { private let context: AccountContext private let tonContext: TonContext private var presentationData: PresentationData private let walletInfo: WalletInfo private let wordList: [String] private let mode: WalletWordDisplayScreenMode private let startTime: Double private let idleTimerExtensionDisposable: Disposable public init(context: AccountContext, tonContext: TonContext, walletInfo: WalletInfo, wordList: [String], mode: WalletWordDisplayScreenMode) { self.context = context self.tonContext = tonContext self.walletInfo = walletInfo self.wordList = wordList self.mode = mode self.presentationData = context.sharedContext.currentPresentationData.with { $0 } let defaultNavigationPresentationData = NavigationBarPresentationData(presentationTheme: self.presentationData.theme, presentationStrings: self.presentationData.strings) let navigationBarTheme = NavigationBarTheme(buttonColor: defaultNavigationPresentationData.theme.buttonColor, disabledButtonColor: defaultNavigationPresentationData.theme.disabledButtonColor, primaryTextColor: defaultNavigationPresentationData.theme.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: defaultNavigationPresentationData.theme.badgeBackgroundColor, badgeStrokeColor: defaultNavigationPresentationData.theme.badgeStrokeColor, badgeTextColor: defaultNavigationPresentationData.theme.badgeTextColor) self.startTime = Date().timeIntervalSince1970 self.idleTimerExtensionDisposable = context.sharedContext.applicationBindings.pushIdleTimerExtension() super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: defaultNavigationPresentationData.strings)) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.navigationPresentation = .modalInLargeLayout self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.navigationBar?.intrinsicCanTransitionInline = false self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.idleTimerExtensionDisposable.dispose() } @objc private func backPressed() { self.dismiss() } override public func loadDisplayNode() { self.displayNode = WalletWordDisplayScreenNode(account: self.context.account, presentationData: self.presentationData, wordList: self.wordList, action: { [weak self] in guard let strongSelf = self else { return } if case .export = strongSelf.mode { strongSelf.dismiss() return } let deltaTime = Date().timeIntervalSince1970 - strongSelf.startTime let minimalTimeout: Double #if DEBUG minimalTimeout = 1.0 #else minimalTimeout = 60.0 #endif if deltaTime < minimalTimeout { strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: "Sure Done?", text: "You didn't have enough time to write those words down.", actions: [TextAlertAction(type: .defaultAction, title: "OK, Sorry", action: { guard let strongSelf = self else { return } if let path = getAppBundle().path(forResource: "thumbsup", ofType: "tgs") { strongSelf.present(UndoOverlayController(context: strongSelf.context, content: UndoOverlayContent.emoji(account: strongSelf.context.account, path: path, text: "Apologies Accepted"), elevatedLayout: false, animateInAsReplacement: false, action: { _ in }), in: .current) } })]), in: .window(.root)) } else { var wordIndices: [Int] = [] while wordIndices.count < 3 { let index = Int(arc4random_uniform(UInt32(strongSelf.wordList.count))) if !wordIndices.contains(index) { wordIndices.append(index) } } wordIndices.sort() strongSelf.push(WalletWordCheckScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, mode: .verify(strongSelf.walletInfo, strongSelf.wordList, wordIndices))) } }) self.displayNodeDidLoad() } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) (self.displayNode as! WalletWordDisplayScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationHeight, transition: transition) } } private final class WalletWordDisplayScreenNode: ViewControllerTracingNode, UIScrollViewDelegate { private var presentationData: PresentationData private let wordList: [String] private let action: () -> Void private let navigationBackgroundNode: ASDisplayNode private let navigationSeparatorNode: ASDisplayNode private let navigationTitleNode: ImmediateTextNode private let scrollNode: ASScrollNode private let animationNode: AnimatedStickerNode private let titleNode: ImmediateTextNode private let textNode: ImmediateTextNode private let wordNodes: [(ImmediateTextNode, ImmediateTextNode, ImmediateTextNode)] private let buttonNode: SolidRoundedButtonNode private var navigationHeight: CGFloat? init(account: Account, presentationData: PresentationData, wordList: [String], action: @escaping () -> Void) { self.presentationData = presentationData self.wordList = wordList self.action = action self.navigationBackgroundNode = ASDisplayNode() self.navigationBackgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor self.navigationBackgroundNode.alpha = 0.0 self.navigationSeparatorNode = ASDisplayNode() self.navigationSeparatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor self.scrollNode = ASScrollNode() self.animationNode = AnimatedStickerNode() if let path = getAppBundle().path(forResource: "WalletWordList", ofType: "tgs") { self.animationNode.setup(account: account, resource: .localFile(path), width: 264, height: 264, playbackMode: .once, mode: .direct) self.animationNode.visibility = true } let title: String = "24 Secret Words" let text: String = "Write down these 24 words in the correct order and store them in a secret place.\n\nUse these secret words to restore access to your wallet if you lose your passcode or Telegram account." let buttonText: String = "Done" self.titleNode = ImmediateTextNode() self.titleNode.displaysAsynchronously = false self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(32.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) self.titleNode.maximumNumberOfLines = 0 self.titleNode.textAlignment = .center self.navigationTitleNode = ImmediateTextNode() self.navigationTitleNode.displaysAsynchronously = false self.navigationTitleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) self.navigationTitleNode.maximumNumberOfLines = 0 self.navigationTitleNode.textAlignment = .center self.textNode = ImmediateTextNode() self.textNode.displaysAsynchronously = false self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) self.textNode.maximumNumberOfLines = 0 self.textNode.lineSpacing = 0.1 self.textNode.textAlignment = .center var wordNodes: [(ImmediateTextNode, ImmediateTextNode, ImmediateTextNode)] = [] for i in 0 ..< wordList.count { let indexNode = ImmediateTextNode() indexNode.displaysAsynchronously = false indexNode.attributedText = NSAttributedString(string: "\(i + 1)", font: Font.regular(18.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) indexNode.maximumNumberOfLines = 1 indexNode.textAlignment = .left let indexDotNode = ImmediateTextNode() indexDotNode.displaysAsynchronously = false indexDotNode.attributedText = NSAttributedString(string: ".", font: Font.regular(18.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) indexDotNode.maximumNumberOfLines = 1 indexDotNode.textAlignment = .left let wordNode = ImmediateTextNode() wordNode.displaysAsynchronously = false wordNode.attributedText = NSAttributedString(string: wordList[i], font: Font.semibold(18.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) wordNode.maximumNumberOfLines = 1 wordNode.textAlignment = .left wordNodes.append((indexNode, indexDotNode, wordNode)) } self.wordNodes = wordNodes self.buttonNode = SolidRoundedButtonNode(title: buttonText, theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 50.0, cornerRadius: 10.0, gloss: false) super.init() self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.addSubnode(self.scrollNode) self.scrollNode.addSubnode(self.animationNode) self.scrollNode.addSubnode(self.titleNode) self.scrollNode.addSubnode(self.textNode) self.scrollNode.addSubnode(self.buttonNode) for (indexNode, indexDotNode, wordNode) in self.wordNodes { self.scrollNode.addSubnode(indexNode) self.scrollNode.addSubnode(indexDotNode) self.scrollNode.addSubnode(wordNode) } self.navigationBackgroundNode.addSubnode(self.navigationSeparatorNode) self.navigationBackgroundNode.addSubnode(self.navigationTitleNode) self.addSubnode(self.navigationBackgroundNode) self.buttonNode.pressed = { action() } } override func didLoad() { super.didLoad() self.scrollNode.view.alwaysBounceVertical = false self.scrollNode.view.showsVerticalScrollIndicator = false self.scrollNode.view.showsHorizontalScrollIndicator = false if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.scrollNode.view.contentInsetAdjustmentBehavior = .never } self.scrollNode.view.delegate = self #if DEBUG self.textNode.view.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(self.textLongPressGesture(_:)))) #endif } @objc func textLongPressGesture(_ recognizer: UILongPressGestureRecognizer) { if case .began = recognizer.state { UIPasteboard.general.string = self.wordList.joined(separator: "\n") } } func scrollViewDidScroll(_ scrollView: UIScrollView) { let navigationHeight = self.navigationHeight ?? 0.0 let alpha: CGFloat = scrollView.contentOffset.y >= (self.titleNode.frame.maxY - navigationHeight) ? 1.0 : 0.0 if self.navigationBackgroundNode.alpha != alpha { let transition: ContainedViewLayoutTransition = .animated(duration: 0.12, curve: .easeInOut) transition.updateAlpha(node: self.navigationBackgroundNode, alpha: alpha, beginWithCurrentState: true) } } func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.navigationHeight = navigationHeight let sideInset: CGFloat = 32.0 let buttonSideInset: CGFloat = 48.0 let iconSpacing: CGFloat = 18.0 let titleSpacing: CGFloat = 19.0 let textSpacing: CGFloat = 37.0 let buttonHeight: CGFloat = 50.0 let buttonSpacing: CGFloat = 45.0 let wordSpacing: CGFloat = 12.0 let indexSpacing: CGFloat = 4.0 transition.updateFrame(node: self.navigationBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: navigationHeight))) transition.updateFrame(node: self.navigationSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) let iconSize = CGSize(width: 132.0, height: 132.0) self.animationNode.updateLayout(size: iconSize) let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height)) let navigationTitleSize = self.navigationTitleNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height)) let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height)) var contentHeight: CGFloat = 0.0 let contentVerticalOrigin = navigationHeight + 10.0 let iconFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0) + 12.0, y: contentVerticalOrigin), size: iconSize) transition.updateFrameAdditive(node: self.animationNode, frame: iconFrame) let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: iconFrame.maxY + iconSpacing), size: titleSize) transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame) let textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: titleFrame.maxY + titleSpacing), size: textSize) transition.updateFrameAdditive(node: self.textNode, frame: textFrame) transition.updateFrameAdditive(node: self.navigationTitleNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - navigationTitleSize.width) / 2.0), y: navigationHeight - 44.0 + floor((44.0 - navigationTitleSize.height) / 2.0)), size: navigationTitleSize)) contentHeight = textFrame.maxY + textSpacing let rowWidth = layout.size.width - buttonSideInset * 2.0 let rowCount = self.wordNodes.count / 2 let indexWidth: CGFloat = 16.0 var wordSizes: [(CGSize, CGSize)] = [] var columnIndexWidth: [CGFloat] = [0.0, 0.0] var columnWordWidth: [CGFloat] = [0.0, 0.0] var dotSize: CGSize = CGSize() for i in 0 ..< self.wordNodes.count { let indexSize = self.wordNodes[i].0.updateLayout(CGSize(width: 200.0, height: 100.0)) dotSize = self.wordNodes[i].1.updateLayout(CGSize(width: 200.0, height: 100.0)) let wordSize = self.wordNodes[i].2.updateLayout(CGSize(width: 200.0, height: 100.0)) wordSizes.append((indexSize, wordSize)) let column = i / rowCount columnIndexWidth[column] = max(columnIndexWidth[column], indexSize.width) columnWordWidth[column] = max(columnWordWidth[column], wordSize.width) } for column in 0 ..< 2 { var columnHeight: CGFloat = 0.0 for i in 0 ..< self.wordNodes.count { if !columnHeight.isZero { columnHeight += wordSpacing } if i / rowCount != column { continue } let horizontalOrigin: CGFloat let verticalOrigin: CGFloat = contentHeight + columnHeight if column == 0 { horizontalOrigin = buttonSideInset + columnIndexWidth[column] } else { horizontalOrigin = layout.size.width - buttonSideInset - columnWordWidth[column] - indexSpacing } let indexSize = self.wordNodes[i].0.updateLayout(CGSize(width: 200.0, height: 100.0)) let wordSize = self.wordNodes[i].2.updateLayout(CGSize(width: 200.0, height: 100.0)) transition.updateFrameAdditive(node: self.wordNodes[i].0, frame: CGRect(origin: CGPoint(x: horizontalOrigin - indexSize.width - dotSize.width, y: verticalOrigin), size: indexSize)) transition.updateFrameAdditive(node: self.wordNodes[i].1, frame: CGRect(origin: CGPoint(x: horizontalOrigin - dotSize.width, y: verticalOrigin), size: indexSize)) transition.updateFrameAdditive(node: self.wordNodes[i].2, frame: CGRect(origin: CGPoint(x: horizontalOrigin + indexSpacing, y: verticalOrigin), size: wordSize)) columnHeight += wordSize.height } if column == 1 { contentHeight += columnHeight } } let minimalFullscreenBottomInset: CGFloat = 74.0 let minimalScrollBottomInset: CGFloat = 30.0 let fullscreenBottomInset = layout.intrinsicInsets.bottom + minimalFullscreenBottomInset let scrollBottomInset = layout.intrinsicInsets.bottom + minimalScrollBottomInset let buttonWidth = layout.size.width - buttonSideInset * 2.0 let buttonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonWidth) / 2.0), y: max(contentHeight + buttonSpacing, layout.size.height - scrollBottomInset - buttonHeight)), size: CGSize(width: buttonWidth, height: buttonHeight)) transition.updateFrame(node: self.buttonNode, frame: buttonFrame) self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition) self.scrollNode.view.contentSize = CGSize(width: layout.size.width, height: max(layout.size.height, buttonFrame.maxY + scrollBottomInset)) } }