Skip to content

Commit

Permalink
Merge pull request #126 from huyduc/feature/inject-receipt-validator
Browse files Browse the repository at this point in the history
Feature/inject receipt validator
  • Loading branch information
bizz84 authored Jan 14, 2017
2 parents 6d9e570 + 03d34ba commit 4fce54d
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 89 deletions.
5 changes: 3 additions & 2 deletions SwiftyStoreKit-iOS-Demo/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}

func verifyReceipt() {

SwiftyStoreKit.verifyReceipt(password: "your-shared-secret") { result in

let appleValidator = AppleReceiptValidator(service: .production)
SwiftyStoreKit.verifyReceipt(using: appleValidator, password: "your-shared-secret") { result in
switch result {
case .success(let receipt):
print("\(receipt)")
Expand Down
8 changes: 5 additions & 3 deletions SwiftyStoreKit-iOS-Demo/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ class ViewController: UIViewController {
@IBAction func verifyReceipt() {

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

self.showAlert(self.alertForVerifyReceipt(result))
Expand All @@ -123,9 +124,10 @@ class ViewController: UIViewController {
}

func verifyPurchase(_ purchase: RegisteredPurchase) {

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

switch result {
Expand Down
8 changes: 8 additions & 0 deletions SwiftyStoreKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
objects = {

/* Begin PBXBuildFile section */
1592CD501E27756500D321E6 /* ReceiptValidators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1592CD4F1E27756500D321E6 /* ReceiptValidators.swift */; };
1592CD511E27756500D321E6 /* ReceiptValidators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1592CD4F1E27756500D321E6 /* ReceiptValidators.swift */; };
1592CD521E27756500D321E6 /* ReceiptValidators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1592CD4F1E27756500D321E6 /* ReceiptValidators.swift */; };
54B069911CF742CE00BAFE38 /* InAppCompleteTransactionsObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 651A71241CD651AF000B4091 /* InAppCompleteTransactionsObserver.swift */; };
54B069921CF742D100BAFE38 /* InAppReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A7C7621C29B8D00053ED64 /* InAppReceipt.swift */; };
54B069931CF742D300BAFE38 /* InAppReceiptRefreshRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4083C561C2AB0A900295248 /* InAppReceiptRefreshRequest.swift */; };
Expand Down Expand Up @@ -96,6 +99,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
1592CD4F1E27756500D321E6 /* ReceiptValidators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiptValidators.swift; sourceTree = "<group>"; };
54C0D52C1CF7404500F90BCE /* SwiftyStoreKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftyStoreKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
6502F5FE1B985833004E342D /* SwiftyStoreKit_iOSDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftyStoreKit_iOSDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
6502F6221B98586A004E342D /* InAppProductPurchaseRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppProductPurchaseRequest.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -200,6 +204,7 @@
6502F6231B98586A004E342D /* InAppProductQueryRequest.swift */,
C4083C561C2AB0A900295248 /* InAppReceiptRefreshRequest.swift */,
C4A7C7621C29B8D00053ED64 /* InAppReceipt.swift */,
1592CD4F1E27756500D321E6 /* ReceiptValidators.swift */,
651A71241CD651AF000B4091 /* InAppCompleteTransactionsObserver.swift */,
653722801DB8282600C8F944 /* SKProduct+LocalizedPrice.swift */,
6502F6241B98586A004E342D /* SwiftyStoreKit.swift */,
Expand Down Expand Up @@ -473,6 +478,7 @@
buildActionMask = 2147483647;
files = (
54B069911CF742CE00BAFE38 /* InAppCompleteTransactionsObserver.swift in Sources */,
1592CD521E27756500D321E6 /* ReceiptValidators.swift in Sources */,
54B069951CF742D900BAFE38 /* InAppProductPurchaseRequest.swift in Sources */,
54C0D5681CF7428400F90BCE /* SwiftyStoreKit.swift in Sources */,
54B069961CF744DC00BAFE38 /* OS.swift in Sources */,
Expand All @@ -499,6 +505,7 @@
buildActionMask = 2147483647;
files = (
C40C68101C29414C00B60B7E /* OS.swift in Sources */,
1592CD501E27756500D321E6 /* ReceiptValidators.swift in Sources */,
651A71251CD651AF000B4091 /* InAppCompleteTransactionsObserver.swift in Sources */,
6502F63A1B985C9E004E342D /* InAppProductPurchaseRequest.swift in Sources */,
6502F63B1B985CA1004E342D /* InAppProductQueryRequest.swift in Sources */,
Expand All @@ -515,6 +522,7 @@
buildActionMask = 2147483647;
files = (
C40C68111C29419500B60B7E /* OS.swift in Sources */,
1592CD511E27756500D321E6 /* ReceiptValidators.swift in Sources */,
651A71261CD651AF000B4091 /* InAppCompleteTransactionsObserver.swift in Sources */,
C4D74BC31C24CEDC0071AD3E /* InAppProductPurchaseRequest.swift in Sources */,
C4D74BC41C24CEDC0071AD3E /* InAppProductQueryRequest.swift in Sources */,
Expand Down
84 changes: 2 additions & 82 deletions SwiftyStoreKit/InAppReceipt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,6 @@ import Foundation
// MARK - receipt mangement
internal class InAppReceipt {

// URL used to verify remotely receipt
enum VerifyReceiptURLType: String {
case production = "https://buy.itunes.apple.com/verifyReceipt"
case sandbox = "https://sandbox.itunes.apple.com/verifyReceipt"
}

static var appStoreReceiptUrl: URL? {
return Bundle.main.appStoreReceiptURL
}
Expand All @@ -59,9 +53,8 @@ internal class InAppReceipt {
* - Parameter completion: handler for result
*/
class func verify(
urlType: VerifyReceiptURLType = .production,
using validator: ReceiptValidator,
password autoRenewPassword: String? = nil,
session: URLSession = URLSession.shared,
completion: @escaping (VerifyReceiptResult) -> ()) {

// If no receipt is present, validation fails.
Expand All @@ -70,80 +63,7 @@ internal class InAppReceipt {
return
}

// Create request
let storeURL = URL(string: urlType.rawValue)! // safe (until no more)
let storeRequest = NSMutableURLRequest(url: storeURL)
storeRequest.httpMethod = "POST"


let requestContents: NSMutableDictionary = [ "receipt-data" : base64EncodedString ]
// password if defined
if let password = autoRenewPassword {
requestContents.setValue(password, forKey: "password")
}

// Encore request body
do {
storeRequest.httpBody = try JSONSerialization.data(withJSONObject: requestContents, options: [])
} catch let e {
completion(.error(error: .requestBodyEncodeError(error: e)))
return
}

// Remote task
let task = session.dataTask(with: storeRequest as URLRequest) { data, response, error -> Void in

// there is an error
if let networkError = error {
completion(.error(error: .networkError(error: networkError)))
return
}

// there is no data
guard let safeData = data else {
completion(.error(error: .noRemoteData))
return
}

// cannot decode data
guard let receiptInfo = try? JSONSerialization.jsonObject(with: data!, options: .mutableLeaves) as? ReceiptInfo ?? [:] else {
let jsonStr = String(data: safeData, encoding: String.Encoding.utf8)
completion(.error(error: .jsonDecodeError(string: jsonStr)))
return
}

// get status from info
if let status = receiptInfo["status"] as? Int {
/*
* http://stackoverflow.com/questions/16187231/how-do-i-know-if-an-in-app-purchase-receipt-comes-from-the-sandbox
* How do I verify my receipt (iOS)?
* Always verify your receipt first with the production URL; proceed to verify
* with the sandbox URL if you receive a 21007 status code. Following this
* approach ensures that you do not have to switch between URLs while your
* application is being tested or reviewed in the sandbox or is live in the
* App Store.

* Note: The 21007 status code indicates that this receipt is a sandbox receipt,
* but it was sent to the production service for verification.
*/
let receiptStatus = ReceiptStatus(rawValue: status) ?? ReceiptStatus.unknown
if case .testReceipt = receiptStatus {
verify(urlType: .sandbox, password: autoRenewPassword, session: session, completion: completion)
}
else {
if receiptStatus.isValid {
completion(.success(receipt: receiptInfo))
}
else {
completion(.error(error: .receiptInvalid(receipt: receiptInfo, status: receiptStatus)))
}
}
}
else {
completion(.error(error: .receiptInvalid(receipt: receiptInfo, status: ReceiptStatus.none)))
}
}
task.resume()
validator.validate(receipt: base64EncodedString, password: autoRenewPassword, completion: completion)
}

/**
Expand Down
120 changes: 120 additions & 0 deletions SwiftyStoreKit/ReceiptValidators.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
//
// InAppReceipt.swift
// SwiftyStoreKit
//
// Created by phimage on 22/12/15.
// Copyright (c) 2015 Andrea Bizzotto (bizz84@gmail.com)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import Foundation

public struct AppleReceiptValidator: ReceiptValidator {

public enum VerifyReceiptURLType: String {
case production = "https://buy.itunes.apple.com/verifyReceipt"
case sandbox = "https://sandbox.itunes.apple.com/verifyReceipt"
}

public init(service: VerifyReceiptURLType) {
self.service = service
}

private let service: VerifyReceiptURLType

public func validate(
receipt: String,
password autoRenewPassword: String? = nil,
completion: @escaping (VerifyReceiptResult) -> Void) {

let storeURL = URL(string: service.rawValue)! // safe (until no more)
let storeRequest = NSMutableURLRequest(url: storeURL)
storeRequest.httpMethod = "POST"

let requestContents: NSMutableDictionary = [ "receipt-data" : receipt ]
// password if defined
if let password = autoRenewPassword {
requestContents.setValue(password, forKey: "password")
}

// Encore request body
do {
storeRequest.httpBody = try JSONSerialization.data(withJSONObject: requestContents, options: [])
} catch let e {
completion(.error(error: .requestBodyEncodeError(error: e)))
return
}

// Remote task
let task = URLSession.shared.dataTask(with: storeRequest as URLRequest) { data, response, error -> Void in

// there is an error
if let networkError = error {
completion(.error(error: .networkError(error: networkError)))
return
}

// there is no data
guard let safeData = data else {
completion(.error(error: .noRemoteData))
return
}

// cannot decode data
guard let receiptInfo = try? JSONSerialization.jsonObject(with: data!, options: .mutableLeaves) as? ReceiptInfo ?? [:] else {
let jsonStr = String(data: safeData, encoding: String.Encoding.utf8)
completion(.error(error: .jsonDecodeError(string: jsonStr)))
return
}

// get status from info
if let status = receiptInfo["status"] as? Int {
/*
* http://stackoverflow.com/questions/16187231/how-do-i-know-if-an-in-app-purchase-receipt-comes-from-the-sandbox
* How do I verify my receipt (iOS)?
* Always verify your receipt first with the production URL; proceed to verify
* with the sandbox URL if you receive a 21007 status code. Following this
* approach ensures that you do not have to switch between URLs while your
* application is being tested or reviewed in the sandbox or is live in the
* App Store.

* Note: The 21007 status code indicates that this receipt is a sandbox receipt,
* but it was sent to the production service for verification.
*/
let receiptStatus = ReceiptStatus(rawValue: status) ?? ReceiptStatus.unknown
if case .testReceipt = receiptStatus {
let sandboxValidator = AppleReceiptValidator(service: .sandbox)
sandboxValidator.validate(receipt: receipt, password: autoRenewPassword, completion: completion)
}
else {
if receiptStatus.isValid {
completion(.success(receipt: receiptInfo))
}
else {
completion(.error(error: .receiptInvalid(receipt: receiptInfo, status: receiptStatus)))
}
}
}
else {
completion(.error(error: .receiptInvalid(receipt: receiptInfo, status: ReceiptStatus.none)))
}
}
task.resume()
}
}
5 changes: 5 additions & 0 deletions SwiftyStoreKit/SwiftyStoreKit+Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ public struct Product {
public let needsFinishTransaction: Bool
}

//Conform to this protocol to provide custom receipt validator
public protocol ReceiptValidator {
func validate(receipt: String, password autoRenewPassword: String?, completion: @escaping (VerifyReceiptResult) -> Void)
}

// Payment transaction
public protocol PaymentTransaction {
var transactionState: SKPaymentTransactionState { get }
Expand Down
5 changes: 3 additions & 2 deletions SwiftyStoreKit/SwiftyStoreKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,11 @@ public class SwiftyStoreKit {
* - Parameter completion: handler for result
*/
public class func verifyReceipt(
using validator: ReceiptValidator,
password: String? = nil,
session: URLSession = URLSession.shared,
completion:@escaping (VerifyReceiptResult) -> ()) {
InAppReceipt.verify(urlType: .production, password: password, session: session) { result in

InAppReceipt.verify(using: validator, password: password) { result in

DispatchQueue.main.async {
completion(result)
Expand Down

0 comments on commit 4fce54d

Please sign in to comment.