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

public import SignalServiceKit
public import SignalUI

public class PaymentsViewUtils {

    private init() {}

    public static func buildMemoLabel(memoMessage: String?) -> UIView? {
        guard let memoMessage = memoMessage?.ows_stripped().nilIfEmpty else {
            return nil
        }

        let label = UILabel()
        label.text = memoMessage
        label.textColor = Theme.primaryTextColor
        label.font = UIFont.dynamicTypeSubheadlineClamped
        label.textAlignment = .center
        label.numberOfLines = 0
        label.lineBreakMode = .byWordWrapping

        let stack = UIStackView(arrangedSubviews: [label])
        stack.axis = .vertical
        stack.layoutMargins = UIEdgeInsets(hMargin: 16, vMargin: 8)
        stack.isLayoutMarginsRelativeArrangement = true

        let backgroundView = OWSLayerView.pillView()
        backgroundView.backgroundColor = Theme.secondaryBackgroundColor
        stack.addSubview(backgroundView)
        backgroundView.autoPinEdgesToSuperviewEdges()
        stack.sendSubviewToBack(backgroundView)

        return stack
    }

    static func buildUnidentifiedTransactionAvatar(avatarSize: UInt) -> UIView {
        let circleView = OWSLayerView.circleView()
        circleView.backgroundColor = (Theme.isDarkThemeEnabled ? .ows_gray75 : .ows_gray02)
        circleView.autoSetDimensions(to: .square(CGFloat(avatarSize)))

        let iconColor: UIColor = (Theme.isDarkThemeEnabled ? .ows_gray05 : .ows_gray75)
        let iconView = UIImageView.withTemplateImageName(
            "mobilecoin-24",
            tintColor: iconColor,
        )
        circleView.addSubview(iconView)
        iconView.autoCenterInSuperview()
        iconView.autoSetDimensions(to: .square(CGFloat(avatarSize) * 20.0 / 36.0))

        return circleView
    }

    static func buildUnidentifiedTransactionString(paymentModel: TSPaymentModel) -> String {
        owsAssertDebug(paymentModel.isUnidentified)
        return paymentModel.isIncoming
            ? OWSLocalizedString(
                "PAYMENTS_UNIDENTIFIED_PAYMENT_INCOMING",
                comment: "Indicator for unidentified incoming payments.",
            )
            : OWSLocalizedString(
                "PAYMENTS_UNIDENTIFIED_PAYMENT_OUTGOING",
                comment: "Indicator for unidentified outgoing payments.",
            )
    }

    // MARK: -

    static func markPaymentAsRead(_ paymentModel: TSPaymentModel, transaction: DBWriteTransaction) {
        owsAssertDebug(paymentModel.isUnread)
        paymentModel.update(withIsUnread: false, transaction: transaction)
    }

    static func markAllUnreadPaymentsAsReadWithSneakyTransaction() {
        SSKEnvironment.shared.databaseStorageRef.write { transaction in
            for paymentModel in PaymentFinder.allUnreadPaymentModels(transaction: transaction) {
                owsAssertDebug(paymentModel.isUnread)
                paymentModel.update(withIsUnread: false, transaction: transaction)
            }
        }
    }

    static func buildPassphraseGrid(
        passphrase: PaymentsPassphrase,
        footerButton: UIView? = nil,
    ) -> UIView {

        struct WordAndIndex {
            let word: String
            let index: Int
        }

        let wordsAndIndices = passphrase.words.enumerated().map { index, word in
            WordAndIndex(word: word, index: index)
        }

        func buildVStack(words: [WordAndIndex]) -> UIStackView {
            let stack = UIStackView()
            stack.axis = .vertical
            stack.alignment = .fill
            stack.spacing = 10

            for wordAndIndex in words {
                let attributedText = NSMutableAttributedString()
                attributedText.append(
                    OWSFormat.formatInt(wordAndIndex.index + 1),
                    attributes: [
                        .font: UIFont.dynamicTypeBodyClamped,
                        .foregroundColor: Theme.secondaryTextAndIconColor,
                    ],
                )
                attributedText.append(
                    ":",
                    attributes: [
                        .font: UIFont.dynamicTypeBodyClamped,
                        .foregroundColor: Theme.secondaryTextAndIconColor,
                    ],
                )
                attributedText.append(
                    " ",
                    attributes: [
                        .font: UIFont.dynamicTypeBodyClamped,
                        .foregroundColor: Theme.secondaryTextAndIconColor,
                    ],
                )
                attributedText.append(
                    wordAndIndex.word,
                    attributes: [
                        .font: UIFont.dynamicTypeHeadlineClamped,
                        .foregroundColor: Theme.primaryTextColor,
                    ],
                )
                let wordLabel = UILabel()
                wordLabel.attributedText = attributedText
                stack.addArrangedSubview(wordLabel)
            }

            return stack
        }

        // Half the words on the each side. If there's an odd number,
        // we want more on the left.
        let pivotIndex = wordsAndIndices.count - (wordsAndIndices.count / 2)
        let leftWords = Array(wordsAndIndices.prefix(pivotIndex))
        let rightWords = Array(wordsAndIndices.suffix(from: pivotIndex))
        let leftWordsStack = buildVStack(words: leftWords)
        let rightWordsStack = buildVStack(words: rightWords)
        let allWordStack = UIStackView(arrangedSubviews: [leftWordsStack, rightWordsStack])
        allWordStack.axis = .horizontal
        allWordStack.alignment = .center
        allWordStack.distribution = .fillEqually
        allWordStack.spacing = 20

        let stack = UIStackView(arrangedSubviews: [allWordStack])
        stack.axis = .vertical
        stack.alignment = .fill
        stack.spacing = 24
        stack.isLayoutMarginsRelativeArrangement = true
        stack.layoutMargins = UIEdgeInsets(hMargin: 20, vMargin: 24)
        let backgroundColor = OWSTableViewController2.cellBackgroundColor(isUsingPresentedStyle: true)
        stack.addBackgroundView(
            withBackgroundColor: backgroundColor,
            cornerRadius: 10,
        )

        if let footerButton {
            let footerButtonStack = UIStackView(arrangedSubviews: [footerButton])
            footerButtonStack.axis = .vertical
            footerButtonStack.alignment = .center
            stack.addArrangedSubview(footerButtonStack)
        }

        return stack
    }

    static func buildTextWithLearnMoreLinkTextView(
        text: String,
        font: UIFont,
        learnMoreUrl: URL,
    ) -> UITextView {
        let textView = LinkingTextView()
        textView.backgroundColor = OWSTableViewController2.tableBackgroundColor(isUsingPresentedStyle: true)
        textView.textColor = .Signal.label
        textView.font = UIFont.dynamicTypeHeadlineClamped
        textView.attributedText = NSAttributedString.composed(of: [
            text,
            " ",
            CommonStrings.learnMore.styled(
                with: .link(learnMoreUrl),
            ),
        ]).styled(
            with: .font(font),
            .color(Theme.secondaryTextAndIconColor),
        )
        textView.linkTextAttributes = [.foregroundColor: Theme.primaryTextColor]
        return textView
    }
}

// MARK: -

public extension TSPaymentModel {

    private static var statusDateShortFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .short
        formatter.timeStyle = .none
        return formatter
    }()

    private static var statusDateTimeLongFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        formatter.timeStyle = .short
        return formatter
    }()

    func statusDescription(isLongForm: Bool) -> String {

        var result: String

        if !isIdentifiedPayment {
            if isOutgoing {
                result = (
                    isLongForm
                        ? OWSLocalizedString(
                            "PAYMENTS_PAYMENT_STATUS_LONG_OUTGOING_COMPLETE",
                            comment: "Status indicator for outgoing payments which are complete.",
                        )
                        : OWSLocalizedString(
                            "PAYMENTS_PAYMENT_STATUS_SHORT_OUTGOING_COMPLETE",
                            comment: "Status indicator for outgoing payments which are complete.",
                        ),
                )
            } else {
                result = (
                    isLongForm
                        ? OWSLocalizedString(
                            "PAYMENTS_PAYMENT_STATUS_LONG_INCOMING_COMPLETE",
                            comment: "Status indicator for incoming payments which are complete.",
                        )
                        : OWSLocalizedString(
                            "PAYMENTS_PAYMENT_STATUS_SHORT_INCOMING_COMPLETE",
                            comment: "Status indicator for incoming payments which are complete.",
                        ),
                )
            }
        } else {
            switch paymentState {
            case .outgoingUnsubmitted:
                result = (
                    isLongForm
                        ? OWSLocalizedString(
                            "PAYMENTS_PAYMENT_STATUS_LONG_OUTGOING_UNSUBMITTED",
                            comment: "Status indicator for outgoing payments which have not yet been submitted.",
                        )
                        : OWSLocalizedString(
                            "PAYMENTS_PAYMENT_STATUS_SHORT_OUTGOING_UNSUBMITTED",
                            comment: "Status indicator for outgoing payments which have not yet been submitted.",
                        ),
                )
            case .outgoingUnverified:
                result = (
                    isLongForm
                        ? OWSLocalizedString(
                            "PAYMENTS_PAYMENT_STATUS_LONG_OUTGOING_UNVERIFIED",
                            comment: "Status indicator for outgoing payments which have been submitted but not yet verified.",
                        )
                        : OWSLocalizedString(
                            "PAYMENTS_PAYMENT_STATUS_SHORT_OUTGOING_UNVERIFIED",
                            comment: "Status indicator for outgoing payments which have been submitted but not yet verified.",
                        ),
                )
            case .outgoingVerified:
                result = (
                    isLongForm
                        ? OWSLocalizedString(
                            "PAYMENTS_PAYMENT_STATUS_LONG_OUTGOING_VERIFIED",
                            comment: "Status indicator for outgoing payments which have been verified but not yet sent.",
                        )
                        : OWSLocalizedString(
                            "PAYMENTS_PAYMENT_STATUS_SHORT_OUTGOING_VERIFIED",
                            comment: "Status indicator for outgoing payments which have been verified but not yet sent.",
                        ),
                )
            case .outgoingSending:
                result = (
                    isLongForm
                        ? OWSLocalizedString(
                            "PAYMENTS_PAYMENT_STATUS_LONG_OUTGOING_SENDING",
                            comment: "Status indicator for outgoing payments which are being sent.",
                        )
                        : OWSLocalizedString(
                            "PAYMENTS_PAYMENT_STATUS_SHORT_OUTGOING_SENDING",
                            comment: "Status indicator for outgoing payments which are being sent.",
                        ),
                )
            case .outgoingSent,
                 .outgoingComplete:
                result = (
                    isLongForm
                        ? OWSLocalizedString(
                            "PAYMENTS_PAYMENT_STATUS_LONG_OUTGOING_SENT",
                            comment: "Status indicator for outgoing payments which have been sent.",
                        )
                        : OWSLocalizedString(
                            "PAYMENTS_PAYMENT_STATUS_SHORT_OUTGOING_SENT",
                            comment: "Status indicator for outgoing payments which have been sent.",
                        ),
                )
            case .outgoingFailed:
                result = Self.description(forFailure: paymentFailure, isIncoming: false, isLongForm: isLongForm)
            case .incomingUnverified:
                result = (
                    isLongForm
                        ? OWSLocalizedString(
                            "PAYMENTS_PAYMENT_STATUS_LONG_INCOMING_UNVERIFIED",
                            comment: "Status indicator for incoming payments which have not yet been verified.",
                        )
                        : OWSLocalizedString(
                            "PAYMENTS_PAYMENT_STATUS_SHORT_INCOMING_UNVERIFIED",
                            comment: "Status indicator for incoming payments which have not yet been verified.",
                        ),
                )
            case .incomingVerified,
                 .incomingComplete:
                result = (
                    isLongForm
                        ? OWSLocalizedString(
                            "PAYMENTS_PAYMENT_STATUS_LONG_INCOMING_VERIFIED",
                            comment: "Status indicator for incoming payments which have been verified.",
                        )
                        : OWSLocalizedString(
                            "PAYMENTS_PAYMENT_STATUS_SHORT_INCOMING_VERIFIED",
                            comment: "Status indicator for incoming payments which have been verified.",
                        ),
                )
            case .incomingFailed:
                result = Self.description(forFailure: paymentFailure, isIncoming: true, isLongForm: isLongForm)
            @unknown default:
                result = (
                    isLongForm
                        ? OWSLocalizedString(
                            "PAYMENTS_PAYMENT_STATUS_LONG_UNKNOWN",
                            comment: "Status indicator for payments which had an unknown failure.",
                        )
                        : OWSLocalizedString(
                            "PAYMENTS_PAYMENT_STATUS_SHORT_UNKNOWN",
                            comment: "Status indicator for payments which had an unknown failure.",
                        ),
                )
            }
        }
        result.append(" ")
        result.append(Self.formatDate(sortDate, isLongForm: isLongForm))

        return result
    }

    static func formatDate(_ date: Date, isLongForm: Bool) -> String {
        if isLongForm {
            return statusDateTimeLongFormatter.string(from: date)
        } else {
            return statusDateShortFormatter.string(from: date)
        }
    }

    private static func description(
        forFailure failure: TSPaymentFailure,
        isIncoming: Bool,
        isLongForm: Bool,
    ) -> String {

        let defaultDescription = (
            isIncoming
                ? OWSLocalizedString(
                    "PAYMENTS_FAILURE_INCOMING_FAILED",
                    comment: "Status indicator for incoming payments which failed.",
                )
                : OWSLocalizedString(
                    "PAYMENTS_FAILURE_OUTGOING_FAILED",
                    comment: "Status indicator for outgoing payments which failed.",
                ),
        )

        switch failure {
        case .none:
            owsFailDebug("Unexpected failure type: \(failure.rawValue)")
            return defaultDescription
        case .unknown:
            owsFailDebug("Unexpected failure type: \(failure.rawValue)")
            return defaultDescription
        case .insufficientFunds:
            owsAssertDebug(!isIncoming)
            return OWSLocalizedString(
                "PAYMENTS_FAILURE_OUTGOING_INSUFFICIENT_FUNDS",
                comment: "Status indicator for outgoing payments which failed due to insufficient funds.",
            )
        case .validationFailed:
            return isIncoming
                ? OWSLocalizedString(
                    "PAYMENTS_FAILURE_INCOMING_VALIDATION_FAILED",
                    comment: "Status indicator for incoming payments which failed to verify.",
                )
                : OWSLocalizedString(
                    "PAYMENTS_FAILURE_OUTGOING_VALIDATION_FAILED",
                    comment: "Status indicator for outgoing payments which failed to verify.",
                )
        case .notificationSendFailed:
            owsAssertDebug(!isIncoming)
            return OWSLocalizedString(
                "PAYMENTS_FAILURE_OUTGOING_NOTIFICATION_SEND_FAILED",
                comment: "Status indicator for outgoing payments for which the notification could not be sent.",
            )
        case .invalid, .expired:
            return OWSLocalizedString(
                "PAYMENTS_FAILURE_INVALID",
                comment: "Status indicator for invalid payments which could not be processed.",
            )
        @unknown default:
            owsFailDebug("Unknown failure type: \(failure.rawValue)")
            return defaultDescription
        }
    }
}

extension OWSActionSheets {
    public static func showPaymentsOutdatedClientSheet(title: OutdatedTitleType) {

        OWSActionSheets.showConfirmationWithNotNowAlert(
            title: title.localizedTitle,
            message: OWSLocalizedString(
                "SETTINGS_PAYMENTS_PAYMENTS_OUTDATED_MESSAGE",
                comment: "Message for payments outdated sheet.",
            ),
            proceedTitle: OWSLocalizedString(
                "SETTINGS_PAYMENTS_PAYMENTS_OUTDATED_BUTTON",
                comment: "Button for payments outdated sheet.",
            ),
            proceedStyle: .default,
        ) { _ in
            let url = TSConstants.appStoreUrl
            UIApplication.shared.open(url, options: [:], completionHandler: nil)
        }
    }
}

public enum OutdatedTitleType {
    case cantSendPayment
    case updateRequired
}

extension OutdatedTitleType {
    var localizedTitle: String {
        switch self {
        case .cantSendPayment:
            return OWSLocalizedString(
                "SETTINGS_PAYMENTS_PAYMENTS_OUTDATED_TITLE_CANT_SEND",
                comment: "Title for payments outdated sheet saying cant send.",
            )
        case .updateRequired:
            return OWSLocalizedString(
                "SETTINGS_PAYMENTS_PAYMENTS_OUTDATED_TITLE_UPDATE",
                comment: "Title for payments outdated sheet saying update required.",
            )
        }
    }
}