// // WelcomeContentView.swift // MullvadVPN // // Created by Mojgan on 2023-06-27. // Copyright © 2025 Mullvad VPN AB. All rights reserved. // import UIKit protocol WelcomeContentViewDelegate: AnyObject, Sendable { func didTapPurchaseButton(welcomeContentView: WelcomeContentView, button: AppButton) func didTapInfoButton(welcomeContentView: WelcomeContentView, button: UIButton) func didTapCopyButton(welcomeContentView: WelcomeContentView, button: UIButton) } struct WelcomeViewModel: Sendable { let deviceName: String let accountNumber: String } final class WelcomeContentView: UIView, Sendable { private var revertCopyImageWorkItem: DispatchWorkItem? private let titleLabel: UILabel = { let label = UILabel() label.font = .mullvadLarge label.textColor = .white label.adjustsFontForContentSizeCategory = true label.lineBreakMode = .byWordWrapping label.numberOfLines = .zero label.text = NSLocalizedString("Congrats!", comment: "") return label }() private let subtitleLabel: UILabel = { let label = UILabel() label.font = .mullvadSmall label.textColor = .white label.adjustsFontForContentSizeCategory = true label.lineBreakMode = .byWordWrapping label.numberOfLines = .zero label.text = NSLocalizedString("Here’s your account number. Save it!", comment: "") return label }() private let accountNumberLabel: UILabel = { let label = UILabel() label.setAccessibilityIdentifier(.welcomeAccountNumberLabel) label.adjustsFontForContentSizeCategory = true label.lineBreakMode = .byWordWrapping label.numberOfLines = .zero label.font = .mullvadMedium label.textColor = .white return label }() private let copyButton: UIButton = { let button = UIButton(type: .system) button.setAccessibilityIdentifier(.copyButton) button.adjustsImageSizeForAccessibilityContentSizeCategory = true button.tintColor = .white button.setContentHuggingPriority(.defaultHigh, for: .horizontal) button.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) return button }() private let deviceNameLabel: UILabel = { let label = UILabel() label.adjustsFontForContentSizeCategory = true label.translatesAutoresizingMaskIntoConstraints = false label.font = .mullvadSmall label.textColor = .white label.setContentHuggingPriority(.defaultLow, for: .horizontal) label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) label.numberOfLines = 0 return label }() private let infoButton: UIButton = { let button = IncreasedHitButton(type: .system) button.setAccessibilityIdentifier(.infoButton) button.adjustsImageSizeForAccessibilityContentSizeCategory = true button.tintColor = .white button.translatesAutoresizingMaskIntoConstraints = false button.setImage(UIImage.Buttons.info, for: .normal) button.setContentHuggingPriority(.defaultHigh, for: .horizontal) button.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) return button }() private let descriptionLabel: UILabel = { let label = UILabel() label.font = .mullvadSmall label.adjustsFontForContentSizeCategory = true label.textColor = .white label.numberOfLines = .zero label.lineBreakMode = .byWordWrapping label.lineBreakStrategy = [] label.text = [ NSLocalizedString( "To start using the app, you first need to add time to your account.", comment: "" ), NSLocalizedString( "Either buy credit on our website or redeem a voucher.", comment: "" ), ].joined(separator: " ") return label }() private let purchaseButton: InAppPurchaseButton = { let button = InAppPurchaseButton() button.setAccessibilityIdentifier(.purchaseButton) let localizedString = NSLocalizedString("Add time", comment: "") button.setTitle(localizedString, for: .normal) return button }() private let textsStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .vertical return stackView }() private let accountRowStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .horizontal stackView.distribution = .fill stackView.spacing = UIMetrics.padding8 return stackView }() private let deviceRowStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .horizontal stackView.distribution = .fill return stackView }() private let spacerView: UIView = { let view = UIView() view.setContentHuggingPriority(.required, for: .horizontal) view.setContentCompressionResistancePriority(.required, for: .horizontal) return view }() private let buttonsStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .vertical stackView.spacing = UIMetrics.interButtonSpacing return stackView }() private let scrollView = UIScrollView() weak var delegate: WelcomeContentViewDelegate? var viewModel: WelcomeViewModel? { didSet { accountNumberLabel.text = viewModel?.accountNumber deviceNameLabel.text = String( format: NSLocalizedString("Device name: %@", comment: ""), viewModel?.deviceName ?? "" ) } } override init(frame: CGRect) { super.init(frame: frame) setAccessibilityIdentifier(.welcomeView) backgroundColor = .primaryColor backgroundColor = .secondaryColor configureUI() addActions() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func configureUI() { accountRowStackView.addArrangedSubview(accountNumberLabel) accountRowStackView.addArrangedSubview(copyButton) accountRowStackView.addArrangedSubview(UIView()) // To push content to the left. textsStackView.addArrangedSubview(titleLabel) textsStackView.setCustomSpacing(UIMetrics.padding8, after: titleLabel) textsStackView.addArrangedSubview(subtitleLabel) textsStackView.setCustomSpacing(UIMetrics.padding16, after: subtitleLabel) textsStackView.addArrangedSubview(accountRowStackView) textsStackView.setCustomSpacing(UIMetrics.padding16, after: accountRowStackView) deviceRowStackView.addArrangedSubview(deviceNameLabel) deviceRowStackView.setCustomSpacing(UIMetrics.padding8, after: deviceNameLabel) deviceRowStackView.addArrangedSubview(infoButton) deviceRowStackView.addArrangedSubview(spacerView) textsStackView.addArrangedSubview(deviceRowStackView) textsStackView.setCustomSpacing(UIMetrics.padding16, after: deviceRowStackView) textsStackView.addArrangedSubview(descriptionLabel) buttonsStackView.addArrangedSubview(purchaseButton) addConstraints() showCheckmark(false) } private func addConstraints() { scrollView.addConstrainedSubviews([textsStackView]) { textsStackView.pinEdgesToSuperviewMargins( PinnableEdges([ .leading(0), .trailing(0), ])) textsStackView.pinEdgesToSuperview( PinnableEdges([ .top(0), .bottom(0), ])) } addConstrainedSubviews([scrollView, buttonsStackView]) { scrollView.pinEdgesToSuperviewMargins( PinnableEdges([ .top(UIMetrics.contentLayoutMargins.top), .leading(0), .trailing(0), ])) buttonsStackView.pinEdgesToSuperviewMargins( PinnableEdges([ .leading(UIMetrics.padding8), .trailing(UIMetrics.padding8), .bottom(UIMetrics.contentLayoutMargins.bottom), ])) buttonsStackView.topAnchor.constraint( equalTo: scrollView.bottomAnchor, constant: UIMetrics.contentLayoutMargins.top ) } } private func addActions() { [purchaseButton, infoButton, copyButton].forEach { $0.addTarget(self, action: #selector(tapped(button:)), for: .touchUpInside) } } @objc private func tapped(button: AppButton) { switch button.accessibilityIdentifier { case AccessibilityIdentifier.purchaseButton.asString: delegate?.didTapPurchaseButton(welcomeContentView: self, button: button) case AccessibilityIdentifier.infoButton.asString: delegate?.didTapInfoButton(welcomeContentView: self, button: button) case AccessibilityIdentifier.copyButton.asString: didTapCopyAccountNumber() default: return } } private func showCheckmark(_ showCheckmark: Bool) { if showCheckmark { let tickIcon = UIImage(named: "IconTick") copyButton.setImage(tickIcon, for: .normal) copyButton.tintColor = .successColor } else { let copyIcon = UIImage(named: "IconCopy") copyButton.setImage(copyIcon, for: .normal) copyButton.tintColor = .white } } @objc private func didTapCopyAccountNumber() { let delayedWorkItem = DispatchWorkItem { [weak self] in self?.showCheckmark(false) } revertCopyImageWorkItem?.cancel() revertCopyImageWorkItem = delayedWorkItem showCheckmark(true) delegate?.didTapCopyButton(welcomeContentView: self, button: copyButton) DispatchQueue.main.asyncAfter( deadline: .now() + .seconds(2), execute: delayedWorkItem ) } }