Skip to content

Commit

Permalink
Merge pull request #1 from bizz84/feature/verifyPurchase
Browse files Browse the repository at this point in the history
Feature/verify purchase
  • Loading branch information
NicolasMahe committed May 23, 2016
2 parents 14dceec + d25d3ef commit fdf87bf
Show file tree
Hide file tree
Showing 7 changed files with 340 additions and 141 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ SwiftyStoreKit.verifyReceipt() { result in

let purchaseResult = SwiftyStoreKit.verifyPurchase(
productId: "com.musevisions.SwiftyStoreKit.Purchase1",
inReceipt: receipt
inReceipt: receipt,
purchaseType: .NonConsumable
)
switch purchaseResult {
case .Purchased(let expiresDate):
Expand Down
108 changes: 61 additions & 47 deletions SwiftyStoreDemo/Base.lproj/Main.storyboard

Large diffs are not rendered by default.

96 changes: 84 additions & 12 deletions SwiftyStoreDemo/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,38 +26,67 @@ import UIKit
import StoreKit
import SwiftyStoreKit

enum RegisteredPurchase : String {

case Purchase1 = "purchase1"
case Purchase2 = "purchase2"
case NonConsumablePurchase = "nonConsumablePurchase"
case ConsumablePurchase = "consumablePurchase"
case AutoRenewablePurchase = "autoRenewablePurchase"

var purchaseType: SwiftyStoreKit.PurchaseType {
switch self {
case .Purchase1: return .NonConsumable
case .Purchase2: return .NonConsumable
case .NonConsumablePurchase: return .NonConsumable
case .ConsumablePurchase: return .Consumable
case .AutoRenewablePurchase: return .AutomaticallyRenewableSubscription(validUntilDate: NSDate())
}
}
}


class ViewController: UIViewController {

let AppBundleId = "com.musevisions.iOS.SwiftyStoreKit"

let Purchase1 = RegisteredPurchase.Purchase1
let Purchase2 = RegisteredPurchase.AutoRenewablePurchase

// MARK: actions
@IBAction func getInfo1() {
getInfo("1")
}
@IBAction func getInfo2() {
getInfo("2")
getInfo(Purchase1)
}
@IBAction func purchase1() {
purchase("1")
purchase(Purchase1)
}
@IBAction func verifyPurchase1() {
verifyPurchase(Purchase1)
}
@IBAction func getInfo2() {
getInfo(Purchase2)
}
@IBAction func purchase2() {
purchase("2")
purchase(Purchase2)
}

func getInfo(no: String) {
@IBAction func verifyPurchase2() {
verifyPurchase(Purchase2)
}

func getInfo(purchase: RegisteredPurchase) {

NetworkActivityIndicatorManager.networkOperationStarted()
SwiftyStoreKit.retrieveProductsInfo([AppBundleId + ".purchase" + no]) { result in
SwiftyStoreKit.retrieveProductsInfo([AppBundleId + "." + purchase.rawValue]) { result in
NetworkActivityIndicatorManager.networkOperationFinished()

self.showAlert(self.alertForProductRetrievalInfo(result))
}
}

func purchase(no: String) {
func purchase(purchase: RegisteredPurchase) {

NetworkActivityIndicatorManager.networkOperationStarted()
SwiftyStoreKit.purchaseProduct(AppBundleId + ".purchase" + no) { result in
SwiftyStoreKit.purchaseProduct(AppBundleId + "." + purchase.rawValue) { result in
NetworkActivityIndicatorManager.networkOperationFinished()

self.showAlert(self.alertForPurchaseResult(result))
Expand Down Expand Up @@ -89,6 +118,31 @@ class ViewController: UIViewController {
}
}

func verifyPurchase(purchase: RegisteredPurchase) {

NetworkActivityIndicatorManager.networkOperationStarted()
SwiftyStoreKit.verifyReceipt() { result in
NetworkActivityIndicatorManager.networkOperationFinished()

switch result {
case .Success(let receipt):

let purchaseResult = SwiftyStoreKit.verifyPurchase(
productId: self.AppBundleId + "." + purchase.rawValue,
inReceipt: receipt,
purchaseType: purchase.purchaseType
)
self.showAlert(self.alertForVerifyPurchase(purchaseResult))

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

func refreshReceipt() {

SwiftyStoreKit.refreshReceipt { (result) -> () in
Expand Down Expand Up @@ -174,7 +228,7 @@ extension ViewController {
}


func alertForVerifyReceipt(result: SwiftyStoreKit.VerifyReceiptResult) -> UIAlertController{
func alertForVerifyReceipt(result: SwiftyStoreKit.VerifyReceiptResult) -> UIAlertController {

switch result {
case .Success(let receipt):
Expand All @@ -189,7 +243,25 @@ extension ViewController {
return alertWithTitle("Receipt verification", message: "Receipt verification failed")
}
}
}

func alertForVerifyPurchase(result: SwiftyStoreKit.VerifyPurchaseResult) -> UIAlertController {

switch result {
case .Purchased(let expiresDate):
if let expiresDate = expiresDate {
print("Product is valid until \(expiresDate)")
return alertWithTitle("Product is purchased", message: "Product is valid until \(expiresDate)")
}
print("Product is purchased")
return alertWithTitle("Product is purchased", message: "Product will not expire")
case .Expired(let expiresDate): // Only for Automatically Renewable Subscription
print("Product is expired since \(expiresDate)")
return alertWithTitle("Product expired", message: "Product is expired since \(expiresDate)")
case .NotPurchased:
print("This product has never been purchased")
return alertWithTitle("Not purchased", message: "This product has never been purchased")
}
}

func alertForRefreshReceipt(result: SwiftyStoreKit.RefreshReceiptResult) -> UIAlertController {
Expand Down
115 changes: 66 additions & 49 deletions SwiftyStoreKit/InAppReceipt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ public typealias ReceiptInfo = [String: AnyObject]

// MARK: - Enumeration
extension SwiftyStoreKit {
public enum PurchaseType {
// A consumable In-App Purchase must be purchased every time the user downloads it.
case Consumable
// Non-consumable In-App Purchases only need to be purchased once by users.
case NonConsumable
// Automatically renewable subscriptions allow the user to purchase updating and dynamic content for a set duration of time.
case AutomaticallyRenewableSubscription(validUntilDate: NSDate)
// Free subscriptions don’t expire and can only be offered in apps that are in the Magazines & Newspapers category.
case FreeSubscription
// Non-Renewing Subscriptions allow the sale of services with a limited duration.
case NonRenewingSubscription(validUntilDate: NSDate)
}
public enum VerifyReceiptResult {
case Success(receipt: ReceiptInfo)
case Error(error: ReceiptError)
Expand Down Expand Up @@ -251,58 +263,63 @@ internal class InAppReceipt {
class func verifyPurchase(
productId productId: String,
inReceipt receipt: ReceiptInfo,
validUntil: NSDate? = nil
purchaseType: SwiftyStoreKit.PurchaseType
) -> SwiftyStoreKit.VerifyPurchaseResult {

// Force type of the latest_receipt_info
guard let inAppPurchases = receipt["receipt"]?["in_app"] as? [ReceiptInfo]
else {
return .NotPurchased
}

// Filter receipt with right product id
let receiptsWithGoodProductId = inAppPurchases
.filter { (receipt) -> Bool in
let product_id = receipt["product_id"] as? String
return product_id == productId
}

// Verify that at least one receipt has the right product id
guard receiptsWithGoodProductId.count >= 1 else {
return .NotPurchased
}

guard let validUntil = validUntil else {
// Do not need to verify the expiration date
return .Purchased(expiresDate: nil)
}
// Get all receipts
guard let allReceipts = receipt["receipt"]?["in_app"] as? [ReceiptInfo] else {
return .NotPurchased
}

// Return the expires dates sorted desc
let expiresDates = receiptsWithGoodProductId
.map { (receipt) -> NSDate in
let expires_date = receipt["expires_date_ms"] as? NSString
let expires_date_double = (expires_date?.doubleValue ?? 0.0) / 1000
return NSDate(timeIntervalSince1970: expires_date_double)
}
.sort { (a, b) -> Bool in
return a.compare(b) == .OrderedDescending
}
// Filter receipts with matching product id
let receiptsMatchingProductId = allReceipts
.filter { (receipt) -> Bool in
let product_id = receipt["product_id"] as? String
return product_id == productId
}

// Filter expired date
let validExpiresDate = expiresDates
.filter { (expires_date) -> Bool in
return expires_date.compare(validUntil) == .OrderedDescending
}
// Verify that at least one receipt has the right product id
guard receiptsMatchingProductId.count >= 1 else {
return .NotPurchased
}

// Check if at least 1 receipt is valid
if validExpiresDate.count >= 1 {
// The subscription is valid
return .Purchased(expiresDate: validExpiresDate.first!)
}
else {
// The subscription is expired
return .Expired(expiresDate: expiresDates.first!)
}
}

switch purchaseType {
// Do not need to verify the expiration date
case .Consumable, .NonConsumable, .FreeSubscription:
return .Purchased(expiresDate: nil)
case .AutomaticallyRenewableSubscription(let validUntilDate):
return verifyPurchase(receiptsMatchingProductId, validUntilDate: validUntilDate)
case .NonRenewingSubscription(let validUntilDate):
return verifyPurchase(receiptsMatchingProductId, validUntilDate: validUntilDate)
}
}

private class func verifyPurchase(receiptsMatchingProductId: [ReceiptInfo], validUntilDate date: NSDate) -> SwiftyStoreKit.VerifyPurchaseResult {
// Return the expires dates sorted desc
let expiresDates = receiptsMatchingProductId
.map { (receipt) -> NSDate in
let expires_date = receipt["expires_date_ms"] as? NSString
let expires_date_double = (expires_date?.doubleValue ?? 0.0) / 1000
return NSDate(timeIntervalSince1970: expires_date_double)
}
.sort { (a, b) -> Bool in
return a.compare(b) == .OrderedDescending
}

// Filter expired date
let validExpiresDate = expiresDates
.filter { (expires_date) -> Bool in
return expires_date.compare(date) == .OrderedDescending
}

// Check if at least 1 receipt is valid
if let firstValidExpiresDate = validExpiresDate.first {
// The subscription is valid
return .Purchased(expiresDate: firstValidExpiresDate)
}
else {
// The subscription is expired
return .Expired(expiresDate: expiresDates.first!)
}
}
}
11 changes: 8 additions & 3 deletions SwiftyStoreKit/SwiftyStoreKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,12 @@ public class SwiftyStoreKit {
password: String? = nil,
session: NSURLSession = NSURLSession.sharedSession(),
completion:(result: VerifyReceiptResult) -> ()) {
InAppReceipt.verify(receiptVerifyURL: url, password: password, session: session, completion: completion)
InAppReceipt.verify(receiptVerifyURL: url, password: password, session: session) { result in

dispatch_async(dispatch_get_main_queue()) {
completion(result: result)
}
}
}

/**
Expand All @@ -170,9 +175,9 @@ public class SwiftyStoreKit {
public class func verifyPurchase(
productId productId: String,
inReceipt receipt: ReceiptInfo,
validUntil: NSDate? = nil
purchaseType: PurchaseType
) -> SwiftyStoreKit.VerifyPurchaseResult {
return InAppReceipt.verifyPurchase(productId: productId, inReceipt: receipt, validUntil: validUntil)
return InAppReceipt.verifyPurchase(productId: productId, inReceipt: receipt, purchaseType: purchaseType)
}

#if os(iOS) || os(tvOS)
Expand Down
Loading

0 comments on commit fdf87bf

Please sign in to comment.