I am new to working in in-app purchases. I have set up my app to allow multiple non-consumable in-apps. If it is a first time purchase it works perfectly. If I try and click the buy button again it shows "This in-app has already been purchased etc. etc." once you click Okay, it does nothing. I have noticed it only shows "Okay" as the option and not "Cancel" and "Okay". In my test app, it shows both and works great.` #IBOutlet weak var buyProductID: UILabel!
let product1 = "TestAd.com"
#IBOutlet weak var adView1: UIView!
func buyProduct1(product1: SKProduct){
print("Sending the Payment Request to Apple 1");
let payment1 = SKPayment(product: product1)
SKPaymentQueue.default().add(payment1);
}
#IBAction func product1Btn(sender: AnyObject) {
buyProductID.text = "Product1"
print("About to fetch the product... 1")
// Can make payments
if (SKPaymentQueue.canMakePayments())
{
let productID1:NSSet = NSSet(object: self.product1);
let productsRequest1:SKProductsRequest = SKProductsRequest(productIdentifiers: productID1 as! Set<String>);
productsRequest1.delegate = self;
productsRequest1.start();
print("Fetching Products 1");
}else{
print("Can't make purchases 1");
}
}
func purchase1ViewDid(){
if (UserDefaults.standard.bool(forKey: "purchased1")){
adView1.isHidden = true
print("No ads for 1")
} else {
print("Yes ads 1")
}
}
func productsRequest (_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
//PRODUCT 1
let count1 : Int = response.products.count
if (count1>0) {
let validProduct1: SKProduct = response.products[0] as SKProduct
if (validProduct1.productIdentifier == self.product1) {
print(validProduct1.localizedTitle)
print(validProduct1.localizedDescription)
print(validProduct1.price)
buyProduct1(product1: validProduct1);
} else {
print(validProduct1.productIdentifier)
}
} else {
print("nothing 1")
}
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("Error Fetching product information 1");
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions1: [SKPaymentTransaction]) {
print("Received Payment Transaction Response from Apple 1");
for transaction1:AnyObject in transactions1 {
if let trans:SKPaymentTransaction = transaction1 as? SKPaymentTransaction{
switch trans.transactionState {
case .purchased:
if buyProductID.text == "Product1" {
print("Product Purchased 1");
SKPaymentQueue.default().finishTransaction(transaction1 as! SKPaymentTransaction)
// Handle the purchase
UserDefaults.standard.set(true , forKey: "purchased1")
adView1.isHidden = true
}
break;
case .failed:
print("Purchased Failed 1");
SKPaymentQueue.default().finishTransaction(transaction1 as! SKPaymentTransaction)
break;
case .restored:
print("Already Purchased 1");
SKPaymentQueue.default().restoreCompletedTransactions()
// Handle the purchase
UserDefaults.standard.set(true , forKey: "purchased1")
adView1.isHidden = true
break;
default:
break;
}
}
}
}
`
Non-consumable purchases can only be purchased once, so this is the expected behavior. The message is displayed because you can only purchase the non-consumable item once, and it remains associated with the account. It thinks that since you have purchased the item already, that you simply want to restore it. As a side note, for testing purposes, IAPs have to be tested using a real device, so the simulator won't test IAPs correctly.
Related
I am trying to implement in-app purchases in an app. this is the firs time i am doing this. I think i have the whole thing set up correctly, when i test on the device it seems to work however i get "Purchased" printed on the console a lot, it purchased 500 times (it seemed to be caught in a loop). I checked this by checking Debug -> Store Kit -> Manage transactions it seems to be purchasing in a loop. I can't see where I've gone wrong with the code.
This started happening after I added in a button that restores the purchase.
enum Product: String, CaseIterable {
case noadspremium = "unlock1"
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
if let oProduct = response.products.first {
print("product is available")
self.purchase(aproduct: oProduct)
}
else {
print("product not available")
}
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing:
print("customer is in the process of purchase")
case .purchased:
SKPaymentQueue.default().finishTransaction(transaction)
print("purchased")
case .failed:
SKPaymentQueue.default().finishTransaction(transaction)
print("failed")
case .restored:
SKPaymentQueue.default().restoreCompletedTransactions()
print("Restored")
case .deferred:
print("deferred")
default:
break
}
}
}
func purchase(aproduct: SKProduct) {
let payment = SKPayment(product: aproduct)
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().add(payment)
}
#IBAction func buyPremium(_ sender: Any) {
if SKPaymentQueue.canMakePayments(){
let set: Set<String> = [Product.noadspremium.rawValue]
let productRequest = SKProductsRequest(productIdentifiers: set)
productRequest.delegate = self
productRequest.start()
}
}
#IBAction func restorePressed(_ sender: Any) {
SKPaymentQueue.default().restoreCompletedTransactions()
}
I need to detect when the bottom In-App Purchase pop-up opens up. In my current development, I show an activity indicator just when a user taps on the "Purchase Button" for a particular product bundle. And the indicator keeps loading until the whole purchase process finishes.
But I want to stop the activity indicator when the bottom purchase pop-up alert will be shown and the indicator will be spinning again when the user fills up all credentials and hit the subscription button on that bottom pop-up. How could I detect that?
In my current implementation when a user tap on the purchase button I start the activity indicator at the beginning like this:
func purchaseProduct(product: SKProduct) -> Bool {
if !IAPManager.shared.canMakePayments() {
return false
} else {
if Reachability.isConnectedToNetwork() {
setActivityIndicator(shouldStart: true)
UserDefaults.standard.set(true, forKey: "receiptValidationAllow")
IAPManager.shared.purchaseProduct(product: product) { (result) in
DispatchQueue.main.async {
switch result {
case .success(let isPurchasedActive):
if isPurchasedActive == true {
self.navigateToHomeViewController()
} else {
GlobalMethod.appdelegate().navigateToInitialViewController()
}
// self.purchasedProductSuccessfully(product: product)
print("finally purchased product \(product.localizedTitle)")
case .failure(let error):
self.showIAPRelatedError(error)
}
self.setActivityIndicator(shouldStart: false)
}
}
} else {
showNoInternetAlert()
}
}
return true
}
This action calls the below function in my IAPManager class:
func purchaseProduct(product: SKProduct, withHandler handler: #escaping ((_ result: Result<Bool, Error>) -> Void)) {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
// Keep the completion handler.
onBuyProductHandler = handler
}
Here is the bottom pop-up that arrives on the screen. But I need to detect this pop-up event. How can I detect that?
In the updatedTransactions delegate all the cases trigger after finishing the transactions. So I can't get the pop-up trigger inside of it.
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
transactions.forEach { (transaction) in
switch transaction.transactionState {
case .purchased:
SKPaymentQueue.default().finishTransaction(transaction)
case .restored:
totalRestoredPurchases += 1
SKPaymentQueue.default().finishTransaction(transaction)
case .failed:
totalPurchaseOrRestoreFailed += 1
SKPaymentQueue.default().finishTransaction(transaction)
case .deferred, .purchasing: break
#unknown default: break
}
}
}
Finally, I call the receipt validation inside of the removedTransactions delegate, get the handler call back and stop the activity indicator inside of the button.
func paymentQueue(_ queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
print("Removed transactions: \(transactions.count)")
print("Unfinished transaction: \(queue.transactions.count)")
//This will be called after finishing all transactions
if queue.transactions.count == 0 {
if totalPurchaseOrRestoreFailed != 0 {
transactions.forEach { (transaction) in
switch transaction.transactionState {
case .purchased:break
case .restored: break
case .failed:
if let error = transaction.error as? SKError {
if error.code != .paymentCancelled {
onBuyProductHandler?(.failure(error))
} else {
onBuyProductHandler?(.failure(IAPManagerError.paymentWasCancelled))
}
print("IAP Error:", error.localizedDescription)
totalPurchaseOrRestoreFailed = 0
}
case .deferred, .purchasing: break
#unknown default: break
}
}
} else {
self.IAPResponseCheck(iapReceiptValidationFrom: .purchaseAndRestoreButton)
UserDefaults.standard.set(false, forKey: "receiptValidationAllow")
}
}
}
My new app has four in app purchases (consumables) and I submitted the first version with these IAPs. These IAP's were marked as "Ready for Review". However, the app got rejected due to another reason, and when I uploaded a new build, I couldn't select these IAP's anymore in the app details page, even though they're still "Ready for Review":
screenshot of the app details page
So after resubmitting a new version of the app for review, I got this rejection information:
We found that your in-app purchase products exhibited one or more bugs when reviewed on iPad running iOS 15.4 on Wi-Fi.
Specifically, we were not able to buy the in app purchases. The buttons did not react to taps
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.
I tested everything on Testflight before and all the IAP's were working fine. I know that prior to submitting an app with IAP's, these purchases have to be selected on the app details page, so I'm curious why I can't select them and if that's causing the issue.
Right when the app launches, in the AppDelegate, I fetch the products: IAPManager.shared.fetchProducts()
and the code for the IAPManager is as follows:
final class IAPManager: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
static let shared = IAPManager()
var products = [SKProduct]()
enum Product: String, CaseIterable {
case firstIdentifier = "com.fahrprueferCreate.tokens_1_1000"
case secondIdentifier = "com.FahrprueferCreate.tokens_5_4000"
case thirdIdentifier = "com.FahrprueferCreate.tokens_10_8000"
case fourthIdentifier = "com.FahrprueferCreate.tokens_20_15000"
var count: Int {
switch self {
case .firstIdentifier:
return 1
case .secondIdentifier:
return 5
case .thirdIdentifier:
return 10
case .fourthIdentifier:
return 20
}
}
}
private var completion: ((Int) -> Void)?
// Fetch Product Objects from Apple
func fetchProducts() {
let request = SKProductsRequest(productIdentifiers: Set(Product.allCases.compactMap({ $0.rawValue})))
request.delegate = self
request.start()
}
// Prompt a product payment transaction
public func purchase(product: Product, completion: #escaping ((Int) -> Void)) {
guard SKPaymentQueue.canMakePayments() else {
// Show some error here
return
}
guard let storeKitProduct = products.first(where: { $0.productIdentifier == product.rawValue }) else {
return
}
self.completion = completion
let payment = SKPayment(product: storeKitProduct)
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().add(payment)
}
// Observe the transaction state
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
transactions.forEach({ transaction in
switch transaction.transactionState {
case.purchasing:
break
case .purchased:
if let product = Product(rawValue: transaction.payment.productIdentifier) {
completion?(product.count)
}
SKPaymentQueue.default().finishTransaction(transaction)
SKPaymentQueue.default().remove(self)
break
case .restored:
break
case .failed:
break
case .deferred:
break
#unknown default:
break
}
})
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
self.products = response.products
print("products: ", response.products)
}
func request(_ request: SKRequest, didFailWithError error: Error) {
guard request is SKProductsRequest else {
return
}
print("Product fetch request failed")
}
}
Friends, my question about testing IAP in the Sandbox.
My Steps:
Sign up in itunes-connect the new sandbox tester.
itunes / appstore on the test phone log out
Delete App from device
RUN in Xcode.
All beautifully displayed on my phone. Everything works exactly the way I want to. Except for one scenario.
I just don't know, maybe it should be in the Sandbox, Sandbox-testers feauture.
In my view-controller, which is implemented non-consumable IAP, there are two buttons: "buy" and "restore."
By clicking "restore" (ONLY "restore"), and entering id / password just registered tester, I expect that nothing will be restored, because this id has never been pressed "Buy" button.
But the recovery is successful. Without buying process.
It's OK?
My code
import UIKit
import StoreKit
class PurchaseUI: UIViewController, SKProductsRequestDelegate, SKPaymentTransactionObserver{
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.setHidesBackButton(true, animated:true)
if (SKPaymentQueue.canMakePayments()) {
let productID: NSSet = NSSet(object: "bla.bla.bla.pro1")
let request: SKProductsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>)
request.delegate = self
request.start()
}
}
#IBAction func buyBtn(sender: AnyObject) {
for product in flag0{
let prodID = product.productIdentifier
if (prodID == ""bla.bla.bla.pro1""){
flag1 = product
buyproduct()
break
}
}
}
func fullVers(){
cashflag = true // global var
}
#IBAction func restorebtn(sender: AnyObject) {
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
var flag0 = [SKProduct]()
var flag1 = SKProduct()
func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
let myProduct = response.products
for product in myProduct {
flag0.append(product)
}
}
func buyproduct(){
let pay = SKPayment(product: flag1)
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().addPayment(pay as SKPayment)
}
func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue) {
for transaction in queue.transactions {
let t: SKPaymentTransaction = transaction as SKPaymentTransaction
let prodID = t.payment.productIdentifier as String
switch prodID {
case "bla.bla.bla.pro1":
fullVers()
default:
break
}
}
}
func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction:AnyObject in transactions {
let trans = transaction as! SKPaymentTransaction
switch trans.transactionState {
case .Purchased, .Restored:
for transaction in queue.transactions {
let t: SKPaymentTransaction = transaction as SKPaymentTransaction
let prodID = t.payment.productIdentifier as String
switch prodID {
case "bla.bla.bla.pro1":
fullVers()
default:
break
}
}
queue.finishTransaction(trans)
break
case .Deferred:
queue.finishTransaction(trans)
break
case .Failed:
queue.finishTransaction(trans)
break
default:
break
}
}
}
func finishTransaction(trans: SKPaymentTransaction){
}
func paymentQueue(queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
}
}
In the paymentQueueRestoreCompletedTransactionsFinished method, you need to check the transaction state - you are granting full access no matter what happened in the transaction. The method has returned saying the transaction finished but, since no purchases were made, nothing should happen. Check for SKPaymentTransactionStateRestored in there and I think that should do the trick.
You're doing the check in paymentQueue:updatedTransactions but not in that one.
I got a problem working on with restoration of purchased product. Every time user clicks on Restore Button the unlock content works before checking if user is logged in, had he purchased it or not. It just unlocks. So here is my question: How do it right? I Add the code with restoration function and purchase one. Btw purchasing works perfect.
func restorePurchases(){
println("hello")
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
func buyProduct(){
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
let payment:SKPayment = SKPayment(product: product)
SKPaymentQueue.defaultQueue().addPayment(payment)
}
func paymentQueue(queue: SKPaymentQueue!, restoreCompletedTransactionsFailedWithError error: NSError!) {
showAlert("error", message: "hoho")
}
func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {
for transaction:AnyObject in transactions {
if let trans:SKPaymentTransaction = transaction as? SKPaymentTransaction{
switch trans.transactionState{
case .Purchased:
self.removeAds()
SKPaymentQueue.defaultQueue().finishTransaction(transaction as! SKPaymentTransaction)
break
case .Failed:
showAlert("Error", message: "Transaction problem")
SKPaymentQueue.defaultQueue().finishTransaction(transaction as! SKPaymentTransaction)
break
case .Restored:
self.removeAds()
break
default:
break
}
}
}
}
I got the answer!
The main problem was that I didnt finish transaction in .Restored, so i got the same thing for .Purchased and .Restored.
func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {
for transaction:AnyObject in transactions {
if let trans:SKPaymentTransaction = transaction as? SKPaymentTransaction{
switch trans.transactionState{
case .Purchased, .Restored:
self.removeAds()
SKPaymentQueue.defaultQueue().finishTransaction(transaction as! SKPaymentTransaction)
break
case .Failed:
showAlert("Error", message: "Transaction problem")
SKPaymentQueue.defaultQueue().finishTransaction(transaction as! SKPaymentTransaction)
break
default:
break
}
}
}
}
Next - i didnt check can customer make purchases or not. But before this i made the observer:
func restorePurchases(){
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
if SKPaymentQueue.canMakePayments(){
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
}
So, that was easy :D
Thanks everyone!