Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use SK2 to get Storefront to prevent potential hang #4047

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,6 @@
4F15B4A12A6774C9005BEFE8 /* CustomerInfo+NonSubscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F15B4A02A6774C9005BEFE8 /* CustomerInfo+NonSubscriptions.swift */; };
4F15B4A22A678A9C005BEFE8 /* MockStoreTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */; };
4F15B4A32A678B81005BEFE8 /* MockStoreTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */; };
4F174F472B07EA7E00FE538E /* StorefrontProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F174F462B07EA7E00FE538E /* StorefrontProvider.swift */; };
4F1E84012A6062C1000AF177 /* ImageSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCEEA622A37A2E9002C2112 /* ImageSnapshot.swift */; };
4F1E84022A6062C9000AF177 /* ImageSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCEEA622A37A2E9002C2112 /* ImageSnapshot.swift */; };
4F2017D52A15587F0061F6EF /* OfflineStoreKitIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F2017D42A15587F0061F6EF /* OfflineStoreKitIntegrationTests.swift */; };
Expand Down Expand Up @@ -1538,7 +1537,6 @@
4F0CE2BC2A215CE600561895 /* TransactionPosterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionPosterTests.swift; sourceTree = "<group>"; };
4F1428A32A4A132C006CD196 /* TestStoreProductDiscount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestStoreProductDiscount.swift; sourceTree = "<group>"; };
4F15B4A02A6774C9005BEFE8 /* CustomerInfo+NonSubscriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomerInfo+NonSubscriptions.swift"; sourceTree = "<group>"; };
4F174F462B07EA7E00FE538E /* StorefrontProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorefrontProvider.swift; sourceTree = "<group>"; };
4F2017D42A15587F0061F6EF /* OfflineStoreKitIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineStoreKitIntegrationTests.swift; sourceTree = "<group>"; };
4F2F2EFE2A3CDAA800652B24 /* FileHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileHandler.swift; sourceTree = "<group>"; };
4F2F2F132A3CEAB500652B24 /* FileHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileHandlerTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2398,7 +2396,6 @@
B372EC53268FEDC60099171E /* StoreProductDiscount.swift */,
2D1015D9275959840086173F /* StoreTransaction.swift */,
2D1015DD275A57FC0086173F /* SubscriptionPeriod.swift */,
4F174F462B07EA7E00FE538E /* StorefrontProvider.swift */,
);
path = StoreKitAbstractions;
sourceTree = "<group>";
Expand Down Expand Up @@ -5455,7 +5452,6 @@
5796A3A927D7C43500653165 /* Deprecations.swift in Sources */,
4F0201C42A13C85500091612 /* Assertions.swift in Sources */,
5753ED8E294A662400CBAB54 /* DateFormatter+Extensions.swift in Sources */,
4F174F472B07EA7E00FE538E /* StorefrontProvider.swift in Sources */,
B3AA6238268B926F00894871 /* SystemInfo.swift in Sources */,
353FDAEC2CA1866A0055F328 /* StoreKitErrorHelper.swift in Sources */,
5746508E275949F20053AB09 /* DispatchTimeInterval+Extensions.swift in Sources */,
Expand Down
15 changes: 15 additions & 0 deletions Sources/Caching/DeviceCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,19 @@ class DeviceCache {
}

// MARK: - StoreKit 2
func cachedStorefront() -> StorefrontType? {
return self.userDefaults.read {
let cachedStorefront: CodableStorefront? = $0.value<CodableStorefront>(forKey: CacheKey.storefront)
return cachedStorefront
}
}

func cache(storefront: CodableStorefront) {
self.userDefaults.write { userDefaults in
userDefaults.set(codable: storefront, forKey: CacheKey.storefront)
}
}

private let cachedSyncedSK2ObserverModeTransactionIDsLock = Lock(.nonRecursive)

func registerNewSyncedSK2ObserverModeTransactionIDs(_ ids: [UInt64]) {
Expand Down Expand Up @@ -391,6 +404,7 @@ class DeviceCache {
case legacySubscriberAttributes(String)
case attributionDataDefaults(String)
case syncedSK2ObserverModeTransactionIDs
case storefront

var rawValue: String {
switch self {
Expand All @@ -401,6 +415,7 @@ class DeviceCache {
case let .attributionDataDefaults(userID): return "\(Self.base)attribution.\(userID)"
case .syncedSK2ObserverModeTransactionIDs:
return "\(Self.base)syncedSK2ObserverModeTransactionIDs"
case .storefront: return "\(Self.base)storefront"
}
}

Expand Down
30 changes: 25 additions & 5 deletions Sources/Misc/SystemInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ class SystemInfo {
var observerMode: Bool { return !self.finishTransactions }

private let sandboxEnvironmentDetector: SandboxEnvironmentDetector
private let storefrontProvider: StorefrontProviderType
private let _finishTransactions: Atomic<Bool>
private let _bundle: Atomic<Bundle>

Expand All @@ -74,8 +73,9 @@ class SystemInfo {
#endif
}

private var _storefront: StorefrontType?
var storefront: StorefrontType? {
return self.storefrontProvider.currentStorefront
return self._storefront
}

var preferredLanguages: [String] {
Expand Down Expand Up @@ -153,12 +153,12 @@ class SystemInfo {
operationDispatcher: OperationDispatcher = .default,
bundle: Bundle = .main,
sandboxEnvironmentDetector: SandboxEnvironmentDetector = BundleSandboxEnvironmentDetector.default,
storefrontProvider: StorefrontProviderType = DefaultStorefrontProvider(),
storeKitVersion: StoreKitVersion = .default,
responseVerificationMode: Signing.ResponseVerificationMode = .default,
dangerousSettings: DangerousSettings? = nil,
clock: ClockType = Clock.default,
preferredLocalesProvider: PreferredLocalesProviderType = PreferredLocalesProvider.default) {
preferredLocalesProvider: PreferredLocalesProviderType = PreferredLocalesProvider.default,
deviceCache: DeviceCache) {
self.platformFlavor = platformInfo?.flavor ?? "native"
self.platformFlavorVersion = platformInfo?.version
self._bundle = .init(bundle)
Expand All @@ -167,11 +167,31 @@ class SystemInfo {
self.operationDispatcher = operationDispatcher
self.storeKitVersion = storeKitVersion
self.sandboxEnvironmentDetector = sandboxEnvironmentDetector
self.storefrontProvider = storefrontProvider
self.responseVerificationMode = responseVerificationMode
self.dangerousSettings = dangerousSettings ?? DangerousSettings()
self.clock = clock
self.preferredLocalesProvider = preferredLocalesProvider

self.setStorefront(deviceCache: deviceCache)
}

func setStorefront(deviceCache: DeviceCache) {

if let cachedStorefront = deviceCache.cachedStorefront() {
self._storefront = cachedStorefront
}

Task { [weak self] in
let storefront = await Storefront.currentStorefront


if let storefront {
self?._storefront = storefront
deviceCache.cache(
storefront: CodableStorefront(storefront: storefront)
)
}
}
}

var supportsOfflineEntitlements: Bool {
Expand Down
9 changes: 6 additions & 3 deletions Sources/Purchasing/Purchases/Purchases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -291,19 +291,22 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
let receiptRefreshRequestFactory = ReceiptRefreshRequestFactory()
let fetcher = StoreKitRequestFetcher(requestFactory: receiptRefreshRequestFactory,
operationDispatcher: operationDispatcher)
let userDefaults = userDefaults ?? UserDefaults.computeDefault()
let deviceCache = DeviceCache(sandboxEnvironmentDetector: BundleSandboxEnvironmentDetector.default,
userDefaults: userDefaults)

let systemInfo = SystemInfo(platformInfo: platformInfo,
finishTransactions: !observerMode,
operationDispatcher: operationDispatcher,
storeKitVersion: storeKitVersion,
responseVerificationMode: responseVerificationMode,
dangerousSettings: dangerousSettings)
dangerousSettings: dangerousSettings,
deviceCache: deviceCache)

let receiptFetcher = ReceiptFetcher(requestFetcher: fetcher, systemInfo: systemInfo)
let eTagManager = ETagManager()
let attributionTypeFactory = AttributionTypeFactory()
let attributionFetcher = AttributionFetcher(attributionFactory: attributionTypeFactory, systemInfo: systemInfo)
let userDefaults = userDefaults ?? UserDefaults.computeDefault()
let deviceCache = DeviceCache(sandboxEnvironmentDetector: systemInfo, userDefaults: userDefaults)

let purchasedProductsFetcher = OfflineCustomerInfoCreator.createPurchasedProductsFetcherIfAvailable()
let transactionFetcher = StoreKit2TransactionFetcher()
Expand Down
2 changes: 1 addition & 1 deletion Sources/Purchasing/Purchases/PurchasesOrchestrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1371,7 +1371,7 @@ private extension PurchasesOrchestrator {
) {
self.productsManager.products(withIdentifiers: [productIdentifier]) { products in
let result = products.value?.first.map {
ProductRequestData(with: $0, storefront: self.paymentQueueWrapper.currentStorefront)
ProductRequestData(with: $0, storefront: self.systemInfo.storefront)
}

completion(result)
Expand Down
10 changes: 0 additions & 10 deletions Sources/Purchasing/StoreKit1/PaymentQueueWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ protocol PaymentQueueWrapperType: AnyObject {
@available(macCatalyst, unavailable)
func presentCodeRedemptionSheet()

var currentStorefront: Storefront? { get }

}

/// The choice between SK1's `StoreKit1Wrapper` or `PaymentQueueWrapper` when SK2 is enabled.
Expand Down Expand Up @@ -101,12 +99,6 @@ class PaymentQueueWrapper: NSObject, PaymentQueueWrapperType {
}
#endif

var currentStorefront: Storefront? {
return self.paymentQueue.storefront
.map(SK1Storefront.init)
.map(Storefront.from(storefront:))
}

}

extension PaymentQueueWrapper: SKPaymentQueueDelegate {
Expand Down Expand Up @@ -154,8 +146,6 @@ extension EitherPaymentQueueWrapper {
}
}

var currentStorefront: StorefrontType? { self.paymentQueueWrapperType.currentStorefront }

var sk1Wrapper: StoreKit1Wrapper? { return self.left }
var sk2Wrapper: PaymentQueueWrapper? { return self.right }

Expand Down
44 changes: 44 additions & 0 deletions Sources/Purchasing/StoreKitAbstractions/Storefront.swift
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,47 @@ extension Storefront {
}

}

// MARK: - CodableStorefront

/// A structure representing a codable version of an App Store Storefront, for use with caching the storefront
/// to the disk.
///
/// This structure includes both a country code, represented as a three-letter ISO 3166-1 Alpha-3 country code,
/// and a storefront identifier, which uniquely identifies an App Store storefront.
internal struct CodableStorefront: StorefrontType, Codable {

/// The three-letter code representing the country or region associated with the App Store storefront.
///
/// This property uses the ISO 3166-1 Alpha-3 country code representation, which corresponds to a
/// specific country or region.
var countryCode: String

/// A unique identifier for the App Store storefront.
///
/// This identifier is assigned by Apple and is used to uniquely identify an App Store storefront.
var identifier: String

/// Creates a new instance of `CodableStorefront` with the provided country code and storefront identifier.
///
/// - Parameters:
/// - countryCode: A three-letter ISO 3166-1 Alpha-3 code representing the country or region.
/// - identifier: A unique string identifier for the App Store storefront.
init(countryCode: String, identifier: String) {
self.countryCode = countryCode
self.identifier = identifier
}

init(storefront: StorefrontType) {
self.init(countryCode: storefront.countryCode, identifier: storefront.identifier)
}

init(sk1Storefront: SKStorefront) {
self.init(countryCode: sk1Storefront.countryCode, identifier: sk1Storefront.identifier)
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
init(sk2Storefront: StoreKit.Storefront) {
self.init(countryCode: sk2Storefront.countryCode, identifier: sk2Storefront.id)
}
}
35 changes: 0 additions & 35 deletions Sources/Purchasing/StoreKitAbstractions/StorefrontProvider.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class LocalReceiptParserStoreKitTests: StoreKitConfigTestCase {
private var systemInfo: SystemInfo!
private var receiptFetcher: ReceiptFetcher!
private var parser: PurchasesReceiptParser!
private var deviceCache = MockDeviceCache()

override func setUpWithError() throws {
try super.setUpWithError()
Expand All @@ -35,7 +36,8 @@ class LocalReceiptParserStoreKitTests: StoreKitConfigTestCase {
self.systemInfo = SystemInfo(platformInfo: Purchases.platformInfo,
finishTransactions: true,
operationDispatcher: operationDispatcher,
storeKitVersion: .storeKit1)
storeKitVersion: .storeKit1,
deviceCache: deviceCache)
self.receiptFetcher = ReceiptFetcher(requestFetcher: self.requestFetcher, systemInfo: systemInfo)
self.parser = .default
}
Expand Down
3 changes: 2 additions & 1 deletion Tests/StoreKitUnitTests/PurchasesOrchestratorSK1Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,8 @@ class PurchasesOrchestratorSK1Tests: BasePurchasesOrchestratorTests, PurchasesOr
finishTransactions: false,
storeKitVersion: .storeKit1,
dangerousSettings: .init(autoSyncPurchases: true,
internalSettings: DangerousSettings.Internal(enableReceiptFetchRetry: true))
internalSettings: DangerousSettings.Internal(enableReceiptFetchRetry: true)),
deviceCache: self.deviceCache
)
self.setUpStoreKit1Wrapper()
self.setUpOrchestrator()
Expand Down
13 changes: 10 additions & 3 deletions Tests/StoreKitUnitTests/StorefrontTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,17 @@ class StorefrontTests: StoreKitConfigTestCase {
let expected = "ESP"
try await self.changeStorefront(expected)

let systemInfo = SystemInfo(platformInfo: nil, finishTransactions: false)
let storefront = try XCTUnwrap(systemInfo.storefront)
let deviceCache = MockDeviceCache()
let systemInfo = SystemInfo(platformInfo: nil, finishTransactions: false, deviceCache: deviceCache)

try await asyncWait(
description: "Storefront change not detected",
timeout: .seconds(1),
pollInterval: .milliseconds(100)
) {
return systemInfo.storefront?.countryCode == expected
}

expect(storefront.countryCode) == expected
}

}
11 changes: 11 additions & 0 deletions Tests/UnitTests/Caching/DeviceCacheTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,17 @@ class DeviceCacheTests: TestCase {
expect(self.deviceCache.isProductEntitlementMappingCacheStale) == true
}

func testSK2StorefrontIsProperlyCached() throws {
let expectedStorefront = CodableStorefront(countryCode: "mock_country", identifier: "mock_id")

self.deviceCache.cache(storefront: expectedStorefront)
let cachedStorefront = self.deviceCache.cachedStorefront()

expect(cachedStorefront).toNot(beNil())
expect(cachedStorefront?.countryCode).to(equal(expectedStorefront.countryCode))
expect(cachedStorefront?.identifier).to(equal(expectedStorefront.identifier))
}

}

private extension DeviceCacheTests {
Expand Down
Loading