Skip to content

Instantly share code, notes, and snippets.

@simsaens
Created December 20, 2021 05:39
Show Gist options
  • Save simsaens/58754935235cd951bea0dc29631d9c22 to your computer and use it in GitHub Desktop.
Save simsaens/58754935235cd951bea0dc29631d9c22 to your computer and use it in GitHub Desktop.
These three files extend the default StoreKit API to use async in Swift 5.5. Based on the PromiseKit StoreKit extensions
//
// SKPayment+Async.swift
//
// Created by Sim Saens on 15/12/21.
//
import Foundation
import StoreKit
//Adds an async extension to SKPayment to purchase products
// Based on the PromiseKit StoreKit extensions and modified for async in Swift 5.5
extension SKPayment {
func purchase() async throws -> SKPaymentTransaction {
try await withCheckedThrowingContinuation { continuation in
PaymentObserver(payment: self) { transaction, error in
if let error = error {
continuation.resume(throwing: error)
return
}
continuation.resume(returning: transaction)
}
}
}
}
private class PaymentObserver: NSObject, SKPaymentTransactionObserver {
enum PaymentError: Error {
case cancelled
}
let completion: (SKPaymentTransaction, Error?) -> ()
let payment: SKPayment
var retainCycle: PaymentObserver?
@discardableResult
init(payment: SKPayment, completion: @escaping (SKPaymentTransaction, Error?) -> ()) {
self.payment = payment
self.completion = completion
super.init()
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().add(payment)
retainCycle = self
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
guard let transaction = transactions.first(where: { $0.payment == payment }) else {
return
}
switch transaction.transactionState {
case .purchased, .restored:
queue.finishTransaction(transaction)
completion(transaction, nil)
queue.remove(self)
retainCycle = nil
case .failed:
let error = transaction.error ?? PaymentError.cancelled
queue.finishTransaction(transaction)
completion(transaction, error)
queue.remove(self)
retainCycle = nil
default:
break
}
}
}
//
// SKPaymentQueue+Async.swift
//
// Created by Sim Saens on 14/12/21.
//
import Foundation
import StoreKit
//Adds an async extension to SKPaymentQueue to restore transactions
// Based on the PromiseKit StoreKit extensions and modified for async in Swift 5.5
extension SKPaymentQueue {
func restoreCompletedTransactions() async throws -> [SKPaymentTransaction] {
try await withCheckedThrowingContinuation{ continuation in
PaymentObserver(self) { transactions, error in
if let error = error {
continuation.resume(throwing: error)
return
}
continuation.resume(returning: transactions)
}
}
}
}
private class PaymentObserver: NSObject, SKPaymentTransactionObserver {
let completion: ([SKPaymentTransaction], Error?) -> ()
var retainCycle: PaymentObserver?
var transactions: [SKPaymentTransaction] = []
@discardableResult
init(_ paymentQueue: SKPaymentQueue, completion: @escaping ([SKPaymentTransaction], Error?) -> ()) {
self.completion = completion
super.init()
paymentQueue.add(self)
paymentQueue.restoreCompletedTransactions()
retainCycle = self
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
self.transactions += transactions.filter { $0.transactionState == .restored }
transactions.forEach(queue.finishTransaction)
}
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
finish(queue)
}
func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
finish(queue, with: error)
}
func finish(_ queue: SKPaymentQueue, with error: Error? = nil) {
if let error = error {
completion([], error)
} else {
completion(transactions, nil)
}
queue.remove(self)
retainCycle = nil
}
}
//
// SKProductsRequest+Async.swift
//
// Created by Sim Saens on 16/12/21.
//
import Foundation
import StoreKit
//Adds an async extension to SKProductsRequest to fetch products from StoreKit
// Based on the PromiseKit StoreKit extensions and modified for async in Swift 5.5
extension SKProductsRequest {
func perform() async throws -> SKProductsResponse {
try await withCheckedThrowingContinuation { continuation in
SKDelegate(request: self) { response, error in
if let error = error {
continuation.resume(throwing: error)
return
}
continuation.resume(returning: response!)
}
}
}
}
private class SKDelegate: NSObject, SKProductsRequestDelegate {
let completion: (SKProductsResponse?, Error?) -> ()
var retainCycle: SKDelegate?
@discardableResult
init(request: SKProductsRequest, completion: @escaping (SKProductsResponse?, Error?) -> ()) {
self.completion = completion
super.init()
self.retainCycle = self
request.delegate = self
request.start()
}
@objc fileprivate func request(_ request: SKRequest, didFailWithError error: Error) {
completion(nil, error)
retainCycle = nil
}
@objc fileprivate func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
completion(response, nil)
retainCycle = nil
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment