From 7f76849f2b99d1b3e0845518b2628d66d0b1071a Mon Sep 17 00:00:00 2001 From: Andrea Bizzotto Date: Wed, 11 Oct 2017 14:27:58 +0200 Subject: [PATCH 1/7] Add fetchReceipt method. Update verifyReceipt to use it. --- SwiftyStoreKit/InAppReceiptVerificator.swift | 34 +++++++++++++++----- SwiftyStoreKit/SwiftyStoreKit+Types.swift | 6 ++++ SwiftyStoreKit/SwiftyStoreKit.swift | 5 +++ 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/SwiftyStoreKit/InAppReceiptVerificator.swift b/SwiftyStoreKit/InAppReceiptVerificator.swift index e7389c18..7e449c94 100644 --- a/SwiftyStoreKit/InAppReceiptVerificator.swift +++ b/SwiftyStoreKit/InAppReceiptVerificator.swift @@ -58,9 +58,24 @@ 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, completion: completion) + case .error(let error): + completion(.error(error: error)) + } + } + } + + 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) + + fetchReceiptHandler(receiptData: receiptData, completion: completion) + } else { receiptRefreshRequest = refresh(nil) { result in @@ -70,7 +85,7 @@ class InAppReceiptVerificator: NSObject { switch result { case .success: if let receiptData = self.appStoreReceiptData { - self.verify(receiptData: receiptData, using: validator, password: password, completion: completion) + self.fetchReceiptHandler(receiptData: receiptData, completion: completion) } else { completion(.error(error: .noReceiptData)) } @@ -80,6 +95,12 @@ class InAppReceiptVerificator: NSObject { } } } + + private func fetchReceiptHandler(receiptData: Data, completion: (FetchReceiptResult) -> Void) { + + let base64EncodedString = receiptData.base64EncodedString(options: []) + completion(.success(encryptedReceipt: base64EncodedString)) + } /** * - Parameter receiptData: encrypted receipt data @@ -87,12 +108,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..722a4e0c 100644 --- a/SwiftyStoreKit/SwiftyStoreKit.swift +++ b/SwiftyStoreKit/SwiftyStoreKit.swift @@ -238,6 +238,11 @@ extension SwiftyStoreKit { sharedInstance.receiptVerificator.verifyReceipt(using: validator, password: password, forceRefresh: forceRefresh, completion: completion) } + 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 * - Parameter productId: the product id of the purchase to verify From 5366efa8b364433d770e3d588b9213105fddee5e Mon Sep 17 00:00:00 2001 From: Andrea Bizzotto Date: Wed, 11 Oct 2017 14:41:59 +0200 Subject: [PATCH 2/7] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) 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)) From 9597acd84562abc34b6e2e4ef112f8517ae84873 Mon Sep 17 00:00:00 2001 From: Andrea Bizzotto Date: Wed, 11 Oct 2017 15:19:01 +0200 Subject: [PATCH 3/7] Update README --- README.md | 59 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index ad9b0cbf..54b0fdd5 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 + +This helper can be used to retrieve the (encrypted) local receipt data: + +```swift +let receiptData = SwiftyStoreKit.localReceiptData +let receiptString = receiptData.base64EncodedString(options: []) +// do your receipt validation here +``` -This is done with a `verifyReceipt` method which does two things: +However, the receipt file may be missing or outdated. -- If the receipt is missing, refresh it -- If the receipt is available or is refreshed, validate it +### Fetch receipt -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`](#verifyReceipt) 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,12 @@ 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 does remote receipt validation 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). ## Verifying purchases and subscriptions From 5ca86704b0c7ad594819b3ff7a1bebdf3a84fb17 Mon Sep 17 00:00:00 2001 From: Andrea Bizzotto Date: Wed, 11 Oct 2017 15:23:57 +0200 Subject: [PATCH 4/7] Restore missing password parameter --- SwiftyStoreKit/InAppReceiptVerificator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SwiftyStoreKit/InAppReceiptVerificator.swift b/SwiftyStoreKit/InAppReceiptVerificator.swift index 7e449c94..96894571 100644 --- a/SwiftyStoreKit/InAppReceiptVerificator.swift +++ b/SwiftyStoreKit/InAppReceiptVerificator.swift @@ -61,7 +61,7 @@ class InAppReceiptVerificator: NSObject { fetchReceipt(forceRefresh: forceRefresh, refresh: refresh) { result in switch result { case .success(let encryptedReceipt): - self.verify(receipt: encryptedReceipt, using: validator, completion: completion) + self.verify(receipt: encryptedReceipt, using: validator, password: password, completion: completion) case .error(let error): completion(.error(error: error)) } From 59cb6cd20e1ea4a40556817c52ef5eebde7f659b Mon Sep 17 00:00:00 2001 From: Andrea Bizzotto Date: Wed, 11 Oct 2017 15:34:59 +0200 Subject: [PATCH 5/7] Update README --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 54b0fdd5..6a74d3e6 100644 --- a/README.md +++ b/README.md @@ -311,7 +311,7 @@ This method works as follows: 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: -1. Validate with Apple via the `AppleReceiptValidator` (see [`verifyReceipt`](#verifyReceipt) below). +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. @@ -334,9 +334,10 @@ SwiftyStoreKit.verifyReceipt(using: appleValidator, password: "your-shared-secre **Notes** * This method is based on `fetchReceipt`, and the same refresh logic discussed above applies. -* `AppleReceiptValidator` is a **reference implementation** that does remote receipt validation with Apple and results in a network call. _This is prone to man-in-the-middle attacks._ +* `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 From 9f91f6571c42cec3dd421f001688c251448398f9 Mon Sep 17 00:00:00 2001 From: Andrea Bizzotto Date: Wed, 11 Oct 2017 15:37:47 +0200 Subject: [PATCH 6/7] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6a74d3e6..fe473718 100644 --- a/README.md +++ b/README.md @@ -270,7 +270,7 @@ 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, located by `Bundle.main.appStoreReceiptURL`. -### Retrieve local receipt +### Retrieve local receipt (encrypted) This helper can be used to retrieve the (encrypted) local receipt data: @@ -282,7 +282,7 @@ let receiptString = receiptData.base64EncodedString(options: []) However, the receipt file may be missing or outdated. -### Fetch receipt +### Fetch receipt (encrypted) Use this method to get the updated receipt: From 22fa870eefa45a6f669ac6884527beca6201c163 Mon Sep 17 00:00:00 2001 From: Andrea Bizzotto Date: Wed, 11 Oct 2017 15:43:04 +0200 Subject: [PATCH 7/7] Update documentation --- SwiftyStoreKit/InAppReceiptVerificator.swift | 18 ++++++++++++------ SwiftyStoreKit/SwiftyStoreKit.swift | 7 ++++++- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/SwiftyStoreKit/InAppReceiptVerificator.swift b/SwiftyStoreKit/InAppReceiptVerificator.swift index 96894571..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. @@ -68,13 +66,21 @@ class InAppReceiptVerificator: NSObject { } } + /** + * 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 { - fetchReceiptHandler(receiptData: receiptData, completion: completion) + fetchReceiptSuccessHandler(receiptData: receiptData, completion: completion) } else { @@ -85,7 +91,7 @@ class InAppReceiptVerificator: NSObject { switch result { case .success: if let receiptData = self.appStoreReceiptData { - self.fetchReceiptHandler(receiptData: receiptData, completion: completion) + self.fetchReceiptSuccessHandler(receiptData: receiptData, completion: completion) } else { completion(.error(error: .noReceiptData)) } @@ -96,7 +102,7 @@ class InAppReceiptVerificator: NSObject { } } - private func fetchReceiptHandler(receiptData: Data, completion: (FetchReceiptResult) -> Void) { + private func fetchReceiptSuccessHandler(receiptData: Data, completion: (FetchReceiptResult) -> Void) { let base64EncodedString = receiptData.base64EncodedString(options: []) completion(.success(encryptedReceipt: base64EncodedString)) diff --git a/SwiftyStoreKit/SwiftyStoreKit.swift b/SwiftyStoreKit/SwiftyStoreKit.swift index 722a4e0c..785d7b77 100644 --- a/SwiftyStoreKit/SwiftyStoreKit.swift +++ b/SwiftyStoreKit/SwiftyStoreKit.swift @@ -237,7 +237,12 @@ 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)