Swift In-App purchases sandbox tester error - swift

I am implementing the In-App purchases function today, and I just followed the tutorial step by step, created sandbox testers, wrote the code, and it says
<SKPaymentQueue: 0x282e50860>: Payment completed with error: Error Domain=ASDServerErrorDomain Code=3502 "This item is not available." UserInfo={NSLocalizedDescription=This item is not available.
Why is "This item is not available."? I searched the relevant information online, but there is no answer for it.
Here is my code
#IBAction func purchaseButtonPressed(_ sender: UIButton) {
print("PRESSED")
purchaseApp()
}
func purchaseApp() {
let productID = "com.crazycat.Reborn.FullFuctionalities"
if SKPaymentQueue.canMakePayments() {
let paymentRequest = SKMutablePayment()
paymentRequest.productIdentifier = productID
SKPaymentQueue.default().add(paymentRequest)
} else {
print("Can't make payments")
}
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
if transaction.transactionState == .purchased {
print("Thanks for shopping")
} else if transaction.transactionState == .failed {
print("purchase Failed")
}
}
}

I had the same issue.
Please make sure your paid application agreement in Appstore connect is Active and not Expired. Check if there are any warnings in the App Store connect. Complete all of the bank, tax, and contact information on your App Store Connect Paid Apps Agreements.
Then relaunch the app from Xcode on your physical device.
The transaction should be successful then.

Check below points
Use the same test account you specified in developer console.
Make sure the In-App product shows a status of Ready to Submit on the developer console.
Make sure the In-App product id matches what your using in your app.

Related

Problem with in-app purchases in apple watch Swift

I have been trying in vain for several days to make a test purchase of a subscription from the watch, but nothing works out for me, all the methods that I found have been tried, and these are: apphud, rebooting devices, logging out with Apple ID, another Apple ID, test / not test apple id, sandbox is logged in, the appstore is also logged in, the card is linked, but, unfortunately, every time I try to buy, I get an error message: "Unable to Purchase App. Sign in with your Apple ID from the Apple Watch app on your iPhone".
Moreover, when I do all the same from the phone, everything works fine, and the purchase is made.
There is an option to try revenuecat, but honestly, I don't think it will be different from apphud. There is also an assumption that it is simply IMPOSSIBLE to make a test purchase from the watch.
Here is a piece of my code from in app manager (recipes are coming):
func startProducts() {
let bundle = appBundleID
let products = Set([
bundle + ".1m",
bundle + ".1y"
])
request = SKProductsRequest(productIdentifiers: Set(products))
request?.delegate = self
request?.start()
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
guard firstRequestFinish else {
return
}
firstRequestFinish = false
defer {
finishProducts()
}
if response.invalidProductIdentifiers.count > 0 {
print("Invalid Products IDs: \(response.invalidProductIdentifiers)")
}
self.allProducts = response.products
let subscriptions = (response.products.compactMap { item in
return IAProduct(id: item.productIdentifier,
title: item.localizedTitle,
price: item.price,
localePrice: item.localizedPrice,
locale: item.priceLocale)
}.sorted { item1, item2 in
return item1.price.doubleValue < item2.price.doubleValue
})
self.availableSubscriptions = subscriptions
if subscriptions.count > 0 {
print("Available products IDs: \(subscriptions.map({ item in return item.id }))")
}
self.monthProduct = subscriptions.first { item in
return item.id.contains(".1m")
}
self.yearProduct = subscriptions.first { item in
return item.id.contains(".1y")
}
delegate?.inappWasLoaded()
}
I would be glad for any advice! Many thanks to everyone who will take part in the discussion of this issue.

Cannot receive results from Square Point of Sale in a small Swift app

I have written a small app in Swift using Xcode 12.5 by following the information and code samples provided here ... https://github.com/square/SquarePointOfSaleSDK-iOS
The app polls a server to see if there is a charge to be made. The output from the server is in JSON format. When a charge comes in, the JSON results are providing a customer id, amount to be charged, and a note to the Square Point of Sale SDK.
Using the SCCAPIRequest example from the GitHub page ...
// Replace with your app's URL scheme.
let callbackURL = URL(string: "<#T##Your URL Scheme##String#>://")!
// Your client ID is the same as your Square Application ID.
// Note: You only need to set your client ID once, before creating your first request.
SCCAPIRequest.setApplicationID(<#T##Application ID##String#>)
do {
// Specify the amount of money to charge.
let money = try SCCMoney(amountCents: 100, currencyCode: "USD")
// Create the request.
let apiRequest =
try SCCAPIRequest(
callbackURL: callbackURL,
amount: money,
userInfoString: nil,
locationID: nil,
notes: "Coffee",
customerID: nil,
supportedTenderTypes: .all,
clearsDefaultFees: false,
returnsAutomaticallyAfterPayment: false,
disablesKeyedInCardEntry: false,
skipsReceipt: false
)
// Open Point of Sale to complete the payment.
try SCCAPIConnection.perform(apiRequest)
} catch let error as NSError {
print(error.localizedDescription)
}
The app successfully switches to Square POS, displays the amount due, and knows which customer I am wanting to charge (via customer id). I can process the payment and Square POS switches back to my app just fine.
This is where I am running in to trouble. I am also using the UIApplication delegate method example on that same page. Under the comment "Handle a successful request" ...
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
guard SCCAPIResponse.isSquareResponse(url) else {
return
}
do {
let response = try SCCAPIResponse(responseURL: url)
if let error = response.error {
// Handle a failed request.
print(error.localizedDescription)
} else {
// Handle a successful request.
}
} catch let error as NSError {
// Handle unexpected errors.
print(error.localizedDescription)
}
return true
}
I have added the following ...
print("Transaction successful: \(response)")
From what I understand, the response should include the transaction id, and anything that was passed along in the userInfoString. It appears that this code example isn't even firing when Square POS returns to my app. I cannot see anything in the Xcode console.
I have assigned a callback URL within Xcode using the documentation on the link above, and it's also added in the Square Developer Portal under the Point of Sale API.
What am I missing? Where should the UIApplication delegate method be placed, in AppDelegate.swift or should it reside in ViewController.swift, or somewhere else? Any insight would be greatly appreciated.
#ElTomato provided me with the hint that I needed to solve the problem I was having. I needed to delete SceneDelegate.swift, remove Application Scene Manifest from Info.plist, and remove some code from AppDelegate.swift
I found detailed instructions on THIS site ...
iOS 13: Swift - 'Set application root view controller programmatically' does not work
Thank you kindly for the fantastic help #ElTomato

In-App purchase SKProduct not received from SKProductsRequest

I had 3 In-App purchases (non-renewable subscriptions) in the App store connect account, all Approved and retrieved in the code successfully
Then I added one more free non-renewable subscription In-App purchase, submitted to the app store New In-App purchase is Approved, all agreements are active, tax and banking info is ok, product identifier in the code is the same as in the App Store connect
I request all four products in SKProductsRequest But in the productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) I receive three old In-App purchase SKProducts, but product identifier of the new In-App purchase is received in invalidProductIdentifiers
func fetchProducts() {
let request = SKProductsRequest(productIdentifiers: ["mentalmind.kz.free", "mentalmind.kz.threemonth", "mentalmind.kz.sixmonth", "mentalmind.kz.oneyear"])
request.delegate = self
request.start()
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("received products: \(response.products.map({ $0.productIdentifier }))")
print("invalid product ids: \(response.invalidProductIdentifiers)")
DispatchQueue.main.async {
self.products = response.products
}
}
Console output:
received products: ["mentalmind.kz.oneyear", "mentalmind.kz.sixmonth", "mentalmind.kz.threemonth"]
invalid product ids: ["mentalmind.kz.free"]
Is it Apple's fault, or you have any code recommendations for me?
Unfortunately I do not know what was the issue, Apple support did not help me
But I created new In-App Purchase with cheapest price, but not free and it worked fine in the App Store Connect
May be problem was because the In-App purchase was free

Swift How to handle Auto-renewable Subscription receipt and validation

I am testing the auto renewable In-app purchases in swift, I found out that there are some strange problems with my code.
I am testing these functions in sandbox environment
User can purchase either one month, one year auto renewable subscription or permanent permission
App should check if the subscription is still valid every time when user open app, if not, lock all premium functions
User is able to restore the purchased plan, app should get the previous purchased type ie. one month, one year, or permanent.
After long research on the tutorials, I am still confused about the validation
I see that there are two ways to validate receipt, one is locally the other is on the server.
But I don't have a server, does that mean I can only validate it locally
Every time the auto-renewal subscription expires, the local receipt is not updated, so when I reopen the app I got a subscription expiration alert (The method I defined by my self for validation check ), when I click the restore button, the app restored successfully and receipt was updated
After 6 times manually restored and refresh the receipt (the sandbox user can only renew 6 times), when I click the restore button, the part transaction == .purchased is till called, and my app unlocks premium function, however when I reopen my app, my app alerts that the subscription is expired, which is it should.
My core problem is how can I check the validation of subscriptions with Apple every time when I open the app, I don't have a server, and I don't know why the receipt is not refreshing automatically
Here are some parts of my code, I call checkUserSubsriptionStatus() when I open the app, I am using TPInAppReceipt Library
class InAppPurchaseManager {
static var shared = InAppPurchaseManager()
init() {
}
public func getUserPurchaseType() -> PurchaseType {
if let receipt = try? InAppReceipt.localReceipt() {
var purchaseType: PurchaseType = .none
if let purchase = receipt.lastAutoRenewableSubscriptionPurchase(ofProductIdentifier: PurchaseType.oneMonth.productID) {
purchaseType = .oneMonth
}
if let purchase = receipt.lastAutoRenewableSubscriptionPurchase(ofProductIdentifier: PurchaseType.oneYear.productID) {
purchaseType = .oneYear
}
if receipt.containsPurchase(ofProductIdentifier: PurchaseType.permanent.productID) {
purchaseType = .permanent
}
return purchaseType
} else {
print("Receipt not found")
return .none
}
}
public func restorePurchase(in viewController: SKPaymentTransactionObserver) {
SKPaymentQueue.default().add(viewController)
if SKPaymentQueue.canMakePayments() {
SKPaymentQueue.default().restoreCompletedTransactions()
} else {
self.userIsNotAbleToPurchase()
}
}
public func checkUserSubsriptionStatus() {
DispatchQueue.main.async {
if let receipt = try? InAppReceipt.localReceipt() {
self.checkUserPermanentSubsriptionStatus(with: receipt)
}
}
}
private func checkUserPermanentSubsriptionStatus(with receipt: InAppReceipt) {
if let receipt = try? InAppReceipt.localReceipt() { //Check permsnent subscription
if receipt.containsPurchase(ofProductIdentifier: PurchaseType.permanent.productID) {
print("User has permament permission")
if !AppEngine.shared.currentUser.isVip {
self.updateAfterAppPurchased(withType: .permanent)
}
} else {
self.checkUserAutoRenewableSubsrption(with: receipt)
}
}
}
private func checkUserAutoRenewableSubsrption(with receipt: InAppReceipt) {
if receipt.hasActiveAutoRenewablePurchases {
print("Subsription still valid")
if !AppEngine.shared.currentUser.isVip {
let purchaseType = InAppPurchaseManager.shared.getUserPurchaseType()
updateAfterAppPurchased(withType: purchaseType)
}
} else {
print("Subsription expired")
if AppEngine.shared.currentUser.isVip {
self.subsrptionCheckFailed()
}
}
}
private func updateAfterAppPurchased(withType purchaseType: PurchaseType) {
AppEngine.shared.currentUser.purchasedType = purchaseType
AppEngine.shared.currentUser.energy += 5
AppEngine.shared.userSetting.hasViewedEnergyUpdate = false
AppEngine.shared.saveUser()
AppEngine.shared.notifyAllUIObservers()
}
public func updateAfterEnergyPurchased() {
AppEngine.shared.currentUser.energy += 3
AppEngine.shared.saveUser()
AppEngine.shared.notifyAllUIObservers()
}
public func purchaseApp(with purchaseType: PurchaseType, in viewController: SKPaymentTransactionObserver) {
SKPaymentQueue.default().add(viewController)
if SKPaymentQueue.canMakePayments() {
let paymentRequest = SKMutablePayment()
paymentRequest.productIdentifier = purchaseType.productID
SKPaymentQueue.default().add(paymentRequest)
} else {
self.userIsNotAbleToPurchase()
}
}
public func purchaseEnergy(in viewController: SKPaymentTransactionObserver) {
SKPaymentQueue.default().add(viewController)
let productID = "com.crazycat.Reborn.threePointOfEnergy"
if SKPaymentQueue.canMakePayments() {
let paymentRequest = SKMutablePayment()
paymentRequest.productIdentifier = productID
SKPaymentQueue.default().add(paymentRequest)
} else {
self.userIsNotAbleToPurchase()
}
}
}
If you do not have the possibility to use a server, you need to validate locally. Since you are already included TPInAppReceipt library, this is relatively easy.
To check if the user has an active premium product and what type it has, you can use the following code:
// Get all active purchases which are convertible to `PurchaseType`.
let premiumPurchases = receipt.activeAutoRenewableSubscriptionPurchases.filter({ PurchaseType(rawValue: $0.productIdentifier) != nil })
// It depends on how your premium access works, but if it doesn't matter what kind of premium the user has, it is enough to take one of the available active premium products.
// Note: with the possibility to share subscriptions via family sharing, the receipt can contain multiple active subscriptions.
guard let product = premiumPurchases.first else {
// User has no active premium product => lock all premium features
return
}
// To be safe you can use a "guard" or a "if let", but since we filtered for products conforming to PurchaseType, this shouldn't fail
let purchaseType = PurchaseType(rawValue: product.productIdentifier)!
// => Setup app corresponding to active premium product type
One point I notice in your code, which could lead to problems, is that you constantly add a new SKPaymentTransactionObserver. You should have one class conforming to SKPaymentTransactionObserver and add this only once on app start and not on every public call. Also, you need to remove it when you no longer need it (if you created it only once, you would do it in the deinit of your class, conforming to the observer protocol.
I assume this is the reason for point 2.
Technically, the behavior described in point 3 is correct because the method you are using asks the payment queue to restore all previously completed purchases (see here).
Apple states restoreCompletedTransactions() should only be used for the following scenarios (see here):
If you use Apple-hosted content, restoring completed transactions gives your app the transaction objects it uses to download the content.
If you need to support versions of iOS earlier than iOS 7, where the app receipt isn’t available, restore completed transactions instead.
If your app uses non-renewing subscriptions, your app is responsible for the restoration process.
For your case, it is recommended to use a SKReceiptRefreshRequest, which requests to update the current receipt.
Get the receipt every time when the app launches by calling the method in AppDelegate.
getAppReceipt(forTransaction: nil)
Now, below is the required method:
func getAppReceipt(forTransaction transaction: SKPaymentTransaction?) {
guard let receiptURL = receiptURL else { /* receiptURL is nil, it would be very weird to end up here */ return }
do {
let receipt = try Data(contentsOf: receiptURL)
receiptValidation(receiptData: receipt, transaction: transaction)
} catch {
// there is no app receipt, don't panic, ask apple to refresh it
let appReceiptRefreshRequest = SKReceiptRefreshRequest(receiptProperties: nil)
appReceiptRefreshRequest.delegate = self
appReceiptRefreshRequest.start()
// If all goes well control will land in the requestDidFinish() delegate method.
// If something bad happens control will land in didFailWithError.
}
}
Here is the method receiptValidation:
func receiptValidation(receiptData: Data?, transaction: SKPaymentTransaction?) {
guard let receiptString = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) else { return }
verify_in_app_receipt(with_receipt_string: receiptString, transaction: transaction)
}
Next is the final method that verifies receipt and gets the expiry date of subscription:
func verify_in_app_receipt(with_receipt_string receiptString: String, transaction: SKPaymentTransaction?) {
let params: [String: Any] = ["receipt-data": receiptString,
"password": "USE YOUR PASSWORD GENERATED FROM ITUNES",
"exclude-old-transactions": true]
// Below are the url's used for in app receipt validation
//appIsInDevelopment ? "https://sandbox.itunes.apple.com/verifyReceipt" : "https://buy.itunes.apple.com/verifyReceipt"
super.startService(apiType: .verify_in_app_receipt, parameters: params, files: [], modelType: SubscriptionReceipt.self) { (result) in
switch result {
case .Success(let receipt):
if let receipt = receipt {
print("Receipt is: \(receipt)")
if let _ = receipt.latest_receipt, let receiptArr = receipt.latest_receipt_info {
var expiryDate: Date? = nil
for latestReceipt in receiptArr {
if let dateInMilliseconds = latestReceipt.expires_date_ms, let product_id = latestReceipt.product_id {
let date = Date(timeIntervalSince1970: dateInMilliseconds / 1000)
if date >= Date() {
// Premium is valid
}
}
}
if expiryDate == nil {
// Premium is not purchased or is expired
}
}
}
case .Error(let message):
print("Error in api is: \(message)")
}
}
}

paymentQueue always going for .restored option

I'm facing a very strange behaviour, As the code was not changed, It seems to me like a version specific issue, as it comes from nowhere
I'm testing on sandbox environment
the scenario is when I tries to buy product using
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
the apple default purchase or any other popup didn't show
and the control goes directly into restored transactionState
public func paymentQueue(_: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
DispatchQueue.main.async {
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
self.complete(transaction: transaction)
break
case .failed:
hideLoader()
self.fail(transaction: transaction)
break
case .restored:
hideLoader()
self.restore(transaction: transaction)
break
case .deferred:
hideLoader()
break
case .purchasing:
showLoader()
break
#unknown default:
break
}
}
}
}
I'm not sure why it is going there, as apple need to show any popup or any information related to the process
After this function we are validating reciept and reciept returns
"pending_renewal_info": [
{
"expiration_intent": "1",
"auto_renew_product_id": "BUNDLE_ID",
"original_transaction_id": "transaction_id",
"is_in_billing_retry_period": "0",
"product_id": "PRODUCT ID",
"auto_renew_status": "0"
}
]
I'm not sure why the expiration_intent is coming 1 in this case
I had the same issue. Problem was that iTunes/Store account is separated from sandbox testing account.
Settings > iTunes & App Stores > at the bottom there should be SANDBOX ACCOUNT section. Change that account to some other that doesn't have the purchase on it.