Files
purchases-ios/Sources/Purchasing/StoreKitAbstractions/StoreTransaction.swift
T
Antonio PallaresandCursor b086906b28 Include attribution data in POST /receipt when using SK2 in Observer mode (#6233)
* Cache presentedOfferingContext when making a purchase from a paywall

* Avoid using stale cached presentedOfferingContext

Co-authored-by: Cursor <cursoragent@cursor.com>

* Only remove cached context when date check passes

* Include productId in cancel events and add tests

* Add log

* One more test

* More tests

* fix warning

* Fix compilation

* Fix integration test compilation for SPI import

Co-authored-by: Cursor <cursoragent@cursor.com>

* Include paywall and offering context in SK2 transaction listener

* Improve comment

* More tests

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 10:24:21 +01:00

212 lines
8.1 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// StoreTransaction.swift
//
// Created by Andrés Boedo on 2/12/21.
import Foundation
import StoreKit
/// TypeAlias to StoreKit 1's Transaction type, called `StoreKit.SKPaymentTransaction`
public typealias SK1Transaction = SKPaymentTransaction
/// TypeAlias to StoreKit 2's Transaction type, called `StoreKit.Transaction`
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
public typealias SK2Transaction = StoreKit.Transaction
/// Abstract class that provides access to properties of a transaction.
/// ``StoreTransaction``s can represent transactions from StoreKit 1, StoreKit 2 or
/// transactions made from other places, like Stripe, Google Play or Amazon Store.
@objc(RCStoreTransaction) public final class StoreTransaction: NSObject, StoreTransactionType {
private let transaction: StoreTransactionType
init(_ transaction: StoreTransactionType) {
self.transaction = transaction
super.init()
}
// Note: docs are inherited through `StoreTransactionType`
// swiftlint:disable missing_docs
@objc public var productIdentifier: String { self.transaction.productIdentifier }
@objc public var purchaseDate: Date { self.transaction.purchaseDate }
@objc public var transactionIdentifier: String { self.transaction.transactionIdentifier }
@objc public var quantity: Int { self.transaction.quantity }
@objc public var storefront: Storefront? { self.transaction.storefront }
@objc internal var jwsRepresentation: String? { self.transaction.jwsRepresentation }
internal var environment: StoreEnvironment? { self.transaction.environment }
internal var reason: TransactionReason? { self.transaction.reason }
var hasKnownPurchaseDate: Bool { return self.transaction.hasKnownPurchaseDate }
var hasKnownTransactionIdentifier: Bool { self.transaction.hasKnownTransactionIdentifier }
func finish(_ wrapper: PaymentQueueWrapperType, completion: @escaping @Sendable () -> Void) {
self.transaction.finish(wrapper, completion: completion)
}
// swiftlint:enable missing_docs
/// Creates an instance from any `StoreTransactionType`.
/// If `transaction` is already a wrapped `StoreTransaction` then this returns it instead.
static func from(transaction: StoreTransactionType) -> StoreTransaction {
return transaction as? StoreTransaction
?? StoreTransaction(transaction)
}
public override func isEqual(_ object: Any?) -> Bool {
self.transactionIdentifier == (object as? StoreTransactionType)?.transactionIdentifier
}
public override var hash: Int {
var hasher = Hasher()
hasher.combine(self.transactionIdentifier)
return hasher.finalize()
}
public override var description: String {
return """
<\(String(describing: StoreTransaction.self)):
identifier="\(self.transactionIdentifier)"
product="\(self.productIdentifier)"
date="\(self.purchaseDate)"
quantity=\(self.quantity)
>
"""
}
}
/// Information that represents the customer’s purchase of a product.
internal protocol StoreTransactionType: Sendable {
/// The product identifier.
var productIdentifier: String { get }
/// The date that App Store charged the user’s account for a purchased or restored product,
/// or for a subscription purchase or renewal after a lapse.
var purchaseDate: Date { get }
/// Whether the underlying transaction has a non-nil purchase date.
/// See `SK1StoreTransaction/purchaseDate``
var hasKnownPurchaseDate: Bool { get }
/// The unique identifier for the transaction.
var transactionIdentifier: String { get }
/// Whether the underlying transaction has a known transaction identifier.
/// See `SKPaymentTransaction.transactionID`.
var hasKnownTransactionIdentifier: Bool { get }
/// The number of consumable products purchased.
/// - Note: multi-quantity purchases aren't currently supported.
var quantity: Int { get }
/// The App Store storefront associated with the transaction.
/// - Note: this is only available for StoreKit 2 transactions starting with iOS 17.
var storefront: Storefront? { get }
/// The raw JWS repesentation of the transaction.
/// - Note: this is only available for StoreKit 2 transactions.
var jwsRepresentation: String? { get }
/// The server environment where the receipt was generated.
/// - Note: this is only available for StoreKit 2 transactions.
var environment: StoreEnvironment? { get }
/// The reason for the transaction, if known.
/// - Note: this is only available for StoreKit 2 transactions starting with iOS 17.
var reason: TransactionReason? { get }
/// Indicates to the App Store that the app delivered the purchased content
/// or enabled the service to finish the transaction.
func finish(_ wrapper: PaymentQueueWrapperType, completion: @escaping @Sendable () -> Void)
}
// MARK: - Wrapper constructors / getters
extension StoreTransaction {
/// Creates a `StoreTransaction` instance for testing purposes.
/// - Parameters:
/// - productIdentifier: The product identifier.
/// - purchaseDate: The date that the user's account was charged for a purchased or restored product.
/// - transactionIdentifier: The unique identifier for the transaction.
/// - quantity: The number of consumable products purchased. Defaults to 1.
/// - storefront: The App Store storefront associated with the transaction. Defaults to `nil`.
public convenience init(
productIdentifier: String,
purchaseDate: Date,
transactionIdentifier: String,
quantity: Int = 1,
storefront: Storefront? = nil
) {
self.init(
TestStoreTransaction(
productIdentifier: productIdentifier,
purchaseDate: purchaseDate,
transactionIdentifier: transactionIdentifier,
quantity: quantity,
storefront: storefront
)
)
}
internal convenience init(sk1Transaction: SK1Transaction) {
self.init(SK1StoreTransaction(sk1Transaction: sk1Transaction))
}
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
internal convenience init(sk2Transaction: SK2Transaction,
jwsRepresentation: String,
environmentOverride: StoreEnvironment? = nil) {
self.init(SK2StoreTransaction(sk2Transaction: sk2Transaction,
jwsRepresentation: jwsRepresentation,
environmentOverride: environmentOverride))
}
/// Returns the `SKPaymentTransaction` if this `StoreTransaction` represents a `SKPaymentTransaction`.
@objc public var sk1Transaction: SK1Transaction? {
return (self.transaction as? SK1StoreTransaction)?.underlyingSK1Transaction
}
/// Returns the `StoreKit.Transaction` if this `StoreTransaction` represents a `StoreKit.Transaction`.
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
public var sk2Transaction: SK2Transaction? {
return (self.transaction as? SK2StoreTransaction)?.underlyingSK2Transaction
}
internal var simulatedStoreTransaction: SimulatedStoreTransaction? {
return self.transaction as? SimulatedStoreTransaction
}
}
extension StoreTransactionType {
/// - Returns: the `Storefront` associated to this transaction, or `Storefront.currentStorefront` if not available.
var storefrontOrCurrent: Storefront? {
get async {
return await self.storefront ??? (await Storefront.currentStorefront)
}
}
}
extension StoreTransaction: Identifiable {
/// The stable identity of the entity associated with this instance.
public var id: String { return self.transactionIdentifier }
}