Skip to content

Commit

Permalink
Merge pull request #278 from bizz84/feature/fetch-receipt
Browse files Browse the repository at this point in the history
Add fetchReceipt method. Update verifyReceipt to use it.
  • Loading branch information
bizz84 authored Oct 11, 2017
2 parents 2331732 + 22fa870 commit 7cf5626
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 28 deletions.
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

0 comments on commit 7cf5626

Please sign in to comment.