Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/src/ViewControllers/AppSettings/CurrencyPickerViewController.swift
1 views
//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalServiceKit
import SignalUI

protocol CurrencyPickerDataSource {
    var currentCurrencyCode: Currency.Code { get }
    var preferredCurrencyInfos: [Currency.Info] { get }
    var supportedCurrencyInfos: [Currency.Info] { get }

    var updateTableContents: (() -> Void)? { get set }
}

class CurrencyPickerViewController<DataSourceType: CurrencyPickerDataSource>: OWSTableViewController2, UISearchResultsUpdating {

    private var dataSource: DataSourceType
    private let completion: (Currency.Code) -> Void

    init(dataSource: DataSourceType, completion: @escaping (Currency.Code) -> Void) {
        self.dataSource = dataSource
        self.completion = completion

        super.init()

        self.dataSource.updateTableContents = { [weak self] in self?.updateTableContents() }

        navigationItem.searchController = searchController
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        title = OWSLocalizedString(
            "CURRENCY_PICKER_VIEW_TITLE",
            comment: "Title for the 'currency picker' view in the app settings.",
        )

        navigationItem.leftBarButtonItem = .cancelButton { [weak self] in
            self?.dismissPicker()
        }

        searchController.hidesNavigationBarDuringPresentation = false
        searchController.searchResultsUpdater = self
        searchController.searchBar.placeholder = CommonStrings.searchBarPlaceholder

        updateTableContents()
    }

    private func updateTableContents() {
        if searchController.isActive, let searchText, !searchText.isEmpty {
            updateTableContentsForSearch(searchText: searchText)
        } else {
            updateTableContentsDefault()
        }
    }

    private func updateTableContentsDefault() {
        let contents = OWSTableContents()

        let currentCurrencyCode = dataSource.currentCurrencyCode
        let preferredCurrencyInfos = dataSource.preferredCurrencyInfos
        let supportedCurrencyInfos = dataSource.supportedCurrencyInfos

        let preferredSection = OWSTableSection()
        preferredSection.customHeaderHeight = 12
        preferredSection.separatorInsetLeading = OWSTableViewController2.cellHInnerMargin
        for currencyInfo in preferredCurrencyInfos {
            preferredSection.add(buildTableItem(
                forCurrencyInfo: currencyInfo,
                currentCurrencyCode: currentCurrencyCode,
            ))
        }
        contents.add(preferredSection)

        let supportedSection = OWSTableSection()
        supportedSection.separatorInsetLeading = OWSTableViewController2.cellHInnerMargin
        supportedSection.headerTitle = OWSLocalizedString(
            "SETTINGS_PAYMENTS_CURRENCY_VIEW_SECTION_ALL_CURRENCIES",
            comment: "Label for 'all currencies' section in the payment currency settings.",
        )
        if supportedCurrencyInfos.isEmpty {
            supportedSection.add(OWSTableItem(
                customCellBlock: {
                    let cell = OWSTableItem.newCell()

                    let activityIndicator = UIActivityIndicatorView(style: .medium)
                    activityIndicator.startAnimating()

                    cell.contentView.addSubview(activityIndicator)
                    activityIndicator.autoHCenterInSuperview()
                    activityIndicator.autoPinEdge(toSuperviewMargin: .top, withInset: 16)
                    activityIndicator.autoPinEdge(toSuperviewMargin: .bottom, withInset: 16)

                    return cell
                },
                actionBlock: nil,
            ))
        } else {
            for currencyInfo in supportedCurrencyInfos {
                supportedSection.add(buildTableItem(
                    forCurrencyInfo: currencyInfo,
                    currentCurrencyCode: currentCurrencyCode,
                ))
            }
        }
        contents.add(supportedSection)

        self.contents = contents
    }

    private func updateTableContentsForSearch(searchText: String) {
        let searchText = searchText.lowercased()

        let contents = OWSTableContents()

        let currentCurrencyCode = dataSource.currentCurrencyCode
        let preferredCurrencyInfos = dataSource.preferredCurrencyInfos
        let supportedCurrencyInfos = dataSource.supportedCurrencyInfos

        let currencyInfosToSearch = supportedCurrencyInfos.isEmpty ? preferredCurrencyInfos : supportedCurrencyInfos
        let matchingCurrencyInfos = currencyInfosToSearch.filter { currencyInfo in
            // We do the simplest possible matching.
            // No terms, no sorting by match quality, etc.
            currencyInfo.name.lowercased().contains(searchText) ||
                currencyInfo.code.lowercased().contains(searchText)
        }

        let resultsSection = OWSTableSection()
        resultsSection.customHeaderHeight = 12
        if matchingCurrencyInfos.isEmpty {
            for currencyInfo in matchingCurrencyInfos {
                resultsSection.add(buildTableItem(
                    forCurrencyInfo: currencyInfo,
                    currentCurrencyCode: currentCurrencyCode,
                ))
            }
        } else {
            for currencyInfo in matchingCurrencyInfos {
                resultsSection.add(buildTableItem(
                    forCurrencyInfo: currencyInfo,
                    currentCurrencyCode: currentCurrencyCode,
                ))
            }
        }
        contents.add(resultsSection)

        self.contents = contents
    }

    private func buildTableItem(
        forCurrencyInfo currencyInfo: Currency.Info,
        currentCurrencyCode: Currency.Code,
    ) -> OWSTableItem {

        let currencyCode = currencyInfo.code

        return OWSTableItem(
            customCellBlock: {
                let cell = OWSTableItem.newCell()

                let nameLabel = UILabel()
                nameLabel.text = currencyInfo.name
                nameLabel.font = .dynamicTypeBodyClamped
                nameLabel.textColor = .Signal.label

                let currencyCodeLabel = UILabel()
                currencyCodeLabel.text = currencyCode.uppercased()
                currencyCodeLabel.font = .dynamicTypeFootnoteClamped
                currencyCodeLabel.textColor = .Signal.secondaryLabel

                let stackView = UIStackView(arrangedSubviews: [nameLabel, currencyCodeLabel])
                stackView.axis = .vertical
                stackView.alignment = .fill
                cell.contentView.addSubview(stackView)
                stackView.autoPinEdgesToSuperviewMargins()

                cell.accessibilityIdentifier = "currency.\(currencyCode)"
                cell.accessibilityLabel = currencyInfo.name
                cell.isAccessibilityElement = true

                if currencyCode == currentCurrencyCode {
                    cell.accessoryType = .checkmark
                } else {
                    cell.accessoryType = .none
                }

                return cell
            },
            actionBlock: { [weak self] in
                self?.didSelectCurrency(currencyCode)
            },
        )
    }

    // MARK: - Events

    func dismissPicker() {
        if navigationController?.viewControllers.count == 1 {
            dismiss(animated: true)
        } else {
            navigationController?.popViewController(animated: true)
        }
    }

    private func didSelectCurrency(_ currencyCode: String) {
        completion(currencyCode)
        dismissPicker()
    }

    // MARK: - Search

    private let searchController = UISearchController()

    fileprivate var searchText: String? {
        searchController.searchBar.text?.ows_stripped()
    }

    func updateSearchResults(for searchController: UISearchController) {
        updateTableContents()
    }
}

struct StripeCurrencyPickerDataSource: CurrencyPickerDataSource {
    let currentCurrencyCode: Currency.Code
    let preferredCurrencyInfos: [Currency.Info]
    let supportedCurrencyInfos: [Currency.Info]

    var updateTableContents: (() -> Void)?

    init(currentCurrencyCode: Currency.Code, supportedCurrencyCodes: Set<Currency.Code>) {
        if supportedCurrencyCodes.contains(currentCurrencyCode) {
            self.currentCurrencyCode = currentCurrencyCode
        } else if supportedCurrencyCodes.contains("USD") {
            Logger.warn("Could not find the desired currency code. Falling back to USD")
            self.currentCurrencyCode = "USD"
        } else {
            owsFail("Could not pick a currency, even USD")
        }

        func isSupported(_ info: Currency.Info) -> Bool { supportedCurrencyCodes.contains(info.code) }
        self.preferredCurrencyInfos = Stripe.preferredCurrencyInfos.filter(isSupported)
        self.supportedCurrencyInfos = Currency.infos(
            for: supportedCurrencyCodes,
            ignoreMissingNames: false,
            shouldSort: true,
        )
    }
}

class PaymentsCurrencyPickerDataSource: NSObject, CurrencyPickerDataSource {
    let currentCurrencyCode = SSKEnvironment.shared.paymentsCurrenciesRef.currentCurrencyCode
    let preferredCurrencyInfos = SSKEnvironment.shared.paymentsCurrenciesRef.preferredCurrencyInfos
    @MainActor
    private(set) var supportedCurrencyInfos = SSKEnvironment.shared.paymentsCurrenciesRef.supportedCurrencyInfosWithCurrencyConversions {
        didSet { updateTableContents?() }
    }

    var updateTableContents: (() -> Void)?

    @MainActor
    override init() {
        super.init()

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(paymentConversionRatesDidChange),
            name: PaymentsCurrenciesImpl.paymentConversionRatesDidChange,
            object: nil,
        )

        SSKEnvironment.shared.paymentsCurrenciesRef.updateConversionRates()
    }

    @objc
    @MainActor
    private func paymentConversionRatesDidChange() {
        supportedCurrencyInfos = SSKEnvironment.shared.paymentsCurrenciesRef.supportedCurrencyInfosWithCurrencyConversions
    }
}