Why is my App Rejected for In-App Purchases? - ios

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

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:
Your app processes the payment and we get return "purchased" if nothing goes wrong
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:
To import Foundation
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:
Implementing Receipt Validation in Swift 3
Lightweight In App Purchases Swift framework for iOS 8.0+, tvOS 9.0+ and macOS 10.10+
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

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.

Related

How can I get IAP receipt If user uninstall the app and install it again? SWIFT

If I purchase something and check the purchase receipt I can get the encoded value of it by this:
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
do {
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
print(receiptData)
let receiptString = receiptData.base64EncodedString(options: [])
print("receiptString \(receiptString)")
// Read receiptData
}
catch { print("Couldn't read receipt data with error: " + error.localizedDescription) }
}
But If I purchase something, then delete the app and reinstall it again and check the receipt, I can't get anything with the upper code. It just get the appStoreReceiptURL and go straight away without entering do {} catch. How can I get the receipt in this scenario?
If you attempt to restore purchase , the user will not be refunded the purchased amount, however you would be able to verify that user purchased the product, and will also receive receipt that contants all the information about his previous purchase, therefore you can safely verify the purchase in your App.
In App Store Review Guides is clearly stated that
Any credits or in-game currencies purchased via in-app purchase may
not expire, and you should make sure you have a restore mechanism for
any restorable in-app purchases.
That means you should provide some mechanism (e.g. Restore Purchase Button) anyway, regardless of scenario when the user uninstalls Application. In your case, you can provide the Restore button directly on your Login screen. Check some inspiration on Restore purchase button in Apple Human Interface guidelines.
I you want to restore purchase directly with Apple API after user taps the Restore Purchase button, you can use:
SKPaymentQueue.default().restoreCompletedTransactions()
,which triggers Apple servers, checks the users Apple ID and gets all of the app purchases they have made before and returns you transaction that you observe with SKPaymentTransactionObserver with this code:
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
if transaction.transactionState == .restored {
//here is your restored transaction
}
}
}
If you however want to get hold of the new receipt with all receipt data, including previous purchases, I recommend not using directly Apple API, but using SwiftyStoreKit library : link on github . Code example below:
func restoreYourPurchases( completion: #escaping (Result<Bool,Error>) -> Void) {
SwiftyStoreKit.restorePurchases(atomically: true) { results in
var restoreSuccessfull = false
if results.restoreFailedPurchases.count > 0 {
print("Restore Failed: \(results.restoreFailedPurchases)")
}
else if results.restoredPurchases.count > 0 {
print("Restore Success: \(results.restoredPurchases)")
for purchase in results.restoredPurchases {
if purchase.productId == "yourID" {
let timeOfPurchase = purchase.originalPurchaseDate
}
}
}
else {
print("Nothing to Restore")
completion(.success(false))
}
}
}

Switching sandbox user to test Auto Renewable Subscription iOS

I have added auto renewable subscription to my iOS app. I have used a sandbox user to test the app and it worked fine. After that I logged out of the previous sandbox account and logged in with another sandbox account. Now my app sends receipts with two original transaction ids to validate from the server. It seems like my previous sandbox user data has not completely wiped off. Does anyone else experiencing the same issue? Any thoughts on this?
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction: AnyObject in transactions {
if let trans = transaction as? SKPaymentTransaction {
switch trans.transactionState {
case .purchased:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
if let completion = self.purchaseProductcompletion {
completion(PurchaseHandlerStatus.purchased, self.productToPurchase, trans)
}
case .failed:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
let errorCode = (trans.error as? SKError)?.code
if (errorCode == .paymentCancelled) {
if let completion = self.purchaseProductcompletion {
completion(PurchaseHandlerStatus.purchaseCancelled, self.productToPurchase, trans)
}
} else {
if let completion = self.purchaseProductcompletion {
completion(PurchaseHandlerStatus.purchaseFailed, self.productToPurchase, trans)
}
}
case .restored:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
totalRestoredPurchases += 1
default:
break
}
}
}
}
To avoid this annoying issue, you should finish all pending transactions from the old sandbox account.
When you debug in-app purchases and/or change Apple ID too often, some transactions may stay in the queue (for example, if you broke execution until transaction is finished). And these transactions will try to finish at every next app launch. In this case you may see a system alert prompting to enter your Apple ID password and this may also lead to sandbox receipt will not immediately update/ may not match logged in sandbox account.
To fix this issue, make sure you finish all pending transactions when launching the app.
SKPaymentQueue.default().finishTransaction(transaction)

Configured all set for In-App Purchase, but still my product count is 0 - Swift 4

I have never integrate IAP before, I am trying to integrate IAP in my iOS App first time, I have search lots of articles on internet, I followed all of them to resolve my issue, but I don't have any idea how to solve it.
I have full filled Agreement Tax and Banking, now Its status is Active.
I have added new user in sandbox tester.
I have choose Non-Renewing Subscription in features menu, now It's status is Ready To Submit,
I have again and again removed application in device (iPhone XR and iPad 5).
I have been waited for a long time, approx 5 to 7 days for activation.
I have already enabled In-App Purchase flag from capabilities.
My Development Profile is also consider with In-App Purchase.
I have sign out (iTunes account) my all device (iPhone XR and iPad 5).
My Product ID is relevant with my Bundle ID.
My Code is below...
func IAPintegrate(){
if (SKPaymentQueue.canMakePayments()) {
let productId : NSSet = NSSet(objects: IAP_DEV_PRODUCT_ID)
productsRequest = SKProductsRequest(productIdentifiers: productId as! Set<String>)
productsRequest.delegate = self
productsRequest.start()
}
}
I have also added Delegate methods,
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print(response.products)
let count : Int = response.products.count
if (count>0) {
let validProduct: SKProduct = response.products[0] as SKProduct
if (validProduct.productIdentifier == IAP_DEV_PRODUCT_ID) {
print(validProduct.localizedTitle)
print(validProduct.localizedDescription)
print(validProduct.price)
self.buyProduct(product: validProduct)
} else {
print(validProduct.productIdentifier)
}
} else {
print("nothing")
}
}
still my product count is always 0 please help me what should i do ?
Check whether you are getting any product identifier strings that is invalid in didReceive response method.
print("Invalid product identifiers \(response.invalidProductIdentifiers)")
The source of the problem is shown in your third screen shot. Your in-app purchases are Ready To Submit. That means they have not yet been submitted and approved. Therefore, as far as the store is concerned, they don't exist.

iOS- In app purchase working proper in sandbox environment, Appstore version content not unlocked

In-app purchase working in the sandbox environment but in Appstore version amount debited from user account but the content is not unlocked.
I don't think there is an issue in coding. otherwise, it will not work in the sandbox environment. I think it may possible that transaction receipt is nil.
When I click to purchase again that it shows "you have already subscribed". But still, it's not unlocking app content. Even I clicked to restore the purchase but its also not working. I surprised why everything is working in the sandbox environment.
Subscription type: auto-renewable.
Content unlocking: Audio, video and pdf tutorials .
I have checked backend log. API never executed and the only issue I fill is that I did not get app store receipt even if the user purchased successfully. everything working perfectly in sandbox environment.
Code:
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing:
handlePurchasingState(for: transaction, in: queue)
case .purchased:
handlePurchasedState(for: transaction, in: queue)
case .restored:
handleRestoredState(for: transaction, queue: queue)
case .failed:
handleFailedState(for: transaction, in: queue)
case .deferred:
handleDeferredState(for: transaction, in: queue)
}
}
}
//On transaction state changed to purchased:
func handlePurchasedState(for transaction: SKPaymentTransaction, in queue: SKPaymentQueue) {
print("User purchased product id: \(transaction.payment.productIdentifier)")
print("User purchased product id: \(transaction)")
queue.finishTransaction(transaction)
self.completeTransaction(transaction:transaction)
}
//In completeTransaction Method:
func completeTransaction(transaction:SKPaymentTransaction)
{
if let receiptURL = Bundle.main.appStoreReceiptURL,FileManager.default.fileExists(atPath: receiptURL.path)
{
let receipt:Data = try! Data(contentsOf: receiptURL)
let jsonObjectString = receipt.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0))
var strIdentifier:String = transaction.transactionIdentifier!
if let identifier = transaction.original?.transactionIdentifier
{
strIdentifier = identifier
}
//API call to save receipt to validate later and unlock the content
//In case API calling failed then I stored data and called API again on app home page.
}
}
Please finish the transaction after you verify the receipt queue.finishTransaction(transaction)
apple also recommends that "Download all Apple-hosted content before finishing the transaction. After a transaction is complete, its download objects can no longer be used."
https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/DeliverProduct.html

In-App Purchase Works on iPhone but not iPad

I'm having an issue with In-App purchases on iOS. I have 5 In-App Purchases in a game, all of which work exactly as expected when tested on an iPhone 6 (iOS 8.3). When I go to test on iPad Air 2 (iOS 8.2), all IAPs fail immediately. Has anyone else experienced this problem? Is there some code that is specific to iPad that I have to add?
EDIT: Strangely, updating the iPad to iOS 8.3 fixed the problem. Any ideas as to why this issue is occurring? Should I change my app to only support iOS 8.3 and above?
To test the app, I'm using TestFlight, the same network connection, and the same Apple ID.
The code I'm using for In-App Purchases is Below:
func inApp() {
if (SKPaymentQueue.canMakePayments())
{
var productID:NSSet = NSSet(object: product_id);
var productsRequest:SKProductsRequest = SKProductsRequest(productIdentifiers: productID as Set<NSObject>);
productsRequest.delegate = self;
productsRequest.start();
}else{
displayAlert()
}
}
func buyProduct(product: SKProduct){
var payment = SKPayment(product: product)
SKPaymentQueue.defaultQueue().addPayment(payment);
}
func productsRequest (request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
var count : Int = response.products.count
if (count>0) {
var validProducts = response.products
var validProduct: SKProduct = response.products[0] as! SKProduct
if (validProduct.productIdentifier == product_id) {
println(validProduct.localizedTitle)
println(validProduct.localizedDescription)
println(validProduct.price)
buyProduct(validProduct);
} else {
println(validProduct.productIdentifier)
}
} else {
displayAlert()
}
}
func request(request: SKRequest!, didFailWithError error: NSError!) {
self.displayAlert()
}
func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {
for transaction:AnyObject in transactions {
if let trans:SKPaymentTransaction = transaction as? SKPaymentTransaction{
switch trans.transactionState {
case .Purchased:
SKPaymentQueue.defaultQueue().finishTransaction(transaction as! SKPaymentTransaction)
if product_id == "com.shv.FrenzyTenLives" {
defaults.setInteger(10, forKey: "totalLives")
} else if product_id == "com.shv.FrenzyFiveLives" {
defaults.setInteger(5, forKey: "totalLives")
} else if product_id == "com.shv.FrenzyInfiniteLives" {
defaults.setBool(true, forKey: "infiniteLives")
} else if product_id == "com.shv.FrenzyShield" {
defaults.setInteger(5, forKey: "shieldValue")
} else if product_id == "com.shv.FrenzyRemoveAds" {
defaults.setBool(true, forKey: "adsRemoved")
adBanner.hidden = true
}
break;
case .Failed:
SKPaymentQueue.defaultQueue().finishTransaction(transaction as! SKPaymentTransaction)
self.displayAlert()
break;
case .Restored:
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
if product_id == "com.shv.FrenzyTenLives" {
defaults.setInteger(10, forKey: "totalLives")
} else if product_id == "com.shv.FrenzyFiveLives" {
defaults.setInteger(5, forKey: "totalLives")
} else if product_id == "com.shv.FrenzyInfiniteLives" {
defaults.setBool(true, forKey: "infiniteLives")
} else if product_id == "com.shv.FrenzyRemoveAds" {
defaults.setBool(true, forKey: "adsRemoved")
adBanner.hidden = true
}
break;
default:
break;
}
}
}
}
Usually, when one device will not perform in app purchase, it's a setting under restrictions.
If you've checked and disabled that restriction or all restrictions - you can often fix this by signing out of the App Store entirely and powering down the device. When it starts cleanly, you can log in again. When you sign in, be sure to buy something (a free song of the week, a free app or even a paid app) to get through the verification questions. Once that's done, recheck an IAP (in-app purchase) to be sure things are functional.
As a last resort, you might need to contact Apple Support for Apple ID - but most times you can fix this without needing their help.
You should check the following things.
Make sure you can answer “Yes” to each of these questions:
Have you enabled In-App Purchases for your App ID?
Have you checked Cleared for Sale for your product?
Does your project’s .plist Bundle ID match your App ID?
Have you generated and installed a new provisioning profile for the new App ID?
Have you configured your project to code sign using this new provisioning profile?
Are you using the full product ID when when making an SKProductRequest?
Have you waited several hours since adding your product to iTunes Connect?
Have you tried deleting the app from your device and reinstalling?
Is your device jailbroken? If so, you need to revert the jailbreak for IAP to work.
If you answered “No” to any one of these questions, there’s your problem.
You should visit following links definitely you will get solution.
https://www.innofied.com/in-app-purchase-working-ios-solution/
Without a definition of "fail immediately" my best guess is that you were not logged in to a valid sandbox testing account on the iPad and in the midst of updating the iOS version this got reconciled, which is why it works on 8.3.
The easiest way to get IAP testing to work is to log out of iTunes:
Settings -> App and iTunes Store -> Tap apple ID and log out.
Once you've logged out, try to make an IAP and you will be prompted to log in - once you enter valid test account credentials, the IAP will download - if it doesn't, post your log files here. Note that you can find or create testing accounts inside iTunes Connect.
IAP is an extremely complicated subject with numerous points of failure - knowing that your IAPs work on one device but not another points to a configuration issue specific to the device on which the IAP failed.

Resources