|  |  |  | // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import UIKit | 
					
						
							|  |  |  | import AVFoundation | 
					
						
							|  |  |  | import SessionUIKit | 
					
						
							|  |  |  | import SessionUtilitiesKit | 
					
						
							|  |  |  | import SessionSnodeKit | 
					
						
							|  |  |  | import SignalUtilitiesKit | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | final class LinkDeviceVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControllerDelegate, QRScannerDelegate { | 
					
						
							|  |  |  |     private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil) | 
					
						
							|  |  |  |     private var pages: [UIViewController] = [] | 
					
						
							|  |  |  |     private var targetVCIndex: Int? | 
					
						
							|  |  |  |     private var tabBarTopConstraint: NSLayoutConstraint! | 
					
						
							|  |  |  |     private var activityIndicatorModal: ModalActivityIndicatorViewController? | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: - Components | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private lazy var tabBar: TabBar = { | 
					
						
							|  |  |  |         let tabs = [ | 
					
						
							|  |  |  |             TabBar.Tab(title: "vc_link_device_recovery_phrase_tab_title".localized()) { [weak self] in | 
					
						
							|  |  |  |                 guard let self = self else { return } | 
					
						
							|  |  |  |                 self.pageVC.setViewControllers([ self.pages[0] ], direction: .forward, animated: false, completion: nil) | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |             TabBar.Tab(title: "vc_link_device_scan_qr_code_tab_title".localized()) { [weak self] in | 
					
						
							|  |  |  |                 guard let self = self else { return } | 
					
						
							|  |  |  |                 self.pageVC.setViewControllers([ self.pages[1] ], direction: .forward, animated: false, completion: nil) | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         ] | 
					
						
							|  |  |  |         return TabBar(tabs: tabs) | 
					
						
							|  |  |  |     }() | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private lazy var recoveryPhraseVC: RecoveryPhraseVC = { | 
					
						
							|  |  |  |         let result = RecoveryPhraseVC() | 
					
						
							|  |  |  |         result.linkDeviceVC = self | 
					
						
							|  |  |  |         return result | 
					
						
							|  |  |  |     }() | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private lazy var scanQRCodePlaceholderVC: ScanQRCodePlaceholderVC = { | 
					
						
							|  |  |  |         let result = ScanQRCodePlaceholderVC() | 
					
						
							|  |  |  |         result.linkDeviceVC = self | 
					
						
							|  |  |  |         return result | 
					
						
							|  |  |  |     }() | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private lazy var scanQRCodeWrapperVC: ScanQRCodeWrapperVC = { | 
					
						
							|  |  |  |         let message = "vc_link_device_scan_qr_code_explanation".localized() | 
					
						
							|  |  |  |         let result = ScanQRCodeWrapperVC(message: message) | 
					
						
							|  |  |  |         result.delegate = self | 
					
						
							|  |  |  |         return result | 
					
						
							|  |  |  |     }() | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: - Lifecycle | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     override func viewDidLoad() { | 
					
						
							|  |  |  |         super.viewDidLoad() | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         setNavBarTitle("vc_link_device_title".localized()) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Page VC | 
					
						
							|  |  |  |         let hasCameraAccess = (AVCaptureDevice.authorizationStatus(for: .video) == .authorized) | 
					
						
							|  |  |  |         pages = [ recoveryPhraseVC, (hasCameraAccess ? scanQRCodeWrapperVC : scanQRCodePlaceholderVC) ] | 
					
						
							|  |  |  |         pageVC.dataSource = self | 
					
						
							|  |  |  |         pageVC.delegate = self | 
					
						
							|  |  |  |         pageVC.setViewControllers([ recoveryPhraseVC ], direction: .forward, animated: false, completion: nil) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Tab bar | 
					
						
							|  |  |  |         view.addSubview(tabBar) | 
					
						
							|  |  |  |         tabBar.pin(.leading, to: .leading, of: view) | 
					
						
							|  |  |  |         tabBarTopConstraint = tabBar.autoPinEdge(toSuperviewSafeArea: .top) | 
					
						
							|  |  |  |         view.pin(.trailing, to: .trailing, of: tabBar) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Page VC constraints | 
					
						
							|  |  |  |         let pageVCView = pageVC.view! | 
					
						
							|  |  |  |         view.addSubview(pageVCView) | 
					
						
							|  |  |  |         pageVCView.pin(.leading, to: .leading, of: view) | 
					
						
							|  |  |  |         pageVCView.pin(.top, to: .bottom, of: tabBar) | 
					
						
							|  |  |  |         view.pin(.trailing, to: .trailing, of: pageVCView) | 
					
						
							|  |  |  |         view.pin(.bottom, to: .bottom, of: pageVCView) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         let screen = UIScreen.main.bounds | 
					
						
							|  |  |  |         pageVCView.set(.width, to: screen.width) | 
					
						
							|  |  |  |         let statusBarHeight = UIApplication.shared.statusBarFrame.height | 
					
						
							|  |  |  |         let height = (navigationController?.view.bounds.height ?? 0) - (navigationController?.navigationBar.bounds.height ?? 0) - TabBar.snHeight - statusBarHeight | 
					
						
							|  |  |  |         pageVCView.set(.height, to: height) | 
					
						
							|  |  |  |         recoveryPhraseVC.constrainHeight(to: height) | 
					
						
							|  |  |  |         scanQRCodePlaceholderVC.constrainHeight(to: height) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     override func viewDidAppear(_ animated: Bool) { | 
					
						
							|  |  |  |         super.viewDidAppear(animated) | 
					
						
							|  |  |  |         tabBarTopConstraint.constant = navigationController!.navigationBar.height() | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: - General | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { | 
					
						
							|  |  |  |         guard let index = pages.firstIndex(of: viewController), index != 0 else { return nil } | 
					
						
							|  |  |  |         return pages[index - 1] | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { | 
					
						
							|  |  |  |         guard let index = pages.firstIndex(of: viewController), index != (pages.count - 1) else { return nil } | 
					
						
							|  |  |  |         return pages[index + 1] | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     fileprivate func handleCameraAccessGranted() { | 
					
						
							|  |  |  |         DispatchQueue.main.async { | 
					
						
							|  |  |  |             self.pages[1] = self.scanQRCodeWrapperVC | 
					
						
							|  |  |  |             self.pageVC.setViewControllers([ self.scanQRCodeWrapperVC ], direction: .forward, animated: false, completion: nil) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: - Updating | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { | 
					
						
							|  |  |  |         guard let targetVC = pendingViewControllers.first, let index = pages.firstIndex(of: targetVC) else { return } | 
					
						
							|  |  |  |         targetVCIndex = index | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating isFinished: Bool, previousViewControllers: [UIViewController], transitionCompleted isCompleted: Bool) { | 
					
						
							|  |  |  |         guard isCompleted, let index = targetVCIndex else { return } | 
					
						
							|  |  |  |         tabBar.selectTab(at: index) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: - Interaction | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     @objc private func close() { | 
					
						
							|  |  |  |         dismiss(animated: true, completion: nil) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String) { | 
					
						
							|  |  |  |         let seed = Data(hex: string) | 
					
						
							|  |  |  |         continueWithSeed(seed) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     func continueWithSeed(_ seed: Data) { | 
					
						
							|  |  |  |         if (seed.count != 16) { | 
					
						
							|  |  |  |             let modal: ConfirmationModal = ConfirmationModal( | 
					
						
							|  |  |  |                 info: ConfirmationModal.Info( | 
					
						
							|  |  |  |                     title: "invalid_recovery_phrase".localized(), | 
					
						
							|  |  |  |                     explanation: "INVALID_RECOVERY_PHRASE_MESSAGE".localized(), | 
					
						
							|  |  |  |                     cancelTitle: "BUTTON_OK".localized(), | 
					
						
							|  |  |  |                     cancelStyle: .alert_text, | 
					
						
							|  |  |  |                     afterClosed: { [weak self] in | 
					
						
							|  |  |  |                         self?.scanQRCodeWrapperVC.startCapture() | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                 ) | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |             present(modal, animated: true) | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         let (ed25519KeyPair, x25519KeyPair) = try! Identity.generate(from: seed) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         Onboarding.Flow.link | 
					
						
							|  |  |  |             .preregister( | 
					
						
							|  |  |  |                 with: seed, | 
					
						
							|  |  |  |                 ed25519KeyPair: ed25519KeyPair, | 
					
						
							|  |  |  |                 x25519KeyPair: x25519KeyPair | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |             // Otherwise continue on to request push notifications permissions | 
					
						
							|  |  |  |             let pnModeVC: PNModeVC = PNModeVC(flow: .link) | 
					
						
							|  |  |  |             self.navigationController?.pushViewController(pnModeVC, animated: true) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | private final class RecoveryPhraseVC: UIViewController { | 
					
						
							|  |  |  |     weak var linkDeviceVC: LinkDeviceVC! | 
					
						
							|  |  |  |     private var spacer1HeightConstraint: NSLayoutConstraint! | 
					
						
							|  |  |  |     private var spacer2HeightConstraint: NSLayoutConstraint! | 
					
						
							|  |  |  |     private var restoreButtonBottomOffsetConstraint: NSLayoutConstraint! | 
					
						
							|  |  |  |     private var bottomConstraint: NSLayoutConstraint! | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private lazy var mnemonicTextView: TextView = { | 
					
						
							|  |  |  |         let result = TextView(placeholder: "vc_restore_seed_text_field_hint".localized()) | 
					
						
							|  |  |  |         result.themeBorderColor = .textPrimary | 
					
						
							|  |  |  |         result.accessibilityLabel = "Enter your recovery phrase" | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         return result | 
					
						
							|  |  |  |     }() | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: - Lifecycle | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     override func viewDidLoad() { | 
					
						
							|  |  |  |         view.themeBackgroundColor = .clear | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Title label | 
					
						
							|  |  |  |         let titleLabel = UILabel() | 
					
						
							|  |  |  |         titleLabel.font = .boldSystemFont(ofSize: isIPhone5OrSmaller ? Values.largeFontSize : Values.veryLargeFontSize) | 
					
						
							|  |  |  |         titleLabel.text = "vc_enter_recovery_phrase_title".localized() | 
					
						
							|  |  |  |         titleLabel.themeTextColor = .textPrimary | 
					
						
							|  |  |  |         titleLabel.lineBreakMode = .byWordWrapping | 
					
						
							|  |  |  |         titleLabel.numberOfLines = 0 | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Explanation label | 
					
						
							|  |  |  |         let explanationLabel = UILabel() | 
					
						
							|  |  |  |         explanationLabel.font = .systemFont(ofSize: Values.smallFontSize) | 
					
						
							|  |  |  |         explanationLabel.text = "vc_enter_recovery_phrase_explanation".localized() | 
					
						
							|  |  |  |         explanationLabel.themeTextColor = .textPrimary | 
					
						
							|  |  |  |         explanationLabel.lineBreakMode = .byWordWrapping | 
					
						
							|  |  |  |         explanationLabel.numberOfLines = 0 | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Spacers | 
					
						
							|  |  |  |         let topSpacer = UIView.vStretchingSpacer() | 
					
						
							|  |  |  |         let spacer1 = UIView() | 
					
						
							|  |  |  |         spacer1HeightConstraint = spacer1.set(.height, to: isIPhone5OrSmaller ? Values.smallSpacing : Values.veryLargeSpacing) | 
					
						
							|  |  |  |         let spacer2 = UIView() | 
					
						
							|  |  |  |         spacer2HeightConstraint = spacer2.set(.height, to: isIPhone5OrSmaller ? Values.smallSpacing : Values.veryLargeSpacing) | 
					
						
							|  |  |  |         let bottomSpacer = UIView.vStretchingSpacer() | 
					
						
							|  |  |  |         let restoreButtonBottomOffsetSpacer = UIView() | 
					
						
							|  |  |  |         restoreButtonBottomOffsetConstraint = restoreButtonBottomOffsetSpacer.set(.height, to: Values.onboardingButtonBottomOffset) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Continue button | 
					
						
							|  |  |  |         let continueButton = SessionButton(style: .filled, size: .large) | 
					
						
							|  |  |  |         continueButton.accessibilityLabel = "Link device" | 
					
						
							|  |  |  |         continueButton.setTitle("continue_2".localized(), for: UIControl.State.normal) | 
					
						
							|  |  |  |         continueButton.addTarget(self, action: #selector(handleContinueButtonTapped), for: UIControl.Event.touchUpInside) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Continue button container | 
					
						
							|  |  |  |         let continueButtonContainer = UIView(wrapping: continueButton, withInsets: UIEdgeInsets(top: 0, leading: Values.massiveSpacing, bottom: 0, trailing: Values.massiveSpacing), shouldAdaptForIPadWithWidth: Values.iPadButtonWidth) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Top stack view | 
					
						
							|  |  |  |         let topStackView = UIStackView(arrangedSubviews: [ titleLabel, spacer1, explanationLabel, spacer2, mnemonicTextView ]) | 
					
						
							|  |  |  |         topStackView.axis = .vertical | 
					
						
							|  |  |  |         topStackView.alignment = .fill | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Top stack view container | 
					
						
							|  |  |  |         let topStackViewContainer = UIView() | 
					
						
							|  |  |  |         topStackViewContainer.addSubview(topStackView) | 
					
						
							|  |  |  |         topStackView.pin(.leading, to: .leading, of: topStackViewContainer, withInset: Values.veryLargeSpacing) | 
					
						
							|  |  |  |         topStackView.pin(.top, to: .top, of: topStackViewContainer) | 
					
						
							|  |  |  |         topStackViewContainer.pin(.trailing, to: .trailing, of: topStackView, withInset: Values.veryLargeSpacing) | 
					
						
							|  |  |  |         topStackViewContainer.pin(.bottom, to: .bottom, of: topStackView) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Main stack view | 
					
						
							|  |  |  |         let mainStackView = UIStackView(arrangedSubviews: [ topSpacer, topStackViewContainer, bottomSpacer, continueButtonContainer, restoreButtonBottomOffsetSpacer ]) | 
					
						
							|  |  |  |         mainStackView.axis = .vertical | 
					
						
							|  |  |  |         mainStackView.alignment = .fill | 
					
						
							|  |  |  |         view.addSubview(mainStackView) | 
					
						
							|  |  |  |         mainStackView.pin(.leading, to: .leading, of: view) | 
					
						
							|  |  |  |         mainStackView.pin(.top, to: .top, of: view) | 
					
						
							|  |  |  |         mainStackView.pin(.trailing, to: .trailing, of: view) | 
					
						
							|  |  |  |         bottomConstraint = mainStackView.pin(.bottom, to: .bottom, of: view) | 
					
						
							|  |  |  |         topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor, multiplier: 1).isActive = true | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Dismiss keyboard on tap | 
					
						
							|  |  |  |         let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) | 
					
						
							|  |  |  |         view.addGestureRecognizer(tapGestureRecognizer) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Listen to keyboard notifications | 
					
						
							|  |  |  |         let notificationCenter = NotificationCenter.default | 
					
						
							|  |  |  |         notificationCenter.addObserver(self, selector: #selector(handleKeyboardWillChangeFrameNotification(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) | 
					
						
							|  |  |  |         notificationCenter.addObserver(self, selector: #selector(handleKeyboardWillHideNotification(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Set up width constraint | 
					
						
							|  |  |  |         view.set(.width, to: UIScreen.main.bounds.width) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     deinit { | 
					
						
							|  |  |  |         NotificationCenter.default.removeObserver(self) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: - General | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     func constrainHeight(to height: CGFloat) { | 
					
						
							|  |  |  |         view.set(.height, to: height) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     @objc private func dismissKeyboard() { | 
					
						
							|  |  |  |         mnemonicTextView.resignFirstResponder() | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: - Updating | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     @objc private func handleKeyboardWillChangeFrameNotification(_ notification: Notification) { | 
					
						
							|  |  |  |         guard let newHeight = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size.height else { return } | 
					
						
							|  |  |  |         bottomConstraint.constant = -newHeight // Negative due to how the constraint is set up | 
					
						
							|  |  |  |         restoreButtonBottomOffsetConstraint.constant = isIPhone6OrSmaller ? Values.smallSpacing : Values.largeSpacing | 
					
						
							|  |  |  |         spacer1HeightConstraint.constant = isIPhone6OrSmaller ? Values.smallSpacing : Values.mediumSpacing | 
					
						
							|  |  |  |         spacer2HeightConstraint.constant = isIPhone6OrSmaller ? Values.smallSpacing : Values.mediumSpacing | 
					
						
							|  |  |  |         UIView.animate(withDuration: 0.25) { | 
					
						
							|  |  |  |             self.view.layoutIfNeeded() | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     @objc private func handleKeyboardWillHideNotification(_ notification: Notification) { | 
					
						
							|  |  |  |         bottomConstraint.constant = 0 | 
					
						
							|  |  |  |         restoreButtonBottomOffsetConstraint.constant = Values.onboardingButtonBottomOffset | 
					
						
							|  |  |  |         spacer1HeightConstraint.constant = isIPhone5OrSmaller ? Values.smallSpacing : Values.veryLargeSpacing | 
					
						
							|  |  |  |         spacer2HeightConstraint.constant = isIPhone5OrSmaller ? Values.smallSpacing : Values.veryLargeSpacing | 
					
						
							|  |  |  |         UIView.animate(withDuration: 0.25) { | 
					
						
							|  |  |  |             self.view.layoutIfNeeded() | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: - Interaction | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     @objc private func handleContinueButtonTapped() { | 
					
						
							|  |  |  |         func showError(title: String, message: String = "") { | 
					
						
							|  |  |  |             let modal: ConfirmationModal = ConfirmationModal( | 
					
						
							|  |  |  |                 targetView: self.view, | 
					
						
							|  |  |  |                 info: ConfirmationModal.Info( | 
					
						
							|  |  |  |                     title: title, | 
					
						
							|  |  |  |                     explanation: message, | 
					
						
							|  |  |  |                     cancelTitle: "BUTTON_OK".localized(), | 
					
						
							|  |  |  |                     cancelStyle: .alert_text | 
					
						
							|  |  |  |                 ) | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |             self.present(modal, animated: true) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         let mnemonic = mnemonicTextView.text!.lowercased() | 
					
						
							|  |  |  |         do { | 
					
						
							|  |  |  |             let hexEncodedSeed = try Mnemonic.decode(mnemonic: mnemonic) | 
					
						
							|  |  |  |             let seed = Data(hex: hexEncodedSeed) | 
					
						
							|  |  |  |             mnemonicTextView.resignFirstResponder() | 
					
						
							|  |  |  |             linkDeviceVC.continueWithSeed(seed) | 
					
						
							|  |  |  |         } catch let error { | 
					
						
							|  |  |  |             let error = error as? Mnemonic.DecodingError ?? Mnemonic.DecodingError.generic | 
					
						
							|  |  |  |             showError(title: error.errorDescription!) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | private final class ScanQRCodePlaceholderVC: UIViewController { | 
					
						
							|  |  |  |     weak var linkDeviceVC: LinkDeviceVC! | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     override func viewDidLoad() { | 
					
						
							|  |  |  |         // Remove background color | 
					
						
							|  |  |  |         view.themeBackgroundColor = .clear | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Set up explanation label | 
					
						
							|  |  |  |         let explanationLabel = UILabel() | 
					
						
							|  |  |  |         explanationLabel.font = .systemFont(ofSize: Values.smallFontSize) | 
					
						
							|  |  |  |         explanationLabel.text = "vc_scan_qr_code_camera_access_explanation".localized() | 
					
						
							|  |  |  |         explanationLabel.themeTextColor = .textPrimary | 
					
						
							|  |  |  |         explanationLabel.textAlignment = .center | 
					
						
							|  |  |  |         explanationLabel.lineBreakMode = .byWordWrapping | 
					
						
							|  |  |  |         explanationLabel.numberOfLines = 0 | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Set up call to action button | 
					
						
							|  |  |  |         let callToActionButton = UIButton() | 
					
						
							|  |  |  |         callToActionButton.titleLabel?.font = .boldSystemFont(ofSize: Values.mediumFontSize) | 
					
						
							|  |  |  |         callToActionButton.setTitle("vc_scan_qr_code_grant_camera_access_button_title".localized(), for: .normal) | 
					
						
							|  |  |  |         callToActionButton.setThemeTitleColor(.primary, for: .normal) | 
					
						
							|  |  |  |         callToActionButton.addTarget(self, action: #selector(requestCameraAccess), for: .touchUpInside) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Set up stack view | 
					
						
							|  |  |  |         let stackView = UIStackView(arrangedSubviews: [ explanationLabel, callToActionButton ]) | 
					
						
							|  |  |  |         stackView.axis = .vertical | 
					
						
							|  |  |  |         stackView.spacing = Values.mediumSpacing | 
					
						
							|  |  |  |         stackView.alignment = .center | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Set up constraints | 
					
						
							|  |  |  |         view.set(.width, to: UIScreen.main.bounds.width) | 
					
						
							|  |  |  |         view.addSubview(stackView) | 
					
						
							|  |  |  |         stackView.pin(.leading, to: .leading, of: view, withInset: Values.massiveSpacing) | 
					
						
							|  |  |  |         view.pin(.trailing, to: .trailing, of: stackView, withInset: Values.massiveSpacing) | 
					
						
							|  |  |  |         let verticalCenteringConstraint = stackView.center(.vertical, in: view) | 
					
						
							|  |  |  |         verticalCenteringConstraint.constant = -16 // Makes things appear centered visually | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     func constrainHeight(to height: CGFloat) { | 
					
						
							|  |  |  |         view.set(.height, to: height) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     @objc private func requestCameraAccess() { | 
					
						
							|  |  |  |         Permissions.requestCameraPermissionIfNeeded { [weak self] in | 
					
						
							|  |  |  |             self?.linkDeviceVC.handleCameraAccessGranted() | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } |