mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-11 22:55:55 +05:45
This fixes the last edge-case for offline entitlements. Solves the
following scenario:
```swift
func testPurchaseWhileServerIsDownPostsReceiptWhenForegroundingApp() async throws {
let logger = TestLogHandler()
// 1. Purchase while server is down
self.serverDown()
try await self.purchaseMonthlyProduct()
logger.verifyMessageWasNotLogged("Finishing transaction")
// 2. Server is back
self.serverUp()
// 3. Request current CustomerInfo
let info1 = try await Purchases.shared.customerInfo()
try await self.verifyEntitlementWentThrough(info1)
// 4. Ensure transaction is finished
logger.verifyMessageWasLogged("Finishing transaction", level: .info)
// 5. Restart app
Purchases.shared.invalidateCustomerInfoCache()
await self.resetSingleton()
// 6. To ensure (with a clean cache) that the receipt was posted
let info2 = try await Purchases.shared.customerInfo()
try await self.verifyEntitlementWentThrough(info2)
}
```
This race condition happened when fetching `CustomerInfo` after we had
computed an offline `CustomerInfo` with a purchase: if the server is
still not aware of the purchase, then we lose that until some arbitrary
amount of time when `StoreKit` might decide to send the pending
transactions in the queue.
For that reason, instead of waiting, we proactively check now whenever
`CustomerInfoManager` fetches new info.
The refactors #2540 and #2542 where required to break a retain cycle.
Also thanks to #2534 we know for sure that there aren't any cycles.
Note that this is an iOS 15.0+ only feature. It would be useful to
better ensure consistency in older versions, but it's not strictly
required, as those versions don't support offline entitlements either.
74 lines
2.1 KiB
Swift
74 lines
2.1 KiB
Swift
//
|
|
// 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
|
|
//
|
|
// StoreKit2TransactionFetcher.swift
|
|
//
|
|
// Created by Nacho Soto on 5/24/23.
|
|
|
|
import Foundation
|
|
import StoreKit
|
|
|
|
protocol StoreKit2TransactionFetcherType: Sendable {
|
|
|
|
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
|
|
var unfinishedVerifiedTransactions: [StoreTransaction] { get async }
|
|
|
|
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
|
|
var hasPendingConsumablePurchase: Bool { get async }
|
|
|
|
}
|
|
|
|
final class StoreKit2TransactionFetcher: StoreKit2TransactionFetcherType {
|
|
|
|
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
|
|
var unfinishedVerifiedTransactions: [StoreTransaction] {
|
|
get async {
|
|
return await StoreKit.Transaction
|
|
.unfinished
|
|
.compactMap { $0.verifiedTransaction }
|
|
.map { StoreTransaction(sk2Transaction: $0) }
|
|
.extractValues()
|
|
}
|
|
}
|
|
|
|
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
|
|
var hasPendingConsumablePurchase: Bool {
|
|
get async {
|
|
return await StoreKit.Transaction
|
|
.unfinished
|
|
.compactMap { $0.verifiedTransaction }
|
|
.map(\.productType)
|
|
.map { StoreProduct.ProductType($0) }
|
|
.contains { $0.productCategory == .nonSubscription }
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
|
|
extension StoreKit.VerificationResult where SignedType == StoreKit.Transaction {
|
|
|
|
var underlyingTransaction: StoreKit.Transaction {
|
|
switch self {
|
|
case let .unverified(transaction, _): return transaction
|
|
case let .verified(transaction): return transaction
|
|
}
|
|
}
|
|
|
|
var verifiedTransaction: StoreKit.Transaction? {
|
|
switch self {
|
|
case let .verified(transaction): return transaction
|
|
case .unverified: return nil
|
|
}
|
|
}
|
|
|
|
}
|