3

Hello I am having getting my App published on the App Store as they keep insisting my in-app purchases are not set up correctly.

Upon testing them myself in the sandbox, everything worked correctly.

This is the message they sent me, I pasted my code below.

Thank you for taking the time to help me out!

Guideline 2.1 - Performance - App Completeness

We found that your in-app purchase products exhibited one or more bugs when reviewed on iPhone and iPad running iOS 12 on Wi-Fi.

Specifically, your in app purchase buttons do not work.

Next Steps

When validating receipts on your server, your server needs to be able to handle a production-signed app getting its receipts from Apple’s test environment. The recommended approach is for your production server to always validate receipts against the production App Store first. If validation fails with the error code “Sandbox receipt used in production,” you should validate against the test environment instead.

class IAPService: NSObject {
    private override init() {}
    static let shared = IAPService()

    var products = [SKProduct]()
    let paymentQueue = SKPaymentQueue.default()

    func getProducts() {
        let products: Set = [IAPProduct.consumable.rawValue,
                             IAPProduct.nonConsumable.rawValue]
        let request = SKProductsRequest(productIdentifiers: products)
        request.delegate = self
        request.start()
        paymentQueue.add(self)
    }

    func purchase(product: IAPProduct) {


        for p in products {
            if p.productIdentifier == product.rawValue {
                let payment = SKPayment(product: p)
                paymentQueue.add(payment)
                print("Adding product to payment queue")
            }
        }
      }

    func restorePurchase() {
        print("Restoring purchases")
        paymentQueue.restoreCompletedTransactions()
    }


    func givePurchasedProduct(productID: String) {

        if productID.range(of: "Zap") != nil {

            NotificationCenter.default.post(name: Notification.Name.init("zapPurchased"), object: nil)

        } else if productID.range(of: "Ads") != nil {

            NotificationCenter.default.post(name: Notification.Name.init("noAdsPurchased"), object: nil)

        }
    }
 }
extension IAPService: SKProductsRequestDelegate {

    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        self.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, .deferred: break // do nothing
                case .purchased:
                    queue.finishTransaction(transaction)
                    givePurchasedProduct(productID: transaction.payment.productIdentifier)
                case .restored:
                    self.restorePurchase()
                    queue.finishTransaction(transaction)

               case .failed:
                    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"
            }
        }
    }
Pat Trudel
  • 317
  • 3
  • 13
  • "your in app purchase buttons do not work" So where is the code that responds to the buttons? They've told you that that's where the trouble manifests itself. — Also, how can a singleton function as the SKPaymentTransactionObserver? You need to show more of your architecture to prove that that's even possible. – matt Oct 06 '18 at 18:27
  • Your logic is incorrect in your transaction observer as well; you are calling `finishTransaction` before you have successfully persisted the purchase. You must not finish the transaction until it is confirmed that the purchase has been recognised by your app. Since you are simply firing a notification, you cannot be sure that this has happened. Also, your restore completed transactions will create an infinite loop. In your transaction observer, the `restored` state should be handled the same as the `.purchased` state. – Paulw11 Oct 06 '18 at 21:05
  • @matt why can't the transaction observer be a singleton (aside from the general reasons not to use singletons)?. The transaction observer effectively needs the same lifetime as the app itself. If I don't use a singleton, I make the object instance a property of the app delegate, which is similar in effect – Paulw11 Oct 06 '18 at 21:11
  • @Paulw11 I have little doubt that _you_ are creating this singleton early and assigning it into a persistent variable / property and setting it as the observer. I have no reason to think that the OP is doing the same. I'm asking the OP to show me proof, that's all. The code isn't working, so why should I take anything for granted? – matt Oct 06 '18 at 21:19
  • Ok, I agree. We need to know how and when their object is instantiated. In my reading of your comment I thought you were raising an issue with Singleton observers in general rather than the specifics around the instantiation of this code. – Paulw11 Oct 06 '18 at 21:25
  • i got the same issue, any update? @Paulw11 – asilturk Jul 12 '20 at 19:03

2 Answers2

4

App review is very strict when it comes to Apple. Speaking from experience, I have had this problem many times. Your code seems fine to me till it goes to the givePurchasedProduct function.

Okay so things i noticed:

  1. Your app processes the payment and we get return "purchased" if nothing goes wrong
  2. If the case was case .purchased: then we invoke the givePurchasedProduct

On your function. you separate the purchase to see if it's either a Zap purchase or it was to remove the ads

However. this line is confusing me-- Why would you use range when contains where introduced recently.

if productID.contains("Zap") {
     // No Zapp? it has to be Ads then
     NotificationCenter.default.post(name: Notification.Name.init("zapPurchased"), object: nil)
} else {
     NotificationCenter.default.post(name: Notification.Name.init("noAdsPurchased"), object: nil)   
}

Side notes. You might have forgot:

  1. To import Foundation
  2. I don't know what goes behind the notification observers since the code is not included. But. It's not delivering

There's more to it. Receipt Validating is a headache, but when it's needed. It's relaxation and more security to your app.

If you're validating the receipt. these question and it's answers helped me a lot. please see:

Bonus. With SwiftyStoreKit. Receipt validating is just like tapping a button:

Use this method to (optionally) refresh the receipt and perform validation in one step.

let appleValidator = AppleReceiptValidator(service: .production, sharedSecret: "your-shared-secret")
SwiftyStoreKit.verifyReceipt(using: appleValidator, forceRefresh: false) { result in
    switch result {
    case .success(let receipt):
        print("Verify receipt success: \(receipt)")
    case .error(let error):
        print("Verify receipt failed: \(error)")
    }
}

Now on the other hand. to the reviewers the purchased content is not delivering. So they think it's purchase validating.

How do you validate the purchase? deliver the content? please update your question. I'm sure i'll be helpful

Good Luck

excitedmicrobe
  • 2,338
  • 1
  • 14
  • 30
  • Hello, Thank you so much for taking the time to check out my issue! I will definitely try this out and keep you posted. I apologize if my code seems a little peculiar at times, I am self taught so I followed a tutorial. – Pat Trudel Oct 07 '18 at 15:31
  • @PatTrudel. I can wait – excitedmicrobe Oct 07 '18 at 16:03
  • Hello I actually do not validate my receipts as the only IAP's that I have are consumable and non consumable. – Pat Trudel Oct 08 '18 at 23:18
  • Apple rejected my app again since fixing my code as per your suggestions, they still insist that my IAP's are not working. – Pat Trudel Oct 08 '18 at 23:19
  • I have tested all of my in app purchases on my own devices and on friends devices using TestFlight, but once at Apple, it does not seem to work on their end, what can I do? I am really getting desperate here..... – Pat Trudel Oct 08 '18 at 23:20
  • Explain that it works on your end, Cause this could be a problem – excitedmicrobe Oct 09 '18 at 05:44
0

I think there is no problem with your iOS code. From the Apple's response, they say that, your server is pointing to production environment of Apple InApp purchase and validating the receipts received from test environment of Apple InApp purchase used within App.

Apple has 2 environments for InApp purchases - Test & Production. Both the environments behave same. When you run the app on your iPhone to test by your QA or while you are debugging, it connects to Test environment. You are not charged in real when using Test environment. But the receipts generated are almost same as that of real production environment

Now when you submit the app to store, it will automatically make purchases from Production environment. Users are charged and real receipts are generated.

Your app is sending those receipts to server I think and your server is using the wrong InApp server environment to verify the receipts. On your server make sure that the Apple InApp purchase environment URL is appropriately picked based on your InApp purchase receipt. If you or your team is testing the app, your server has to use Test URL and when the app is submitted to production, your server has to use Production URL of InApp Purchase.

Satyam
  • 15,493
  • 31
  • 131
  • 244