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

public import SignalServiceKit
import SignalUI

// MARK: -

protocol PaymentsHistoryDataSourceDelegate: AnyObject {
    var recordType: PaymentsHistoryDataSource.RecordType { get }

    var maxRecordCount: Int? { get }

    func didUpdateContent()
}

// MARK: -

@MainActor
class PaymentsHistoryDataSource {

    enum RecordType: Int, CustomStringConvertible {
        case all = 0
        case incoming
        case outgoing

        var description: String {
            switch self {
            case .all:
                return ".all"
            case .incoming:
                return ".incoming"
            case .outgoing:
                return ".outgoing"
            }
        }
    }

    // MARK: -

    weak var delegate: PaymentsHistoryDataSourceDelegate? {
        didSet {
            updateContent()
        }
    }

    private(set) var items = [PaymentsHistoryItem]()

    var hasItems: Bool {
        !items.isEmpty
    }

    var count: Int {
        items.count
    }

    init() {
        DependenciesBridge.shared.databaseChangeObserver.appendDatabaseChangeDelegate(self)

        updateContent()
    }

    func updateContent() {
        guard let delegate else {
            return
        }
        items = loadAllPaymentsHistoryItems(delegate: delegate)
        delegate.didUpdateContent()
    }

    private func loadAllPaymentsHistoryItems(delegate: PaymentsHistoryDataSourceDelegate) -> [PaymentsHistoryItem] {
        SSKEnvironment.shared.databaseStorageRef.read { transaction in
            // PAYMENTS TODO: Should we using paging, etc?
            // PAYMENTS TODO: Sort in query?
            var paymentModels: [TSPaymentModel] = TSPaymentModel.anyFetchAll(transaction: transaction)
            paymentModels.sortBySortDate(descending: true)
            paymentModels = paymentModels.filter { paymentModel in
                switch delegate.recordType {
                case .all:
                    return true
                case .incoming:
                    return paymentModel.isIncoming
                case .outgoing:
                    return paymentModel.isOutgoing
                }
            }
            if
                let maxRecordCount = delegate.maxRecordCount,
                maxRecordCount < paymentModels.count
            {
                paymentModels = Array(paymentModels.prefix(maxRecordCount))
            }

            return paymentModels.map { paymentModel in
                var displayName: String
                if paymentModel.isUnidentified {
                    displayName = PaymentsViewUtils.buildUnidentifiedTransactionString(paymentModel: paymentModel)
                } else if let senderOrRecipientAci = paymentModel.senderOrRecipientAci {
                    displayName = SSKEnvironment.shared.contactManagerRef.displayName(for: SignalServiceAddress(senderOrRecipientAci), tx: transaction).resolvedValue()
                } else if paymentModel.isOutgoingTransfer {
                    displayName = OWSLocalizedString(
                        "PAYMENTS_TRANSFER_OUT_PAYMENT",
                        comment: "Label for 'transfer out' payments.",
                    )
                } else if paymentModel.isDefragmentation {
                    displayName = OWSLocalizedString(
                        "PAYMENTS_DEFRAGMENTATION_PAYMENT",
                        comment: "Label for 'defragmentation' payments.",
                    )
                } else {
                    displayName = OWSLocalizedString(
                        "PAYMENTS_UNKNOWN_PAYMENT",
                        comment: "Label for unknown payments.",
                    )
                }
                return PaymentsHistoryModelItem(paymentModel: paymentModel, displayName: displayName)
            }
        }
    }

    func cell(forIndexPath indexPath: IndexPath, tableView: UITableView) -> UITableViewCell {
        guard let paymentItem = items[safe: indexPath.row] else {
            owsFailDebug("Invalid index path.")
            return UITableViewCell()
        }
        return Self.cell(forPaymentItem: paymentItem, tableView: tableView, indexPath: indexPath)
    }

    static func cell(
        forPaymentItem paymentItem: PaymentsHistoryItem,
        tableView: UITableView,
        indexPath: IndexPath,
    ) -> UITableViewCell {
        guard
            let cell = tableView.dequeueReusableCell(
                withIdentifier: PaymentModelCell.reuseIdentifier,
                for: indexPath,
            ) as? PaymentModelCell
        else {
            owsFailDebug("Cell had unexpected type.")
            return UITableViewCell()
        }
        cell.configure(paymentItem: paymentItem)
        return cell
    }
}

// MARK: -

extension PaymentsHistoryDataSource: DatabaseChangeDelegate {

    func databaseChangesDidUpdate(databaseChanges: DatabaseChanges) {
        guard databaseChanges.didUpdate(tableName: TSPaymentModel.databaseTableName) else {
            return
        }

        updateContent()
    }

    func databaseChangesDidUpdateExternally() {
        updateContent()
    }

    func databaseChangesDidReset() {
        updateContent()
    }
}

extension ArchivedPayment {
    public func statusDescription(isOutgoing: Bool) -> String? {
        if status.isFailure {
            switch (failureReason, isOutgoing) {
            case (.insufficientFundsFailure, true):
                return OWSLocalizedString(
                    "PAYMENTS_FAILURE_OUTGOING_INSUFFICIENT_FUNDS",
                    comment: "Status indicator for outgoing payments which failed due to insufficient funds.",
                )
            case (.networkFailure, true):
                return OWSLocalizedString(
                    "PAYMENTS_FAILURE_OUTGOING_NOTIFICATION_SEND_FAILED",
                    comment: "Status indicator for outgoing payments for which the notification could not be sent.",
                )
            case (_, true):
                return OWSLocalizedString(
                    "PAYMENTS_FAILURE_OUTGOING_FAILED",
                    comment: "Status indicator for outgoing payments which failed.",
                )
            case (_, false):
                return OWSLocalizedString(
                    "PAYMENTS_FAILURE_INCOMING_FAILED",
                    comment: "Status indicator for incoming payments which failed.",
                )
            }
        } else {
            switch (status, isOutgoing) {
            case (.initial, true):
                return OWSLocalizedString(
                    "PAYMENTS_PAYMENT_STATUS_SHORT_OUTGOING_UNSUBMITTED",
                    comment: "Status indicator for outgoing payments which have not yet been submitted.",
                )
            case (.submitted, true):
                return OWSLocalizedString(
                    "PAYMENTS_PAYMENT_STATUS_SHORT_OUTGOING_SENDING",
                    comment: "Status indicator for outgoing payments which are being sent.",
                )
            case (_, true):
                return OWSLocalizedString(
                    "PAYMENTS_PAYMENT_STATUS_SHORT_OUTGOING_SENT",
                    comment: "Status indicator for outgoing payments which have been sent.",
                )
            case (_, false):
                return OWSLocalizedString(
                    "PAYMENTS_PAYMENT_STATUS_SHORT_INCOMING_COMPLETE",
                    comment: "Status indicator for incoming payments which are complete.",
                )
            }
        }
    }
}