Merge pull request #526 from Olmrzklv/master

Add `getDistinctPurchaseIds` method
This commit is contained in:
Samuel Spencer
2020-05-29 17:28:36 -04:00
committed by GitHub
4 changed files with 143 additions and 5 deletions

View File

@@ -5,7 +5,7 @@
[![Language](https://img.shields.io/badge/swift-5.0-orange.svg)](https://developer.apple.com/swift) [![Language](https://img.shields.io/badge/swift-5.0-orange.svg)](https://developer.apple.com/swift)
[![Build](https://img.shields.io/travis/bizz84/SwiftyStoreKit.svg?style=flat)](https://travis-ci.org/bizz84/SwiftyStoreKit) [![Build](https://img.shields.io/travis/bizz84/SwiftyStoreKit.svg?style=flat)](https://travis-ci.org/bizz84/SwiftyStoreKit)
[![Issues](https://img.shields.io/github/issues/bizz84/SwiftyStoreKit.svg?style=flat)](https://github.com/bizz84/SwiftyStoreKit/issues) [![Issues](https://img.shields.io/github/issues/bizz84/SwiftyStoreKit.svg?style=flat)](https://github.com/bizz84/SwiftyStoreKit/issues)
[![Slack](https://img.shields.io/badge/Slack-Join-green.svg?style=flat)](https://join.slack.com/t/swiftystorekit/shared_invite/enQtNjkzNTg5NTMyMTgwLTcyZGIzMTg0MWFmMTQyMDYxNDcyYWNhOTlmNjUyM2E0OTllNjE2ZDJiNDI0ZDAzMWU2Mzc3Nzk1YzJmMTE2NjI) [![Slack](https://img.shields.io/badge/Slack-Join-green.svg?style=flat)](https://join.slack.com/t/swiftystorekit/shared_invite/enQtODY3OTYxOTExMzE5LWVkNGY4MzcwY2VjNGM4MGU4NDFhMGE5YmUxMGM3ZTQ4NjVjNTRkNTJhNDAyMWZmY2M5OWE5MDE0ODc3OGJjMmM)
[![Cocoapod](http://img.shields.io/cocoapods/v/SwiftyStoreKit.svg?style=flat)](http://cocoadocs.org/docsets/SwiftyStoreKit/) [![Cocoapod](http://img.shields.io/cocoapods/v/SwiftyStoreKit.svg?style=flat)](http://cocoadocs.org/docsets/SwiftyStoreKit/)
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
[![Accio supported](https://img.shields.io/badge/Accio-supported-0A7CF5.svg?style=flat)](https://github.com/JamitLabs/Accio) [![Accio supported](https://img.shields.io/badge/Accio-supported-0A7CF5.svg?style=flat)](https://github.com/JamitLabs/Accio)
@@ -19,12 +19,11 @@ SwiftyStoreKit is a lightweight In App Purchases framework for iOS 8.0+, tvOS 9.
### Join on Slack ### Join on Slack
SwiftyStoreKit is on Slack. [Join here](https://join.slack.com/t/swiftystorekit/shared_invite/enQtNjkzNTg5NTMyMTgwLTcyZGIzMTg0MWFmMTQyMDYxNDcyYWNhOTlmNjUyM2E0OTllNjE2ZDJiNDI0ZDAzMWU2Mzc3Nzk1YzJmMTE2NjI). SwiftyStoreKit is on Slack. [Join here](https://join.slack.com/t/swiftystorekit/shared_invite/enQtODY3OTYxOTExMzE5LWVkNGY4MzcwY2VjNGM4MGU4NDFhMGE5YmUxMGM3ZTQ4NjVjNTRkNTJhNDAyMWZmY2M5OWE5MDE0ODc3OGJjMmM).
### Maintainers Wanted ### Maintainers Wanted
- The author no longer maintaining this project actively. If you'd like to become a maintainer, [join the Slack workspace](https://join.slack.com/t/swiftystorekit/shared_invite/enQtNjkzNTg5NTMyMTgwLTcyZGIzMTg0MWFmMTQyMDYxNDcyYWNhOTlmNjUyM2E0OTllNjE2ZDJiNDI0ZDAzMWU2Mzc3Nzk1YzJmMTE2NjI - The author no longer maintaining this project actively. If you'd like to become a maintainer, [join the Slack workspace](https://join.slack.com/t/swiftystorekit/shared_invite/enQtODY3OTYxOTExMzE5LWVkNGY4MzcwY2VjNGM4MGU4NDFhMGE5YmUxMGM3ZTQ4NjVjNTRkNTJhNDAyMWZmY2M5OWE5MDE0ODc3OGJjMmM) and enter the [#maintainers](https://app.slack.com/client/TL2JYQ458/CLG62K26A/details/) channel.
) and enter the [#maintainers](https://app.slack.com/client/TL2JYQ458/CLG62K26A/details/) channel.
- Going forward, SwiftyStoreKit should be made for the community, by the community. - Going forward, SwiftyStoreKit should be made for the community, by the community.
More info here: More info here:
@@ -36,6 +35,7 @@ More info here:
- [Installation](#installation) - [Installation](#installation)
- [CocoaPods](#cocoapods) - [CocoaPods](#cocoapods)
- [Carthage](#carthage) - [Carthage](#carthage)
- [Swift Package Manager](#swift-package-manager)
- [Features](#features) - [Features](#features)
- [Contributing](#contributing) - [Contributing](#contributing)
- [App startup](#app-startup) - [App startup](#app-startup)
@@ -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).
@@ -695,7 +721,7 @@ I have also written about building SwiftyStoreKit on Medium:
#### [@rebeloper](https://github.com/rebeloper): Ultimate In-app Purchases Guide #### [@rebeloper](https://github.com/rebeloper): Ultimate In-app Purchases Guide
<a href="https://www.youtube.com/watch?v=bIyj6BZ1-Qw&list=PL_csAAO9PQ8b9kqrltk2_SpYslTwyrwjb"><img src="https://raw.githubusercontent.com/bizz84/SwiftyStoreKit/master/Screenshots/VideoTutorial-Rebeloper.jpg" width="854" /></a> <a href="https://www.youtube.com/watch?v=MP-U5gQylHc"><img src="https://user-images.githubusercontent.com/2488011/65576278-55cccc80-df7a-11e9-8db5-244e2afa3e46.png" width="100%" /></a>
## Payment flows: implementation details ## Payment flows: implementation details
In order to make a purchase, two operations are needed: In order to make a purchase, two operations are needed:
@@ -754,6 +780,7 @@ It would be great to showcase apps using SwiftyStoreKit here. Pull requests welc
* [OB Monitor](https://itunes.apple.com/app/id1073398446) - The app for Texas Longhorns athletics fans * [OB Monitor](https://itunes.apple.com/app/id1073398446) - The app for Texas Longhorns athletics fans
* [Talk Dim Sum](https://itunes.apple.com/us/app/talk-dim-sum/id953929066) - Your dim sum companion * [Talk Dim Sum](https://itunes.apple.com/us/app/talk-dim-sum/id953929066) - Your dim sum companion
* [Sluggard](https://itunes.apple.com/app/id1160131071) - Perform simple exercises to reduce the risks of sedentary lifestyle * [Sluggard](https://itunes.apple.com/app/id1160131071) - Perform simple exercises to reduce the risks of sedentary lifestyle
* [Debts iOS](https://debts.ivanvorobei.by/ios) & [Debts macOS](https://debts.ivanvorobei.by/macos) - Track amounts owed
A full list of apps is published [on AppSight](https://www.appsight.io/sdk/574154). A full list of apps is published [on AppSight](https://www.appsight.io/sdk/574154).

View File

@@ -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 {

View File

@@ -316,4 +316,18 @@ extension SwiftyStoreKit {
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 upproduct identifiers.
* - return: 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)
}
} }

View File

@@ -382,6 +382,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] {