iOS IAP- need advice on premium version of app - ios

I've followed a tutorial to create an IAP for my game. My goal is to have a "Remove Ads" button. Once the user gets this IAP, the remove ads button disappears, and ads stop showing. On future launches of the app this purchase is loaded automatically.
I've got the basics of this down, but here are my issues.
I've created a sandboxed user, and every time I start my app it's asking me to login to itunes. I'd figure it should really only be asking me to login when I'm deciding to purchase the app. Is this related to me being a sandboxed user?
I'm also restoring purchases on every app launch. This seems to be happening automatically without login.. so why is it me asking to login every time? Should I be utilizing NSUserDefaults to avoid restoring purchases in the future?
Here is my code so far:
override func viewDidLoad() {
// storekit delegation
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
self.getProductInfo()
}
func getProductInfo(){
if SKPaymentQueue.canMakePayments() {
let request = SKProductsRequest(productIdentifiers: NSSet(object: self.productID))
request.delegate = self
request.start()
}
// else {
// please enable in app purchases
// }
}
Delegate methods
func productsRequest(request: SKProductsRequest!, didReceiveResponse response: SKProductsResponse!) {
var products = response.products
if products.count != 0 {
self.product = products[0] as? SKProduct
}
}
func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue!) {
if queue.transactions.count != 0 {
if let purchase = queue.transactions[0] as? SKPaymentTransaction {
if purchase.payment.productIdentifier == self.productID {
println("you bought it already")
}
}
}
}
func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {
for transaction in transactions as [SKPaymentTransaction] {
switch transaction.transactionState {
case SKPaymentTransactionState.Purchased:
// self.unlockFeature()
SKPaymentQueue.defaultQueue().finishTransaction(transaction)
case SKPaymentTransactionState.Failed:
SKPaymentQueue.defaultQueue().finishTransaction(transaction)
default:
break
}
}
}
Is this the correct approach?

1) Yes, the login requests are related to the sandbox user
2) You should be checking the receipt each time, not restoring purchases or storing separate local lists of purchases. See Apple's Receipt Validation Programming Guide: https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573-CH105-SW1
Restoring purchases should be a user-initiated operation, with dedicated UI (a restore button in the store somewhere) separate from checking if a purchase has been made.

Should I be utilizing NSUserDefaults to avoid restoring purchases in the future?
You need to avoid NSUserDefaults for IAP. NSUserDefaults are stored in plist in binary format, with no encryption. If you need security you should use KeyChain. It encodes values and is best way to save some security information.

Related

Proper way to add and remove `SKPaymentQueue` observer Swift

I am adding the observer in the didFinishLaunchingWithOptions like this and then call receipt validation:
IAPManager.shared.startObserving()
IAPManager.shared.IAPResponseCheck(iapReceiptValidationFrom: .didFinishLaunchingWithOptions)
And removing it in the applicationWillTerminate like this:
IAPManager.shared.stopObserving()
I am also checking the state of the purchase at applicationWillEnterForeground by calling the receipt validation:
IAPManager.shared.IAPResponseCheck(iapReceiptValidationFrom: .applicationWillEnterForeground)
IAP Manager class in short:
class IAPManager: NSObject {
static let shared = IAPManager()
private override init() { super.init() }
func startObserving() {
SKPaymentQueue.default().add(self)
}
func stopObserving() {
SKPaymentQueue.default().remove(self)
}
}
In the IAP manager class inside of updatedTransactions, I am verifying the receipt and then finishing the transaction after each purchase & restore like this:
case .purchased:
self.IAPResponseCheck(iapReceiptValidationFrom: .purchaseButton)
SKPaymentQueue.default().finishTransaction(transaction)
case .restored:
totalRestoredPurchases += 1
SKPaymentQueue.default().finishTransaction(transaction)
And lastly call the receipt validation inside of paymentQueueRestoreCompletedTransactionsFinished:
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
if totalRestoredPurchases != 0 {
self.IAPResponseCheck(iapReceiptValidationFrom: .restoreButton)
} else {
print("IAP: No purchases to restore!")
onBuyProductHandler?(.success(false))
}
}
But my receipt validation Func gets called multiple times randomly when I came back from background to foreground, or restart the app.
func IAPResponseCheck(iapReceiptValidationFrom: IAPReceiptValidationFrom) {
print("iapReceiptValidationFrom \(iapReceiptValidationFrom)")
}
I search on the internet I found that it is happening because somehow there are multiple observers are added. But I add it according to apple's guidelines. So according to my implementation what am I missing here?
I want to call my receipt validation func just one time

How to check purchase status of In-App Purchase (Auto Renewing Subscription)?

I created an In-App Purchase feature for the app, using an Auto-Renewing Subscription. The problem is that I'm not sure how to check the purchase status, whether the subscription has run out or is still active.
Here's where I try saving the purchased status:
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
print("add payment")
for transaction: AnyObject in transactions {
let trans = transaction as! SKPaymentTransaction
print(trans.error)
switch trans.transactionState {
case .purchased:
print("buy ok, unlock IAP HERE")
print(p.productIdentifier)
let prodID = p.productIdentifier
switch prodID {
case "com.test.UnlockTools.Subscription1":
print("tool set 1 unlocked")
uTool1()
print("tool set 2 unlocked")
uTool2()
print("tool set 3 unlocked")
uTool3()
UserDefaults.standard.set(true, forKey: "isSubbed")
default:
print("IAP not found")
}
queue.finishTransaction(trans)
case .failed:
print("buy error")
queue.finishTransaction(trans)
break
default:
print("Default")
break
}
}
}
This is where I call the UserDefaults and allow or deny button interaction:
override func viewDidLoad() {
super.viewDidLoad()
if(SKPaymentQueue.canMakePayments()) {
print("IAP is enabled, loading")
let productID: NSSet = NSSet(object: "com.test.UnlockTools.Subscription1")
let request: SKProductsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>)
request.delegate = self
request.start()
} else {
print("please enable IAPS")
}
status = UserDefaults.standard.bool(forKey: "isSubbed") ?? false
if status == true {
unlockTool1.isUserInteractionEnabled = true
unlockTool2.isUserInteractionEnabled = true
unlockTool3.isUserInteractionEnabled = true
} else {
unlockTool1.isUserInteractionEnabled = false
unlockTool2.isUserInteractionEnabled = false
unlockTool3.isUserInteractionEnabled = false
}
}
If I find out the status then I will be able to save the state as true/false. I tried using UserDefaults with some success. Not sure if I placed the code in the best location though.
If the subscription is still active, I'd like to allow the user to click a button, and deny the button click if the subscription has run out.
I'll add more code if needed. Any help would be greatly appreciated.
The only way to check the subscription status is to verify the receipt with Apple to see if it's still valid using their /verifyReceipt endpoint - docs.
What you could do is cache some expiration date after the purchase and use that to check if the subscription is valid. If it's passed the expiration date you can re-check the receipt with Apple to see if it's renewed. There are also edge cases where a user is refunded and their subscription is cancelled before the expiration date - you should update your receipts periodically with Apple to check this case. Ideally, this should all be happening server side as well to avoid piracy.
Here's a great post that summarizes the nauces of Apple subscriptions very well: iOS Subscriptions are Hard

Wait until In App Purchase is complete until perform actions

When a button is pressed I use storekit to purchase it. But the issue is the createItems() function and segue are performed before the purchase is confirmed. Is there any way to do this?
func addItems() {
IAPService.shared.purchase(product: .nonConsumable)
createItems()
performSegue(withIdentifier: "returnItems", sender: self)
}
I need the function + segue to be run after the in app purchase has been completed.
IAP function:
func purchase(product: IAPProduct) {
guard let productToPurchase = products.filter({ $0.productIdentifier == product.rawValue}).first else {return}
let payment = SKPayment(product: productToPurchase)
paymentQueue.add(payment)
}
payment Queue function
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
print(transaction.transactionState.status(), transaction.payment.productIdentifier)
switch transaction.transactionState {
case .purchasing:
break
default:
queue.finishTransaction(transaction)
}
}
}
One simple solution is for the paymentQueue method, when it is called because the purchase has taken place, to post a Notification thru the NotificationCenter. Any other view controller that needs to be informed instantly when the purchase takes place will have registered for this Notification, and thus will hear about it at that moment and can take any desired action.
Keep in mind, too, that you are going to record somewhere, typically in UserDefaults, the fact that the purchase has occurred. Thus any subsequent view controller can always check in its viewDidLoad or viewWillAppear to see whether the purchase has happened, and can modify its behavior accordingly.

Swift In App Purchase Non Consumable

I am developing IAP functionality of non consumable (removing Ads).
I've created an helper for all the operation and everything works fine.
When a user buy/restore the purchase i set this:
let save = UserDefaults.standard
save.set(true, forKey: "Purchase")
save.synchronize()
This works fine as long as the user never delete the app.
I was wondering...
Is there a way to know if the user (after deleting and reinstalling the app) already had made a purchase? so to change the title of the button from "purchase" to "restore"?
For every app, Apple requires that you include a "Restore Purchases" button. This is exactly for the problem you are facing. It will recreate the SKPaymentTransaction for every previously purchased IAP with a "restored" state for the iCloud account that is currently signed in at the App Store, and put on the SKPaymentQueue. Read more...
Maybe it would make sense to have a receipt validation in your code, when the user is interacting in your app. (Apple docs: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Introduction.html)
Just have a look at this small example:
Init your product requests:
if SKPaymentQueue.canMakePayments() {
print("starting IAPS")
let productIdentifiers = Set(["YOUR_IAPP_IDENTIFIER#1", "YOUR_IAPP_IDENTIFIER#1"])
let request = SKProductsRequest(productIdentifiers: productIdentifiers as Set<String>)
request.delegate = self
request.start()
} else {
print("please enable IAPS")
}
Set your products in your app and do the receipt validation
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("product request")
let myProduct = response.products
for product in myProduct {
if product.productIdentifier == "YOUR_IAPP_IDENTIFIER#1" {
self.productYear = product
} else if product.productIdentifier == "YOUR_IAPP_IDENTIFIER#2" {
self.productMonth = product
}
print("product added")
print(product.productIdentifier)
print(product.localizedTitle)
print(product.localizedDescription)
print(product.price)
self.receiptValidation()
}
Validation example
func receiptValidation() {
self.logger.log.debug("In receipt validation")
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: [])
let dict = ["receipt-data" : receiptString, "password" : "YOUR_PASSWORD"] as [String : Any]
// Getting the receipt data from the JSON file and other logic
// ...
// ...
}
}
}
Please let me know, if you need more clarification on this. Furthermore you can simply restore it with the PaymentQueue (Apple Doc: https://developer.apple.com/documentation/storekit/skpaymentqueue)
If you provide a login system you will know easily , otherwise
no other way you should put buy and restore buttons for payment Or you store this key-value in
device key-chain and read them in first setup ,but you should know from ios 10.3 If you deleted the app the associated key-chain items will be deleted
Note: Apple states that all apps that have IAP should put a restore payment functionality so, If there is a workaround app will be rejected by Apple

Restore Purchase : Non-Consumable

I have completed a small app where I have a non-consumable purchase option. It is on the App Store.
The purchase of the product runs OK. It's my Restore Purchase function that seems to do nothing.
I have added this code for the Restore Purchase #IBAction:
#IBAction func restorePurchases(sender: AnyObject) {
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
But nothing happens when I hit the restore purchase button.
I think I have to add a function that checks if the restore was successful or not. Am planning to amend code to the following:
#IBAction func restorePurchases(sender: AnyObject) {
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {
for transaction:AnyObject in transactions {
if let trans:SKPaymentTransaction = transaction as? SKPaymentTransaction{
switch trans.transactionState {
case .Restored:
SKPaymentQueue.defaultQueue().finishTransaction(transaction as SKPaymentTransaction)
var alert = UIAlertView(title: "Thank You", message: "Your purchase(s) were restored.", delegate: nil, cancelButtonTitle: "OK")
alert.show()
break;
case .Failed:
SKPaymentQueue.defaultQueue().finishTransaction(transaction as SKPaymentTransaction)
var alert = UIAlertView(title: "Sorry", message: "Your purchase(s) could not be restored.", delegate: nil, cancelButtonTitle: "OK")
alert.show()
break;
default:
break;
}
}
}
Will this do the trick?
I have been through every thread in relation to effecting Restore Purchase transactions, and my research has led me to the above. So I don't think this is a duplicate of a question, but perhaps may clarify how to successfully restore purchases for others facing my similar situation.
Your codes looks pretty fine for the most part, although some parts seem to be from older tutorials . There is some changes you should make, one of them is that you need to call your unlockProduct function again.
This is the code I use (Swift 3).
/// Updated transactions
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing:
// Transaction is being added to the server queue.
case .purchased:
// Transaction is in queue, user has been charged. Client should complete the transaction.
defer {
queue.finishTransaction(transaction)
}
let productIdentifier = transaction.payment.productIdentifier
unlockProduct(withIdentifier: productIdentifier)
case .failed:
// Transaction was cancelled or failed before being added to the server queue.
defer {
queue.finishTransaction(transaction)
}
let errorCode = (transaction.error as? SKError)?.code
if errorCode == .paymentCancelled {
print("Transaction failed - user cancelled payment")
} else if errorCode == .paymentNotAllowed { // Will show alert automatically
print("Transaction failed - payments are not allowed")
} else {
print("Transaction failed - other error")
// Show alert with localised error description
}
case .restored:
// Transaction was restored from user's purchase history. Client should complete the transaction.
defer {
queue.finishTransaction(transaction)
}
if let productIdentifier = transaction.original?.payment.productIdentifier {
unlockProduct(withIdentifier: productIdentifier)
}
case .deferred:
// The transaction is in the queue, but its final status is pending external action
// e.g family member approval (FamilySharing).
// DO NOT freeze up app. Treate as if transaction has not started yet.
}
}
}
Than use the delegate methods to show the restore alert
/// Restore finished
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
guard queue.transactions.count != 0 else {
// showAlert that nothing restored
return
}
// show restore successful alert
}
/// Restore failed
func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: NSError) {
/// handle the restore error if you need to.
}
Unlock product is just a method I am sure you already have too.
func unlockProduct(withIdentifier productIdentifier: String) {
switch productIdentifier {
/// unlock product for correct ID
}
}
As a side note, you should move this line
SKPaymentQueue.default().add(self)
out of your restore and buy function and put it in viewDidLoad.
Apple recommends you add the transaction observer as soon as your app launches and only remove it when your app is closed. A lot of tutorials unfortunately dont teach you this correctly. This way you unsure that any incomplete transactions e.g due to network error, will always resume correctly.
https://developer.apple.com/library/content/technotes/tn2387/_index.html
In my real projects my code for IAPs is in a Singleton class so I would actually using delegation to forward the unlockProduct method to my class that handles gameData. I can than also make sure the observer is added at app launch.
Hope this helps
Took me a while to suss out, but the reason my StoreKit was not updatingTransactions and restoring the purchase was because of a broken Configuration setting in my app's Scheme. When I set that to None, it worked!
In Xcode I went into Edit>Scheme (image1) clicked on the Run>Options tab and selected None for StoreKit Configuration (image2). I also went on my physical device, and logged out of my personal Apple purchase account (Settings >Your Name/Pic at the Top > Media & Purchases > Sign Out) (image3). And finally, this step might not be critical, but I logged into my test sandbox account on my device at the bottom of the Settings>App Store menu(image4 and image5). That is an account that I setup in developer.apple.com under test users.

Resources