I’m not entirely sure what I’m doing wrong.
This is what their message was:
We found that your in-app purchase products exhibited one or more bugs which create a poor user experience. Specifically, the “Remove Ads” button was irresponsive. Please review the details and resources below and complete the next steps.
and then after:
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.
Here’s my entire in-app purchase class
import StoreKit
import SystemConfiguration
class InAppPurchaseManager: NSObject, SKPaymentTransactionObserver, SKProductsRequestDelegate {
static let shared = InAppPurchaseManager()
var productRequest: SKProductsRequest?
var products: [SKProduct]!
override init() {
super.init()
SKPaymentQueue.default().add(self)
}
func unlockContent() {
adsRemoved = true
TitleScreen.shared.removeAdsButton.isHidden = true
GameViewController.shared.bannerView.isHidden = true
SavedSettings.shared.setAdsSettings()
}
func isInternetAvailable() -> Bool {
var zeroAddress = sockaddr_in()
zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress))
zeroAddress.sin_family = sa_family_t(AF_INET)
let defaultRouteReachability = withUnsafePointer(to: &zeroAddress) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {zeroSockAddress in
SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress)
}
}
var flags = SCNetworkReachabilityFlags()
if !SCNetworkReachabilityGetFlags(defaultRouteReachability!, &flags) {
return false
}
let isReachable = flags.contains(.reachable)
let needsConnection = flags.contains(.connectionRequired)
return (isReachable && !needsConnection)
}
func fetchProducts() {
let productIdentifiers = Set(["remove_ads"])
productRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productRequest?.delegate = self
productRequest?.start()
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
self.products = response.products
if let product = products.first {
print("Fetched product: (product.productIdentifier)")
} else {
print("Failed to fetch product")
}
}
// These two methods below are to request a purchase/restore which are called from TitleScreen buttons "removeAdButton" and "restorePurchases"
func requestPurchase() {
if isInternetAvailable() {
if let product = InAppPurchaseManager.shared.products.first(where: { $0.productIdentifier == "remove_ads" }) {
Task {
do {
try await InAppPurchaseManager.shared.purchase(product)
} catch {
print("Failed to initiate purchase: (error)")
}
}
}
} else {
print("No internet connection available")
GameViewController.shared.showAlert(title: "Failure", message: "No internet connection available")
}
}
func restorePurchases() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
func purchase(_ product: SKProduct) async throws {
print("request called")
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
print("Transaction Successful: (transaction.payment.productIdentifier)")
unlockContent()
verifyReceipt()
SKPaymentQueue.default().finishTransaction(transaction)
case .failed:
print("Transaction Failed: (transaction.error?.localizedDescription ?? "Unknown error")")
SKPaymentQueue.default().finishTransaction(transaction)
case .restored:
print("Transaction Restored: (transaction.payment.productIdentifier)")
unlockContent()
verifyReceipt()
SKPaymentQueue.default().finishTransaction(transaction)
case .deferred:
print("Transaction Deferred: (transaction.payment.productIdentifier)")
default:
break
}
}
}
func refreshReceipt() {
let receiptRefreshRequest = SKReceiptRefreshRequest()
receiptRefreshRequest.start()
}
func verifyReceipt() {
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
do {
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
let receiptString = receiptData.base64EncodedString(options: [])
} catch {
print("Couldn't read receipt data with error: " + error.localizedDescription)
}
}
}
}
I call requestPurchase()
from a SKSpriteNode button from a menu. When I test it from a sandbox environment, it prompts as it should and then properly does what it’s supposed to. I’m guessing review doesn’t get any prompt at all, but I also thought in-app purchases aren’t supposed to work until it’s approved and live? I also don’t have a server to validate receipts, I was under the assumption that StoreKit 2 validates on-device.
If someone could help, I’d really appreciate it, I’m clueless at the moment.