Эх сурвалжийг харах

Merge pull request #278 from bizz84/feature/fetch-receipt

Add fetchReceipt method. Update verifyReceipt to use it.
Andrea Bizzotto 7 жил өмнө
parent
commit
7cf5626fa2

+ 4 - 0
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))

+ 43 - 17
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
 

+ 35 - 11
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)

+ 6 - 0
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)

+ 10 - 0
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