158 lines
6.0 KiB
Swift
158 lines
6.0 KiB
Swift
//
|
|
// Copyright 2024 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import LibSignalClient
|
|
|
|
final class BackupSubscriptionRedemptionContext: Codable {
|
|
/// Represents the state of an in-progress attempt to redeem a subscription.
|
|
///
|
|
/// It's important that we update this over the duration of the job, because
|
|
/// (generally) once we've made a receipt-credential-related request to the
|
|
/// server remote state has been set. If the app exits between making two
|
|
/// requests, we need to have stored the data we sent in the first request
|
|
/// so we can retry the second.
|
|
///
|
|
/// Much like for donations, there are two network requests required to
|
|
/// redeem a receipt credential for a Backups subscription.
|
|
///
|
|
/// The first is to "request a receipt credential", which takes a
|
|
/// locally-generated "receipt credential request" and returns us data we
|
|
/// can use to construct a "receipt credential presentation". Once we have
|
|
/// the receipt credential presentation, we can discard the receipt
|
|
/// credential request.
|
|
///
|
|
/// The second is to "redeem the receipt credential", which sends the
|
|
/// receipt credential presentation from the first request to the service,
|
|
/// which validates it and subsequently records that our account is now
|
|
/// eligible (or has extended its eligibility) for paid-tier Backups. When
|
|
/// this completes, the attempt is complete.
|
|
enum RedemptionAttemptState {
|
|
/// This attempt is at a clean slate.
|
|
case unattempted
|
|
|
|
/// We need to request a receipt credential, using the associated
|
|
/// request and context objects.
|
|
///
|
|
/// Note that it is safe to request a receipt credential multiple times,
|
|
/// as long as the request/context are the same across retries. Receipt
|
|
/// credential requests do not expire, and the returned receipt
|
|
/// credential will always correspond to the latest entitling
|
|
/// transaction.
|
|
case receiptCredentialRequesting(
|
|
request: ReceiptCredentialRequest,
|
|
context: ReceiptCredentialRequestContext,
|
|
)
|
|
|
|
/// We have a receipt credential, and need to redeem it.
|
|
///
|
|
/// Note that it is safe to attempt to redeem a receipt credential
|
|
/// multiple times for the same subscription period.
|
|
case receiptCredentialRedemption(ReceiptCredential)
|
|
}
|
|
|
|
let subscriberId: Data
|
|
/// `nil` for legacy contexts.
|
|
let subscriptionEndOfCurrentPeriod: Date?
|
|
var attemptState: RedemptionAttemptState
|
|
|
|
init(
|
|
subscriberId: Data,
|
|
subscriptionEndOfCurrentPeriod: Date,
|
|
) {
|
|
self.subscriberId = subscriberId
|
|
self.subscriptionEndOfCurrentPeriod = subscriptionEndOfCurrentPeriod
|
|
self.attemptState = .unattempted
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private enum StoreKeys {
|
|
static let context = "context"
|
|
}
|
|
|
|
private static let kvStore = KeyValueStore(collection: "BackupSubscriptionRedemptionContext")
|
|
|
|
static func fetch(tx: DBReadTransaction) -> BackupSubscriptionRedemptionContext? {
|
|
guard let jsonData = kvStore.getData(StoreKeys.context, transaction: tx) else {
|
|
return nil
|
|
}
|
|
|
|
do {
|
|
return try JSONDecoder().decode(BackupSubscriptionRedemptionContext.self, from: jsonData)
|
|
} catch {
|
|
owsFailDebug("Failed to decode context! \(error)")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func upsert(tx: DBWriteTransaction) {
|
|
let jsonData: Data
|
|
do {
|
|
jsonData = try JSONEncoder().encode(self)
|
|
} catch {
|
|
owsFailDebug("Failed to encode context! \(error)")
|
|
return
|
|
}
|
|
|
|
Self.kvStore.setData(jsonData, key: StoreKeys.context, transaction: tx)
|
|
}
|
|
|
|
func delete(tx: DBWriteTransaction) {
|
|
Self.kvStore.removeValue(forKey: StoreKeys.context, transaction: tx)
|
|
}
|
|
|
|
// MARK: - Codable
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case subscriberId
|
|
case subscriptionEndOfCurrentPeriod
|
|
case receiptCredentialRequest
|
|
case receiptCredentialRequestContext
|
|
case receiptCredential
|
|
}
|
|
|
|
init(from decoder: any Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
|
|
self.subscriberId = try container.decode(Data.self, forKey: .subscriberId)
|
|
self.subscriptionEndOfCurrentPeriod = try container.decodeIfPresent(Date.self, forKey: .subscriptionEndOfCurrentPeriod)
|
|
|
|
if
|
|
let requestData = try container.decodeIfPresent(Data.self, forKey: .receiptCredentialRequest),
|
|
let contextData = try container.decodeIfPresent(Data.self, forKey: .receiptCredentialRequestContext)
|
|
{
|
|
attemptState = .receiptCredentialRequesting(
|
|
request: try ReceiptCredentialRequest(contents: requestData),
|
|
context: try ReceiptCredentialRequestContext(contents: contextData),
|
|
)
|
|
} else if
|
|
let credentialData = try container.decodeIfPresent(Data.self, forKey: .receiptCredential)
|
|
{
|
|
attemptState = .receiptCredentialRedemption(
|
|
try ReceiptCredential(contents: credentialData),
|
|
)
|
|
} else {
|
|
attemptState = .unattempted
|
|
}
|
|
}
|
|
|
|
func encode(to encoder: any Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
|
|
try container.encode(subscriberId, forKey: .subscriberId)
|
|
try container.encodeIfPresent(subscriptionEndOfCurrentPeriod, forKey: .subscriptionEndOfCurrentPeriod)
|
|
|
|
switch attemptState {
|
|
case .receiptCredentialRequesting(let request, let context):
|
|
try container.encode(request.serialize(), forKey: .receiptCredentialRequest)
|
|
try container.encode(context.serialize(), forKey: .receiptCredentialRequestContext)
|
|
case .receiptCredentialRedemption(let credential):
|
|
try container.encode(credential.serialize(), forKey: .receiptCredential)
|
|
case .unattempted:
|
|
break
|
|
}
|
|
}
|
|
}
|