Skip to content

Commit

Permalink
Merge pull request #544 from bizz84/develop
Browse files Browse the repository at this point in the history
0.16.0 update
  • Loading branch information
Sam-Spencer authored Jun 5, 2020
2 parents 5698784 + 0c5f990 commit a45217b
Show file tree
Hide file tree
Showing 27 changed files with 946 additions and 265 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ More info here:
- [Verify Purchase](#verify-purchase)
- [Verify Subscription](#verify-subscription)
- [Subscription Groups](#subscription-groups)
- [Get distinct purchase identifiers](#get-distinct-purchase-identifiers)
- [Notes](#notes)
- [Change Log](#change-log)
- [Sample Code](#sample-code)
Expand Down Expand Up @@ -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
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).
Expand Down
Binary file modified SwiftyStoreKit-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion SwiftyStoreKit.podspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftyStoreKit'
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.homepage = 'https://github.com/bizz84/SwiftyStoreKit'
s.author = { 'Andrea Bizzotto' => 'bizz84@gmail.com' }
Expand Down
182 changes: 177 additions & 5 deletions SwiftyStoreKit.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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>
11 changes: 7 additions & 4 deletions SwiftyStoreKit/AppleReceiptValidator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,17 @@ import Foundation

// https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html

public struct AppleReceiptValidator: ReceiptValidator {
public class AppleReceiptValidator: ReceiptValidator {

public enum VerifyReceiptURLType: String {
case production = "https://buy.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?

/**
Expand Down Expand Up @@ -106,8 +109,8 @@ public struct AppleReceiptValidator: ReceiptValidator {
*/
let receiptStatus = ReceiptStatus(rawValue: status) ?? ReceiptStatus.unknown
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 {
if receiptStatus.isValid {
completion(.success(receipt: receiptInfo))
Expand Down
4 changes: 3 additions & 1 deletion SwiftyStoreKit/InAppProductQueryRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ import StoreKit

typealias InAppProductRequestCallback = (RetrieveResults) -> Void

protocol InAppProductRequest: class {
public protocol InAppRequest: class {
func start()
func cancel()
}

protocol InAppProductRequest: InAppRequest { }

class InAppProductQueryRequest: NSObject, InAppProductRequest, SKProductsRequestDelegate {

private let callback: InAppProductRequestCallback
Expand Down
41 changes: 41 additions & 0 deletions SwiftyStoreKit/InAppReceipt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,38 @@ internal class InAppReceipt {
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)] {

Expand All @@ -194,6 +226,15 @@ internal class InAppReceipt {
#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?) {
switch subscriptionType {
Expand Down
6 changes: 5 additions & 1 deletion SwiftyStoreKit/InAppReceiptRefreshRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import StoreKit
import Foundation

class InAppReceiptRefreshRequest: NSObject, SKRequestDelegate {
class InAppReceiptRefreshRequest: NSObject, SKRequestDelegate, InAppRequest {

enum ResultType {
case success
Expand Down Expand Up @@ -60,6 +60,10 @@ class InAppReceiptRefreshRequest: NSObject, SKRequestDelegate {
self.refreshReceiptRequest.start()
}

func cancel() {
self.refreshReceiptRequest.cancel()
}

func requestDidFinish(_ request: SKRequest) {
/*if let resoreRequest = request as? SKReceiptRefreshRequest {
let receiptProperties = resoreRequest.receiptProperties ?? [:]
Expand Down
10 changes: 7 additions & 3 deletions SwiftyStoreKit/InAppReceiptVerificator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,13 @@ class InAppReceiptVerificator: NSObject {
* - Parameter refresh: closure to perform receipt refresh (this is made explicit for testability)
* - Parameter completion: handler for result
*/
@discardableResult
public func verifyReceipt(using validator: ReceiptValidator,
forceRefresh: Bool,
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 {
case .success(let receiptData):
self.verify(receiptData: receiptData, using: validator, completion: completion)
Expand All @@ -72,12 +73,14 @@ class InAppReceiptVerificator: NSObject {
* - Parameter refresh: closure to perform receipt refresh (this is made explicit for testability)
* - Parameter completion: handler for result
*/
@discardableResult
public func fetchReceipt(forceRefresh: Bool,
refresh: InAppReceiptRefreshRequest.ReceiptRefresh = InAppReceiptRefreshRequest.refresh,
completion: @escaping (FetchReceiptResult) -> Void) {
completion: @escaping (FetchReceiptResult) -> Void) -> InAppRequest? {

if let receiptData = appStoreReceiptData, forceRefresh == false {
completion(.success(receiptData: receiptData))
return nil
} else {

receiptRefreshRequest = refresh(nil) { result in
Expand All @@ -95,6 +98,7 @@ class InAppReceiptVerificator: NSObject {
completion(.error(error: .networkError(error: e)))
}
}
return receiptRefreshRequest
}
}

Expand Down
47 changes: 30 additions & 17 deletions SwiftyStoreKit/OS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,29 @@
// OS.swift
// 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

// MARK: - missing SKMutablePayment init with product on OSX
// MARK: - Missing SKMutablePayment init with product on macOS
#if os(OSX)
extension SKMutablePayment {
convenience init(product: SKProduct) {
Expand All @@ -33,3 +33,16 @@ import StoreKit
}
}
#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
26 changes: 20 additions & 6 deletions SwiftyStoreKit/PaymentQueueController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,10 @@ import StoreKit

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]
}

Expand All @@ -50,7 +49,11 @@ public protocol PaymentQueue: class {

func start(_ downloads: [SKDownload])
func pause(_ downloads: [SKDownload])
#if os(watchOS)
func resumeDownloads(_ downloads: [SKDownload])
#else
func resume(_ downloads: [SKDownload])
#endif
func cancel(_ downloads: [SKDownload])

func restoreCompletedTransactions(withApplicationUsername username: String?)
Expand Down Expand Up @@ -123,8 +126,14 @@ class PaymentQueueController: NSObject, SKPaymentTransactionObserver {
skPayment.applicationUsername = payment.applicationUsername
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 #available(iOS 8.3, tvOS 9.0, *) {
if #available(iOS 8.3, *) {
skPayment.simulatesAskToBuyInSandbox = payment.simulatesAskToBuyInSandbox
}
#endif
Expand Down Expand Up @@ -170,8 +179,13 @@ class PaymentQueueController: NSObject, SKPaymentTransactionObserver {
func pause(_ downloads: [SKDownload]) {
paymentQueue.pause(downloads)
}

func resume(_ downloads: [SKDownload]) {
#if os(watchOS)
paymentQueue.resumeDownloads(downloads)
#else
paymentQueue.resume(downloads)
#endif
}
func cancel(_ downloads: [SKDownload]) {
paymentQueue.cancel(downloads)
Expand Down
Loading

0 comments on commit a45217b

Please sign in to comment.