398 lines
19 KiB
Swift
398 lines
19 KiB
Swift
//
|
|
// CredentialsController.swift
|
|
// PrivadoVPN
|
|
//
|
|
// Created by Zhandos Bolatbekov on 13.10.2021.
|
|
// Copyright © 2021 Privado LLC. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
final class CredentialsController: UIViewController {
|
|
|
|
private enum Constants {
|
|
|
|
enum Font {
|
|
static let headerTitle = UIFont.primary(size: 30, weight: .semibold)
|
|
static let headerSubtitle = UIFont.primary(size: 21.3, weight: .semibold)
|
|
static let email = UIFont.primary(size: 20.7, weight: .medium)
|
|
static let credentials = UIFont.primary(size: 20, weight: .medium)
|
|
static let prompt = UIFont.primary(size: 14.7, weight: .medium)
|
|
static let buttonTitle = UIFont.primary(size: 21.3, weight: .bold)
|
|
}
|
|
|
|
enum LocalizedString {
|
|
static let headerTitle = "credentials.header.title"
|
|
static let headerSubtitle = "credentials.header.subtitle"
|
|
static let username = "credentials.username"
|
|
static let password = "credentials.password"
|
|
static let prompt = "credentials.prompt"
|
|
static let buttonTutorial = "credentials.button.tutorial"
|
|
static let buttonUseApp = "credentials.button.useApp"
|
|
}
|
|
|
|
enum Color {
|
|
static let headerTitle = Colors.basicText.color
|
|
static let headerSubtitle = Colors.SystemCTA.lightPurple.color
|
|
static let emailLabel = Colors.SystemCTA.lightPurple.color
|
|
static let credentialName = Colors.basicText.color
|
|
static let credentialValue = Colors.SystemCTA.lightPurple.color
|
|
static let copyButton = Colors.SystemCTA.lightPurple.color
|
|
static let prompt = Colors.SystemCTA.lightPurple.color
|
|
|
|
static let background = UIColor(rgb: 0x202350)
|
|
static let credentialItemBackground = UIColor(rgb: 0x202864)
|
|
static let buttonBackground = UIColor(rgb: 0x3E449C)
|
|
static let buttonsContainerBackground = UIColor(rgb: 0x202864)
|
|
static let buttonTitle = UIColor.white
|
|
}
|
|
|
|
enum Geometry {
|
|
static let credentialLabelIndentX: CGFloat = 21
|
|
static let credentialLabelTop: CGFloat = 26.7
|
|
static let credentialLabelBottom: CGFloat = 22
|
|
static let credentialItemsIndentY: CGFloat = 6
|
|
static let credentialsSeparatorHeight: CGFloat = 3.3
|
|
static let credentialsContainerCornerRadius: CGFloat = 16.7
|
|
|
|
static let headerTitleTop: CGFloat = 66.7
|
|
static let commonIndentX: CGFloat = 30
|
|
static let headerSubtitleTop: CGFloat = 10
|
|
static let emailTop: CGFloat = 36.7
|
|
static let credentialsTop: CGFloat = 20
|
|
static let promptTop: CGFloat = 25
|
|
static let promptIndentX: CGFloat = 40
|
|
static let promptBottom: CGFloat = 40
|
|
static let promptLineSpacing: CGFloat = 5
|
|
static let copyButtonSize: CGFloat = 32
|
|
|
|
static let emailMinScaleFactor: CGFloat = 0.8
|
|
static let credentialMinScaleFactor: CGFloat = 0.8
|
|
static let buttonsIndentY: CGFloat = 36
|
|
static let buttonsIndentX: CGFloat = 31
|
|
static let buttonsSpacing: CGFloat = 20
|
|
static let buttonHeight: CGFloat = 60
|
|
|
|
static let buttonsContainerShadowOffset = CGSize(width: 0, height: -1.7)
|
|
static let buttonsContainerShadowRadius: CGFloat = 10
|
|
}
|
|
|
|
static let buttonsContainerShadowOpacity: Float = 0.33
|
|
static let copyButtonImageName = "copy"
|
|
static let credentialSeparator = " "
|
|
}
|
|
|
|
// MARK: - Properties
|
|
|
|
private let output: CredentialsControllerOutput
|
|
|
|
private var buttonsContainer: UIView?
|
|
|
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
|
return .lightContent
|
|
}
|
|
|
|
private var credentialLabels = [UILabel]()
|
|
|
|
// MARK: - Init
|
|
|
|
init(output: CredentialsControllerOutput) {
|
|
self.output = output
|
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
required init?(coder: NSCoder) { nil }
|
|
|
|
// MARK: - Life-cycle
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
self.createUI()
|
|
}
|
|
|
|
override func viewDidLayoutSubviews() {
|
|
super.viewDidLayoutSubviews()
|
|
self.buttonsContainer?.dropShadow(shadowColor: .black,
|
|
fillColor: Constants.Color.buttonsContainerBackground,
|
|
opacity: Constants.buttonsContainerShadowOpacity,
|
|
offset: Constants.Geometry.buttonsContainerShadowOffset,
|
|
shadowRadius: Constants.Geometry.buttonsContainerShadowRadius)
|
|
|
|
self.syncCredentialsFontSize()
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func syncCredentialsFontSize() {
|
|
let minFontSize: CGFloat = self.credentialLabels.reduce(into: Constants.Font.credentials.pointSize) { result, label in
|
|
if let size = label.getFontSizeForLabel() {
|
|
result = min(result, size)
|
|
}
|
|
}
|
|
for i in 0..<self.credentialLabels.count {
|
|
self.credentialLabels[i].font = self.credentialLabels[i].font.withSize(minFontSize)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - UI
|
|
|
|
extension CredentialsController {
|
|
|
|
private func createUI() {
|
|
self.view.backgroundColor = Constants.Color.background
|
|
let headerTitle = self.createHeaderTitle()
|
|
let headerSubtitle = self.createHeaderSubtitle()
|
|
let emailLabel = self.createEmailLabel()
|
|
let credentials = self.createCredentialsContainer()
|
|
let promptLabel = self.createPromptLabel()
|
|
|
|
let contentView = UIView()
|
|
contentView.translatesAutoresizingMaskIntoConstraints = false
|
|
[headerTitle, headerSubtitle, emailLabel, credentials, promptLabel]
|
|
.forEach(contentView.addSubview(_:))
|
|
let scrollView = UIScrollView()
|
|
scrollView.alwaysBounceVertical = true
|
|
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
scrollView.addSubview(contentView)
|
|
self.view.addSubview(scrollView)
|
|
|
|
let buttons = self.createButtons()
|
|
self.view.addSubview(buttons)
|
|
self.buttonsContainer = buttons
|
|
|
|
NSLayoutConstraint.activate([
|
|
headerTitle.topAnchor.constraint(equalTo: contentView.topAnchor, constant: Constants.Geometry.headerTitleTop),
|
|
headerTitle.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.Geometry.commonIndentX),
|
|
headerTitle.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.Geometry.commonIndentX),
|
|
|
|
headerSubtitle.topAnchor.constraint(equalTo: headerTitle.bottomAnchor, constant: Constants.Geometry.headerSubtitleTop),
|
|
headerSubtitle.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.Geometry.commonIndentX),
|
|
headerSubtitle.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.Geometry.commonIndentX),
|
|
|
|
emailLabel.topAnchor.constraint(equalTo: headerSubtitle.bottomAnchor, constant: Constants.Geometry.emailTop),
|
|
emailLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.Geometry.commonIndentX),
|
|
emailLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.Geometry.commonIndentX),
|
|
|
|
credentials.topAnchor.constraint(equalTo: emailLabel.bottomAnchor, constant: Constants.Geometry.credentialsTop),
|
|
credentials.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.Geometry.commonIndentX),
|
|
credentials.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.Geometry.commonIndentX),
|
|
|
|
promptLabel.topAnchor.constraint(equalTo: credentials.bottomAnchor, constant: Constants.Geometry.promptTop),
|
|
promptLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.Geometry.promptIndentX),
|
|
promptLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.Geometry.promptIndentX),
|
|
promptLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -Constants.Geometry.promptBottom),
|
|
|
|
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
|
|
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
|
|
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
|
|
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
|
|
contentView.widthAnchor.constraint(equalTo: self.view.widthAnchor),
|
|
|
|
scrollView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
|
|
scrollView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
|
|
scrollView.topAnchor.constraint(equalTo: self.view.topAnchor),
|
|
scrollView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
|
|
|
|
buttons.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
|
|
buttons.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
|
|
buttons.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
|
|
])
|
|
}
|
|
|
|
private func createCredentialsContainer() -> UIView {
|
|
let view = UIView()
|
|
view.layer.cornerRadius = Constants.Geometry.credentialsContainerCornerRadius
|
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
view.clipsToBounds = true
|
|
let stackView = UIStackView(arrangedSubviews: [
|
|
self.createCredentialItemContainer(name: NSLocalizedString(Constants.LocalizedString.username, comment: "") + Constants.credentialSeparator,
|
|
value: self.output.username),
|
|
self.createCredentialItemContainer(name: NSLocalizedString(Constants.LocalizedString.password, comment: "") + Constants.credentialSeparator,
|
|
value: self.output.password)
|
|
])
|
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
stackView.distribution = .fillEqually
|
|
stackView.axis = .vertical
|
|
stackView.spacing = Constants.Geometry.credentialsSeparatorHeight
|
|
view.addSubview(stackView)
|
|
NSLayoutConstraint.activate([
|
|
stackView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
|
])
|
|
return view
|
|
}
|
|
|
|
private func createCredentialItemContainer(name: String, value: String?) -> UIView {
|
|
let view = UIView()
|
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
view.backgroundColor = Constants.Color.credentialItemBackground
|
|
|
|
let label = self.createCredentialLabel(name: name, value: value)
|
|
self.credentialLabels.append(label)
|
|
view.addSubview(label)
|
|
|
|
let copyButton = self.createCopyButton(for: label)
|
|
view.addSubview(copyButton)
|
|
NSLayoutConstraint.activate([
|
|
label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Constants.Geometry.credentialLabelIndentX),
|
|
label.trailingAnchor.constraint(equalTo: copyButton.leadingAnchor, constant: -Constants.Geometry.credentialLabelIndentX / 2),
|
|
label.topAnchor.constraint(equalTo: view.topAnchor, constant: Constants.Geometry.credentialLabelTop),
|
|
label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -Constants.Geometry.credentialLabelBottom),
|
|
|
|
copyButton.centerYAnchor.constraint(equalTo: label.centerYAnchor),
|
|
copyButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Constants.Geometry.credentialLabelIndentX),
|
|
copyButton.heightAnchor.constraint(equalToConstant: Constants.Geometry.copyButtonSize),
|
|
copyButton.widthAnchor.constraint(equalToConstant: Constants.Geometry.copyButtonSize)
|
|
])
|
|
return view
|
|
}
|
|
|
|
private func createHeaderTitle() -> UILabel {
|
|
let label = UILabel()
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
label.font = Constants.Font.headerTitle
|
|
label.textAlignment = .center
|
|
label.textColor = Constants.Color.headerTitle
|
|
label.numberOfLines = 0
|
|
label.text = NSLocalizedString(Constants.LocalizedString.headerTitle, comment: "")
|
|
return label
|
|
}
|
|
|
|
private func createHeaderSubtitle() -> UILabel {
|
|
let label = UILabel()
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
label.font = Constants.Font.headerSubtitle
|
|
label.textAlignment = .center
|
|
label.textColor = Constants.Color.headerSubtitle
|
|
label.numberOfLines = 0
|
|
label.text = NSLocalizedString(Constants.LocalizedString.headerSubtitle, comment: "")
|
|
return label
|
|
}
|
|
|
|
private func createEmailLabel() -> UILabel {
|
|
let label = UILabel()
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
label.font = Constants.Font.email
|
|
label.textAlignment = .center
|
|
label.textColor = Constants.Color.emailLabel
|
|
label.numberOfLines = 1
|
|
label.adjustsFontSizeToFitWidth = true
|
|
label.minimumScaleFactor = Constants.Geometry.emailMinScaleFactor
|
|
label.lineBreakMode = .byTruncatingTail
|
|
label.text = self.output.email
|
|
return label
|
|
}
|
|
|
|
private func createCredentialLabel(name: String, value: String?) -> UILabel {
|
|
let label = UILabel()
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
label.font = Constants.Font.credentials
|
|
label.numberOfLines = 1
|
|
label.adjustsFontSizeToFitWidth = true
|
|
label.minimumScaleFactor = Constants.Geometry.credentialMinScaleFactor
|
|
label.lineBreakMode = .byTruncatingTail
|
|
label.tag = self.credentialLabels.count
|
|
|
|
let attributedString = NSMutableAttributedString(string: name,
|
|
attributes: [.foregroundColor: Constants.Color.credentialName])
|
|
let suffix = NSAttributedString(string: value ?? "",
|
|
attributes: [.foregroundColor: Constants.Color.credentialValue])
|
|
attributedString.append(suffix)
|
|
|
|
label.attributedText = attributedString
|
|
return label
|
|
}
|
|
|
|
private func createCopyButton(for label: UILabel) -> UIButton {
|
|
let button = UIButton()
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
button.tintColor = Constants.Color.copyButton
|
|
button.setImage(UIImage(imageLiteralResourceName: Constants.copyButtonImageName), for: .normal)
|
|
button.tag = label.tag
|
|
button.addTarget(self, action: #selector(self.didTapCopyButton(sender:)), for: .touchUpInside)
|
|
return button
|
|
}
|
|
|
|
private func createPromptLabel() -> UILabel {
|
|
let label = UILabel()
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
label.font = Constants.Font.prompt
|
|
label.textColor = Constants.Color.prompt
|
|
label.numberOfLines = 0
|
|
|
|
let paragraphStyle = NSMutableParagraphStyle()
|
|
paragraphStyle.lineSpacing = Constants.Geometry.promptLineSpacing
|
|
label.attributedText = NSAttributedString(
|
|
string: NSLocalizedString(Constants.LocalizedString.prompt, comment: ""),
|
|
attributes: [.paragraphStyle: paragraphStyle]
|
|
)
|
|
return label
|
|
}
|
|
|
|
private func createButtons() -> UIView {
|
|
let view = UIView()
|
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
view.backgroundColor = Constants.Color.buttonsContainerBackground
|
|
|
|
let tutorialButton = self.createActionButton(title: NSLocalizedString(Constants.LocalizedString.buttonTutorial, comment: ""))
|
|
tutorialButton.addTarget(self, action: #selector(self.didTapTutorialButton), for: .touchUpInside)
|
|
|
|
let skipButton = self.createActionButton(title: NSLocalizedString(Constants.LocalizedString.buttonUseApp, comment: ""))
|
|
skipButton.addTarget(self, action: #selector(self.didTapSkipButton), for: .touchUpInside)
|
|
|
|
let stackView = UIStackView(arrangedSubviews: [skipButton])
|
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
stackView.spacing = Constants.Geometry.buttonsSpacing
|
|
stackView.axis = .vertical
|
|
stackView.distribution = .fillEqually
|
|
view.addSubview(stackView)
|
|
|
|
NSLayoutConstraint.activate([
|
|
stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: Constants.Geometry.buttonsIndentY),
|
|
stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -Constants.Geometry.buttonsIndentY),
|
|
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Constants.Geometry.buttonsIndentX),
|
|
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Constants.Geometry.buttonsIndentX)
|
|
])
|
|
return view
|
|
}
|
|
|
|
private func createActionButton(title: String) -> UIButton {
|
|
let button = UIButton()
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
button.backgroundColor = Constants.Color.buttonBackground
|
|
button.layer.cornerRadius = Constants.Geometry.buttonHeight / 2
|
|
let title = NSAttributedString(
|
|
string: title,
|
|
attributes: [.font: Constants.Font.buttonTitle,
|
|
.foregroundColor: Constants.Color.buttonTitle]
|
|
)
|
|
button.setAttributedTitle(title, for: .normal)
|
|
button.heightAnchor.constraint(equalToConstant: Constants.Geometry.buttonHeight).isActive = true
|
|
return button
|
|
}
|
|
|
|
@objc
|
|
private func didTapTutorialButton() {
|
|
self.output.didTapTutorial()
|
|
}
|
|
|
|
@objc
|
|
private func didTapSkipButton() {
|
|
self.output.didTapSkip()
|
|
}
|
|
|
|
@objc
|
|
private func didTapCopyButton(sender: UIButton) {
|
|
guard let label = self.credentialLabels.first(where: { $0.tag == sender.tag }),
|
|
let text = label.attributedText?.string,
|
|
let value = text.split(separator: String.Element(Constants.credentialSeparator)).last else { return }
|
|
|
|
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
|
UIPasteboard.general.string = String(value)
|
|
}
|
|
}
|