// C o p y r i g h t © 2 0 2 2 R a n g e p r o o f P t y L t d . A l l r i g h t s r e s e r v e d .
import UIKit
import PromiseKit
import SessionUtilitiesKit
final class LinkDeviceVC : BaseVC , UIPageViewControllerDataSource , UIPageViewControllerDelegate , OWSQRScannerDelegate {
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: C o m p o n e n t s
private lazy var tabBar : TabBar = {
let tabs = [
TabBar . Tab ( title : NSLocalizedString ( " vc_link_device_recovery_phrase_tab_title " , 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 ( " vc_link_device_scan_qr_code_tab_title " , 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 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 = NSLocalizedString ( " vc_link_device_scan_qr_code_explanation " , comment : " " )
let result = ScanQRCodeWrapperVC ( message : message )
result . delegate = self
return result
} ( )
// MARK: L i f e c y c l e
override func viewDidLoad ( ) {
super . viewDidLoad ( )
setUpGradientBackground ( )
setUpNavBarStyle ( )
setNavBarTitle ( NSLocalizedString ( " vc_link_device_title " , comment : " " ) )
let navigationBar = navigationController ! . navigationBar
// P a g e V C
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 )
// T a b b a r
view . addSubview ( tabBar )
tabBar . pin ( . leading , to : . leading , of : view )
tabBarTopConstraint = tabBar . autoPinEdge ( toSuperviewSafeArea : . top )
view . pin ( . trailing , to : . trailing , of : tabBar )
// P a g e V C c o n s t r a i n t s
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 - navigationBar . height ( ) - 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 ( )
}
deinit {
NotificationCenter . default . removeObserver ( self )
}
// MARK: G e n e r a l
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: U p d a t i n g
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: I n t e r a c t i o n
@objc private func close ( ) {
dismiss ( animated : true , completion : nil )
}
func controller ( _ controller : OWSQRCodeScanningViewController , didDetectQRCodeWith string : String ) {
let seed = Data ( hex : string )
continueWithSeed ( seed )
}
func continueWithSeed ( _ seed : Data ) {
if ( seed . count != 16 ) {
let alert = UIAlertController ( title : NSLocalizedString ( " invalid_recovery_phrase " , comment : " " ) , message : NSLocalizedString ( " Please check the Recovery Phrase and try again. " , comment : " " ) , preferredStyle : . alert )
alert . addAction ( UIAlertAction ( title : NSLocalizedString ( " BUTTON_OK " , comment : " " ) , style : . default , handler : { _ in
self . scanQRCodeWrapperVC . startCapture ( )
} ) )
presentAlert ( alert )
return
}
let ( ed25519KeyPair , x25519KeyPair ) = try ! Identity . generate ( from : seed )
Onboarding . Flow . link . preregister ( with : seed , ed25519KeyPair : ed25519KeyPair , x25519KeyPair : x25519KeyPair )
TSAccountManager . sharedInstance ( ) . didRegister ( )
NotificationCenter . default . addObserver ( self , selector : #selector ( handleInitialConfigurationMessageReceived ) , name : . initialConfigurationMessageReceived , object : nil )
ModalActivityIndicatorViewController . present ( fromViewController : navigationController ! ) { [ weak self ] modal in
self ? . activityIndicatorModal = modal
}
}
@objc private func handleInitialConfigurationMessageReceived ( _ notification : Notification ) {
TSAccountManager . sharedInstance ( ) . phoneNumberAwaitingVerification = getUserHexEncodedPublicKey ( )
DispatchQueue . main . async {
self . navigationController ! . dismiss ( animated : true ) {
let pnModeVC = PNModeVC ( )
self . navigationController ! . setViewControllers ( [ 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 : NSLocalizedString ( " vc_restore_seed_text_field_hint " , comment : " " ) )
result . layer . borderColor = Colors . text . cgColor
result . accessibilityLabel = " Recovery phrase text view "
return result
} ( )
// MARK: L i f e c y c l e
override func viewDidLoad ( ) {
view . backgroundColor = . clear
// T i t l e l a b e l
let titleLabel = UILabel ( )
titleLabel . textColor = Colors . text
titleLabel . font = . boldSystemFont ( ofSize : isIPhone5OrSmaller ? Values . largeFontSize : Values . veryLargeFontSize )
titleLabel . text = NSLocalizedString ( " vc_enter_recovery_phrase_title " , comment : " " )
titleLabel . numberOfLines = 0
titleLabel . lineBreakMode = . byWordWrapping
// E x p l a n a t i o n l a b e l
let explanationLabel = UILabel ( )
explanationLabel . textColor = Colors . text
explanationLabel . font = . systemFont ( ofSize : Values . smallFontSize )
explanationLabel . text = NSLocalizedString ( " vc_enter_recovery_phrase_explanation " , comment : " " )
explanationLabel . numberOfLines = 0
explanationLabel . lineBreakMode = . byWordWrapping
// S p a c e r s
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 )
// C o n t i n u e b u t t o n
let continueButton = Button ( style : . prominentFilled , size : . large )
continueButton . setTitle ( NSLocalizedString ( " continue_2 " , comment : " " ) , for : UIControl . State . normal )
continueButton . titleLabel ! . font = . boldSystemFont ( ofSize : Values . mediumFontSize )
continueButton . addTarget ( self , action : #selector ( handleContinueButtonTapped ) , for : UIControl . Event . touchUpInside )
// C o n t i n u e b u t t o n c o n t a i n e r
let continueButtonContainer = UIView ( )
continueButtonContainer . addSubview ( continueButton )
continueButton . pin ( . leading , to : . leading , of : continueButtonContainer , withInset : Values . massiveSpacing )
continueButton . pin ( . top , to : . top , of : continueButtonContainer )
continueButtonContainer . pin ( . trailing , to : . trailing , of : continueButton , withInset : Values . massiveSpacing )
continueButtonContainer . pin ( . bottom , to : . bottom , of : continueButton )
// T o p s t a c k v i e w
let topStackView = UIStackView ( arrangedSubviews : [ titleLabel , spacer1 , explanationLabel , spacer2 , mnemonicTextView ] )
topStackView . axis = . vertical
topStackView . alignment = . fill
// T o p s t a c k v i e w c o n t a i n e r
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 )
// M a i n s t a c k v i e w
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
// D i s m i s s k e y b o a r d o n t a p
let tapGestureRecognizer = UITapGestureRecognizer ( target : self , action : #selector ( dismissKeyboard ) )
view . addGestureRecognizer ( tapGestureRecognizer )
// L i s t e n t o k e y b o a r d n o t i f i c a t i o n s
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 )
// S e t u p w i d t h c o n s t r a i n t
view . set ( . width , to : UIScreen . main . bounds . width )
}
deinit {
NotificationCenter . default . removeObserver ( self )
}
// MARK: G e n e r a l
func constrainHeight ( to height : CGFloat ) {
view . set ( . height , to : height )
}
@objc private func dismissKeyboard ( ) {
mnemonicTextView . resignFirstResponder ( )
}
// MARK: U p d a t i n g
@objc private func handleKeyboardWillChangeFrameNotification ( _ notification : Notification ) {
guard let newHeight = ( notification . userInfo ? [ UIResponder . keyboardFrameEndUserInfoKey ] as ? NSValue ) ? . cgRectValue . size . height else { return }
bottomConstraint . constant = - newHeight // N e g a t i v e d u e t o h o w t h e c o n s t r a i n t i s s e t u p
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: I n t e r a c t i o n
@objc private func handleContinueButtonTapped ( ) {
func showError ( title : String , message : String = " " ) {
let alert = UIAlertController ( title : title , message : message , preferredStyle : . alert )
alert . addAction ( UIAlertAction ( title : NSLocalizedString ( " BUTTON_OK " , comment : " " ) , style : . default , handler : nil ) )
presentAlert ( alert )
}
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 ( ) {
// R e m o v e b a c k g r o u n d c o l o r
view . backgroundColor = . clear
// S e t u p e x p l a n a t i o n l a b e l
let explanationLabel = UILabel ( )
explanationLabel . textColor = Colors . text
explanationLabel . font = . systemFont ( ofSize : Values . smallFontSize )
explanationLabel . text = NSLocalizedString ( " vc_scan_qr_code_camera_access_explanation " , comment : " " )
explanationLabel . numberOfLines = 0
explanationLabel . textAlignment = . center
explanationLabel . lineBreakMode = . byWordWrapping
// S e t u p c a l l t o a c t i o n b u t t o n
let callToActionButton = UIButton ( )
callToActionButton . titleLabel ! . font = . boldSystemFont ( ofSize : Values . mediumFontSize )
callToActionButton . setTitleColor ( Colors . accent , for : UIControl . State . normal )
callToActionButton . setTitle ( NSLocalizedString ( " vc_scan_qr_code_grant_camera_access_button_title " , comment : " " ) , for : UIControl . State . normal )
callToActionButton . addTarget ( self , action : #selector ( requestCameraAccess ) , for : UIControl . Event . touchUpInside )
// S e t u p s t a c k v i e w
let stackView = UIStackView ( arrangedSubviews : [ explanationLabel , callToActionButton ] )
stackView . axis = . vertical
stackView . spacing = Values . mediumSpacing
stackView . alignment = . center
// S e t u p c o n s t r a i n t s
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 // M a k e s t h i n g s a p p e a r c e n t e r e d v i s u a l l y
}
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 {
// D o n o t h i n g
}
} )
}
}