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
41 changes: 23 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,20 @@ 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, 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.

**Note**: As of version 0.10.0, _clients no longer need to refresh the receipt explicitly_.

### Retrieve local receipt

Expand All @@ -215,30 +229,21 @@ 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

* 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 is not logged to iTunes when `verifyReceipt` 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 and verified.
* If the user cancels, receipt refresh will fail with a **Cannot connect to iTunes Store** error.
* The receipt is only refreshed if it's not already stored in `Bundle.main.appStoreReceiptURL`.


### Verify Purchase
Expand Down
55 changes: 16 additions & 39 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 :
case .noReceiptData:
return alertWithTitle("Receipt verification", message: "No receipt data, application will try to get a new one. 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")
}
}

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

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

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()
}
}
}
verifyReceipt { result in
self.showAlert(self.alertForVerifyReceipt(result))
}
}

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

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 +155,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 +233,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, application will try to get a new one. 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 +273,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
37 changes: 0 additions & 37 deletions SwiftyStoreKit/InAppReceipt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,43 +80,6 @@ extension ReceiptItem {
// MARK - receipt mangement
internal class InAppReceipt {

static var appStoreReceiptUrl: URL? {
return Bundle.main.appStoreReceiptURL
}

static var appStoreReceiptData: Data? {
guard let receiptDataURL = appStoreReceiptUrl, let data = try? Data(contentsOf: receiptDataURL) else {
return nil
}
return data
}

// The base64 encoded receipt data.
static var appStoreReceiptBase64Encoded: String? {
return appStoreReceiptData?.base64EncodedString(options: [])
}

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

/**
* - Parameter receiptVerifyURL: receipt verify url (default: Production)
* - Parameter password: Only used for receipts that contain auto-renewable subscriptions. Your app’s shared secret (a hexadecimal string).
* - Parameter session: the session used to make remote call.
* - Parameter completion: handler for result
*/
class func verify(using validator: ReceiptValidator,
password autoRenewPassword: String? = nil,
completion: @escaping (VerifyReceiptResult) -> Void) {

// If no receipt is present, validation fails.
guard let base64EncodedString = appStoreReceiptBase64Encoded else {
completion(.error(error: .noReceiptData))
return
}

validator.validate(receipt: base64EncodedString, password: autoRenewPassword, completion: completion)
}

/**
* Verify the purchase of a Consumable or NonConsumable product in a receipt
* - Parameter productId: the product id of the purchase to verify
Expand Down
3 changes: 2 additions & 1 deletion SwiftyStoreKit/InAppReceiptRefreshRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class InAppReceiptRefreshRequest: NSObject, SKRequestDelegate {
}

typealias RequestCallback = (ResultType) -> Void
typealias ReceiptRefresh = (_ receiptProperties: [String : Any]?, _ callback: @escaping RequestCallback) -> InAppReceiptRefreshRequest

class func refresh(_ receiptProperties: [String : Any]? = nil, callback: @escaping RequestCallback) -> InAppReceiptRefreshRequest {
let request = InAppReceiptRefreshRequest(receiptProperties: receiptProperties, callback: callback)
Expand All @@ -48,7 +49,7 @@ class InAppReceiptRefreshRequest: NSObject, SKRequestDelegate {
refreshReceiptRequest.delegate = nil
}

private init(receiptProperties: [String : Any]? = nil, callback: @escaping RequestCallback) {
init(receiptProperties: [String : Any]? = nil, callback: @escaping RequestCallback) {
self.callback = callback
self.refreshReceiptRequest = SKReceiptRefreshRequest(receiptProperties: receiptProperties)
super.init()
Expand Down
Loading