0

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)
    }
}

leonboe1
  • 1,004
  • 9
  • 27

1 Answers1

0

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.

Samin
  • 275
  • 5
  • 13
  • Thanks. Is my code correct? Or do I need to change something for the release? – leonboe1 Nov 01 '20 at 16:48
  • If payment was succeeded using sandbox account then your code should be fine. Also did you fill complete test information in testflight? I suggest you do that and wait for few hours before testing again. – Samin Nov 01 '20 at 16:57
  • About "interrupt purchase" issue, I'm not sure as I've not faced it myself. But once you validate purchase from you own server, maybe that will be taken care of as there'll be extra check before closing the purchase. – Samin Nov 01 '20 at 16:58
  • Yes I filled out all test information in TestFlight. Still can't login with my normal iCloud Account. When I test the app, the login page will disappear after I typed in my password and id and the purchase will fail. – leonboe1 Nov 02 '20 at 18:51