Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/ConversationView/ConversationViewController+GiftBadges.swift
1 views
//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
public import SignalServiceKit
import UIKit

extension ConversationViewController {

    public func willWrapGift(_ messageUniqueId: String) -> Bool {
        // If a gift is unwrapped at roughly the same we're reloading the
        // conversation for an unrelated reason, there's a chance we'll try to
        // re-wrap a gift that was just unwrapped. This provides an opportunity to
        // override that behavior.
        return !self.viewState.unwrappedGiftMessageIds.contains(messageUniqueId)
    }

    public func willShakeGift(_ messageUniqueId: String) -> Bool {
        let (justInserted, _) = self.viewState.shakenGiftMessageIds.insert(messageUniqueId)
        return justInserted
    }

    public func willUnwrapGift(_ itemViewModel: CVItemViewModelImpl) {
        self.viewState.unwrappedGiftMessageIds.insert(itemViewModel.interaction.uniqueId)
        self.markGiftAsOpened(itemViewModel.interaction)
    }

    private func markGiftAsOpened(_ interaction: TSInteraction) {
        guard let outgoingMessage = interaction as? TSOutgoingMessage else {
            return
        }
        guard outgoingMessage.giftBadge?.redemptionState == .pending else {
            return
        }
        SSKEnvironment.shared.databaseStorageRef.asyncWrite { transaction in
            outgoingMessage.anyUpdateOutgoingMessage(transaction: transaction) {
                $0.giftBadge?.redemptionState = .opened
            }

            SSKEnvironment.shared.receiptManagerRef.outgoingGiftWasOpened(outgoingMessage, transaction: transaction)
        }
    }

    public func didTapGiftBadge(_ itemViewModel: CVItemViewModelImpl, profileBadge: ProfileBadge, isExpired: Bool, isRedeemed: Bool) {
        AssertIsOnMainThread()

        let viewControllerToPresent: UIViewController

        switch itemViewModel.interaction {
        case let incomingMessage as TSIncomingMessage:
            viewControllerToPresent = self.incomingGiftSheet(
                incomingMessage: incomingMessage,
                profileBadge: profileBadge,
                isExpired: isExpired,
                isRedeemed: isRedeemed,
            )

        case is TSOutgoingMessage:
            guard let thread = thread as? TSContactThread else {
                owsFailDebug("Clicked a gift badge that wasn't in a contact thread")
                return
            }

            viewControllerToPresent = BadgeGiftingThanksSheet(thread: thread, badge: profileBadge)

        default:
            owsFailDebug("Tapped on gift that's not a message")
            return
        }

        self.present(viewControllerToPresent, animated: true)
    }

    private func incomingGiftSheet(
        incomingMessage: TSIncomingMessage,
        profileBadge: ProfileBadge,
        isExpired: Bool,
        isRedeemed: Bool,
    ) -> UIViewController {
        if isExpired {
            let mode: BadgeIssueSheetState.Mode
            if isRedeemed {
                let hasCurrentSubscription = SSKEnvironment.shared.databaseStorageRef.read { tx -> Bool in
                    return DonationSubscriptionManager.probablyHasCurrentSubscription(tx: tx)
                }
                mode = .giftBadgeExpired(hasCurrentSubscription: hasCurrentSubscription)
            } else {
                let fullName = SSKEnvironment.shared.databaseStorageRef.read { transaction -> String in
                    let authorAddress = incomingMessage.authorAddress
                    return SSKEnvironment.shared.contactManagerRef.displayName(for: authorAddress, tx: transaction).resolvedValue()
                }
                mode = .giftNotRedeemed(fullName: fullName)
            }
            let sheet = BadgeIssueSheet(badge: profileBadge, mode: mode)
            sheet.delegate = self
            return sheet
        }
        if isRedeemed {
            let shortName = SSKEnvironment.shared.databaseStorageRef.read { transaction -> String in
                let authorAddress = incomingMessage.authorAddress
                return SSKEnvironment.shared.contactManagerRef.displayName(for: authorAddress, tx: transaction).resolvedValue(useShortNameIfAvailable: true)
            }
            return BadgeGiftingAlreadyRedeemedSheet(badge: profileBadge, shortName: shortName)
        }
        return self.giftRedemptionSheet(incomingMessage: incomingMessage, profileBadge: profileBadge)
    }

    private func giftRedemptionSheet(incomingMessage: TSIncomingMessage, profileBadge: ProfileBadge) -> UIViewController {
        let authorAddress = incomingMessage.authorAddress
        let contactManager = SSKEnvironment.shared.contactManagerRef
        let profileManager = SSKEnvironment.shared.profileManagerRef
        let databaseStorage = SSKEnvironment.shared.databaseStorageRef
        let (shortName, oldBadgesSnapshot) = databaseStorage.read { tx in
            return (
                contactManager.displayName(for: authorAddress, tx: tx).resolvedValue(useShortNameIfAvailable: true),
                ProfileBadgesSnapshot.forLocalProfile(profileManager: profileManager, tx: tx),
            )
        }
        return BadgeThanksSheet(
            newBadge: profileBadge,
            thanksType: .giftReceived(
                shortName: shortName,
                notNowAction: { [weak self] in self?.showRedeemBadgeLaterText() },
                incomingMessage: incomingMessage,
            ),
            oldBadgesSnapshot: oldBadgesSnapshot,
        )
    }

    private func showRedeemBadgeLaterText() {
        let text = OWSLocalizedString(
            "DONATION_ON_BEHALF_OF_A_FRIEND_REDEEM_BADGE_LATER",
            comment: "When you receive a badge as a result of a donation from a friend, a screen is shown. This toast is shown when dismissing that screen if you do not redeem the badge.",
        )
        self.presentToastCVC(text)
    }

}

extension ConversationViewController: BadgeIssueSheetDelegate {
    func badgeIssueSheetActionTapped(_ action: BadgeIssueSheetAction) {
        switch action {
        case .dismiss:
            break
        case .openDonationView:
            let appSettings = AppSettingsViewController.inModalNavigationController(appReadiness: appReadiness)
            let donateViewController = DonateViewController(preferredDonateMode: .oneTime) { [weak self] finishResult in
                switch finishResult {
                case let .completedDonation(donateSheet, receiptCredentialSuccessMode):
                    donateSheet.dismiss(animated: true) { [weak self] in
                        guard
                            let self,
                            let badgeThanksSheetPresenter = BadgeThanksSheetPresenter.fromGlobalsWithSneakyTransaction(
                                successMode: receiptCredentialSuccessMode,
                            )
                        else { return }

                        Task {
                            await badgeThanksSheetPresenter.presentAndRecordBadgeThanks(
                                fromViewController: self,
                            )
                        }
                    }
                case let .monthlySubscriptionCancelled(donateSheet, toastText):
                    donateSheet.dismiss(animated: true) { [weak self] in
                        guard let self else { return }
                        self.presentToastCVC(toastText)
                    }
                }
            }
            appSettings.viewControllers += [
                DonationSettingsViewController(),
                donateViewController,
            ]
            self.presentFormSheet(appSettings, animated: true, completion: nil)
        }
    }
}