Skip to content

Commit

Permalink
add sk2 and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
vegaro committed Sep 10, 2024
1 parent a64875d commit b96a7a3
Show file tree
Hide file tree
Showing 11 changed files with 341 additions and 35 deletions.
2 changes: 1 addition & 1 deletion Sources/Diagnostics/DiagnosticsEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ extension DiagnosticsEvent {
case backendErrorCodeKey
case errorMessageKey
case errorCodeKey
case skErrorCodeKey
case skErrorDescriptionKey
case eTagHitKey

}
Expand Down
6 changes: 3 additions & 3 deletions Sources/Diagnostics/DiagnosticsTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ protocol DiagnosticsTrackerType {
storeKitVersion: StoreKitVersion,
errorMessage: String?,
errorCode: Int?,
skErrorCode: Int?) async
storeKitErrorDescription: String?) async

}

Expand Down Expand Up @@ -102,15 +102,15 @@ final class DiagnosticsTracker: DiagnosticsTrackerType {
storeKitVersion: StoreKitVersion,
errorMessage: String?,
errorCode: Int?,
skErrorCode: Int?) async {
storeKitErrorDescription: String?) async {
await track(
DiagnosticsEvent(eventType: .applePurchaseAttempt,
properties: [
.successfulKey: AnyEncodable(wasSuccessful),
.storeKitVersion: AnyEncodable("store_kit_\(storeKitVersion.debugDescription)"),
.errorMessageKey: AnyEncodable(errorMessage),
.errorCodeKey: AnyEncodable(errorCode),
.skErrorCodeKey: AnyEncodable(skErrorCode)
.skErrorDescriptionKey: AnyEncodable(storeKitErrorDescription)
],
timestamp: self.dateProvider.now())
)
Expand Down
4 changes: 2 additions & 2 deletions Sources/Diagnostics/Networking/DiagnosticsEventsRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ private extension DiagnosticsEvent.DiagnosticsPropertyKey {
return "error_message"
case .errorCodeKey:
return "error_code"
case .skErrorCodeKey:
return "sk_error_code"
case .skErrorDescriptionKey:
return "sk_error_description"
case .eTagHitKey:
return "etag_hit"
}
Expand Down
51 changes: 51 additions & 0 deletions Sources/Error Handling/SKError+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,57 @@ extension SKError: PurchasesErrorConvertible {

}

extension SKError.Code {
var trackingDescription: String {
switch self {
case .unknown:
return "unknown"
case .clientInvalid:
return "client_invalid"
case .paymentCancelled:
return "payment_cancelled"
case .paymentInvalid:
return "payment_invalid"
case .paymentNotAllowed:
return "payment_not_allowed"
case .storeProductNotAvailable:
return "store_product_not_available"
case .cloudServicePermissionDenied:
return "cloud_service_permission_denied"
case .cloudServiceNetworkConnectionFailed:
return "cloud_service_network_connection_failed"
case .cloudServiceRevoked:
return "cloud_service_revoked"
case .privacyAcknowledgementRequired:
return "privacy_acknowledgement_required"
case .unauthorizedRequestData:
return "unauthorized_request_data"
case .invalidOfferIdentifier:
return "invalid_offer_identifier"
case .invalidSignature:
return "invalid_signature"
case .missingOfferParams:
return "missing_offer_params"
case .invalidOfferPrice:
return "invalid_offer_price"
case .overlayCancelled:
return "overlay_cancelled"
case .overlayInvalidConfiguration:
return "overlay_invalid_configuration"
case .overlayTimeout:
return "overlay_timeout"
case .ineligibleForOffer:
return "ineligible_for_offer"
case .unsupportedPlatform:
return "unsupported_platform"
case .overlayPresentedInBackgroundScene:
return "overlay_presented_in_background_scene"
@unknown default:
return "unknown_future_error"
}
}
}

private extension SKError {

enum UndocumentedCode: Int {
Expand Down
23 changes: 23 additions & 0 deletions Sources/Error Handling/StoreKitError+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,29 @@ extension StoreKitError: PurchasesErrorConvertible {
}
}

var trackingDescription: String {
switch self {
case .unknown:
return "unknown"
case .userCancelled:
return "user_cancelled"
case .networkError(let urlError):
return "network_error_\(urlError.code.rawValue)"
case .systemError(let error):
return "system_error_\(String(describing: error))"
case .notAvailableInStorefront:
return "not_available_in_storefront"
case .notEntitled:
if #available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 8.5, visionOS 1.0, *) {
return "not_entitled"
} else {
return "unknown"
}
@unknown default:
return "unknown_future_error"
}
}

}

@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
Expand Down
3 changes: 2 additions & 1 deletion Sources/Purchasing/Purchases/Purchases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,8 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
storeKit2StorefrontListener: StoreKit2StorefrontListener(delegate: nil),
storeKit2ObserverModePurchaseDetector: storeKit2ObserverModePurchaseDetector,
storeMessagesHelper: storeMessagesHelper,
diagnosticsSynchronizer: diagnosticsSynchronizer
diagnosticsSynchronizer: diagnosticsSynchronizer,
diagnosticsTracker: diagnosticsTracker
)
} else {
return .init(
Expand Down
47 changes: 32 additions & 15 deletions Sources/Purchasing/Purchases/PurchasesOrchestrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -484,12 +484,11 @@ final class PurchasesOrchestrator {
}
}

// swiftlint:disable function_body_length
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
func purchase(
sk2Product: SK2Product,
package: Package?,
promotionalOffer: PromotionalOffer.SignedData?
) async throws -> PurchaseResultData {
func purchase(sk2Product: SK2Product,
package: Package?,
promotionalOffer: PromotionalOffer.SignedData?) async throws -> PurchaseResultData {
let result: Product.PurchaseResult

do {
Expand Down Expand Up @@ -526,10 +525,14 @@ final class PurchasesOrchestrator {
userCancelled: true
)
} catch let error as PromotionalOffer.SignedData.Error {
throw ErrorUtils.invalidPromotionalOfferError(error: error,
message: error.localizedDescription)
let error = ErrorUtils.invalidPromotionalOfferError(error: error,
message: error.localizedDescription)
self.trackPurchaseEventIfNeeded(storeKitVersion: .storeKit2, error: error.asPublicError)
throw error
} catch {
throw ErrorUtils.purchasesError(withStoreKitError: error)
let purchasesError = ErrorUtils.purchasesError(withStoreKitError: error)
self.trackPurchaseEventIfNeeded(storeKitVersion: .storeKit2, error: purchasesError.asPublicError)
throw error
}

// `userCancelled` above comes from `StoreKitError.userCancelled`.
Expand All @@ -552,8 +555,13 @@ final class PurchasesOrchestrator {
fetchPolicy: .cachedOrFetched)
}

if !userCancelled {
self.trackPurchaseEventIfNeeded(storeKitVersion: .storeKit2, error: nil)
}

return (transaction, customerInfo, userCancelled)
}
// swiftlint:enable function_body_length

@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
private func purchase(
Expand Down Expand Up @@ -885,17 +893,26 @@ private extension PurchasesOrchestrator {
error: PublicError?) {
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *),
let diagnosticsTracker = self.diagnosticsTracker {
let errorMessage = (error?.userInfo[NSUnderlyingErrorKey] as? Error)?.localizedDescription ?? error?.localizedDescription
let errorCode = error?.code
let skError = error?.userInfo[NSUnderlyingErrorKey] as? SKError
let skErrorCode = skError?.code
Async.call(with: {}, asyncMethod: {
Task(priority: .background) {
let errorMessage =
(error?.userInfo[NSUnderlyingErrorKey] as? Error)?.localizedDescription ?? error?.localizedDescription
let errorCode = error?.code
let storeKitErrorDescription: String?

if let skError = error?.userInfo[NSUnderlyingErrorKey] as? SKError {
storeKitErrorDescription = skError.code.trackingDescription
} else if let storeKitError = error?.userInfo[NSUnderlyingErrorKey] as? StoreKitError {
storeKitErrorDescription = storeKitError.trackingDescription
} else {
storeKitErrorDescription = nil
}

await diagnosticsTracker.trackPurchaseRequest(wasSuccessful: error == nil,
storeKitVersion: storeKitVersion,
errorMessage: errorMessage,
errorCode: errorCode,
skErrorCode: skErrorCode?.rawValue)
})
storeKitErrorDescription: storeKitErrorDescription)
}
}
}

Expand Down
17 changes: 4 additions & 13 deletions Tests/StoreKitUnitTests/BasePurchasesOrchestratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,17 +109,6 @@ class BasePurchasesOrchestratorTests: StoreKitConfigTestCase {

}

fileprivate func setUpDiagnosticSynchronizer() {
if #available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) {
self.orchestrator._diagnosticsSynchronizer = MockDiagnosticsSynchronizer()
}
}

@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
var mockDiagnosticsSynchronizer: MockDiagnosticsSynchronizer? {
return self.orchestrator.diagnosticsSynchronizer as? MockDiagnosticsSynchronizer
}

func setUpStoreKit2Listener() {
if #available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) {
self.orchestrator._storeKit2TransactionListener = MockStoreKit2TransactionListener()
Expand Down Expand Up @@ -190,7 +179,8 @@ class BasePurchasesOrchestratorTests: StoreKitConfigTestCase {
storeKit2TransactionListener: StoreKit2TransactionListenerType,
storeKit2StorefrontListener: StoreKit2StorefrontListener,
storeKit2ObserverModePurchaseDetector: StoreKit2ObserverModePurchaseDetectorType,
diagnosticsSynchronizer: DiagnosticsSynchronizerType? = nil
diagnosticsSynchronizer: DiagnosticsSynchronizerType? = nil,
diagnosticsTracker: DiagnosticsTrackerType? = nil
) {
self.orchestrator = PurchasesOrchestrator(
productsManager: self.productsManager,
Expand All @@ -214,7 +204,8 @@ class BasePurchasesOrchestratorTests: StoreKitConfigTestCase {
storeKit2StorefrontListener: storeKit2StorefrontListener,
storeKit2ObserverModePurchaseDetector: storeKit2ObserverModePurchaseDetector,
storeMessagesHelper: self.mockStoreMessagesHelper,
diagnosticsSynchronizer: diagnosticsSynchronizer
diagnosticsSynchronizer: diagnosticsSynchronizer,
diagnosticsTracker: diagnosticsTracker
)
self.storeKit1Wrapper.delegate = self.orchestrator
}
Expand Down
87 changes: 87 additions & 0 deletions Tests/StoreKitUnitTests/PurchasesOrchestratorSK1Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -675,3 +675,90 @@ class PurchasesOrchestratorSK1Tests: BasePurchasesOrchestratorTests, PurchasesOr
}

}

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

func testPurchaseSK1TracksCorrectly() async throws {
try AvailabilityChecks.iOS16APIAvailableOrSkipTest()

let transactionListener = MockStoreKit2TransactionListener()
let storeKit2ObserverModePurchaseDetector = MockStoreKit2ObserverModePurchaseDetector()
let diagnosticsSynchronizer = MockDiagnosticsSynchronizer()
let diagnosticsTracker = MockDiagnosticsTracker()

self.setUpOrchestrator(storeKit2TransactionListener: transactionListener,
storeKit2StorefrontListener: StoreKit2StorefrontListener(delegate: nil),
storeKit2ObserverModePurchaseDetector: storeKit2ObserverModePurchaseDetector,
diagnosticsSynchronizer: diagnosticsSynchronizer,
diagnosticsTracker: diagnosticsTracker)

backend.stubbedPostReceiptResult = .success(mockCustomerInfo)

let product = try await self.fetchSk1Product()
let payment = storeKit1Wrapper.payment(with: product)

let (transaction, _, _, _) = await withCheckedContinuation { continuation in
orchestrator.purchase(sk1Product: product,
payment: payment,
package: nil,
wrapper: self.storeKit1Wrapper) { transaction, customerInfo, error, userCancelled in
continuation.resume(returning: (transaction, customerInfo, error, userCancelled))
}
}

expect(transaction).toNot(beNil())
expect(diagnosticsTracker.trackedPurchaseRequestParams.count) == 1

let params = try XCTUnwrap(diagnosticsTracker.trackedPurchaseRequestParams.first)
expect(params.wasSuccessful).to(beTrue())
expect(params.storeKitVersion) == .storeKit1
expect(params.errorMessage).to(beNil())
expect(params.errorCode).to(beNil())
expect(params.storeKitErrorDescription).to(beNil())
}

func testPurchaseWithInvalidPromotionalOfferSignatureTracksError() async throws {
try AvailabilityChecks.iOS16APIAvailableOrSkipTest()

let transactionListener = MockStoreKit2TransactionListener()
let storeKit2ObserverModePurchaseDetector = MockStoreKit2ObserverModePurchaseDetector()
let diagnosticsSynchronizer = MockDiagnosticsSynchronizer()
let diagnosticsTracker = MockDiagnosticsTracker()

self.setUpOrchestrator(storeKit2TransactionListener: transactionListener,
storeKit2StorefrontListener: StoreKit2StorefrontListener(delegate: nil),
storeKit2ObserverModePurchaseDetector: storeKit2ObserverModePurchaseDetector,
diagnosticsSynchronizer: diagnosticsSynchronizer,
diagnosticsTracker: diagnosticsTracker)

storeKit1Wrapper.mockAddPaymentTransactionState = .failed
storeKit1Wrapper.mockTransactionError = NSError(domain: SKErrorDomain,
code: SKError.Code.invalidSignature.rawValue)
let product = try await self.fetchSk1Product()
let offer = PromotionalOffer.SignedData(identifier: "",
keyIdentifier: "",
nonce: UUID(),
signature: "",
timestamp: 0)

let (transaction, _, _, _) = await withCheckedContinuation { continuation in
orchestrator.purchase(sk1Product: product,
promotionalOffer: offer,
package: nil,
wrapper: self.storeKit1Wrapper) { transaction, customerInfo, error, userCancelled in
continuation.resume(returning: (transaction, customerInfo, error, userCancelled))
}
}
expect(transaction).toNot(beNil())
expect(diagnosticsTracker.trackedPurchaseRequestParams.count) == 1

let params = try XCTUnwrap(diagnosticsTracker.trackedPurchaseRequestParams.first)
expect(params.wasSuccessful).to(beFalse())
expect(params.storeKitVersion) == .storeKit1
expect(params.errorMessage) == "The operation couldn’t be completed. (SKErrorDomain error 12.)"
expect(params.errorCode) == ErrorCode.invalidPromotionalOfferError.rawValue
expect(params.storeKitErrorDescription) == SKError.Code.invalidSignature.trackingDescription
}

}
Loading

0 comments on commit b96a7a3

Please sign in to comment.