Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalServiceKit/Subscriptions/Backups/BackupSubscriptionIssueStore.swift
1 views
//
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

extension Notification.Name {
    public static let backupSubscriptionAlreadyRedeemedDidChange = Notification.Name("BackupSubscriptionAlreadyRedeemedDidChange")
    public static let backupIAPNotFoundLocallyDidChange = Notification.Name("BackupIAPNotFoundLocallyDidChange")
}

public struct BackupSubscriptionIssueStore {
    private enum Keys {
        enum IAPSubscriptionFailedToRenew {
            static let shouldWarn = "shouldWarnIAPSubscriptionFailedToRenew"
            static let lastWarnedEndOfCurrentPeriod = "lastWarnedIAPSubscriptionFailedToRenewEndOfCurrentPeriod"
        }

        enum IAPSubscriptionAlreadyRedeemed {
            static let shouldWarn = "IAPSubscriptionAlreadyRedeemed.shouldWarn"
            static let lastWarnedEndOfCurrentPeriod = "IAPSubscriptionAlreadyRedeemed.lastWarnedEndOfCurrentPeriod"
            static let shouldShowChatListBadge = "IAPSubscriptionAlreadyRedeemed.shouldShowChatListBadge"
            static let shouldShowChatListMenuItem = "IAPSubscriptionAlreadyRedeemed.shouldShowChatListMenuItem"
        }

        enum IAPSubscriptionNotFoundLocally {
            static let shouldWarn = "IAPSubscriptionNotFoundLocally.shouldWarn"
            static let shouldShowChatListBadge = "IAPSubscriptionNotFoundLocally.shouldShowChatListBadge"
            static let shouldShowChatListMenuItem = "IAPSubscriptionNotFoundLocally.shouldShowChatListMenuItem"
        }

        enum IAPSubscriptionExpired {
            static let shouldWarn = "shouldWarnIAPSubscriptionExpired"
        }

        enum TestFlightSubscriptionExpired {
            static let shouldWarn = "shouldWarnTestFlightSubscriptionExpired"
        }
    }

    private let kvStore: NewKeyValueStore
    private let logger: PrefixedLogger

    public init() {
        self.kvStore = NewKeyValueStore(collection: "BackupSubscriptionIssueStore")
        self.logger = PrefixedLogger(prefix: "[Backups][Sub]")
    }

    // MARK: -

    public func shouldWarnIAPSubscriptionFailedToRenew(tx: DBReadTransaction) -> Bool {
        return kvStore.fetchValue(Bool.self, forKey: Keys.IAPSubscriptionFailedToRenew.shouldWarn, tx: tx) ?? false
    }

    public func setShouldWarnIAPSubscriptionFailedToRenew(
        endOfCurrentPeriod: Date,
        tx: DBWriteTransaction,
    ) {
        if
            let lastWarnedEndOfCurrentPeriod = kvStore.fetchValue(
                Date.self,
                forKey: Keys.IAPSubscriptionFailedToRenew.lastWarnedEndOfCurrentPeriod,
                tx: tx,
            ),
            endOfCurrentPeriod == lastWarnedEndOfCurrentPeriod
        {
            // Only save a single warning per period-that-failed-to-renew.
            return
        }

        logger.warn("")
        kvStore.writeValue(true, forKey: Keys.IAPSubscriptionFailedToRenew.shouldWarn, tx: tx)
        kvStore.writeValue(endOfCurrentPeriod, forKey: Keys.IAPSubscriptionFailedToRenew.lastWarnedEndOfCurrentPeriod, tx: tx)
    }

    public func setDidWarnIAPSubscriptionFailedToRenew(tx: DBWriteTransaction) {
        kvStore.writeValue(false, forKey: Keys.IAPSubscriptionFailedToRenew.shouldWarn, tx: tx)
        // Don't wipe lastWarnedEndOfCurrentPeriod, because we never again need
        // to re-warn for a given subscription failing to renew.
    }

    // MARK: -

    public func shouldShowIAPSubscriptionAlreadyRedeemedWarning(tx: DBReadTransaction) -> Bool {
        return kvStore.fetchValue(Bool.self, forKey: Keys.IAPSubscriptionAlreadyRedeemed.shouldWarn, tx: tx) ?? false
    }

    public func shouldShowIAPSubscriptionFailedToRenewChatListBadge(tx: DBReadTransaction) -> Bool {
        return kvStore.fetchValue(Bool.self, forKey: Keys.IAPSubscriptionAlreadyRedeemed.shouldShowChatListBadge, tx: tx) ?? false
    }

    public func shouldShowIAPSubscriptionFailedToRenewChatListMenuItem(tx: DBReadTransaction) -> Bool {
        return kvStore.fetchValue(Bool.self, forKey: Keys.IAPSubscriptionAlreadyRedeemed.shouldShowChatListMenuItem, tx: tx) ?? false
    }

    public func setShouldWarnIAPSubscriptionAlreadyRedeemed(
        endOfCurrentPeriod: Date,
        tx: DBWriteTransaction,
    ) {
        if
            let lastWarnedEndOfCurrentPeriod = kvStore.fetchValue(
                Date.self,
                forKey: Keys.IAPSubscriptionAlreadyRedeemed.lastWarnedEndOfCurrentPeriod,
                tx: tx,
            ),
            endOfCurrentPeriod == lastWarnedEndOfCurrentPeriod
        {
            // Only save a single warning per period-that-was already-redeemed.
            return
        }

        logger.warn("")
        kvStore.writeValue(true, forKey: Keys.IAPSubscriptionAlreadyRedeemed.shouldWarn, tx: tx)
        kvStore.writeValue(true, forKey: Keys.IAPSubscriptionAlreadyRedeemed.shouldShowChatListBadge, tx: tx)
        kvStore.writeValue(true, forKey: Keys.IAPSubscriptionAlreadyRedeemed.shouldShowChatListMenuItem, tx: tx)
        kvStore.writeValue(endOfCurrentPeriod, forKey: Keys.IAPSubscriptionAlreadyRedeemed.lastWarnedEndOfCurrentPeriod, tx: tx)

        tx.addSyncCompletion {
            NotificationCenter.default.postOnMainThread(name: .backupSubscriptionAlreadyRedeemedDidChange, object: nil)
        }
    }

    public func setDidAckIAPSubscriptionAlreadyRedeemedChatListBadge(tx: DBWriteTransaction) {
        kvStore.writeValue(false, forKey: Keys.IAPSubscriptionAlreadyRedeemed.shouldShowChatListBadge, tx: tx)
    }

    public func setDidAckIAPSubscriptionAlreadyRedeemedChatListMenuItem(tx: DBWriteTransaction) {
        kvStore.writeValue(false, forKey: Keys.IAPSubscriptionAlreadyRedeemed.shouldShowChatListMenuItem, tx: tx)
    }

    public func setStopWarningIAPSubscriptionAlreadyRedeemed(tx: DBWriteTransaction) {
        logger.info("")
        kvStore.removeValue(forKey: Keys.IAPSubscriptionAlreadyRedeemed.shouldWarn, tx: tx)
        kvStore.removeValue(forKey: Keys.IAPSubscriptionAlreadyRedeemed.shouldShowChatListBadge, tx: tx)
        kvStore.removeValue(forKey: Keys.IAPSubscriptionAlreadyRedeemed.shouldShowChatListMenuItem, tx: tx)
        // Remove the lastWarnedEndOfCurrentPeriod, because it's possible to
        // clear this warning (e.g., downgrade to free) and try again to redeem
        // the same already-redeemed subscription period, in which case we
        // should warn again.
        kvStore.removeValue(forKey: Keys.IAPSubscriptionAlreadyRedeemed.lastWarnedEndOfCurrentPeriod, tx: tx)

        tx.addSyncCompletion {
            NotificationCenter.default.postOnMainThread(name: .backupSubscriptionAlreadyRedeemedDidChange, object: nil)
        }
    }

    // MARK: -

    public func shouldShowIAPSubscriptionNotFoundLocallyWarning(tx: DBReadTransaction) -> Bool {
        return kvStore.fetchValue(Bool.self, forKey: Keys.IAPSubscriptionNotFoundLocally.shouldWarn, tx: tx) ?? false
    }

    public func shouldShowIAPSubscriptionNotFoundLocallyChatListBadge(tx: DBReadTransaction) -> Bool {
        return kvStore.fetchValue(Bool.self, forKey: Keys.IAPSubscriptionNotFoundLocally.shouldShowChatListBadge, tx: tx) ?? false
    }

    public func shouldShowIAPSubscriptionNotFoundLocallyChatListMenuItem(tx: DBReadTransaction) -> Bool {
        return kvStore.fetchValue(Bool.self, forKey: Keys.IAPSubscriptionNotFoundLocally.shouldShowChatListMenuItem, tx: tx) ?? false
    }

    public func setShouldWarnIAPSubscriptionNotFoundLocally(tx: DBWriteTransaction) {
        logger.warn("")
        kvStore.writeValue(true, forKey: Keys.IAPSubscriptionNotFoundLocally.shouldWarn, tx: tx)
        kvStore.writeValue(true, forKey: Keys.IAPSubscriptionNotFoundLocally.shouldShowChatListBadge, tx: tx)
        kvStore.writeValue(true, forKey: Keys.IAPSubscriptionNotFoundLocally.shouldShowChatListMenuItem, tx: tx)

        tx.addSyncCompletion {
            NotificationCenter.default.postOnMainThread(name: .backupIAPNotFoundLocallyDidChange, object: nil)
        }
    }

    public func setDidAckIAPSubscriptionNotFoundLocallyChatListBadge(tx: DBWriteTransaction) {
        kvStore.writeValue(false, forKey: Keys.IAPSubscriptionNotFoundLocally.shouldShowChatListBadge, tx: tx)
    }

    public func setDidAckIAPSubscriptionNotFoundLocallyChatListMenuItem(tx: DBWriteTransaction) {
        kvStore.writeValue(false, forKey: Keys.IAPSubscriptionNotFoundLocally.shouldShowChatListMenuItem, tx: tx)
    }

    public func setStopWarningIAPSubscriptionNotFoundLocally(tx: DBWriteTransaction) {
        logger.info("")
        kvStore.removeValue(forKey: Keys.IAPSubscriptionNotFoundLocally.shouldWarn, tx: tx)
        kvStore.removeValue(forKey: Keys.IAPSubscriptionNotFoundLocally.shouldShowChatListBadge, tx: tx)
        kvStore.removeValue(forKey: Keys.IAPSubscriptionNotFoundLocally.shouldShowChatListMenuItem, tx: tx)

        tx.addSyncCompletion {
            NotificationCenter.default.postOnMainThread(name: .backupIAPNotFoundLocallyDidChange, object: nil)
        }
    }

    // MARK: -

    public func shouldWarnIAPSubscriptionExpired(tx: DBReadTransaction) -> Bool {
        return kvStore.fetchValue(Bool.self, forKey: Keys.IAPSubscriptionExpired.shouldWarn, tx: tx) ?? false
    }

    public func setShouldWarnIAPSubscriptionExpired(_ value: Bool, tx: DBWriteTransaction) {
        if value { logger.warn("") }
        kvStore.writeValue(value, forKey: Keys.IAPSubscriptionExpired.shouldWarn, tx: tx)
    }

    // MARK: -

    public func shouldWarnTestFlightSubscriptionExpired(tx: DBReadTransaction) -> Bool {
        return kvStore.fetchValue(Bool.self, forKey: Keys.TestFlightSubscriptionExpired.shouldWarn, tx: tx) ?? false
    }

    public func setShouldWarnTestFlightSubscriptionExpired(_ value: Bool, tx: DBWriteTransaction) {
        if value { logger.warn("") }
        kvStore.writeValue(value, forKey: Keys.TestFlightSubscriptionExpired.shouldWarn, tx: tx)
    }
}