I'm doing my first In-App purchase and a weird behaviour is that an consumable product is being restored instead of creating new transaction.
I've followed the tuto https://www.raywenderlich.com/122144/in-app-purchase-tutorial
Which I found an elegant approach.
here is my StoreKit helper:
import StoreKit
public typealias ProductIdentifier = String
public typealias ProductsRequestCompletionHandler = (_ success: Bool, _ products: [SKProduct]?) -> ()
open class IAPHelper : NSObject {
fileprivate let productIdentifiers: Set<ProductIdentifier>
fileprivate var purchasedProductIdentifiers: Set<ProductIdentifier> = Set()
fileprivate var productsRequest: SKProductsRequest?
fileprivate var productsRequestCompletionHandler: ProductsRequestCompletionHandler?
static let IAPHelperPurchaseNotification = "IAPHelperPurchaseNotification"
public init(productIds: Set<ProductIdentifier>) {
productIdentifiers = productIds
super.init()
SKPaymentQueue.default().add(self)
}
}
// MARK: - StoreKit API
extension IAPHelper {
public func requestProducts(_ completionHandler: #escaping ProductsRequestCompletionHandler) {
productsRequest?.cancel()
productsRequestCompletionHandler = completionHandler
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productsRequest!.delegate = self
productsRequest!.start()
}
public func buyProduct(_ product: SKProduct) {
print("Buying \(product.productIdentifier)...")
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
public func isProductPurchased(_ productIdentifier: ProductIdentifier) -> Bool {
return purchasedProductIdentifiers.contains(productIdentifier)
}
public class func canMakePayments() -> Bool {
return SKPaymentQueue.canMakePayments()
}
public func restorePurchases() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
// MARK: - SKProductsRequestDelegate
extension IAPHelper: SKProductsRequestDelegate {
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("Loaded list of products...")
let products = response.products
productsRequestCompletionHandler?(true, products)
clearRequestAndHandler()
for p in products {
print("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)")
}
}
public func request(_ request: SKRequest, didFailWithError error: Error) {
print("Failed to load list of products.")
print("Error: \(error.localizedDescription)")
productsRequestCompletionHandler?(false, nil)
clearRequestAndHandler()
}
private func clearRequestAndHandler() {
productsRequest = nil
productsRequestCompletionHandler = nil
}
}
// MARK: - SKPaymentTransactionObserver
extension IAPHelper: SKPaymentTransactionObserver {
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch (transaction.transactionState) {
case .purchased:
complete(transaction: transaction)
break
case .failed:
fail(transaction: transaction)
break
case .restored:
restore(transaction: transaction)
break
case .deferred:
break
case .purchasing:
break
}
}
}
private func complete(transaction: SKPaymentTransaction) {
print("complete...")
validateReceipt()
deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func restore(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restore... \(productIdentifier)")
deliverPurchaseNotificationFor(identifier: productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func fail(transaction: SKPaymentTransaction) {
print("fail...")
if let transactionError = transaction.error as? NSError {
if transactionError.code != SKError.paymentCancelled.rawValue {
print("Transaction Error: \(transaction.error?.localizedDescription)")
}
}
SKPaymentQueue.default().finishTransaction(transaction)
}
private func deliverPurchaseNotificationFor(identifier: String?) {
guard let identifier = identifier else { return }
purchasedProductIdentifiers.insert(identifier)
UserDefaults.standard.set(true, forKey: identifier)
UserDefaults.standard.synchronize()
NotificationCenter.default.post(name: NSNotification.Name(rawValue: IAPHelper.IAPHelperPurchaseNotification), object: identifier)
}
}
I'm able to buy once, second it is restored automatically:
This In-App purchase has already been bought. It will be restored for
free
When I get this message, none of IAPHelper methods is called.
My iTunes showing it is a consumable:
Even uninstalling the app the purchased still being restored.
It really looks like an Apple bug as my firsts tests I could buy 2..3 times without this message.
If it is not a bug, how can I prevent this situation ?
For those stuck in this silly situation, here are some WA and solution:
queue.finishTransaction(transaction)
IMPORTANT: Finish the transaction in your paymentQueue method:
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch (transaction.transactionState) {
case .purchased:
complete(transaction: transaction)
queue.finishTransaction(transaction)
break
case .failed:
fail(transaction: transaction)
queue.finishTransaction(transaction)
break
case .restored:
restore(transaction: transaction)
queue.finishTransaction(transaction)
break
case .deferred:
break
case .purchasing:
break
}
}
}
This will avoid the situation.
If you are already stuck on it, here is the work around, just for tests purposes !
for transaction in SKPaymentQueue.default().transactions {
print(transaction)
SKPaymentQueue.default().finishTransaction(transaction)
}
You can place it on a temporary button and clear all transactions while testing in one tap.
At least it saved my life today.
Related
I have consumable, auto renewable and non renewable in app purchase in my application. I am able to purchase product in sandbox mode successfully. But sometimes i am getting following error randomly.
transaction error code is Optional(Error Domain=SKErrorDomain Code=0 "An unknown error occurred" UserInfo={NSLocalizedDescription=An unknown error occurred, NSUnderlyingError=0x28225aca0 {Error Domain=ASDErrorDomain Code=500 "Unhandled exception" UserInfo={NSUnderlyingError=0x28225b1e0 {Error Domain=AMSErrorDomain Code=301 "Invalid Status Code" UserInfo={NSLocalizedDescription=Invalid Status Code, NSLocalizedFailureReason=The response has an invalid status code}}, NSLocalizedFailureReason=An unknown error occurred, NSLocalizedDescription=Unhandled exception}}})
Following is the code i have implemented.
open class IAPHelper : NSObject {
static let IAPHelperPurchaseNotification = "IAPHelperPurchaseNotification"
var productIdentifiers: Set<ProductIdentifier>?
fileprivate var purchasedProductIdentifiers = Set<ProductIdentifier>()
fileprivate var productsRequest: SKProductsRequest?
fileprivate var productsRequestCompletionHandler: ProductsRequestCompletionHandler?
static let shared : IAPHelper = IAPHelper()
override public init() {
super.init()
print("IAP Init method called")
}
public func getProducts(productIds: Set<ProductIdentifier>)
{
productIdentifiers = productIds
for productIdentifier in productIds {
let purchased = UserDefaults.standard.bool(forKey: productIdentifier)
if purchased {
purchasedProductIdentifiers.insert(productIdentifier)
print("Previously purchased: \(productIdentifier)")
} else {
print("Not purchased: \(productIdentifier)")
}
}
if(appDel.isObserverAdded == false)
{
print("observer added")
// SKPaymentQueue.default().remove(self)
SKPaymentQueue.default().add(self)
appDel.isObserverAdded = true
}
}
}
// MARK: - StoreKit API
extension IAPHelper {
public func requestProducts(completionHandler: #escaping ProductsRequestCompletionHandler) {
productsRequest?.cancel()
productsRequestCompletionHandler = completionHandler
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers!)
productsRequest!.delegate = self
productsRequest!.start()
}
public func buyProduct(_ product: SKProduct) {
appDelChat.isPurchaseDone = true
print("Buying \(product.productIdentifier)...")
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
public func isProductPurchased(_ productIdentifier: ProductIdentifier) -> Bool {
return purchasedProductIdentifiers.contains(productIdentifier)
}
public class func canMakePayments() -> Bool {
return SKPaymentQueue.canMakePayments()
}
public func restorePurchases() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
// MARK: - SKProductsRequestDelegate
extension IAPHelper: SKProductsRequestDelegate {
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse)
{
print("sk payment response is",response.invalidProductIdentifiers)
var products = response.products
print("Loaded list of products...",products)
products = products.sorted(by: { (item1, item2) -> Bool in
return item1.price.doubleValue < item2.price.doubleValue
})
print("sorted products are",products)
productsRequestCompletionHandler?(true, products)
clearRequestAndHandler()
for p in products {
print("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)")
}
}
public func request(_ request: SKRequest, didFailWithError error: Error) {
print("Failed to load list of products.")
print("Error: \(error.localizedDescription)")
productsRequestCompletionHandler?(false, nil)
clearRequestAndHandler()
}
private func clearRequestAndHandler() {
productsRequest = nil
productsRequestCompletionHandler = nil
}
}
// MARK: - SKPaymentTransactionObserver
extension IAPHelper: SKPaymentTransactionObserver {
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction])
{
print("updated transaction")
for transaction in transactions {
switch (transaction.transactionState)
{
case .purchased:
print("product purchased",transaction.payment.productIdentifier)
complete(transaction: transaction)
print("credit purchase delegate",creditPurchasedDelegate)
print("all did purchase delegate",allDIDPurchasedDelegate)
NotificationCenter.default.post(name: NSNotification.Name(rawValue: IAPHelper.IAPHelperPurchaseNotification), object: transaction)
break
case .failed:
fail(transaction: transaction)
NotificationCenter.default.post(name: NSNotification.Name(rawValue: IAPHelper.IAPHelperPurchaseNotification), object: transaction)
break
case .restored:
restore(transaction: transaction)
NotificationCenter.default.post(name: NSNotification.Name(rawValue: IAPHelper.IAPHelperPurchaseNotification), object: transaction)
break
case .deferred:
NotificationCenter.default.post(name: NSNotification.Name(rawValue: IAPHelper.IAPHelperPurchaseNotification), object: transaction)
break
case .purchasing:
NotificationCenter.default.post(name: NSNotification.Name(rawValue: IAPHelper.IAPHelperPurchaseNotification), object: transaction)
break
}
}
}
private func complete(transaction: SKPaymentTransaction) {
print("complete...")
SKPaymentQueue.default().finishTransaction(transaction)
}
private func restore(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restore... \(productIdentifier)")
SKPaymentQueue.default().finishTransaction(transaction)
}
private func fail(transaction: SKPaymentTransaction) {
print("fail...")
if let transactionError = transaction.error as? NSError {
if transactionError.code != SKError.paymentCancelled.rawValue {
print("Transaction Error: \(transaction.error?.localizedDescription)")
}
}
SKPaymentQueue.default().finishTransaction(transaction)
}
What could be the cause of this error?
I implemented In-App-Purchases in my app using the tutorial from https://www.raywenderlich.com/5456-in-app-purchase-tutorial-getting-started. I am not using my own server to validate purchases or something like that.
I created a sandbox user to test the code. Everything works fine, however, if I try to log in with my personal Apple ID, the purchase will fail (tested on TestFlight).
Is this expected behavior or am I doing something wrong? https://stackoverflow.com/a/37042040/11912101 states that every account should be able to purchase items.
Also, I enabled the option "interrupt purchase" for the sandbox user in App Store Connect. The code below will run the function failed(...), even though the purchase went through and the item is unlocked in my app and gets added to purchasedProductIdentifiers. Is there a way to handle those interrupted purchases?
Thanks for answering!
import StoreKit
public struct InAppPurchases {
static let proVersionID = "myapp.proversion"
private static let productIdentifiers: Set<ProductIdentifier> = [proVersionID]
public static let helper = InAppPurchaseHelper(productIds: InAppPurchases.productIdentifiers)
}
func resourceNameForProductIdentifier(_ productIdentifier: String) -> String? {
return productIdentifier.components(separatedBy: ".").last
}
public typealias ProductIdentifier = String
public typealias ProductsRequestCompletionHandler = (_ success: Bool, _ products: [SKProduct]?) -> Void
extension Notification.Name {
static let IAPHelperPurchaseNotification = Notification.Name("IAPHelperPurchaseNotification")
}
open class InAppPurchaseHelper: NSObject, ObservableObject {
private let productIdentifiers: Set<ProductIdentifier>
private var purchasedProductIdentifiers: Set<ProductIdentifier> = [] {
willSet {
self.objectWillChange.send()
}
}
private var productsRequest: SKProductsRequest?
private var productsRequestCompletionHandler: ProductsRequestCompletionHandler?
#Published var didFail = false
var availableProducts = [SKProduct]() {
willSet {
DispatchQueue.main.async {
self.objectWillChange.send()
}
}}
public init(productIds: Set<ProductIdentifier>) {
productIdentifiers = productIds
for productIdentifier in productIds {
let purchased = UserDefaults.standard.bool(forKey: productIdentifier)
if purchased {
purchasedProductIdentifiers.insert(productIdentifier)
print("Previously purchased: \(productIdentifier)")
} else {
print("Not purchased: \(productIdentifier)")
}
}
super.init()
SKPaymentQueue.default().add(self)
reloadInAppPurchases()
}
func reloadInAppPurchases() {
DispatchQueue.main.async {
InAppPurchases.helper.requestProducts{ [weak self] success, products in
guard let self = self else { return }
if success {
self.availableProducts = products!
}
}
}
}
}
extension InAppPurchaseHelper {
public func requestProducts(_ completionHandler: #escaping ProductsRequestCompletionHandler) {
productsRequest?.cancel()
productsRequestCompletionHandler = completionHandler
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productsRequest!.delegate = self
productsRequest!.start()
}
public func buyProduct(_ product: SKProduct) {
print("Buying \(product.productIdentifier)...")
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
public func isProductPurchased(_ productIdentifier: ProductIdentifier) -> Bool {
return purchasedProductIdentifiers.contains(productIdentifier)
}
public func isProVersion () -> Bool {
if(isProductPurchased(InAppPurchases.proVersionID)) {
return true
}
return false
}
public class func canMakePayments() -> Bool {
return SKPaymentQueue.canMakePayments()
}
public func restorePurchases() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
extension InAppPurchaseHelper: SKProductsRequestDelegate {
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("Loaded list of products...")
let products = response.products
productsRequestCompletionHandler?(true, products)
clearRequestAndHandler()
for p in products {
print("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)")
}
}
public func request(_ request: SKRequest, didFailWithError error: Error) {
print("Failed to load list of products.")
print("Error: \(error.localizedDescription)")
productsRequestCompletionHandler?(false, nil)
clearRequestAndHandler()
}
private func clearRequestAndHandler() {
productsRequest = nil
productsRequestCompletionHandler = nil
}
}
extension InAppPurchaseHelper: SKPaymentTransactionObserver {
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch (transaction.transactionState) {
case .purchased:
complete(transaction: transaction)
break
case .failed:
fail(transaction: transaction)
break
case .restored:
restore(transaction: transaction)
break
case .deferred:
break
case .purchasing:
break
#unknown default:
break
}
}
}
private func complete(transaction: SKPaymentTransaction) {
print("complete...")
deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func restore(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restore... \(productIdentifier)")
deliverPurchaseNotificationFor(identifier: productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func fail(transaction: SKPaymentTransaction) {
print("fail...")
if let transactionError = transaction.error as NSError?,
let localizedDescription = transaction.error?.localizedDescription,
transactionError.code != SKError.paymentCancelled.rawValue {
print("Transaction Error: \(localizedDescription)")
}
SKPaymentQueue.default().finishTransaction(transaction)
if(!isProVersion()){
didFail.toggle()
}
}
private func deliverPurchaseNotificationFor(identifier: String?) {
guard let identifier = identifier else { return }
purchasedProductIdentifiers.insert(identifier)
UserDefaults.standard.set(true, forKey: identifier)
NotificationCenter.default.post(name: .IAPHelperPurchaseNotification, object: identifier)
}
}
See this and this.
I had no issue testing IAP with other account recently. This is odd as testflight is sandbox environment by default so I don't see need for waiting like for release version but maybe wait for few hours just in case.
I was following the Kilo Loco tutorial for store kit but I can't seem to purchase the product when its is called. This is my class:
import Foundation
import StoreKit
enum GameProducts: String {
case removeAds = "BundleID" // I have registered one but Id rather keep it private
}
class GameIAP: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
static let shared = GameIAP()
var products = [SKProduct]()
let myQueue = SKPaymentQueue.default()
override init() {
}
func getProduct() {
let products: Set = [GameProducts.removeAds.rawValue]
let request = SKProductsRequest(productIdentifiers: products)
request.delegate = self
request.start()
myQueue.add(self)
}
func purchase(product: GameProducts) {
guard let theProduct = products.filter({ $0.productIdentifier == product.rawValue}).first else
{ return }
let payment = SKPayment(product: theProduct)
myQueue.add(payment)
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
products = response.products
}
func restore() {
myQueue.restoreCompletedTransactions()
}
func complete(transaction: SKPaymentTransaction) {
SKPaymentQueue.default().finishTransaction(transaction)
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "payment"), object: nil)
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
print(transaction.transactionState.currentStatus(), transaction.payment.productIdentifier)
switch transaction.transactionState {
case .purchased: complete(transaction: transaction)
default: break
}
}
}
}
extension SKPaymentTransactionState {
func currentStatus() -> String {
switch self {
case .deferred: return "Deferred"
case .failed: return "Failed"
case .purchased: return "Purchased"
case .purchasing: return "Purchasing"
case .restored: return "Restored"
}
}
}
When the ViewController loads it calls getProduct() and then when I hit a button it calls the purchase function but this doesn't seem to work??
The restore function works absolutely fine but the purchase doesn't return anything...
Has anyone done this before and know where I've gone wrong or is this something on app store connect???
I am making a game in Sprite Kit.
I have IAPProducts Swift File with code:
import Foundation
enum IAPProducts: String {
case nonConsumable1 = "Kingdom.Lion"
case nonConsumable2 = "Kingdom.Sheep"
}
and IAPService.Swift file with code:
import Foundation
import StoreKit
var lionpurchased = false
var sheeppurchased = false
class IAPService: NSObject {
private override init() {}
static let shared = IAPService()
var products = [SKProduct]()
let paymentQueue = SKPaymentQueue.default()
func getProducts() {
let products: Set = [
IAPProducts.nonConsumable1.rawValue,
IAPProducts.nonConsumable2.rawValue
]
let request = SKProductsRequest(productIdentifiers: products)
request.delegate = self
request.start()
paymentQueue.add(self)
}
func purchase(product: IAPProducts) {
guard let productToPurchase = products.filter({ $0.productIdentifier == product.rawValue }).first else { return }
let payment = SKPayment(product: productToPurchase)
paymentQueue.add(payment)
}
func restorePurchases() {
print("restore purchases")
paymentQueue.restoreCompletedTransactions()
}
}
extension IAPService: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
products = response.products
for product in response.products {
print(product.localizedTitle)
}
}
}
extension IAPService: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
print(transaction.transactionState.status(), transaction.payment.productIdentifier)
switch transaction.transactionState {
case .purchasing: break
default: queue.finishTransaction(transaction)
}
}
}
}
extension SKPaymentTransactionState {
func status() -> String {
switch self {
case .deferred: return "deferred"
case .failed: return "failed"
case .purchased:
return "purchased"
case .purchasing: return "purchasing"
case .restored: return "restored"
}
}
}
The problem is I need to know which NonConsumable Product was purchased? I need to know what should I check is equal to IAPProducts.nonConsumable1.rawValue and IAPProducts.nonConsumable2.rawValue
In your paymentQueue(_:updatedTransactions:) method you need to check the state of each transaction. If the state is .purchased, then look at the transaction's payment. The payment's productIdentifier tells you which in-app purchase was purchased.
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
if transaction.state == .purchased {
let productId = transaction.payment.productIdentifier
// Your code to handle the completed purchase of this IAP
}
queue.finishTransaction(transaction)
}
}
I'm trying to implement that new paymentQueue(_:shouldAddStorePayment:for:) method so my app can handle IAPs directly from the App Store.
I'm using the itms-services:// url to test it, like it says here.
The thing is, my SKPaymentTransactionObserver is a specific view controller, and if it's not visible when I open the itms-services:// link the delegate method won't be called.
What can I do about that? I think I'd have to detect if the user is coming from the App Store to push the right view controller, but I don't know how. The only other option I can think of right now is to make the App Delegate an SKPaymentTransactionObserver, but that seems really cumbersome and I couldn't get it to work when I tried it. Is there any other way?
Here's a class I did that can help you achieve what you want, simply copy the code below and paste it inside a new file and then you can simply access the class StoreManager.shared to whatever method/variable you want to access.
1- To init this class, just call from your didFinishLaunchingWithOptions
StoreManager.shared.Begin() and then payment observer is added.
import Foundation
import StoreKit
class StoreManager: NSObject{
/**
Initialize StoreManager and load subscriptions SKProducts from Store
*/
static let shared = StoreManager()
func Begin() {
print("StoreManager initialized"))
}
override init() {
super.init()
// Add pyament observer to payment qu
SKPaymentQueue.default().add(self)
}
func requestProductWithID(identifers:Set<String>){
if SKPaymentQueue.canMakePayments() {
let request = SKProductsRequest(productIdentifiers:
identifers)
request.delegate = self
request.start()
} else {
print("ERROR: Store Not Available")
}
}
func buyProduct(product: SKProduct) {
print("Buying \(product.productIdentifier)...")
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
func restorePurchases() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
// MARK:
// MARK: SKProductsRequestDelegate
//The delegate receives the product information that the request was interested in.
extension StoreManager:SKProductsRequestDelegate{
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
var products = response.products as [SKProduct]
var buys = [SKProduct]()
if (products.count > 0) {
for i in 0 ..< products.count {
let product = products[i]
print("Product Found: ",product.localizedTitle)
}
} else {
print("No products found")
}
let productsInvalidIds = response.invalidProductIdentifiers
for product in productsInvalidIds {
print("Product not found: \(product)")
}
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("Something went wrong: \(error.localizedDescription)")
}
}
// MARK:
// MARK: SKTransactions
extension StoreManager: SKPaymentTransactionObserver {
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch (transaction.transactionState) {
case .purchased:
completeTransaction(transaction: transaction)
break
case .failed:
failedTransaction(transaction: transaction)
break
case .restored:
restoreTransaction(transaction: transaction)
break
case .deferred:
// TODO show user that is waiting for approval
break
case .purchasing:
break
}
}
}
private func completeTransaction(transaction: SKPaymentTransaction) {
print("completeTransaction...")
deliverPurchaseForIdentifier(identifier: transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func restoreTransaction(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restoreTransaction... \(productIdentifier)")
deliverPurchaseForIdentifier(identifier: productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func failedTransaction(transaction: SKPaymentTransaction) {
if let error = transaction.error as NSError? {
if error.domain == SKErrorDomain {
// handle all possible errors
switch (error.code) {
case SKError.unknown.rawValue:
print("Unknown error")
case SKError.clientInvalid.rawValue:
print("client is not allowed to issue the request")
case SKError.paymentCancelled.rawValue:
print("user cancelled the request")
case SKError.paymentInvalid.rawValue:
print("purchase identifier was invalid")
case SKError.paymentNotAllowed.rawValue:
print("this device is not allowed to make the payment")
default:
break;
}
}
}
SKPaymentQueue.default().finishTransaction(transaction)
}
private func deliverPurchaseForIdentifier(identifier: String?) {
guard let identifier = identifier else { return }
}
}
//In-App Purchases App Store
extension StoreManager{
func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
return true
//To hold
//return false
//And then to continue
//SKPaymentQueue.default().add(savedPayment)
}
}