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

Feature/verify receipt auto refresh #213

Merged
merged 9 commits into from
May 18, 2017
65 changes: 42 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,28 @@ According to [Apple - Delivering Products](https://developer.apple.com/library/c

> Information about all other kinds of purchases is added to the receipt when they’re paid for and remains in the receipt indefinitely.

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 the local receipt is always encrypted, a verification step is needed to get all the receipt fields in readable form.

This is done with a `verifyReceipt` method which does two things:

- If the receipt is missing, refresh it
- If the receipt is available or is refreshed, validate it

Receipt validation can be done remotely with Apple via the `AppleReceiptValidator` class, or with a client-supplied validator conforming to the `ReceiptValidator` protocol.

**Notes**

* If the local receipt is missing when calling `verifyReceipt`, 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).


### Retrieve local receipt

Expand All @@ -215,31 +237,25 @@ let receiptString = receiptData.base64EncodedString(options: [])
```swift
let appleValidator = AppleReceiptValidator(service: .production)
SwiftyStoreKit.verifyReceipt(using: appleValidator, password: "your-shared-secret") { result in
if case .error(let error) = result {
if case .noReceiptData = error {
self.refreshReceipt()
}
}
}

func refreshReceipt() {
SwiftyStoreKit.refreshReceipt { result in
switch result {
case .success(let receiptData):
print("Receipt refresh success: \(receiptData.base64EncodedString)")
case .error(let error):
print("Receipt refresh failed: \(error)")
}
}
switch result {
case .success(let receipt):
print("Verify receipt Success: \(receipt)")
case .error(let error):
print("Verify receipt Failed: \(error)")
}
}
```

#### Notes
## Verifying purchases and subscriptions

* If the user is not logged to iTunes when `refreshReceipt` is called, StoreKit will present a popup asking to **Sign In to the iTunes Store**.
* If the user enters valid credentials, the receipt will be refreshed.
* If the user cancels, receipt refresh will fail with a **Cannot connect to iTunes Store** error.
Once you have retrieved the receipt using the `verifyReceipt` method, you can verify your purchases and subscriptions by product identifier.

Verifying multiple purchases and subscriptions in one call is not yet supported (see [issue #194](https://github.com/bizz84/SwiftyStoreKit/issues/194) for more details).

If you need to verify multiple purchases / subscriptions, you can either:

* manually parse the receipt dictionary returned by `verifyReceipt`
* call `verifyPurchase` or `verifySubscription` multiple times with different product identifiers

### Verify Purchase

Expand Down Expand Up @@ -304,23 +320,26 @@ SwiftyStoreKit.verifyReceipt(using: appleValidator, password: "your-shared-secre
```

#### Auto-Renewable
```
```swift
let purchaseResult = SwiftyStoreKit.verifySubscription(
type: .autoRenewable,
productId: "com.musevisions.SwiftyStoreKit.Subscription",
inReceipt: receipt)
```

#### Non-Renewing
```
```swift
// validDuration: time interval in seconds
let purchaseResult = SwiftyStoreKit.verifySubscription(
type: .nonRenewing(validDuration: 3600 * 24 * 30),
productId: "com.musevisions.SwiftyStoreKit.Subscription",
inReceipt: receipt)
```

**Note**: When purchasing subscriptions in sandbox mode, the expiry dates are set just minutes after the purchase date for testing purposes.
**Notes**

* The expiration dates are calculated against the receipt date. This is the date of the last successful call to `verifyReceipt`.
* When purchasing subscriptions in sandbox mode, the expiry dates are set just minutes after the purchase date for testing purposes.

#### Purchasing and verifying a subscription

Expand Down
57 changes: 17 additions & 40 deletions SwiftyStoreKit-iOS-Demo/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class ViewController: UIViewController {
@IBAction func verifyPurchase2() {
verifyPurchase(purchase2Suffix)
}

func getInfo(_ purchase: RegisteredPurchase) {

NetworkActivityIndicatorManager.networkOperationStarted()
Expand Down Expand Up @@ -108,25 +108,23 @@ class ViewController: UIViewController {
@IBAction func verifyReceipt() {

NetworkActivityIndicatorManager.networkOperationStarted()
let appleValidator = AppleReceiptValidator(service: .production)
SwiftyStoreKit.verifyReceipt(using: appleValidator, password: "your-shared-secret") { result in
verifyReceipt { result in
NetworkActivityIndicatorManager.networkOperationFinished()

self.showAlert(self.alertForVerifyReceipt(result))

if case .error(let error) = result {
if case .noReceiptData = error {
self.refreshReceipt()
}
}
}
}

func verifyReceipt(completion: @escaping (VerifyReceiptResult) -> Void) {

let appleValidator = AppleReceiptValidator(service: .production)
let password = "your-shared-secret"
SwiftyStoreKit.verifyReceipt(using: appleValidator, password: password, completion: completion)
}

func verifyPurchase(_ purchase: RegisteredPurchase) {

NetworkActivityIndicatorManager.networkOperationStarted()
let appleValidator = AppleReceiptValidator(service: .production)
SwiftyStoreKit.verifyReceipt(using: appleValidator, password: "your-shared-secret") { result in
verifyReceipt { result in
NetworkActivityIndicatorManager.networkOperationFinished()

switch result {
Expand Down Expand Up @@ -159,23 +157,12 @@ class ViewController: UIViewController {
self.showAlert(self.alertForVerifyPurchase(purchaseResult))
}

case .error(let error):
case .error:
self.showAlert(self.alertForVerifyReceipt(result))
if case .noReceiptData = error {
self.refreshReceipt()
}
}
}
}

func refreshReceipt() {

SwiftyStoreKit.refreshReceipt { result in

self.showAlert(self.alertForRefreshReceipt(result))
}
}

#if os(iOS)
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
Expand Down Expand Up @@ -262,14 +249,16 @@ extension ViewController {
switch result {
case .success(let receipt):
print("Verify receipt Success: \(receipt)")
return alertWithTitle("Receipt verified", message: "Receipt verified remotly")
return alertWithTitle("Receipt verified", message: "Receipt verified remotely")
case .error(let error):
print("Verify receipt Failed: \(error)")
switch error {
case .noReceiptData :
return alertWithTitle("Receipt verification", message: "No receipt data, application will try to get a new one. Try again.")
case .noReceiptData:
return alertWithTitle("Receipt verification", message: "No receipt data. Try again.")
case .networkError(let error):
return alertWithTitle("Receipt verification", message: "Network error while verifying receipt: \(error)")
default:
return alertWithTitle("Receipt verification", message: "Receipt verification failed")
return alertWithTitle("Receipt verification", message: "Receipt verification failed: \(error)")
}
}
}
Expand Down Expand Up @@ -300,16 +289,4 @@ extension ViewController {
return alertWithTitle("Not purchased", message: "This product has never been purchased")
}
}

func alertForRefreshReceipt(_ result: RefreshReceiptResult) -> UIAlertController {
switch result {
case .success(let receiptData):
print("Receipt refresh Success: \(receiptData.base64EncodedString)")
return alertWithTitle("Receipt refreshed", message: "Receipt refreshed successfully")
case .error(let error):
print("Receipt refresh Failed: \(error)")
return alertWithTitle("Receipt refresh failed", message: "Receipt refresh failed")
}
}

}
55 changes: 19 additions & 36 deletions SwiftyStoreKit-macOS-Demo/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,24 +105,21 @@ class ViewController: NSViewController {

@IBAction func verifyReceipt(_ sender: Any?) {

verifyReceipt(completion: { result in
self.showAlert(self.alertForVerifyReceipt(result))
})
}

func verifyReceipt(completion: @escaping (VerifyReceiptResult) -> Void) {

let appleValidator = AppleReceiptValidator(service: .production)
SwiftyStoreKit.verifyReceipt(using: appleValidator, password: "your-shared-secret") { result in

self.showAlert(self.alertForVerifyReceipt(result)) { _ in

if case .error(let error) = result {
if case .noReceiptData = error {
self.refreshReceipt()
}
}
}
}
let password = "your-shared-secret"
SwiftyStoreKit.verifyReceipt(using: appleValidator, password: password, completion: completion)
}

func verifyPurchase(_ purchase: RegisteredPurchase) {

let appleValidator = AppleReceiptValidator(service: .production)
SwiftyStoreKit.verifyReceipt(using: appleValidator, password: "your-shared-secret") { result in
verifyReceipt { result in

switch result {
case .success(let receipt):
Expand Down Expand Up @@ -157,15 +154,6 @@ class ViewController: NSViewController {
}
}
}

func refreshReceipt() {

SwiftyStoreKit.refreshReceipt { result in

self.showAlert(self.alertForRefreshReceipt(result))
}
}

}

// MARK: User facing alerts
Expand Down Expand Up @@ -244,10 +232,17 @@ extension ViewController {
switch result {
case .success(let receipt):
print("Verify receipt Success: \(receipt)")
return self.alertWithTitle("Receipt verified", message: "Receipt verified remotly")
return self.alertWithTitle("Receipt verified", message: "Receipt verified remotely")
case .error(let error):
print("Verify receipt Failed: \(error)")
return self.alertWithTitle("Receipt verification failed", message: "The application will exit to create receipt data. You must have signed the application with your developer id to test and be outside of XCode")
switch error {
case .noReceiptData:
return alertWithTitle("Receipt verification", message: "No receipt data. Try again.")
case .networkError(let error):
return alertWithTitle("Receipt verification", message: "Network error while verifying receipt: \(error)")
default:
return alertWithTitle("Receipt verification", message: "Receipt verification failed: \(error)")
}
}
}

Expand Down Expand Up @@ -277,16 +272,4 @@ extension ViewController {
return alertWithTitle("Not purchased", message: "This product has never been purchased")
}
}

func alertForRefreshReceipt(_ result: RefreshReceiptResult) -> NSAlert {
switch result {
case .success(let receiptData):
print("Receipt refresh Success: \(receiptData.base64EncodedString)")
return alertWithTitle("Receipt refreshed", message: "Receipt refreshed successfully")
case .error(let error):
print("Receipt refresh Failed: \(error)")
return alertWithTitle("Receipt refresh failed", message: "Receipt refresh failed")
}
}

}
12 changes: 12 additions & 0 deletions SwiftyStoreKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@
65BB6CE81DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */; };
65BB6CE91DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */; };
65BB6CEA1DDB018900218A0B /* SwiftyStoreKit+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */; };
65CEF0F41ECC80D9007DC3B6 /* InAppReceiptVerificatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65CEF0F31ECC80D9007DC3B6 /* InAppReceiptVerificatorTests.swift */; };
65E9E0791ECADF5E005CF7B4 /* InAppReceiptVerificator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E9E0781ECADF5E005CF7B4 /* InAppReceiptVerificator.swift */; };
65E9E07A1ECADF5E005CF7B4 /* InAppReceiptVerificator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E9E0781ECADF5E005CF7B4 /* InAppReceiptVerificator.swift */; };
65E9E07B1ECADF5E005CF7B4 /* InAppReceiptVerificator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E9E0781ECADF5E005CF7B4 /* InAppReceiptVerificator.swift */; };
65F70AC71E2ECBB300BF040D /* PaymentTransactionObserverFake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F70AC61E2ECBB300BF040D /* PaymentTransactionObserverFake.swift */; };
65F70AC91E2EDC3700BF040D /* PaymentsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F70AC81E2EDC3700BF040D /* PaymentsController.swift */; };
65F70ACA1E2EDC3700BF040D /* PaymentsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F70AC81E2EDC3700BF040D /* PaymentsController.swift */; };
Expand Down Expand Up @@ -179,6 +183,8 @@
658A084B1E2EC5960074A98F /* PaymentQueueSpy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentQueueSpy.swift; sourceTree = "<group>"; };
65B8C9281EC0BE62009439D9 /* InAppReceiptTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppReceiptTests.swift; sourceTree = "<group>"; };
65BB6CE71DDB018900218A0B /* SwiftyStoreKit+Types.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SwiftyStoreKit+Types.swift"; sourceTree = "<group>"; };
65CEF0F31ECC80D9007DC3B6 /* InAppReceiptVerificatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppReceiptVerificatorTests.swift; sourceTree = "<group>"; };
65E9E0781ECADF5E005CF7B4 /* InAppReceiptVerificator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppReceiptVerificator.swift; sourceTree = "<group>"; };
65F70AC61E2ECBB300BF040D /* PaymentTransactionObserverFake.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentTransactionObserverFake.swift; sourceTree = "<group>"; };
65F70AC81E2EDC3700BF040D /* PaymentsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentsController.swift; sourceTree = "<group>"; };
65F7DF681DCD4DF000835D30 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -306,6 +312,7 @@
650307F71E317BCF001332A4 /* CompleteTransactionsController.swift */,
C4083C561C2AB0A900295248 /* InAppReceiptRefreshRequest.swift */,
C4A7C7621C29B8D00053ED64 /* InAppReceipt.swift */,
65E9E0781ECADF5E005CF7B4 /* InAppReceiptVerificator.swift */,
1592CD4F1E27756500D321E6 /* AppleReceiptValidator.swift */,
653722801DB8282600C8F944 /* SKProduct+LocalizedPrice.swift */,
C40C680F1C29414C00B60B7E /* OS.swift */,
Expand Down Expand Up @@ -333,6 +340,7 @@
650307F11E3163AA001332A4 /* RestorePurchasesControllerTests.swift */,
C3099C181E3206C700392A54 /* CompleteTransactionsControllerTests.swift */,
65B8C9281EC0BE62009439D9 /* InAppReceiptTests.swift */,
65CEF0F31ECC80D9007DC3B6 /* InAppReceiptVerificatorTests.swift */,
658A084B1E2EC5960074A98F /* PaymentQueueSpy.swift */,
65F70AC61E2ECBB300BF040D /* PaymentTransactionObserverFake.swift */,
C3099C081E2FCE3A00392A54 /* TestProduct.swift */,
Expand Down Expand Up @@ -710,6 +718,7 @@
650307FE1E33154F001332A4 /* ProductsInfoController.swift in Sources */,
650307F61E3177EF001332A4 /* RestorePurchasesController.swift in Sources */,
658A08391E2EC24E0074A98F /* PaymentQueueController.swift in Sources */,
65E9E07B1ECADF5E005CF7B4 /* InAppReceiptVerificator.swift in Sources */,
653722831DB8290B00C8F944 /* SKProduct+LocalizedPrice.swift in Sources */,
54B069921CF742D100BAFE38 /* InAppReceipt.swift in Sources */,
650307FA1E317BCF001332A4 /* CompleteTransactionsController.swift in Sources */,
Expand Down Expand Up @@ -740,6 +749,7 @@
650307FC1E33154F001332A4 /* ProductsInfoController.swift in Sources */,
650307F41E3177EF001332A4 /* RestorePurchasesController.swift in Sources */,
658A08371E2EC24E0074A98F /* PaymentQueueController.swift in Sources */,
65E9E0791ECADF5E005CF7B4 /* InAppReceiptVerificator.swift in Sources */,
653722811DB8282600C8F944 /* SKProduct+LocalizedPrice.swift in Sources */,
C4A7C7631C29B8D00053ED64 /* InAppReceipt.swift in Sources */,
650307F81E317BCF001332A4 /* CompleteTransactionsController.swift in Sources */,
Expand All @@ -764,6 +774,7 @@
files = (
C3099C071E2FCDAA00392A54 /* PaymentsControllerTests.swift in Sources */,
650307F21E3163AA001332A4 /* RestorePurchasesControllerTests.swift in Sources */,
65CEF0F41ECC80D9007DC3B6 /* InAppReceiptVerificatorTests.swift in Sources */,
C3099C0B1E2FD13200392A54 /* TestPaymentTransaction.swift in Sources */,
65F70AC71E2ECBB300BF040D /* PaymentTransactionObserverFake.swift in Sources */,
658A084A1E2EC5350074A98F /* PaymentQueueControllerTests.swift in Sources */,
Expand All @@ -786,6 +797,7 @@
650307FD1E33154F001332A4 /* ProductsInfoController.swift in Sources */,
650307F51E3177EF001332A4 /* RestorePurchasesController.swift in Sources */,
658A08381E2EC24E0074A98F /* PaymentQueueController.swift in Sources */,
65E9E07A1ECADF5E005CF7B4 /* InAppReceiptVerificator.swift in Sources */,
653722821DB8290A00C8F944 /* SKProduct+LocalizedPrice.swift in Sources */,
C4083C551C2AADB500295248 /* InAppReceipt.swift in Sources */,
650307F91E317BCF001332A4 /* CompleteTransactionsController.swift in Sources */,
Expand Down
2 changes: 2 additions & 0 deletions SwiftyStoreKit/AppleReceiptValidator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

import Foundation

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

public struct AppleReceiptValidator: ReceiptValidator {

public enum VerifyReceiptURLType: String {
Expand Down
Loading