Signal-iOS/SignalServiceKit/Subscriptions/Backups/BackupSubscriptionIssueStore.swift

290 lines
12 KiB
Swift

//
// 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 IAPSubscriptionExpiringSoon {
static let firstWarningDate = "IAPSubscriptionExpiringSoon.firstWarningDate"
static let secondWarningDate = "IAPSubscriptionExpiringSoon.secondWarningDate"
}
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 shouldShowIAPSubscriptionAlreadyRedeemedChatListBadge(tx: DBReadTransaction) -> Bool {
return kvStore.fetchValue(Bool.self, forKey: Keys.IAPSubscriptionAlreadyRedeemed.shouldShowChatListBadge, tx: tx) ?? false
}
public func shouldShowIAPSubscriptionAlreadyRedeemedChatListMenuItem(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 enum IAPSubscriptionExpiringSoonWarning {
case firstWarning(Date)
case secondWarning(Date)
public var date: Date {
switch self {
case .firstWarning(let date): date
case .secondWarning(let date): date
}
}
}
public func setShouldWarnIAPSubscriptionExpiringSoon(
endOfCurrentPeriod: Date,
now: Date,
tx: DBWriteTransaction,
) {
logger.warn("")
let halfwayTillEndOfCurrentPeriod = endOfCurrentPeriod.timeIntervalSince(now) / 2
// Warn twice: once halfway till the expiration (or three days out), and
// again two days out.
let firstWarningDate = endOfCurrentPeriod.addingTimeInterval(-1 * max(3 * .day, halfwayTillEndOfCurrentPeriod))
let secondWarningDate = endOfCurrentPeriod.addingTimeInterval(-2 * .day)
kvStore.writeValue(firstWarningDate, forKey: Keys.IAPSubscriptionExpiringSoon.firstWarningDate, tx: tx)
kvStore.writeValue(secondWarningDate, forKey: Keys.IAPSubscriptionExpiringSoon.secondWarningDate, tx: tx)
}
public func shouldWarnIAPSubscriptionExpiringSoon(tx: DBReadTransaction) -> IAPSubscriptionExpiringSoonWarning? {
if
let firstWarningDate = kvStore.fetchValue(
Date.self,
forKey: Keys.IAPSubscriptionExpiringSoon.firstWarningDate,
tx: tx,
)
{
return .firstWarning(firstWarningDate)
}
if
let secondWarningDate = kvStore.fetchValue(
Date.self,
forKey: Keys.IAPSubscriptionExpiringSoon.secondWarningDate,
tx: tx,
)
{
return .secondWarning(secondWarningDate)
}
return nil
}
public func setDidWarnIAPSubscriptionExpiringSoon(
warning: IAPSubscriptionExpiringSoonWarning,
tx: DBWriteTransaction,
) {
switch warning {
case .firstWarning:
kvStore.removeValue(forKey: Keys.IAPSubscriptionExpiringSoon.firstWarningDate, tx: tx)
case .secondWarning:
kvStore.removeValue(forKey: Keys.IAPSubscriptionExpiringSoon.secondWarningDate, tx: tx)
}
}
public func setStopWarningIAPSubscriptionExpiringSoon(tx: DBWriteTransaction) {
kvStore.removeValue(forKey: Keys.IAPSubscriptionExpiringSoon.firstWarningDate, tx: tx)
kvStore.removeValue(forKey: Keys.IAPSubscriptionExpiringSoon.secondWarningDate, tx: tx)
}
// 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)
}
}