bizz84 / bizz84/SwiftyStoreKit

SwiftyStoreKit not Local receipt validation friendly

Open
#431 0 comments 9 reactions 0 assignees View on GitHub
Dominant language
Swift
Stars
6.7k
Forks
801
PR merge metrics
No merged PRs in 30d

Description

As my app is often used in offline environments, then I had the requirement to implement Local Receipt validation. I did this successfully by using another great library: [SwiftyLocalReceiptValidator](https://github.com/andrewcbancroft/SwiftyLocalReceiptValidator). Everything is working without issues (with hacky solutions to some problems), but I wanted to point out some issues and potential weak spots for SwiftyStoreKit.

1. The fact that ReceiptInfo is actually a dictionary, makes building it together from local validation data difficult. You have to dig through source code of SwiftyStoreKit and replicate the Apple validateReceipt endpoint response data. This also makes updating the library difficult for me, because I have to validate with every version that the library is not using some new data to work properly.
2. Although a ReceiptValidator protocol is public and can be implemented, then the library does not really support this. For example ReceiptError and ReceiptStatus is only dedicated for AppleReceiptValidator and it do not provide a generic options. Second example would be that the subscription expiry validation is done against "request_date_ms", which again does not make sense in Local verification case. There are more examples.
3. Upon searching through source code, then I saw that ReceiptInfo uses a lot of fields, that are not supposed to be used according to Apples own documentation: "[Keys not documented below are reserved for use by Apple and must be ignored by your app.](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1)". Examples of fields used, are all the date fields. The undocumented "_ms" fields are used instead of dates. "Request date" field is also an undocumented field, which is heavily used. Changes from apple could render all the Apps using SwiftyStoreKit useless without warning.

Solutions?

ReceiptInfo should be standardised as a structure, some generic status codes and errors added and AppleReceiptValidator to be rewritten to only use the supported fields.

I just wanted to point out these thoughts I had, hopefully it will be some of help. Unfortuntely I'm unable to contribute, but I will share the extension I wrote to the SwiftLocalReceiptValidator library to get it working with SwiftyStoreKit.

`

extension LocalReceiptValidator {

func validateReceipt(receiptData: Data) -> ReceiptValidationResult {
do {
let receiptContainer = try receiptExtractor.extractPKCS7Container(receiptData)

try receiptSignatureValidator.checkSignaturePresence(receiptContainer)
try receiptSignatureValidator.checkSignatureAuthenticity(receiptContainer)

let parsedReceipt = try receiptParser.parse(receiptContainer)
try validateHash(receipt: parsedReceipt)

return .success(parsedReceipt)
} catch {
// swiftlint:disable force_cast
return .error(error as! ReceiptValidationError)
}
}

}

extension LocalReceiptValidator: ReceiptValidator {
func validate(receiptData: Data, completion: @escaping (VerifyReceiptResult) -> Void) {
let result = self.validateReceipt(receiptData: receiptData)
switch result {
case let .success(data):
completion(VerifyReceiptResult.success(receipt: data.receiptInfo))
case let .error(err):
completion(VerifyReceiptResult.error(error: ReceiptError.networkError(error: err)))
}
}
}

extension ParsedReceipt {
var receiptInfo: ReceiptInfo {
var receipt: ReceiptInfo = [:]
var info: [ReceiptInfo] = []
if let id = bundleIdentifier {
receipt["bundle_id"] = id as NSString
}
if let appVers = appVersion {
receipt["application_version"] = appVers as NSString
}
if let inAppReceipts = inAppPurchaseReceipts {
info = inAppReceipts.map { receipt in receipt.receiptInfo }
receipt["in_app"] = info as AnyObject
}
if let originalAppVers = originalAppVersion {
receipt["original_application_version"] = originalAppVers as NSString
}
if let receiptCreationDate = receiptCreationDate {
receipt["receipt_creation_date"] = receiptCreationDate.iso8601 as NSString
}
if let expirationDate = expirationDate {
receipt["expiration_date"] = expirationDate.iso8601 as NSString
}
receipt["request_date_ms"] = ((self.receiptCreationDate ?? Date()).timeIntervalSince1970 * 1000.0).description as NSString
return ["latest_receipt_info": info as AnyObject,
"receipt": receipt as AnyObject,
"latest_receipt": receipt as AnyObject]
}
}

extension ParsedInAppPurchaseReceipt {
var receiptInfo: ReceiptInfo {
var receipt: ReceiptInfo = [:]
if let quantity = quantity {
receipt["quantity"] = quantity.description as NSString
}
if let productId = productIdentifier {
receipt["product_id"] = productId as NSString
}
if let transactionId = transactionIdentifier {
receipt["transaction_id"] = transactionId as NSString
}
if let originalTransactionId = originalTransactionIdentifier {
receipt["original_transaction_id"] = originalTransactionId as NSString
}
if let purchaseDate = purchaseDate {
enterDateToReceipt(receipt: &receipt, key: "purchase_date", date: purchaseDate)
}
if let originalPurchaseDate = originalPurchaseDate {
enterDateToReceipt(receipt: &receipt, key: "original_purchase_date", date: originalPurchaseDate)
}
if let subscriptionExpirationDate = subscriptionExpirationDate {
enterDateToReceipt(receipt: &receipt, key: "expires_date", date: subscriptionExpirationDate)
}
if let cancellationDate = cancellationDate {
enterDateToReceipt(receipt: &receipt, key: "cancellation_date", date: cancellationDate)
}
if let webOrderLineId = webOrderLineItemId {
receipt["web_order_line_item_id"] = webOrderLineId.description as NSString
}
return receipt
}

private func enterDateToReceipt(receipt: inout ReceiptInfo, key: String, date: Date) {
receipt[key] = date.iso8601 as NSString
receipt[key + "_ms"] = Int64((Double(date.timeIntervalSince1970) * 1000.0)).description as NSString
}
}
`

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.