Skip to content

Commit

Permalink
StoreKit 2: Optionally send JWS tokens instead of receipts to the b…
Browse files Browse the repository at this point in the history
…ackend (#3227)

- Added a new option `usesStoreKit2JWS` under `DangerousSettings`. If
enabled, the SDK will send a JWS token instead of a receipt to the
applicable backend endpoints.
- The option must be used in conjunction with `usesStoreKit2IfAvailable`
configuration option.

### ToDo

- [x] Send JWS token when making a purchase in SK2 mode
- [x] Send JWS token when calculating promo offer eligibility
- [x] Update `syncTransactions` to send the latest transaction JWS.
- [x] Add tests

---------

Co-authored-by: RevenueCat Git Bot <[email protected]>
Co-authored-by: Distiller <[email protected]>
Co-authored-by: Distiller <[email protected]>
Co-authored-by: Distiller <[email protected]>
Co-authored-by: Distiller <[email protected]>
Co-authored-by: Distiller <[email protected]>
  • Loading branch information
7 people authored Oct 16, 2023
1 parent 1ad14d8 commit 235a7dc
Show file tree
Hide file tree
Showing 45 changed files with 1,003 additions and 207 deletions.
4 changes: 4 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@
37E3578711F5FDD5DC6458A8 /* AttributionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E3521731D8DC16873F55F3 /* AttributionFetcher.swift */; };
37E35C8515C5E2D01B0AF5C1 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E3507939634ED5A9280544 /* Strings.swift */; };
42F1DF385E3C1F9903A07FBF /* ProductsFetcherSK1.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFB3CBAA73855779FE828CE2 /* ProductsFetcherSK1.swift */; };
4DC546272AD44BBE005CDB35 /* EncodedAppleReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DC546262AD44BBE005CDB35 /* EncodedAppleReceipt.swift */; };
4F0201C42A13C85500091612 /* Assertions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0201C32A13C85500091612 /* Assertions.swift */; };
4F05876F2A5DE03F00E9A834 /* PaywallDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F05876E2A5DE03F00E9A834 /* PaywallDataTests.swift */; };
4F062D322A85A11600A8A613 /* PaywallData+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F062D312A85A11600A8A613 /* PaywallData+Localization.swift */; };
Expand Down Expand Up @@ -985,6 +986,7 @@
37E35EEE7783629CDE41B70C /* SystemInfoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemInfoTests.swift; sourceTree = "<group>"; };
37E35F783903362B65FB7AF3 /* MockProductsRequestFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockProductsRequestFactory.swift; sourceTree = "<group>"; };
37E35FDA0A44EA03EA12DAA2 /* DateFormatter+ExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DateFormatter+ExtensionsTests.swift"; sourceTree = "<group>"; };
4DC546262AD44BBE005CDB35 /* EncodedAppleReceipt.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncodedAppleReceipt.swift; sourceTree = "<group>"; };
4F0201C32A13C85500091612 /* Assertions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assertions.swift; sourceTree = "<group>"; };
4F05876E2A5DE03F00E9A834 /* PaywallDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallDataTests.swift; sourceTree = "<group>"; };
4F062D312A85A11600A8A613 /* PaywallData+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PaywallData+Localization.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1540,6 +1542,7 @@
isa = PBXGroup;
children = (
4F1428A52A4A1330006CD196 /* Test Data */,
4DC546262AD44BBE005CDB35 /* EncodedAppleReceipt.swift */,
57EFDC6A27BC1F370057EC39 /* ProductType.swift */,
2C0B98CC2797070B00C5874F /* PromotionalOffer.swift */,
57DE807028074C23008D6C6F /* SK1Storefront.swift */,
Expand Down Expand Up @@ -3584,6 +3587,7 @@
57C381DA2796153D009E3940 /* SK1StoreProductDiscount.swift in Sources */,
57DE807328074C76008D6C6F /* SK2Storefront.swift in Sources */,
57A17727276A721D0052D3A8 /* Set+Extensions.swift in Sources */,
4DC546272AD44BBE005CDB35 /* EncodedAppleReceipt.swift in Sources */,
37E350C67712B9E054FEF297 /* AttributionData.swift in Sources */,
37E3578711F5FDD5DC6458A8 /* AttributionFetcher.swift in Sources */,
B302206A27271BCB008F1A0D /* Decoder+Extensions.swift in Sources */,
Expand Down
4 changes: 4 additions & 0 deletions Sources/LocalReceiptParsing/Helpers/ReceiptStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ enum ReceiptStrings {
case refreshing_empty_receipt
case unable_to_load_receipt(Error)
case posting_receipt(AppleReceipt, initiationSource: String)
case posting_jws(String, initiationSource: String)
case receipt_subscription_purchase_equals_expiration(
productIdentifier: String,
purchase: Date,
Expand Down Expand Up @@ -91,6 +92,9 @@ extension ReceiptStrings: LogMessage {
return "Posting receipt (source: '\(initiationSource)') (note: the contents might not be up-to-date, " +
"but it will be refreshed with Apple's servers):\n\(receipt.debugDescription)"

case let .posting_jws(token, initiationSource):
return "Posting JWS token (source: '\(initiationSource)'):\n\(token)"

case let .receipt_subscription_purchase_equals_expiration(
productIdentifier,
purchase,
Expand Down
18 changes: 16 additions & 2 deletions Sources/Misc/DangerousSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Foundation
internal struct Internal: InternalDangerousSettingsType {

let enableReceiptFetchRetry: Bool
let usesStoreKit2JWS: Bool

#if DEBUG
let forceServerErrors: Bool
Expand All @@ -24,18 +25,24 @@ import Foundation

init(
enableReceiptFetchRetry: Bool = false,
usesStoreKit2JWS: Bool = false,
forceServerErrors: Bool = false,
forceSignatureFailures: Bool = false,
testReceiptIdentifier: String? = nil
) {
self.enableReceiptFetchRetry = enableReceiptFetchRetry
self.usesStoreKit2JWS = usesStoreKit2JWS
self.forceServerErrors = forceServerErrors
self.forceSignatureFailures = forceSignatureFailures
self.testReceiptIdentifier = testReceiptIdentifier
}
#else
init(enableReceiptFetchRetry: Bool = false) {
init(
enableReceiptFetchRetry: Bool = false,
usesStoreKit2JWS: Bool = false
) {
self.enableReceiptFetchRetry = enableReceiptFetchRetry
self.usesStoreKit2JWS = usesStoreKit2JWS
}
#endif

Expand Down Expand Up @@ -87,7 +94,8 @@ import Foundation

/// - Note: this is `internal` only so the only `public` way to enable `customEntitlementComputation`
/// is through ``Purchases/configureInCustomEntitlementsComputationMode(apiKey:appUserID:)``.
@objc internal convenience init(autoSyncPurchases: Bool = true, customEntitlementComputation: Bool) {
@objc internal convenience init(autoSyncPurchases: Bool = true,
customEntitlementComputation: Bool) {
self.init(autoSyncPurchases: autoSyncPurchases,
customEntitlementComputation: customEntitlementComputation,
internalSettings: Internal.default)
Expand All @@ -113,6 +121,12 @@ internal protocol InternalDangerousSettingsType: Sendable {
/// Whether `ReceiptFetcher` can retry fetching receipts.
var enableReceiptFetchRetry: Bool { get }

/**
* Controls whether StoreKit 2 JWS tokens are sent to RevenueCat instead of StoreKit 1 receipts.
* Must be used in conjunction with the `usesStoreKit2IfAvailable configuration` option.
*/
var usesStoreKit2JWS: Bool { get }

#if DEBUG
/// Whether `HTTPClient` will fake server errors
var forceServerErrors: Bool { get }
Expand Down
1 change: 1 addition & 0 deletions Sources/Misc/Deprecations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ extension CustomerInfo {
let transactionIdentifier: String
let quantity: Int
var storefront: Storefront? { return nil }
internal var jwsRepresentation: String? { return nil }

var hasKnownPurchaseDate: Bool { true }
var hasKnownTransactionIdentifier: Bool { return true }
Expand Down
4 changes: 2 additions & 2 deletions Sources/Networking/Backend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,12 @@ class Backend {
completion: completion)
}

func post(receiptData: Data,
func post(receipt: EncodedAppleReceipt,
productData: ProductRequestData?,
transactionData: PurchasedTransactionData,
observerMode: Bool,
completion: @escaping CustomerAPI.CustomerInfoResponseHandler) {
self.customer.post(receiptData: receiptData,
self.customer.post(receipt: receipt,
productData: productData,
transactionData: transactionData,
observerMode: observerMode,
Expand Down
4 changes: 2 additions & 2 deletions Sources/Networking/CustomerAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ final class CustomerAPI {
self.backendConfig.operationQueue.addOperation(postAttributionDataOperation)
}

func post(receiptData: Data,
func post(receipt: EncodedAppleReceipt,
productData: ProductRequestData?,
transactionData: PurchasedTransactionData,
observerMode: Bool,
Expand All @@ -109,7 +109,7 @@ final class CustomerAPI {
let postData = PostReceiptDataOperation.PostData(
transactionData: transactionData.withAttributesToPost(subscriberAttributesToPost),
productData: productData,
receiptData: receiptData,
receipt: receipt,
observerMode: observerMode,
testReceiptIdentifier: self.backendConfig.systemInfo.testReceiptIdentifier
)
Expand Down
4 changes: 2 additions & 2 deletions Sources/Networking/OfferingsAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class OfferingsAPI {
func post(offerIdForSigning offerIdentifier: String,
productIdentifier: String,
subscriptionGroup: String,
receiptData: Data,
receipt: EncodedAppleReceipt,
appUserID: String,
completion: @escaping OfferSigningResponseHandler) {
let config = NetworkOperation.UserSpecificConfiguration(httpClient: self.backendConfig.httpClient,
Expand All @@ -79,7 +79,7 @@ class OfferingsAPI {
let postOfferData = PostOfferForSigningOperation.PostOfferForSigningData(offerIdentifier: offerIdentifier,
productIdentifier: productIdentifier,
subscriptionGroup: subscriptionGroup,
receiptData: receiptData)
receipt: receipt)
let postOfferForSigningOperation = PostOfferForSigningOperation(configuration: config,
postOfferForSigningData: postOfferData,
responseHandler: completion)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class PostOfferForSigningOperation: NetworkOperation {
let offerIdentifier: String
let productIdentifier: String
let subscriptionGroup: String
let receiptData: Data
let receipt: EncodedAppleReceipt

}

Expand Down Expand Up @@ -127,7 +127,7 @@ private extension PostOfferForSigningOperation {

init(appUserID: String, data: PostOfferForSigningData) {
self.appUserID = appUserID
self.fetchToken = data.receiptData.asFetchToken
self.fetchToken = data.receipt.serialized()
self.generateOffers = [
.init(
offerID: data.offerIdentifier,
Expand Down
59 changes: 41 additions & 18 deletions Sources/Networking/Operations/PostReceiptDataOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ final class PostReceiptDataOperation: CacheableNetworkOperation {
/// - `subscriberAttributesByKey`
let cacheKey =
"""
\(configuration.appUserID)-\(postData.isRestore)-\(postData.receiptData.hashString)
\(configuration.appUserID)-\(postData.isRestore)-\(postData.receipt.hash)
-\(postData.productData?.cacheKey ?? "")
-\(postData.presentedOfferingIdentifier ?? "")-\(postData.observerMode)
-\(postData.subscriberAttributesByKey?.debugDescription ?? "")
Expand Down Expand Up @@ -120,7 +120,7 @@ extension PostReceiptDataOperation {
struct PostData {

let appUserID: String
let receiptData: Data
let receipt: EncodedAppleReceipt
let isRestore: Bool
let productData: ProductRequestData?
let presentedOfferingIdentifier: String?
Expand Down Expand Up @@ -151,13 +151,13 @@ extension PostReceiptDataOperation.PostData {
init(
transactionData data: PurchasedTransactionData,
productData: ProductRequestData?,
receiptData: Data,
receipt: EncodedAppleReceipt,
observerMode: Bool,
testReceiptIdentifier: String?
) {
self.init(
appUserID: data.appUserID,
receiptData: receiptData,
receipt: receipt,
isRestore: data.source.isRestore,
productData: productData,
presentedOfferingIdentifier: data.presentedOfferingID,
Expand Down Expand Up @@ -191,23 +191,31 @@ private extension PurchasedTransactionData {
private extension PostReceiptDataOperation {

func printReceiptData() {
do {
let receipt = try PurchasesReceiptParser.default.parse(from: self.postData.receiptData)
self.log(Strings.receipt.posting_receipt(
receipt,
switch self.postData.receipt {
case .jws(let content):
self.log(Strings.receipt.posting_jws(
content,
initiationSource: self.postData.initiationSource.rawValue
))

for purchase in receipt.inAppPurchases where purchase.purchaseDateEqualsExpiration {
Logger.appleError(Strings.receipt.receipt_subscription_purchase_equals_expiration(
productIdentifier: purchase.productId,
purchase: purchase.purchaseDate,
expiration: purchase.expiresDate
case .receipt(let data):
do {
let receipt = try PurchasesReceiptParser.default.parse(from: data)
self.log(Strings.receipt.posting_receipt(
receipt,
initiationSource: self.postData.initiationSource.rawValue
))
}

} catch {
Logger.appleError(Strings.receipt.parse_receipt_locally_error(error: error))
for purchase in receipt.inAppPurchases where purchase.purchaseDateEqualsExpiration {
Logger.appleError(Strings.receipt.receipt_subscription_purchase_equals_expiration(
productIdentifier: purchase.productId,
purchase: purchase.purchaseDate,
expiration: purchase.expiresDate
))
}

} catch {
Logger.appleError(Strings.receipt.parse_receipt_locally_error(error: error))
}
}
}

Expand Down Expand Up @@ -259,7 +267,7 @@ extension PostReceiptDataOperation.PostData: Encodable {
try container.encodeIfPresent(self.testReceiptIdentifier, forKey: .testReceiptIdentifier)
}

var fetchToken: String { return self.receiptData.asFetchToken }
var fetchToken: String { return self.receipt.serialized() }

}

Expand Down Expand Up @@ -313,3 +321,18 @@ extension ProductRequestData.InitiationSource: Encodable, RawRepresentable {
.dictionaryWithKeys { $0.rawValue }

}

// MARK: - EncodedAppleReceipt

private extension EncodedAppleReceipt {

var hash: String {
switch self {
case let .jws(content):
return content.asData.hashString
case let .receipt(data):
return data.hashString
}
}

}
5 changes: 4 additions & 1 deletion Sources/Purchasing/Purchases/Purchases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
let deviceCache = DeviceCache(sandboxEnvironmentDetector: systemInfo, userDefaults: userDefaults)

let purchasedProductsFetcher = OfflineCustomerInfoCreator.createPurchasedProductsFetcherIfAvailable()
let transactionFetcher = StoreKit2TransactionFetcher()

let backend = Backend(
apiKey: apiKey,
Expand Down Expand Up @@ -340,7 +341,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
operationDispatcher: operationDispatcher,
deviceCache: deviceCache,
backend: backend,
transactionFetcher: StoreKit2TransactionFetcher(),
transactionFetcher: transactionFetcher,
transactionPoster: transactionPoster,
systemInfo: systemInfo)

Expand Down Expand Up @@ -420,6 +421,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
operationDispatcher: operationDispatcher,
receiptFetcher: receiptFetcher,
receiptParser: receiptParser,
transactionFetcher: transactionFetcher,
customerInfoManager: customerInfoManager,
backend: backend,
transactionPoster: transactionPoster,
Expand All @@ -442,6 +444,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
operationDispatcher: operationDispatcher,
receiptFetcher: receiptFetcher,
receiptParser: receiptParser,
transactionFetcher: transactionFetcher,
customerInfoManager: customerInfoManager,
backend: backend,
transactionPoster: transactionPoster,
Expand Down
Loading

0 comments on commit 235a7dc

Please sign in to comment.