IAP autoRenewable subscriptions, SKPaymentQueue update transaction function is not responding - ios

I have implemented in app purchases for auto renewable subscriptions in my app, but I am trying to implement something once a purchased has been completed.
Below is my SubscriptionService for the IAPs
class SubscriptionService: NSObject {
static let sessionIdSetNotification = Notification.Name("SubscriptionServiceSessionIdSetNotification")
static let optionsLoadedNotification = Notification.Name("SubscriptionServiceOptionsLoadedNotification")
static let restoreSuccessfulNotification = Notification.Name("SubscriptionServiceRestoreSuccessfulNotification")
static let purchaseSuccessfulNotification = Notification.Name("SubscriptionServiceRestoreSuccessfulNotification")
static let shared = SubscriptionService()
private var purchasedProductIdentifiers: Set<ProductIdentifier> = []
var productDidPurchased: (() -> Void)?
var hasReceiptData: Bool {
return loadReceipt() != nil
}
var options: [Subscription]? {
didSet {
NotificationCenter.default.post(name: SubscriptionService.optionsLoadedNotification, object: options)
}
}
func loadSubscriptionOptions() {
let products: Set = ["productIDS"]
let request = SKProductsRequest(productIdentifiers: products)
request.delegate = self
request.start()
}
func uploadReceipt(completion: ((_ success: Bool) -> Void)? = nil) {
if let receiptData = loadReceipt() {
}
}
private func loadReceipt() -> Data? {
guard let url = Bundle.main.appStoreReceiptURL else {
return nil
}
do {
let data = try Data(contentsOf: url)
return data
} catch {
print("Error loading receipt data: \(error.localizedDescription)")
return nil
}
}
func purchase(subscription: Subscription) {
let payment = SKPayment(product: subscription.product)
print("Product being bought: \(payment)")
SKPaymentQueue.default().add(payment)
}
func restorePurchases() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
extension SubscriptionService: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
options = response.products.map { Subscription(product: $0) }
print("in here 1")
print(options!)
// let purchased = UserDefaults.standard.bool(forKey: "com.mylawnow.sub.allaccess")
//print("Purchased\(purchased)")
print("in here 2")
}
func request(_ request: SKRequest, didFailWithError error: Error) {
if request is SKProductsRequest {
print("Subscription Options Failed Loading: \(error.localizedDescription)")
}
}
}
//-------------------------------------------------------------
This is the part that does not seem to be working. None of the functions seem to be firing (I have implemented print statements to see if they are ever hit, but they don't seem to be.)
extension SubscriptionService: SKPaymentTransactionObserver{
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
print("updating")
for transaction in transactions {
switch (transaction.transactionState) {
case .purchased:
print("purchased made")
complete(transaction: transaction)
break
case .failed:
print("purchased failed")
fail(transaction: transaction)
break
case .restored:
print("restored")
restore(transaction: transaction)
break
case .deferred:
print("purchase deferred")
break
case .purchasing:
print("purchase being made")
break
}
}
}
I am assuming that the complete function should be used to do certain functionality once the purchased is made.
private func complete(transaction: SKPaymentTransaction) {
print("completed...")
deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func restore(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
deliverPurchaseNotificationFor(identifier: productIdentifier)
print("identifier: \(productIdentifier)")
SKPaymentQueue.default().finishTransaction(transaction)
}
private func fail(transaction: SKPaymentTransaction) {
print("failed...")
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)
}
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)
}
}

So I found the error. In my loadSubscriptions function, I needed to app an observer to the SKPaymentQueue. So it should be like below:
func loadSubscriptionOptions() {
let products: Set = ["productIDS"]
let request = SKProductsRequest(productIdentifiers: products)
request.delegate = self
request.start()
SKPaymentQueue.default().add(self) //--> this line is needed
}

Related

An unknown error occurred while purchasing in app product in ios

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?

iOS In-App-Purchases don't work for non-sandbox accounts in TestFlight

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.

StoreKit not responding

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???

Best practice to add a transaction queue observer at application launch for Storekit

I am having trouble trying to follow Apple's recommendation of adding a transaction queue observer in didFinishLaunchingWithOptions.
Specifically I am trying to adapt code from a Ray Wenderlich tutorial which does not do this - it adds the observer only once the 'buy' button is tapped.
My app crashes when the buyProduct function is called:
public func buyProduct(_ product: SKProduct) {
print("Buying \(product.productIdentifier)...")
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
In my log I can see that the init of IAPHelper is called twice, thus calling SKPaymentQueue.default().add(self) twice. I am sure that's the problem but I'm confused how to fix it.
Here is my code...
AppDelegate:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
IAPHelper.sharedInstance = IAPHelper() // creates the singleton for IAPHelper
// other code here //
return true
}
IAPHelper.swift:
import StoreKit
/// Notification that is generated when a product is purchased.
public let IAPHelperPurchaseNotification = "IAPHelperPurchaseNotification"
/// Notification that is generated when a transaction fails.
public let IAPHelperTransactionFailedNotification = "IAPHelperTransactionFailedNotification"
/// Notification that is generated when cannot retrieve IAPs from iTunes.
public let IAPHelperConnectionErrorNotification = "IAPHelperConnectionErrorNotification"
/// Notification that is generated when we need to stop the spinner.
public let IAPHelperStopSpinnerNotification = "IAPHelperStopSpinnerNotification"
/// Product identifiers are unique strings registered on the app store.
public typealias ProductIdentifier = String
/// Completion handler called when products are fetched.
public typealias ProductsRequestCompletionHandler = (_ success: Bool, _ products: [SKProduct]?) -> ()
open class IAPHelper : NSObject {
/// MARK: - User facing API
fileprivate let productIdentifiers: Set<ProductIdentifier>
fileprivate var purchasedProductIdentifiers = Set<ProductIdentifier>()
fileprivate var productsRequest: SKProductsRequest?
fileprivate var productsRequestCompletionHandler: ProductsRequestCompletionHandler?
static var sharedInstance = IAPHelper() // singleton
override init() {
// Set up the list of productIdentifiers
let PackOf4000Coins = "com.xxx.xxx.4000Coins"
let PackOf10000Coins = "com.xxx.xxx.10000Coins"
let PackOf30000Coins = "com.xxx.xxx.30000Coins"
let PackOf75000Coins = "com.xxx.xxx.75000Coins"
let PackOf175000Coins = "com.xxx.xxx.175000Coins"
let PackOf750000Coins = "com.xxx.xxx.750000Coins"
let RemoveAds = "com.xxx.xxx.RemoveAds"
let PlayerEditor = "com.xxx.xxx.PlayerEditor"
self.productIdentifiers = [PackOf4000Coins, PackOf10000Coins, PackOf30000Coins, PackOf75000Coins, PackOf175000Coins, PackOf750000Coins, RemoveAds, PlayerEditor]
for productIdentifier in self.productIdentifiers {
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)
}
}
// 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()
}
public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
print("Restore queue finished.")
NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperStopSpinnerNotification), object: nil)
}
public func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
print("Restore queue failed.")
NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperConnectionErrorNotification), object: nil)
}
}
// 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)
NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperConnectionErrorNotification), object: nil)
clearRequestAndHandler()
}
fileprivate 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:
completeTransaction(transaction)
break
case .failed:
failedTransaction(transaction)
break
case .restored:
restoreTransaction(transaction)
break
case .deferred:
break
case .purchasing:
break
}
}
}
fileprivate func completeTransaction(_ transaction: SKPaymentTransaction) {
print("completeTransaction...")
deliverPurchaseNotificationForIdentifier(
transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
fileprivate func restoreTransaction(_ transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restoreTransaction... \(productIdentifier)")
deliverPurchaseNotificationForIdentifier(productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
fileprivate func failedTransaction(_ transaction: SKPaymentTransaction) {
print("failedTransaction...")
NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperStopSpinnerNotification), object: nil)
if transaction.error!._code != SKError.paymentCancelled.rawValue {
print("Transaction Error: \(String(describing: transaction.error?.localizedDescription))")
NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperTransactionFailedNotification), object: nil)
} else {
print("Transaction Error else statement")
}
SKPaymentQueue.default().finishTransaction(transaction)
NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperTransactionFailedNotification), object: nil)
}
fileprivate func deliverPurchaseNotificationForIdentifier(_ identifier: String?) {
guard let identifier = identifier else { return }
purchasedProductIdentifiers.insert(identifier)
UserDefaults.standard.set(true, forKey: identifier)
UserDefaults.standard.synchronize()
NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperPurchaseNotification), object: identifier)
}
}
GameStoreViewController.swift (relevant code only):
#objc func tableView(_ tableView: UITableView!, didSelectRowAtIndexPath indexPath: IndexPath!) {
if IAPHelper.canMakePayments() {
activitySpinnerStart()
let product = _coinProducts[(indexPath as NSIndexPath).row]
IAPHelper.sharedInstance.buyProduct(product) // Purchasing the product. Fires productPurchased(notification:)
} else {
showAlertWith(Localization("NoConnectionAlertTitle"), message: Localization("NoIAPAllowedMessage"))
}
}
In the end I used SwiftyStoreKit which solved the problem for me. I highly recommend it.
Edit to show SwiftyStoreKit.completeTransactions which goes in didFinishLaunchingWithOptions() in appDelegate. unlockIAPContent() is where I put the purchase logic and this will handle promo codes and unfinished transactions:
// This registers the transaction observer and listens for unfinished transactions
SwiftyStoreKit.completeTransactions(atomically: true) { purchases in
for purchase in purchases {
switch purchase.transaction.transactionState {
case .purchased, .restored:
// Unlock content
self.unlockIAPContent(productID: purchase.productId)
if purchase.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(purchase.transaction)
}
case .failed, .purchasing, .deferred:
break // do nothing
}
}
}

In-App Purchase consumable being restored automatically

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.

Resources