Skip to content

Commit

Permalink
Always listen to updates
Browse files Browse the repository at this point in the history
  • Loading branch information
NachoSoto committed Jun 13, 2023
1 parent 89ef553 commit 77d1f77
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 46 deletions.
6 changes: 3 additions & 3 deletions Sources/Logging/Strings/PurchaseStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ enum PurchaseStrings {
case begin_refund_multiple_active_entitlements
case begin_refund_customer_info_error(entitlementID: String?)
case missing_cached_customer_info
case sk2_transactions_update_received_transaction(StoreTransaction)
case sk2_transactions_update_received_transaction(productID: String)
case sk1_purchase_too_slow
case sk2_purchase_too_slow
case payment_queue_wrapper_delegate_call_sk1_enabled
Expand Down Expand Up @@ -282,8 +282,8 @@ extension PurchaseStrings: CustomStringConvertible {
case .missing_cached_customer_info:
return "Requested a cached CustomerInfo but it's not available."

case let .sk2_transactions_update_received_transaction(transaction):
return "StoreKit.Transaction.updates: received transaction for product '\(transaction.productIdentifier)'"
case let .sk2_transactions_update_received_transaction(productID):
return "StoreKit.Transaction.updates: received transaction for product '\(productID)'"

case .sk1_purchase_too_slow:
return "StoreKit 1 purchase took longer than expected"
Expand Down
5 changes: 5 additions & 0 deletions Sources/Logging/Strings/StoreKitStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ enum StoreKitStrings {

case sk2_product_request_too_slow

case sk2_observing_transaction_updates

#if DEBUG

case sk1_wrapper_notifying_delegate_of_existing_transactions(count: Int)
Expand Down Expand Up @@ -140,6 +142,9 @@ extension StoreKitStrings: CustomStringConvertible {
case .sk2_product_request_too_slow:
return "StoreKit 2 product request took longer than expected"

case .sk2_observing_transaction_updates:
return "Observing StoreKit.Transaction.updates"

#if DEBUG
case let .sk1_wrapper_notifying_delegate_of_existing_transactions(count):
return "StoreKit1Wrapper: sending delegate \(count) existing transactions " +
Expand Down
3 changes: 2 additions & 1 deletion Sources/Purchasing/Purchases/PurchasesOrchestrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,9 @@ final class PurchasesOrchestrator {
storeKit2TransactionListener.delegate = self
storeKit2StorefrontListener.delegate = self

storeKit2TransactionListener.listenForTransactions()

if systemInfo.storeKit2Setting == .enabledForCompatibleDevices {
storeKit2TransactionListener.listenForTransactions()
storeKit2StorefrontListener.listenForStorefrontChanges()
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Purchasing/StoreKit1/PaymentQueueWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ extension PaymentQueueWrapper: SKPaymentQueueDelegate {
extension PaymentQueueWrapper: SKPaymentTransactionObserver {

func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
// Ignored. Either `StoreKit1Wrapper` will handle this, or `StoreKit2TransactionListener` if `SK2` is enabled.
// Ignored. Either `StoreKit1Wrapper` or `StoreKit2TransactionListener` will handle this.
}

#if !os(watchOS)
Expand Down
14 changes: 10 additions & 4 deletions Sources/Purchasing/StoreKit2/StoreKit2TransactionListener.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,26 @@ protocol StoreKit2TransactionListenerDelegate: AnyObject {

}

/// Observes `StoreKit.Transaction.updates`, which receives:
/// - Updates from outside `Product.purchase()`, like renewals
/// - Purchases from SwiftUI's paywalls.
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
class StoreKit2TransactionListener {

/// Similar to ``PurchaseResultData`` but with an optional `CustomerInfo`
typealias ResultData = (userCancelled: Bool, transaction: SK2Transaction?)

private(set) var taskHandle: Task<Void, Never>?

weak var delegate: StoreKit2TransactionListenerDelegate?

init(delegate: StoreKit2TransactionListenerDelegate?) {
self.delegate = delegate
}

func listenForTransactions() {
Logger.debug(Strings.storeKit.sk2_observing_transaction_updates)

self.taskHandle?.cancel()
self.taskHandle = Task(priority: .utility) { [weak self] in
for await result in StoreKit.Transaction.updates {
Expand Down Expand Up @@ -103,13 +109,13 @@ private extension StoreKit2TransactionListener {

case let .verified(verifiedTransaction):
if fromTransactionUpdate, let delegate = self.delegate {
let transaction = StoreTransaction(sk2Transaction: verifiedTransaction)

Logger.debug(Strings.purchase.sk2_transactions_update_received_transaction(transaction))
Logger.debug(Strings.purchase.sk2_transactions_update_received_transaction(
productID: verifiedTransaction.productID
))

try await delegate.storeKit2TransactionListener(
self,
updatedTransaction: transaction
updatedTransaction: StoreTransaction(sk2Transaction: verifiedTransaction)
)
}

Expand Down
8 changes: 4 additions & 4 deletions Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -967,7 +967,7 @@ class PurchasesOrchestratorTests: StoreKitConfigTestCase {
}

@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
func testDoesNotListenForSK2TransactionsWithSK2Disabled() throws {
func testListensForSK2TransactionsWithSK2Disabled() throws {
try AvailabilityChecks.iOS15APIAvailableOrSkipTest()

let transactionListener = MockStoreKit2TransactionListener()
Expand All @@ -977,11 +977,11 @@ class PurchasesOrchestratorTests: StoreKitConfigTestCase {
self.setUpOrchestrator(storeKit2TransactionListener: transactionListener,
storeKit2StorefrontListener: StoreKit2StorefrontListener(delegate: nil))

expect(transactionListener.invokedListenForTransactions) == false
expect(transactionListener.invokedListenForTransactions) == true
}

@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
func testDoesNotListenForSK2TransactionsWithSK2EnabledOnlyForOptimizations() throws {
func testListensForSK2TransactionsWithSK2EnabledOnlyForOptimizations() throws {
try AvailabilityChecks.iOS15APIAvailableOrSkipTest()

let transactionListener = MockStoreKit2TransactionListener()
Expand All @@ -991,7 +991,7 @@ class PurchasesOrchestratorTests: StoreKitConfigTestCase {
self.setUpOrchestrator(storeKit2TransactionListener: transactionListener,
storeKit2StorefrontListener: StoreKit2StorefrontListener(delegate: nil))

expect(transactionListener.invokedListenForTransactions) == false
expect(transactionListener.invokedListenForTransactions) == true
}

@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,32 @@ import StoreKit
import StoreKitTest
import XCTest

@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
// swiftlint:disable type_name

@MainActor
class StoreKit2TransactionListenerTests: StoreKitConfigTestCase {
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
class StoreKit2TransactionListenerBaseTests: StoreKitConfigTestCase {

private var listener: StoreKit2TransactionListener! = nil
private var delegate: MockStoreKit2TransactionListenerDelegate! = nil
fileprivate var listener: StoreKit2TransactionListener! = nil
fileprivate var delegate: MockStoreKit2TransactionListenerDelegate! = nil

override func setUpWithError() throws {
try super.setUpWithError()
override func setUp() async throws {
try await super.setUp()

try AvailabilityChecks.iOS15APIAvailableOrSkipTest()

// Unfinished transactions before beginning the test might lead to false positives / negatives
await self.verifyNoUnfinishedTransactions()

self.delegate = .init()
self.listener = .init(delegate: self.delegate)
}

}

@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
class StoreKit2TransactionListenerTests: StoreKit2TransactionListenerBaseTests {

func testStopsListeningToTransactions() throws {
try AvailabilityChecks.iOS15APIAvailableOrSkipTest()

Expand Down Expand Up @@ -113,30 +123,6 @@ class StoreKit2TransactionListenerTests: StoreKitConfigTestCase {
try await self.verifyUnfinishedTransaction(withId: purchasedTransaction.id)
}

func testPurchasingInTheAppDoesNotNotifyDelegate() async throws {
self.listener.listenForTransactions()

_ = try await self.fetchSk2Product().purchase()

try await self.verifyTransactionsWereNotUpdated()
}

func testPurchasingOutsideTheAppNotifiesDelegate() throws {
self.listener.listenForTransactions()

try self.testSession.buyProduct(productIdentifier: Self.productID)

expect(self.delegate.invokedTransactionUpdated).toEventually(beTrue())
}

func testNotifiesDelegateForExistingTransactions() throws {
try self.testSession.buyProduct(productIdentifier: Self.productID)

self.listener.listenForTransactions()

expect(self.delegate.invokedTransactionUpdated).toEventually(beTrue())
}

func testHandlePurchaseResultDoesNotFinishTransaction() async throws {
let (purchaseResult, _, purchasedTransaction) = try await self.purchase()

Expand Down Expand Up @@ -189,8 +175,60 @@ class StoreKit2TransactionListenerTests: StoreKitConfigTestCase {

}

// MARK: - Transaction.updates tests

@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
private extension StoreKit2TransactionListenerTests {
class StoreKit2TransactionListenerTransactionUpdatesTests: StoreKit2TransactionListenerBaseTests {

func testPurchasingInTheAppDoesNotNotifyDelegate() async throws {
self.listener.listenForTransactions()

try await self.simulateAnyPurchase(finishTransaction: true)
try await self.verifyTransactionsWereNotUpdated()
}

func testPurchasingOutsideTheAppNotifiesDelegate() throws {
self.listener.listenForTransactions()

try self.testSession.buyProduct(productIdentifier: Self.productID)

expect(self.delegate.invokedTransactionUpdated).toEventually(beTrue())
}

func testNotifiesDelegateForExistingTransactions() throws {
try self.testSession.buyProduct(productIdentifier: Self.productID)

self.listener.listenForTransactions()

expect(self.delegate.invokedTransactionUpdated).toEventually(beTrue())
}

@available(iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4, *)
func testNotifiesDelegateForRenewals() async throws {
let logger = TestLogHandler()

try await self.simulateAnyPurchase(finishTransaction: true)

self.listener.listenForTransactions()

try? self.testSession.forceRenewalOfSubscription(productIdentifier: Self.productID)

try await self.waitForTransactionUpdated()

expect(self.delegate.updatedTransactions)
.to(containElementSatisfying { transaction in
transaction.productIdentifier == Self.productID
})

logger.verifyMessageWasLogged(Strings.purchase.sk2_transactions_update_received_transaction(
productID: Self.productID
))
}

}

@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
private extension StoreKit2TransactionListenerBaseTests {

private enum Error: Swift.Error {
case invalidResult(Product.PurchaseResult)
Expand Down Expand Up @@ -220,4 +258,18 @@ private extension StoreKit2TransactionListenerTests {
expect(self.delegate.invokedTransactionUpdated) == false
}

func waitForTransactionUpdated(
file: FileString = #fileID,
line: UInt = #line
) async throws {
try await asyncWait(
until: { await self.delegate.invokedTransactionUpdated == true },
timeout: .seconds(4),
pollInterval: .milliseconds(100),
description: "Transaction update",
file: file,
line: line
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ final class MockStoreKit2TransactionListenerDelegate: StoreKit2TransactionListen
var invokedTransactionUpdated: Bool { return self._invokedTransactionUpdated.value }
var updatedTransactions: [StoreTransactionType] { return self._updatedTransactions.value }

private var _invokedTransactionUpdated: Atomic<Bool> = false
private var _updatedTransactions: Atomic<[StoreTransactionType]> = .init([])
private let _invokedTransactionUpdated: Atomic<Bool> = false
private let _updatedTransactions: Atomic<[StoreTransactionType]> = .init([])

func storeKit2TransactionListener(
_ listener: StoreKit2TransactionListener,
Expand All @@ -31,3 +31,6 @@ final class MockStoreKit2TransactionListenerDelegate: StoreKit2TransactionListen
}

}

@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
extension MockStoreKit2TransactionListenerDelegate: Sendable {}

0 comments on commit 77d1f77

Please sign in to comment.