Skip to content

Commit

Permalink
Xcode 15.3 beta 1: fix compilation errors (#3599)
Browse files Browse the repository at this point in the history
### Changes:
- `InternalAPI` is now `Sendable` to deal with new compilation error
- `PaywallEventStoreType` is now `Sendable`
- Extracted `SK2BeginRefundRequestHelperType` to make
`SK2BeginRefundRequestHelper` `Sendable`
- Updated `MockSK2BeginRefundRequestHelper` to be thread-safe and
`Sendable`
- Fixed compilation error in `DebugContentViews`
- Fixed compilation error in `DebugViewModel`
- Fixed compilation error in `StoreMessagesHelper`
  • Loading branch information
NachoSoto authored Jan 25, 2024
1 parent 7670dfb commit 53fe9cd
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 44 deletions.
4 changes: 4 additions & 0 deletions Sources/Networking/InternalAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,7 @@ extension InternalAPI {
}

}

// @unchecked because:
// - Class is not `final` (it's mocked). This implicitly makes subclasses `Sendable` even if they're not thread-safe.
extension InternalAPI: @unchecked Sendable {}
2 changes: 1 addition & 1 deletion Sources/Paywalls/Events/PaywallEventStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

import Foundation

protocol PaywallEventStoreType {
protocol PaywallEventStoreType: Sendable {

/// Stores `event` into the store.
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
Expand Down
37 changes: 28 additions & 9 deletions Sources/Purchasing/StoreKit2/SK2BeginRefundRequestHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,23 @@ import Foundation
import StoreKit
import UIKit

/// Helper class responsible for calling into StoreKit2 and translating results/errors for consumption by RevenueCat.
@available(iOS 15.0, *)
@available(watchOS, unavailable)
@available(tvOS, unavailable)
class SK2BeginRefundRequestHelper {
protocol SK2BeginRefundRequestHelperType: Sendable {

/// Calls `initiateSK2RefundRequest` and maps the result for consumption by `BeginRefundRequestHelper`
@MainActor
func initiateRefundRequest(transactionID: UInt64, windowScene: UIWindowScene) async throws -> RefundRequestStatus {
let sk2Result = await initiateSK2RefundRequest(transactionID: transactionID, windowScene: windowScene)
return try mapSk2Result(from: sk2Result)
}
func initiateSK2RefundRequest(transactionID: UInt64, windowScene: UIWindowScene) async ->
Result<StoreKit.Transaction.RefundRequestStatus, Error>

func verifyTransaction(productID: String) async throws -> UInt64

}

/// Helper class responsible for calling into StoreKit2 and translating results/errors for consumption by RevenueCat.
@available(iOS 15.0, *)
@available(watchOS, unavailable)
@available(tvOS, unavailable)
final class SK2BeginRefundRequestHelper: SK2BeginRefundRequestHelperType {

/* Checks with StoreKit2 that the given `productID` has an existing verified transaction, and maps the
* result for consumption by `BeginRefundRequestHelper`.
Expand Down Expand Up @@ -76,7 +81,21 @@ class SK2BeginRefundRequestHelper {
@available(iOS 15.0, *)
@available(watchOS, unavailable)
@available(tvOS, unavailable)
private extension SK2BeginRefundRequestHelper {
extension SK2BeginRefundRequestHelperType {

/// Calls `initiateSK2RefundRequest` and maps the result for consumption by `BeginRefundRequestHelper`
@MainActor
func initiateRefundRequest(transactionID: UInt64, windowScene: UIWindowScene) async throws -> RefundRequestStatus {
let sk2Result = await self.initiateSK2RefundRequest(transactionID: transactionID, windowScene: windowScene)
return try self.mapSk2Result(from: sk2Result)
}

}

@available(iOS 15.0, *)
@available(watchOS, unavailable)
@available(tvOS, unavailable)
private extension SK2BeginRefundRequestHelperType {

func getErrorMessage(from sk2Error: Error?) -> String {
let details = sk2Error?.localizedDescription ?? "No extra info"
Expand Down
10 changes: 5 additions & 5 deletions Sources/Support/BeginRefundRequestHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ class BeginRefundRequestHelper {
@available(macOS, unavailable)
@available(watchOS, unavailable)
@available(tvOS, unavailable)
var sk2Helper: SK2BeginRefundRequestHelper {
var sk2Helper: SK2BeginRefundRequestHelperType {
get {
// swiftlint:disable:next force_cast
return self._sk2Helper! as! SK2BeginRefundRequestHelper
return self._sk2Helper! as! SK2BeginRefundRequestHelperType
}

set {
Expand Down Expand Up @@ -71,9 +71,9 @@ class BeginRefundRequestHelper {
func beginRefundRequest(forProduct productID: String) async throws -> RefundRequestStatus {
let windowScene = try self.systemInfo.currentWindowScene

let transactionID = try await sk2Helper.verifyTransaction(productID: productID)
return try await sk2Helper.initiateRefundRequest(transactionID: transactionID,
windowScene: windowScene)
let transactionID = try await self.sk2Helper.verifyTransaction(productID: productID)
return try await self.sk2Helper.initiateRefundRequest(transactionID: transactionID,
windowScene: windowScene)
}

@available(iOS 15.0, *)
Expand Down
3 changes: 2 additions & 1 deletion Sources/Support/DebugUI/DebugContentViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ private struct DebugPackageView: View {

Section("Purchasing") {
Button {
_ = Task<Void, Never> {
_ = Task<Void, Never> { @MainActor in
do {
self.purchasing = true
try await self.purchase()
Expand Down Expand Up @@ -407,6 +407,7 @@ private struct DebugPackageView: View {
}
}

@MainActor
private func purchase() async throws {
_ = try await Purchases.shared.purchase(package: self.package)
}
Expand Down
17 changes: 15 additions & 2 deletions Sources/Support/DebugUI/DebugViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ final class DebugViewModel: ObservableObject {
set { self._navigationPath = newValue }
}

@MainActor
func load() async {
self.configuration = .loaded(.create())

Expand All @@ -68,12 +69,24 @@ final class DebugViewModel: ObservableObject {
self.customerInfo = await .create { try await Purchases.shared.customerInfo() }
self.currentAppUserID = Purchases.shared.appUserID

await self.listenToCustomerInfoChanges()
#endif
}

#if !ENABLE_CUSTOM_ENTITLEMENT_COMPUTATION
// `nonisolated` required to work around Swift 5.10 issue.
// See https://github.com/RevenueCat/purchases-ios/pull/3599
nonisolated private func listenToCustomerInfoChanges() async {
for await info in Purchases.shared.customerInfoStream {
self.customerInfo = .loaded(info)
await self.updateCustomerInfo(info)
}
#endif
}

private func updateCustomerInfo(_ info: CustomerInfo) {
self.customerInfo = .loaded(info)
}
#endif

}

@available(iOS 16.0, macOS 13.0, *)
Expand Down
12 changes: 9 additions & 3 deletions Sources/Support/StoreMessagesHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,15 @@ actor StoreMessagesHelper: StoreMessagesHelperType {

#if os(iOS) || targetEnvironment(macCatalyst) || VISION_OS

func deferMessagesIfNeeded() async throws {
// `nonisolated` required to work around Swift 5.10 issue.
// See https://github.com/RevenueCat/purchases-ios/pull/3599
nonisolated func deferMessagesIfNeeded() async throws {
guard !self.showStoreMessagesAutomatically else {
return
}

for try await message in self.storeMessagesProvider.messages {
self.deferredMessages.append(message)
await self.add(message)
}
}

Expand All @@ -69,13 +71,17 @@ actor StoreMessagesHelper: StoreMessagesHelperType {
self.deferredMessages.removeAll()
}

private func add(_ message: StoreMessage) {
self.deferredMessages.append(message)
}

#endif
}

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

protocol StoreMessagesProviderType {
protocol StoreMessagesProviderType: Sendable {

#if os(iOS) || targetEnvironment(macCatalyst) || VISION_OS

Expand Down
61 changes: 38 additions & 23 deletions Tests/UnitTests/Mocks/MockSK2BeginRefundRequestHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,41 +21,56 @@ import StoreKit
@available(watchOS, unavailable)
@available(tvOS, unavailable)
@available(tvOS, unavailable)
class MockSK2BeginRefundRequestHelper: SK2BeginRefundRequestHelper {
final class MockSK2BeginRefundRequestHelper: SK2BeginRefundRequestHelperType {

var mockSK2Error: Error?
private let _mockSK2Error: Atomic<Error?> = .init(nil)
private let _sk2Status: Atomic<StoreKit.Transaction.RefundRequestStatus?> = .init(nil)
private let _transactionVerified: Atomic<Bool> = true
private let _refundRequestCalled: Atomic<Bool> = false
private let _verifyTransactionCalled: Atomic<Bool> = false

var mockSK2Error: Error? {
get { self._mockSK2Error.value }
set { self._mockSK2Error.value = newValue }
}

// We can't directly store instances of `StoreKit.Transaction.RefundRequestStatus`, since that causes
// linking issues in iOS < 15, even with @available checks correctly in place.
// https://openradar.appspot.com/radar?id=4970535809187840
// https://github.com/apple/swift/issues/58099
private var untypedSK2Status: Box<StoreKit.Transaction.RefundRequestStatus?> = .init(nil)
var mockSK2Status: StoreKit.Transaction.RefundRequestStatus? {
get { return self.untypedSK2Status.value }
set { self.untypedSK2Status = .init(newValue) }
get { return self._sk2Status.value }
set { self._sk2Status.value = newValue }
}

var transactionVerified = true
var refundRequestCalled = false
var verifyTransactionCalled = false
var refundRequestCalled: Bool {
get { return self._refundRequestCalled.value }
set { self._refundRequestCalled.value = newValue }
}
var transactionVerified: Bool {
get { return self._transactionVerified.value }
set { self._transactionVerified.value = newValue }
}
var verifyTransactionCalled: Bool {
get { return self._verifyTransactionCalled.value }
set { self._verifyTransactionCalled.value = newValue }
}

func initiateSK2RefundRequest(
transactionID: UInt64, windowScene: UIWindowScene
) async -> Result<StoreKit.Transaction.RefundRequestStatus, Error> {
self.refundRequestCalled = true

override func initiateSK2RefundRequest(transactionID: UInt64, windowScene: UIWindowScene) async ->
Result<StoreKit.Transaction.RefundRequestStatus, Error> {
refundRequestCalled = true
if let error = mockSK2Error {
if let error = self.mockSK2Error {
return .failure(error)
} else {
return .success(mockSK2Status ?? StoreKit.Transaction.RefundRequestStatus.success)
return .success(self.mockSK2Status ?? .success)
}
}

override func verifyTransaction(productID: String) async throws -> UInt64 {
verifyTransactionCalled = true
if transactionVerified {
return UInt64()
func verifyTransaction(productID: String) async throws -> UInt64 {
self.verifyTransactionCalled = true

if self.transactionVerified {
return 0
} else {
let message = "Test error"
throw ErrorUtils.beginRefundRequestError(withMessage: message)
throw ErrorUtils.beginRefundRequestError(withMessage: "Test error")
}
}

Expand Down

0 comments on commit 53fe9cd

Please sign in to comment.