diff --git a/CHANGELOG.md b/CHANGELOG.md index 092e34fb..4d0e3907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. +## [0.10.9](https://github.com/bizz84/SwiftyStoreKit/releases/tag/0.10.9) Add `fetchReceipt` method + +* Add `fetchReceipt` method. Update `verifyReceipt` to use it ([#278](https://github.com/bizz84/SwiftyStoreKit/pull/278), related issues: [#272](https://github.com/bizz84/SwiftyStoreKit/issues/272), [#223](https://github.com/bizz84/SwiftyStoreKit/issues/223)). + ## [0.10.8](https://github.com/bizz84/SwiftyStoreKit/releases/tag/0.10.8) Update to swiftlint 0.22.0 * Update to swiftlint 0.22.0 ([#270](https://github.com/bizz84/SwiftyStoreKit/pull/270), fix for [#273](https://github.com/bizz84/SwiftyStoreKit/issues/273)) diff --git a/README.md b/README.md index ad9b0cbf..fe473718 100644 --- a/README.md +++ b/README.md @@ -268,37 +268,57 @@ According to [Apple - Delivering Products](https://developer.apple.com/library/c When an app is first installed, the app receipt is missing. -As soon as a user completes a purchase or restores purchases, StoreKit creates and stores the receipt locally as a file. +As soon as a user completes a purchase or restores purchases, StoreKit creates and stores the receipt locally as a file, located by `Bundle.main.appStoreReceiptURL`. -As the local receipt is always encrypted, a verification step is needed to get all the receipt fields in readable form. +### Retrieve local receipt (encrypted) -This is done with a `verifyReceipt` method which does two things: +This helper can be used to retrieve the (encrypted) local receipt data: -- If the receipt is missing, refresh it -- If the receipt is available or is refreshed, validate it +```swift +let receiptData = SwiftyStoreKit.localReceiptData +let receiptString = receiptData.base64EncodedString(options: []) +// do your receipt validation here +``` + +However, the receipt file may be missing or outdated. + +### Fetch receipt (encrypted) -Receipt validation can be done remotely with Apple via the `AppleReceiptValidator` class, or with a client-supplied validator conforming to the `ReceiptValidator` protocol. +Use this method to get the updated receipt: + +```swift +SwiftyStoreKit.fetchReceipt(forceRefresh: true) { result in + switch result { + case .success(let encryptedReceipt): + print("fetchReceipt success:\n\(encryptedReceipt)") + case .error(let error): + print("fetchReceipt error: \(error)") + } +} +``` + +This method works as follows: + +* If `forceRefresh = false`, it returns the local receipt from file, or refreshes it if missing. +* If `forceRefresh = true`, it always refreshes the receipt regardless. **Notes** -* If the local receipt is missing when calling `verifyReceipt`, a network call is made to refresh it. +* If the local receipt is missing or `forceRefresh = true` when calling `fetchReceipt`, a network call is made to refresh it. * If the user is not logged to the App Store, StoreKit will present a popup asking to **Sign In to the iTunes Store**. * If the user enters valid credentials, the receipt will be refreshed and verified. * If the user cancels, receipt refresh will fail with a **Cannot connect to iTunes Store** error. -* Using `AppleReceiptValidator` (see below) does remote receipt validation and also results in a network call. -* Local receipt validation is not implemented (see [issue #101](https://github.com/bizz84/SwiftyStoreKit/issues/101) for details). +If `fetchReceipt` is successful, it will return the **encrypted** receipt as a string. For this reason, a **validation** step is needed to get all the receipt fields in readable form. This can be done in various ways: -### Retrieve local receipt - -```swift -let receiptData = SwiftyStoreKit.localReceiptData -let receiptString = receiptData.base64EncodedString(options: []) -// do your receipt validation here -``` +1. Validate with Apple via the `AppleReceiptValidator` (see [`verifyReceipt`](#verify-receipt) below). +2. Perform local receipt validation (see [#101](https://github.com/bizz84/SwiftyStoreKit/issues/101)). +3. Post the receipt data and validate on server. ### Verify Receipt +Use this method to (optionally) refresh the receipt and perform validation in one step. + ```swift let appleValidator = AppleReceiptValidator(service: .production) SwiftyStoreKit.verifyReceipt(using: appleValidator, password: "your-shared-secret", forceRefresh: false) { result in @@ -311,7 +331,13 @@ SwiftyStoreKit.verifyReceipt(using: appleValidator, password: "your-shared-secre } ``` -Note: you can specify `forceRefresh: true` to force SwiftyStoreKit to refresh the receipt with Apple, even if a local receipt is already stored. +**Notes** + +* This method is based on `fetchReceipt`, and the same refresh logic discussed above applies. +* `AppleReceiptValidator` is a **reference implementation** that validates the receipt with Apple and results in a network call. _This is prone to man-in-the-middle attacks._ +* You should implement your secure logic by validating your receipt locally, or sending the encrypted receipt data and validating it in your server. +* Local receipt validation is not implemented (see [issue #101](https://github.com/bizz84/SwiftyStoreKit/issues/101) for details). +* You can implement your own receipt validator by conforming to the `ReceiptValidator` protocol and passing it to `verifyReceipt`. ## Verifying purchases and subscriptions diff --git a/SwiftyStoreKit/InAppReceiptVerificator.swift b/SwiftyStoreKit/InAppReceiptVerificator.swift index e7389c18..b39e0346 100644 --- a/SwiftyStoreKit/InAppReceiptVerificator.swift +++ b/SwiftyStoreKit/InAppReceiptVerificator.swift @@ -43,9 +43,7 @@ class InAppReceiptVerificator: NSObject { private var receiptRefreshRequest: InAppReceiptRefreshRequest? /** - * Verify application receipt. This method does two things: - * * If the receipt is missing, refresh it - * * If the receipt is available or is refreshed, validate it + * Verify application receipt. * - Parameter validator: Validator to check the encrypted receipt and return the receipt in readable format * - Parameter password: Your app’s shared secret (a hexadecimal string). Only used for receipts that contain auto-renewable subscriptions. * - Parameter forceRefresh: If true, refreshes the receipt even if one already exists. @@ -58,9 +56,32 @@ class InAppReceiptVerificator: NSObject { refresh: InAppReceiptRefreshRequest.ReceiptRefresh = InAppReceiptRefreshRequest.refresh, completion: @escaping (VerifyReceiptResult) -> Void) { + fetchReceipt(forceRefresh: forceRefresh, refresh: refresh) { result in + switch result { + case .success(let encryptedReceipt): + self.verify(receipt: encryptedReceipt, using: validator, password: password, completion: completion) + case .error(let error): + completion(.error(error: error)) + } + } + } + + /** + * Fetch application receipt. This method does two things: + * * If the receipt is missing, refresh it + * * If the receipt is available or is refreshed, validate it + * - Parameter forceRefresh: If true, refreshes the receipt even if one already exists. + * - Parameter refresh: closure to perform receipt refresh (this is made explicit for testability) + * - Parameter completion: handler for result + */ + public func fetchReceipt(forceRefresh: Bool, + refresh: InAppReceiptRefreshRequest.ReceiptRefresh = InAppReceiptRefreshRequest.refresh, + completion: @escaping (FetchReceiptResult) -> Void) { + if let receiptData = appStoreReceiptData, forceRefresh == false { - - verify(receiptData: receiptData, using: validator, password: password, completion: completion) + + fetchReceiptSuccessHandler(receiptData: receiptData, completion: completion) + } else { receiptRefreshRequest = refresh(nil) { result in @@ -70,7 +91,7 @@ class InAppReceiptVerificator: NSObject { switch result { case .success: if let receiptData = self.appStoreReceiptData { - self.verify(receiptData: receiptData, using: validator, password: password, completion: completion) + self.fetchReceiptSuccessHandler(receiptData: receiptData, completion: completion) } else { completion(.error(error: .noReceiptData)) } @@ -80,6 +101,12 @@ class InAppReceiptVerificator: NSObject { } } } + + private func fetchReceiptSuccessHandler(receiptData: Data, completion: (FetchReceiptResult) -> Void) { + + let base64EncodedString = receiptData.base64EncodedString(options: []) + completion(.success(encryptedReceipt: base64EncodedString)) + } /** * - Parameter receiptData: encrypted receipt data @@ -87,12 +114,9 @@ class InAppReceiptVerificator: NSObject { * - Parameter password: Your app’s shared secret (a hexadecimal string). Only used for receipts that contain auto-renewable subscriptions. * - Parameter completion: handler for result */ - private func verify(receiptData: Data, using validator: ReceiptValidator, password: String? = nil, completion: @escaping (VerifyReceiptResult) -> Void) { - - // The base64 encoded receipt data. - let base64EncodedString = receiptData.base64EncodedString(options: []) + private func verify(receipt: String, using validator: ReceiptValidator, password: String? = nil, completion: @escaping (VerifyReceiptResult) -> Void) { - validator.validate(receipt: base64EncodedString, password: password) { result in + validator.validate(receipt: receipt, password: password) { result in DispatchQueue.main.async { completion(result) diff --git a/SwiftyStoreKit/SwiftyStoreKit+Types.swift b/SwiftyStoreKit/SwiftyStoreKit+Types.swift index f6d78e0d..92abd139 100644 --- a/SwiftyStoreKit/SwiftyStoreKit+Types.swift +++ b/SwiftyStoreKit/SwiftyStoreKit+Types.swift @@ -89,6 +89,12 @@ public enum RefreshReceiptResult { case error(error: Error) } +// Fetch receipt result +public enum FetchReceiptResult { + case success(encryptedReceipt: String) + case error(error: ReceiptError) +} + // Verify receipt result public enum VerifyReceiptResult { case success(receipt: ReceiptInfo) diff --git a/SwiftyStoreKit/SwiftyStoreKit.swift b/SwiftyStoreKit/SwiftyStoreKit.swift index f3104757..785d7b77 100644 --- a/SwiftyStoreKit/SwiftyStoreKit.swift +++ b/SwiftyStoreKit/SwiftyStoreKit.swift @@ -237,6 +237,16 @@ extension SwiftyStoreKit { sharedInstance.receiptVerificator.verifyReceipt(using: validator, password: password, forceRefresh: forceRefresh, completion: completion) } + + /** + * Fetch application receipt + * - Parameter forceRefresh: If true, refreshes the receipt even if one already exists. + * - Parameter completion: handler for result + */ + public class func fetchReceipt(forceRefresh: Bool, completion: @escaping (FetchReceiptResult) -> Void) { + + sharedInstance.receiptVerificator.fetchReceipt(forceRefresh: forceRefresh, completion: completion) + } /** * Verify the purchase of a Consumable or NonConsumable product in a receipt