Signal-iOS/SignalMessaging/Subscriptions/SubscriptionReceiptCredentialRedemptionJob.swift
2022-01-03 12:00:19 -08:00

210 lines
8.8 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
import SignalClient
@objc(OWSSubscriptionReceiptCredentialJobQueue)
public class SubscriptionReceiptCredentialJobQueue: NSObject, JobQueue {
// Add optional paymentIntentID / isBoost
public func add(
isBoost: Bool,
receiptCredentialRequestContext: Data,
receiptCredentailRequest: Data,
subscriberID: Data,
targetSubscriptionLevel: UInt,
priorSubscriptionLevel: UInt,
boostPaymentIntentID: String,
transaction: SDSAnyWriteTransaction
) {
let jobRecord = OWSReceiptCredentialRedemptionJobRecord(
receiptCredentialRequestContext: receiptCredentialRequestContext,
receiptCredentailRequest: receiptCredentailRequest,
subscriberID: subscriberID,
targetSubscriptionLevel: targetSubscriptionLevel,
priorSubscriptionLevel: priorSubscriptionLevel,
isBoost: isBoost,
boostPaymentIntentID: boostPaymentIntentID,
label: self.jobRecordLabel)
self.add(jobRecord: jobRecord, transaction: transaction)
}
public typealias DurableOperationType = SubscriptionReceiptCredentailRedemptionOperation
@objc
public static let jobRecordLabel: String = "SubscriptionReceiptCredentailRedemption"
public var jobRecordLabel: String {
return type(of: self).jobRecordLabel
}
// per OWSOperation.retryIntervalForExponentialBackoff(failureCount:),
// 110 retries will yield ~24 hours of retry.
public static let maxRetries: UInt = 110
public let requiresInternet: Bool = true
public var isEnabled: Bool { CurrentAppContext().isMainApp }
public var runningOperations = AtomicArray<SubscriptionReceiptCredentailRedemptionOperation>()
@objc
public override init() {
super.init()
AppReadiness.runNowOrWhenAppDidBecomeReadySync {
self.setup()
}
}
@objc
public func setup() {
defaultSetup()
}
public var isSetup = AtomicBool(false)
public func didMarkAsReady(oldJobRecord: JobRecordType, transaction: SDSAnyWriteTransaction) {
// no special handling
}
let operationQueue: OperationQueue = {
let operationQueue = OperationQueue()
operationQueue.name = "SubscriptionReceiptCredential.OperationQueue"
return operationQueue
}()
public func operationQueue(jobRecord: OWSReceiptCredentialRedemptionJobRecord) -> OperationQueue {
return self.operationQueue
}
public func buildOperation(jobRecord: OWSReceiptCredentialRedemptionJobRecord, transaction: SDSAnyReadTransaction) throws -> SubscriptionReceiptCredentailRedemptionOperation {
return try SubscriptionReceiptCredentailRedemptionOperation(jobRecord)
}
}
public class SubscriptionReceiptCredentailRedemptionOperation: OWSOperation, DurableOperation {
// MARK: DurableOperation
public let jobRecord: OWSReceiptCredentialRedemptionJobRecord
weak public var durableOperationDelegate: SubscriptionReceiptCredentialJobQueue?
public var operation: OWSOperation {
return self
}
let isBoost: Bool
let subscriberID: Data
var receiptCredentialRequestContext: ReceiptCredentialRequestContext
var receiptCredentialRequest: ReceiptCredentialRequest
var receiptCredentialPresentation: ReceiptCredentialPresentation?
let targetSubscriptionLevel: UInt
let priorSubscriptionLevel: UInt
let boostPaymentIntentID: String
@objc public required init(_ jobRecord: OWSReceiptCredentialRedemptionJobRecord) throws {
self.jobRecord = jobRecord
self.isBoost = jobRecord.isBoost
self.subscriberID = jobRecord.subscriberID
self.targetSubscriptionLevel = jobRecord.targetSubscriptionLevel
self.priorSubscriptionLevel = jobRecord.priorSubscriptionLevel
self.boostPaymentIntentID = jobRecord.boostPaymentIntentID
self.receiptCredentialRequestContext = try ReceiptCredentialRequestContext(
contents: [UInt8](jobRecord.receiptCredentailRequestContext))
self.receiptCredentialRequest = try ReceiptCredentialRequest(
contents: [UInt8](jobRecord.receiptCredentailRequest))
if let receiptCredentialPresentation = jobRecord.receiptCredentialPresentation {
self.receiptCredentialPresentation = try ReceiptCredentialPresentation(
contents: [UInt8](receiptCredentialPresentation))
}
}
override public func run() {
assert(self.durableOperationDelegate != nil)
firstly(on: .global()) { () -> Promise<ReceiptCredentialPresentation> in
// We already have a receiptCredentialPresentation, lets use it
if let receiptCredentialPresentation = self.receiptCredentialPresentation {
Logger.info("[Subscriptions] Using persisted receipt credential presentation")
return Promise.value(receiptCredentialPresentation)
}
Logger.info("[Subscriptions] Creating new receipt credential presentation")
// Create a new receipt credential presentation
return firstly(on: .global()) { () -> Promise<ReceiptCredentialPresentation> in
if self.isBoost {
Logger.info("[Subscriptions] Durable job requesting receipt for boost")
return try SubscriptionManager.requestBoostReceiptCredentialPresentation(
for: self.boostPaymentIntentID,
context: self.receiptCredentialRequestContext,
request: self.receiptCredentialRequest
)
} else {
Logger.info("[Subscriptions] Durable job requesting receipt for subscription")
return try SubscriptionManager.requestReceiptCredentialPresentation(
for: self.subscriberID,
context: self.receiptCredentialRequestContext,
request: self.receiptCredentialRequest,
targetSubscriptionLevel: self.targetSubscriptionLevel,
priorSubscriptionLevel: self.priorSubscriptionLevel
)
}
}.then(on: .global()) { newReceiptCredentialPresentation in
// Store the receipt credential presentation, in case the job fails.
self.databaseStorage.writePromise { transaction in
self.jobRecord.update(
withReceiptCredentialPresentation: newReceiptCredentialPresentation.serialize().asData,
transaction: transaction
)
}.map { _ in newReceiptCredentialPresentation }
}
}.then(on: .global()) { newReceiptCredentialPresentation in
return try SubscriptionManager.redeemReceiptCredentialPresentation(
receiptCredentialPresentation: newReceiptCredentialPresentation
)
}.done(on: .global()) {
Logger.info("[Subscriptions] Successfully redeemed receipt credential presentation")
self.didSucceed()
}.catch(on: .global()) { error in
self.reportError(error)
}
}
override public func didSucceed() {
self.databaseStorage.write { transaction in
if !self.isBoost {
SubscriptionManager.setLastReceiptRedemptionFailed(failureReason: .none, transaction: transaction)
}
self.durableOperationDelegate?.durableOperationDidSucceed(self, transaction: transaction)
NotificationCenter.default.postNotificationNameAsync(
SubscriptionManager.SubscriptionJobQueueDidFinishJobNotification,
object: nil
)
}
}
override public func didReportError(_ error: Error) {
Logger.debug("remainingRetries: \(self.remainingRetries)")
self.databaseStorage.write { transaction in
self.durableOperationDelegate?.durableOperation(self, didReportError: error, transaction: transaction)
}
}
override public func retryInterval() -> TimeInterval {
let timeInterval = OWSOperation.retryIntervalForExponentialBackoff(failureCount: jobRecord.failureCount)
return timeInterval
}
override public func didFail(error: Error) {
Logger.error("failed to redeem receipt credential with error: \(error.userErrorDescription)")
self.databaseStorage.write { transaction in
NotificationCenter.default.postNotificationNameAsync(
SubscriptionManager.SubscriptionJobQueueDidFailJobNotification,
object: nil
)
self.durableOperationDelegate?.durableOperation(self, didFailWithError: error, transaction: transaction)
}
}
}