Browse Source

Merge pull request #107 from bizz84/feature/non-atomic-purchases

Feature/non atomic purchases
Andrea Bizzotto 8 năm trước cách đây
mục cha
commit
d4579d9dc9

+ 88 - 11
README.md

@@ -23,7 +23,9 @@ SwiftyStoreKit is a lightweight In App Purchases framework for iOS 8.0+, tvOS 9.
 <img src="https://github.com/bizz84/SwiftyStoreKit/raw/master/Screenshots/Preview.png" width="320">
 <img src="https://github.com/bizz84/SwiftyStoreKit/raw/master/Screenshots/Preview.png" width="320">
 <img src="https://github.com/bizz84/SwiftyStoreKit/raw/master/Screenshots/Preview2.png" width="320">
 <img src="https://github.com/bizz84/SwiftyStoreKit/raw/master/Screenshots/Preview2.png" width="320">
 
 
-### Setup + Complete Transactions
+## App startup
+
+### Complete Transactions
 
 
 Apple recommends to register a transaction observer [as soon as the app starts](https://developer.apple.com/library/ios/technotes/tn2387/_index.html):
 Apple recommends to register a transaction observer [as soon as the app starts](https://developer.apple.com/library/ios/technotes/tn2387/_index.html):
 > Adding your app's observer at launch ensures that it will persist during all launches of your app, thus allowing your app to receive all the payment queue notifications.
 > Adding your app's observer at launch ensures that it will persist during all launches of your app, thus allowing your app to receive all the payment queue notifications.
@@ -33,13 +35,17 @@ SwiftyStoreKit supports this by calling `completeTransactions()` when the app st
 ```swift
 ```swift
 func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
 func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
 
 
-	SwiftyStoreKit.completeTransactions() { completedTransactions in
+	SwiftyStoreKit.completeTransactions(atomically: true) { products in
 	
 	
-	    for completedTransaction in completedTransactions {
+	    for product in products {
 	
 	
-	        if completedTransaction.transactionState == .purchased || completedTransaction.transactionState == .restored {
+	        if product.transaction.transactionState == .purchased || product.transaction.transactionState == .restored {
 	
 	
-	            print("purchased: \(completedTransaction.productId)")
+               if product.needsFinishTransaction {
+                   // Deliver content from server, then:
+                   SwiftyStoreKit.finishTransaction(product.transaction)
+               }
+               print("purchased: \(product)")
 	        }
 	        }
 	    }
 	    }
 	}
 	}
@@ -49,6 +55,8 @@ func application(application: UIApplication, didFinishLaunchingWithOptions launc
 
 
 If there are any pending transactions at this point, these will be reported by the completion block so that the app state and UI can be updated.
 If there are any pending transactions at this point, these will be reported by the completion block so that the app state and UI can be updated.
 
 
+## Purchases
+
 ### Retrieve products info
 ### Retrieve products info
 ```swift
 ```swift
 SwiftyStoreKit.retrieveProductsInfo(["com.musevisions.SwiftyStoreKit.Purchase1"]) { result in
 SwiftyStoreKit.retrieveProductsInfo(["com.musevisions.SwiftyStoreKit.Purchase1"]) { result in
@@ -64,13 +72,33 @@ SwiftyStoreKit.retrieveProductsInfo(["com.musevisions.SwiftyStoreKit.Purchase1"]
     }
     }
 }
 }
 ```
 ```
+
 ### Purchase a product
 ### Purchase a product
 
 
+* **Atomic**: to be used when the content is delivered immediately.
+
 ```swift
 ```swift
-SwiftyStoreKit.purchaseProduct("com.musevisions.SwiftyStoreKit.Purchase1") { result in
+SwiftyStoreKit.purchaseProduct("com.musevisions.SwiftyStoreKit.Purchase1", atomically: true) { result in
     switch result {
     switch result {
-    case .success(let productId):
-        print("Purchase Success: \(productId)")
+    case .success(let product):
+        print("Purchase Success: \(product.productId)")
+    case .error(let error):
+        print("Purchase Failed: \(error)")
+    }
+}
+```
+
+* **Non-Atomic**: to be used when the content is delivered by the server.
+
+```swift
+SwiftyStoreKit.purchaseProduct("com.musevisions.SwiftyStoreKit.Purchase1", atomically: false) { result in
+    switch result {
+    case .success(let product):
+        // fetch content from your server, then:
+        if product.needsFinishTransaction {
+            SwiftyStoreKit.finishTransaction(product.transaction)
+        }
+        print("Purchase Success: \(product.productId)")
     case .error(let error):
     case .error(let error):
         print("Purchase Failed: \(error)")
         print("Purchase Failed: \(error)")
     }
     }
@@ -79,13 +107,37 @@ SwiftyStoreKit.purchaseProduct("com.musevisions.SwiftyStoreKit.Purchase1") { res
 
 
 ### Restore previous purchases
 ### Restore previous purchases
 
 
+* **Atomic**: to be used when the content is delivered immediately.
+
+```swift
+SwiftyStoreKit.restorePurchases(atomically: true) { results in
+    if results.restoreFailedProducts.count > 0 {
+        print("Restore Failed: \(results.restoreFailedProducts)")
+    }
+    else if results.restoredProducts.count > 0 {
+        print("Restore Success: \(results.restoredProducts)")
+    }
+    else {
+        print("Nothing to Restore")
+    }
+}
+```
+
+* **Non-Atomic**: to be used when the content is delivered by the server.
+
 ```swift
 ```swift
-SwiftyStoreKit.restorePurchases() { results in
+SwiftyStoreKit.restorePurchases(atomically: false) { results in
     if results.restoreFailedProducts.count > 0 {
     if results.restoreFailedProducts.count > 0 {
         print("Restore Failed: \(results.restoreFailedProducts)")
         print("Restore Failed: \(results.restoreFailedProducts)")
     }
     }
-    else if results.restoredProductIds.count > 0 {
-        print("Restore Success: \(results.restoredProductIds)")
+    else if results.restoredProducts.count > 0 {
+        for product in results.restoredProducts {
+            // fetch content from your server, then:
+            if product.needsFinishTransaction {
+                SwiftyStoreKit.finishTransaction(product.transaction)
+            }
+        }
+        print("Restore Success: \(results.restoredProducts)")
     }
     }
     else {
     else {
         print("Nothing to Restore")
         print("Nothing to Restore")
@@ -93,6 +145,31 @@ SwiftyStoreKit.restorePurchases() { results in
 }
 }
 ```
 ```
 
 
+#### What does atomic / non-atomic mean?
+
+When you purchase a product the following things happen:
+
+* A payment is added to the payment queue for your IAP.
+* When the payment has been processed with Apple, the payment queue is updated so that the appropriate transaction can be handled.
+* If the transaction state is **purchased** or **restored**, the app can unlock the functionality purchased by the user.
+* The app should call `finishTransaction()` to complete the purchase.
+
+This is what is [recommended by Apple](https://developer.apple.com/reference/storekit/skpaymentqueue/1506003-finishtransaction):
+
+> Your application should call finishTransaction(_:) only after it has successfully processed the transaction and unlocked the functionality purchased by the user.
+
+* A purchase is **atomic** when the app unlocks the functionality purchased by the user immediately and call `finishTransaction()` at the same time. This is desirable if you're unlocking functionality that is already inside the app.
+
+* In cases when you need to make a request to your own server in order to unlock the functionality, you can use a **non-atomic** purchase instead.
+
+SwiftyStoreKit provides three operations that can be performed **atomically** or **non-atomically**:
+
+* Making a purchase
+* Restoring purchases
+* Completing transactions on app launch
+
+## Receipt verification
+
 ### Retrieve local receipt
 ### Retrieve local receipt
 
 
 ```swift
 ```swift

+ 8 - 4
SwiftyStoreKit-iOS-Demo/AppDelegate.swift

@@ -55,13 +55,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
     
     
     func completeIAPTransactions() {
     func completeIAPTransactions() {
         
         
-        SwiftyStoreKit.completeTransactions() { completedTransactions in
+        SwiftyStoreKit.completeTransactions(atomically: true) { products in
             
             
-            for completedTransaction in completedTransactions {
+            for product in products {
                 
                 
-                if completedTransaction.transactionState == .purchased || completedTransaction.transactionState == .restored {
+                if product.transaction.transactionState == .purchased || product.transaction.transactionState == .restored {
                     
                     
-                    print("purchased: \(completedTransaction.productId)")
+                    if product.needsFinishTransaction {
+                        // Deliver content from server, then:
+                        SwiftyStoreKit.finishTransaction(product.transaction)
+                    }
+                    print("purchased: \(product.productId)")
                 }
                 }
             }
             }
         }
         }

+ 26 - 13
SwiftyStoreKit-iOS-Demo/ViewController.swift

@@ -77,18 +77,31 @@ class ViewController: UIViewController {
     func purchase(_ purchase: RegisteredPurchase) {
     func purchase(_ purchase: RegisteredPurchase) {
         
         
         NetworkActivityIndicatorManager.networkOperationStarted()
         NetworkActivityIndicatorManager.networkOperationStarted()
-        SwiftyStoreKit.purchaseProduct(AppBundleId + "." + purchase.rawValue) { result in
+        SwiftyStoreKit.purchaseProduct(AppBundleId + "." + purchase.rawValue, atomically: true) { result in
             NetworkActivityIndicatorManager.networkOperationFinished()
             NetworkActivityIndicatorManager.networkOperationFinished()
             
             
+            if case .success(let product) = result {
+                // Deliver content from server, then:
+                if product.needsFinishTransaction {
+                    SwiftyStoreKit.finishTransaction(product.transaction)
+                }
+            }
             self.showAlert(self.alertForPurchaseResult(result))
             self.showAlert(self.alertForPurchaseResult(result))
         }
         }
     }
     }
+    
     @IBAction func restorePurchases() {
     @IBAction func restorePurchases() {
         
         
         NetworkActivityIndicatorManager.networkOperationStarted()
         NetworkActivityIndicatorManager.networkOperationStarted()
-        SwiftyStoreKit.restorePurchases() { results in
+        SwiftyStoreKit.restorePurchases(atomically: true) { results in
             NetworkActivityIndicatorManager.networkOperationFinished()
             NetworkActivityIndicatorManager.networkOperationFinished()
             
             
+            for product in results.restoredProducts {
+                // Deliver content from server, then:
+                if product.needsFinishTransaction {
+                    SwiftyStoreKit.finishTransaction(product.transaction)
+                }
+            }
             self.showAlert(self.alertForRestorePurchases(results))
             self.showAlert(self.alertForRestorePurchases(results))
         }
         }
     }
     }
@@ -176,7 +189,7 @@ extension ViewController {
         }
         }
     }
     }
 
 
-    func alertForProductRetrievalInfo(_ result: SwiftyStoreKit.RetrieveResults) -> UIAlertController {
+    func alertForProductRetrievalInfo(_ result: RetrieveResults) -> UIAlertController {
         
         
         if let product = result.retrievedProducts.first {
         if let product = result.retrievedProducts.first {
             let priceString = product.localizedPrice!
             let priceString = product.localizedPrice!
@@ -191,10 +204,10 @@ extension ViewController {
         }
         }
     }
     }
 
 
-    func alertForPurchaseResult(_ result: SwiftyStoreKit.PurchaseResult) -> UIAlertController {
+    func alertForPurchaseResult(_ result: PurchaseResult) -> UIAlertController {
         switch result {
         switch result {
-        case .success(let productId):
-            print("Purchase Success: \(productId)")
+        case .success(let product):
+            print("Purchase Success: \(product.productId)")
             return alertWithTitle("Thank You", message: "Purchase completed")
             return alertWithTitle("Thank You", message: "Purchase completed")
         case .error(let error):
         case .error(let error):
             print("Purchase Failed: \(error)")
             print("Purchase Failed: \(error)")
@@ -214,14 +227,14 @@ extension ViewController {
         }
         }
     }
     }
     
     
-    func alertForRestorePurchases(_ results: SwiftyStoreKit.RestoreResults) -> UIAlertController {
+    func alertForRestorePurchases(_ results: RestoreResults) -> UIAlertController {
 
 
         if results.restoreFailedProducts.count > 0 {
         if results.restoreFailedProducts.count > 0 {
             print("Restore Failed: \(results.restoreFailedProducts)")
             print("Restore Failed: \(results.restoreFailedProducts)")
             return alertWithTitle("Restore failed", message: "Unknown error. Please contact support")
             return alertWithTitle("Restore failed", message: "Unknown error. Please contact support")
         }
         }
-        else if results.restoredProductIds.count > 0 {
-            print("Restore Success: \(results.restoredProductIds)")
+        else if results.restoredProducts.count > 0 {
+            print("Restore Success: \(results.restoredProducts)")
             return alertWithTitle("Purchases Restored", message: "All purchases have been restored")
             return alertWithTitle("Purchases Restored", message: "All purchases have been restored")
         }
         }
         else {
         else {
@@ -231,7 +244,7 @@ extension ViewController {
     }
     }
 
 
 
 
-    func alertForVerifyReceipt(_ result: SwiftyStoreKit.VerifyReceiptResult) -> UIAlertController {
+    func alertForVerifyReceipt(_ result: VerifyReceiptResult) -> UIAlertController {
 
 
         switch result {
         switch result {
         case .success(let receipt):
         case .success(let receipt):
@@ -248,7 +261,7 @@ extension ViewController {
         }
         }
     }
     }
   
   
-    func alertForVerifySubscription(_ result: SwiftyStoreKit.VerifySubscriptionResult) -> UIAlertController {
+    func alertForVerifySubscription(_ result: VerifySubscriptionResult) -> UIAlertController {
     
     
         switch result {
         switch result {
         case .purchased(let expiresDate):
         case .purchased(let expiresDate):
@@ -263,7 +276,7 @@ extension ViewController {
         }
         }
     }
     }
 
 
-    func alertForVerifyPurchase(_ result: SwiftyStoreKit.VerifyPurchaseResult) -> UIAlertController {
+    func alertForVerifyPurchase(_ result: VerifyPurchaseResult) -> UIAlertController {
         
         
         switch result {
         switch result {
         case .purchased:
         case .purchased:
@@ -275,7 +288,7 @@ extension ViewController {
         }
         }
     }
     }
 
 
-    func alertForRefreshReceipt(_ result: SwiftyStoreKit.RefreshReceiptResult) -> UIAlertController {
+    func alertForRefreshReceipt(_ result: RefreshReceiptResult) -> UIAlertController {
         switch result {
         switch result {
         case .success(let receiptData):
         case .success(let receiptData):
             print("Receipt refresh Success: \(receiptData.base64EncodedString)")
             print("Receipt refresh Success: \(receiptData.base64EncodedString)")

+ 8 - 4
SwiftyStoreKit-macOS-Demo/AppDelegate.swift

@@ -35,13 +35,17 @@ class AppDelegate: NSObject, NSApplicationDelegate {
     
     
     func completeIAPTransactions() {
     func completeIAPTransactions() {
         
         
-        SwiftyStoreKit.completeTransactions() { completedTransactions in
+        SwiftyStoreKit.completeTransactions(atomically: true) { products in
             
             
-            for completedTransaction in completedTransactions {
+            for product in products {
                 
                 
-                if completedTransaction.transactionState == .purchased || completedTransaction.transactionState == .restored {
+                if product.transaction.transactionState == .purchased || product.transaction.transactionState == .restored {
                     
                     
-                    print("purchased: \(completedTransaction.productId)")
+                    if product.needsFinishTransaction {
+                        // Deliver content from server, then:
+                        SwiftyStoreKit.finishTransaction(product.transaction)
+                    }
+                    print("purchased: \(product.productId)")
                 }
                 }
             }
             }
         }
         }

+ 25 - 11
SwiftyStoreKit-macOS-Demo/ViewController.swift

@@ -75,7 +75,14 @@ class ViewController: NSViewController {
 
 
     func purchase(_ purchase: RegisteredPurchase) {
     func purchase(_ purchase: RegisteredPurchase) {
 
 
-        SwiftyStoreKit.purchaseProduct(AppBundleId + "." + purchase.rawValue) { result in
+        SwiftyStoreKit.purchaseProduct(AppBundleId + "." + purchase.rawValue, atomically: true) { result in
+
+            if case .success(let product) = result {
+                // Deliver content from server, then:
+                if product.needsFinishTransaction {
+                    SwiftyStoreKit.finishTransaction(product.transaction)
+                }
+            }
 
 
             self.showAlert(self.alertForPurchaseResult(result))
             self.showAlert(self.alertForPurchaseResult(result))
         }
         }
@@ -83,8 +90,15 @@ class ViewController: NSViewController {
 
 
     @IBAction func restorePurchases(_ sender: AnyObject?) {
     @IBAction func restorePurchases(_ sender: AnyObject?) {
 
 
-        SwiftyStoreKit.restorePurchases() { results in
+        SwiftyStoreKit.restorePurchases(atomically: true) { results in
             
             
+            for product in results.restoredProducts {
+                // Deliver content from server, then:
+                if product.needsFinishTransaction {
+                    SwiftyStoreKit.finishTransaction(product.transaction)
+                }
+            }
+
             self.showAlert(self.alertForRestorePurchases(results))
             self.showAlert(self.alertForRestorePurchases(results))
         }
         }
     }
     }
@@ -162,7 +176,7 @@ extension ViewController {
         }
         }
     }
     }
 
 
-    func alertForProductRetrievalInfo(_ result: SwiftyStoreKit.RetrieveResults) -> NSAlert {
+    func alertForProductRetrievalInfo(_ result: RetrieveResults) -> NSAlert {
         
         
         if let product = result.retrievedProducts.first {
         if let product = result.retrievedProducts.first {
             let priceString = product.localizedPrice!
             let priceString = product.localizedPrice!
@@ -177,7 +191,7 @@ extension ViewController {
         }
         }
     }
     }
     
     
-    func alertForPurchaseResult(_ result: SwiftyStoreKit.PurchaseResult) -> NSAlert {
+    func alertForPurchaseResult(_ result: PurchaseResult) -> NSAlert {
 
 
         switch result {
         switch result {
         case .success(let productId):
         case .success(let productId):
@@ -201,14 +215,14 @@ extension ViewController {
         }
         }
     }
     }
     
     
-    func alertForRestorePurchases(_ results: SwiftyStoreKit.RestoreResults) -> NSAlert {
+    func alertForRestorePurchases(_ results: RestoreResults) -> NSAlert {
         
         
         if results.restoreFailedProducts.count > 0 {
         if results.restoreFailedProducts.count > 0 {
             print("Restore Failed: \(results.restoreFailedProducts)")
             print("Restore Failed: \(results.restoreFailedProducts)")
             return alertWithTitle("Restore failed", message: "Unknown error. Please contact support")
             return alertWithTitle("Restore failed", message: "Unknown error. Please contact support")
         }
         }
-        else if results.restoredProductIds.count > 0 {
-            print("Restore Success: \(results.restoredProductIds)")
+        else if results.restoredProducts.count > 0 {
+            print("Restore Success: \(results.restoredProducts)")
             return alertWithTitle("Purchases Restored", message: "All purchases have been restored")
             return alertWithTitle("Purchases Restored", message: "All purchases have been restored")
         }
         }
         else {
         else {
@@ -217,7 +231,7 @@ extension ViewController {
         }
         }
     }
     }
     
     
-    func alertForVerifyReceipt(_ result: SwiftyStoreKit.VerifyReceiptResult) -> NSAlert {
+    func alertForVerifyReceipt(_ result: VerifyReceiptResult) -> NSAlert {
 
 
         switch result {
         switch result {
         case .success(let receipt):
         case .success(let receipt):
@@ -229,7 +243,7 @@ extension ViewController {
         }
         }
     }
     }
     
     
-    func alertForVerifySubscription(_ result: SwiftyStoreKit.VerifySubscriptionResult) -> NSAlert {
+    func alertForVerifySubscription(_ result: VerifySubscriptionResult) -> NSAlert {
         
         
         switch result {
         switch result {
         case .purchased(let expiresDate):
         case .purchased(let expiresDate):
@@ -245,7 +259,7 @@ extension ViewController {
     }
     }
 
 
 
 
-    func alertForVerifyPurchase(_ result: SwiftyStoreKit.VerifyPurchaseResult) -> NSAlert {
+    func alertForVerifyPurchase(_ result: VerifyPurchaseResult) -> NSAlert {
         
         
         switch result {
         switch result {
         case .purchased:
         case .purchased:
@@ -257,7 +271,7 @@ extension ViewController {
         }
         }
     }
     }
     
     
-    func alertForRefreshReceipt(_ result: SwiftyStoreKit.RefreshReceiptResult) -> NSAlert {
+    func alertForRefreshReceipt(_ result: RefreshReceiptResult) -> NSAlert {
         switch result {
         switch result {
         case .success(let receiptData):
         case .success(let receiptData):
             print("Receipt refresh Success: \(receiptData.base64EncodedString)")
             print("Receipt refresh Success: \(receiptData.base64EncodedString)")

+ 8 - 0
SwiftyStoreKit.xcodeproj/project.pbxproj

@@ -22,6 +22,9 @@
 		653722811DB8282600C8F944 /* SKProduct+LocalizedPrice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653722801DB8282600C8F944 /* SKProduct+LocalizedPrice.swift */; };
 		653722811DB8282600C8F944 /* SKProduct+LocalizedPrice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653722801DB8282600C8F944 /* SKProduct+LocalizedPrice.swift */; };
 		653722821DB8290A00C8F944 /* SKProduct+LocalizedPrice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653722801DB8282600C8F944 /* SKProduct+LocalizedPrice.swift */; };
 		653722821DB8290A00C8F944 /* SKProduct+LocalizedPrice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653722801DB8282600C8F944 /* SKProduct+LocalizedPrice.swift */; };
 		653722831DB8290B00C8F944 /* SKProduct+LocalizedPrice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653722801DB8282600C8F944 /* SKProduct+LocalizedPrice.swift */; };
 		653722831DB8290B00C8F944 /* SKProduct+LocalizedPrice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653722801DB8282600C8F944 /* SKProduct+LocalizedPrice.swift */; };
+		65BB6CE81DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */; };
+		65BB6CE91DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */; };
+		65BB6CEA1DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */; };
 		65F7DF711DCD4DF000835D30 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F7DF681DCD4DF000835D30 /* AppDelegate.swift */; };
 		65F7DF711DCD4DF000835D30 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F7DF681DCD4DF000835D30 /* AppDelegate.swift */; };
 		65F7DF721DCD4DF000835D30 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 65F7DF691DCD4DF000835D30 /* Assets.xcassets */; };
 		65F7DF721DCD4DF000835D30 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 65F7DF691DCD4DF000835D30 /* Assets.xcassets */; };
 		65F7DF731DCD4DF000835D30 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 65F7DF6A1DCD4DF000835D30 /* LaunchScreen.storyboard */; };
 		65F7DF731DCD4DF000835D30 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 65F7DF6A1DCD4DF000835D30 /* LaunchScreen.storyboard */; };
@@ -101,6 +104,7 @@
 		6502F62D1B985C40004E342D /* SwiftyStoreKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftyStoreKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		6502F62D1B985C40004E342D /* SwiftyStoreKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftyStoreKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		651A71241CD651AF000B4091 /* InAppCompleteTransactionsObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppCompleteTransactionsObserver.swift; sourceTree = "<group>"; };
 		651A71241CD651AF000B4091 /* InAppCompleteTransactionsObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppCompleteTransactionsObserver.swift; sourceTree = "<group>"; };
 		653722801DB8282600C8F944 /* SKProduct+LocalizedPrice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SKProduct+LocalizedPrice.swift"; sourceTree = "<group>"; };
 		653722801DB8282600C8F944 /* SKProduct+LocalizedPrice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SKProduct+LocalizedPrice.swift"; sourceTree = "<group>"; };
+		65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SwiftyStoreKit+Types.swift"; sourceTree = "<group>"; };
 		65F7DF681DCD4DF000835D30 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
 		65F7DF681DCD4DF000835D30 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
 		65F7DF691DCD4DF000835D30 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 		65F7DF691DCD4DF000835D30 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 		65F7DF6B1DCD4DF000835D30 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
 		65F7DF6B1DCD4DF000835D30 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
@@ -199,6 +203,7 @@
 				651A71241CD651AF000B4091 /* InAppCompleteTransactionsObserver.swift */,
 				651A71241CD651AF000B4091 /* InAppCompleteTransactionsObserver.swift */,
 				653722801DB8282600C8F944 /* SKProduct+LocalizedPrice.swift */,
 				653722801DB8282600C8F944 /* SKProduct+LocalizedPrice.swift */,
 				6502F6241B98586A004E342D /* SwiftyStoreKit.swift */,
 				6502F6241B98586A004E342D /* SwiftyStoreKit.swift */,
+				65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */,
 				C40C680F1C29414C00B60B7E /* OS.swift */,
 				C40C680F1C29414C00B60B7E /* OS.swift */,
 				65F7DF931DCD536100835D30 /* Platforms */,
 				65F7DF931DCD536100835D30 /* Platforms */,
 			);
 			);
@@ -474,6 +479,7 @@
 				54B069931CF742D300BAFE38 /* InAppReceiptRefreshRequest.swift in Sources */,
 				54B069931CF742D300BAFE38 /* InAppReceiptRefreshRequest.swift in Sources */,
 				653722831DB8290B00C8F944 /* SKProduct+LocalizedPrice.swift in Sources */,
 				653722831DB8290B00C8F944 /* SKProduct+LocalizedPrice.swift in Sources */,
 				54B069921CF742D100BAFE38 /* InAppReceipt.swift in Sources */,
 				54B069921CF742D100BAFE38 /* InAppReceipt.swift in Sources */,
+				65BB6CEA1DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */,
 				54B069941CF742D600BAFE38 /* InAppProductQueryRequest.swift in Sources */,
 				54B069941CF742D600BAFE38 /* InAppProductQueryRequest.swift in Sources */,
 			);
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			runOnlyForDeploymentPostprocessing = 0;
@@ -499,6 +505,7 @@
 				C4083C571C2AB0A900295248 /* InAppReceiptRefreshRequest.swift in Sources */,
 				C4083C571C2AB0A900295248 /* InAppReceiptRefreshRequest.swift in Sources */,
 				653722811DB8282600C8F944 /* SKProduct+LocalizedPrice.swift in Sources */,
 				653722811DB8282600C8F944 /* SKProduct+LocalizedPrice.swift in Sources */,
 				C4A7C7631C29B8D00053ED64 /* InAppReceipt.swift in Sources */,
 				C4A7C7631C29B8D00053ED64 /* InAppReceipt.swift in Sources */,
+				65BB6CE81DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */,
 				6502F63C1B985CA4004E342D /* SwiftyStoreKit.swift in Sources */,
 				6502F63C1B985CA4004E342D /* SwiftyStoreKit.swift in Sources */,
 			);
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			runOnlyForDeploymentPostprocessing = 0;
@@ -514,6 +521,7 @@
 				C4F69A8A1C2E0D21009DD8BD /* InAppReceiptRefreshRequest.swift in Sources */,
 				C4F69A8A1C2E0D21009DD8BD /* InAppReceiptRefreshRequest.swift in Sources */,
 				653722821DB8290A00C8F944 /* SKProduct+LocalizedPrice.swift in Sources */,
 				653722821DB8290A00C8F944 /* SKProduct+LocalizedPrice.swift in Sources */,
 				C4083C551C2AADB500295248 /* InAppReceipt.swift in Sources */,
 				C4083C551C2AADB500295248 /* InAppReceipt.swift in Sources */,
+				65BB6CE91DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */,
 				C4D74BC51C24CEDC0071AD3E /* SwiftyStoreKit.swift in Sources */,
 				C4D74BC51C24CEDC0071AD3E /* SwiftyStoreKit.swift in Sources */,
 			);
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			runOnlyForDeploymentPostprocessing = 0;

+ 8 - 5
SwiftyStoreKit/InAppCompleteTransactionsObserver.swift

@@ -42,20 +42,23 @@ class InAppCompleteTransactionsObserver: NSObject, SKPaymentTransactionObserver
     
     
     private var callbackCalled: Bool = false
     private var callbackCalled: Bool = false
         
         
-    typealias TransactionsCallback = ([SwiftyStoreKit.CompletedTransaction]) -> ()
+    typealias TransactionsCallback = ([Product]) -> ()
     
     
     var paymentQueue: SKPaymentQueue {
     var paymentQueue: SKPaymentQueue {
         return SKPaymentQueue.default()
         return SKPaymentQueue.default()
     }
     }
 
 
+    let atomically: Bool
+    
     deinit {
     deinit {
         paymentQueue.remove(self)
         paymentQueue.remove(self)
     }
     }
 
 
     let callback: TransactionsCallback
     let callback: TransactionsCallback
     
     
-    init(callback: @escaping TransactionsCallback) {
+    init(atomically: Bool, callback: @escaping TransactionsCallback) {
     
     
+        self.atomically = atomically
         self.callback = callback
         self.callback = callback
         super.init()
         super.init()
         paymentQueue.add(self)
         paymentQueue.add(self)
@@ -70,7 +73,7 @@ class InAppCompleteTransactionsObserver: NSObject, SKPaymentTransactionObserver
             return
             return
         }
         }
         
         
-        var completedTransactions: [SwiftyStoreKit.CompletedTransaction] = []
+        var completedTransactions: [Product] = []
         
         
         for transaction in transactions {
         for transaction in transactions {
             
             
@@ -78,9 +81,9 @@ class InAppCompleteTransactionsObserver: NSObject, SKPaymentTransactionObserver
 
 
             if transactionState != .purchasing {
             if transactionState != .purchasing {
                 
                 
-                let completedTransaction = SwiftyStoreKit.CompletedTransaction(productId: transaction.payment.productIdentifier, transactionState: transactionState)
+                let product = Product(productId: transaction.payment.productIdentifier, transaction: transaction, needsFinishTransaction: !atomically)
                 
                 
-                completedTransactions.append(completedTransaction)
+                completedTransactions.append(product)
                 
                 
                 print("Finishing transaction for payment \"\(transaction.payment.productIdentifier)\" with state: \(transactionState.stringValue)")
                 print("Finishing transaction for payment \"\(transaction.payment.productIdentifier)\" with state: \(transactionState.stringValue)")
                 
                 

+ 28 - 12
SwiftyStoreKit/InAppProductPurchaseRequest.swift

@@ -28,44 +28,54 @@ import Foundation
 class InAppProductPurchaseRequest: NSObject, SKPaymentTransactionObserver {
 class InAppProductPurchaseRequest: NSObject, SKPaymentTransactionObserver {
 
 
     enum TransactionResult {
     enum TransactionResult {
-        case purchased(productId: String)
-        case restored(productId: String)
+        case purchased(product: Product)
+        case restored(product: Product)
         case failed(error: Error)
         case failed(error: Error)
     }
     }
     
     
     typealias RequestCallback = ([TransactionResult]) -> ()
     typealias RequestCallback = ([TransactionResult]) -> ()
     private let callback: RequestCallback
     private let callback: RequestCallback
     private var purchases : [SKPaymentTransactionState: [String]] = [:]
     private var purchases : [SKPaymentTransactionState: [String]] = [:]
-
+    
     var paymentQueue: SKPaymentQueue {
     var paymentQueue: SKPaymentQueue {
         return SKPaymentQueue.default()
         return SKPaymentQueue.default()
     }
     }
     
     
     let product : SKProduct?
     let product : SKProduct?
+    let atomically: Bool
     
     
     deinit {
     deinit {
         paymentQueue.remove(self)
         paymentQueue.remove(self)
     }
     }
     // Initialiser for product purchase
     // Initialiser for product purchase
-    private init(product: SKProduct?, callback: @escaping RequestCallback) {
+    private init(product: SKProduct?, atomically: Bool, callback: @escaping RequestCallback) {
 
 
+        self.atomically = atomically
         self.product = product
         self.product = product
         self.callback = callback
         self.callback = callback
         super.init()
         super.init()
         paymentQueue.add(self)
         paymentQueue.add(self)
     }
     }
     // MARK: Public methods
     // MARK: Public methods
-    class func startPayment(_ product: SKProduct, applicationUsername: String = "", callback: @escaping RequestCallback) -> InAppProductPurchaseRequest {
-        let request = InAppProductPurchaseRequest(product: product, callback: callback)
+    class func startPayment(product: SKProduct, atomically: Bool, applicationUsername: String = "", callback: @escaping RequestCallback) -> InAppProductPurchaseRequest {
+        let request = InAppProductPurchaseRequest(product: product, atomically: atomically, callback: callback)
         request.startPayment(product, applicationUsername: applicationUsername)
         request.startPayment(product, applicationUsername: applicationUsername)
         return request
         return request
     }
     }
-    class func restorePurchases(_ callback: @escaping RequestCallback) -> InAppProductPurchaseRequest {
-        let request = InAppProductPurchaseRequest(product: nil, callback: callback)
+    class func restorePurchases(atomically: Bool, callback: @escaping RequestCallback) -> InAppProductPurchaseRequest {
+        let request = InAppProductPurchaseRequest(product: nil, atomically: atomically, callback: callback)
         request.startRestorePurchases()
         request.startRestorePurchases()
         return request
         return request
     }
     }
     
     
+    class func finishTransaction(_ transaction: PaymentTransaction) {
+        guard let skTransaction = transaction as? SKPaymentTransaction else {
+            print("Object is not a SKPaymentTransaction: \(transaction)")
+            return
+        }
+        SKPaymentQueue.default().finishTransaction(skTransaction)
+    }
+    
     // MARK: Private methods
     // MARK: Private methods
     private func startPayment(_ product: SKProduct, applicationUsername: String = "") {
     private func startPayment(_ product: SKProduct, applicationUsername: String = "") {
         guard let _ = product._productIdentifier else {
         guard let _ = product._productIdentifier else {
@@ -109,8 +119,11 @@ class InAppProductPurchaseRequest: NSObject, SKPaymentTransactionObserver {
             switch transactionState {
             switch transactionState {
             case .purchased:
             case .purchased:
                 if isPurchaseRequest {
                 if isPurchaseRequest {
-                    transactionResults.append(.purchased(productId: transactionProductIdentifier))
-                    paymentQueue.finishTransaction(transaction)
+                    let product = Product(productId: transactionProductIdentifier, transaction: transaction, needsFinishTransaction: !atomically)
+                    transactionResults.append(.purchased(product: product))
+                    if atomically {
+                        paymentQueue.finishTransaction(transaction)
+                    }
                 }
                 }
             case .failed:
             case .failed:
                 // TODO: How to discriminate between purchase and restore?
                 // TODO: How to discriminate between purchase and restore?
@@ -122,8 +135,11 @@ class InAppProductPurchaseRequest: NSObject, SKPaymentTransactionObserver {
                 paymentQueue.finishTransaction(transaction)
                 paymentQueue.finishTransaction(transaction)
             case .restored:
             case .restored:
                 if !isPurchaseRequest {
                 if !isPurchaseRequest {
-                    transactionResults.append(.restored(productId: transactionProductIdentifier))
-                    paymentQueue.finishTransaction(transaction)
+                    let product = Product(productId: transactionProductIdentifier, transaction: transaction, needsFinishTransaction: !atomically)
+                    transactionResults.append(.restored(product: product))
+                    if atomically {
+                        paymentQueue.finishTransaction(transaction)
+                    }
                 }
                 }
             case .purchasing:
             case .purchasing:
                 // In progress: do nothing
                 // In progress: do nothing

+ 3 - 3
SwiftyStoreKit/InAppProductQueryRequest.swift

@@ -26,7 +26,7 @@ import StoreKit
 
 
 class InAppProductQueryRequest: NSObject, SKProductsRequestDelegate {
 class InAppProductQueryRequest: NSObject, SKProductsRequestDelegate {
 
 
-    typealias RequestCallback = (SwiftyStoreKit.RetrieveResults) -> ()
+    typealias RequestCallback = (RetrieveResults) -> ()
     private let callback: RequestCallback
     private let callback: RequestCallback
     private let request: SKProductsRequest
     private let request: SKProductsRequest
     // http://stackoverflow.com/questions/24011575/what-is-the-difference-between-a-weak-reference-and-an-unowned-reference
     // http://stackoverflow.com/questions/24011575/what-is-the-difference-between-a-weak-reference-and-an-unowned-reference
@@ -65,7 +65,7 @@ class InAppProductQueryRequest: NSObject, SKProductsRequestDelegate {
             
             
             let retrievedProducts = Set<SKProduct>(response.products)
             let retrievedProducts = Set<SKProduct>(response.products)
             let invalidProductIDs = Set<String>(response.invalidProductIdentifiers)
             let invalidProductIDs = Set<String>(response.invalidProductIdentifiers)
-            self.callback(SwiftyStoreKit.RetrieveResults(retrievedProducts: retrievedProducts,
+            self.callback(RetrieveResults(retrievedProducts: retrievedProducts,
                 invalidProductIDs: invalidProductIDs, error: nil))
                 invalidProductIDs: invalidProductIDs, error: nil))
         }
         }
     }
     }
@@ -80,7 +80,7 @@ class InAppProductQueryRequest: NSObject, SKProductsRequestDelegate {
 
 
     func requestFailed(_ error: Error){
     func requestFailed(_ error: Error){
         DispatchQueue.main.async {
         DispatchQueue.main.async {
-            self.callback(SwiftyStoreKit.RetrieveResults(retrievedProducts: Set<SKProduct>(), invalidProductIDs: Set<String>(), error: error))
+            self.callback(RetrieveResults(retrievedProducts: Set<SKProduct>(), invalidProductIDs: Set<String>(), error: error))
         }
         }
     }
     }
 }
 }

+ 3 - 123
SwiftyStoreKit/InAppReceipt.swift

@@ -25,126 +25,6 @@
 
 
 import Foundation
 import Foundation
 
 
-// Info for receipt returned by server
-public typealias ReceiptInfo = [String: AnyObject]
-
-// MARK: - Enumeration
-extension SwiftyStoreKit {
-    public enum VerifyReceiptResult {
-        case success(receipt: ReceiptInfo)
-        case error(error: ReceiptError)
-    }
-  
-    // Result for Consumable and NonConsumable
-    public enum VerifyPurchaseResult {
-        case purchased
-        case notPurchased
-    }
-  
-    //  Result for Subscription
-    public enum VerifySubscriptionResult {
-        case purchased(expiryDate: Date)
-        case expired(expiryDate: Date)
-        case notPurchased
-    }
-}
-
-// Error when managing receipt
-public enum ReceiptError: Swift.Error {
-    // No receipt data
-    case noReceiptData
-    // No data receice
-    case noRemoteData
-    // Error when encoding HTTP body into JSON
-    case requestBodyEncodeError(error: Swift.Error)
-    // Error when proceeding request
-    case networkError(error: Swift.Error)
-    // Error when decoding response
-    case jsonDecodeError(string: String?)
-    // Receive invalid - bad status returned
-    case receiptInvalid(receipt: ReceiptInfo, status: ReceiptStatus)
-}
-
-// Status code returned by remote server
-// see Table 2-1  Status codes
-public enum ReceiptStatus: Int {
-    // Not decodable status
-    case unknown = -2
-    // No status returned
-    case none = -1
-    // valid statu
-    case valid = 0
-    // The App Store could not read the JSON object you provided.
-    case jsonNotReadable = 21000
-    // The data in the receipt-data property was malformed or missing.
-    case malformedOrMissingData = 21002
-    // The receipt could not be authenticated.
-    case receiptCouldNotBeAuthenticated = 21003
-    // The shared secret you provided does not match the shared secret on file for your account.
-    case secretNotMatching = 21004
-    // The receipt server is not currently available.
-    case receiptServerUnavailable = 21005
-    // This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.
-    case subscriptionExpired = 21006
-    //  This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.
-    case testReceipt = 21007
-    // This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead.
-    case productionEnvironment = 21008
-
-    var isValid: Bool { return self == .valid}
-}
-
-// Receipt field as defined in : https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1
-public enum ReceiptInfoField: String {
-     // Bundle Identifier. This corresponds to the value of CFBundleIdentifier in the Info.plist file.
-    case bundle_id
-    // The app’s version number.This corresponds to the value of CFBundleVersion (in iOS) or CFBundleShortVersionString (in OS X) in the Info.plist.
-    case application_version
-    // The version of the app that was originally purchased. This corresponds to the value of CFBundleVersion (in iOS) or CFBundleShortVersionString (in OS X) in the Info.plist file when the purchase was originally made.
-    case original_application_version
-    // The date when the app receipt was created.
-    case creation_date
-    // The date that the app receipt expires. This key is present only for apps purchased through the Volume Purchase Program.
-    case expiration_date
-
-    // The receipt for an in-app purchase.
-    case in_app
-
-    public enum InApp: String {
-        // The number of items purchased. This value corresponds to the quantity property of the SKPayment object stored in the transaction’s payment property.
-        case quantity
-        // The product identifier of the item that was purchased. This value corresponds to the productIdentifier property of the SKPayment object stored in the transaction’s payment property.
-        case product_id
-        // The transaction identifier of the item that was purchased. This value corresponds to the transaction’s transactionIdentifier property.
-        case transaction_id
-        // For a transaction that restores a previous transaction, the transaction identifier of the original transaction. Otherwise, identical to the transaction identifier. This value corresponds to the original transaction’s transactionIdentifier property. All receipts in a chain of renewals for an auto-renewable subscription have the same value for this field.
-        case original_transaction_id
-        // The date and time that the item was purchased. This value corresponds to the transaction’s transactionDate property.
-        case purchase_date
-        // For a transaction that restores a previous transaction, the date of the original transaction. This value corresponds to the original transaction’s transactionDate property. In an auto-renewable subscription receipt, this indicates the beginning of the subscription period, even if the subscription has been renewed.
-        case original_purchase_date
-        // The expiration date for the subscription, expressed as the number of milliseconds since January 1, 1970, 00:00:00 GMT. This key is only present for auto-renewable subscription receipts.
-        case expires_date
-        // For a transaction that was canceled by Apple customer support, the time and date of the cancellation. Treat a canceled receipt the same as if no purchase had ever been made.
-        case cancellation_date
-        #if os(iOS) || os(tvOS)
-        // A string that the App Store uses to uniquely identify the application that created the transaction. If your server supports multiple applications, you can use this value to differentiate between them. Apps are assigned an identifier only in the production environment, so this key is not present for receipts created in the test environment. This field is not present for Mac apps. See also Bundle Identifier.
-        case app_item_id
-        #endif
-        // An arbitrary number that uniquely identifies a revision of your application. This key is not present for receipts created in the test environment.
-        case version_external_identifier
-        // The primary key for identifying subscription purchases.
-        case web_order_line_item_id
-    }
-}
-
-#if os(OSX)
-    public enum ReceiptExitCode: Int32 {
-        // If validation fails in OS X, call exit with a status of 173. This exit status notifies the system that your application has determined that its receipt is invalid. At this point, the system attempts to obtain a valid receipt and may prompt for the user’s iTunes credentials
-        case notValid = 173
-    }
-#endif
-
 // MARK - receipt mangement
 // MARK - receipt mangement
 internal class InAppReceipt {
 internal class InAppReceipt {
 
 
@@ -182,7 +62,7 @@ internal class InAppReceipt {
         urlType: VerifyReceiptURLType = .production,
         urlType: VerifyReceiptURLType = .production,
         password autoRenewPassword: String? = nil,
         password autoRenewPassword: String? = nil,
         session: URLSession = URLSession.shared,
         session: URLSession = URLSession.shared,
-        completion: @escaping (SwiftyStoreKit.VerifyReceiptResult) -> ()) {
+        completion: @escaping (VerifyReceiptResult) -> ()) {
 
 
             // If no receipt is present, validation fails.
             // If no receipt is present, validation fails.
             guard let base64EncodedString = appStoreReceiptBase64Encoded else {
             guard let base64EncodedString = appStoreReceiptBase64Encoded else {
@@ -275,7 +155,7 @@ internal class InAppReceipt {
     class func verifyPurchase(
     class func verifyPurchase(
         productId: String,
         productId: String,
         inReceipt receipt: ReceiptInfo
         inReceipt receipt: ReceiptInfo
-    ) -> SwiftyStoreKit.VerifyPurchaseResult {
+    ) -> VerifyPurchaseResult {
       
       
         // Get receipts info for the product
         // Get receipts info for the product
         let receiptsInfo = getReceiptsInfo(forProductId: productId, inReceipt: receipt)
         let receiptsInfo = getReceiptsInfo(forProductId: productId, inReceipt: receipt)
@@ -297,7 +177,7 @@ internal class InAppReceipt {
         inReceipt receipt: ReceiptInfo,
         inReceipt receipt: ReceiptInfo,
         validUntil date: Date = Date(),
         validUntil date: Date = Date(),
         validDuration duration: TimeInterval? = nil
         validDuration duration: TimeInterval? = nil
-    ) -> SwiftyStoreKit.VerifySubscriptionResult {
+    ) -> VerifySubscriptionResult {
       
       
         // Verify that at least one receipt has the right product id
         // Verify that at least one receipt has the right product id
         let receiptsInfo = getReceiptsInfo(forProductId: productId, inReceipt: receipt)
         let receiptsInfo = getReceiptsInfo(forProductId: productId, inReceipt: receipt)

+ 196 - 0
SwiftyStoreKit/SwiftyStoreKit+Types.swift

@@ -0,0 +1,196 @@
+//
+// SwiftyStoreKit+Types.swift
+// SwiftyStoreKit
+//
+// Copyright (c) 2015 Andrea Bizzotto (bizz84@gmail.com)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+import StoreKit
+
+// MARK: Purchases
+
+// Purchased or restored product
+public struct Product {
+    public let productId: String
+    public let transaction: PaymentTransaction
+    public let needsFinishTransaction: Bool
+}
+
+// Payment transaction
+public protocol PaymentTransaction {
+    var transactionState: SKPaymentTransactionState { get }
+    var transactionIdentifier: String? { get }
+}
+
+// Add PaymentTransaction conformance to SKPaymentTransaction
+extension SKPaymentTransaction : PaymentTransaction { }
+
+// Products information
+public struct RetrieveResults {
+    public let retrievedProducts: Set<SKProduct>
+    public let invalidProductIDs: Set<String>
+    public let error: Error?
+}
+
+// Purchase error types
+public enum PurchaseError {
+    case failed(error: Error)
+    case invalidProductId(productId: String)
+    case noProductIdentifier
+    case paymentNotAllowed
+}
+
+// Purchase result
+public enum PurchaseResult {
+    case success(product: Product)
+    case error(error: PurchaseError)
+}
+
+// Restore purchase results
+public struct RestoreResults {
+    public let restoredProducts: [Product]
+    public let restoreFailedProducts: [(Swift.Error, String?)]
+}
+
+// MARK: Receipt verification
+
+// Info for receipt returned by server
+public typealias ReceiptInfo = [String: AnyObject]
+
+// Refresh receipt result
+public enum RefreshReceiptResult {
+    case success(receiptData: Data)
+    case error(error: Error)
+}
+
+// Verify receipt result
+public enum VerifyReceiptResult {
+    case success(receipt: ReceiptInfo)
+    case error(error: ReceiptError)
+}
+
+// Result for Consumable and NonConsumable
+public enum VerifyPurchaseResult {
+    case purchased
+    case notPurchased
+}
+
+// Verify subscription result
+public enum VerifySubscriptionResult {
+    case purchased(expiryDate: Date)
+    case expired(expiryDate: Date)
+    case notPurchased
+}
+
+// Error when managing receipt
+public enum ReceiptError: Swift.Error {
+    // No receipt data
+    case noReceiptData
+    // No data receice
+    case noRemoteData
+    // Error when encoding HTTP body into JSON
+    case requestBodyEncodeError(error: Swift.Error)
+    // Error when proceeding request
+    case networkError(error: Swift.Error)
+    // Error when decoding response
+    case jsonDecodeError(string: String?)
+    // Receive invalid - bad status returned
+    case receiptInvalid(receipt: ReceiptInfo, status: ReceiptStatus)
+}
+
+// Status code returned by remote server
+// see Table 2-1  Status codes
+public enum ReceiptStatus: Int {
+    // Not decodable status
+    case unknown = -2
+    // No status returned
+    case none = -1
+    // valid statu
+    case valid = 0
+    // The App Store could not read the JSON object you provided.
+    case jsonNotReadable = 21000
+    // The data in the receipt-data property was malformed or missing.
+    case malformedOrMissingData = 21002
+    // The receipt could not be authenticated.
+    case receiptCouldNotBeAuthenticated = 21003
+    // The shared secret you provided does not match the shared secret on file for your account.
+    case secretNotMatching = 21004
+    // The receipt server is not currently available.
+    case receiptServerUnavailable = 21005
+    // This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.
+    case subscriptionExpired = 21006
+    //  This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.
+    case testReceipt = 21007
+    // This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead.
+    case productionEnvironment = 21008
+    
+    var isValid: Bool { return self == .valid}
+}
+
+// Receipt field as defined in : https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1
+public enum ReceiptInfoField: String {
+    // Bundle Identifier. This corresponds to the value of CFBundleIdentifier in the Info.plist file.
+    case bundle_id
+    // The app’s version number.This corresponds to the value of CFBundleVersion (in iOS) or CFBundleShortVersionString (in OS X) in the Info.plist.
+    case application_version
+    // The version of the app that was originally purchased. This corresponds to the value of CFBundleVersion (in iOS) or CFBundleShortVersionString (in OS X) in the Info.plist file when the purchase was originally made.
+    case original_application_version
+    // The date when the app receipt was created.
+    case creation_date
+    // The date that the app receipt expires. This key is present only for apps purchased through the Volume Purchase Program.
+    case expiration_date
+    
+    // The receipt for an in-app purchase.
+    case in_app
+    
+    public enum InApp: String {
+        // The number of items purchased. This value corresponds to the quantity property of the SKPayment object stored in the transaction’s payment property.
+        case quantity
+        // The product identifier of the item that was purchased. This value corresponds to the productIdentifier property of the SKPayment object stored in the transaction’s payment property.
+        case product_id
+        // The transaction identifier of the item that was purchased. This value corresponds to the transaction’s transactionIdentifier property.
+        case transaction_id
+        // For a transaction that restores a previous transaction, the transaction identifier of the original transaction. Otherwise, identical to the transaction identifier. This value corresponds to the original transaction’s transactionIdentifier property. All receipts in a chain of renewals for an auto-renewable subscription have the same value for this field.
+        case original_transaction_id
+        // The date and time that the item was purchased. This value corresponds to the transaction’s transactionDate property.
+        case purchase_date
+        // For a transaction that restores a previous transaction, the date of the original transaction. This value corresponds to the original transaction’s transactionDate property. In an auto-renewable subscription receipt, this indicates the beginning of the subscription period, even if the subscription has been renewed.
+        case original_purchase_date
+        // The expiration date for the subscription, expressed as the number of milliseconds since January 1, 1970, 00:00:00 GMT. This key is only present for auto-renewable subscription receipts.
+        case expires_date
+        // For a transaction that was canceled by Apple customer support, the time and date of the cancellation. Treat a canceled receipt the same as if no purchase had ever been made.
+        case cancellation_date
+        #if os(iOS) || os(tvOS)
+        // A string that the App Store uses to uniquely identify the application that created the transaction. If your server supports multiple applications, you can use this value to differentiate between them. Apps are assigned an identifier only in the production environment, so this key is not present for receipts created in the test environment. This field is not present for Mac apps. See also Bundle Identifier.
+        case app_item_id
+        #endif
+        // An arbitrary number that uniquely identifies a revision of your application. This key is not present for receipts created in the test environment.
+        case version_external_identifier
+        // The primary key for identifying subscription purchases.
+        case web_order_line_item_id
+    }
+}
+
+#if os(OSX)
+    public enum ReceiptExitCode: Int32 {
+        // If validation fails in OS X, call exit with a status of 173. This exit status notifies the system that your application has determined that its receipt is invalid. At this point, the system attempts to obtain a valid receipt and may prompt for the user’s iTunes credentials
+        case notValid = 173
+    }
+#endif

+ 27 - 50
SwiftyStoreKit/SwiftyStoreKit.swift

@@ -55,37 +55,8 @@ public class SwiftyStoreKit {
     #if os(iOS) || os(tvOS)
     #if os(iOS) || os(tvOS)
     private var receiptRefreshRequest: InAppReceiptRefreshRequest?
     private var receiptRefreshRequest: InAppReceiptRefreshRequest?
     #endif
     #endif
-    // MARK: Enums
-    public struct RetrieveResults {
-        public let retrievedProducts: Set<SKProduct>
-        public let invalidProductIDs: Set<String>
-        public let error: Error?
-    }
-
-    public enum PurchaseError {
-        case failed(error: Error)
-        case invalidProductId(productId: String)
-        case noProductIdentifier
-        case paymentNotAllowed
-    }
-    public enum PurchaseResult {
-        case success(productId: String)
-        case error(error: PurchaseError)
-    }
-    public struct RestoreResults {
-        public let restoredProductIds: [String]
-        public let restoreFailedProducts: [(Swift.Error, String?)]
-    }
-    public enum RefreshReceiptResult {
-        case success(receiptData: Data)
-        case error(error: Error)
-    }
-    public struct CompletedTransaction {
-        public let productId: String
-        public let transactionState: SKPaymentTransactionState
-    }
-
-    public enum InternalErrorCode: Int {
+    
+    private enum InternalErrorCode: Int {
         case restoredPurchaseWhenPurchasing = 0
         case restoredPurchaseWhenPurchasing = 0
         case purchasedWhenRestoringPurchase = 1
         case purchasedWhenRestoringPurchase = 1
     }
     }
@@ -101,8 +72,8 @@ public class SwiftyStoreKit {
         return sharedInstance.inflightPurchases.count > 0 || sharedInstance.restoreRequest != nil
         return sharedInstance.inflightPurchases.count > 0 || sharedInstance.restoreRequest != nil
     }
     }
     
     
-    public class func completeTransactions(_ completion: @escaping ([CompletedTransaction]) -> ()) {
-        sharedInstance.completeTransactionsObserver = InAppCompleteTransactionsObserver(callback: completion)
+    public class func completeTransactions(atomically: Bool = true, completion: @escaping ([Product]) -> ()) {
+        sharedInstance.completeTransactionsObserver = InAppCompleteTransactionsObserver(atomically: atomically, callback: completion)
     }
     }
     
     
     // MARK: Public methods
     // MARK: Public methods
@@ -119,18 +90,19 @@ public class SwiftyStoreKit {
     /**
     /**
      *  Purchase a product
      *  Purchase a product
      *  - Parameter productId: productId as specified in iTunes Connect
      *  - Parameter productId: productId as specified in iTunes Connect
+     *  - Parameter atomically: whether the product is purchased atomically (e.g. finishTransaction is called immediately)
      *  - Parameter applicationUsername: an opaque identifier for the user’s account on your system
      *  - Parameter applicationUsername: an opaque identifier for the user’s account on your system
      *  - Parameter completion: handler for result
      *  - Parameter completion: handler for result
      */
      */
-    public class func purchaseProduct(_ productId: String, applicationUsername: String = "", completion: @escaping ( PurchaseResult) -> ()) {
+    public class func purchaseProduct(_ productId: String, atomically: Bool = true, applicationUsername: String = "", completion: @escaping ( PurchaseResult) -> ()) {
         
         
         if let product = sharedInstance.store.products[productId] {
         if let product = sharedInstance.store.products[productId] {
-            sharedInstance.purchase(product: product, applicationUsername: applicationUsername, completion: completion)
+            sharedInstance.purchase(product: product, atomically: atomically, applicationUsername: applicationUsername, completion: completion)
         }
         }
         else {
         else {
             retrieveProductsInfo(Set([productId])) { result -> () in
             retrieveProductsInfo(Set([productId])) { result -> () in
                 if let product = result.retrievedProducts.first {
                 if let product = result.retrievedProducts.first {
-                    sharedInstance.purchase(product: product, applicationUsername: applicationUsername, completion: completion)
+                    sharedInstance.purchase(product: product, atomically: atomically, applicationUsername: applicationUsername, completion: completion)
                 }
                 }
                 else if let error = result.error {
                 else if let error = result.error {
                     completion(.error(error: .failed(error: error)))
                     completion(.error(error: .failed(error: error)))
@@ -142,15 +114,20 @@ public class SwiftyStoreKit {
         }
         }
     }
     }
     
     
-    public class func restorePurchases(_ completion: @escaping (RestoreResults) -> ()) {
+    public class func restorePurchases(atomically: Bool = true, completion: @escaping (RestoreResults) -> ()) {
 
 
-        sharedInstance.restoreRequest = InAppProductPurchaseRequest.restorePurchases() { results in
+        sharedInstance.restoreRequest = InAppProductPurchaseRequest.restorePurchases(atomically: atomically) { results in
         
         
             sharedInstance.restoreRequest = nil
             sharedInstance.restoreRequest = nil
             let results = sharedInstance.processRestoreResults(results)
             let results = sharedInstance.processRestoreResults(results)
             completion(results)
             completion(results)
         }
         }
     }
     }
+    
+    public class func finishTransaction(_ transaction: PaymentTransaction) {
+     
+        InAppProductPurchaseRequest.finishTransaction(transaction)
+    }
 
 
     /**
     /**
      * Return receipt data from the application bundle. This is read from Bundle.main.appStoreReceiptURL
      * Return receipt data from the application bundle. This is read from Bundle.main.appStoreReceiptURL
@@ -235,7 +212,7 @@ public class SwiftyStoreKit {
     #endif
     #endif
 
 
     // MARK: private methods
     // MARK: private methods
-    private func purchase(product: SKProduct, applicationUsername: String = "", completion: @escaping (PurchaseResult) -> ()) {
+    private func purchase(product: SKProduct, atomically: Bool, applicationUsername: String = "", completion: @escaping (PurchaseResult) -> ()) {
         guard SwiftyStoreKit.canMakePayments else {
         guard SwiftyStoreKit.canMakePayments else {
             completion(.error(error: .paymentNotAllowed))
             completion(.error(error: .paymentNotAllowed))
             return
             return
@@ -245,7 +222,7 @@ public class SwiftyStoreKit {
             return
             return
         }
         }
 
 
-        inflightPurchases[productIdentifier] = InAppProductPurchaseRequest.startPayment(product, applicationUsername: applicationUsername) { results in
+        inflightPurchases[productIdentifier] = InAppProductPurchaseRequest.startPayment(product: product, atomically: atomically, applicationUsername: applicationUsername) { results in
 
 
             self.inflightPurchases[productIdentifier] = nil
             self.inflightPurchases[productIdentifier] = nil
             
             
@@ -258,29 +235,29 @@ public class SwiftyStoreKit {
 
 
     private func processPurchaseResult(_ result: InAppProductPurchaseRequest.TransactionResult) -> PurchaseResult {
     private func processPurchaseResult(_ result: InAppProductPurchaseRequest.TransactionResult) -> PurchaseResult {
         switch result {
         switch result {
-        case .purchased(let productId):
-            return .success(productId: productId)
+        case .purchased(let product):
+            return .success(product: product)
         case .failed(let error):
         case .failed(let error):
             return .error(error: .failed(error: error))
             return .error(error: .failed(error: error))
-        case .restored(let productId):
-            return .error(error: .failed(error: storeInternalError(code: InternalErrorCode.restoredPurchaseWhenPurchasing.rawValue, description: "Cannot restore product \(productId) from purchase path")))
+        case .restored(let product):
+            return .error(error: .failed(error: storeInternalError(code: InternalErrorCode.restoredPurchaseWhenPurchasing.rawValue, description: "Cannot restore product \(product.productId) from purchase path")))
         }
         }
     }
     }
     
     
     private func processRestoreResults(_ results: [InAppProductPurchaseRequest.TransactionResult]) -> RestoreResults {
     private func processRestoreResults(_ results: [InAppProductPurchaseRequest.TransactionResult]) -> RestoreResults {
-        var restoredProductIds: [String] = []
+        var restoredProducts: [Product] = []
         var restoreFailedProducts: [(Swift.Error, String?)] = []
         var restoreFailedProducts: [(Swift.Error, String?)] = []
         for result in results {
         for result in results {
             switch result {
             switch result {
-            case .purchased(let productId):
-                restoreFailedProducts.append((storeInternalError(code: InternalErrorCode.purchasedWhenRestoringPurchase.rawValue, description: "Cannot purchase product \(productId) from restore purchases path"), productId))
+            case .purchased(let product):
+                restoreFailedProducts.append((storeInternalError(code: InternalErrorCode.purchasedWhenRestoringPurchase.rawValue, description: "Cannot purchase product \(product.productId) from restore purchases path"), product.productId))
             case .failed(let error):
             case .failed(let error):
                 restoreFailedProducts.append((error, nil))
                 restoreFailedProducts.append((error, nil))
-            case .restored(let productId):
-                restoredProductIds.append(productId)
+            case .restored(let product):
+                restoredProducts.append(product)
             }
             }
         }
         }
-        return RestoreResults(restoredProductIds: restoredProductIds, restoreFailedProducts: restoreFailedProducts)
+        return RestoreResults(restoredProducts: restoredProducts, restoreFailedProducts: restoreFailedProducts)
     }
     }
     
     
     private func requestProducts(_ productIds: Set<String>, completion: @escaping (RetrieveResults) -> ()) {
     private func requestProducts(_ productIds: Set<String>, completion: @escaping (RetrieveResults) -> ()) {