浏览代码

Merge pull request #544 from bizz84/develop

0.16.0 update
Samuel Spencer 5 年之前
父节点
当前提交
a45217b2cf

+ 26 - 0
README.md

@@ -55,6 +55,7 @@ More info here:
 	- [Verify Purchase](#verify-purchase)
 	- [Verify Purchase](#verify-purchase)
 	- [Verify Subscription](#verify-subscription)
 	- [Verify Subscription](#verify-subscription)
 	- [Subscription Groups](#subscription-groups)
 	- [Subscription Groups](#subscription-groups)
+        - [Get distinct purchase identifiers](#get-distinct-purchase-identifiers)
 - [Notes](#notes)
 - [Notes](#notes)
 - [Change Log](#change-log)
 - [Change Log](#change-log)
 - [Sample Code](#sample-code)
 - [Sample Code](#sample-code)
@@ -637,6 +638,31 @@ SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
     }
     }
 }
 }
 ```
 ```
+#### Get distinct purchase identifiers 
+
+You can retrieve all product identifiers with the `getDistinctPurchaseIds` method:
+
+```swift
+let appleValidator = AppleReceiptValidator(service: .production, sharedSecret: "your-shared-secret")
+SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
+    switch result {
+    case .success(let receipt):
+        let productIds = SwiftyStoreKit.getDistinctPurchaseIds(inReceipt receipt: ReceiptInfo)
+        let purchaseResult = SwiftyStoreKit.verifySubscriptions(productIds: productIds, inReceipt: receipt)
+        switch purchaseResult {
+        case .purchased(let expiryDate, let items):
+            print("\(productIds) are valid until \(expiryDate)\n\(items)\n")
+        case .expired(let expiryDate, let items):
+            print("\(productIds) are expired since \(expiryDate)\n\(items)\n")
+        case .notPurchased:
+            print("The user has never purchased \(productIds)")
+        }
+    case .error(let error):
+        print("Receipt verification failed: \(error)")
+    }
+}
+```
+
 
 
 ## Notes
 ## Notes
 The framework provides a simple block based API with robust error handling on top of the existing StoreKit framework. It does **NOT** persist in app purchases data locally. It is up to clients to do this with a storage solution of choice (i.e. NSUserDefaults, CoreData, Keychain).
 The framework provides a simple block based API with robust error handling on top of the existing StoreKit framework. It does **NOT** persist in app purchases data locally. It is up to clients to do this with a storage solution of choice (i.e. NSUserDefaults, CoreData, Keychain).

二进制
SwiftyStoreKit-logo.png


+ 1 - 1
SwiftyStoreKit.podspec

@@ -1,7 +1,7 @@
 Pod::Spec.new do |s|
 Pod::Spec.new do |s|
   s.name         = 'SwiftyStoreKit'
   s.name         = 'SwiftyStoreKit'
   s.version      = '0.15.1'
   s.version      = '0.15.1'
-  s.summary      = 'Lightweight In App Purchases Swift framework for iOS 8.0+, tvOS 9.0+ and OSX 10.10+'
+  s.summary      = 'Lightweight In App Purchases Swift framework for iOS 8.0+, tvOS 9.0+ and macOS 10.10+'
   s.license      = 'MIT'
   s.license      = 'MIT'
   s.homepage     = 'https://github.com/bizz84/SwiftyStoreKit'
   s.homepage     = 'https://github.com/bizz84/SwiftyStoreKit'
   s.author       = { 'Andrea Bizzotto' => 'bizz84@gmail.com' }
   s.author       = { 'Andrea Bizzotto' => 'bizz84@gmail.com' }

+ 177 - 5
SwiftyStoreKit.xcodeproj/project.pbxproj

@@ -71,6 +71,25 @@
 		65F7DF9A1DCD536700835D30 /* SwiftyStoreKit-iOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 65F7DF971DCD536100835D30 /* SwiftyStoreKit-iOS.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		65F7DF9A1DCD536700835D30 /* SwiftyStoreKit-iOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 65F7DF971DCD536100835D30 /* SwiftyStoreKit-iOS.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		65F7DF9B1DCD537800835D30 /* SwiftyStoreKit-macOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 65F7DF981DCD536100835D30 /* SwiftyStoreKit-macOS.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		65F7DF9B1DCD537800835D30 /* SwiftyStoreKit-macOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 65F7DF981DCD536100835D30 /* SwiftyStoreKit-macOS.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		65F7DF9C1DCD537F00835D30 /* SwiftyStoreKit-tvOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 65F7DF991DCD536100835D30 /* SwiftyStoreKit-tvOS.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		65F7DF9C1DCD537F00835D30 /* SwiftyStoreKit-tvOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 65F7DF991DCD536100835D30 /* SwiftyStoreKit-tvOS.h */; settings = {ATTRIBUTES = (Public, ); }; };
+		A61BF4BE2481F0560017D9BC /* SKProductDiscount+LocalizedPrice.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61BF4BD2481F0560017D9BC /* SKProductDiscount+LocalizedPrice.swift */; };
+		A61BF4BF2481F0560017D9BC /* SKProductDiscount+LocalizedPrice.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61BF4BD2481F0560017D9BC /* SKProductDiscount+LocalizedPrice.swift */; };
+		A61BF4C02481F0560017D9BC /* SKProductDiscount+LocalizedPrice.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61BF4BD2481F0560017D9BC /* SKProductDiscount+LocalizedPrice.swift */; };
+		A61BF4C62481F4970017D9BC /* OS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C680F1C29414C00B60B7E /* OS.swift */; };
+		A61BF4C72481F4970017D9BC /* AppleReceiptValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1592CD4F1E27756500D321E6 /* AppleReceiptValidator.swift */; };
+		A61BF4C82481F4970017D9BC /* InAppProductQueryRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6502F6231B98586A004E342D /* InAppProductQueryRequest.swift */; };
+		A61BF4C92481F4970017D9BC /* InAppReceiptRefreshRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4083C561C2AB0A900295248 /* InAppReceiptRefreshRequest.swift */; };
+		A61BF4CA2481F4970017D9BC /* PaymentsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F70AC81E2EDC3700BF040D /* PaymentsController.swift */; };
+		A61BF4CB2481F4970017D9BC /* ProductsInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 650307FB1E33154F001332A4 /* ProductsInfoController.swift */; };
+		A61BF4CC2481F4970017D9BC /* RestorePurchasesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 650307F31E3177EF001332A4 /* RestorePurchasesController.swift */; };
+		A61BF4CD2481F4970017D9BC /* PaymentQueueController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 658A08361E2EC24E0074A98F /* PaymentQueueController.swift */; };
+		A61BF4CE2481F4970017D9BC /* InAppReceiptVerificator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E9E0781ECADF5E005CF7B4 /* InAppReceiptVerificator.swift */; };
+		A61BF4CF2481F4970017D9BC /* SKProduct+LocalizedPrice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653722801DB8282600C8F944 /* SKProduct+LocalizedPrice.swift */; };
+		A61BF4D02481F4970017D9BC /* InAppReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A7C7621C29B8D00053ED64 /* InAppReceipt.swift */; };
+		A61BF4D12481F4970017D9BC /* CompleteTransactionsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 650307F71E317BCF001332A4 /* CompleteTransactionsController.swift */; };
+		A61BF4D22481F4970017D9BC /* SwiftyStoreKit+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */; };
+		A61BF4D32481F4970017D9BC /* SwiftyStoreKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6502F6241B98586A004E342D /* SwiftyStoreKit.swift */; };
+		A61BF4D42481F4970017D9BC /* SKProductDiscount+LocalizedPrice.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61BF4BD2481F0560017D9BC /* SKProductDiscount+LocalizedPrice.swift */; };
+		A61BF4E02481F7400017D9BC /* SwiftyStoreKit-watchOS.h in Headers */ = {isa = PBXBuildFile; fileRef = A61BF4DF2481F7150017D9BC /* SwiftyStoreKit-watchOS.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		C3099C071E2FCDAA00392A54 /* PaymentsControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3099C061E2FCDAA00392A54 /* PaymentsControllerTests.swift */; };
 		C3099C071E2FCDAA00392A54 /* PaymentsControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3099C061E2FCDAA00392A54 /* PaymentsControllerTests.swift */; };
 		C3099C091E2FCE3A00392A54 /* TestProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3099C081E2FCE3A00392A54 /* TestProduct.swift */; };
 		C3099C091E2FCE3A00392A54 /* TestProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3099C081E2FCE3A00392A54 /* TestProduct.swift */; };
 		C3099C0B1E2FD13200392A54 /* TestPaymentTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3099C0A1E2FD13200392A54 /* TestPaymentTransaction.swift */; };
 		C3099C0B1E2FD13200392A54 /* TestPaymentTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3099C0A1E2FD13200392A54 /* TestPaymentTransaction.swift */; };
@@ -207,6 +226,10 @@
 		65F7DF971DCD536100835D30 /* SwiftyStoreKit-iOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SwiftyStoreKit-iOS.h"; sourceTree = "<group>"; };
 		65F7DF971DCD536100835D30 /* SwiftyStoreKit-iOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SwiftyStoreKit-iOS.h"; sourceTree = "<group>"; };
 		65F7DF981DCD536100835D30 /* SwiftyStoreKit-macOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SwiftyStoreKit-macOS.h"; sourceTree = "<group>"; };
 		65F7DF981DCD536100835D30 /* SwiftyStoreKit-macOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SwiftyStoreKit-macOS.h"; sourceTree = "<group>"; };
 		65F7DF991DCD536100835D30 /* SwiftyStoreKit-tvOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SwiftyStoreKit-tvOS.h"; sourceTree = "<group>"; };
 		65F7DF991DCD536100835D30 /* SwiftyStoreKit-tvOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SwiftyStoreKit-tvOS.h"; sourceTree = "<group>"; };
+		A61BF4BD2481F0560017D9BC /* SKProductDiscount+LocalizedPrice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKProductDiscount+LocalizedPrice.swift"; sourceTree = "<group>"; };
+		A61BF4DD2481F4970017D9BC /* SwiftyStoreKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftyStoreKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		A61BF4DF2481F7150017D9BC /* SwiftyStoreKit-watchOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SwiftyStoreKit-watchOS.h"; sourceTree = "<group>"; };
+		A61BF4E12481F7B00017D9BC /* Info-watchOS.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-watchOS.plist"; sourceTree = "<group>"; };
 		C3099C061E2FCDAA00392A54 /* PaymentsControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentsControllerTests.swift; sourceTree = "<group>"; };
 		C3099C061E2FCDAA00392A54 /* PaymentsControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentsControllerTests.swift; sourceTree = "<group>"; };
 		C3099C081E2FCE3A00392A54 /* TestProduct.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestProduct.swift; sourceTree = "<group>"; };
 		C3099C081E2FCE3A00392A54 /* TestProduct.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestProduct.swift; sourceTree = "<group>"; };
 		C3099C0A1E2FD13200392A54 /* TestPaymentTransaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestPaymentTransaction.swift; sourceTree = "<group>"; };
 		C3099C0A1E2FD13200392A54 /* TestPaymentTransaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestPaymentTransaction.swift; sourceTree = "<group>"; };
@@ -257,6 +280,13 @@
 			);
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			runOnlyForDeploymentPostprocessing = 0;
 		};
 		};
+		A61BF4D52481F4970017D9BC /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		C4D74BB71C24CEC90071AD3E /* Frameworks */ = {
 		C4D74BB71C24CEC90071AD3E /* Frameworks */ = {
 			isa = PBXFrameworksBuildPhase;
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			buildActionMask = 2147483647;
@@ -297,6 +327,7 @@
 				54C0D52C1CF7404500F90BCE /* SwiftyStoreKit.framework */,
 				54C0D52C1CF7404500F90BCE /* SwiftyStoreKit.framework */,
 				658A083E1E2EC5120074A98F /* SwiftyStoreKitTests.xctest */,
 				658A083E1E2EC5120074A98F /* SwiftyStoreKitTests.xctest */,
 				654287EE1E79F5A000F61800 /* SwiftyStoreKit_tvOSDemo.app */,
 				654287EE1E79F5A000F61800 /* SwiftyStoreKit_tvOSDemo.app */,
+				A61BF4DD2481F4970017D9BC /* SwiftyStoreKit.framework */,
 			);
 			);
 			name = Products;
 			name = Products;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -317,6 +348,7 @@
 				65E9E0781ECADF5E005CF7B4 /* InAppReceiptVerificator.swift */,
 				65E9E0781ECADF5E005CF7B4 /* InAppReceiptVerificator.swift */,
 				1592CD4F1E27756500D321E6 /* AppleReceiptValidator.swift */,
 				1592CD4F1E27756500D321E6 /* AppleReceiptValidator.swift */,
 				653722801DB8282600C8F944 /* SKProduct+LocalizedPrice.swift */,
 				653722801DB8282600C8F944 /* SKProduct+LocalizedPrice.swift */,
+				A61BF4BD2481F0560017D9BC /* SKProductDiscount+LocalizedPrice.swift */,
 				C40C680F1C29414C00B60B7E /* OS.swift */,
 				C40C680F1C29414C00B60B7E /* OS.swift */,
 				65F7DF931DCD536100835D30 /* Platforms */,
 				65F7DF931DCD536100835D30 /* Platforms */,
 			);
 			);
@@ -384,9 +416,11 @@
 				65F7DF941DCD536100835D30 /* Info-iOS.plist */,
 				65F7DF941DCD536100835D30 /* Info-iOS.plist */,
 				65F7DF951DCD536100835D30 /* Info-macOS.plist */,
 				65F7DF951DCD536100835D30 /* Info-macOS.plist */,
 				65F7DF961DCD536100835D30 /* Info-tvOS.plist */,
 				65F7DF961DCD536100835D30 /* Info-tvOS.plist */,
+				A61BF4E12481F7B00017D9BC /* Info-watchOS.plist */,
 				65F7DF971DCD536100835D30 /* SwiftyStoreKit-iOS.h */,
 				65F7DF971DCD536100835D30 /* SwiftyStoreKit-iOS.h */,
 				65F7DF981DCD536100835D30 /* SwiftyStoreKit-macOS.h */,
 				65F7DF981DCD536100835D30 /* SwiftyStoreKit-macOS.h */,
 				65F7DF991DCD536100835D30 /* SwiftyStoreKit-tvOS.h */,
 				65F7DF991DCD536100835D30 /* SwiftyStoreKit-tvOS.h */,
+				A61BF4DF2481F7150017D9BC /* SwiftyStoreKit-watchOS.h */,
 			);
 			);
 			path = Platforms;
 			path = Platforms;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -410,6 +444,14 @@
 			);
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			runOnlyForDeploymentPostprocessing = 0;
 		};
 		};
+		A61BF4D62481F4970017D9BC /* Headers */ = {
+			isa = PBXHeadersBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				A61BF4E02481F7400017D9BC /* SwiftyStoreKit-watchOS.h in Headers */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		C4D74BB81C24CEC90071AD3E /* Headers */ = {
 		C4D74BB81C24CEC90071AD3E /* Headers */ = {
 			isa = PBXHeadersBuildPhase;
 			isa = PBXHeadersBuildPhase;
 			buildActionMask = 2147483647;
 			buildActionMask = 2147483647;
@@ -515,6 +557,25 @@
 			productReference = 658A083E1E2EC5120074A98F /* SwiftyStoreKitTests.xctest */;
 			productReference = 658A083E1E2EC5120074A98F /* SwiftyStoreKitTests.xctest */;
 			productType = "com.apple.product-type.bundle.unit-test";
 			productType = "com.apple.product-type.bundle.unit-test";
 		};
 		};
+		A61BF4C42481F4970017D9BC /* SwiftyStoreKit_watchOS */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = A61BF4DA2481F4970017D9BC /* Build configuration list for PBXNativeTarget "SwiftyStoreKit_watchOS" */;
+			buildPhases = (
+				A61BF4C52481F4970017D9BC /* Sources */,
+				A61BF4D62481F4970017D9BC /* Headers */,
+				A61BF4D52481F4970017D9BC /* Frameworks */,
+				A61BF4D82481F4970017D9BC /* Resources */,
+				A61BF4D92481F4970017D9BC /* swiftlint */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = SwiftyStoreKit_watchOS;
+			productName = SwiftyStoreKit;
+			productReference = A61BF4DD2481F4970017D9BC /* SwiftyStoreKit.framework */;
+			productType = "com.apple.product-type.framework";
+		};
 		C4D74BBA1C24CEC90071AD3E /* SwiftyStoreKit_macOS */ = {
 		C4D74BBA1C24CEC90071AD3E /* SwiftyStoreKit_macOS */ = {
 			isa = PBXNativeTarget;
 			isa = PBXNativeTarget;
 			buildConfigurationList = C4D74BC21C24CECA0071AD3E /* Build configuration list for PBXNativeTarget "SwiftyStoreKit_macOS" */;
 			buildConfigurationList = C4D74BC21C24CECA0071AD3E /* Build configuration list for PBXNativeTarget "SwiftyStoreKit_macOS" */;
@@ -582,7 +643,7 @@
 					};
 					};
 					658A083D1E2EC5120074A98F = {
 					658A083D1E2EC5120074A98F = {
 						CreatedOnToolsVersion = 8.2.1;
 						CreatedOnToolsVersion = 8.2.1;
-						LastSwiftMigration = 1000;
+						LastSwiftMigration = 1150;
 						ProvisioningStyle = Automatic;
 						ProvisioningStyle = Automatic;
 						TestTargetID = 6502F5FD1B985833004E342D;
 						TestTargetID = 6502F5FD1B985833004E342D;
 					};
 					};
@@ -611,6 +672,7 @@
 				6502F62C1B985C40004E342D /* SwiftyStoreKit_iOS */,
 				6502F62C1B985C40004E342D /* SwiftyStoreKit_iOS */,
 				C4D74BBA1C24CEC90071AD3E /* SwiftyStoreKit_macOS */,
 				C4D74BBA1C24CEC90071AD3E /* SwiftyStoreKit_macOS */,
 				54C0D52B1CF7404500F90BCE /* SwiftyStoreKit_tvOS */,
 				54C0D52B1CF7404500F90BCE /* SwiftyStoreKit_tvOS */,
+				A61BF4C42481F4970017D9BC /* SwiftyStoreKit_watchOS */,
 				6502F5FD1B985833004E342D /* SwiftyStoreKit_iOSDemo */,
 				6502F5FD1B985833004E342D /* SwiftyStoreKit_iOSDemo */,
 				C4FD3A001C2954C10035CFF3 /* SwiftyStoreKit_macOSDemo */,
 				C4FD3A001C2954C10035CFF3 /* SwiftyStoreKit_macOSDemo */,
 				654287ED1E79F5A000F61800 /* SwiftyStoreKit_tvOSDemo */,
 				654287ED1E79F5A000F61800 /* SwiftyStoreKit_tvOSDemo */,
@@ -660,6 +722,13 @@
 			);
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			runOnlyForDeploymentPostprocessing = 0;
 		};
 		};
+		A61BF4D82481F4970017D9BC /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		C4D74BB91C24CEC90071AD3E /* Resources */ = {
 		C4D74BB91C24CEC90071AD3E /* Resources */ = {
 			isa = PBXResourcesBuildPhase;
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			buildActionMask = 2147483647;
@@ -679,6 +748,20 @@
 /* End PBXResourcesBuildPhase section */
 /* End PBXResourcesBuildPhase section */
 
 
 /* Begin PBXShellScriptBuildPhase section */
 /* Begin PBXShellScriptBuildPhase section */
+		A61BF4D92481F4970017D9BC /* swiftlint */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = swiftlint;
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"SwiftLint does not exist, download from https://github.com/realm/SwiftLint\"\nfi\n";
+		};
 		C4B298341E5C25DD007C87C2 /* swiftlint */ = {
 		C4B298341E5C25DD007C87C2 /* swiftlint */ = {
 			isa = PBXShellScriptBuildPhase;
 			isa = PBXShellScriptBuildPhase;
 			buildActionMask = 2147483647;
 			buildActionMask = 2147483647;
@@ -728,6 +811,7 @@
 				650307FA1E317BCF001332A4 /* CompleteTransactionsController.swift in Sources */,
 				650307FA1E317BCF001332A4 /* CompleteTransactionsController.swift in Sources */,
 				65BB6CEA1DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */,
 				65BB6CEA1DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */,
 				54B069941CF742D600BAFE38 /* InAppProductQueryRequest.swift in Sources */,
 				54B069941CF742D600BAFE38 /* InAppProductQueryRequest.swift in Sources */,
+				A61BF4C02481F0560017D9BC /* SKProductDiscount+LocalizedPrice.swift in Sources */,
 			);
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			runOnlyForDeploymentPostprocessing = 0;
 		};
 		};
@@ -759,6 +843,7 @@
 				650307F81E317BCF001332A4 /* CompleteTransactionsController.swift in Sources */,
 				650307F81E317BCF001332A4 /* CompleteTransactionsController.swift in Sources */,
 				65BB6CE81DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */,
 				65BB6CE81DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */,
 				6502F63C1B985CA4004E342D /* SwiftyStoreKit.swift in Sources */,
 				6502F63C1B985CA4004E342D /* SwiftyStoreKit.swift in Sources */,
+				A61BF4BE2481F0560017D9BC /* SKProductDiscount+LocalizedPrice.swift in Sources */,
 			);
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			runOnlyForDeploymentPostprocessing = 0;
 		};
 		};
@@ -790,6 +875,28 @@
 			);
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			runOnlyForDeploymentPostprocessing = 0;
 		};
 		};
+		A61BF4C52481F4970017D9BC /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				A61BF4C62481F4970017D9BC /* OS.swift in Sources */,
+				A61BF4C72481F4970017D9BC /* AppleReceiptValidator.swift in Sources */,
+				A61BF4C82481F4970017D9BC /* InAppProductQueryRequest.swift in Sources */,
+				A61BF4C92481F4970017D9BC /* InAppReceiptRefreshRequest.swift in Sources */,
+				A61BF4CA2481F4970017D9BC /* PaymentsController.swift in Sources */,
+				A61BF4CB2481F4970017D9BC /* ProductsInfoController.swift in Sources */,
+				A61BF4CC2481F4970017D9BC /* RestorePurchasesController.swift in Sources */,
+				A61BF4CD2481F4970017D9BC /* PaymentQueueController.swift in Sources */,
+				A61BF4CE2481F4970017D9BC /* InAppReceiptVerificator.swift in Sources */,
+				A61BF4CF2481F4970017D9BC /* SKProduct+LocalizedPrice.swift in Sources */,
+				A61BF4D02481F4970017D9BC /* InAppReceipt.swift in Sources */,
+				A61BF4D12481F4970017D9BC /* CompleteTransactionsController.swift in Sources */,
+				A61BF4D22481F4970017D9BC /* SwiftyStoreKit+Types.swift in Sources */,
+				A61BF4D32481F4970017D9BC /* SwiftyStoreKit.swift in Sources */,
+				A61BF4D42481F4970017D9BC /* SKProductDiscount+LocalizedPrice.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		C4D74BB61C24CEC90071AD3E /* Sources */ = {
 		C4D74BB61C24CEC90071AD3E /* Sources */ = {
 			isa = PBXSourcesBuildPhase;
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			buildActionMask = 2147483647;
@@ -808,6 +915,7 @@
 				650307F91E317BCF001332A4 /* CompleteTransactionsController.swift in Sources */,
 				650307F91E317BCF001332A4 /* CompleteTransactionsController.swift in Sources */,
 				65BB6CE91DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */,
 				65BB6CE91DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */,
 				C4D74BC51C24CEDC0071AD3E /* SwiftyStoreKit.swift in Sources */,
 				C4D74BC51C24CEDC0071AD3E /* SwiftyStoreKit.swift in Sources */,
+				A61BF4BF2481F0560017D9BC /* SKProductDiscount+LocalizedPrice.swift in Sources */,
 			);
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			runOnlyForDeploymentPostprocessing = 0;
 		};
 		};
@@ -1183,8 +1291,8 @@
 				PRODUCT_BUNDLE_IDENTIFIER = com.musevisions.iOS.SwiftyStoreKitTests;
 				PRODUCT_BUNDLE_IDENTIFIER = com.musevisions.iOS.SwiftyStoreKitTests;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
-				SWIFT_SWIFT3_OBJC_INFERENCE = On;
-				SWIFT_VERSION = 4.2;
+				SWIFT_SWIFT3_OBJC_INFERENCE = Default;
+				SWIFT_VERSION = 5.0;
 				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftyStoreKit_iOSDemo.app/SwiftyStoreKit_iOSDemo";
 				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftyStoreKit_iOSDemo.app/SwiftyStoreKit_iOSDemo";
 			};
 			};
 			name = Debug;
 			name = Debug;
@@ -1200,12 +1308,67 @@
 				PRODUCT_BUNDLE_IDENTIFIER = com.musevisions.iOS.SwiftyStoreKitTests;
 				PRODUCT_BUNDLE_IDENTIFIER = com.musevisions.iOS.SwiftyStoreKitTests;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
 				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
-				SWIFT_SWIFT3_OBJC_INFERENCE = On;
-				SWIFT_VERSION = 4.2;
+				SWIFT_SWIFT3_OBJC_INFERENCE = Default;
+				SWIFT_VERSION = 5.0;
 				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftyStoreKit_iOSDemo.app/SwiftyStoreKit_iOSDemo";
 				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftyStoreKit_iOSDemo.app/SwiftyStoreKit_iOSDemo";
 			};
 			};
 			name = Release;
 			name = Release;
 		};
 		};
+		A61BF4DB2481F4970017D9BC /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				APPLICATION_EXTENSION_API_ONLY = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
+				CURRENT_PROJECT_VERSION = 1;
+				DEFINES_MODULE = YES;
+				DEVELOPMENT_TEAM = "";
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				INFOPLIST_FILE = "$(SRCROOT)/SwiftyStoreKit/Platforms/Info-watchOS.plist";
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.musevisions.iOS.SwiftyStoreKit;
+				PRODUCT_NAME = SwiftyStoreKit;
+				SDKROOT = watchos;
+				SKIP_INSTALL = YES;
+				SUPPORTED_PLATFORMS = "watchsimulator watchos";
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = 4;
+				VERSIONING_SYSTEM = "apple-generic";
+				VERSION_INFO_PREFIX = "";
+			};
+			name = Debug;
+		};
+		A61BF4DC2481F4970017D9BC /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				APPLICATION_EXTENSION_API_ONLY = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
+				CURRENT_PROJECT_VERSION = 1;
+				DEFINES_MODULE = YES;
+				DEVELOPMENT_TEAM = "";
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				INFOPLIST_FILE = "$(SRCROOT)/SwiftyStoreKit/Platforms/Info-watchOS.plist";
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.musevisions.iOS.SwiftyStoreKit;
+				PRODUCT_NAME = SwiftyStoreKit;
+				SDKROOT = watchos;
+				SKIP_INSTALL = YES;
+				SUPPORTED_PLATFORMS = "watchsimulator watchos";
+				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = 4;
+				VERSIONING_SYSTEM = "apple-generic";
+				VERSION_INFO_PREFIX = "";
+			};
+			name = Release;
+		};
 		C4D74BC01C24CECA0071AD3E /* Debug */ = {
 		C4D74BC01C24CECA0071AD3E /* Debug */ = {
 			isa = XCBuildConfiguration;
 			isa = XCBuildConfiguration;
 			buildSettings = {
 			buildSettings = {
@@ -1345,6 +1508,15 @@
 			defaultConfigurationIsVisible = 0;
 			defaultConfigurationIsVisible = 0;
 			defaultConfigurationName = Release;
 			defaultConfigurationName = Release;
 		};
 		};
+		A61BF4DA2481F4970017D9BC /* Build configuration list for PBXNativeTarget "SwiftyStoreKit_watchOS" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				A61BF4DB2481F4970017D9BC /* Debug */,
+				A61BF4DC2481F4970017D9BC /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
 		C4D74BC21C24CECA0071AD3E /* Build configuration list for PBXNativeTarget "SwiftyStoreKit_macOS" */ = {
 		C4D74BC21C24CECA0071AD3E /* Build configuration list for PBXNativeTarget "SwiftyStoreKit_macOS" */ = {
 			isa = XCConfigurationList;
 			isa = XCConfigurationList;
 			buildConfigurations = (
 			buildConfigurations = (

+ 67 - 0
SwiftyStoreKit.xcodeproj/xcshareddata/xcschemes/SwiftyStoreKit_watchOS.xcscheme

@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1150"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "A61BF4C42481F4970017D9BC"
+               BuildableName = "SwiftyStoreKit_watchOS.framework"
+               BlueprintName = "SwiftyStoreKit_watchOS"
+               ReferencedContainer = "container:SwiftyStoreKit.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <Testables>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "A61BF4C42481F4970017D9BC"
+            BuildableName = "SwiftyStoreKit_watchOS.framework"
+            BlueprintName = "SwiftyStoreKit_watchOS"
+            ReferencedContainer = "container:SwiftyStoreKit.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 7 - 4
SwiftyStoreKit/AppleReceiptValidator.swift

@@ -27,14 +27,17 @@ import Foundation
 
 
 // https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html
 // https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html
 
 
-public struct AppleReceiptValidator: ReceiptValidator {
+public class AppleReceiptValidator: ReceiptValidator {
 
 
 	public enum VerifyReceiptURLType: String {
 	public enum VerifyReceiptURLType: String {
 		case production = "https://buy.itunes.apple.com/verifyReceipt"
 		case production = "https://buy.itunes.apple.com/verifyReceipt"
 		case sandbox = "https://sandbox.itunes.apple.com/verifyReceipt"
 		case sandbox = "https://sandbox.itunes.apple.com/verifyReceipt"
 	}
 	}
 
 
-    private let service: VerifyReceiptURLType
+    /// You should always verify your receipt first with the `production` service
+    /// Note: will auto change to `.sandbox` and validate again if received a 21007 status code from Apple
+    public var service: VerifyReceiptURLType
+
     private let sharedSecret: String?
     private let sharedSecret: String?
 
 
     /**
     /**
@@ -106,8 +109,8 @@ public struct AppleReceiptValidator: ReceiptValidator {
 				*/
 				*/
 				let receiptStatus = ReceiptStatus(rawValue: status) ?? ReceiptStatus.unknown
 				let receiptStatus = ReceiptStatus(rawValue: status) ?? ReceiptStatus.unknown
 				if case .testReceipt = receiptStatus {
 				if case .testReceipt = receiptStatus {
-                    let sandboxValidator = AppleReceiptValidator(service: .sandbox, sharedSecret: self.sharedSecret)
-					sandboxValidator.validate(receiptData: receiptData, completion: completion)
+                    self.service = .sandbox
+                    self.validate(receiptData: receiptData, completion: completion)
 				} else {
 				} else {
 					if receiptStatus.isValid {
 					if receiptStatus.isValid {
 						completion(.success(receipt: receiptInfo))
 						completion(.success(receipt: receiptInfo))

+ 3 - 1
SwiftyStoreKit/InAppProductQueryRequest.swift

@@ -26,11 +26,13 @@ import StoreKit
 
 
 typealias InAppProductRequestCallback = (RetrieveResults) -> Void
 typealias InAppProductRequestCallback = (RetrieveResults) -> Void
 
 
-protocol InAppProductRequest: class {
+public protocol InAppRequest: class {
     func start()
     func start()
     func cancel()
     func cancel()
 }
 }
 
 
+protocol InAppProductRequest: InAppRequest { }
+
 class InAppProductQueryRequest: NSObject, InAppProductRequest, SKProductsRequestDelegate {
 class InAppProductQueryRequest: NSObject, InAppProductRequest, SKProductsRequestDelegate {
 
 
     private let callback: InAppProductRequestCallback
     private let callback: InAppProductRequestCallback

+ 41 - 0
SwiftyStoreKit/InAppReceipt.swift

@@ -168,6 +168,38 @@ internal class InAppReceipt {
             return .expired(expiryDate: firstExpiryDateItemPair.0, items: sortedReceiptItems)
             return .expired(expiryDate: firstExpiryDateItemPair.0, items: sortedReceiptItems)
         }
         }
     }
     }
+    
+    /**
+     *  Get the distinct product identifiers from receipt.
+     *
+     *  This Method extracts all product identifiers. (Including cancelled ones).
+     *  - Note: You can use this method to get all unique product identifiers from receipt.
+     *  - Parameter type: .autoRenewable or .nonRenewing.
+     *  - Parameter receipt: The receipt to use for looking up the product identifiers.
+     *  - return: Either Set<String> or nil.
+     */
+    class func getDistinctPurchaseIds(
+        ofType type: SubscriptionType,
+        inReceipt receipt: ReceiptInfo
+    ) -> Set<String>? {
+        
+        // Get receipts array from receipt
+        guard let receipts = getReceipts(for: type, inReceipt: receipt) else {
+            return nil
+        }
+        
+        #if swift(>=4.1)
+            let receiptIds = receipts.compactMap { ReceiptItem(receiptInfo: $0)?.productId }
+        #else
+            let receiptIds = receipts.flatMap { ReceiptItem(receiptInfo: $0)?.productId }
+        #endif
+        
+        if receiptIds.isEmpty {
+            return nil
+        }
+        
+        return Set(receiptIds)
+    }
 
 
     private class func expiryDatesAndItems(receiptItems: [ReceiptItem], duration: TimeInterval?) -> [(Date, ReceiptItem)] {
     private class func expiryDatesAndItems(receiptItems: [ReceiptItem], duration: TimeInterval?) -> [(Date, ReceiptItem)] {
 
 
@@ -194,6 +226,15 @@ internal class InAppReceipt {
             #endif
             #endif
         }
         }
     }
     }
+    
+    private class func getReceipts(for subscriptionType: SubscriptionType, inReceipt receipt: ReceiptInfo) -> [ReceiptInfo]? {
+        switch subscriptionType {
+        case .autoRenewable:
+            return receipt["latest_receipt_info"] as? [ReceiptInfo]
+        case .nonRenewing:
+            return getInAppReceipts(receipt: receipt)
+        }
+    }
 
 
     private class func getReceiptsAndDuration(for subscriptionType: SubscriptionType, inReceipt receipt: ReceiptInfo) -> ([ReceiptInfo]?, TimeInterval?) {
     private class func getReceiptsAndDuration(for subscriptionType: SubscriptionType, inReceipt receipt: ReceiptInfo) -> ([ReceiptInfo]?, TimeInterval?) {
         switch subscriptionType {
         switch subscriptionType {

+ 5 - 1
SwiftyStoreKit/InAppReceiptRefreshRequest.swift

@@ -26,7 +26,7 @@
 import StoreKit
 import StoreKit
 import Foundation
 import Foundation
 
 
-class InAppReceiptRefreshRequest: NSObject, SKRequestDelegate {
+class InAppReceiptRefreshRequest: NSObject, SKRequestDelegate, InAppRequest {
 
 
     enum ResultType {
     enum ResultType {
         case success
         case success
@@ -60,6 +60,10 @@ class InAppReceiptRefreshRequest: NSObject, SKRequestDelegate {
         self.refreshReceiptRequest.start()
         self.refreshReceiptRequest.start()
     }
     }
 
 
+    func cancel() {
+        self.refreshReceiptRequest.cancel()
+    }
+    
     func requestDidFinish(_ request: SKRequest) {
     func requestDidFinish(_ request: SKRequest) {
         /*if let resoreRequest = request as? SKReceiptRefreshRequest {
         /*if let resoreRequest = request as? SKReceiptRefreshRequest {
          let receiptProperties = resoreRequest.receiptProperties ?? [:]
          let receiptProperties = resoreRequest.receiptProperties ?? [:]

+ 7 - 3
SwiftyStoreKit/InAppReceiptVerificator.swift

@@ -49,12 +49,13 @@ class InAppReceiptVerificator: NSObject {
      *  - Parameter refresh: closure to perform receipt refresh (this is made explicit for testability)
      *  - Parameter refresh: closure to perform receipt refresh (this is made explicit for testability)
      *  - Parameter completion: handler for result
      *  - Parameter completion: handler for result
      */
      */
+    @discardableResult
     public func verifyReceipt(using validator: ReceiptValidator,
     public func verifyReceipt(using validator: ReceiptValidator,
                               forceRefresh: Bool,
                               forceRefresh: Bool,
                               refresh: InAppReceiptRefreshRequest.ReceiptRefresh = InAppReceiptRefreshRequest.refresh,
                               refresh: InAppReceiptRefreshRequest.ReceiptRefresh = InAppReceiptRefreshRequest.refresh,
-                              completion: @escaping (VerifyReceiptResult) -> Void) {
+                              completion: @escaping (VerifyReceiptResult) -> Void) -> InAppRequest? {
         
         
-        fetchReceipt(forceRefresh: forceRefresh, refresh: refresh) { result in
+        return fetchReceipt(forceRefresh: forceRefresh, refresh: refresh) { result in
             switch result {
             switch result {
             case .success(let receiptData):
             case .success(let receiptData):
                 self.verify(receiptData: receiptData, using: validator, completion: completion)
                 self.verify(receiptData: receiptData, using: validator, completion: completion)
@@ -72,12 +73,14 @@ class InAppReceiptVerificator: NSObject {
      *  - Parameter refresh: closure to perform receipt refresh (this is made explicit for testability)
      *  - Parameter refresh: closure to perform receipt refresh (this is made explicit for testability)
      *  - Parameter completion: handler for result
      *  - Parameter completion: handler for result
      */
      */
+    @discardableResult
     public func fetchReceipt(forceRefresh: Bool,
     public func fetchReceipt(forceRefresh: Bool,
                              refresh: InAppReceiptRefreshRequest.ReceiptRefresh = InAppReceiptRefreshRequest.refresh,
                              refresh: InAppReceiptRefreshRequest.ReceiptRefresh = InAppReceiptRefreshRequest.refresh,
-                             completion: @escaping (FetchReceiptResult) -> Void) {
+                             completion: @escaping (FetchReceiptResult) -> Void) -> InAppRequest? {
 
 
         if let receiptData = appStoreReceiptData, forceRefresh == false {
         if let receiptData = appStoreReceiptData, forceRefresh == false {
             completion(.success(receiptData: receiptData))
             completion(.success(receiptData: receiptData))
+            return nil
         } else {
         } else {
             
             
             receiptRefreshRequest = refresh(nil) { result in
             receiptRefreshRequest = refresh(nil) { result in
@@ -95,6 +98,7 @@ class InAppReceiptVerificator: NSObject {
                     completion(.error(error: .networkError(error: e)))
                     completion(.error(error: .networkError(error: e)))
                 }
                 }
             }
             }
+            return receiptRefreshRequest
         }
         }
     }
     }
     
     

+ 30 - 17
SwiftyStoreKit/OS.swift

@@ -2,29 +2,29 @@
 //  OS.swift
 //  OS.swift
 //  SwiftyStoreKit
 //  SwiftyStoreKit
 //
 //
-// Copyright (c) 2015 Andrea Bizzotto (bizz84@gmail.com)
+//  Copyright (c) 2020 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:
+//  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 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.
+//  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
 import StoreKit
 
 
-// MARK: - missing SKMutablePayment init with product on OSX
+// MARK: - Missing SKMutablePayment init with product on macOS
 #if os(OSX)
 #if os(OSX)
     extension SKMutablePayment {
     extension SKMutablePayment {
         convenience init(product: SKProduct) {
         convenience init(product: SKProduct) {
@@ -33,3 +33,16 @@ import StoreKit
         }
         }
     }
     }
 #endif
 #endif
+
+// MARK: - Missing SKError on watchOS
+#if os(watchOS)
+public struct SKError: Error {
+    
+    var Code: SKErrorCode = .unknown
+    var _nsError: NSError?
+    
+    static var unknown: SKErrorCode = .unknown
+    static var paymentInvalid: SKErrorCode = .paymentInvalid
+    
+}
+#endif

+ 20 - 6
SwiftyStoreKit/PaymentQueueController.swift

@@ -27,11 +27,10 @@ import StoreKit
 
 
 protocol TransactionController {
 protocol TransactionController {
 
 
-    /**
-     * - param transactions: transactions to process
-     * - param paymentQueue: payment queue for finishing transactions
-     * - return: array of unhandled transactions
-     */
+    /// Process the supplied transactions on a given queue.
+    /// - parameter transactions: transactions to process
+    /// - parameter paymentQueue: payment queue for finishing transactions
+    /// - returns: array of unhandled transactions
     func processTransactions(_ transactions: [SKPaymentTransaction], on paymentQueue: PaymentQueue) -> [SKPaymentTransaction]
     func processTransactions(_ transactions: [SKPaymentTransaction], on paymentQueue: PaymentQueue) -> [SKPaymentTransaction]
 }
 }
 
 
@@ -50,7 +49,11 @@ public protocol PaymentQueue: class {
     
     
     func start(_ downloads: [SKDownload])
     func start(_ downloads: [SKDownload])
     func pause(_ downloads: [SKDownload])
     func pause(_ downloads: [SKDownload])
+    #if os(watchOS)
+    func resumeDownloads(_ downloads: [SKDownload])
+    #else
     func resume(_ downloads: [SKDownload])
     func resume(_ downloads: [SKDownload])
+    #endif
     func cancel(_ downloads: [SKDownload])
     func cancel(_ downloads: [SKDownload])
     
     
     func restoreCompletedTransactions(withApplicationUsername username: String?)
     func restoreCompletedTransactions(withApplicationUsername username: String?)
@@ -123,8 +126,14 @@ class PaymentQueueController: NSObject, SKPaymentTransactionObserver {
         skPayment.applicationUsername = payment.applicationUsername
         skPayment.applicationUsername = payment.applicationUsername
         skPayment.quantity = payment.quantity
         skPayment.quantity = payment.quantity
         
         
+        if #available(iOS 12.2, tvOS 12.2, OSX 10.14.4, *) {
+            if let discount = payment.paymentDiscount?.discount as? SKPaymentDiscount {
+                skPayment.paymentDiscount = discount
+            }
+        }
+        
 #if os(iOS) || os(tvOS)
 #if os(iOS) || os(tvOS)
-        if #available(iOS 8.3, tvOS 9.0, *) {
+        if #available(iOS 8.3, *) {
             skPayment.simulatesAskToBuyInSandbox = payment.simulatesAskToBuyInSandbox
             skPayment.simulatesAskToBuyInSandbox = payment.simulatesAskToBuyInSandbox
         }
         }
 #endif
 #endif
@@ -170,8 +179,13 @@ class PaymentQueueController: NSObject, SKPaymentTransactionObserver {
     func pause(_ downloads: [SKDownload]) {
     func pause(_ downloads: [SKDownload]) {
         paymentQueue.pause(downloads)
         paymentQueue.pause(downloads)
     }
     }
+    
     func resume(_ downloads: [SKDownload]) {
     func resume(_ downloads: [SKDownload]) {
+        #if os(watchOS)
+        paymentQueue.resumeDownloads(downloads)
+        #else
         paymentQueue.resume(downloads)
         paymentQueue.resume(downloads)
+        #endif
     }
     }
     func cancel(_ downloads: [SKDownload]) {
     func cancel(_ downloads: [SKDownload]) {
         paymentQueue.cancel(downloads)
         paymentQueue.cancel(downloads)

+ 30 - 3
SwiftyStoreKit/PaymentsController.swift

@@ -27,6 +27,8 @@ import StoreKit
 
 
 struct Payment: Hashable {
 struct Payment: Hashable {
     let product: SKProduct
     let product: SKProduct
+    
+    let paymentDiscount: PaymentDiscount?
     let quantity: Int
     let quantity: Int
     let atomically: Bool
     let atomically: Bool
     let applicationUsername: String
     let applicationUsername: String
@@ -46,6 +48,19 @@ struct Payment: Hashable {
     }
     }
 }
 }
 
 
+public struct PaymentDiscount {
+    let discount: AnyObject?
+    
+    @available(iOS 12.2, tvOS 12.2, OSX 10.14.4, watchOS 6.2, macCatalyst 13.0, *)
+    public init(discount: SKPaymentDiscount) {
+        self.discount = discount
+    }
+    
+    private init() {
+        self.discount = nil
+    }
+}
+
 class PaymentsController: TransactionController {
 class PaymentsController: TransactionController {
 
 
     private var payments: [Payment] = []
     private var payments: [Payment] = []
@@ -88,6 +103,21 @@ class PaymentsController: TransactionController {
             payments.remove(at: paymentIndex)
             payments.remove(at: paymentIndex)
             return true
             return true
         }
         }
+
+        if transactionState == .restored {
+            print("Unexpected restored transaction for payment \(transactionProductIdentifier)")
+
+            let purchase = PurchaseDetails(productId: transactionProductIdentifier, quantity: transaction.payment.quantity, product: payment.product, transaction: transaction, originalTransaction: transaction.original, needsFinishTransaction: !payment.atomically)
+
+            payment.callback(.purchased(purchase: purchase))
+
+            if payment.atomically {
+                paymentQueue.finishTransaction(transaction)
+            }
+            payments.remove(at: paymentIndex)
+            return true
+        }
+
         if transactionState == .failed {
         if transactionState == .failed {
 
 
             payment.callback(.failed(error: transactionError(for: transaction.error as NSError?)))
             payment.callback(.failed(error: transactionError(for: transaction.error as NSError?)))
@@ -97,9 +127,6 @@ class PaymentsController: TransactionController {
             return true
             return true
         }
         }
 
 
-        if transactionState == .restored {
-            print("Unexpected restored transaction for payment \(transactionProductIdentifier)")
-        }
         return false
         return false
     }
     }
 
 

+ 26 - 0
SwiftyStoreKit/Platforms/Info-watchOS.plist

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>FMWK</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>$(CURRENT_PROJECT_VERSION)</string>
+	<key>NSPrincipalClass</key>
+	<string></string>
+</dict>
+</plist>

+ 38 - 0
SwiftyStoreKit/Platforms/SwiftyStoreKit-watchOS.h

@@ -0,0 +1,38 @@
+//
+//  SwiftyStoreKit-watchOS.h
+//  SwiftyStoreKit
+//
+//  Created by Sam Spencer on 5/29/20.
+//  Copyright © 2020 Sam Spencer. All rights reserved.
+//
+// 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 <UIKit/UIKit.h>
+#import <WatchKit/WatchKit.h>
+#import <StoreKit/StoreKit.h>
+
+//! Project version number for SwiftyStoreKit.
+FOUNDATION_EXPORT double SwiftyStoreKitVersionNumber;
+
+//! Project version string for SwiftyStoreKit.
+FOUNDATION_EXPORT const unsigned char SwiftyStoreKitVersionString[];
+
+// In this header, you should import all the public headers of your framework using statements like #import <SwiftyStoreKit/PublicHeader.h>
+
+

+ 4 - 1
SwiftyStoreKit/ProductsInfoController.swift

@@ -51,7 +51,8 @@ class ProductsInfoController: NSObject {
     // As we can have multiple inflight requests, we store them in a dictionary by product ids
     // As we can have multiple inflight requests, we store them in a dictionary by product ids
     private var inflightRequests: [Set<String>: InAppProductQuery] = [:]
     private var inflightRequests: [Set<String>: InAppProductQuery] = [:]
 
 
-    func retrieveProductsInfo(_ productIds: Set<String>, completion: @escaping (RetrieveResults) -> Void) {
+    @discardableResult
+    func retrieveProductsInfo(_ productIds: Set<String>, completion: @escaping (RetrieveResults) -> Void) -> InAppProductRequest {
 
 
         if inflightRequests[productIds] == nil {
         if inflightRequests[productIds] == nil {
             let request = inAppProductRequestBuilder.request(productIds: productIds) { results in
             let request = inAppProductRequestBuilder.request(productIds: productIds) { results in
@@ -68,8 +69,10 @@ class ProductsInfoController: NSObject {
             }
             }
             inflightRequests[productIds] = InAppProductQuery(request: request, completionHandlers: [completion])
             inflightRequests[productIds] = InAppProductQuery(request: request, completionHandlers: [completion])
             request.start()
             request.start()
+            return request
         } else {
         } else {
             inflightRequests[productIds]!.completionHandlers.append(completion)
             inflightRequests[productIds]!.completionHandlers.append(completion)
+            return inflightRequests[productIds]!.request
         }
         }
     }
     }
 }
 }

+ 21 - 0
SwiftyStoreKit/SKProduct+LocalizedPrice.swift

@@ -37,4 +37,25 @@ public extension SKProduct {
         formatter.numberStyle = .currency
         formatter.numberStyle = .currency
         return formatter
         return formatter
     }
     }
+    
+    @available(iOSApplicationExtension 11.2, iOS 11.2, OSX 10.13.2, tvOS 11.2, watchOS 6.2, macCatalyst 13.0, *)
+    var localizedSubscriptionPeriod: String {
+        guard let subscriptionPeriod = self.subscriptionPeriod else { return "" }
+        
+        let dateComponents: DateComponents
+        
+        switch subscriptionPeriod.unit {
+        case .day: dateComponents = DateComponents(day: subscriptionPeriod.numberOfUnits)
+        case .week: dateComponents = DateComponents(weekOfMonth: subscriptionPeriod.numberOfUnits)
+        case .month: dateComponents = DateComponents(month: subscriptionPeriod.numberOfUnits)
+        case .year: dateComponents = DateComponents(year: subscriptionPeriod.numberOfUnits)
+        @unknown default: 
+            print("WARNING: SwiftyStoreKit localizedSubscriptionPeriod does not handle all SKProduct.PeriodUnit cases.")
+            // Default to month units in the unlikely event a different unit type is added to a future OS version
+            dateComponents = DateComponents(month: subscriptionPeriod.numberOfUnits) 
+        }
+
+        return DateComponentsFormatter.localizedString(from: dateComponents, unitsStyle: .short) ?? ""
+    }
+    
 }
 }

+ 63 - 0
SwiftyStoreKit/SKProductDiscount+LocalizedPrice.swift

@@ -0,0 +1,63 @@
+//
+//  SKProductDiscount+LocalizedPrice.swift
+//  SwiftyStoreKit
+//
+//  Created by Sam Spencer on 5/29/20.
+//  Copyright © 2020 Sam Spencer. All rights reserved.
+//
+// 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
+
+@available(iOSApplicationExtension 11.2, iOS 11.2, OSX 10.13.2, tvOS 11.2, watchOS 4.2, macCatalyst 13.0, *)
+public extension SKProductDiscount {
+    
+    /// The formatted discount price of the product using the local currency.
+    var localizedPrice: String? {
+        return priceFormatter(locale: priceLocale).string(from: price)
+    }
+    
+    private func priceFormatter(locale: Locale) -> NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.locale = locale
+        formatter.numberStyle = .currency
+        return formatter
+    }
+    
+    /// The formatted, localized period / date for the product discount.
+    /// - note: The subscription period for the discount is independent of the product's regular subscription period, and does not have to match in units or duration.
+    var localizedSubscriptionPeriod: String {
+        let dateComponents: DateComponents
+        
+        switch subscriptionPeriod.unit {
+        case .day: dateComponents = DateComponents(day: subscriptionPeriod.numberOfUnits)
+        case .week: dateComponents = DateComponents(weekOfMonth: subscriptionPeriod.numberOfUnits)
+        case .month: dateComponents = DateComponents(month: subscriptionPeriod.numberOfUnits)
+        case .year: dateComponents = DateComponents(year: subscriptionPeriod.numberOfUnits)
+        @unknown default: 
+            print("WARNING: SwiftyStoreKit localizedSubscriptionPeriod does not handle all SKProduct.PeriodUnit cases.")
+            // Default to month units in the unlikely event a different unit type is added to a future OS version
+            dateComponents = DateComponents(month: subscriptionPeriod.numberOfUnits) 
+        }
+        
+        return DateComponentsFormatter.localizedString(from: dateComponents, unitsStyle: .full) ?? ""
+    }
+    
+}
+

+ 167 - 75
SwiftyStoreKit/SwiftyStoreKit+Types.swift

@@ -26,6 +26,40 @@ import StoreKit
 
 
 // MARK: Purchases
 // MARK: Purchases
 
 
+/// The Purchased protocol allows different purchase flows to be handled by common code in simple cases
+/// 
+/// For example you could route through to
+///
+///     func didPurchase<P:Purchased>(item:P) { ... }
+///
+/// for example
+///  - SwiftyStoreKit.completeTransactions (in .purchased and .restored cases)
+///  - SwiftyStoreKit.restorePurchases (for results.restoredPurchases)
+///  - SwiftyStoreKit.purchaseProducd (in .success case)
+public protocol Purchased {
+    var productId: String { get }
+    var quantity: Int { get }
+    var originalPurchaseDate: Date { get }
+}
+
+extension Purchase: Purchased {
+    public var originalPurchaseDate: Date {
+        guard let date = originalTransaction?.transactionDate ?? transaction.transactionDate else {
+            fatalError("there should always be a transaction date, so this should not happen...")
+        }
+        return  date
+    }
+}
+
+extension PurchaseDetails: Purchased {
+    public var originalPurchaseDate: Date {
+        guard let date = originalTransaction?.transactionDate ?? transaction.transactionDate else {
+            fatalError("there should always be a transaction date, so this should not happen...")
+        }
+        return  date
+    }
+}
+
 // Restored product
 // Restored product
 public struct Purchase {
 public struct Purchase {
     public let productId: String
     public let productId: String
@@ -33,9 +67,17 @@ public struct Purchase {
     public let transaction: PaymentTransaction
     public let transaction: PaymentTransaction
     public let originalTransaction: PaymentTransaction?
     public let originalTransaction: PaymentTransaction?
     public let needsFinishTransaction: Bool
     public let needsFinishTransaction: Bool
+    
+    public init(productId: String, quantity: Int, transaction: PaymentTransaction, originalTransaction: PaymentTransaction?, needsFinishTransaction: Bool) {
+        self.productId = productId
+        self.quantity = quantity
+        self.transaction = transaction
+        self.originalTransaction = originalTransaction
+        self.needsFinishTransaction = needsFinishTransaction
+    }
 }
 }
 
 
-// Purchased product
+/// Purchased product
 public struct PurchaseDetails {
 public struct PurchaseDetails {
     public let productId: String
     public let productId: String
     public let quantity: Int
     public let quantity: Int
@@ -43,14 +85,23 @@ public struct PurchaseDetails {
     public let transaction: PaymentTransaction
     public let transaction: PaymentTransaction
     public let originalTransaction: PaymentTransaction?
     public let originalTransaction: PaymentTransaction?
     public let needsFinishTransaction: Bool
     public let needsFinishTransaction: Bool
+    
+    public init(productId: String, quantity: Int, product: SKProduct, transaction: PaymentTransaction, originalTransaction: PaymentTransaction?, needsFinishTransaction: Bool) {
+        self.productId = productId
+        self.quantity = quantity
+        self.product = product
+        self.transaction = transaction
+        self.originalTransaction = originalTransaction
+        self.needsFinishTransaction = needsFinishTransaction
+    }
 }
 }
 
 
-//Conform to this protocol to provide custom receipt validator
+/// Conform to this protocol to provide custom receipt validator
 public protocol ReceiptValidator {
 public protocol ReceiptValidator {
 	func validate(receiptData: Data, completion: @escaping (VerifyReceiptResult) -> Void)
 	func validate(receiptData: Data, completion: @escaping (VerifyReceiptResult) -> Void)
 }
 }
 
 
-// Payment transaction
+/// Payment transaction
 public protocol PaymentTransaction {
 public protocol PaymentTransaction {
     var transactionDate: Date? { get }
     var transactionDate: Date? { get }
     var transactionState: SKPaymentTransactionState { get }
     var transactionState: SKPaymentTransactionState { get }
@@ -58,26 +109,37 @@ public protocol PaymentTransaction {
     var downloads: [SKDownload] { get }
     var downloads: [SKDownload] { get }
 }
 }
 
 
-// Add PaymentTransaction conformance to SKPaymentTransaction
+/// Add PaymentTransaction conformance to SKPaymentTransaction
 extension SKPaymentTransaction: PaymentTransaction { }
 extension SKPaymentTransaction: PaymentTransaction { }
 
 
-// Products information
+/// Products information
 public struct RetrieveResults {
 public struct RetrieveResults {
     public let retrievedProducts: Set<SKProduct>
     public let retrievedProducts: Set<SKProduct>
     public let invalidProductIDs: Set<String>
     public let invalidProductIDs: Set<String>
     public let error: Error?
     public let error: Error?
+    
+    public init(retrievedProducts: Set<SKProduct>, invalidProductIDs: Set<String>, error: Error?) {
+        self.retrievedProducts = retrievedProducts
+        self.invalidProductIDs = invalidProductIDs
+        self.error = error
+    }
 }
 }
 
 
-// Purchase result
+/// Purchase result
 public enum PurchaseResult {
 public enum PurchaseResult {
     case success(purchase: PurchaseDetails)
     case success(purchase: PurchaseDetails)
     case error(error: SKError)
     case error(error: SKError)
 }
 }
 
 
-// Restore purchase results
+/// Restore purchase results
 public struct RestoreResults {
 public struct RestoreResults {
     public let restoredPurchases: [Purchase]
     public let restoredPurchases: [Purchase]
     public let restoreFailedPurchases: [(SKError, String?)]
     public let restoreFailedPurchases: [(SKError, String?)]
+    
+    public init(restoredPurchases: [Purchase], restoreFailedPurchases: [(SKError, String?)]) {
+        self.restoredPurchases = restoredPurchases
+        self.restoreFailedPurchases = restoreFailedPurchases
+    }
 }
 }
 
 
 public typealias ShouldAddStorePaymentHandler = (_ payment: SKPayment, _ product: SKProduct) -> Bool
 public typealias ShouldAddStorePaymentHandler = (_ payment: SKPayment, _ product: SKProduct) -> Bool
@@ -85,28 +147,28 @@ public typealias UpdatedDownloadsHandler = (_ downloads: [SKDownload]) -> Void
 
 
 // MARK: Receipt verification
 // MARK: Receipt verification
 
 
-// Info for receipt returned by server
+/// Info for receipt returned by server
 public typealias ReceiptInfo = [String: AnyObject]
 public typealias ReceiptInfo = [String: AnyObject]
 
 
-// Fetch receipt result
+/// Fetch receipt result
 public enum FetchReceiptResult {
 public enum FetchReceiptResult {
     case success(receiptData: Data)
     case success(receiptData: Data)
     case error(error: ReceiptError)
     case error(error: ReceiptError)
 }
 }
 
 
-// Verify receipt result
+/// Verify receipt result
 public enum VerifyReceiptResult {
 public enum VerifyReceiptResult {
     case success(receipt: ReceiptInfo)
     case success(receipt: ReceiptInfo)
     case error(error: ReceiptError)
     case error(error: ReceiptError)
 }
 }
 
 
-// Result for Consumable and NonConsumable
+/// Result for Consumable and NonConsumable
 public enum VerifyPurchaseResult {
 public enum VerifyPurchaseResult {
     case purchased(item: ReceiptItem)
     case purchased(item: ReceiptItem)
     case notPurchased
     case notPurchased
 }
 }
 
 
-// Verify subscription result
+/// Verify subscription result
 public enum VerifySubscriptionResult {
 public enum VerifySubscriptionResult {
     case purchased(expiryDate: Date, items: [ReceiptItem])
     case purchased(expiryDate: Date, items: [ReceiptItem])
     case expired(expiryDate: Date, items: [ReceiptItem])
     case expired(expiryDate: Date, items: [ReceiptItem])
@@ -118,71 +180,101 @@ public enum SubscriptionType {
     case nonRenewing(validDuration: TimeInterval)
     case nonRenewing(validDuration: TimeInterval)
 }
 }
 
 
-public struct ReceiptItem {
-    // 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.
-    public let productId: String
-    // The number of items purchased. This value corresponds to the quantity property of the SKPayment object stored in the transaction’s payment property.
-    public let quantity: Int
-    // The transaction identifier of the item that was purchased. This value corresponds to the transaction’s transactionIdentifier property.
-    public let transactionId: String
-    // 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.
-    public let originalTransactionId: String
-    // The date and time that the item was purchased. This value corresponds to the transaction’s transactionDate property.
-    public let purchaseDate: 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.
-    public let originalPurchaseDate: Date
-    // The primary key for identifying subscription purchases.
-    public let webOrderLineItemId: String?
-    // 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.
-    public let subscriptionExpirationDate: 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.
-    public let cancellationDate: Date?
-
-    public let isTrialPeriod: Bool
-    
-    public let isInIntroOfferPeriod: Bool
-}
-
-// Error when managing receipt
+public struct ReceiptItem: Purchased, Codable {
+    
+    /// 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.
+    public var productId: String
+    
+    /// The number of items purchased. This value corresponds to the `quantity` property of the `SKPayment` object stored in the transaction’s payment property.
+    public var quantity: Int
+    
+    /// The transaction identifier of the item that was purchased. This value corresponds to the transaction’s `transactionIdentifier` property.
+    public var transactionId: String
+    
+    /// 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.
+    public var originalTransactionId: String
+    
+    /// The date and time that the item was purchased. This value corresponds to the transaction’s `transactionDate` property.
+    public var purchaseDate: 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.
+    public var originalPurchaseDate: Date
+    
+    /// The primary key for identifying subscription purchases.
+    public var webOrderLineItemId: String?
+    
+    /// 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.
+    public var subscriptionExpirationDate: 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.
+    public var cancellationDate: Date?
+    
+    /// Indicates whether or not the subscription item is currently within a given trial period.
+    public var isTrialPeriod: Bool
+    
+    /// Indicates whether or not the subscription item is currently within an intro offer period.
+    public var isInIntroOfferPeriod: Bool
+    
+    public init(productId: String, quantity: Int, transactionId: String, originalTransactionId: String, purchaseDate: Date, originalPurchaseDate: Date, webOrderLineItemId: String?, subscriptionExpirationDate: Date?, cancellationDate: Date?, isTrialPeriod: Bool, isInIntroOfferPeriod: Bool) {
+        self.productId = productId
+        self.quantity = quantity
+        self.transactionId = transactionId
+        self.originalTransactionId = originalTransactionId
+        self.purchaseDate = purchaseDate
+        self.originalPurchaseDate = originalPurchaseDate
+        self.webOrderLineItemId = webOrderLineItemId
+        self.subscriptionExpirationDate = subscriptionExpirationDate
+        self.cancellationDate = cancellationDate
+        self.isTrialPeriod = isTrialPeriod
+        self.isInIntroOfferPeriod = isInIntroOfferPeriod
+    }
+}
+
+/// Error when managing receipt
 public enum ReceiptError: Swift.Error {
 public enum ReceiptError: Swift.Error {
-    // No receipt data
+    /// No receipt data
     case noReceiptData
     case noReceiptData
-    // No data received
+    /// No data received
     case noRemoteData
     case noRemoteData
-    // Error when encoding HTTP body into JSON
+    /// Error when encoding HTTP body into JSON
     case requestBodyEncodeError(error: Swift.Error)
     case requestBodyEncodeError(error: Swift.Error)
-    // Error when proceeding request
+    /// Error when proceeding request
     case networkError(error: Swift.Error)
     case networkError(error: Swift.Error)
-    // Error when decoding response
+    /// Error when decoding response
     case jsonDecodeError(string: String?)
     case jsonDecodeError(string: String?)
-    // Receive invalid - bad status returned
+    /// Receive invalid - bad status returned
     case receiptInvalid(receipt: ReceiptInfo, status: ReceiptStatus)
     case receiptInvalid(receipt: ReceiptInfo, status: ReceiptStatus)
 }
 }
 
 
-// Status code returned by remote server
-// see Table 2-1  Status codes
+/// Status code returned by remote server
+/// 
+/// See Table 2-1  Status codes
 public enum ReceiptStatus: Int {
 public enum ReceiptStatus: Int {
-    // Not decodable status
+    /// Not decodable status
     case unknown = -2
     case unknown = -2
-    // No status returned
+    /// No status returned
     case none = -1
     case none = -1
-    // valid statu
+    /// valid statua
     case valid = 0
     case valid = 0
-    // The App Store could not read the JSON object you provided.
+    /// The App Store could not read the JSON object you provided.
     case jsonNotReadable = 21000
     case jsonNotReadable = 21000
-    // The data in the receipt-data property was malformed or missing.
+    /// The data in the receipt-data property was malformed or missing.
     case malformedOrMissingData = 21002
     case malformedOrMissingData = 21002
-    // The receipt could not be authenticated.
+    /// The receipt could not be authenticated.
     case receiptCouldNotBeAuthenticated = 21003
     case receiptCouldNotBeAuthenticated = 21003
-    // The shared secret you provided does not match the shared secret on file for your account.
+    /// The shared secret you provided does not match the shared secret on file for your account.
     case secretNotMatching = 21004
     case secretNotMatching = 21004
-    // The receipt server is not currently available.
+    /// The receipt server is not currently available.
     case receiptServerUnavailable = 21005
     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.
+    /// 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
     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.
+    ///  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
     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.
+    /// 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
     case productionEnvironment = 21008
 
 
     var isValid: Bool { return self == .valid}
     var isValid: Bool { return self == .valid}
@@ -190,51 +282,51 @@ public enum ReceiptStatus: Int {
 
 
 // Receipt field as defined in : https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1
 // 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 {
 public enum ReceiptInfoField: String {
-    // Bundle Identifier. This corresponds to the value of CFBundleIdentifier in the Info.plist file.
+    /// Bundle Identifier. This corresponds to the value of CFBundleIdentifier in the Info.plist file.
     case bundle_id
     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.
+    /// 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
     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.
+    /// 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
     case original_application_version
-    // The date when the app receipt was created.
+    /// The date when the app receipt was created.
     case creation_date
     case creation_date
-    // The date that the app receipt expires. This key is present only for apps purchased through the Volume Purchase Program.
+    /// The date that the app receipt expires. This key is present only for apps purchased through the Volume Purchase Program.
     case expiration_date
     case expiration_date
 
 
-    // The receipt for an in-app purchase.
+    /// The receipt for an in-app purchase.
     case in_app
     case in_app
 
 
     public enum InApp: String {
     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.
+        /// 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
         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.
+        /// 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
         case product_id
-        // The transaction identifier of the item that was purchased. This value corresponds to the transaction’s transactionIdentifier property.
+        /// The transaction identifier of the item that was purchased. This value corresponds to the transaction’s transactionIdentifier property.
         case transaction_id
         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.
+        /// 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
         case original_transaction_id
-        // The date and time that the item was purchased. This value corresponds to the transaction’s transactionDate property.
+        /// The date and time that the item was purchased. This value corresponds to the transaction’s transactionDate property.
         case purchase_date
         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.
+        /// 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
         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.
+        /// 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
         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.
+        /// 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
         case cancellation_date
         #if os(iOS) || os(tvOS)
         #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.
+        /// 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
         case app_item_id
         #endif
         #endif
-        // An arbitrary number that uniquely identifies a revision of your application. This key is not present for receipts created in the test environment.
+        /// 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
         case version_external_identifier
-        // The primary key for identifying subscription purchases.
+        /// The primary key for identifying subscription purchases.
         case web_order_line_item_id
         case web_order_line_item_id
     }
     }
 }
 }
 
 
 #if os(OSX)
 #if os(OSX)
     public enum ReceiptExitCode: Int32 {
     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
+        /// 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
         case notValid = 173
     }
     }
 #endif
 #endif

+ 129 - 124
SwiftyStoreKit/SwiftyStoreKit.swift

@@ -25,30 +25,30 @@
 import StoreKit
 import StoreKit
 
 
 public class SwiftyStoreKit {
 public class SwiftyStoreKit {
-
+    
     private let productsInfoController: ProductsInfoController
     private let productsInfoController: ProductsInfoController
-
+    
     fileprivate let paymentQueueController: PaymentQueueController
     fileprivate let paymentQueueController: PaymentQueueController
-
+    
     fileprivate let receiptVerificator: InAppReceiptVerificator
     fileprivate let receiptVerificator: InAppReceiptVerificator
-
+    
     init(productsInfoController: ProductsInfoController = ProductsInfoController(),
     init(productsInfoController: ProductsInfoController = ProductsInfoController(),
          paymentQueueController: PaymentQueueController = PaymentQueueController(paymentQueue: SKPaymentQueue.default()),
          paymentQueueController: PaymentQueueController = PaymentQueueController(paymentQueue: SKPaymentQueue.default()),
          receiptVerificator: InAppReceiptVerificator = InAppReceiptVerificator()) {
          receiptVerificator: InAppReceiptVerificator = InAppReceiptVerificator()) {
-
+        
         self.productsInfoController = productsInfoController
         self.productsInfoController = productsInfoController
         self.paymentQueueController = paymentQueueController
         self.paymentQueueController = paymentQueueController
         self.receiptVerificator = receiptVerificator
         self.receiptVerificator = receiptVerificator
     }
     }
-
+    
     // MARK: private methods
     // MARK: private methods
-    fileprivate func retrieveProductsInfo(_ productIds: Set<String>, completion: @escaping (RetrieveResults) -> Void) {
+    fileprivate func retrieveProductsInfo(_ productIds: Set<String>, completion: @escaping (RetrieveResults) -> Void) -> InAppProductRequest {
         return productsInfoController.retrieveProductsInfo(productIds, completion: completion)
         return productsInfoController.retrieveProductsInfo(productIds, completion: completion)
     }
     }
     
     
-    fileprivate func purchaseProduct(_ productId: String, quantity: Int = 1, atomically: Bool = true, applicationUsername: String = "", simulatesAskToBuyInSandbox: Bool = false, completion: @escaping ( PurchaseResult) -> Void) {
-
-        retrieveProductsInfo(Set([productId])) { result -> Void in
+    fileprivate func purchaseProduct(_ productId: String, quantity: Int = 1, atomically: Bool = true, applicationUsername: String = "", simulatesAskToBuyInSandbox: Bool = false, completion: @escaping ( PurchaseResult) -> Void) -> InAppProductRequest {
+        
+        return retrieveProductsInfo(Set([productId])) { result -> Void in
             if let product = result.retrievedProducts.first {
             if let product = result.retrievedProducts.first {
                 self.purchase(product: product, quantity: quantity, atomically: atomically, applicationUsername: applicationUsername, simulatesAskToBuyInSandbox: simulatesAskToBuyInSandbox, completion: completion)
                 self.purchase(product: product, quantity: quantity, atomically: atomically, applicationUsername: applicationUsername, simulatesAskToBuyInSandbox: simulatesAskToBuyInSandbox, completion: completion)
             } else if let error = result.error {
             } else if let error = result.error {
@@ -63,33 +63,33 @@ public class SwiftyStoreKit {
             }
             }
         }
         }
     }
     }
-
-    fileprivate func purchase(product: SKProduct, quantity: Int, atomically: Bool, applicationUsername: String = "", simulatesAskToBuyInSandbox: Bool = false, completion: @escaping (PurchaseResult) -> Void) {
-        paymentQueueController.startPayment(Payment(product: product, quantity: quantity, atomically: atomically, applicationUsername: applicationUsername, simulatesAskToBuyInSandbox: simulatesAskToBuyInSandbox) { result in
+    
+    fileprivate func purchase(product: SKProduct, quantity: Int, atomically: Bool, applicationUsername: String = "", simulatesAskToBuyInSandbox: Bool = false, paymentDiscount: PaymentDiscount? = nil, completion: @escaping (PurchaseResult) -> Void) {
+        paymentQueueController.startPayment(Payment(product: product, paymentDiscount: paymentDiscount, quantity: quantity, atomically: atomically, applicationUsername: applicationUsername, simulatesAskToBuyInSandbox: simulatesAskToBuyInSandbox) { result in
             
             
             completion(self.processPurchaseResult(result))
             completion(self.processPurchaseResult(result))
         })
         })
     }
     }
-
+    
     fileprivate func restorePurchases(atomically: Bool = true, applicationUsername: String = "", completion: @escaping (RestoreResults) -> Void) {
     fileprivate func restorePurchases(atomically: Bool = true, applicationUsername: String = "", completion: @escaping (RestoreResults) -> Void) {
-
+        
         paymentQueueController.restorePurchases(RestorePurchases(atomically: atomically, applicationUsername: applicationUsername) { results in
         paymentQueueController.restorePurchases(RestorePurchases(atomically: atomically, applicationUsername: applicationUsername) { results in
-
+            
             let results = self.processRestoreResults(results)
             let results = self.processRestoreResults(results)
             completion(results)
             completion(results)
         })
         })
     }
     }
-
+    
     fileprivate func completeTransactions(atomically: Bool = true, completion: @escaping ([Purchase]) -> Void) {
     fileprivate func completeTransactions(atomically: Bool = true, completion: @escaping ([Purchase]) -> Void) {
-
+        
         paymentQueueController.completeTransactions(CompleteTransactions(atomically: atomically, callback: completion))
         paymentQueueController.completeTransactions(CompleteTransactions(atomically: atomically, callback: completion))
     }
     }
-
+    
     fileprivate func finishTransaction(_ transaction: PaymentTransaction) {
     fileprivate func finishTransaction(_ transaction: PaymentTransaction) {
-
+        
         paymentQueueController.finishTransaction(transaction)
         paymentQueueController.finishTransaction(transaction)
     }
     }
-
+    
     private func processPurchaseResult(_ result: TransactionResult) -> PurchaseResult {
     private func processPurchaseResult(_ result: TransactionResult) -> PurchaseResult {
         switch result {
         switch result {
         case .purchased(let purchase):
         case .purchased(let purchase):
@@ -100,7 +100,7 @@ public class SwiftyStoreKit {
             return .error(error: storeInternalError(description: "Cannot restore product \(purchase.productId) from purchase path"))
             return .error(error: storeInternalError(description: "Cannot restore product \(purchase.productId) from purchase path"))
         }
         }
     }
     }
-
+    
     private func processRestoreResults(_ results: [TransactionResult]) -> RestoreResults {
     private func processRestoreResults(_ results: [TransactionResult]) -> RestoreResults {
         var restoredPurchases: [Purchase] = []
         var restoredPurchases: [Purchase] = []
         var restoreFailedPurchases: [(SKError, String?)] = []
         var restoreFailedPurchases: [(SKError, String?)] = []
@@ -117,106 +117,103 @@ public class SwiftyStoreKit {
         }
         }
         return RestoreResults(restoredPurchases: restoredPurchases, restoreFailedPurchases: restoreFailedPurchases)
         return RestoreResults(restoredPurchases: restoredPurchases, restoreFailedPurchases: restoreFailedPurchases)
     }
     }
-
+    
+    #if os(watchOS)
+    private func storeInternalError(code: SKErrorCode = SKErrorCode.unknown, description: String = "") -> SKError {
+        let error = NSError(domain: SKErrorDomain, code: code.rawValue, userInfo: [ NSLocalizedDescriptionKey: description ])
+        return SKError.init(Code: code, _nsError: error)
+    }
+    #else
     private func storeInternalError(code: SKError.Code = SKError.unknown, description: String = "") -> SKError {
     private func storeInternalError(code: SKError.Code = SKError.unknown, description: String = "") -> SKError {
         let error = NSError(domain: SKErrorDomain, code: code.rawValue, userInfo: [ NSLocalizedDescriptionKey: description ])
         let error = NSError(domain: SKErrorDomain, code: code.rawValue, userInfo: [ NSLocalizedDescriptionKey: description ])
         return SKError(_nsError: error)
         return SKError(_nsError: error)
     }
     }
+    #endif
 }
 }
 
 
 extension SwiftyStoreKit {
 extension SwiftyStoreKit {
-
+    
     // MARK: Singleton
     // MARK: Singleton
     fileprivate static let sharedInstance = SwiftyStoreKit()
     fileprivate static let sharedInstance = SwiftyStoreKit()
-
+    
     // MARK: Public methods - Purchases
     // MARK: Public methods - Purchases
     
     
-    /**
-     * Return NO if this device is not able or allowed to make payments
-     */
+    /// Check if the current device can make payments.
+    /// - returns: `false` if this device is not able or allowed to make payments
     public class var canMakePayments: Bool {
     public class var canMakePayments: Bool {
         return SKPaymentQueue.canMakePayments()
         return SKPaymentQueue.canMakePayments()
     }
     }
-
-    /**
-     *  Retrieve products information
-     *  - Parameter productIds: The set of product identifiers to retrieve corresponding products for
-     *  - Parameter completion: handler for result
-     */
-    public class func retrieveProductsInfo(_ productIds: Set<String>, completion: @escaping (RetrieveResults) -> Void) {
-
+    
+    /// Retrieve products information
+    /// - Parameter productIds: The set of product identifiers to retrieve corresponding products for
+    /// - Parameter completion: handler for result
+    /// - returns: A cancellable `InAppRequest` object 
+    @discardableResult
+    public class func retrieveProductsInfo(_ productIds: Set<String>, completion: @escaping (RetrieveResults) -> Void) -> InAppRequest {
         return sharedInstance.retrieveProductsInfo(productIds, completion: completion)
         return sharedInstance.retrieveProductsInfo(productIds, completion: completion)
     }
     }
-
-    /**
-     *  Purchase a product
-     *  - Parameter productId: productId as specified in iTunes Connect
-     *  - Parameter quantity: quantity of the product to be purchased
-     *  - 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 completion: handler for result
-     */
-    public class func purchaseProduct(_ productId: String, quantity: Int = 1, atomically: Bool = true, applicationUsername: String = "", simulatesAskToBuyInSandbox: Bool = false, completion: @escaping (PurchaseResult) -> Void) {
-
-        sharedInstance.purchaseProduct(productId, quantity: quantity, atomically: atomically, applicationUsername: applicationUsername, simulatesAskToBuyInSandbox: simulatesAskToBuyInSandbox, completion: completion)
+    
+    /// Purchase a product
+    ///  - Parameter productId: productId as specified in App Store Connect
+    ///  - Parameter quantity: quantity of the product to be purchased
+    ///  - 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 completion: handler for result
+    ///  - returns: A cancellable `InAppRequest` object   
+    @discardableResult
+    public class func purchaseProduct(_ productId: String, quantity: Int = 1, atomically: Bool = true, applicationUsername: String = "", simulatesAskToBuyInSandbox: Bool = false, completion: @escaping (PurchaseResult) -> Void) -> InAppRequest {
+        
+        return sharedInstance.purchaseProduct(productId, quantity: quantity, atomically: atomically, applicationUsername: applicationUsername, simulatesAskToBuyInSandbox: simulatesAskToBuyInSandbox, completion: completion)
     }
     }
     
     
-    /**
-     *  Purchase a product
-     *  - Parameter product: product to be purchased
-     *  - Parameter quantity: quantity of the product to be purchased
-     *  - 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 completion: handler for result
-     */
-    public class func purchaseProduct(_ product: SKProduct, quantity: Int = 1, atomically: Bool = true, applicationUsername: String = "", simulatesAskToBuyInSandbox: Bool = false, completion: @escaping ( PurchaseResult) -> Void) {
+    /// Purchase a product
+    ///  - Parameter product: product to be purchased
+    ///  - Parameter quantity: quantity of the product to be purchased
+    ///  - 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 product: optional discount to be applied. Must be of `SKProductPayment` type
+    ///  - Parameter completion: handler for result
+    public class func purchaseProduct(_ product: SKProduct, quantity: Int = 1, atomically: Bool = true, applicationUsername: String = "", simulatesAskToBuyInSandbox: Bool = false, paymentDiscount: PaymentDiscount? = nil, completion: @escaping ( PurchaseResult) -> Void) {
         
         
-        sharedInstance.purchase(product: product, quantity: quantity, atomically: atomically, applicationUsername: applicationUsername, simulatesAskToBuyInSandbox: simulatesAskToBuyInSandbox, completion: completion)
+        sharedInstance.purchase(product: product, quantity: quantity, atomically: atomically, applicationUsername: applicationUsername, simulatesAskToBuyInSandbox: simulatesAskToBuyInSandbox, paymentDiscount: paymentDiscount, completion: completion)
     }
     }
-
-    /**
-     *  Restore purchases
-     *  - 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 completion: handler for result
-     */
+    
+    /// Restore purchases
+    ///  - 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 completion: handler for result
     public class func restorePurchases(atomically: Bool = true, applicationUsername: String = "", completion: @escaping (RestoreResults) -> Void) {
     public class func restorePurchases(atomically: Bool = true, applicationUsername: String = "", completion: @escaping (RestoreResults) -> Void) {
-
+        
         sharedInstance.restorePurchases(atomically: atomically, applicationUsername: applicationUsername, completion: completion)
         sharedInstance.restorePurchases(atomically: atomically, applicationUsername: applicationUsername, completion: completion)
     }
     }
-
-    /**
-     *  Complete transactions
-     *  - Parameter atomically: whether the product is purchased atomically (e.g. finishTransaction is called immediately)
-     *  - Parameter completion: handler for result
-     */
+    
+    /// Complete transactions
+    ///  - Parameter atomically: whether the product is purchased atomically (e.g. `finishTransaction` is called immediately)
+    ///  - Parameter completion: handler for result
     public class func completeTransactions(atomically: Bool = true, completion: @escaping ([Purchase]) -> Void) {
     public class func completeTransactions(atomically: Bool = true, completion: @escaping ([Purchase]) -> Void) {
-
+        
         sharedInstance.completeTransactions(atomically: atomically, completion: completion)
         sharedInstance.completeTransactions(atomically: atomically, completion: completion)
     }
     }
-
-    /**
-     *  Finish a transaction
-     *  Once the content has been delivered, call this method to finish a transaction that was performed non-atomically
-     *  - Parameter transaction: transaction to finish
-     */
+    
+    /// Finish a transaction
+    /// 
+    /// Once the content has been delivered, call this method to finish a transaction that was performed non-atomically
+    /// - Parameter transaction: transaction to finish
     public class func finishTransaction(_ transaction: PaymentTransaction) {
     public class func finishTransaction(_ transaction: PaymentTransaction) {
-
+        
         sharedInstance.finishTransaction(transaction)
         sharedInstance.finishTransaction(transaction)
     }
     }
     
     
-    /**
-     * Register a handler for SKPaymentQueue.shouldAddStorePayment delegate method in iOS 11
-     */
+    /// Register a handler for `SKPaymentQueue.shouldAddStorePayment` delegate method.
+    /// - requires: iOS 11.0+
     public static var shouldAddStorePaymentHandler: ShouldAddStorePaymentHandler? {
     public static var shouldAddStorePaymentHandler: ShouldAddStorePaymentHandler? {
         didSet {
         didSet {
             sharedInstance.paymentQueueController.shouldAddStorePaymentHandler = shouldAddStorePaymentHandler
             sharedInstance.paymentQueueController.shouldAddStorePaymentHandler = shouldAddStorePaymentHandler
         }
         }
     }
     }
     
     
-    /**
-     * Register a handler for paymentQueue(_:updatedDownloads:)
-     */
+    /// Register a handler for `paymentQueue(_:updatedDownloads:)`
+    /// - seealso: `paymentQueue(_:updatedDownloads:)`
     public static var updatedDownloadsHandler: UpdatedDownloadsHandler? {
     public static var updatedDownloadsHandler: UpdatedDownloadsHandler? {
         didSet {
         didSet {
             sharedInstance.paymentQueueController.updatedDownloadsHandler = updatedDownloadsHandler
             sharedInstance.paymentQueueController.updatedDownloadsHandler = updatedDownloadsHandler
@@ -238,60 +235,56 @@ extension SwiftyStoreKit {
 }
 }
 
 
 extension SwiftyStoreKit {
 extension SwiftyStoreKit {
-
+    
     // MARK: Public methods - Receipt verification
     // MARK: Public methods - Receipt verification
-
-    /**
-     * 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`.
     public static var localReceiptData: Data? {
     public static var localReceiptData: Data? {
         return sharedInstance.receiptVerificator.appStoreReceiptData
         return sharedInstance.receiptVerificator.appStoreReceiptData
     }
     }
-
-    /**
-     *  Verify application receipt
-     *  - Parameter validator: receipt validator to use
-     *  - Parameter forceRefresh: If true, refreshes the receipt even if one already exists.
-     *  - Parameter completion: handler for result
-     */
-    public class func verifyReceipt(using validator: ReceiptValidator, forceRefresh: Bool = false, completion: @escaping (VerifyReceiptResult) -> Void) {
-
-        sharedInstance.receiptVerificator.verifyReceipt(using: validator, forceRefresh: forceRefresh, completion: completion)
+    
+    /// Verify application receipt
+    /// - Parameter validator: receipt validator to use
+    /// - Parameter forceRefresh: If `true`, refreshes the receipt even if one already exists.
+    /// - Parameter completion: handler for result
+    /// - returns: A cancellable `InAppRequest` object 
+    @discardableResult
+    public class func verifyReceipt(using validator: ReceiptValidator, forceRefresh: Bool = false, completion: @escaping (VerifyReceiptResult) -> Void) -> InAppRequest? {
+        
+        return sharedInstance.receiptVerificator.verifyReceipt(using: validator, 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)
+    /// Fetch application receipt
+    /// - Parameter forceRefresh: If true, refreshes the receipt even if one already exists.
+    /// - Parameter completion: handler for result
+    /// - returns: A cancellable `InAppRequest` object 
+    @discardableResult
+    public class func fetchReceipt(forceRefresh: Bool, completion: @escaping (FetchReceiptResult) -> Void) -> InAppRequest? {
+        
+        return 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
-     *  - Parameter inReceipt: the receipt to use for looking up the purchase
-     *  - return: either notPurchased or purchased
-     */
+    ///  Verify the purchase of a Consumable or NonConsumable product in a receipt
+    ///  - Parameter productId: the product id of the purchase to verify
+    ///  - Parameter inReceipt: the receipt to use for looking up the purchase
+    ///  - returns: A `VerifyPurchaseResult`, which may be either `notPurchased` or `purchased`.
     public class func verifyPurchase(productId: String, inReceipt receipt: ReceiptInfo) -> VerifyPurchaseResult {
     public class func verifyPurchase(productId: String, inReceipt receipt: ReceiptInfo) -> VerifyPurchaseResult {
-
+        
         return InAppReceipt.verifyPurchase(productId: productId, inReceipt: receipt)
         return InAppReceipt.verifyPurchase(productId: productId, inReceipt: receipt)
     }
     }
-
+    
     /**
     /**
      *  Verify the validity of a subscription (auto-renewable, free or non-renewing) in a receipt.
      *  Verify the validity of a subscription (auto-renewable, free or non-renewing) in a receipt.
      *
      *
      *  This method extracts all transactions matching the given productId and sorts them by date in descending order. It then compares the first transaction expiry date against the receipt date to determine its validity.
      *  This method extracts all transactions matching the given productId and sorts them by date in descending order. It then compares the first transaction expiry date against the receipt date to determine its validity.
-     *  - Parameter type: .autoRenewable or .nonRenewing.
+     *  - Parameter type: `.autoRenewable` or `.nonRenewing`.
      *  - Parameter productId: The product id of the subscription to verify.
      *  - Parameter productId: The product id of the subscription to verify.
      *  - Parameter receipt: The receipt to use for looking up the subscription.
      *  - Parameter receipt: The receipt to use for looking up the subscription.
      *  - Parameter validUntil: Date to check against the expiry date of the subscription. This is only used if a date is not found in the receipt.
      *  - Parameter validUntil: Date to check against the expiry date of the subscription. This is only used if a date is not found in the receipt.
-     *  - return: Either .notPurchased or .purchased / .expired with the expiry date found in the receipt.
+     *  - returns: Either `.notPurchased` or `.purchased` / `.expired` with the expiry date found in the receipt.
      */
      */
     public class func verifySubscription(ofType type: SubscriptionType, productId: String, inReceipt receipt: ReceiptInfo, validUntil date: Date = Date()) -> VerifySubscriptionResult {
     public class func verifySubscription(ofType type: SubscriptionType, productId: String, inReceipt receipt: ReceiptInfo, validUntil date: Date = Date()) -> VerifySubscriptionResult {
-
+        
         return InAppReceipt.verifySubscriptions(ofType: type, productIds: [productId], inReceipt: receipt, validUntil: date)
         return InAppReceipt.verifySubscriptions(ofType: type, productIds: [productId], inReceipt: receipt, validUntil: date)
     }
     }
     
     
@@ -301,14 +294,26 @@ extension SwiftyStoreKit {
      *  This method extracts all transactions matching the given productIds and sorts them by date in descending order. It then compares the first transaction expiry date against the receipt date, to determine its validity.
      *  This method extracts all transactions matching the given productIds and sorts them by date in descending order. It then compares the first transaction expiry date against the receipt date, to determine its validity.
      *  - Note: You can use this method to check the validity of (mutually exclusive) subscriptions in a subscription group.
      *  - Note: You can use this method to check the validity of (mutually exclusive) subscriptions in a subscription group.
      *  - Remark: The type parameter determines how the expiration dates are calculated for all subscriptions. Make sure all productIds match the specified subscription type to avoid incorrect results.
      *  - Remark: The type parameter determines how the expiration dates are calculated for all subscriptions. Make sure all productIds match the specified subscription type to avoid incorrect results.
-     *  - Parameter type: .autoRenewable or .nonRenewing.
-     *  - Parameter productIds: The product ids of the subscriptions to verify.
+     *  - Parameter type: `.autoRenewable` or `.nonRenewing`.
+     *  - Parameter productIds: The product IDs of the subscriptions to verify.
      *  - Parameter receipt: The receipt to use for looking up the subscriptions
      *  - Parameter receipt: The receipt to use for looking up the subscriptions
      *  - Parameter validUntil: Date to check against the expiry date of the subscriptions. This is only used if a date is not found in the receipt.
      *  - Parameter validUntil: Date to check against the expiry date of the subscriptions. This is only used if a date is not found in the receipt.
-     *  - return: Either .notPurchased or .purchased / .expired with the expiry date found in the receipt.
+     *  - returns: Either `.notPurchased` or `.purchased` / `.expired` with the expiry date found in the receipt.
      */
      */
     public class func verifySubscriptions(ofType type: SubscriptionType = .autoRenewable, productIds: Set<String>, inReceipt receipt: ReceiptInfo, validUntil date: Date = Date()) -> VerifySubscriptionResult {
     public class func verifySubscriptions(ofType type: SubscriptionType = .autoRenewable, productIds: Set<String>, inReceipt receipt: ReceiptInfo, validUntil date: Date = Date()) -> VerifySubscriptionResult {
-
+        
         return InAppReceipt.verifySubscriptions(ofType: type, productIds: productIds, inReceipt: receipt, validUntil: date)
         return InAppReceipt.verifySubscriptions(ofType: type, productIds: productIds, inReceipt: receipt, validUntil: date)
     }
     }
+    
+    ///  Get the distinct product identifiers from receipt.
+    ///
+    /// This Method extracts all product identifiers. (Including cancelled ones).
+    /// - Note: You can use this method to get all unique product identifiers from receipt.
+    /// - Parameter type: `.autoRenewable` or `.nonRenewing`.
+    /// - Parameter receipt: The receipt to use for looking up product identifiers.
+    /// - returns: Either `Set<String>` or `nil`.
+    public class func getDistinctPurchaseIds(ofType type: SubscriptionType = .autoRenewable, inReceipt receipt: ReceiptInfo) -> Set<String>? {
+        
+        return InAppReceipt.getDistinctPurchaseIds(ofType: type, inReceipt: receipt)
+    }
 }
 }

+ 58 - 1
SwiftyStoreKitTests/InAppReceiptTests.swift

@@ -26,7 +26,6 @@
 import XCTest
 import XCTest
 import SwiftyStoreKit
 import SwiftyStoreKit
 
 
-// swiftlint:disable file_length
 private extension TimeInterval {
 private extension TimeInterval {
     var millisecondsNSString: NSString {
     var millisecondsNSString: NSString {
         return String(format: "%.0f", self * 1000) as NSString
         return String(format: "%.0f", self * 1000) as NSString
@@ -36,6 +35,7 @@ private extension TimeInterval {
 extension ReceiptItem: Equatable {
 extension ReceiptItem: Equatable {
 
 
     init(productId: String, purchaseDate: Date, subscriptionExpirationDate: Date? = nil, cancellationDate: Date? = nil, isTrialPeriod: Bool = false, isInIntroOfferPeriod: Bool = false) {
     init(productId: String, purchaseDate: Date, subscriptionExpirationDate: Date? = nil, cancellationDate: Date? = nil, isTrialPeriod: Bool = false, isInIntroOfferPeriod: Bool = false) {
+        self.init(productId: productId, quantity: 1, transactionId: UUID().uuidString, originalTransactionId: UUID().uuidString, purchaseDate: purchaseDate, originalPurchaseDate: purchaseDate, webOrderLineItemId: UUID().uuidString, subscriptionExpirationDate: subscriptionExpirationDate, cancellationDate: cancellationDate, isTrialPeriod: isTrialPeriod, isInIntroOfferPeriod: isInIntroOfferPeriod)
         self.productId = productId
         self.productId = productId
         self.quantity = 1
         self.quantity = 1
         self.purchaseDate = purchaseDate
         self.purchaseDate = purchaseDate
@@ -107,6 +107,7 @@ extension VerifyPurchaseResult: Equatable {
     }
     }
 }
 }
 
 
+// swiftlint:disable file_length
 class InAppReceiptTests: XCTestCase {
 class InAppReceiptTests: XCTestCase {
 
 
     // MARK: Verify Purchase
     // MARK: Verify Purchase
@@ -382,6 +383,62 @@ class InAppReceiptTests: XCTestCase {
         let expectedSubscriptionResult = VerifySubscriptionResult.purchased(expiryDate: newerExpirationDate, items: [newerItem, olderItem])
         let expectedSubscriptionResult = VerifySubscriptionResult.purchased(expiryDate: newerExpirationDate, items: [newerItem, olderItem])
         XCTAssertEqual(verifySubscriptionResult, expectedSubscriptionResult)
         XCTAssertEqual(verifySubscriptionResult, expectedSubscriptionResult)
     }
     }
+    
+    // MARK: Get Distinct Purchase Identifiers, empty receipt item tests
+    func testGetDistinctPurchaseIds_when_noReceipt_then_resultIsNil() {
+
+        let receiptRequestDate = makeDateAtMidnight(year: 2017, month: 5, day: 14)
+        let receipt = makeReceipt(items: [], requestDate: receiptRequestDate)
+
+        let getdistinctProductIdsResult = SwiftyStoreKit.getDistinctPurchaseIds(ofType: .autoRenewable, inReceipt: receipt)
+        XCTAssertNil(getdistinctProductIdsResult)
+    }
+    
+    // MARK: Get Distinct Purchase Identifiers, multiple receipt item tests
+    func testGetDistinctPurchaseIds_when_Receipt_then_resultIsNotNil() {
+
+        let receiptRequestDateOne = makeDateAtMidnight(year: 2020, month: 2, day: 20)
+        let purchaseDateOne = makeDateAtMidnight(year: 2020, month: 2, day: 1)
+        let purchaseDateTwo = makeDateAtMidnight(year: 2020, month: 1, day: 1)
+        
+        let productId1 = "product1"
+        let productId2 = "product2"
+        
+        let product1 = ReceiptItem(productId: productId1, purchaseDate: purchaseDateOne)
+        let product2 = ReceiptItem(productId: productId2, purchaseDate: purchaseDateTwo)
+        
+        let receipt = makeReceipt(items: [product1, product2], requestDate: receiptRequestDateOne)
+
+        let getdistinctProductIdsResult = SwiftyStoreKit.getDistinctPurchaseIds(ofType: .autoRenewable, inReceipt: receipt)
+                
+        XCTAssertNotNil(getdistinctProductIdsResult)
+    }
+    
+    // MARK: Get Distinct Purchase Identifiers, multiple non unique product identifiers tests
+    func testGetDistinctPurchaseIds_when_nonUniqueIdentifiers_then_resultIsUnique() {
+
+        let receiptRequestDateOne = makeDateAtMidnight(year: 2020, month: 2, day: 20)
+        let purchaseDateOne = makeDateAtMidnight(year: 2020, month: 2, day: 1)
+        let purchaseDateTwo = makeDateAtMidnight(year: 2020, month: 2, day: 2)
+        let purchaseDateThree = makeDateAtMidnight(year: 2020, month: 2, day: 3)
+        let purchaseDateFour = makeDateAtMidnight(year: 2020, month: 2, day: 4)
+
+        let productId1 = "product1"
+        let productId2 = "product2"
+        let productId3 = "product1"
+        let productId4 = "product2"
+
+        let product1 = ReceiptItem(productId: productId1, purchaseDate: purchaseDateOne)
+        let product2 = ReceiptItem(productId: productId2, purchaseDate: purchaseDateTwo)
+        let product3 = ReceiptItem(productId: productId3, purchaseDate: purchaseDateThree)
+        let product4 = ReceiptItem(productId: productId4, purchaseDate: purchaseDateFour)
+
+        let receipt = makeReceipt(items: [product1, product2, product3, product4], requestDate: receiptRequestDateOne)
+
+        let getdistinctProductIdsResult = SwiftyStoreKit.getDistinctPurchaseIds(ofType: .autoRenewable, inReceipt: receipt)
+        let expectedProductIdsResult = Set([productId1, productId2, productId3, productId4])
+        XCTAssertEqual(getdistinctProductIdsResult, expectedProductIdsResult)
+    }
 
 
     // MARK: Helper methods
     // MARK: Helper methods
     func makeReceipt(items: [ReceiptItem], requestDate: Date) -> [String: AnyObject] {
     func makeReceipt(items: [ReceiptItem], requestDate: Date) -> [String: AnyObject] {

+ 16 - 8
SwiftyStoreKitTests/InAppReceiptVerificatorTests.swift

@@ -76,7 +76,7 @@ class InAppReceiptVerificatorTests: XCTestCase {
         let verificator = InAppReceiptVerificator(appStoreReceiptURL: nil)
         let verificator = InAppReceiptVerificator(appStoreReceiptURL: nil)
         
         
         var refreshCalled = false
         var refreshCalled = false
-        verificator.verifyReceipt(using: validator, forceRefresh: false, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
+        let request = verificator.verifyReceipt(using: validator, forceRefresh: false, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
             
             
             refreshCalled = true
             refreshCalled = true
             return TestInAppReceiptRefreshRequest(receiptProperties: properties, callback: callback)
             return TestInAppReceiptRefreshRequest(receiptProperties: properties, callback: callback)
@@ -84,6 +84,7 @@ class InAppReceiptVerificatorTests: XCTestCase {
         }, completion: { _ in
         }, completion: { _ in
             
             
         })
         })
+        XCTAssertNotNil(request)
         XCTAssertTrue(refreshCalled)
         XCTAssertTrue(refreshCalled)
     }
     }
 
 
@@ -95,7 +96,7 @@ class InAppReceiptVerificatorTests: XCTestCase {
         let verificator = InAppReceiptVerificator(appStoreReceiptURL: testReceiptURL)
         let verificator = InAppReceiptVerificator(appStoreReceiptURL: testReceiptURL)
         
         
         var refreshCalled = false
         var refreshCalled = false
-        verificator.verifyReceipt(using: validator, forceRefresh: false, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
+        let request = verificator.verifyReceipt(using: validator, forceRefresh: false, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
             
             
             refreshCalled = true
             refreshCalled = true
             return TestInAppReceiptRefreshRequest(receiptProperties: properties, callback: callback)
             return TestInAppReceiptRefreshRequest(receiptProperties: properties, callback: callback)
@@ -103,6 +104,7 @@ class InAppReceiptVerificatorTests: XCTestCase {
         }, completion: { _ in
         }, completion: { _ in
             
             
         })
         })
+        XCTAssertNotNil(request)
         XCTAssertTrue(refreshCalled)
         XCTAssertTrue(refreshCalled)
     }
     }
     
     
@@ -115,7 +117,7 @@ class InAppReceiptVerificatorTests: XCTestCase {
         let verificator = InAppReceiptVerificator(appStoreReceiptURL: testReceiptURL)
         let verificator = InAppReceiptVerificator(appStoreReceiptURL: testReceiptURL)
         
         
         var refreshCalled = false
         var refreshCalled = false
-        verificator.verifyReceipt(using: validator, forceRefresh: true, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
+        let request = verificator.verifyReceipt(using: validator, forceRefresh: true, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
             
             
             refreshCalled = true
             refreshCalled = true
             return TestInAppReceiptRefreshRequest(receiptProperties: properties, callback: callback)
             return TestInAppReceiptRefreshRequest(receiptProperties: properties, callback: callback)
@@ -123,6 +125,7 @@ class InAppReceiptVerificatorTests: XCTestCase {
         }, completion: { _ in
         }, completion: { _ in
             
             
         })
         })
+        XCTAssertNotNil(request)
         XCTAssertTrue(refreshCalled)
         XCTAssertTrue(refreshCalled)
     }
     }
 
 
@@ -132,7 +135,7 @@ class InAppReceiptVerificatorTests: XCTestCase {
         let verificator = InAppReceiptVerificator(appStoreReceiptURL: nil)
         let verificator = InAppReceiptVerificator(appStoreReceiptURL: nil)
         let refreshError = NSError(domain: "", code: 0, userInfo: nil)
         let refreshError = NSError(domain: "", code: 0, userInfo: nil)
         
         
-        verificator.verifyReceipt(using: validator, forceRefresh: false, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
+        let request = verificator.verifyReceipt(using: validator, forceRefresh: false, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
             
             
             callback(.error(e: refreshError))
             callback(.error(e: refreshError))
             return TestInAppReceiptRefreshRequest(receiptProperties: properties, callback: callback)
             return TestInAppReceiptRefreshRequest(receiptProperties: properties, callback: callback)
@@ -141,6 +144,7 @@ class InAppReceiptVerificatorTests: XCTestCase {
             
             
             XCTAssertEqual(result, VerifyReceiptResult.error(error: ReceiptError.networkError(error: refreshError)))
             XCTAssertEqual(result, VerifyReceiptResult.error(error: ReceiptError.networkError(error: refreshError)))
         })
         })
+        XCTAssertNotNil(request)
     }
     }
 
 
     func testVerifyReceipt_when_appStoreReceiptURLIsNil_refreshCallbackSuccess_receiptDataNotWritten_then_errorNoReceiptData_validateNotCalled() {
     func testVerifyReceipt_when_appStoreReceiptURLIsNil_refreshCallbackSuccess_receiptDataNotWritten_then_errorNoReceiptData_validateNotCalled() {
@@ -148,7 +152,7 @@ class InAppReceiptVerificatorTests: XCTestCase {
         let validator = TestReceiptValidator()
         let validator = TestReceiptValidator()
         let verificator = InAppReceiptVerificator(appStoreReceiptURL: nil)
         let verificator = InAppReceiptVerificator(appStoreReceiptURL: nil)
         
         
-        verificator.verifyReceipt(using: validator, forceRefresh: false, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
+        let request = verificator.verifyReceipt(using: validator, forceRefresh: false, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
             
             
             callback(.success)
             callback(.success)
             return TestInAppReceiptRefreshRequest(receiptProperties: properties, callback: callback)
             return TestInAppReceiptRefreshRequest(receiptProperties: properties, callback: callback)
@@ -157,6 +161,7 @@ class InAppReceiptVerificatorTests: XCTestCase {
 
 
             XCTAssertEqual(result, VerifyReceiptResult.error(error: ReceiptError.noReceiptData))
             XCTAssertEqual(result, VerifyReceiptResult.error(error: ReceiptError.noReceiptData))
         })
         })
+        XCTAssertNotNil(request)
         XCTAssertFalse(validator.validateCalled)
         XCTAssertFalse(validator.validateCalled)
     }
     }
 
 
@@ -167,7 +172,7 @@ class InAppReceiptVerificatorTests: XCTestCase {
         let validator = TestReceiptValidator()
         let validator = TestReceiptValidator()
         let verificator = InAppReceiptVerificator(appStoreReceiptURL: nil)
         let verificator = InAppReceiptVerificator(appStoreReceiptURL: nil)
         
         
-        verificator.verifyReceipt(using: validator, forceRefresh: false, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
+        let request = verificator.verifyReceipt(using: validator, forceRefresh: false, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
             
             
             writeReceiptData(to: testReceiptURL)
             writeReceiptData(to: testReceiptURL)
             callback(.success)
             callback(.success)
@@ -177,6 +182,7 @@ class InAppReceiptVerificatorTests: XCTestCase {
             
             
             XCTAssertEqual(result, VerifyReceiptResult.error(error: ReceiptError.noReceiptData))
             XCTAssertEqual(result, VerifyReceiptResult.error(error: ReceiptError.noReceiptData))
         })
         })
+        XCTAssertNotNil(request)
         XCTAssertFalse(validator.validateCalled)
         XCTAssertFalse(validator.validateCalled)
         removeReceiptData(at: testReceiptURL)
         removeReceiptData(at: testReceiptURL)
     }
     }
@@ -188,7 +194,7 @@ class InAppReceiptVerificatorTests: XCTestCase {
         let validator = TestReceiptValidator()
         let validator = TestReceiptValidator()
         let verificator = InAppReceiptVerificator(appStoreReceiptURL: testReceiptURL)
         let verificator = InAppReceiptVerificator(appStoreReceiptURL: testReceiptURL)
         
         
-        verificator.verifyReceipt(using: validator, forceRefresh: false, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
+        let request = verificator.verifyReceipt(using: validator, forceRefresh: false, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
             
             
             writeReceiptData(to: testReceiptURL)
             writeReceiptData(to: testReceiptURL)
             callback(.success)
             callback(.success)
@@ -197,6 +203,7 @@ class InAppReceiptVerificatorTests: XCTestCase {
         }, completion: { _ in
         }, completion: { _ in
             
             
         })
         })
+        XCTAssertNil(request)
         XCTAssertTrue(validator.validateCalled)
         XCTAssertTrue(validator.validateCalled)
         removeReceiptData(at: testReceiptURL)
         removeReceiptData(at: testReceiptURL)
     }
     }
@@ -210,7 +217,7 @@ class InAppReceiptVerificatorTests: XCTestCase {
         let validator = TestReceiptValidator()
         let validator = TestReceiptValidator()
         let verificator = InAppReceiptVerificator(appStoreReceiptURL: testReceiptURL)
         let verificator = InAppReceiptVerificator(appStoreReceiptURL: testReceiptURL)
         
         
-        verificator.verifyReceipt(using: validator, forceRefresh: false, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
+        let request = verificator.verifyReceipt(using: validator, forceRefresh: false, refresh: { (properties, callback) -> InAppReceiptRefreshRequest in
             
             
             XCTFail("refresh should not be called if we already have a receipt")
             XCTFail("refresh should not be called if we already have a receipt")
             return TestInAppReceiptRefreshRequest(receiptProperties: properties, callback: callback)
             return TestInAppReceiptRefreshRequest(receiptProperties: properties, callback: callback)
@@ -218,6 +225,7 @@ class InAppReceiptVerificatorTests: XCTestCase {
         }, completion: { _ in
         }, completion: { _ in
             
             
         })
         })
+        XCTAssertNil(request)
         XCTAssertTrue(validator.validateCalled)
         XCTAssertTrue(validator.validateCalled)
         removeReceiptData(at: testReceiptURL)
         removeReceiptData(at: testReceiptURL)
     }
     }

+ 1 - 12
SwiftyStoreKitTests/PaymentQueueControllerTests.swift

@@ -28,17 +28,6 @@ import XCTest
 import StoreKit
 import StoreKit
 @testable import SwiftyStoreKit
 @testable import SwiftyStoreKit
 
 
-extension Payment {
-    init(product: SKProduct, quantity: Int, atomically: Bool, applicationUsername: String, simulatesAskToBuyInSandbox: Bool, callback: @escaping (TransactionResult) -> Void) {
-        self.product = product
-        self.quantity = quantity
-        self.atomically = atomically
-        self.applicationUsername = applicationUsername
-        self.simulatesAskToBuyInSandbox = simulatesAskToBuyInSandbox
-        self.callback = callback
-    }
-}
-
 class PaymentQueueControllerTests: XCTestCase {
 class PaymentQueueControllerTests: XCTestCase {
 
 
     // MARK: init/deinit
     // MARK: init/deinit
@@ -301,6 +290,6 @@ class PaymentQueueControllerTests: XCTestCase {
     func makeTestPayment(productIdentifier: String, quantity: Int = 1, atomically: Bool = true, callback: @escaping (TransactionResult) -> Void) -> Payment {
     func makeTestPayment(productIdentifier: String, quantity: Int = 1, atomically: Bool = true, callback: @escaping (TransactionResult) -> Void) -> Payment {
 
 
         let testProduct = TestProduct(productIdentifier: productIdentifier)
         let testProduct = TestProduct(productIdentifier: productIdentifier)
-        return Payment(product: testProduct, quantity: quantity, atomically: atomically, applicationUsername: "", simulatesAskToBuyInSandbox: false, callback: callback)
+        return Payment(product: testProduct, paymentDiscount: nil, quantity: quantity, atomically: atomically, applicationUsername: "", simulatesAskToBuyInSandbox: false, callback: callback)
     }
     }
 }
 }

+ 1 - 1
SwiftyStoreKitTests/PaymentsControllerTests.swift

@@ -240,7 +240,7 @@ class PaymentsControllerTests: XCTestCase {
 
 
     func makeTestPayment(product: SKProduct, atomically: Bool = true, callback: @escaping (TransactionResult) -> Void) -> Payment {
     func makeTestPayment(product: SKProduct, atomically: Bool = true, callback: @escaping (TransactionResult) -> Void) -> Payment {
 
 
-        return Payment(product: product, quantity: 1, atomically: atomically, applicationUsername: "", simulatesAskToBuyInSandbox: false, callback: callback)
+        return Payment(product: product, paymentDiscount: nil, quantity: 1, atomically: atomically, applicationUsername: "", simulatesAskToBuyInSandbox: false, callback: callback)
     }
     }
 
 
     func makeTestPayment(productIdentifier: String, atomically: Bool = true, callback: @escaping (TransactionResult) -> Void) -> Payment {
     func makeTestPayment(productIdentifier: String, atomically: Bool = true, callback: @escaping (TransactionResult) -> Void) -> Payment {

+ 1 - 1
SwiftyStoreKitTests/ProductsInfoControllerTests.swift

@@ -1,5 +1,5 @@
 //
 //
-//  ProductsInfoControllerTests.swift
+// ProductsInfoControllerTests.swift
 // SwiftyStoreKit
 // SwiftyStoreKit
 //
 //
 // Copyright (c) 2017 Andrea Bizzotto (bizz84@gmail.com)
 // Copyright (c) 2017 Andrea Bizzotto (bizz84@gmail.com)

+ 6 - 0
scripts/build.sh

@@ -19,6 +19,12 @@ echo "/* Build: SwiftyStoreKit_tvOS */"
 echo "/******************************/${normal}"
 echo "/******************************/${normal}"
 set -o pipefail && xcodebuild -project SwiftyStoreKit.xcodeproj -target SwiftyStoreKit_tvOS | tee xcodebuild.log | xcpretty
 set -o pipefail && xcodebuild -project SwiftyStoreKit.xcodeproj -target SwiftyStoreKit_tvOS | tee xcodebuild.log | xcpretty
 
 
+echo ""
+echo "${bold}/******************************/"
+echo "/* Build: SwiftyStoreKit_watchOS */"
+echo "/******************************/${normal}"
+set -o pipefail && xcodebuild -project SwiftyStoreKit.xcodeproj -target SwiftyStoreKit_watchOS | tee xcodebuild.log | xcpretty
+
 echo ""
 echo ""
 echo "${bold}/****************************/"
 echo "${bold}/****************************/"
 echo "/* Run: SwiftyStoreKitTests */"
 echo "/* Run: SwiftyStoreKitTests */"

+ 1 - 1
scripts/install_swiftlint.sh

@@ -7,7 +7,7 @@
 set -e
 set -e
 
 
 SWIFTLINT_PKG_PATH="/tmp/SwiftLint.pkg"
 SWIFTLINT_PKG_PATH="/tmp/SwiftLint.pkg"
-SWIFTLINT_PKG_URL="https://github.com/realm/SwiftLint/releases/download/0.22.0/SwiftLint.pkg"
+SWIFTLINT_PKG_URL="https://github.com/realm/SwiftLint/releases/download/0.39.2/SwiftLint.pkg"
 
 
 wget --output-document=$SWIFTLINT_PKG_PATH $SWIFTLINT_PKG_URL
 wget --output-document=$SWIFTLINT_PKG_PATH $SWIFTLINT_PKG_URL