Skip to content

Commit

Permalink
Merge pull request #213 from bizz84/feature/verify-receipt-auto-refresh
Browse files Browse the repository at this point in the history
Feature/verify receipt auto refresh
  • Loading branch information
bizz84 authored May 18, 2017
2 parents 7db6cd4 + 26338ff commit 85ac45c
Show file tree
Hide file tree
Showing 11 changed files with 459 additions and 190 deletions.
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

0 comments on commit 85ac45c

Please sign in to comment.