Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fetchReceipt method. Update verifyReceipt to use it. #278

Merged
merged 7 commits into from
Oct 11, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to this project will be documented in this file.

## [0.10.9](https://github.com/bizz84/SwiftyStoreKit/releases/tag/0.10.9) Add `fetchReceipt` method

* Add `fetchReceipt` method. Update `verifyReceipt` to use it ([#278](https://github.com/bizz84/SwiftyStoreKit/pull/278), related issues: [#272](https://github.com/bizz84/SwiftyStoreKit/issues/272), [#223](https://github.com/bizz84/SwiftyStoreKit/issues/223)).

## [0.10.8](https://github.com/bizz84/SwiftyStoreKit/releases/tag/0.10.8) Update to swiftlint 0.22.0

* Update to swiftlint 0.22.0 ([#270](https://github.com/bizz84/SwiftyStoreKit/pull/270), fix for [#273](https://github.com/bizz84/SwiftyStoreKit/issues/273))
Expand Down
60 changes: 43 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,37 +268,57 @@ According to [Apple - Delivering Products](https://developer.apple.com/library/c

When an app is first installed, the app receipt is missing.

As soon as a user completes a purchase or restores purchases, StoreKit creates and stores the receipt locally as a file.
As soon as a user completes a purchase or restores purchases, StoreKit creates and stores the receipt locally as a file, located by `Bundle.main.appStoreReceiptURL`.

As the local receipt is always encrypted, a verification step is needed to get all the receipt fields in readable form.
### Retrieve local receipt (encrypted)

This is done with a `verifyReceipt` method which does two things:
This helper can be used to retrieve the (encrypted) local receipt data:

- If the receipt is missing, refresh it
- If the receipt is available or is refreshed, validate it
```swift
let receiptData = SwiftyStoreKit.localReceiptData
let receiptString = receiptData.base64EncodedString(options: [])
// do your receipt validation here
```

However, the receipt file may be missing or outdated.

### Fetch receipt (encrypted)

Receipt validation can be done remotely with Apple via the `AppleReceiptValidator` class, or with a client-supplied validator conforming to the `ReceiptValidator` protocol.
Use this method to get the updated receipt:

```swift
SwiftyStoreKit.fetchReceipt(forceRefresh: true) { result in
switch result {
case .success(let encryptedReceipt):
print("fetchReceipt success:\n\(encryptedReceipt)")
case .error(let error):
print("fetchReceipt error: \(error)")
}
}
```

This method works as follows:

* If `forceRefresh = false`, it returns the local receipt from file, or refreshes it if missing.
* If `forceRefresh = true`, it always refreshes the receipt regardless.

**Notes**

* If the local receipt is missing when calling `verifyReceipt`, a network call is made to refresh it.
* If the local receipt is missing or `forceRefresh = true` when calling `fetchReceipt`, a network call is made to refresh it.
* If the user is not logged to the App Store, StoreKit will present a popup asking to **Sign In to the iTunes Store**.
* If the user enters valid credentials, the receipt will be refreshed and verified.
* If the user cancels, receipt refresh will fail with a **Cannot connect to iTunes Store** error.
* Using `AppleReceiptValidator` (see below) does remote receipt validation and also results in a network call.
* Local receipt validation is not implemented (see [issue #101](https://github.com/bizz84/SwiftyStoreKit/issues/101) for details).

If `fetchReceipt` is successful, it will return the **encrypted** receipt as a string. For this reason, a **validation** step is needed to get all the receipt fields in readable form. This can be done in various ways:

### Retrieve local receipt

```swift
let receiptData = SwiftyStoreKit.localReceiptData
let receiptString = receiptData.base64EncodedString(options: [])
// do your receipt validation here
```
1. Validate with Apple via the `AppleReceiptValidator` (see [`verifyReceipt`](#verify-receipt) below).
2. Perform local receipt validation (see [#101](https://github.com/bizz84/SwiftyStoreKit/issues/101)).
3. Post the receipt data and validate on server.

### Verify Receipt

Use this method to (optionally) refresh the receipt and perform validation in one step.

```swift
let appleValidator = AppleReceiptValidator(service: .production)
SwiftyStoreKit.verifyReceipt(using: appleValidator, password: "your-shared-secret", forceRefresh: false) { result in
Expand All @@ -311,7 +331,13 @@ SwiftyStoreKit.verifyReceipt(using: appleValidator, password: "your-shared-secre
}
```

Note: you can specify `forceRefresh: true` to force SwiftyStoreKit to refresh the receipt with Apple, even if a local receipt is already stored.
**Notes**

* This method is based on `fetchReceipt`, and the same refresh logic discussed above applies.
* `AppleReceiptValidator` is a **reference implementation** that validates the receipt with Apple and results in a network call. _This is prone to man-in-the-middle attacks._
* You should implement your secure logic by validating your receipt locally, or sending the encrypted receipt data and validating it in your server.
* Local receipt validation is not implemented (see [issue #101](https://github.com/bizz84/SwiftyStoreKit/issues/101) for details).
* You can implement your own receipt validator by conforming to the `ReceiptValidator` protocol and passing it to `verifyReceipt`.

## Verifying purchases and subscriptions

Expand Down
46 changes: 35 additions & 11 deletions SwiftyStoreKit/InAppReceiptVerificator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,7 @@ class InAppReceiptVerificator: NSObject {
private var receiptRefreshRequest: InAppReceiptRefreshRequest?

/**
* Verify application receipt. This method does two things:
* * If the receipt is missing, refresh it
* * If the receipt is available or is refreshed, validate it
* Verify application receipt.
* - Parameter validator: Validator to check the encrypted receipt and return the receipt in readable format
* - Parameter password: Your app’s shared secret (a hexadecimal string). Only used for receipts that contain auto-renewable subscriptions.
* - Parameter forceRefresh: If true, refreshes the receipt even if one already exists.
Expand All @@ -58,9 +56,32 @@ class InAppReceiptVerificator: NSObject {
refresh: InAppReceiptRefreshRequest.ReceiptRefresh = InAppReceiptRefreshRequest.refresh,
completion: @escaping (VerifyReceiptResult) -> Void) {

fetchReceipt(forceRefresh: forceRefresh, refresh: refresh) { result in
switch result {
case .success(let encryptedReceipt):
self.verify(receipt: encryptedReceipt, using: validator, password: password, completion: completion)
case .error(let error):
completion(.error(error: error))
}
}
}

/**
* Fetch application receipt. This method does two things:
* * If the receipt is missing, refresh it
* * If the receipt is available or is refreshed, validate it
* - Parameter forceRefresh: If true, refreshes the receipt even if one already exists.
* - Parameter refresh: closure to perform receipt refresh (this is made explicit for testability)
* - Parameter completion: handler for result
*/
public func fetchReceipt(forceRefresh: Bool,
refresh: InAppReceiptRefreshRequest.ReceiptRefresh = InAppReceiptRefreshRequest.refresh,
completion: @escaping (FetchReceiptResult) -> Void) {

if let receiptData = appStoreReceiptData, forceRefresh == false {

verify(receiptData: receiptData, using: validator, password: password, completion: completion)

fetchReceiptSuccessHandler(receiptData: receiptData, completion: completion)

} else {

receiptRefreshRequest = refresh(nil) { result in
Expand All @@ -70,7 +91,7 @@ class InAppReceiptVerificator: NSObject {
switch result {
case .success:
if let receiptData = self.appStoreReceiptData {
self.verify(receiptData: receiptData, using: validator, password: password, completion: completion)
self.fetchReceiptSuccessHandler(receiptData: receiptData, completion: completion)
} else {
completion(.error(error: .noReceiptData))
}
Expand All @@ -80,19 +101,22 @@ class InAppReceiptVerificator: NSObject {
}
}
}

private func fetchReceiptSuccessHandler(receiptData: Data, completion: (FetchReceiptResult) -> Void) {

let base64EncodedString = receiptData.base64EncodedString(options: [])
completion(.success(encryptedReceipt: base64EncodedString))
}

/**
* - Parameter receiptData: encrypted receipt data
* - Parameter validator: Validator to check the encrypted receipt and return the receipt in readable format
* - Parameter password: Your app’s shared secret (a hexadecimal string). Only used for receipts that contain auto-renewable subscriptions.
* - Parameter completion: handler for result
*/
private func verify(receiptData: Data, using validator: ReceiptValidator, password: String? = nil, completion: @escaping (VerifyReceiptResult) -> Void) {

// The base64 encoded receipt data.
let base64EncodedString = receiptData.base64EncodedString(options: [])
private func verify(receipt: String, using validator: ReceiptValidator, password: String? = nil, completion: @escaping (VerifyReceiptResult) -> Void) {

validator.validate(receipt: base64EncodedString, password: password) { result in
validator.validate(receipt: receipt, password: password) { result in

DispatchQueue.main.async {
completion(result)
Expand Down
6 changes: 6 additions & 0 deletions SwiftyStoreKit/SwiftyStoreKit+Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ public enum RefreshReceiptResult {
case error(error: Error)
}

// Fetch receipt result
public enum FetchReceiptResult {
case success(encryptedReceipt: String)
case error(error: ReceiptError)
}

// Verify receipt result
public enum VerifyReceiptResult {
case success(receipt: ReceiptInfo)
Expand Down
10 changes: 10 additions & 0 deletions SwiftyStoreKit/SwiftyStoreKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,16 @@ extension SwiftyStoreKit {

sharedInstance.receiptVerificator.verifyReceipt(using: validator, password: password, forceRefresh: forceRefresh, completion: completion)
}

/**
* Fetch application receipt
* - Parameter forceRefresh: If true, refreshes the receipt even if one already exists.
* - Parameter completion: handler for result
*/
public class func fetchReceipt(forceRefresh: Bool, completion: @escaping (FetchReceiptResult) -> Void) {

sharedInstance.receiptVerificator.fetchReceipt(forceRefresh: forceRefresh, completion: completion)
}

/**
* Verify the purchase of a Consumable or NonConsumable product in a receipt
Expand Down