// // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // public enum _ReceiptCredentialResultStore_Mode: CaseIterable { /// Refers to a one-time boost. case oneTimeBoost /// Refers to a recurring subscription that was started for the first time. case recurringSubscriptionInitiation /// Refers to a recurring subscription that automatically renewed. case recurringSubscriptionRenewal } public protocol ReceiptCredentialResultStore { typealias Mode = _ReceiptCredentialResultStore_Mode // MARK: Error persistence func getRequestError( errorMode: Mode, tx: DBReadTransaction ) -> ReceiptCredentialRequestError? func setRequestError( error: ReceiptCredentialRequestError, errorMode: Mode, tx: DBWriteTransaction ) func clearRequestError( errorMode: Mode, tx: DBWriteTransaction ) // MARK: Success persistence func getRedemptionSuccess( successMode: Mode, tx: DBReadTransaction ) -> ReceiptCredentialRedemptionSuccess? func setRedemptionSuccess( success: ReceiptCredentialRedemptionSuccess, successMode: Mode, tx: DBWriteTransaction ) func clearRedemptionSuccess( successMode: Mode, tx: DBWriteTransaction ) // MARK: Presentation func hasPresentedError(errorMode: Mode, tx: DBReadTransaction) -> Bool func setHasPresentedError(errorMode: Mode, tx: DBWriteTransaction) func hasPresentedSuccess(successMode: Mode, tx: DBReadTransaction) -> Bool func setHasPresentedSuccess(successMode: Mode, tx: DBWriteTransaction) } public extension ReceiptCredentialResultStore { func getRequestErrorForAnyRecurringSubscription( tx: DBReadTransaction ) -> ReceiptCredentialRequestError? { if let initiationError = getRequestError( errorMode: .recurringSubscriptionInitiation, tx: tx ) { return initiationError } else if let renewalError = getRequestError( errorMode: .recurringSubscriptionRenewal, tx: tx ) { return renewalError } return nil } func getRedemptionSuccessForAnyRecurringSubscription( tx: DBReadTransaction ) -> ReceiptCredentialRedemptionSuccess? { if let initiationSuccess = getRedemptionSuccess( successMode: .recurringSubscriptionInitiation, tx: tx ) { return initiationSuccess } else if let renewalSuccess = getRedemptionSuccess( successMode: .recurringSubscriptionRenewal, tx: tx ) { return renewalSuccess } return nil } func clearRequestErrorForAnyRecurringSubscription(tx: DBWriteTransaction) { clearRequestError(errorMode: .recurringSubscriptionInitiation, tx: tx) clearRequestError(errorMode: .recurringSubscriptionRenewal, tx: tx) } func clearRedemptionSuccessForAnyRecurringSubscription(tx: DBWriteTransaction) { clearRedemptionSuccess(successMode: .recurringSubscriptionInitiation, tx: tx) clearRedemptionSuccess(successMode: .recurringSubscriptionRenewal, tx: tx) } } final class ReceiptCredentialResultStoreImpl: ReceiptCredentialResultStore { /// Uses values taken from ``SubscriptionManagerImpl``, to preserve /// compatibility with legacy data stored there. /// /// Specifically, recurring subscriptions have historically stored error /// codes. One-time boosts never stored an error code, and neither stored /// any information beyond the error code. private enum LegacyErrorConstants { static let collection = "SubscriptionKeyValueStore" static let recurringSubscriptionKey = "lastSubscriptionReceiptRequestFailedKey" } private enum StoreConstants { static let errorCollection = "SubRecCredReqErrorStore" static let successCollection = "SubRecCredReqSuccessStore" static let errorPresentationCollection = "SubRecCredReqErrorPresStore" static let successPresentationCollection = "SubRecCredReqSuccessPresStore" static let oneTimeBoostKey = "oneTimeBoost" static let recurringSubscriptionInitiationKey = "recurringSubscriptionInitiation" static let recurringSubscriptionRenewalKey = "recurringSubscriptionRenewal" } private let legacyErrorKVStore: KeyValueStore private let errorKVStore: KeyValueStore private let successKVStore: KeyValueStore private let errorPresentationKVStore: KeyValueStore private let successPresentationKVStore: KeyValueStore init(kvStoreFactory: KeyValueStoreFactory) { legacyErrorKVStore = kvStoreFactory.keyValueStore(collection: LegacyErrorConstants.collection) errorKVStore = kvStoreFactory.keyValueStore(collection: StoreConstants.errorCollection) successKVStore = kvStoreFactory.keyValueStore(collection: StoreConstants.successCollection) errorPresentationKVStore = kvStoreFactory.keyValueStore(collection: StoreConstants.errorPresentationCollection) successPresentationKVStore = kvStoreFactory.keyValueStore(collection: StoreConstants.successPresentationCollection) } private func key(mode: Mode) -> String { switch mode { case .oneTimeBoost: return StoreConstants.oneTimeBoostKey case .recurringSubscriptionInitiation: return StoreConstants.recurringSubscriptionInitiationKey case .recurringSubscriptionRenewal: return StoreConstants.recurringSubscriptionRenewalKey } } // MARK: - Error persistence func getRequestError( errorMode: Mode, tx: DBReadTransaction ) -> ReceiptCredentialRequestError? { if let error: ReceiptCredentialRequestError = try? errorKVStore.getCodableValue( forKey: key(mode: errorMode), transaction: tx ) { return error } else if let legacyErrorCodeInt = legacyErrorKVStore.getInt( LegacyErrorConstants.recurringSubscriptionKey, transaction: tx ), let legacyErrorCode = ReceiptCredentialRequestError.ErrorCode( rawValue: legacyErrorCodeInt ) { // See note above – we might have just the error code int, and if so // we'll do our best without the rest of the state. return ReceiptCredentialRequestError( legacyErrorCode: legacyErrorCode ) } return nil } func setRequestError( error: ReceiptCredentialRequestError, errorMode: Mode, tx: DBWriteTransaction ) { switch errorMode { case .oneTimeBoost: break case .recurringSubscriptionInitiation, .recurringSubscriptionRenewal: legacyErrorKVStore.removeValue( forKey: LegacyErrorConstants.recurringSubscriptionKey, transaction: tx ) } let modeKey = key(mode: errorMode) try? errorKVStore.setCodable(error, key: modeKey, transaction: tx) // Setting a new error means we haven't presented it, either. errorPresentationKVStore.removeValue(forKey: modeKey, transaction: tx) } func clearRequestError(errorMode: Mode, tx: DBWriteTransaction) { switch errorMode { case .oneTimeBoost: break case .recurringSubscriptionInitiation, .recurringSubscriptionRenewal: legacyErrorKVStore.removeValue( forKey: LegacyErrorConstants.recurringSubscriptionKey, transaction: tx ) } let modeKey = key(mode: errorMode) errorKVStore.removeValue(forKey: modeKey, transaction: tx) // Clearing the error means we haven't presented it, either. errorPresentationKVStore.removeValue(forKey: modeKey, transaction: tx) } // MARK: - Success persistence func getRedemptionSuccess( successMode: Mode, tx: DBReadTransaction ) -> ReceiptCredentialRedemptionSuccess? { return try? successKVStore.getCodableValue( forKey: key(mode: successMode), transaction: tx ) } func setRedemptionSuccess( success: ReceiptCredentialRedemptionSuccess, successMode: Mode, tx: DBWriteTransaction ) { let modeKey = key(mode: successMode) try? successKVStore.setCodable( success, key: modeKey, transaction: tx ) // Setting a new success means we haven't presented it, either. successPresentationKVStore.removeValue(forKey: modeKey, transaction: tx) } func clearRedemptionSuccess( successMode: Mode, tx: DBWriteTransaction ) { let modeKey = key(mode: successMode) successKVStore.removeValue( forKey: modeKey, transaction: tx ) // Clearing the success means we haven't presented it, either. successPresentationKVStore.removeValue(forKey: modeKey, transaction: tx) } // MARK: - Presentation func hasPresentedError(errorMode: Mode, tx: DBReadTransaction) -> Bool { return errorPresentationKVStore.getBool( key(mode: errorMode), defaultValue: false, transaction: tx ) } func setHasPresentedError(errorMode: Mode, tx: DBWriteTransaction) { errorPresentationKVStore.setBool( true, key: key(mode: errorMode), transaction: tx ) } func hasPresentedSuccess(successMode: Mode, tx: DBReadTransaction) -> Bool { return successPresentationKVStore.getBool( key(mode: successMode), defaultValue: false, transaction: tx ) } func setHasPresentedSuccess(successMode: Mode, tx: DBWriteTransaction) { successPresentationKVStore.setBool( true, key: key(mode: successMode), transaction: tx ) } }