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

Promo code changes added #662

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions Sources/SwiftyStoreKit/CodeRedemptionControlller.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import Foundation
import StoreKit

struct CodeRedemption: Hashable {

let atomically: Bool
let callback: (TransactionResult) -> Void

func hash(into hasher: inout Hasher) {
hasher.combine(atomically)
}

static func == (lhs: CodeRedemption, rhs: CodeRedemption) -> Bool {
return true
}
}

class CodeRedemptionController: TransactionController {

private var codeRedemption: CodeRedemption?

func set(_ codeRedemption: CodeRedemption) {
self.codeRedemption = codeRedemption
}

func clearCodeRedemption() {
self.codeRedemption = nil
}

func processTransaction(_ transaction: SKPaymentTransaction, on paymentQueue: PaymentQueue) -> Bool {

let transactionProductIdentifier = transaction.payment.productIdentifier

guard let codeRedemption = self.codeRedemption else {

return false
}

let transactionState = transaction.transactionState

if transactionState == .purchased {
let purchase = PurchaseCodeRedemptionDetails(productId: transactionProductIdentifier, quantity: transaction.payment.quantity, transaction: transaction, originalTransaction: transaction.original, needsFinishTransaction: !codeRedemption.atomically)

codeRedemption.callback(.redeemed(purchase: purchase))

if codeRedemption.atomically {
paymentQueue.finishTransaction(transaction)
}

self.clearCodeRedemption()
return true
}

if transactionState == .failed {

codeRedemption.callback(.failed(error: transactionError(for: transaction.error as NSError?)))

paymentQueue.finishTransaction(transaction)

self.clearCodeRedemption()
return true
}

self.clearCodeRedemption()
return false
}

func transactionError(for error: NSError?) -> SKError {
let message = "Unknown error"
let altError = NSError(domain: SKErrorDomain, code: SKError.unknown.rawValue, userInfo: [ NSLocalizedDescriptionKey: message ])
let nsError = error ?? altError
return SKError(_nsError: nsError)
}

func processTransactions(_ transactions: [SKPaymentTransaction], on paymentQueue: PaymentQueue) -> [SKPaymentTransaction] {

return transactions.filter { !processTransaction($0, on: paymentQueue) }
}
}
27 changes: 27 additions & 0 deletions Sources/SwiftyStoreKit/PaymentQueueController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ protocol TransactionController {

public enum TransactionResult {
case purchased(purchase: PurchaseDetails)
case redeemed(purchase: PurchaseCodeRedemptionDetails)
case restored(purchase: Purchase)
case deferred(purchase: PurchaseDetails)
case failed(error: SKError)
Expand All @@ -57,6 +58,9 @@ public protocol PaymentQueue: AnyObject {
func restoreCompletedTransactions(withApplicationUsername username: String?)

func finishTransaction(_ transaction: SKPaymentTransaction)

@available(iOS 14.0, *)
func presentCodeRedemptionSheet()
}

extension SKPaymentQueue: PaymentQueue {
Expand Down Expand Up @@ -103,6 +107,7 @@ struct EntitlementRevocation {
class PaymentQueueController: NSObject, SKPaymentTransactionObserver {

private let paymentsController: PaymentsController
private let codeRedemptionController: CodeRedemptionController
private let restorePurchasesController: RestorePurchasesController
private let completeTransactionsController: CompleteTransactionsController
unowned let paymentQueue: PaymentQueue
Expand All @@ -114,11 +119,13 @@ class PaymentQueueController: NSObject, SKPaymentTransactionObserver {

init(paymentQueue: PaymentQueue = SKPaymentQueue.default(),
paymentsController: PaymentsController = PaymentsController(),
codeRedemptionController: CodeRedemptionController = CodeRedemptionController(),
restorePurchasesController: RestorePurchasesController = RestorePurchasesController(),
completeTransactionsController: CompleteTransactionsController = CompleteTransactionsController()) {

self.paymentQueue = paymentQueue
self.paymentsController = paymentsController
self.codeRedemptionController = codeRedemptionController
self.restorePurchasesController = restorePurchasesController
self.completeTransactionsController = completeTransactionsController
super.init()
Expand All @@ -131,7 +138,15 @@ class PaymentQueueController: NSObject, SKPaymentTransactionObserver {
assert(completeTransactionsController.completeTransactions != nil, message)
}

/**
This method is used to clean up the callback of the controller that handles the offer code redemptions (CodeRedemptionController) each time a normal purchase or a purchase restore is to be made. As the code redemption is managed by Apple and we do not find out if the Apple modal that we use to redeem the code (presentCodeRedemptionSheet ()) is closed, we have to delete the callback assigned previously so that there is no crossing of flows in SKPaymentTransactionObserver, thus, the controller in charge of managing the code exchanges will not manage any transaction.
*/
private func clearCompletionCodeRedemptionController() {
codeRedemptionController.clearCodeRedemption()
}

func startPayment(_ payment: Payment) {
clearCompletionCodeRedemptionController()
assertCompleteTransactionsWasCalled()

let skPayment = SKMutablePayment(product: payment.product)
Expand Down Expand Up @@ -165,6 +180,7 @@ class PaymentQueueController: NSObject, SKPaymentTransactionObserver {
}

func restorePurchases(_ restorePurchases: RestorePurchases) {
clearCompletionCodeRedemptionController()
assertCompleteTransactionsWasCalled()

if restorePurchasesController.restorePurchases != nil {
Expand Down Expand Up @@ -193,6 +209,15 @@ class PaymentQueueController: NSObject, SKPaymentTransactionObserver {
paymentQueue.finishTransaction(skTransaction)
}

@available(iOS 14.0, *)
func presentCodeRedemptionSheet(_ codeRedemption: CodeRedemption) {
assertCompleteTransactionsWasCalled()

codeRedemptionController.set(codeRedemption)

paymentQueue.presentCodeRedemptionSheet()
}

func start(_ downloads: [SKDownload]) {
paymentQueue.start(downloads)
}
Expand Down Expand Up @@ -243,6 +268,8 @@ class PaymentQueueController: NSObject, SKPaymentTransactionObserver {

unhandledTransactions = paymentsController.processTransactions(transactions, on: paymentQueue)

unhandledTransactions = codeRedemptionController.processTransactions(unhandledTransactions, on: paymentQueue)

unhandledTransactions = restorePurchasesController.processTransactions(unhandledTransactions, on: paymentQueue)

unhandledTransactions = completeTransactionsController.processTransactions(unhandledTransactions, on: paymentQueue)
Expand Down
24 changes: 24 additions & 0 deletions Sources/SwiftyStoreKit/SwiftyStoreKit+Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,23 @@ public struct PurchaseDetails {
}
}

/// Purchased product with offer code
public struct PurchaseCodeRedemptionDetails {
public let productId: String
public let quantity: Int
public let transaction: PaymentTransaction
public let originalTransaction: PaymentTransaction?
public let needsFinishTransaction: Bool

public init(productId: String, quantity: Int, transaction: PaymentTransaction, originalTransaction: PaymentTransaction?, needsFinishTransaction: Bool) {
self.productId = productId
self.quantity = quantity
self.transaction = transaction
self.originalTransaction = originalTransaction
self.needsFinishTransaction = needsFinishTransaction
}
}

/// Conform to this protocol to provide custom receipt validator
public protocol ReceiptValidator {
func validate(receiptData: Data, completion: @escaping (VerifyReceiptResult) -> Void)
Expand Down Expand Up @@ -132,6 +149,13 @@ public enum PurchaseResult {
case error(error: SKError)
}

/// CodeRedemption result
public enum CodeRedemptionResult {
case redeemed(purchase: PurchaseCodeRedemptionDetails)
case deferred(purchase: PurchaseCodeRedemptionDetails)
case error(error: SKError)
}

/// Restore purchase results
public struct RestoreResults {
public let restoredPurchases: [Purchase]
Expand Down
40 changes: 40 additions & 0 deletions Sources/SwiftyStoreKit/SwiftyStoreKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@ public class SwiftyStoreKit {
paymentQueueController.completeTransactions(CompleteTransactions(atomically: atomically, callback: completion))
}


fileprivate func redeemOfferCode(atomically: Bool, completion: @escaping (CodeRedemptionResult) -> Void) {

if #available(iOS 14.0, *) {
paymentQueueController.presentCodeRedemptionSheet(CodeRedemption(atomically: atomically) { result in

completion(self.processCodeRedemptionResult(result))
})
}
}

fileprivate func onEntitlementRevocation(completion: @escaping ([String]) -> Void) {

paymentQueueController.onEntitlementRevocation(EntitlementRevocation(callback: completion))
Expand All @@ -101,13 +112,31 @@ public class SwiftyStoreKit {
return .success(purchase: purchase)
case .deferred(let purchase):
return .deferred(purchase: purchase)
case .redeemed(purchase: let purchase):
return .error(error: storeInternalError(description: "Cannot redeemed code product \(purchase.productId) from purchase path"))
case .failed(let error):
return .error(error: error)
case .restored(let purchase):
return .error(error: storeInternalError(description: "Cannot restore product \(purchase.productId) from purchase path"))
}
}

private func processCodeRedemptionResult(_ result: TransactionResult) -> CodeRedemptionResult {
switch result {
case .purchased(purchase: let purchase):
return .error(error: storeInternalError(description: "Cannot purchase product \(purchase.productId) from code redemption path"))
case .redeemed(let purchase):
return .redeemed(purchase: purchase)
case .deferred(let purchase):
let error = storeInternalError(description: "Cannot purchase product \(purchase.productId) from restore purchases path")
return .error(error: error)
case .failed(let error):
return .error(error: error)
case .restored(let purchase):
return .error(error: storeInternalError(description: "Cannot restore product \(purchase.productId) from code redemption path"))
}
}

private func processRestoreResults(_ results: [TransactionResult]) -> RestoreResults {
var restoredPurchases: [Purchase] = []
var restoreFailedPurchases: [(SKError, String?)] = []
Expand All @@ -116,6 +145,9 @@ public class SwiftyStoreKit {
case .purchased(let purchase):
let error = storeInternalError(description: "Cannot purchase product \(purchase.productId) from restore purchases path")
restoreFailedPurchases.append((error, purchase.productId))
case .redeemed(purchase: let purchase):
let error = storeInternalError(description: "Cannot redeem code product \(purchase.productId) from restore purchases path")
restoreFailedPurchases.append((error, purchase.productId))
case .deferred(let purchase):
let error = storeInternalError(description: "Cannot purchase product \(purchase.productId) from restore purchases path")
restoreFailedPurchases.append((error, purchase.productId))
Expand Down Expand Up @@ -190,6 +222,14 @@ extension SwiftyStoreKit {
sharedInstance.restorePurchases(atomically: atomically, applicationUsername: applicationUsername, completion: completion)
}

/// RedeemOfferCode
/// - Parameter atomically: whether the code is redeemed atomically (e.g. `finishTransaction` is called immediately)
public class func redeemOfferCode(atomically: Bool = true, completion: @escaping (CodeRedemptionResult) -> Void) {

sharedInstance.redeemOfferCode(atomically: atomically, completion: completion)

}

/// Complete transactions
/// - Parameter atomically: whether the product is purchased atomically (e.g. `finishTransaction` is called immediately)
/// - Parameter completion: handler for result
Expand Down
7 changes: 7 additions & 0 deletions Tests/SwiftyStoreKitTests/PaymentQueueSpy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import SwiftyStoreKit
import StoreKit

class PaymentQueueSpy: PaymentQueue {



weak var observer: SKPaymentTransactionObserver?

Expand Down Expand Up @@ -45,6 +47,10 @@ class PaymentQueueSpy: PaymentQueue {
finishTransactionCalledCount += 1
}

func presentCodeRedemptionSheet() {

}

func start(_ downloads: [SKDownload]) {

}
Expand All @@ -60,4 +66,5 @@ class PaymentQueueSpy: PaymentQueue {
func cancel(_ downloads: [SKDownload]) {

}

}