Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/Registration/UserInterface/RegistrationChangePhoneNumberConfirmationViewController.swift
1 views
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
import SignalServiceKit
import SignalUI
import UIKit

// MARK: - RegistrationChangePhoneNumberConfirmationPresenter

protocol RegistrationChangePhoneNumberConfirmationPresenter: AnyObject {
    func confirmChangeNumber(newE164: E164)

    func returnToPhoneNumberEntry()
}

// MARK: - RegistrationChangePhoneNumberConfirmationViewController

class RegistrationChangePhoneNumberConfirmationViewController: OWSViewController, OWSNavigationChildController {

    var preferredNavigationBarStyle: OWSNavigationBarStyle {
        return .solid
    }

    var navbarBackgroundColorOverride: UIColor? {
        return view.backgroundColor
    }

    private var state: RegistrationPhoneNumberViewState.ChangeNumberConfirmation
    private weak var presenter: RegistrationChangePhoneNumberConfirmationPresenter?

    private lazy var descriptionLabel: UILabel = {
        let label = UILabel()
        label.textAlignment = .center
        label.numberOfLines = 0
        label.lineBreakMode = .byWordWrapping
        return label
    }()

    private lazy var phoneNumberLabel: UILabel = {
        let label = UILabel()
        label.font = .dynamicTypeTitle2.semibold()
        label.textColor = .Signal.label
        label.textAlignment = .center
        return label
    }()

    private func reloadTextLabels() {
        let descriptionFormat = OWSLocalizedString(
            "SETTINGS_CHANGE_PHONE_NUMBER_CONFIRM_DESCRIPTION_FORMAT",
            comment: "Format for the description text in the 'change phone number splash' view. Embeds: {{ %1$@ the old phone number, %2$@ the new phone number }}.",
        )
        let oldPhoneNumberFormatted = PhoneNumber.bestEffortLocalizedPhoneNumber(e164: state.oldE164.stringValue)
        let newPhoneNumberFormatted = PhoneNumber.bestEffortLocalizedPhoneNumber(e164: state.newE164.stringValue)
        let descriptionText = String.nonPluralLocalizedStringWithFormat(
            descriptionFormat,
            oldPhoneNumberFormatted,
            newPhoneNumberFormatted,
        )
        let descriptionAttributedText = NSMutableAttributedString(
            string: descriptionText,
            attributes: [
                .foregroundColor: UIColor.Signal.secondaryLabel,
                .font: UIFont.dynamicTypeBody,
            ],
        )
        descriptionAttributedText.setAttributes(
            [.foregroundColor: UIColor.Signal.label],
            forSubstring: oldPhoneNumberFormatted,
        )
        descriptionAttributedText.setAttributes(
            [.foregroundColor: UIColor.Signal.label],
            forSubstring: newPhoneNumberFormatted,
        )
        descriptionLabel.attributedText = descriptionAttributedText
        phoneNumberLabel.text = newPhoneNumberFormatted
    }

    private lazy var warningLabel: UILabel = {
        let label = UILabel()
        label.textColor = .ows_accentRed
        label.numberOfLines = 0
        label.font = .dynamicTypeSubheadlineClamped
        label.accessibilityIdentifier = "registration.phonenumber.validationWarningLabel"
        return label
    }()

    init(
        state: RegistrationPhoneNumberViewState.ChangeNumberConfirmation,
        presenter: RegistrationChangePhoneNumberConfirmationPresenter,
    ) {
        self.state = state
        self.presenter = presenter
        super.init()
    }

    func updateState(_ state: RegistrationPhoneNumberViewState.ChangeNumberConfirmation) {
        self.state = state
        updateContents()
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .Signal.groupedBackground
        title = OWSLocalizedString(
            "SETTINGS_CHANGE_PHONE_NUMBER_VIEW_TITLE",
            comment: "Title for the 'change phone number' views in settings.",
        )

        // Text
        reloadTextLabels()
        let phoneNumberContainerView = UIView()
        phoneNumberContainerView.backgroundColor = .Signal.secondaryGroupedBackground
        if #available(iOS 26, *) {
            phoneNumberContainerView.layer.cornerRadius = 26
        } else {
            phoneNumberContainerView.layer.cornerRadius = 10
        }
        phoneNumberContainerView.directionalLayoutMargins = NSDirectionalEdgeInsets(margin: 24)
        phoneNumberContainerView.addSubview(phoneNumberLabel)
        phoneNumberLabel.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            phoneNumberLabel.topAnchor.constraint(equalTo: phoneNumberContainerView.layoutMarginsGuide.topAnchor),
            phoneNumberLabel.leadingAnchor.constraint(equalTo: phoneNumberContainerView.layoutMarginsGuide.leadingAnchor),
            phoneNumberLabel.bottomAnchor.constraint(equalTo: phoneNumberContainerView.layoutMarginsGuide.bottomAnchor),
            phoneNumberLabel.trailingAnchor.constraint(equalTo: phoneNumberContainerView.layoutMarginsGuide.trailingAnchor),
        ])

        // Buttons
        let continueButton = UIButton(
            configuration: .largePrimary(title: OWSLocalizedString(
                "SETTINGS_CHANGE_PHONE_NUMBER_CONFIRM_BUTTON",
                comment: "Label for the 'confirm change phone number' button in the 'change phone number' views.",
            )),
            primaryAction: UIAction { [weak self] _ in
                self?.didTapContinue()
            },
        )
        continueButton.isEnabled = state.rateLimitedError?.canSubmit(e164: self.state.newE164, dateProvider: Date.provider) ?? true
        let editButton = UIButton(
            configuration: .largeSecondary(title: OWSLocalizedString(
                "SETTINGS_CHANGE_PHONE_NUMBER_BACK_TO_EDIT_BUTTON",
                comment: "Label for the 'edit phone number' button in the 'change phone number' views.",
            )),
            primaryAction: UIAction { [weak self] _ in
                self?.didTapEdit()
            },
        )

        let stackView = addStaticContentStackView(arrangedSubviews: [
            descriptionLabel,
            phoneNumberContainerView,
            warningLabel,
            .vStretchingSpacer(),
            [continueButton, editButton].enclosedInVerticalStackView(isFullWidthButtons: true),
        ])
        stackView.spacing = 20
        stackView.setCustomSpacing(12, after: phoneNumberContainerView)

        updateContents()
    }

    private var rateLimitErrorTimer: Timer?

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        updateContents()

        // We only need this timer if the user has been rate limited, but it's simpler to always
        // start it.
        rateLimitErrorTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            self?.updateContents()
        }
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)

        rateLimitErrorTimer?.invalidate()
        rateLimitErrorTimer = nil
    }

    override func themeDidChange() {
        super.themeDidChange()
        updateContents()
    }

    private func updateContents() {
        reloadTextLabels()

        let now = Date()
        if
            let rateLimitedError = state.rateLimitedError,
            !rateLimitedError.canSubmit(e164: self.state.newE164, dateProvider: { now })
        {
            warningLabel.text = rateLimitedError.warningLabelText(dateProvider: { now })
            warningLabel.isHiddenInStackView = false
        } else {
            warningLabel.isHiddenInStackView = true
        }
    }

    private func didTapEdit() {
        AssertIsOnMainThread()

        presenter?.returnToPhoneNumberEntry()
    }

    private func didTapContinue() {
        AssertIsOnMainThread()

        guard state.rateLimitedError?.canSubmit(e164: self.state.newE164, dateProvider: Date.provider) != false else {
            return
        }

        presenter?.confirmChangeNumber(newE164: state.newE164)
    }
}

// MARK: -

#if DEBUG

private class PreviewRegistrationChangePhoneNumberConfirmationPresenter: RegistrationChangePhoneNumberConfirmationPresenter {
    func confirmChangeNumber(newE164: E164) {
        print("confirmChangeNumber")
    }

    func returnToPhoneNumberEntry() {
        print("returnToPhoneNumberEntry")
    }
}

@available(iOS 17, *)
#Preview {
    let semaphore = DispatchSemaphore(value: 0)
    Task.detached {
        await MockSSKEnvironment.activate()
        semaphore.signal()
    }
    semaphore.wait()
    let presenter = PreviewRegistrationChangePhoneNumberConfirmationPresenter()
    return UINavigationController(
        rootViewController: RegistrationChangePhoneNumberConfirmationViewController(
            state: RegistrationPhoneNumberViewState.ChangeNumberConfirmation(
                oldE164: E164("+12395550180")!,
                newE164: E164("+12395550185")!,
                rateLimitedError: nil,
            ),
            presenter: presenter,
        ),
    )
}

#endif