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

import Foundation
public import SignalServiceKit
import SignalUI
import UIKit

protocol BadgeIssueSheetDelegate: AnyObject {
    func badgeIssueSheetActionTapped(_ action: BadgeIssueSheetAction)
}

public enum BadgeIssueSheetAction {
    case dismiss
    case openDonationView
}

public class BadgeIssueSheetState {
    public enum Mode {
        case subscriptionExpiredBecauseOfChargeFailure(
            chargeFailureCode: String?,
            paymentMethod: DonationPaymentMethod?,
        )
        case boostExpired(hasCurrentSubscription: Bool)
        case giftBadgeExpired(hasCurrentSubscription: Bool)

        case giftNotRedeemed(fullName: String)

        case bankPaymentFailed(chargeFailureCode: String?)
        case boostBankPaymentProcessing
        case subscriptionBankPaymentProcessing
    }

    public struct Body {
        public let text: String
        public let learnMoreLink: URL?

        public init(_ text: String, learnMoreLink: URL? = nil) {
            self.text = text
            self.learnMoreLink = learnMoreLink
        }
    }

    public struct ActionButton {
        public let action: BadgeIssueSheetAction
        public let text: String
        public let hasNotNow: Bool

        public init(action: BadgeIssueSheetAction, text: String, hasNotNow: Bool) {
            self.action = action
            self.text = text
            self.hasNotNow = hasNotNow
        }
    }

    public let badge: ProfileBadge
    private let mode: Mode
    private let canDonate: Bool

    public init(badge: ProfileBadge, mode: Mode, canDonate: Bool) {
        self.badge = badge
        self.mode = mode
        self.canDonate = canDonate
    }

    public lazy var titleText: String = {
        switch mode {
        case .subscriptionExpiredBecauseOfChargeFailure:
            return OWSLocalizedString(
                "BADGE_EXPIRED_SUBSCRIPTION_TITLE",
                comment: "Title for subscription on the badge expiration sheet.",
            )
        case .boostExpired:
            return OWSLocalizedString(
                "BADGE_EXPIRED_BOOST_TITLE",
                comment: "Title for boost on the badge expiration sheet.",
            )
        case .giftBadgeExpired:
            return OWSLocalizedString(
                "DONATION_FROM_A_FRIEND_BADGE_EXPIRED_TITLE",
                comment: "Someone donated on your behalf and you got a badge, which expired. A sheet appears to tell you about this. This is the title on that sheet.",
            )
        case .giftNotRedeemed:
            return OWSLocalizedString(
                "DONATION_FROM_A_FRIEND_BADGE_NOT_REDEEMED_TITLE",
                comment: "Someone donated on your behalf and you got a badge, which expired before you could redeem it. A sheet appears to tell you about this. This is the title on that sheet.",
            )
        case .bankPaymentFailed:
            return OWSLocalizedString(
                "DONATION_BADGE_ISSUE_SHEET_BANK_PAYMENT_FAILED_TITLE",
                comment: "Title for a sheet explaining that a donation via bank payment has failed.",
            )
        case .boostBankPaymentProcessing, .subscriptionBankPaymentProcessing:
            return OWSLocalizedString(
                "DONATION_BADGE_ISSUE_SHEET_BANK_PAYMENT_PROCESSING_TITLE",
                comment: "Title for a sheet explaining that a donation via bank payment is pending.",
            )
        }
    }()

    public lazy var body: Body = {
        switch mode {
        case let .subscriptionExpiredBecauseOfChargeFailure(
            chargeFailureCode,
            paymentMethod,
        ):
            let formatText = OWSLocalizedString(
                "BADGE_SUBSCRIPTION_EXPIRED_BECAUSE_OF_CHARGE_FAILURE_BODY_FORMAT",
                comment: "String explaining to the user on a badge expiry sheet that their subscription badge has expired because the renewal payment failed. Embeds {{ a specific, already-localized string describing the payment failure }}. Will have a 'learn more' link appended, when it is rendered.",
            )

            let (chargeFailureString, _) = DonationViewsUtil.localizedDonationFailure(
                chargeErrorCode: chargeFailureCode,
                paymentMethod: paymentMethod,
            )

            return Body(
                String.nonPluralLocalizedStringWithFormat(formatText, chargeFailureString),
                learnMoreLink: URL.Support.Donations.badgeExpiration,
            )
        case let .boostExpired(hasCurrentSubscription):
            let bodyText: String
            if hasCurrentSubscription {
                bodyText = OWSLocalizedString(
                    "BADGE_EXPIRED_BOOST_CURRENT_SUSTAINER_BODY",
                    comment: "String explaining to the user that their boost badge has expired while they are a current subscription sustainer on the badge expiry sheet.",
                )
            } else {
                bodyText = OWSLocalizedString(
                    "BADGE_EXPIRED_BOOST_BODY",
                    comment: "String explaining to the user that their boost badge has expired on the badge expiry sheet.",
                )
            }
            return Body(bodyText)
        case .giftBadgeExpired:
            let bodyText = OWSLocalizedString(
                "DONATION_FROM_A_FRIEND_BADGE_EXPIRED_BODY",
                comment: "Someone donated on your behalf and you got a badge, which expired. A sheet appears to tell you about this. This is the text on that sheet.",
            )
            return Body(bodyText)
        case let .giftNotRedeemed(fullName):
            let formatText = OWSLocalizedString(
                "DONATION_FROM_A_FRIEND_BADGE_NOT_REDEEMED_BODY_FORMAT",
                comment: "Someone donated on your behalf and you got a badge, which expired before you could redeem it. A sheet appears to tell you about this. This is the text on that sheet. Embeds {{contact name}}.",
            )
            return Body(String.nonPluralLocalizedStringWithFormat(formatText, fullName))
        case let .bankPaymentFailed(chargeFailureCode):
            let formatText = OWSLocalizedString(
                "DONATION_BADGE_ISSUE_SHEET_BANK_PAYMENT_FAILED_MESSAGE",
                comment: "Message for a sheet explaining that a donation via bank payment has failed. Embeds {{ a specific, already-localized string describing the payment failure }}.",
            )

            let (chargeFailureString, _) = DonationViewsUtil.localizedDonationFailure(
                chargeErrorCode: chargeFailureCode,
                paymentMethod: .sepa,
            )

            return Body(String.nonPluralLocalizedStringWithFormat(formatText, chargeFailureString))
        case .boostBankPaymentProcessing:
            let bodyFormat = OWSLocalizedString(
                "DONATION_BADGE_ISSUE_SHEET_ONE_TIME_BANK_PAYMENT_PROCESSING_MESSAGE",
                comment: "Message for a sheet explaining that a one-time donation via bank payment is pending, and how that will affect the user's badge. Embeds {{ the name of the badge }}. Will have a 'learn more' link appended, when it is rendered.",
            )

            return Body(
                String.nonPluralLocalizedStringWithFormat(bodyFormat, badge.localizedName),
                learnMoreLink: URL.Support.Donations.donationPending,
            )
        case .subscriptionBankPaymentProcessing:
            let bodyFormat = OWSLocalizedString(
                "DONATION_BADGE_ISSUE_SHEET_RECURRING_BANK_PAYMENT_PROCESSING_MESSAGE",
                comment: "Message for a sheet explaining that a recurring donation via bank payment is pending, and how that will affect the user's badge. Embeds {{ the name of the badge }}. Will have a 'learn more' link appended, when it is rendered.",
            )

            return Body(
                String.nonPluralLocalizedStringWithFormat(bodyFormat, badge.localizedName),
                learnMoreLink: URL.Support.Donations.donationPending,
            )
        }
    }()

    public lazy var actionButton: ActionButton = {
        enum AskUserToDonateMode {
            case dontAsk
            case askToDonate
            case askToTryAgain
            case askToRenewSubscription
        }

        let askUserToDonateMode: AskUserToDonateMode = {
            guard canDonate else { return .dontAsk }

            switch mode {
            case
                .boostExpired,
                .giftBadgeExpired(hasCurrentSubscription: false):
                return .askToDonate
            case .bankPaymentFailed:
                return .askToTryAgain
            case .subscriptionExpiredBecauseOfChargeFailure:
                return .askToRenewSubscription
            case
                .giftBadgeExpired(hasCurrentSubscription: true),
                .giftNotRedeemed,
                .boostBankPaymentProcessing,
                .subscriptionBankPaymentProcessing:
                return .dontAsk
            }
        }()

        switch askUserToDonateMode {
        case .dontAsk:
            return ActionButton(
                action: .dismiss,
                text: CommonStrings.okayButton,
                hasNotNow: false,
            )
        case .askToDonate:
            return ActionButton(
                action: .openDonationView,
                text: OWSLocalizedString(
                    "BADGE_EXPIRED_DONATE_BUTTON",
                    comment: "Button text when a badge expires, asking users to donate",
                ),
                hasNotNow: true,
            )
        case .askToTryAgain:
            return ActionButton(
                action: .openDonationView,
                text: OWSLocalizedString(
                    "DONATION_BADGE_ISSUE_SHEET_TRY_AGAIN_BUTTON_TITLE",
                    comment: "Title for a button asking the user to try their donation again, because something went wrong.",
                ),
                hasNotNow: true,
            )
        case .askToRenewSubscription:
            return ActionButton(
                action: .openDonationView,
                text: OWSLocalizedString(
                    "DONATION_BADGE_ISSUE_SHEET_RENEW_SUBSCRIPTION_BUTTON_TITLE",
                    comment: "Title for a button asking the user to renew their subscription, because it has expired.",
                ),
                hasNotNow: true,
            )
        }
    }()

    var showIconAlert: Bool {
        switch mode {
        case
            .boostExpired,
            .giftBadgeExpired,
            .bankPaymentFailed,
            .subscriptionExpiredBecauseOfChargeFailure:
            return true
        case
            .giftNotRedeemed,
            .boostBankPaymentProcessing,
            .subscriptionBankPaymentProcessing:
            return false
        }
    }
}

class BadgeIssueSheet: OWSTableSheetViewController {
    private let state: BadgeIssueSheetState

    weak var delegate: BadgeIssueSheetDelegate?

    init(badge: ProfileBadge, mode: BadgeIssueSheetState.Mode) {
        self.state = BadgeIssueSheetState(
            badge: badge,
            mode: mode,
            canDonate: DonationUtilities.canDonateInAnyWay(
                tsAccountManager: DependenciesBridge.shared.tsAccountManager,
            ),
        )
        owsAssertDebug(state.badge.assets != nil)

        super.init()

        updateTableContents()
    }

    override func tableContents() -> OWSTableContents {
        let contents = OWSTableContents()

        let headerSection = OWSTableSection()
        headerSection.hasBackground = false
        headerSection.customHeaderHeight = 1
        contents.add(headerSection)

        headerSection.add(.init(customCellBlock: { [weak self] in
            let cell = OWSTableItem.newCell()
            guard let self else { return cell }
            cell.selectionStyle = .none

            let stackView = UIStackView()
            stackView.axis = .vertical
            stackView.alignment = .center
            stackView.layoutMargins = .init(top: 24, left: 24, bottom: 0, right: 24)
            stackView.isLayoutMarginsRelativeArrangement = true

            cell.contentView.addSubview(stackView)
            stackView.autoPinEdgesToSuperviewEdges()

            let containerView = UIView()
            stackView.addArrangedSubview(containerView)
            stackView.setCustomSpacing(24, after: containerView)

            let badgeImageView = UIImageView()
            badgeImageView.image = self.state.badge.assets?.universal160
            badgeImageView.autoSetDimensions(to: CGSize(square: 80))
            containerView.addSubview(badgeImageView)
            badgeImageView.autoPinEdgesToSuperviewEdges()

            if self.state.showIconAlert {
                let alertImageView = UIImageView()
                alertImageView.image = UIImage(named: "alert")
                alertImageView.autoSetDimensions(to: CGSize(square: 24))
                containerView.addSubview(alertImageView)
                alertImageView.autoPinEdge(.right, to: .right, of: badgeImageView)
                alertImageView.autoPinEdge(.top, to: .top, of: badgeImageView)
            }

            let titleLabel = UILabel.title2Label(text: self.state.titleText)
            stackView.addArrangedSubview(titleLabel)
            stackView.setCustomSpacing(12, after: titleLabel)

            let bodyLabel: UIView
            if let learnMoreLink = self.state.body.learnMoreLink {
                let learnMore = OWSLocalizedString(
                    "BADGE_EXPIRED_LEARN_MORE_LINK",
                    comment: "Text for the 'learn more' link in a sheet explaining there's been an issue with your badge.",
                ).styled(with: .link(learnMoreLink))
                let label = LinkingTextView()
                label.attributedText = .composed(of: [self.state.body.text, " ", learnMore])
                    .styled(with: .color(.Signal.secondaryLabel), .font(.dynamicTypeSubheadlineClamped))
                label.textAlignment = .center
                bodyLabel = label
            } else {
                let label = UILabel()
                label.font = .dynamicTypeSubheadlineClamped
                label.textColor = .Signal.secondaryLabel
                label.numberOfLines = 0
                label.text = self.state.body.text
                label.textAlignment = .center
                bodyLabel = label
            }
            stackView.addArrangedSubview(bodyLabel)
            stackView.setCustomSpacing(24, after: bodyLabel)

            return cell
        }, actionBlock: nil))

        let buttonSection = OWSTableSection()
        buttonSection.hasBackground = false
        contents.add(buttonSection)
        buttonSection.add(.init(customCellBlock: { [weak self] in
            let cell = OWSTableItem.newCell()
            cell.selectionStyle = .none
            guard let self else { return cell }

            let actionButton = UIButton(
                configuration: .largePrimary(title: self.state.actionButton.text),
                primaryAction: UIAction { _ in
                    self.didTapAction()
                },
            )

            var buttons = [actionButton]

            if self.state.actionButton.hasNotNow {
                let notNowButton = UIButton(
                    configuration: .largeSecondary(title: CommonStrings.notNowButton),
                    primaryAction: UIAction { _ in
                        self.didDismiss()
                    },
                )
                buttons.append(notNowButton)
            }

            let stackView = UIStackView.verticalButtonStack(buttons: buttons, isFullWidthButtons: true)
            stackView.directionalLayoutMargins.bottom = 0
            cell.contentView.addSubview(stackView)
            stackView.autoPinEdgesToSuperviewEdges()

            return cell
        }, actionBlock: nil))

        return contents
    }

    override func willDismissInteractively() {
        didDismiss()
        super.willDismissInteractively()
    }

    @objc
    private func didTapAction() {
        didDismiss()
        delegate?.badgeIssueSheetActionTapped(state.actionButton.action)
    }

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