800 lines
31 KiB
Swift
800 lines
31 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
|
|
import Lottie
|
|
import PureLayout
|
|
import SafariServices
|
|
import SignalMessaging
|
|
import SignalServiceKit
|
|
import UIKit
|
|
|
|
public class MultiFingerprintViewController: OWSViewController, OWSNavigationChildController {
|
|
|
|
public var preferredNavigationBarStyle: OWSNavigationBarStyle {
|
|
return .solid
|
|
}
|
|
|
|
public var navbarBackgroundColorOverride: UIColor? {
|
|
return Self.backgroundColor
|
|
}
|
|
|
|
public override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
|
return .portrait
|
|
}
|
|
|
|
private let recipientAddress: SignalServiceAddress
|
|
private let recipientIdentity: OWSRecipientIdentity
|
|
private let contactName: String
|
|
private let identityKey: IdentityKey
|
|
private let fingerprints: [OWSFingerprint]
|
|
private var selectedIndex: Int
|
|
|
|
public init(
|
|
fingerprints: [OWSFingerprint],
|
|
defaultIndex: Int,
|
|
recipientAddress: SignalServiceAddress,
|
|
recipientIdentity: OWSRecipientIdentity
|
|
) {
|
|
self.recipientAddress = recipientAddress
|
|
self.contactName = SSKEnvironment.shared.contactsManagerRef.displayName(for: recipientAddress)
|
|
// By capturing the identity key when we enter these views, we prevent the edge case
|
|
// where the user verifies a key that we learned about while this view was open.
|
|
self.recipientIdentity = recipientIdentity
|
|
self.identityKey = recipientIdentity.identityKey
|
|
self.fingerprints = fingerprints
|
|
self.selectedIndex = defaultIndex
|
|
|
|
super.init()
|
|
|
|
title = NSLocalizedString("PRIVACY_VERIFICATION_TITLE", comment: "Navbar title")
|
|
navigationItem.leftBarButtonItem = .init(
|
|
barButtonSystemItem: .done,
|
|
target: self, action: #selector(didTapDone),
|
|
accessibilityIdentifier: "FingerprintViewController.done"
|
|
)
|
|
|
|
identityStateChangeObserver = NotificationCenter.default.addObserver(
|
|
forName: .identityStateDidChange,
|
|
object: nil,
|
|
queue: .main) { [weak self] _ in
|
|
self?.identityStateDidChange()
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
if let identityStateChangeObserver {
|
|
NotificationCenter.default.removeObserver(identityStateChangeObserver)
|
|
}
|
|
}
|
|
|
|
private static var backgroundColor: UIColor {
|
|
return Theme.isDarkThemeEnabled ? .ows_gray90 : .ows_gray02
|
|
}
|
|
|
|
public override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
view.backgroundColor = Self.backgroundColor
|
|
|
|
configureUI()
|
|
}
|
|
|
|
public override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
|
|
scrollToSelectedIndex(animated: false)
|
|
}
|
|
|
|
public override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
if !DependenciesBridge.shared.db.read(block: self.hasShownTransitionSheet) {
|
|
// Its fine to not re-read the value in the write tx; stakes are low.
|
|
DependenciesBridge.shared.db.write(block: self.showTransitionSheet)
|
|
}
|
|
}
|
|
|
|
public override func themeDidChange() {
|
|
super.themeDidChange()
|
|
view.backgroundColor = Self.backgroundColor
|
|
|
|
updateVerificationStateLabel()
|
|
setSafetyNumbersUpdateTextViewText()
|
|
setCarouselPageControlColors()
|
|
setInstructionsText()
|
|
setVerifyUnverifyButtonColors()
|
|
}
|
|
|
|
// MARK: UI
|
|
|
|
private lazy var safetyNumbersUpdateTextView: LinkingTextView = {
|
|
let textView = LinkingTextView()
|
|
textView.delegate = self
|
|
return textView
|
|
}()
|
|
|
|
private func setSafetyNumbersUpdateTextViewText() {
|
|
// Link doesn't matter, we will override tap behavior.
|
|
let learnMoreString = CommonStrings.learnMore.styled(with: .link(URL(string: Constants.transitionLearnMoreUrl)!))
|
|
safetyNumbersUpdateTextView.attributedText = NSAttributedString.composed(of: [
|
|
OWSLocalizedString(
|
|
"SAFETY_NUMBER_TRANSITION_HEADER_ALERT",
|
|
comment: "Header informing the user about the transition from phone number to user identifier based."
|
|
),
|
|
"\n",
|
|
learnMoreString
|
|
]).styled(
|
|
with: .font(.dynamicTypeFootnote),
|
|
.color(Theme.secondaryTextAndIconColor)
|
|
)
|
|
safetyNumbersUpdateTextView.linkTextAttributes = [
|
|
.foregroundColor: Theme.primaryTextColor,
|
|
.underlineColor: UIColor.clear,
|
|
.underlineStyle: NSUnderlineStyle.single.rawValue
|
|
]
|
|
}
|
|
|
|
private lazy var safetyNumbersUpdateView: UIView = {
|
|
let stackView = UIStackView()
|
|
stackView.axis = .horizontal
|
|
stackView.distribution = .fill
|
|
stackView.alignment = .center
|
|
stackView.spacing = 16
|
|
|
|
let imageView = UIImageView(image: UIImage(named: "safety_number_transition"))
|
|
imageView.autoSetDimensions(to: .square(48))
|
|
stackView.addArrangedSubview(imageView)
|
|
|
|
stackView.addArrangedSubview(safetyNumbersUpdateTextView)
|
|
|
|
return stackView
|
|
}()
|
|
|
|
private lazy var fingerprintCards: [FingerprintCard] = {
|
|
return fingerprints.map { fingerprint in
|
|
return FingerprintCard(fingerprint: fingerprint, controller: self)
|
|
}
|
|
}()
|
|
|
|
private lazy var fingerprintCarousel: UIScrollView = {
|
|
let scrollView = UIScrollView()
|
|
scrollView.isPagingEnabled = true
|
|
scrollView.isDirectionalLockEnabled = true
|
|
scrollView.alwaysBounceVertical = false
|
|
scrollView.alwaysBounceHorizontal = true
|
|
scrollView.showsVerticalScrollIndicator = false
|
|
scrollView.showsHorizontalScrollIndicator = false
|
|
|
|
var xOffset: CGFloat = Constants.cardHInset
|
|
var previousView: UIView = scrollView
|
|
var nextEdge: ALEdge = .leading
|
|
for fingerprintCard in fingerprintCards {
|
|
scrollView.addSubview(fingerprintCard)
|
|
fingerprintCard.autoPinVerticalEdges(toEdgesOf: scrollView)
|
|
scrollView.autoPinHeight(toHeightOf: fingerprintCard, relation: .greaterThanOrEqual)
|
|
fingerprintCard.autoPinEdge(.leading, to: nextEdge, of: previousView, withOffset: xOffset)
|
|
previousView = fingerprintCard
|
|
xOffset = Constants.interCardSpacing
|
|
nextEdge = .trailing
|
|
}
|
|
previousView.autoPinEdge(.trailing, to: .trailing, of: scrollView, withOffset: -Constants.cardHInset)
|
|
|
|
scrollView.delegate = self
|
|
|
|
return scrollView
|
|
}()
|
|
|
|
private lazy var fingerprintCarouselPageControl: UIPageControl = {
|
|
let control = UIPageControl()
|
|
control.numberOfPages = fingerprints.count
|
|
control.addTarget(self, action: #selector(didUpdatePageControl), for: .valueChanged)
|
|
return control
|
|
}()
|
|
|
|
private func setCarouselPageControlColors() {
|
|
fingerprintCarouselPageControl.pageIndicatorTintColor = Theme.isDarkThemeEnabled ? .ows_gray65 : .ows_gray25
|
|
fingerprintCarouselPageControl.currentPageIndicatorTintColor = Theme.primaryTextColor
|
|
}
|
|
|
|
private lazy var instructionsTextView: UITextView = {
|
|
let textView = LinkingTextView()
|
|
textView.delegate = self
|
|
return textView
|
|
}()
|
|
|
|
private func setInstructionsText() {
|
|
let instructionsFormat = OWSLocalizedString(
|
|
"VERIFY_SAFETY_NUMBER_INSTRUCTIONS",
|
|
comment: "Instructions for verifying your safety number. Embeds {{contact's name}}"
|
|
)
|
|
// Link doesn't matter, we will override tap behavior.
|
|
let learnMoreString = CommonStrings.learnMore.styled(with: .link(URL(string: Constants.learnMoreUrl)!))
|
|
instructionsTextView.attributedText = NSAttributedString.composed(of: [
|
|
String(format: instructionsFormat, contactName),
|
|
" ",
|
|
learnMoreString
|
|
]).styled(
|
|
with: .font(.dynamicTypeFootnote),
|
|
.color(Theme.secondaryTextAndIconColor),
|
|
.alignment(.center)
|
|
)
|
|
instructionsTextView.linkTextAttributes = [
|
|
.foregroundColor: Theme.primaryTextColor,
|
|
.underlineColor: UIColor.clear,
|
|
.underlineStyle: NSUnderlineStyle.single.rawValue
|
|
]
|
|
}
|
|
|
|
private lazy var verifyUnverifyButtonLabel = UILabel()
|
|
private lazy var verifyUnverifyPillbox = PillBoxView()
|
|
|
|
private lazy var verifyUnverifyButton: UIView = {
|
|
verifyUnverifyPillbox.layer.masksToBounds = true
|
|
verifyUnverifyPillbox.accessibilityIdentifier = "FingerprintViewController.verifyUnverifyButton"
|
|
verifyUnverifyPillbox.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapVerifyUnverify)))
|
|
|
|
verifyUnverifyButtonLabel.font = .systemFont(ofSize: 13, weight: .bold)
|
|
verifyUnverifyButtonLabel.textAlignment = .center
|
|
verifyUnverifyButtonLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
|
|
verifyUnverifyPillbox.addSubview(verifyUnverifyButtonLabel)
|
|
verifyUnverifyButtonLabel.autoPinWidthToSuperview(withMargin: 24)
|
|
verifyUnverifyButtonLabel.autoPinHeightToSuperview(withMargin: 12)
|
|
|
|
return verifyUnverifyPillbox
|
|
}()
|
|
|
|
private func setVerifyUnverifyButtonColors() {
|
|
verifyUnverifyButtonLabel.textColor = Theme.primaryTextColor
|
|
verifyUnverifyPillbox.backgroundColor = Theme.isDarkThemeEnabled ? .ows_gray80 : .white
|
|
}
|
|
|
|
private func configureUI() {
|
|
|
|
let scrollView = UIScrollView()
|
|
scrollView.showsVerticalScrollIndicator = false
|
|
scrollView.showsHorizontalScrollIndicator = false
|
|
let containerView = UIView()
|
|
view.addSubview(scrollView)
|
|
scrollView.addSubview(containerView)
|
|
|
|
scrollView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .bottom)
|
|
containerView.autoPinEdges(toEdgesOf: scrollView)
|
|
containerView.autoPinWidth(toWidthOf: view)
|
|
|
|
containerView.addSubview(safetyNumbersUpdateView)
|
|
containerView.addSubview(fingerprintCarousel)
|
|
containerView.addSubview(fingerprintCarouselPageControl)
|
|
containerView.addSubview(instructionsTextView)
|
|
view.addSubview(verifyUnverifyButton)
|
|
|
|
safetyNumbersUpdateView.autoPinEdge(.leading, to: .leading, of: containerView, withOffset: .scaleFromIPhone5To7Plus(18, 24))
|
|
safetyNumbersUpdateView.autoPinEdge(.trailing, to: .trailing, of: containerView, withOffset: -.scaleFromIPhone5To7Plus(18, 24))
|
|
safetyNumbersUpdateView.autoPinEdge(toSuperviewSafeArea: .top, withInset: 12)
|
|
|
|
fingerprintCarousel.autoPinHorizontalEdges(toEdgesOf: containerView)
|
|
|
|
fingerprintCards.forEach {
|
|
$0.autoPinWidth(toWidthOf: containerView, offset: -.scaleFromIPhone5To7Plus(60, 105))
|
|
}
|
|
|
|
fingerprintCarouselPageControl.autoHCenterInSuperview()
|
|
fingerprintCarouselPageControl.autoPinEdge(.top, to: .bottom, of: fingerprintCarousel, withOffset: 8)
|
|
|
|
instructionsTextView.autoPinEdge(.leading, to: .leading, of: containerView, withOffset: .scaleFromIPhone5To7Plus(18, 28))
|
|
instructionsTextView.autoPinEdge(.trailing, to: .trailing, of: containerView, withOffset: -.scaleFromIPhone5To7Plus(18, 28))
|
|
instructionsTextView.autoPinEdge(.bottom, to: .bottom, of: scrollView)
|
|
|
|
verifyUnverifyButton.autoHCenterInSuperview()
|
|
verifyUnverifyButton.autoPinEdge(.top, to: .bottom, of: scrollView, withOffset: .scaleFromIPhone5To7Plus(12, 24))
|
|
verifyUnverifyButton.autoPinEdge(toSuperviewSafeArea: .bottom, withInset: .scaleFromIPhone5To7Plus(16, 40))
|
|
|
|
if fingerprints.count <= 1 {
|
|
safetyNumbersUpdateView.isHidden = true
|
|
fingerprintCarouselPageControl.isHidden = true
|
|
scrollView.isScrollEnabled = false
|
|
|
|
fingerprintCarousel.autoPinEdge(toSuperviewSafeArea: .top, withInset: 56)
|
|
instructionsTextView.autoPinEdge(.top, to: .bottom, of: fingerprintCarousel, withOffset: 24)
|
|
} else {
|
|
fingerprintCarousel.autoPinEdge(.top, to: .bottom, of: safetyNumbersUpdateView, withOffset: 24)
|
|
instructionsTextView.autoPinEdge(.top, to: .bottom, of: fingerprintCarouselPageControl, withOffset: 16)
|
|
}
|
|
|
|
updateVerificationStateLabel()
|
|
setSafetyNumbersUpdateTextViewText()
|
|
setCarouselPageControlColors()
|
|
setInstructionsText()
|
|
setVerifyUnverifyButtonColors()
|
|
}
|
|
|
|
private func updateVerificationStateLabel() {
|
|
owsAssertBeta(recipientAddress.isValid)
|
|
|
|
let isVerified = OWSIdentityManager.shared.verificationState(for: recipientAddress) == .verified
|
|
|
|
if isVerified {
|
|
verifyUnverifyButtonLabel.text = NSLocalizedString(
|
|
"PRIVACY_UNVERIFY_BUTTON",
|
|
comment: "Button that lets user mark another user's identity as unverified."
|
|
)
|
|
} else {
|
|
verifyUnverifyButtonLabel.text = OWSLocalizedString(
|
|
"PRIVACY_VERIFY_BUTTON",
|
|
comment: "Button that lets user mark another user's identity as verified."
|
|
)
|
|
}
|
|
view.setNeedsLayout()
|
|
}
|
|
|
|
// MARK: - Fingerprint Card
|
|
|
|
class FingerprintCard: UIView {
|
|
|
|
private let fingerprint: OWSFingerprint
|
|
private weak var controller: MultiFingerprintViewController?
|
|
|
|
init(fingerprint: OWSFingerprint, controller: MultiFingerprintViewController) {
|
|
self.fingerprint = fingerprint
|
|
self.controller = controller
|
|
super.init(frame: .zero)
|
|
|
|
layer.cornerRadius = Constants.cornerRadius
|
|
|
|
self.backgroundColor = {
|
|
switch fingerprint.source {
|
|
case .aci: return UIColor(rgbHex: 0x506ecd)
|
|
case .e164: return UIColor(rgbHex: 0xdeddda)
|
|
}
|
|
}()
|
|
|
|
addSubview(shareButton)
|
|
addSubview(qrCodeView)
|
|
addSubview(safetyNumberLabel)
|
|
|
|
shareButton.autoPinEdge(.top, to: .top, of: self, withOffset: 16)
|
|
shareButton.autoPinEdge(.trailing, to: .trailing, of: self, withOffset: -16)
|
|
|
|
qrCodeView.autoPinEdge(.top, to: .bottom, of: shareButton, withOffset: 8)
|
|
qrCodeView.autoPinEdge(.leading, to: .leading, of: self, withOffset: .scaleFromIPhone5To7Plus(44, 64))
|
|
qrCodeView.autoPinEdge(.trailing, to: .trailing, of: self, withOffset: -.scaleFromIPhone5To7Plus(44, 64))
|
|
|
|
safetyNumberLabel.autoPinEdge(.top, to: .bottom, of: qrCodeView, withOffset: 30)
|
|
safetyNumberLabel.autoPinEdge(.leading, to: .leading, of: self, withOffset: .scaleFromIPhone5To7Plus(20, 35))
|
|
safetyNumberLabel.autoPinEdge(.trailing, to: .trailing, of: self, withOffset: -.scaleFromIPhone5To7Plus(20, 35))
|
|
safetyNumberLabel.autoPinEdge(.bottom, to: .bottom, of: self, withOffset: -.scaleFromIPhone5To7Plus(27, 47))
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError()
|
|
}
|
|
|
|
private lazy var shareButton: UIButton = {
|
|
let button = UIButton()
|
|
let tintColor: UIColor
|
|
switch fingerprint.source {
|
|
case .aci:
|
|
tintColor = .white
|
|
case .e164:
|
|
tintColor = .black
|
|
}
|
|
button.setTemplateImage(
|
|
Theme.iconImage(.buttonShare).withRenderingMode(.alwaysTemplate),
|
|
tintColor: tintColor
|
|
)
|
|
button.addTarget(self, action: #selector(didTapShare), for: .touchUpInside)
|
|
return button
|
|
}()
|
|
|
|
private lazy var qrCodeView: UIView = {
|
|
let containerView = UIView()
|
|
containerView.backgroundColor = .white
|
|
containerView.layer.cornerRadius = Constants.cornerRadius
|
|
containerView.layer.masksToBounds = true
|
|
|
|
let fingerprintImageView = UIImageView()
|
|
fingerprintImageView.image = fingerprint.image
|
|
// Don't antialias QR Codes.
|
|
fingerprintImageView.layer.magnificationFilter = .nearest
|
|
fingerprintImageView.layer.minificationFilter = .nearest
|
|
fingerprintImageView.setCompressionResistanceLow()
|
|
containerView.addSubview(fingerprintImageView)
|
|
fingerprintImageView.autoPin(toAspectRatio: 1)
|
|
fingerprintImageView.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(margin: 20), excludingEdge: .bottom)
|
|
|
|
let scanLabel = UILabel()
|
|
scanLabel.text = NSLocalizedString("PRIVACY_TAP_TO_SCAN", comment: "Button that shows the 'scan with camera' view.")
|
|
scanLabel.font = .systemFont(ofSize: .scaleFromIPhone5To7Plus(13, 15))
|
|
scanLabel.textColor = Theme.lightThemeSecondaryTextAndIconColor
|
|
containerView.addSubview(scanLabel)
|
|
scanLabel.autoHCenterInSuperview()
|
|
scanLabel.autoPinEdge(.top, to: .bottom, of: fingerprintImageView, withOffset: 12)
|
|
scanLabel.autoPinEdge(.bottom, to: .bottom, of: containerView, withOffset: -14)
|
|
|
|
containerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapToScan)))
|
|
|
|
return containerView
|
|
}()
|
|
|
|
private lazy var safetyNumberLabel: UILabel = {
|
|
let label = UILabel()
|
|
label.text = fingerprint.displayableText
|
|
label.font = UIFont(name: "Menlo-Regular", size: 23)
|
|
label.textAlignment = .center
|
|
switch fingerprint.source {
|
|
case .aci:
|
|
label.textColor = .white
|
|
case .e164:
|
|
label.textColor = Theme.lightThemeSecondaryTextAndIconColor
|
|
}
|
|
label.numberOfLines = 3
|
|
label.lineBreakMode = .byTruncatingTail
|
|
label.adjustsFontSizeToFitWidth = true
|
|
label.isUserInteractionEnabled = true
|
|
label.accessibilityIdentifier = "FingerprintViewController.fingerprintLabel"
|
|
return label
|
|
}()
|
|
|
|
@objc
|
|
func didTapToScan() {
|
|
controller?.didTapToScan()
|
|
}
|
|
|
|
@objc
|
|
func didTapShare() {
|
|
controller?.shareFingerprint(from: shareButton)
|
|
}
|
|
|
|
enum Constants {
|
|
static let cornerRadius: CGFloat = 18
|
|
}
|
|
}
|
|
|
|
// MARK: PillBoxView
|
|
|
|
class PillBoxView: UIView {
|
|
|
|
override var bounds: CGRect {
|
|
didSet {
|
|
self.layer.cornerRadius = bounds.height / 2
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Transition Sheet
|
|
|
|
private lazy var kvStore: KeyValueStore = {
|
|
return DependenciesBridge.shared.keyValueStoreFactory.keyValueStore(collection: "MultiFingerprintVC")
|
|
}()
|
|
|
|
private static let hasShownTransitionSheetKey = "hasShownTransitionSheetKey"
|
|
|
|
private func hasShownTransitionSheet(_ tx: DBReadTransaction) -> Bool {
|
|
return self.kvStore.getBool(Self.hasShownTransitionSheetKey, defaultValue: false, transaction: tx)
|
|
}
|
|
|
|
private func setHasShownTransitionSheet(_ tx: DBWriteTransaction) {
|
|
self.kvStore.setBool(true, key: Self.hasShownTransitionSheetKey, transaction: tx)
|
|
}
|
|
|
|
private func showTransitionSheet(_ tx: DBWriteTransaction) {
|
|
self.setHasShownTransitionSheet(tx)
|
|
tx.addAsyncCompletion(on: DispatchQueue.main) {
|
|
let sheet = TransitionSheetViewController(parent: self)
|
|
self.present(sheet, animated: true)
|
|
}
|
|
}
|
|
|
|
class TransitionSheetViewController: InteractiveSheetViewController {
|
|
let contentScrollView = UIScrollView()
|
|
let stackView = UIStackView()
|
|
public override var interactiveScrollViews: [UIScrollView] { [contentScrollView] }
|
|
public override var sheetBackgroundColor: UIColor { Theme.tableView2PresentedBackgroundColor }
|
|
|
|
private weak var parentVc: MultiFingerprintViewController?
|
|
|
|
init(parent: MultiFingerprintViewController) {
|
|
self.parentVc = parent
|
|
super.init()
|
|
}
|
|
|
|
override public func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
minimizedHeight = 600
|
|
super.allowsExpansion = true
|
|
|
|
contentView.addSubview(contentScrollView)
|
|
|
|
stackView.axis = .vertical
|
|
stackView.layoutMargins = UIEdgeInsets(hMargin: 24, vMargin: 24)
|
|
stackView.spacing = 16
|
|
stackView.isLayoutMarginsRelativeArrangement = true
|
|
contentScrollView.addSubview(stackView)
|
|
stackView.autoPinHeightToSuperview()
|
|
// Pin to the scroll view's viewport, not to its scrollable area
|
|
stackView.autoPinWidth(toWidthOf: contentScrollView)
|
|
|
|
contentScrollView.autoPinEdgesToSuperviewEdges()
|
|
contentScrollView.alwaysBounceVertical = true
|
|
|
|
buildContents()
|
|
}
|
|
|
|
override public func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
if !animationView.isAnimationQueued && !animationView.isAnimationPlaying {
|
|
animationView.play { [weak self] success in
|
|
guard success else { return }
|
|
self?.loopAnimation()
|
|
}
|
|
}
|
|
}
|
|
|
|
override public func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
|
|
if animationView.isAnimationQueued || animationView.isAnimationPlaying {
|
|
animationView.stop()
|
|
}
|
|
}
|
|
|
|
private func loopAnimation() {
|
|
animationView.play(fromFrame: 60, toFrame: 360, completion: { [weak self] success in
|
|
guard success else { return }
|
|
self?.loopAnimation()
|
|
})
|
|
}
|
|
|
|
private lazy var animationView: AnimationView = {
|
|
let animationView = AnimationView(name: "safety-numbers")
|
|
animationView.contentMode = .scaleAspectFit
|
|
animationView.isUserInteractionEnabled = false
|
|
animationView.backgroundColor = .white
|
|
animationView.layer.cornerRadius = 12
|
|
animationView.layer.masksToBounds = true
|
|
return animationView
|
|
}()
|
|
|
|
private func buildContents() {
|
|
let titleLabel = UILabel()
|
|
titleLabel.textAlignment = .center
|
|
titleLabel.font = UIFont.dynamicTypeTitle2.semibold()
|
|
titleLabel.text = OWSLocalizedString(
|
|
"SAFETY_NUMBER_TRANSITION_SHEET_TITLE",
|
|
comment: "Title for a sheet informing the user about the transition from phone number to user identifier based."
|
|
)
|
|
titleLabel.numberOfLines = 0
|
|
titleLabel.lineBreakMode = .byWordWrapping
|
|
stackView.addArrangedSubview(titleLabel)
|
|
|
|
let paragraphs: [String] = [
|
|
OWSLocalizedString(
|
|
"SAFETY_NUMBER_TRANSITION_SHEET_PARAGRAPH_1",
|
|
comment: "Informs the user about the transition from phone number to user identifier based."
|
|
),
|
|
OWSLocalizedString(
|
|
"SAFETY_NUMBER_TRANSITION_SHEET_PARAGRAPH_2",
|
|
comment: "Informs the user about the transition from phone number to user identifier based."
|
|
)
|
|
]
|
|
var lastParagraphLabel: UILabel!
|
|
for paragraph in paragraphs {
|
|
let paragraphLabel = UILabel()
|
|
paragraphLabel.text = paragraph
|
|
paragraphLabel.textAlignment = .natural
|
|
paragraphLabel.font = .dynamicTypeSubheadlineClamped
|
|
paragraphLabel.numberOfLines = 0
|
|
paragraphLabel.lineBreakMode = .byWordWrapping
|
|
paragraphLabel.textColor = Theme.secondaryTextAndIconColor
|
|
stackView.addArrangedSubview(paragraphLabel)
|
|
lastParagraphLabel = paragraphLabel
|
|
}
|
|
stackView.setCustomSpacing(20, after: lastParagraphLabel)
|
|
|
|
stackView.addArrangedSubview(animationView)
|
|
stackView.setCustomSpacing(24, after: animationView)
|
|
animationView.autoPinWidth(toWidthOf: self.view, offset: -48)
|
|
animationView.autoMatch(.height, to: .width, of: animationView, withMultiplier: 172/346)
|
|
|
|
let learnMoreLabel = UILabel()
|
|
learnMoreLabel.text = OWSLocalizedString(
|
|
"SAFETY_NUMBER_TRANSITION_SHEET_HELP_TEXT",
|
|
comment: "Button text for a sheet informing the user about the transition from phone number to user identifier based."
|
|
)
|
|
learnMoreLabel.textAlignment = .center
|
|
learnMoreLabel.font = .dynamicTypeBody
|
|
learnMoreLabel.textColor = Theme.isDarkThemeEnabled ? .ows_accentBlueDark : .link
|
|
learnMoreLabel.isUserInteractionEnabled = true
|
|
learnMoreLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapLearnMore)))
|
|
stackView.addArrangedSubview(learnMoreLabel)
|
|
stackView.setCustomSpacing(30, after: learnMoreLabel)
|
|
|
|
let continueButton = OWSButton(
|
|
title: OWSLocalizedString(
|
|
"ALERT_ACTION_ACKNOWLEDGE",
|
|
comment: "generic button text to acknowledge that the corresponding text was read."
|
|
)
|
|
) { [weak self] in
|
|
self?.dismiss(animated: true)
|
|
}
|
|
continueButton.layer.cornerRadius = 16
|
|
continueButton.backgroundColor = .ows_accentBlue
|
|
continueButton.titleLabel?.font = UIFont.dynamicTypeBody.semibold()
|
|
continueButton.autoSetDimension(.height, toSize: 50, relation: .greaterThanOrEqual)
|
|
stackView.addArrangedSubview(continueButton)
|
|
}
|
|
|
|
@objc
|
|
func didTapLearnMore() {
|
|
self.dismiss(animated: true) { [weak self] in
|
|
self?.parentVc?.didTapLearnMore()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Actions
|
|
|
|
@objc
|
|
private func didTapDone() {
|
|
dismiss(animated: true)
|
|
}
|
|
|
|
private func didTapLearnMore() {
|
|
let learnMoreUrl = URL(string: "https://support.signal.org/hc/articles/213134107")!
|
|
let safariVC = SFSafariViewController(url: learnMoreUrl)
|
|
present(safariVC, animated: true)
|
|
}
|
|
|
|
@objc
|
|
private func didUpdatePageControl() {
|
|
self.selectedIndex = fingerprintCarouselPageControl.currentPage
|
|
scrollToSelectedIndex()
|
|
}
|
|
|
|
@objc
|
|
private func didTapVerifyUnverify(_ gestureRecognizer: UITapGestureRecognizer) {
|
|
guard gestureRecognizer.state == .recognized else { return }
|
|
|
|
databaseStorage.write { transaction in
|
|
let isVerified = OWSIdentityManager.shared.verificationState(for: recipientAddress, transaction: transaction) == .verified
|
|
let newVerificationState: OWSVerificationState = isVerified ? .default : .verified
|
|
OWSIdentityManager.shared.setVerificationState(
|
|
newVerificationState,
|
|
identityKey: identityKey,
|
|
address: recipientAddress,
|
|
isUserInitiatedChange: true,
|
|
transaction: transaction
|
|
)
|
|
}
|
|
|
|
dismiss(animated: true)
|
|
}
|
|
|
|
private func shareFingerprint(from fromView: UIView) {
|
|
let fingerprint = fingerprints[selectedIndex]
|
|
|
|
Logger.debug("Sharing safety numbers")
|
|
|
|
let compareActivity = CompareSafetyNumbersActivity(delegate: self)
|
|
|
|
let shareFormat = NSLocalizedString(
|
|
"SAFETY_NUMBER_SHARE_FORMAT",
|
|
comment: "Snippet to share {{safety number}} with a friend. sent e.g. via SMS"
|
|
)
|
|
let shareString = String(format: shareFormat, fingerprint.displayableText)
|
|
|
|
let activityController = UIActivityViewController(
|
|
activityItems: [ shareString ],
|
|
applicationActivities: [ compareActivity ]
|
|
)
|
|
|
|
if let popoverPresentationController = activityController.popoverPresentationController {
|
|
popoverPresentationController.sourceView = fromView
|
|
}
|
|
|
|
// This value was extracted by inspecting `activityType` in the activityController.completionHandler
|
|
let iCloudActivityType = "com.apple.CloudDocsUI.AddToiCloudDrive"
|
|
activityController.excludedActivityTypes = [
|
|
.postToFacebook,
|
|
.postToWeibo,
|
|
.airDrop,
|
|
.postToTwitter,
|
|
.init(rawValue: iCloudActivityType) // This isn't being excluded. RADAR https://openradar.appspot.com/27493621
|
|
]
|
|
|
|
present(activityController, animated: true)
|
|
}
|
|
|
|
fileprivate func didTapToScan() {
|
|
let viewController = FingerprintScanViewController(
|
|
recipientAddress: recipientAddress,
|
|
recipientIdentity: recipientIdentity,
|
|
fingerprints: .multiFingerprint(self.fingerprints, defaultIndex: self.selectedIndex)
|
|
)
|
|
navigationController?.pushViewController(viewController, animated: true)
|
|
}
|
|
|
|
private func scrollToSelectedIndex(animated: Bool = true) {
|
|
let xOffset: CGFloat
|
|
if selectedIndex == 0 {
|
|
xOffset = 0
|
|
} else {
|
|
xOffset = (CGFloat(selectedIndex) * UIScreen.main.bounds.width) - (Constants.interCardSpacing + Constants.cardHInset)
|
|
}
|
|
fingerprintCarousel.setContentOffset(.init(x: xOffset, y: 0), animated: animated)
|
|
}
|
|
|
|
// MARK: Notifications
|
|
|
|
private var identityStateChangeObserver: Any?
|
|
|
|
private func identityStateDidChange() {
|
|
AssertIsOnMainThread()
|
|
updateVerificationStateLabel()
|
|
}
|
|
|
|
// MARK: - Constants
|
|
|
|
enum Constants {
|
|
static let cardHInset: CGFloat = .scaleFromIPhone5To7Plus(30, 53)
|
|
static var interCardSpacing: CGFloat = cardHInset / 2
|
|
|
|
// Link doesn't matter, we will override tap behavior.
|
|
static let transitionLearnMoreUrl = "https://support.signal.org/"
|
|
static let learnMoreUrl = "https://support.signal.org/learnMore"
|
|
}
|
|
}
|
|
|
|
extension MultiFingerprintViewController: CompareSafetyNumbersActivityDelegate {
|
|
|
|
public func compareSafetyNumbersActivitySucceeded(activity: CompareSafetyNumbersActivity) {
|
|
FingerprintScanViewController.showVerificationSucceeded(
|
|
from: self,
|
|
identityKey: identityKey,
|
|
recipientAddress: recipientAddress,
|
|
contactName: contactName,
|
|
tag: logTag
|
|
)
|
|
}
|
|
|
|
public func compareSafetyNumbersActivity(_ activity: CompareSafetyNumbersActivity, failedWithError error: Error) {
|
|
let isUserError = (error as NSError).code == OWSErrorCode.userError.rawValue
|
|
|
|
FingerprintScanViewController.showVerificationFailed(
|
|
from: self,
|
|
isUserError: isUserError,
|
|
localizedErrorDescription: error.userErrorDescription,
|
|
tag: logTag
|
|
)
|
|
}
|
|
|
|
}
|
|
|
|
extension MultiFingerprintViewController: UITextViewDelegate {
|
|
|
|
public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
|
if URL.absoluteString == Constants.transitionLearnMoreUrl {
|
|
DependenciesBridge.shared.db.write {
|
|
self.showTransitionSheet($0)
|
|
}
|
|
} else if URL.absoluteString == Constants.learnMoreUrl {
|
|
self.didTapLearnMore()
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
extension MultiFingerprintViewController: UIScrollViewDelegate {
|
|
|
|
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
let selectedIndex = Int(scrollView.contentOffset.x / (scrollView.frame.width - (Constants.cardHInset * 2)))
|
|
self.selectedIndex = selectedIndex
|
|
self.fingerprintCarouselPageControl.currentPage = selectedIndex
|
|
}
|
|
}
|