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

import Lottie
public import SignalServiceKit
public import SignalUI

public enum PaymentsSettingsMode: UInt, CustomStringConvertible {
    case inAppSettings
    case standalone

    // MARK: - CustomStringConvertible

    public var description: String {
        switch self {
        case .inAppSettings:
            return "inAppSettings"
        case .standalone:
            return "standalone"
        }
    }
}

// MARK: -

public class PaymentsSettingsViewController: OWSTableViewController2 {

    private let appReadiness: AppReadinessSetter
    private let mode: PaymentsSettingsMode

    private let paymentsHistoryDataSource = PaymentsHistoryDataSource()

    fileprivate static let maxHistoryCount: Int = 4

    private let topHeaderStackView = {
        let result = UIStackView()
        result.axis = .vertical
        result.spacing = 0
        return result
    }()

    private var outdatedClientReminderView: ReminderView?

    public init(
        mode: PaymentsSettingsMode,
        appReadiness: AppReadinessSetter,
    ) {
        self.mode = mode
        self.appReadiness = appReadiness

        super.init()

        self.topHeader = topHeaderStackView

        // Add placeholder view to stackview so it always has an calculable height.
        let placeholderView = UIView()
        placeholderView.translatesAutoresizingMaskIntoConstraints = false
        placeholderView.heightAnchor.constraint(equalToConstant: 1).isActive = true
        topHeaderStackView.addArrangedSubview(placeholderView)
    }

    // MARK: - Update Balance Timer

    private var updateBalanceTimer: Timer?

    private func stopUpdateBalanceTimer() {
        updateBalanceTimer?.invalidate()
        updateBalanceTimer = nil
    }

    private func startUpdateBalanceTimer() {
        stopUpdateBalanceTimer()

        let updateInterval: TimeInterval = .second * 30
        self.updateBalanceTimer = WeakTimer.scheduledTimer(
            timeInterval: updateInterval,
            target: self,
            userInfo: nil,
            repeats: true,
        ) { _ in
            Self.updateBalanceTimerDidFire()
        }
    }

    private static func updateBalanceTimerDidFire() {
        guard CurrentAppContext().isMainAppAndActive else {
            return
        }
        SUIEnvironment.shared.paymentsSwiftRef.updateCurrentPaymentBalance()
    }

    private var hasSignificantBalance: Bool {
        guard let paymentBalance = SUIEnvironment.shared.paymentsSwiftRef.currentPaymentBalance else {
            return false
        }
        let significantPicoMob = 500 * Double(PaymentsConstants.picoMobPerMob)
        return Double(paymentBalance.amount.picoMob) >= significantPicoMob
    }

    private static let keyValueStore = KeyValueStore(collection: "PaymentSettings")

    private static let savePassphraseShownKey = "PaymentsSavePassphraseShown"
    private var savePassphraseShown: Bool {
        get {
            SSKEnvironment.shared.databaseStorageRef.read { transaction in
                Self.keyValueStore.getBool(
                    Self.savePassphraseShownKey,
                    defaultValue: false,
                    transaction: transaction,
                )
            }
        }
        set {
            SSKEnvironment.shared.databaseStorageRef.write { transaction in
                Self.keyValueStore.setBool(
                    newValue,
                    key: Self.savePassphraseShownKey,
                    transaction: transaction,
                )
            }
        }
    }

    private static let savePassphraseHelpCardEnabledKey = "PaymentsSavePassphraseHelpCardEnabled"
    private var savePassphraseHelpCardEnabled: Bool {
        get {
            SSKEnvironment.shared.databaseStorageRef.read { transaction in
                Self.keyValueStore.getBool(
                    Self.savePassphraseHelpCardEnabledKey,
                    defaultValue: false,
                    transaction: transaction,
                )
            }
        }
        set {
            SSKEnvironment.shared.databaseStorageRef.write { transaction in
                Self.keyValueStore.setBool(
                    newValue,
                    key: Self.savePassphraseHelpCardEnabledKey,
                    transaction: transaction,
                )
            }
            updateTableContents()
        }
    }

    private func showSavePaymentsPassphraseUIIfNeeded() {
        guard savePassphraseShown == false else { return }
        guard let amount = SUIEnvironment.shared.paymentsSwiftRef.currentPaymentBalance?.amount else { return }
        guard amount.picoMob > 0 else { return }
        savePassphraseShown = true
        showPaymentsPassphraseUI(style: .fromBalance)
    }

    private func clearHelpCardEnabledFromDismissedList() {
        SSKEnvironment.shared.databaseStorageRef.write { transaction in
            Self.helpCardStore.removeValue(forKey: HelpCard.saveRecoveryPhrase.rawValue, transaction: transaction)
        }
    }

    // MARK: - Help Cards

    private enum HelpCard: String, Equatable, CaseIterable {
        case saveRecoveryPhrase
        case updatePin
        case aboutMobileCoin
        case addMoney
        case cashOut
    }

    private var helpCardsForNotEnabled: [HelpCard] {
        let helpCards: [HelpCard] = [
            .aboutMobileCoin,
            .addMoney,
            .cashOut,
        ]
        return filterDismissedHelpCards(helpCards)
    }

    private var helpCardsForEnabled: [HelpCard] {
        // Order matters as we build this list.
        var helpCards = OrderedSet<HelpCard>()

        if savePassphraseHelpCardEnabled {
            helpCards.append(.saveRecoveryPhrase)
        }

        if hasSignificantBalance {
            if !helpCards.contains(.saveRecoveryPhrase) {
                helpCards.append(.saveRecoveryPhrase)
            }

            let hasShortOrMissingPin: Bool = {
                guard let pinCode = SSKEnvironment.shared.ows2FAManagerRef.pinCodeWithSneakyTransaction else {
                    return true
                }
                let shortPinLength: UInt = 4
                return pinCode.count <= shortPinLength
            }()
            if hasShortOrMissingPin {
                helpCards.append(.updatePin)
            }
        } else {
            clearHelpCardEnabledFromDismissedList()
        }

        let defaultCards: [HelpCard] = [
            .aboutMobileCoin,
            .addMoney,
            .cashOut,
        ]
        for card in defaultCards {
            helpCards.append(card)
        }
        return filterDismissedHelpCards(helpCards.orderedMembers)
    }

    private static let helpCardStore = KeyValueStore(collection: "paymentsHelpCardStore")

    private func filterDismissedHelpCards(_ helpCards: [HelpCard]) -> [HelpCard] {
        let dismissedKeys = SSKEnvironment.shared.databaseStorageRef.read { transaction in
            Self.helpCardStore.allKeys(transaction: transaction)
        }
        return helpCards.filter { helpCard in !dismissedKeys.contains(helpCard.rawValue) }
    }

    private func dismissHelpCard(_ helpCard: HelpCard) {
        if helpCard == .saveRecoveryPhrase {
            showPaymentsPassphraseUI(style: .fromHelpCardDismiss)
            savePassphraseHelpCardEnabled = false
        }
        SSKEnvironment.shared.databaseStorageRef.write { transaction in
            Self.helpCardStore.setString(helpCard.rawValue, key: helpCard.rawValue, transaction: transaction)
        }
        updateTableContents()
    }

    // MARK: - Outdated Client Banner

    private func createOutdatedClientReminderView() {
        guard outdatedClientReminderView == nil else {
            return
        }

        let reminderView = ReminderView(
            style: .warning,
            text: OWSLocalizedString(
                "OUTDATED_PAYMENT_CLIENT_REMINDER_TEXT",
                comment: "Label warning the user that they should update Signal to continue using payments.",
            ),
            actionTitle: OWSLocalizedString(
                "OUTDATED_PAYMENT_CLIENT_ACTION_TITLE",
                comment: "Label for action link when the user has an outdated payment client",
            ),
            tapAction: { [weak self] in self?.didTapOutdatedPaymentClientReminder() },
        )
        reminderView.accessibilityIdentifier = "outdatedClientView"
        topHeaderStackView.addArrangedSubview(reminderView)

        outdatedClientReminderView = reminderView
    }

    private func didTapOutdatedPaymentClientReminder() {
        let url = TSConstants.appStoreUrl
        UIApplication.shared.open(url, options: [:], completionHandler: nil)
    }

    // MARK: -

    override public func viewDidLoad() {
        super.viewDidLoad()

        title = OWSLocalizedString(
            "SETTINGS_PAYMENTS_VIEW_TITLE",
            comment: "Title for the 'payments settings' view in the app settings.",
        )

        if mode == .standalone {
            navigationItem.leftBarButtonItem = UIBarButtonItem(
                barButtonSystemItem: .done,
                target: self,
                action: #selector(didTapDismiss),
                accessibilityIdentifier: "dismiss",
            )
        }

        addListeners()

        updateTableContents()

        updateNavbar()

        paymentsHistoryDataSource.delegate = self
    }

    private func updateNavbar() {
        if SSKEnvironment.shared.paymentsHelperRef.arePaymentsEnabled {
            navigationItem.rightBarButtonItem = UIBarButtonItem(
                image: Theme.iconImage(.buttonMore),
                landscapeImagePhone: nil,
                style: .plain,
                target: self,
                action: #selector(didTapSettings),
            )
        } else {
            navigationItem.rightBarButtonItem = nil
        }
    }

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

        updateTableContents()
        updateNavbar()
    }

    override public func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        SUIEnvironment.shared.paymentsSwiftRef.updateCurrentPaymentBalance()
        SSKEnvironment.shared.paymentsCurrenciesRef.updateConversionRates()

        startUpdateBalanceTimer()
        let clientOutdated = SSKEnvironment.shared.paymentsHelperRef.isPaymentsVersionOutdated
        if clientOutdated {
            OWSActionSheets.showPaymentsOutdatedClientSheet(title: .updateRequired)
            createOutdatedClientReminderView()
        }
        outdatedClientReminderView?.isHidden = !clientOutdated
    }

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

        if
            isMovingFromParent,
            mode == .inAppSettings
        {
            PaymentsViewUtils.markAllUnreadPaymentsAsReadWithSneakyTransaction()
        }

        stopUpdateBalanceTimer()
    }

    override public func themeDidChange() {
        super.themeDidChange()

        updateTableContents()
    }

    private func addListeners() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(arePaymentsEnabledDidChange),
            name: PaymentsConstants.arePaymentsEnabledDidChange,
            object: nil,
        )
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(isPaymentsVersionOutdatedDidChange),
            name: PaymentsConstants.isPaymentsVersionOutdatedDidChange,
            object: nil,
        )
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(updateTableContents),
            name: PaymentsImpl.currentPaymentBalanceDidChange,
            object: nil,
        )
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(updateTableContents),
            name: PaymentsCurrenciesImpl.paymentConversionRatesDidChange,
            object: nil,
        )
    }

    @objc
    private func arePaymentsEnabledDidChange() {
        updateTableContents()
        updateNavbar()

        if !SSKEnvironment.shared.paymentsHelperRef.arePaymentsEnabled {
            presentToast(text: OWSLocalizedString(
                "SETTINGS_PAYMENTS_PAYMENTS_DISABLED_TOAST",
                comment: "Message indicating that payments have been disabled in the app settings.",
            ))
        }
    }

    @objc
    private func isPaymentsVersionOutdatedDidChange() {
        guard UIApplication.shared.frontmostViewController == self else { return }
        if SSKEnvironment.shared.paymentsHelperRef.isPaymentsVersionOutdated {
            OWSActionSheets.showPaymentsOutdatedClientSheet(title: .updateRequired)
        }
    }

    @objc
    private func updateTableContents() {
        AssertIsOnMainThread()

        let arePaymentsEnabled = SSKEnvironment.shared.paymentsHelperRef.arePaymentsEnabled
        if arePaymentsEnabled {
            updateTableContentsEnabled()
        } else {
            updateTableContentsNotEnabled()
        }
    }

    private func updateTableContentsEnabled() {
        AssertIsOnMainThread()

        showSavePaymentsPassphraseUIIfNeeded()

        let contents = OWSTableContents()

        let headerSection = OWSTableSection()
        headerSection.hasBackground = false
        headerSection.shouldDisableCellSelection = true
        headerSection.add(OWSTableItem(
            customCellBlock: { [weak self] in
                let cell = OWSTableItem.newCell()
                self?.configureEnabledHeader(cell: cell)
                return cell
            },
            actionBlock: nil,
        ))
        contents.add(headerSection)

        let historySection = OWSTableSection()
        configureHistorySection(historySection, paymentsHistoryDataSource: paymentsHistoryDataSource)
        contents.add(historySection)

        addHelpCards(
            contents: contents,
            helpCards: helpCardsForEnabled,
        )

        self.contents = contents
    }

    private func configureEnabledHeader(cell: UITableViewCell) {
        let balanceLabel = UILabel()
        balanceLabel.font = UIFont.regularFont(ofSize: 54)
        balanceLabel.textAlignment = .center
        balanceLabel.adjustsFontSizeToFitWidth = true

        let balanceStack = UIStackView(arrangedSubviews: [balanceLabel])
        balanceStack.axis = .vertical
        balanceStack.alignment = .fill

        let conversionRefreshSize: CGFloat = 20
        let conversionRefreshIcon = UIImageView.withTemplateImageName(
            "refresh-20",
            tintColor: Theme.primaryIconColor,
        )
        conversionRefreshIcon.autoSetDimensions(to: .square(conversionRefreshSize))
        conversionRefreshIcon.isUserInteractionEnabled = true
        conversionRefreshIcon.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapConversionRefresh)))

        let conversionLabel = UILabel()
        conversionLabel.font = UIFont.dynamicTypeSubheadlineClamped
        conversionLabel.textColor = Theme.secondaryTextAndIconColor

        let conversionInfoView = UIImageView()
        conversionInfoView.setTemplateImageName("info-compact", tintColor: Theme.secondaryTextAndIconColor)
        conversionInfoView.autoSetDimensions(to: .square(16))
        conversionInfoView.setCompressionResistanceHigh()

        let conversionStack1 = UIStackView(arrangedSubviews: [
            conversionRefreshIcon,
            conversionLabel,
            conversionInfoView,
        ])
        conversionStack1.axis = .horizontal
        conversionStack1.alignment = .center
        conversionStack1.spacing = 12
        conversionStack1.isUserInteractionEnabled = true
        conversionStack1.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapCurrencyConversionInfo)))

        let conversionStack2 = UIStackView(arrangedSubviews: [conversionStack1])
        conversionStack2.axis = .vertical
        conversionStack2.alignment = .center

        func hideConversions() {
            conversionRefreshIcon.tintColor = .clear
            conversionLabel.text = " "
            conversionInfoView.tintColor = .clear
        }

        if let paymentBalance = SUIEnvironment.shared.paymentsSwiftRef.currentPaymentBalance {
            balanceLabel.attributedText = PaymentsFormat.attributedFormat(
                paymentAmount: paymentBalance.amount,
                isShortForm: false,
            )

            if let balanceConversionText = Self.buildBalanceConversionText(paymentBalance: paymentBalance) {
                conversionLabel.text = balanceConversionText
            } else {
                hideConversions()
            }
        } else {
            // Use an empty string to avoid jitter in layout between the
            // "pending balance" and "has balance" states.
            balanceLabel.text = " "

            let activityIndicator = UIActivityIndicatorView(style: .medium)
            balanceStack.addSubview(activityIndicator)
            activityIndicator.autoCenterInSuperview()
            activityIndicator.startAnimating()

            hideConversions()
        }

        let addMoneyButton = buildHeaderButton(
            title: OWSLocalizedString(
                "SETTINGS_PAYMENTS_ADD_MONEY",
                comment: "Label for 'add money' view in the payment settings.",
            ),
            iconName: "plus",
            selector: #selector(didTapAddMoneyButton),
        )
        let sendPaymentButton = buildHeaderButton(
            title: OWSLocalizedString(
                "SETTINGS_PAYMENTS_SEND_PAYMENT",
                comment: "Label for 'send payment' button in the payment settings.",
            ),
            iconName: "send-mob-24",
            selector: #selector(didTapSendPaymentButton),
        )
        let buttonStack = UIStackView(arrangedSubviews: [
            addMoneyButton,
            sendPaymentButton,
        ])
        buttonStack.axis = .horizontal
        buttonStack.spacing = 8
        buttonStack.alignment = .fill
        buttonStack.distribution = .fillEqually

        let headerStack = OWSStackView(
            name: "headerStack",
            arrangedSubviews: [
                balanceStack,
                UIView.spacer(withHeight: 8),
                conversionStack2,
                UIView.spacer(withHeight: 44),
                buttonStack,
            ],
        )
        headerStack.axis = .vertical
        headerStack.alignment = .fill
        headerStack.layoutMargins = .init(top: 30, left: 0, bottom: 8, right: 0)
        headerStack.isLayoutMarginsRelativeArrangement = true
        cell.contentView.addSubview(headerStack)
        headerStack.autoPinEdgesToSuperviewEdges()

        headerStack.addTapGesture {
            SUIEnvironment.shared.paymentsSwiftRef.updateCurrentPaymentBalance()
            SSKEnvironment.shared.paymentsCurrenciesRef.updateConversionRates()
        }
    }

    private func buildHeaderButton(title: String, iconName: String, selector: Selector) -> UIView {

        let iconView = UIImageView.withTemplateImageName(
            iconName,
            tintColor: Theme.primaryIconColor,
        )
        iconView.autoSetDimensions(to: .square(24))

        let label = UILabel()
        label.text = title
        label.textColor = Theme.primaryTextColor
        label.font = .dynamicTypeCaption2Clamped

        let stack = UIStackView(arrangedSubviews: [
            iconView,
            label,
        ])
        stack.axis = .vertical
        stack.alignment = .center
        stack.spacing = 5
        stack.layoutMargins = UIEdgeInsets(top: 12, leading: 20, bottom: 6, trailing: 20)
        stack.isLayoutMarginsRelativeArrangement = true
        stack.isUserInteractionEnabled = true
        stack.addGestureRecognizer(UITapGestureRecognizer(target: self, action: selector))

        let backgroundView = UIView()
        backgroundView.backgroundColor = OWSTableViewController2.cellBackgroundColor(isUsingPresentedStyle: true)
        backgroundView.layer.cornerRadius = 10
        stack.addSubview(backgroundView)
        stack.sendSubviewToBack(backgroundView)
        backgroundView.autoPinEdgesToSuperviewEdges()

        return stack
    }

    private static func buildBalanceConversionText(paymentBalance: PaymentBalance) -> String? {
        let localCurrencyCode = SSKEnvironment.shared.paymentsCurrenciesRef.currentCurrencyCode
        guard let currencyConversionInfo = SSKEnvironment.shared.paymentsCurrenciesRef.conversionInfo(forCurrencyCode: localCurrencyCode) else {
            return nil
        }
        guard
            let fiatAmountString = PaymentsFormat.formatAsFiatCurrency(
                paymentAmount: paymentBalance.amount,
                currencyConversionInfo: currencyConversionInfo,
            )
        else {
            return nil
        }

        // NOTE: conversion freshness is different than the balance freshness.
        //
        // We format the conversion freshness date using the local locale.
        // We format the currency using the EN/US locale.
        //
        // It is sufficient to format as a time, currency conversions go stale in less than a day.
        let conversionFreshnessString = DateUtil.formatDateAsTime(currencyConversionInfo.conversionDate)
        let formatString = OWSLocalizedString(
            "SETTINGS_PAYMENTS_BALANCE_CONVERSION_FORMAT",
            comment: "Format string for the 'local balance converted into local currency' indicator. Embeds: {{ %1$@ the local balance in the local currency, %2$@ the local currency code, %3$@ the date the currency conversion rate was obtained. }}..",
        )
        return String.nonPluralLocalizedStringWithFormat(formatString, fiatAmountString, localCurrencyCode, conversionFreshnessString)
    }

    private func configureHistorySection(
        _ section: OWSTableSection,
        paymentsHistoryDataSource: PaymentsHistoryDataSource,
    ) {

        guard paymentsHistoryDataSource.hasItems else {
            section.hasBackground = false
            section.shouldDisableCellSelection = true
            section.add(OWSTableItem(
                customCellBlock: {
                    let cell = OWSTableItem.newCell()

                    let label = UILabel()
                    label.text = OWSLocalizedString(
                        "SETTINGS_PAYMENTS_NO_ACTIVITY_INDICATOR",
                        comment: "Message indicating that there is no payment activity to display in the payment settings.",
                    )
                    label.textColor = Theme.secondaryTextAndIconColor
                    label.font = UIFont.dynamicTypeBodyClamped
                    label.numberOfLines = 0
                    label.lineBreakMode = .byWordWrapping
                    label.textAlignment = .center

                    let stack = UIStackView(arrangedSubviews: [label])
                    stack.axis = .vertical
                    stack.alignment = .fill
                    stack.layoutMargins = UIEdgeInsets(top: 10, leading: 0, bottom: 30, trailing: 0)
                    stack.isLayoutMarginsRelativeArrangement = true

                    cell.contentView.addSubview(stack)
                    stack.autoPinEdgesToSuperviewMargins()

                    return cell
                },
                actionBlock: nil,
            ))
            return
        }

        section.headerTitle = OWSLocalizedString(
            "SETTINGS_PAYMENTS_RECENT_PAYMENTS",
            comment: "Label for the 'recent payments' section in the payment settings.",
        )

        section.separatorInsetLeading = OWSTableViewController2.cellHInnerMargin + PaymentModelCell.separatorInsetLeading

        var hasMoreItems = false
        for (index, paymentItem) in paymentsHistoryDataSource.items.enumerated() {
            guard index < PaymentsSettingsViewController.maxHistoryCount else {
                hasMoreItems = true
                break
            }
            section.add(OWSTableItem(
                customCellBlock: {
                    let cell = PaymentModelCell()
                    cell.configure(paymentItem: paymentItem)
                    return cell
                },
                actionBlock: { [weak self] in
                    self?.didTapPaymentItem(paymentItem: paymentItem)
                },
            ))
        }

        if hasMoreItems {
            section.add(OWSTableItem(
                customCellBlock: {
                    let cell = OWSTableItem.newCell()
                    cell.selectionStyle = .none

                    let label = UILabel()
                    label.text = CommonStrings.seeAllButton
                    label.font = .dynamicTypeBodyClamped
                    label.textColor = Theme.primaryTextColor

                    let stack = UIStackView(arrangedSubviews: [label])
                    stack.axis = .vertical
                    stack.alignment = .fill
                    cell.contentView.addSubview(stack)
                    stack.autoPinEdgesToSuperviewMargins()
                    stack.layoutMargins = UIEdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0)
                    stack.isLayoutMarginsRelativeArrangement = true

                    cell.accessoryType = .disclosureIndicator
                    return cell
                },
                actionBlock: { [weak self] in
                    self?.showPaymentsHistoryView()
                },
            ))
        }
    }

    private func updateTableContentsNotEnabled() {
        AssertIsOnMainThread()

        let contents = OWSTableContents()

        let headerSection = OWSTableSection()
        headerSection.shouldDisableCellSelection = true
        headerSection.add(OWSTableItem(
            customCellBlock: { [weak self] in
                let cell = OWSTableItem.newCell()
                self?.configureNotEnabledCell(cell)
                return cell
            },
            actionBlock: nil,
        ))
        contents.add(headerSection)

        addHelpCards(
            contents: contents,
            helpCards: helpCardsForNotEnabled,
        )

        self.contents = contents
    }

    private func configureNotEnabledCell(_ cell: UITableViewCell) {

        let titleLabel = UILabel()
        titleLabel.text = OWSLocalizedString(
            "SETTINGS_PAYMENTS_OPT_IN_TITLE",
            comment: "Title for the 'payments opt-in' view in the app settings.",
        )
        titleLabel.textColor = Theme.primaryTextColor
        titleLabel.font = UIFont.dynamicTypeHeadlineClamped
        titleLabel.numberOfLines = 0
        titleLabel.lineBreakMode = .byWordWrapping
        titleLabel.textAlignment = .center

        let heroImageView = LottieAnimationView(name: "activate-payments")
        heroImageView.contentMode = .scaleAspectFit
        let viewSize = view.bounds.size
        let heroSize = min(viewSize.width, viewSize.height) * 0.5
        heroImageView.autoSetDimension(.height, toSize: heroSize)

        let bodyLabel = UILabel()
        bodyLabel.text = OWSLocalizedString(
            "SETTINGS_PAYMENTS_OPT_IN_MESSAGE",
            comment: "Message for the 'payments opt-in' view in the app settings.",
        )
        bodyLabel.font = .dynamicTypeSubheadlineClamped
        bodyLabel.textColor = Theme.secondaryTextAndIconColor
        bodyLabel.numberOfLines = 0
        bodyLabel.lineBreakMode = .byWordWrapping
        bodyLabel.textAlignment = .center

        let buttonTitle = (
            SUIEnvironment.shared.paymentsRef.paymentsEntropy != nil
                ? OWSLocalizedString(
                    "SETTINGS_PAYMENTS_OPT_IN_REACTIVATE_BUTTON",
                    comment: "Label for 'activate' button in the 'payments opt-in' view in the app settings.",
                )
                : OWSLocalizedString(
                    "SETTINGS_PAYMENTS_OPT_IN_ACTIVATE_BUTTON",
                    comment: "Label for 'activate' button in the 'payments opt-in' view in the app settings.",
                ),
        )
        let activateButton = OWSFlatButton.button(
            title: buttonTitle,
            font: UIFont.dynamicTypeHeadline,
            titleColor: .white,
            backgroundColor: .ows_accentBlue,
            target: self,
            selector: #selector(didTapEnablePaymentsButton),
        )
        activateButton.autoSetHeightUsingFont()

        let stack = UIStackView(arrangedSubviews: [
            titleLabel,
            UIView.spacer(withHeight: 24),
            heroImageView,
            UIView.spacer(withHeight: 20),
            bodyLabel,
            UIView.spacer(withHeight: 20),
            activateButton,
        ])

        if SUIEnvironment.shared.paymentsRef.paymentsEntropy == nil {
            let buttonTitle = OWSLocalizedString(
                "SETTINGS_PAYMENTS_RESTORE_PAYMENTS_BUTTON",
                comment: "Label for 'restore payments' button in the payments settings.",
            )
            let restorePaymentsButton = OWSFlatButton.button(
                title: buttonTitle,
                font: UIFont.dynamicTypeHeadline,
                titleColor: .ows_accentBlue,
                backgroundColor: self.tableBackgroundColor,
                target: self,
                selector: #selector(didTapRestorePaymentsButton),
            )
            restorePaymentsButton.autoSetHeightUsingFont()
            stack.addArrangedSubviews([
                UIView.spacer(withHeight: 8),
                restorePaymentsButton,
            ])
        }

        stack.axis = .vertical
        stack.alignment = .fill
        stack.layoutMargins = UIEdgeInsets(top: 20, leading: 0, bottom: 32, trailing: 0)
        stack.isLayoutMarginsRelativeArrangement = true
        cell.contentView.addSubview(stack)
        stack.autoPinEdgesToSuperviewMargins()
    }

    private func addHelpCards(
        contents: OWSTableContents,
        helpCards: [HelpCard],
    ) {

        for helpCard in helpCards {
            switch helpCard {
            case .saveRecoveryPhrase:
                contents.add(buildHelpCard(
                    helpCard: helpCard,
                    title: OWSLocalizedString(
                        "SETTINGS_PAYMENTS_HELP_CARD_SAVE_PASSPHRASE_TITLE",
                        comment: "Title for the 'Save Passphrase' help card in the payments settings.",
                    ),
                    body: OWSLocalizedString(
                        "SETTINGS_PAYMENTS_HELP_CARD_SAVE_PASSPHRASE_DESCRIPTION",
                        comment: "Description for the 'Save Passphrase' help card in the payments settings.",
                    ),
                    buttonText: OWSLocalizedString(
                        "SETTINGS_PAYMENTS_HELP_CARD_SAVE_PASSPHRASE_BUTTON",
                        comment: "Label for button in the 'Save Passphrase' help card in the payments settings.",
                    ),
                    iconName: Theme.isDarkThemeEnabled
                        ? "restore-dark"
                        : "restore",
                    selector: #selector(didTapSavePassphraseCard),
                ))
            case .updatePin:
                contents.add(buildHelpCard(
                    helpCard: helpCard,
                    title: OWSLocalizedString(
                        "SETTINGS_PAYMENTS_HELP_CARD_UPDATE_PIN_TITLE",
                        comment: "Title for the 'Update PIN' help card in the payments settings.",
                    ),
                    body: OWSLocalizedString(
                        "SETTINGS_PAYMENTS_HELP_CARD_UPDATE_PIN_DESCRIPTION",
                        comment: "Description for the 'Update PIN' help card in the payments settings.",
                    ),
                    buttonText: OWSLocalizedString(
                        "SETTINGS_PAYMENTS_HELP_CARD_UPDATE_PIN_BUTTON",
                        comment: "Label for button in the 'Update PIN' help card in the payments settings.",
                    ),
                    iconName: Theme.isDarkThemeEnabled
                        ? "update-pin-dark"
                        : "update-pin",
                    selector: #selector(didTapUpdatePinCard),
                ))
            case .aboutMobileCoin:
                contents.add(buildHelpCard(
                    helpCard: helpCard,
                    title: OWSLocalizedString(
                        "SETTINGS_PAYMENTS_HELP_CARD_ABOUT_MOBILECOIN_TITLE",
                        comment: "Title for the 'About MobileCoin' help card in the payments settings.",
                    ),
                    body: OWSLocalizedString(
                        "SETTINGS_PAYMENTS_HELP_CARD_ABOUT_MOBILECOIN_DESCRIPTION",
                        comment: "Description for the 'About MobileCoin' help card in the payments settings.",
                    ),
                    buttonText: CommonStrings.learnMore,
                    iconName: "about-mobilecoin",
                    selector: #selector(didTapAboutMobileCoinCard),
                ))
            case .addMoney:
                contents.add(buildHelpCard(
                    helpCard: helpCard,
                    title: OWSLocalizedString(
                        "SETTINGS_PAYMENTS_HELP_CARD_ADDING_TO_YOUR_WALLET_TITLE",
                        comment: "Title for the 'Adding to your wallet' help card in the payments settings.",
                    ),
                    body: OWSLocalizedString(
                        "SETTINGS_PAYMENTS_HELP_CARD_ADDING_TO_YOUR_WALLET_DESCRIPTION",
                        comment: "Description for the 'Adding to your wallet' help card in the payments settings.",
                    ),
                    buttonText: CommonStrings.learnMore,
                    iconName: "add-money",
                    selector: #selector(didTapAddingToYourWalletCard),
                ))
            case .cashOut:
                contents.add(buildHelpCard(
                    helpCard: helpCard,
                    title: OWSLocalizedString(
                        "SETTINGS_PAYMENTS_HELP_CARD_CASHING_OUT_TITLE",
                        comment: "Title for the 'Cashing Out' help card in the payments settings.",
                    ),
                    body: OWSLocalizedString(
                        "SETTINGS_PAYMENTS_HELP_CARD_CASHING_OUT_DESCRIPTION",
                        comment: "Description for the 'Cashing Out' help card in the payments settings.",
                    ),
                    buttonText: CommonStrings.learnMore,
                    iconName: "cash-out",
                    selector: #selector(didTapCashingOutCoinCard),
                ))
            }
        }
    }

    private func buildHelpCard(
        helpCard: HelpCard,
        title: String,
        body: String,
        buttonText: String,
        iconName: String,
        selector: Selector,
    ) -> OWSTableSection {
        let section = OWSTableSection()

        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: selector)

        section.add(OWSTableItem(
            customCellBlock: { [weak self] in
                guard let self else { return OWSTableItem.newCell() }

                let titleLabel = UILabel()
                titleLabel.text = title
                titleLabel.textColor = Theme.primaryTextColor
                titleLabel.font = UIFont.dynamicTypeHeadlineClamped

                let bodyLabel = UILabel()
                bodyLabel.text = body
                bodyLabel.textColor = Theme.secondaryTextAndIconColor
                bodyLabel.font = UIFont.dynamicTypeSubheadlineClamped
                bodyLabel.numberOfLines = 0
                bodyLabel.lineBreakMode = .byWordWrapping

                let buttonLabel = UILabel()
                buttonLabel.text = buttonText
                buttonLabel.textColor = Theme.accentBlueColor
                buttonLabel.font = UIFont.dynamicTypeSubheadlineClamped

                let animationView = LottieAnimationView(name: iconName)
                animationView.contentMode = .scaleAspectFit
                animationView.autoSetDimensions(to: .square(80))

                let vStack = UIStackView(arrangedSubviews: [
                    titleLabel,
                    bodyLabel,
                    buttonLabel,
                ])
                vStack.axis = .vertical
                vStack.alignment = .leading
                vStack.spacing = 8

                let hStack = UIStackView(arrangedSubviews: [
                    vStack,
                    animationView,
                ])
                hStack.axis = .horizontal
                hStack.alignment = .center
                hStack.spacing = 16

                let cell = OWSTableItem.newCell()
                cell.contentView.addSubview(hStack)
                hStack.autoPinEdgesToSuperviewMargins()

                let dismissIconView = UIImageView.withTemplateImage(
                    Theme.iconImage(.buttonX),
                    tintColor: Theme.isDarkThemeEnabled
                        ? .ows_gray05
                        : .ows_gray45,
                )
                dismissIconView.autoSetDimensions(to: .square(10))
                let dismissButton = OWSLayerView.circleView()
                dismissButton.backgroundColor = (
                    Theme.isDarkThemeEnabled
                        ? .ows_gray65
                        : .ows_gray02,
                )
                dismissButton.addTapGesture { [weak self] in
                    self?.dismissHelpCard(helpCard)
                }
                dismissButton.autoSetDimensions(to: .square(20))
                dismissButton.addSubview(dismissIconView)
                dismissIconView.autoCenterInSuperview()
                cell.contentView.addSubview(dismissButton)
                dismissButton.autoPinEdge(toSuperviewEdge: .top, withInset: 8)
                dismissButton.autoPinEdge(toSuperviewEdge: .trailing, withInset: 8)

                cell.isUserInteractionEnabled = true
                cell.addGestureRecognizer(tapGestureRecognizer)

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

        return section
    }

    // MARK: -

    private func showSettingsActionSheet() {
        let actionSheet = ActionSheetController(title: nil, message: nil)

        actionSheet.addAction(ActionSheetAction(
            title: OWSLocalizedString(
                "SETTINGS_PAYMENTS_TRANSFER_TO_EXCHANGE",
                comment: "Label for the 'transfer to exchange' button in the payment settings.",
            ),
            style: .default,
        ) { [weak self] _ in
            self?.didTapTransferToExchangeButton()
        })

        actionSheet.addAction(ActionSheetAction(
            title: OWSLocalizedString(
                "SETTINGS_PAYMENTS_SET_CURRENCY",
                comment: "Title for the 'set currency' view in the app settings.",
            ),
            style: .default,
        ) { [weak self] _ in
            self?.didTapSetCurrencyButton()
        })

        actionSheet.addAction(ActionSheetAction(
            title: OWSLocalizedString(
                "SETTINGS_PAYMENTS_DEACTIVATE_PAYMENTS",
                comment: "Label for 'deactivate payments' button in the app settings.",
            ),
            style: .default,
        ) { [weak self] _ in
            self?.didTapDeactivatePaymentsButton()
        })

        actionSheet.addAction(ActionSheetAction(
            title: OWSLocalizedString(
                "SETTINGS_PAYMENTS_VIEW_RECOVERY_PASSPHRASE",
                comment: "Label for 'view payments recovery passphrase' button in the app settings.",
            ),
            style: .default,
        ) { [weak self] _ in
            self?.didTapViewPaymentsPassphraseButton()
        })

        actionSheet.addAction(ActionSheetAction(
            title: CommonStrings.help,
            style: .default,
        ) { [weak self] _ in
            self?.didTapHelpButton()
        })

        actionSheet.addAction(OWSActionSheets.cancelAction)

        presentActionSheet(actionSheet)
    }

    private func showConfirmDeactivatePaymentsUI() {
        let actionSheet = ActionSheetController(
            title: OWSLocalizedString(
                "SETTINGS_PAYMENTS_DEACTIVATE_PAYMENTS_CONFIRM_TITLE",
                comment: "Title for the 'deactivate payments confirmation' UI in the payment settings.",
            ),
            message: OWSLocalizedString(
                "SETTINGS_PAYMENTS_DEACTIVATE_PAYMENTS_CONFIRM_DESCRIPTION",
                comment: "Description for the 'deactivate payments confirmation' UI in the payment settings.",
            ),
        )

        actionSheet.addAction(ActionSheetAction(
            title: CommonStrings.continueButton,
            style: .default,
        ) { [weak self] _ in
            self?.didTapConfirmDeactivatePaymentsButton()
        })

        actionSheet.addAction(OWSActionSheets.cancelAction)

        presentActionSheet(actionSheet)
    }

    @objc
    private func didTapConversionRefresh() {
        SUIEnvironment.shared.paymentsSwiftRef.updateCurrentPaymentBalance()
        SSKEnvironment.shared.paymentsCurrenciesRef.updateConversionRates()
    }

    @objc
    private func didTapCurrencyConversionInfo() {
        PaymentsSettingsViewController.showCurrencyConversionInfoAlert(fromViewController: self)
    }

    public static func showCurrencyConversionInfoAlert(fromViewController: UIViewController) {
        let message = OWSLocalizedString(
            "SETTINGS_PAYMENTS_CURRENCY_CONVERSIONS_INFO_ALERT_MESSAGE",
            comment: "Message for the 'currency conversions info' alert.",
        )
        let actionSheet = ActionSheetController(title: nil, message: message)
        actionSheet.addAction(ActionSheetAction(
            title: CommonStrings.learnMore,
            style: .default,
            handler: { _ in
                CurrentAppContext().open(
                    URL.Support.Payments.currencyConversion,
                    completion: nil,
                )
            },
        ))
        actionSheet.addAction(OWSActionSheets.okayAction)
        fromViewController.presentActionSheet(actionSheet)
    }

    // MARK: - Events

    @objc
    func didTapDismiss() {
        dismiss(animated: true, completion: nil)
    }

    @objc
    func didTapEnablePaymentsButton(_ sender: UIButton) {
        AssertIsOnMainThread()

        showEnablePaymentsConfirmUI()
    }

    private func showEnablePaymentsConfirmUI() {
        AssertIsOnMainThread()

        let actionSheet = ActionSheetController(
            title: OWSLocalizedString(
                "SETTINGS_PAYMENTS_ACTIVATE_PAYMENTS_CONFIRM_TITLE",
                comment: "Title for the 'activate payments confirmation' UI in the payment settings.",
            ),
            message: OWSLocalizedString(
                "SETTINGS_PAYMENTS_ACTIVATE_PAYMENTS_CONFIRM_DESCRIPTION",
                comment: "Description for the 'activate payments confirmation' UI in the payment settings.",
            ),
        )

        actionSheet.addAction(ActionSheetAction(
            title: OWSLocalizedString(
                "SETTINGS_PAYMENTS_ACTIVATE_PAYMENTS_CONFIRM_AGREE",
                comment: "Label for the 'agree to payments terms' button in the 'activate payments confirmation' UI in the payment settings.",
            ),
            style: .default,
        ) { [weak self] _ in
            self?.enablePayments()
            self?.promptBiometryPaymentsLock()
        })
        actionSheet.addAction(ActionSheetAction(
            title: OWSLocalizedString(
                "SETTINGS_PAYMENTS_ACTIVATE_PAYMENTS_CONFIRM_VIEW_TERMS",
                comment: "Label for the 'view payments terms' button in the 'activate payments confirmation' UI in the payment settings.",
            ),
            style: .default,
        ) { _ in
            UIApplication.shared.open(
                URL(string: "https://www.mobilecoin.com/terms-of-use.html")!,
                options: [:],
                completionHandler: nil,
            )
        })
        actionSheet.addAction(OWSActionSheets.cancelAction)

        presentActionSheet(actionSheet)
    }

    private func enablePayments() {
        AssertIsOnMainThread()

        guard !SUIEnvironment.shared.paymentsRef.isKillSwitchActive else {
            OWSActionSheets.showErrorAlert(message: OWSLocalizedString(
                "SETTINGS_PAYMENTS_CANNOT_ACTIVATE_PAYMENTS_KILL_SWITCH",
                comment: "Error message indicating that payments could not be activated because the feature is not currently available.",
            ))
            return
        }

        if SSKEnvironment.shared.paymentsHelperRef.isPaymentsVersionOutdated {
            OWSActionSheets.showPaymentsOutdatedClientSheet(title: .updateRequired)
            return
        }

        SSKEnvironment.shared.databaseStorageRef.asyncWrite { transaction in
            SSKEnvironment.shared.paymentsHelperRef.enablePayments(transaction: transaction)

            transaction.addSyncCompletion {
                Task { @MainActor in
                    self.showPaymentsActivatedToast()
                }
            }
        }
    }

    private func promptBiometryPaymentsLock() {
        AssertIsOnMainThread()

        guard let view = PaymentsBiometryLockPromptViewController(deviceOwnerAuthenticationType: .current, delegate: nil) else {
            owsFailDebug("Unknown biometry type, cannot enable payments lock")
            return
        }
        let navigationVC = OWSNavigationController(rootViewController: view)
        present(navigationVC, animated: true)
    }

    private func showPaymentsActivatedToast() {
        AssertIsOnMainThread()
        let toastText = OWSLocalizedString(
            "SETTINGS_PAYMENTS_OPT_IN_ACTIVATED_TOAST",
            comment: "Message shown when payments are activated in the 'payments opt-in' view in the app settings.",
        )
        self.presentToast(text: toastText)
    }

    @objc
    func didTapRestorePaymentsButton() {
        AssertIsOnMainThread()

        guard SUIEnvironment.shared.paymentsRef.paymentsEntropy == nil else {
            owsFailDebug("paymentsEntropy already set.")
            return
        }

        let view = PaymentsRestoreWalletSplashViewController(restoreWalletDelegate: self)
        let navigationVC = OWSNavigationController(rootViewController: view)
        present(navigationVC, animated: true)
    }

    @objc
    func didTapSettings() {
        showSettingsActionSheet()
    }

    private func didTapSetCurrencyButton() {
        let view = CurrencyPickerViewController(
            dataSource: PaymentsCurrencyPickerDataSource(),
        ) { currencyCode in
            SSKEnvironment.shared.databaseStorageRef.write { transaction in
                SSKEnvironment.shared.paymentsCurrenciesRef.setCurrentCurrencyCode(currencyCode, transaction: transaction)
            }
        }
        navigationController?.pushViewController(view, animated: true)
    }

    private func didTapViewPaymentsPassphraseButton() {
        if Self.hasReviewedPassphraseWithSneakyTransaction() {
            showPaymentsPassphraseUI(style: .reviewed)
        } else {
            showPaymentsPassphraseUI(style: .view)
        }
    }

    private func showPaymentsPassphraseUI(style: PaymentsViewPassphraseSplashViewController.Style) {
        guard let passphrase = SUIEnvironment.shared.paymentsSwiftRef.passphrase else {
            owsFailDebug("Missing passphrase.")
            return
        }
        let view = PaymentsViewPassphraseSplashViewController(
            passphrase: passphrase,
            style: style,
            viewPassphraseDelegate: self,
        )
        let navigationVC = OWSNavigationController(rootViewController: view)
        present(navigationVC, animated: true)
    }

    private func didTapDeactivatePaymentsButton() {
        showConfirmDeactivatePaymentsUI()
    }

    private func didTapConfirmDeactivatePaymentsButton() {
        guard let paymentBalance = SUIEnvironment.shared.paymentsSwiftRef.currentPaymentBalance else {
            OWSActionSheets.showErrorAlert(message: OWSLocalizedString(
                "SETTINGS_PAYMENTS_CANNOT_DEACTIVATE_PAYMENTS_NO_BALANCE",
                comment: "Error message indicating that payments could not be deactivated because the current balance is unavailable.",
            ))
            return
        }
        guard paymentBalance.amount.picoMob > 0 else {
            SSKEnvironment.shared.databaseStorageRef.write { transaction in
                SSKEnvironment.shared.paymentsHelperRef.disablePayments(transaction: transaction)
            }
            return
        }
        let vc = PaymentsDeactivateViewController(paymentBalance: paymentBalance)
        let navigationVC = OWSNavigationController(rootViewController: vc)
        present(navigationVC, animated: true)
    }

    private func didTapHelpButton() {
        let view = ContactSupportViewController()
        view.selectedFilter = .payments
        let navigationVC = OWSNavigationController(rootViewController: view)
        present(navigationVC, animated: true)
    }

    private func didTapTransferToExchangeButton() {
        if SSKEnvironment.shared.paymentsHelperRef.isPaymentsVersionOutdated {
            OWSActionSheets.showPaymentsOutdatedClientSheet(title: .updateRequired)
            return
        }
        let view = PaymentsTransferOutViewController(transferAmount: nil)
        let navigationController = OWSNavigationController(rootViewController: view)
        present(navigationController, animated: true, completion: nil)
    }

    private func showPaymentsHistoryView() {
        let view = PaymentsHistoryViewController()
        navigationController?.pushViewController(view, animated: true)
    }

    @objc
    func didTapAddMoneyButton(sender: UIGestureRecognizer) {
        guard !SUIEnvironment.shared.paymentsRef.isKillSwitchActive else {
            OWSActionSheets.showErrorAlert(message: OWSLocalizedString(
                "SETTINGS_PAYMENTS_CANNOT_TRANSFER_IN_KILL_SWITCH",
                comment: "Error message indicating that you cannot transfer into your payments wallet because the feature is not currently available.",
            ))
            return
        }
        let view = PaymentsTransferInViewController()
        let navigationController = OWSNavigationController(rootViewController: view)
        present(navigationController, animated: true, completion: nil)
    }

    @objc
    func didTapSendPaymentButton(sender: UIGestureRecognizer) {
        guard !SUIEnvironment.shared.paymentsRef.isKillSwitchActive else {
            OWSActionSheets.showErrorAlert(message: OWSLocalizedString(
                "SETTINGS_PAYMENTS_CANNOT_SEND_PAYMENTS_KILL_SWITCH",
                comment: "Error message indicating that payments cannot be sent because the feature is not currently available.",
            ))
            return
        }

        if SSKEnvironment.shared.paymentsHelperRef.isPaymentsVersionOutdated {
            OWSActionSheets.showPaymentsOutdatedClientSheet(title: .updateRequired)
            return
        }

        PaymentsSendRecipientViewController.presentAsFormSheet(fromViewController: self, isOutgoingTransfer: false)
    }

    private func didTapPaymentItem(paymentItem: PaymentsHistoryItem) {
        let view = PaymentsDetailViewController(paymentItem: paymentItem)
        navigationController?.pushViewController(view, animated: true)
    }

    @objc
    private func didTapAboutMobileCoinCard() {
        CurrentAppContext().open(
            URL.Support.Payments.whichOnes,
            completion: nil,
        )
    }

    @objc
    private func didTapAddingToYourWalletCard() {
        CurrentAppContext().open(
            URL.Support.Payments.transferFromExchange,
            completion: nil,
        )
    }

    @objc
    private func didTapCashingOutCoinCard() {
        CurrentAppContext().open(
            URL.Support.Payments.transferToExchange,
            completion: nil,
        )
    }

    @objc
    private func didTapUpdatePinCard() {
        guard let navigationController = self.navigationController else {
            owsFailDebug("Missing navigationController.")
            return
        }
        switch mode {
        case .inAppSettings:
            navigationController.popViewController(animated: true) { [appReadiness] in
                let accountSettingsView = AccountSettingsViewController(appReadiness: appReadiness)
                navigationController.pushViewController(accountSettingsView, animated: true)
            }
        case .standalone:
            let accountSettingsView = AccountSettingsViewController(appReadiness: appReadiness)
            navigationController.pushViewController(accountSettingsView, animated: true)
        }
    }

    @objc
    private func didTapSavePassphraseCard() {
        showPaymentsPassphraseUI(style: .fromHelpCard)
    }
}

// MARK: -

extension PaymentsSettingsViewController: PaymentsHistoryDataSourceDelegate {
    var recordType: PaymentsHistoryDataSource.RecordType {
        .all
    }

    var maxRecordCount: Int? {
        // Load an extra item so we can detect if there's more items
        // to render.
        Self.maxHistoryCount + 1
    }

    func didUpdateContent() {
        AssertIsOnMainThread()

        updateTableContents()
    }
}

// MARK: -

extension PaymentsSettingsViewController: PaymentsViewPassphraseDelegate {

    private static let hasReviewedPassphraseKey = "hasReviewedPassphrase"

    public static func hasReviewedPassphraseWithSneakyTransaction() -> Bool {
        SSKEnvironment.shared.databaseStorageRef.read { transaction in
            Self.keyValueStore.getBool(
                Self.hasReviewedPassphraseKey,
                defaultValue: false,
                transaction: transaction,
            )
        }
    }

    public static func setHasReviewedPassphraseWithSneakyTransaction() {
        SSKEnvironment.shared.databaseStorageRef.write { transaction in
            Self.keyValueStore.setBool(
                true,
                key: Self.hasReviewedPassphraseKey,
                transaction: transaction,
            )
        }
    }

    public func viewPassphraseDidComplete() {
        self.savePassphraseHelpCardEnabled = false
        if !Self.hasReviewedPassphraseWithSneakyTransaction() {
            Self.setHasReviewedPassphraseWithSneakyTransaction()

            presentToast(text: OWSLocalizedString(
                "SETTINGS_PAYMENTS_VIEW_PASSPHRASE_COMPLETE_TOAST",
                comment: "Message indicating that 'payments passphrase review' is complete.",
            ))
        }
    }

    public func viewPassphraseDidCancel(viewController: PaymentsViewPassphraseSplashViewController) {
        viewController.dismiss(animated: true)
        if viewController.style.shouldShowHelpCardAfterCancel {
            self.clearHelpCardEnabledFromDismissedList()
            self.savePassphraseHelpCardEnabled = true
        }
    }
}

// MARK: -

extension PaymentsSettingsViewController: PaymentsRestoreWalletDelegate {

    public func restoreWalletDidComplete() {
        presentToast(text: OWSLocalizedString(
            "SETTINGS_PAYMENTS_RESTORE_WALLET_COMPLETE_TOAST",
            comment: "Message indicating that 'restore payments wallet' is complete.",
        ))
    }
}