Background: My app provides a number of prompts from a group of arrays. The 'packs' determine which prompts can be toggled on. You start with a default pack and then can purchase the three additional packs (packA, packB, and packC) for a non-consumable IAP.
My goal is to have the user pay for it once, and then be able to access the packs whenever he/she likes; however, once the sandbox user makes the IAP, a popup says "You've already purchased this. Would you like to get it again for free?". I obviously don't want this popping up every time the user selects a pack. Is there anyway to make the purchase a permanent use and not need to constantly restore the purchase?
Below is my current code (anonymized and reduced to only the essential components):
import UIKit
import QuartzCore
import StoreKit
class ViewController: UIViewController, SKPaymentTransactionObserver {
//MAIN SETUP SECTION XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
let productID = "com.domain.appName.additionalPackages"
let defaults = UserDefaults.standard
//I'm just putting "Various" here as a placeholder for my multiple buttons
@IBOutlet weak var (Various): UIButton!
override func viewDidLoad() {
super.viewDidLoad()
SKPaymentQueue.default().add(self)
//I don’t know if this actually does anything
func cleanUp() {
for transaction in SKPaymentQueue.default().transactions {
SKPaymentQueue.default().finishTransaction(transaction)
}
}
}
//PACK SELECTION SECTION XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
var packA = 2
var packB = 2
var packC = 2
var packsUnlocked = false
@IBAction func selectPackA(_ sender: UIButton) {
if packsUnlocked == false {
print("It's locked, ‘Pack A’ not enabled")
} else if packCounterA % 2 == 0 {
if SKPaymentQueue.canMakePayments() { // In App Purchase
let paymentRequest = SKMutablePayment()
paymentRequest.productIdentifier = productID
SKPaymentQueue.default().add(paymentRequest)
print("Initiating Transaction")
} else {
print("No Purchased")
}
promptProvider.includeA.toggle()
packCounterA += 1
} else {
promptProvider.includeA.toggle()
packCounterA += 1
}
}
@IBAction func selectPackB(_ sender: UIButton) {
if packsUnlocked == false {
print("It's locked, ‘Pack B’ not enabled")
} else if packCounterB % 2 == 0 {
if SKPaymentQueue.canMakePayments() { // In App Purchase
let paymentRequest = SKMutablePayment()
paymentRequest.productIdentifier = productID
SKPaymentQueue.default().add(paymentRequest)
print("Initiating Transaction")
} else {
print("No Purchased")
}
promptProvider.includeB.toggle()
packCounterB += 1
} else {
promptProvider.includeB.toggle()
packCounterB += 1
}
}
@IBAction func selectPackC(_ sender: UIButton) {
if packsUnlocked == false {
print("It's locked, ‘Pack C' not enabled")
} else if packCounterC % 2 == 0 {
if SKPaymentQueue.canMakePayments() { // In App Purchase
let paymentRequest = SKMutablePayment()
paymentRequest.productIdentifier = productID
SKPaymentQueue.default().add(paymentRequest)
print("Initiating Transaction")
} else {
print("No Purchased")
}
promptProvider.includeC.toggle()
packCounterC += 1
} else {
promptProvider.includeC.toggle()
packCounterC += 1
}
}
//TRANSACTION FINALIZATION SECTION XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
guard
transaction.transactionState != .purchasing,
transaction.transactionState != .deferred
else {
//Optionally provide user feedback for pending or processing transactions
continue
}
if transaction.transactionState == .purchased || transaction.transactionState == .restored {
print("Transaction Successful")
packsUnlocked = true
//new
defaults.set(true, forKey: "Purchase Pack")
UserDefaults.standard.synchronize()
} else if transaction.transactionState == .failed {
print("Transaction Failed with error")
}
//Transaction can now be safely finished
queue.finishTransaction(transaction)
}
}
}
This is my first app but I think the issue is with the saving of the purchase to UserDefaults
. I'm very new to this so any help is extremely appreciated.
Thank you
After the purchase was done you can access the receipt at Bundle.main.appStoreReceiptURL
. You can use that receipt and validate it against the App Store, to prevent someone from using a fake receipt.
The validation response will tell you that the receipt it legit and has not been refunded. It also tells you that the subscription is still active and when it will expire.
You can do all this on the client but it would be best practice to store the receipt on the server so you can validate it on your end instead of leaving it up to the client for potential manipulation.
Do not call the App Store server verifyReceipt endpoint from your app. You can't build a trusted connection between a user’s device and the App Store directly, because you don’t control either end of that connection, which makes it susceptible to a man-in-the-middle attack.
You can find more infos including code examples in the docs. In addition you can set up hooks so that your sever will actively be notified when the subscription status changes.
If you don't care about the aforementioned security implications and just want to do a proof of concept, you can do this in the app and check whether the user has already made a purchase:
Bundle.main.appStoreReceiptURL?.path
product_id
and expires_date
from the validation response to know which product has been purchased and whether it's already expiredRemember that if the user uninstalls and reinstalls the app, the receipt is not stored on the device. The user has then to restore the purchase where the app downloads the receipt from the App Store server. That is what happens when you press the "Restore Purchase" button that you are probably familiar with from other apps. Restoring is simply done with the SKReceiptRefreshRequest
.