Implement link device screen redesign

pull/68/head
Niels Andriesse 5 years ago
parent 94f3e86e72
commit 405cd5ed25

@ -577,6 +577,8 @@
B85357BF23A1AE0800AAF6CD /* SeedReminderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85357BE23A1AE0800AAF6CD /* SeedReminderView.swift */; };
B85357C123A1B81900AAF6CD /* SeedReminderViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85357C023A1B81900AAF6CD /* SeedReminderViewDelegate.swift */; };
B85357C323A1BD1200AAF6CD /* SeedVCV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85357C223A1BD1200AAF6CD /* SeedVCV2.swift */; };
B85357C523A1F13800AAF6CD /* LinkDeviceVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85357C423A1F13800AAF6CD /* LinkDeviceVC.swift */; };
B85357C723A1FB5100AAF6CD /* LinkDeviceVCDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85357C623A1FB5100AAF6CD /* LinkDeviceVCDelegate.swift */; };
B86BD08423399ACF000F5AE3 /* Modal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08323399ACF000F5AE3 /* Modal.swift */; };
B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08523399CEF000F5AE3 /* SeedModal.swift */; };
B885D5F4233491AB00EE0D8E /* DeviceLinkingModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B885D5F3233491AB00EE0D8E /* DeviceLinkingModal.swift */; };
@ -1415,6 +1417,8 @@
B85357BE23A1AE0800AAF6CD /* SeedReminderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedReminderView.swift; sourceTree = "<group>"; };
B85357C023A1B81900AAF6CD /* SeedReminderViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedReminderViewDelegate.swift; sourceTree = "<group>"; };
B85357C223A1BD1200AAF6CD /* SeedVCV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedVCV2.swift; sourceTree = "<group>"; };
B85357C423A1F13800AAF6CD /* LinkDeviceVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkDeviceVC.swift; sourceTree = "<group>"; };
B85357C623A1FB5100AAF6CD /* LinkDeviceVCDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkDeviceVCDelegate.swift; sourceTree = "<group>"; };
B86BD08323399ACF000F5AE3 /* Modal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modal.swift; sourceTree = "<group>"; };
B86BD08523399CEF000F5AE3 /* SeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedModal.swift; sourceTree = "<group>"; };
B885D5F3233491AB00EE0D8E /* DeviceLinkingModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLinkingModal.swift; sourceTree = "<group>"; };
@ -2788,11 +2792,11 @@
B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */,
B8BB82AC238F734800BA5194 /* ProfilePictureView.swift */,
B8BB82B02390C37000BA5194 /* SearchBar.swift */,
B85357BE23A1AE0800AAF6CD /* SeedReminderView.swift */,
B85357C023A1B81900AAF6CD /* SeedReminderViewDelegate.swift */,
B8BB82B82394911B00BA5194 /* Separator.swift */,
B8CCF638239721E20091D419 /* TabBar.swift */,
B8BB82B423947F2D00BA5194 /* TextField.swift */,
B85357BE23A1AE0800AAF6CD /* SeedReminderView.swift */,
B85357C023A1B81900AAF6CD /* SeedReminderViewDelegate.swift */,
);
path = Components;
sourceTree = "<group>";
@ -2819,6 +2823,8 @@
B8BB82A4238F627000BA5194 /* HomeVC.swift */,
B8CCF63E23975CFB0091D419 /* JoinPublicChatVC.swift */,
B82B40872399EB0E00A248E7 /* LandingVC.swift */,
B85357C423A1F13800AAF6CD /* LinkDeviceVC.swift */,
B85357C623A1FB5100AAF6CD /* LinkDeviceVCDelegate.swift */,
B86BD08323399ACF000F5AE3 /* Modal.swift */,
B8CCF63623961D6D0091D419 /* NewPrivateChatVC.swift */,
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */,
@ -3966,6 +3972,7 @@
B8BB82B12390C37000BA5194 /* SearchBar.swift in Sources */,
348BB25D20A0C5530047AEC2 /* ContactShareViewHelper.swift in Sources */,
34B3F8801E8DF1700035BE1A /* InviteFlow.swift in Sources */,
B85357C523A1F13800AAF6CD /* LinkDeviceVC.swift in Sources */,
457C87B82032645C008D52D6 /* DebugUINotifications.swift in Sources */,
4C21D5D8223AC60F00EF8A77 /* PhotoCapture.swift in Sources */,
4C13C9F620E57BA30089A98B /* ColorPickerViewController.swift in Sources */,
@ -4058,6 +4065,7 @@
34D2CCDF206939B400CB1A14 /* DebugUIMessagesAction.m in Sources */,
340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */,
B85357BF23A1AE0800AAF6CD /* SeedReminderView.swift in Sources */,
B85357C723A1FB5100AAF6CD /* LinkDeviceVCDelegate.swift in Sources */,
340FC8C5204DE223007AEB0F /* DebugUIBackup.m in Sources */,
4C11AA5020FD59C700351FBD /* MessageStatusView.swift in Sources */,
340FC8AE204DAC8D007AEB0F /* OWSSoundSettingsViewController.m in Sources */,

@ -4,7 +4,7 @@ final class Button : UIButton {
private let size: Size
enum Style {
case unimportant, regular, prominentOutline, prominentFilled
case unimportant, regular, prominentOutline, prominentFilled, regularBorderless
}
enum Size {
@ -33,6 +33,7 @@ final class Button : UIButton {
case .regular: fillColor = UIColor.clear
case .prominentOutline: fillColor = UIColor.clear
case .prominentFilled: fillColor = Colors.accent
case .regularBorderless: fillColor = UIColor.clear
}
let borderColor: UIColor
switch style {
@ -40,6 +41,7 @@ final class Button : UIButton {
case .regular: borderColor = Colors.text
case .prominentOutline: borderColor = Colors.accent
case .prominentFilled: borderColor = Colors.accent
case .regularBorderless: borderColor = UIColor.clear
}
let textColor: UIColor
switch style {
@ -47,6 +49,7 @@ final class Button : UIButton {
case .regular: textColor = Colors.text
case .prominentOutline: textColor = Colors.accent
case .prominentFilled: textColor = Colors.text
case .regularBorderless: textColor = Colors.text
}
let height: CGFloat
switch size {

@ -107,13 +107,7 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
case .master:
qrCodeImageView.set(.height, to: 128)
let hexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
let data = hexEncodedPublicKey.data(using: .utf8)
let filter = CIFilter(name: "CIQRCodeGenerator")!
filter.setValue(data, forKey: "inputMessage")
let qrCodeAsCIImage = filter.outputImage!
let scaledQRCodeAsCIImage = qrCodeAsCIImage.transformed(by: CGAffineTransform(scaleX: 4.8, y: 4.8))
let qrCode = UIImage(ciImage: scaledQRCodeAsCIImage)
qrCodeImageView.image = qrCode
qrCodeImageView.image = QRCode.generate(for: hexEncodedPublicKey)
case .slave:
spinner.set(.height, to: 64)
spinner.startAnimating()
@ -126,7 +120,7 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
}()
subtitleLabel.text = {
switch mode {
case .master: return NSLocalizedString("Create a new account on your other device and click \"Link Device\" when you're at the \"Create Your Loki Messenger Account\" step to start the linking process", comment: "")
case .master: return NSLocalizedString("Create a new account on your other device and click \"Link to an existing account\" to start the linking process", comment: "")
case .slave: return NSLocalizedString("Please check that the words below match the ones shown on your other device", comment: "")
}
}()
@ -190,8 +184,9 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
print("[Loki] Failed to add device link due to error: \(error).")
}
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in
self.delegate?.handleDeviceLinkAuthorized(deviceLink)
self.dismiss(animated: true, completion: nil)
self.dismiss(animated: true) {
self.delegate?.handleDeviceLinkAuthorized(deviceLink)
}
}
}

@ -155,7 +155,6 @@ final class DisplayNameVC : UIViewController {
return showError(title: NSLocalizedString("Please pick a shorter display name", comment: ""))
}
TSAccountManager.sharedInstance().didRegister()
UserDefaults.standard.set(true, forKey: "didUpdateForMainnet")
OWSProfileManager.shared().updateLocalProfileName(displayName, avatarImage: nil, success: { }, failure: { }) // Try to save the user name but ignore the result
let homeVC = HomeVC()
navigationController!.setViewControllers([ homeVC ], animated: true)

@ -1,5 +1,5 @@
final class LandingVC : UIViewController {
final class LandingVC : UIViewController, LinkDeviceVCDelegate, DeviceLinkingModalDelegate {
private var fakeChatViewContentOffset: CGPoint!
// MARK: Components
@ -9,6 +9,30 @@ final class LandingVC : UIViewController {
return result
}()
private lazy var registerButton: Button = {
let result = Button(style: .prominentFilled, size: .large)
result.setTitle(NSLocalizedString("Create Account", comment: ""), for: UIControl.State.normal)
result.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.addTarget(self, action: #selector(register), for: UIControl.Event.touchUpInside)
return result
}()
private lazy var restoreButton: Button = {
let result = Button(style: .prominentOutline, size: .large)
result.setTitle(NSLocalizedString("Continue your Loki Messenger", comment: ""), for: UIControl.State.normal)
result.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.addTarget(self, action: #selector(restore), for: UIControl.Event.touchUpInside)
return result
}()
private lazy var linkButton: Button = {
let result = Button(style: .regularBorderless, size: .small)
result.setTitle(NSLocalizedString("Link to an existing account", comment: ""), for: UIControl.State.normal)
result.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
result.addTarget(self, action: #selector(linkDevice), for: UIControl.Event.touchUpInside)
return result
}()
// MARK: Settings
override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent }
@ -48,16 +72,14 @@ final class LandingVC : UIViewController {
// Set up spacers
let topSpacer = UIView.vStretchingSpacer()
let bottomSpacer = UIView.vStretchingSpacer()
// Set up register button
let registerButton = Button(style: .prominentFilled, size: .large)
registerButton.setTitle(NSLocalizedString("Create Account", comment: ""), for: UIControl.State.normal)
registerButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
registerButton.addTarget(self, action: #selector(register), for: UIControl.Event.touchUpInside)
// Set up restore button
let restoreButton = Button(style: .prominentOutline, size: .large)
restoreButton.setTitle(NSLocalizedString("Continue your Loki Messenger", comment: ""), for: UIControl.State.normal)
restoreButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
restoreButton.addTarget(self, action: #selector(restore), for: UIControl.Event.touchUpInside)
// Set up link button container
let linkButtonContainer = UIView()
linkButtonContainer.set(.height, to: Values.onboardingButtonBottomOffset)
linkButtonContainer.addSubview(linkButton)
linkButton.pin(.leading, to: .leading, of: linkButtonContainer, withInset: Values.massiveSpacing)
linkButton.pin(.top, to: .top, of: linkButtonContainer)
linkButtonContainer.pin(.trailing, to: .trailing, of: linkButton, withInset: Values.massiveSpacing)
linkButtonContainer.pin(.bottom, to: .bottom, of: linkButton, withInset: 10)
// Set up button stack view
let buttonStackView = UIStackView(arrangedSubviews: [ registerButton, restoreButton ])
buttonStackView.axis = .vertical
@ -71,7 +93,7 @@ final class LandingVC : UIViewController {
buttonStackViewContainer.pin(.trailing, to: .trailing, of: buttonStackView, withInset: Values.massiveSpacing)
buttonStackViewContainer.pin(.bottom, to: .bottom, of: buttonStackView)
// Set up main stack view
let mainStackView = UIStackView(arrangedSubviews: [ topSpacer, titleLabelContainer, UIView.spacer(withHeight: Values.mediumSpacing), fakeChatView, bottomSpacer, buttonStackViewContainer, UIView.spacer(withHeight: Values.onboardingButtonBottomOffset) ])
let mainStackView = UIStackView(arrangedSubviews: [ topSpacer, titleLabelContainer, UIView.spacer(withHeight: Values.mediumSpacing), fakeChatView, bottomSpacer, buttonStackViewContainer, linkButtonContainer ])
mainStackView.axis = .vertical
mainStackView.alignment = .fill
view.addSubview(mainStackView)
@ -102,4 +124,83 @@ final class LandingVC : UIViewController {
let restoreVC = RestoreVC()
navigationController!.pushViewController(restoreVC, animated: true)
}
@objc private func linkDevice() {
let linkDeviceVC = LinkDeviceVC()
linkDeviceVC.delegate = self
let navigationController = OWSNavigationController(rootViewController: linkDeviceVC)
present(navigationController, animated: true, completion: nil)
}
// MARK: Device Linking
func requestDeviceLink(with hexEncodedPublicKey: String) {
guard ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) else {
let alert = UIAlertController(title: NSLocalizedString("Invalid Public Key", comment: ""), message: NSLocalizedString("Please make sure the public key you entered is correct and try again.", comment: ""), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), accessibilityIdentifier: nil, style: .default, handler: nil))
return present(alert, animated: true, completion: nil)
}
let seed = Randomness.generateRandomBytes(16)!
let keyPair = Curve25519.generateKeyPair(fromSeed: seed + seed)
let identityManager = OWSIdentityManager.shared()
let databaseConnection = identityManager.value(forKey: "dbConnection") as! YapDatabaseConnection
databaseConnection.setObject(seed.toHexString(), forKey: "LKLokiSeed", inCollection: OWSPrimaryStorageIdentityKeyStoreCollection)
databaseConnection.setObject(keyPair, forKey: OWSPrimaryStorageIdentityKeyStoreIdentityKey, inCollection: OWSPrimaryStorageIdentityKeyStoreCollection)
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = keyPair.hexEncodedPublicKey
TSAccountManager.sharedInstance().didRegister()
setUserInteractionEnabled(false)
let _ = LokiStorageAPI.getDeviceLinks(associatedWith: hexEncodedPublicKey).done(on: DispatchQueue.main) { [weak self] deviceLinks in
guard let self = self else { return }
defer { self.setUserInteractionEnabled(true) }
guard deviceLinks.count < 2 else {
let alert = UIAlertController(title: "Multi Device Limit Reached", message: "It's currently not allowed to link more than one device.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", accessibilityIdentifier: nil, style: .default, handler: nil))
return self.present(alert, animated: true, completion: nil)
}
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.startLongPollerIfNeeded()
let deviceLinkingModal = DeviceLinkingModal(mode: .slave, delegate: self)
deviceLinkingModal.modalPresentationStyle = .overFullScreen
deviceLinkingModal.modalTransitionStyle = .crossDissolve
self.present(deviceLinkingModal, animated: true, completion: nil)
let linkingRequestMessage = DeviceLinkingUtilities.getLinkingRequestMessage(for: hexEncodedPublicKey)
ThreadUtil.enqueue(linkingRequestMessage)
}.catch(on: DispatchQueue.main) { [weak self] _ in
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.stopLongPollerIfNeeded()
DispatchQueue.main.async {
// FIXME: For some reason resetForRegistration() complains about not being on the main queue
// without this (even though the catch closure should be executed on the main queue)
TSAccountManager.sharedInstance().resetForReregistration()
}
guard let self = self else { return }
let alert = UIAlertController(title: NSLocalizedString("Couldn't Link Device", comment: ""), message: NSLocalizedString("Please check your internet connection and try again", comment: ""), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", accessibilityIdentifier: nil, style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
self.setUserInteractionEnabled(true)
}
}
func handleDeviceLinkAuthorized(_ deviceLink: DeviceLink) {
let userDefaults = UserDefaults.standard
userDefaults.set(deviceLink.master.hexEncodedPublicKey, forKey: "masterDeviceHexEncodedPublicKey")
fakeChatViewContentOffset = fakeChatView.contentOffset
DispatchQueue.main.async {
self.fakeChatView.contentOffset = self.fakeChatViewContentOffset
}
let homeVC = HomeVC()
navigationController!.setViewControllers([ homeVC ], animated: true)
}
func handleDeviceLinkingModalDismissed() {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.stopLongPollerIfNeeded()
TSAccountManager.sharedInstance().resetForReregistration()
}
// MARK: Convenience
private func setUserInteractionEnabled(_ isEnabled: Bool) {
[ registerButton, restoreButton, linkButton ].forEach {
$0.isUserInteractionEnabled = isEnabled
}
}
}

@ -0,0 +1,287 @@
final class LinkDeviceVC : UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, OWSQRScannerDelegate {
private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
private var pages: [UIViewController] = []
private var targetVCIndex: Int?
var delegate: LinkDeviceVCDelegate?
// MARK: Settings
override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent }
// MARK: Components
private lazy var tabBar: TabBar = {
let tabs = [
TabBar.Tab(title: NSLocalizedString("Enter Public Key", comment: "")) { [weak self] in
guard let self = self else { return }
self.pageVC.setViewControllers([ self.pages[0] ], direction: .forward, animated: false, completion: nil)
},
TabBar.Tab(title: NSLocalizedString("Scan QR Code", comment: "")) { [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 enterPublicKeyVC: EnterPublicKeyVC = {
let result = EnterPublicKeyVC()
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 = NSLocalizedString("Link to your existing device by going into your in-app settings and clicking \"Linked Devices\".", comment: "")
let result = ScanQRCodeWrapperVC(message: message)
result.delegate = self
return result
}()
// MARK: Lifecycle
override func viewDidLoad() {
// Set gradient background
view.backgroundColor = .clear
let gradient = Gradients.defaultLokiBackground
view.setGradient(gradient)
// Set navigation bar background color
let navigationBar = navigationController!.navigationBar
navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
navigationBar.shadowImage = UIImage()
navigationBar.isTranslucent = false
navigationBar.barTintColor = Colors.navigationBarBackground
// Set up navigation bar buttons
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
closeButton.tintColor = Colors.text
navigationItem.leftBarButtonItem = closeButton
// Customize title
let titleLabel = UILabel()
titleLabel.text = NSLocalizedString("Link Device", comment: "")
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
navigationItem.titleView = titleLabel
// Set up page VC
let hasCameraAccess = (AVCaptureDevice.authorizationStatus(for: .video) == .authorized)
pages = [ enterPublicKeyVC, (hasCameraAccess ? scanQRCodeWrapperVC : scanQRCodePlaceholderVC) ]
pageVC.dataSource = self
pageVC.delegate = self
pageVC.setViewControllers([ enterPublicKeyVC ], direction: .forward, animated: false, completion: nil)
// Set up tab bar
view.addSubview(tabBar)
tabBar.pin(.leading, to: .leading, of: view)
tabBar.pin(.top, to: .top, of: view, withInset: navigationBar.height())
view.pin(.trailing, to: .trailing, of: tabBar)
// Set up 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 height = navigationController!.view.bounds.height - navigationBar.height() - Values.tabBarHeight
pageVCView.set(.height, to: height)
enterPublicKeyVC.constrainHeight(to: height)
scanQRCodePlaceholderVC.constrainHeight(to: 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() {
pages[1] = scanQRCodeWrapperVC
pageVC.setViewControllers([ 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: OWSQRCodeScanningViewController, didDetectQRCodeWith string: String) {
let hexEncodedPublicKey = string
requestDeviceLink(with: hexEncodedPublicKey)
}
fileprivate func requestDeviceLink(with hexEncodedPublicKey: String) {
delegate?.requestDeviceLink(with: hexEncodedPublicKey)
dismiss(animated: true, completion: nil)
}
}
private final class EnterPublicKeyVC : UIViewController {
weak var linkDeviceVC: LinkDeviceVC!
private var bottomConstraint: NSLayoutConstraint!
private var linkButtonBottomConstraint: NSLayoutConstraint!
// MARK: Components
private lazy var publicKeyTextField = TextField(placeholder: NSLocalizedString("Enter your public key", comment: ""))
// MARK: Lifecycle
override func viewDidLoad() {
// Remove background color
view.backgroundColor = .clear
// Set up title label
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
titleLabel.text = NSLocalizedString("Enter your public key", comment: "")
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
// Set up explanation label
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = "explanation explanation explanation explanation explanation explanation explanation explanation explanation explanation explanation explanation explanation explanation explanation explanation"
explanationLabel.numberOfLines = 0
explanationLabel.lineBreakMode = .byWordWrapping
// Link button
let linkButton = Button(style: .prominentOutline, size: .large)
linkButton.setTitle(NSLocalizedString("Continue", comment: ""), for: UIControl.State.normal)
linkButton.addTarget(self, action: #selector(requestDeviceLink), for: UIControl.Event.touchUpInside)
let linkButtonContainer = UIView()
linkButtonContainer.addSubview(linkButton)
linkButton.pin(.leading, to: .leading, of: linkButtonContainer, withInset: 80)
linkButton.pin(.top, to: .top, of: linkButtonContainer)
linkButtonContainer.pin(.trailing, to: .trailing, of: linkButton, withInset: 80)
linkButtonBottomConstraint = linkButtonContainer.pin(.bottom, to: .bottom, of: linkButton, withInset: Values.veryLargeSpacing)
// Set up top stack view
let topStackView = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, publicKeyTextField ])
topStackView.axis = .vertical
topStackView.spacing = Values.largeSpacing
// Set up spacers
let topSpacer = UIView.vStretchingSpacer()
let bottomSpacer = UIView.vStretchingSpacer()
// Set up stack view
let stackView = UIStackView(arrangedSubviews: [ topSpacer, topStackView, bottomSpacer, linkButtonContainer ])
stackView.axis = .vertical
stackView.alignment = .fill
stackView.layoutMargins = UIEdgeInsets(top: 0, left: Values.veryLargeSpacing, bottom: 0, right: Values.veryLargeSpacing)
stackView.isLayoutMarginsRelativeArrangement = true
view.addSubview(stackView)
stackView.pin(.leading, to: .leading, of: view)
stackView.pin(.top, to: .top, of: view)
stackView.pin(.trailing, to: .trailing, of: view)
bottomConstraint = stackView.pin(.bottom, to: .bottom, of: view)
topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor, multiplier: 1).isActive = true
// Set up width constraint
view.set(.width, to: UIScreen.main.bounds.width)
// 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)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: General
func constrainHeight(to height: CGFloat) {
view.set(.height, to: height)
}
@objc private func dismissKeyboard() {
publicKeyTextField.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
linkButtonBottomConstraint.constant = Values.mediumSpacing
UIView.animate(withDuration: 0.25) {
self.view.layoutIfNeeded()
}
}
@objc private func handleKeyboardWillHideNotification(_ notification: Notification) {
bottomConstraint.constant = 0
linkButtonBottomConstraint.constant = Values.veryLargeSpacing
UIView.animate(withDuration: 0.25) {
self.view.layoutIfNeeded()
}
}
// MARK: Interaction
@objc private func requestDeviceLink() {
let hexEncodedPublicKey = publicKeyTextField.text?.trimmingCharacters(in: .whitespaces) ?? ""
linkDeviceVC.requestDeviceLink(with: hexEncodedPublicKey)
}
}
private final class ScanQRCodePlaceholderVC : UIViewController {
weak var linkDeviceVC: LinkDeviceVC!
override func viewDidLoad() {
// Remove background color
view.backgroundColor = .clear
// Set up explanation label
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("Loki Messenger needs camera access to scan QR codes", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
// Set up call to action button
let callToActionButton = UIButton()
callToActionButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
callToActionButton.setTitleColor(Colors.accent, for: UIControl.State.normal)
callToActionButton.setTitle(NSLocalizedString("Enable Camera Access", comment: ""), for: UIControl.State.normal)
callToActionButton.addTarget(self, action: #selector(requestCameraAccess), for: UIControl.Event.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() {
ows_ask(forCameraPermissions: { [weak self] hasCameraAccess in
if hasCameraAccess {
self?.linkDeviceVC.handleCameraAccessGranted()
} else {
// Do nothing
}
})
}
}

@ -0,0 +1,5 @@
protocol LinkDeviceVCDelegate {
func requestDeviceLink(with hexEncodedPublicKey: String)
}

@ -2753,3 +2753,7 @@
"Press the covered words to view your seed and secure your account" = "Press the covered words to view your seed and secure your account";
"Hold to reveal" = "Hold to reveal";
"Make sure to store your seed in a safe place" = "Make sure to store your seed in a safe place";
"Link to an existing account" = "Link to an existing account";
"Enter your public key" = "Enter your public key";
"Link to your existing device by going into your in-app settings and clicking \"Linked Devices\"." = "Link to your existing device by going into your in-app settings and clicking \"Linked Devices\".";
"Create a new account on your other device and click \"Link to an existing account\" to start the linking process" = "Create a new account on your other device and click \"Link to an existing account\" to start the linking process";

Loading…
Cancel
Save