I've created some consumable IAPs and test it in the sandbox environment.
However, restoring purchases doesn't return any transaction even i've purchased few items...
How do i test it?
Here is my code
button's trigger to activate restore purchases
if (SKPaymentQueue.canMakePayments()) {
print("restore payment")
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
Restore purchases paymentQueueRestoreCompletedTransactionsFinished
func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue) {
print("\(queue.transactions.count)")
for transaction:AnyObject in queue.transactions
{
let trans : SKPaymentTransaction = transaction as! SKPaymentTransaction
//var identifier : NSString = trans.payment.productIdentifier
let identifier : NSString = trans.originalTransaction!.payment.productIdentifier
print("pdi \(identifier)")
}
}
Consumable products can't be restored from apple server. For consumable products you will have to manage the purchase list manually at your own server.
If you want restorable products then you can simply do it with Non-Consumable products in iTunes.
Related
I am testing using local StoreKit, for non consumable in-app item.
If I purchase the non consumable in-app item, I can receive callback in SKPaymentTransactionObserver without issue
extension IAPHelper: SKPaymentTransactionObserver {
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
complete(transaction)
...
}
}
private func complete(_ transaction: SKPaymentTransaction) {
print("complete...")
// Save purchase record to disk to indicate user owns this non-consumable item.
persistPurchase(identifier: transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
If I go to Debug/ StoreKit/ Manage Transactions... , to perform refund, I do not receive any callback in SKPaymentTransactionObserver.
Even if I close the app and start the app again, there isn't any callback to SKPaymentTransactionObserver, to indicate the non consumable in-app item has been refunded, so that I can block user from accessing the in-app item.
May I know, is it possible to detect a non-consumable in-app item has been refunded, in SKPaymentTransactionObserver? If not, what is the right way to do so?
If I purchase something and check the purchase receipt I can get the encoded value of it by this:
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
do {
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
print(receiptData)
let receiptString = receiptData.base64EncodedString(options: [])
print("receiptString \(receiptString)")
// Read receiptData
}
catch { print("Couldn't read receipt data with error: " + error.localizedDescription) }
}
But If I purchase something, then delete the app and reinstall it again and check the receipt, I can't get anything with the upper code. It just get the appStoreReceiptURL and go straight away without entering do {} catch. How can I get the receipt in this scenario?
If you attempt to restore purchase , the user will not be refunded the purchased amount, however you would be able to verify that user purchased the product, and will also receive receipt that contants all the information about his previous purchase, therefore you can safely verify the purchase in your App.
In App Store Review Guides is clearly stated that
Any credits or in-game currencies purchased via in-app purchase may
not expire, and you should make sure you have a restore mechanism for
any restorable in-app purchases.
That means you should provide some mechanism (e.g. Restore Purchase Button) anyway, regardless of scenario when the user uninstalls Application. In your case, you can provide the Restore button directly on your Login screen. Check some inspiration on Restore purchase button in Apple Human Interface guidelines.
I you want to restore purchase directly with Apple API after user taps the Restore Purchase button, you can use:
SKPaymentQueue.default().restoreCompletedTransactions()
,which triggers Apple servers, checks the users Apple ID and gets all of the app purchases they have made before and returns you transaction that you observe with SKPaymentTransactionObserver with this code:
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
if transaction.transactionState == .restored {
//here is your restored transaction
}
}
}
If you however want to get hold of the new receipt with all receipt data, including previous purchases, I recommend not using directly Apple API, but using SwiftyStoreKit library : link on github . Code example below:
func restoreYourPurchases( completion: #escaping (Result<Bool,Error>) -> Void) {
SwiftyStoreKit.restorePurchases(atomically: true) { results in
var restoreSuccessfull = false
if results.restoreFailedPurchases.count > 0 {
print("Restore Failed: \(results.restoreFailedPurchases)")
}
else if results.restoredPurchases.count > 0 {
print("Restore Success: \(results.restoredPurchases)")
for purchase in results.restoredPurchases {
if purchase.productId == "yourID" {
let timeOfPurchase = purchase.originalPurchaseDate
}
}
}
else {
print("Nothing to Restore")
completion(.success(false))
}
}
}
Hello I am having getting my App published on the App Store as they keep insisting my in-app purchases are not set up correctly.
Upon testing them myself in the sandbox, everything worked correctly.
This is the message they sent me, I pasted my code below.
Thank you for taking the time to help me out!
Guideline 2.1 - Performance - App Completeness
We found that your in-app purchase products exhibited one or more bugs
when reviewed on iPhone and iPad running iOS 12 on Wi-Fi.
Specifically, your in app purchase buttons do not work.
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.
class IAPService: NSObject {
private override init() {}
static let shared = IAPService()
var products = [SKProduct]()
let paymentQueue = SKPaymentQueue.default()
func getProducts() {
let products: Set = [IAPProduct.consumable.rawValue,
IAPProduct.nonConsumable.rawValue]
let request = SKProductsRequest(productIdentifiers: products)
request.delegate = self
request.start()
paymentQueue.add(self)
}
func purchase(product: IAPProduct) {
for p in products {
if p.productIdentifier == product.rawValue {
let payment = SKPayment(product: p)
paymentQueue.add(payment)
print("Adding product to payment queue")
}
}
}
func restorePurchase() {
print("Restoring purchases")
paymentQueue.restoreCompletedTransactions()
}
func givePurchasedProduct(productID: String) {
if productID.range(of: "Zap") != nil {
NotificationCenter.default.post(name: Notification.Name.init("zapPurchased"), object: nil)
} else if productID.range(of: "Ads") != nil {
NotificationCenter.default.post(name: Notification.Name.init("noAdsPurchased"), object: nil)
}
}
}
extension IAPService: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
self.products = response.products
for product in response.products {
print(product.localizedTitle)
}
}
}
extension IAPService: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
print(transaction.transactionState.status(), transaction.payment.productIdentifier)
switch transaction.transactionState {
case .purchasing, .deferred: break // do nothing
case .purchased:
queue.finishTransaction(transaction)
givePurchasedProduct(productID: transaction.payment.productIdentifier)
case .restored:
self.restorePurchase()
queue.finishTransaction(transaction)
case .failed:
queue.finishTransaction(transaction)
}
}
}
}
extension SKPaymentTransactionState {
func status() -> String {
switch self {
case .deferred:
return "deferred"
case .failed:
return "failed"
case .purchased:
return "purchased"
case .purchasing:
return "purchasing"
case .restored:
return "restored"
}
}
}
App review is very strict when it comes to Apple. Speaking from experience, I have had this problem many times. Your code seems fine to me till it goes to the givePurchasedProduct function.
Okay so things i noticed:
Your app processes the payment and we get return "purchased" if nothing goes wrong
If the case was case .purchased: then we invoke the givePurchasedProduct
On your function. you separate the purchase to see if it's either a Zap purchase or it was to remove the ads
However. this line is confusing me-- Why would you use range when contains where introduced recently.
if productID.contains("Zap") {
// No Zapp? it has to be Ads then
NotificationCenter.default.post(name: Notification.Name.init("zapPurchased"), object: nil)
} else {
NotificationCenter.default.post(name: Notification.Name.init("noAdsPurchased"), object: nil)
}
Side notes. You might have forgot:
To import Foundation
I don't know what goes behind the notification observers since the code is not included. But. It's not delivering
There's more to it. Receipt Validating is a headache, but when it's needed. It's relaxation and more security to your app.
If you're validating the receipt. these question and it's answers helped me a lot. please see:
Implementing Receipt Validation in Swift 3
Lightweight In App Purchases Swift framework for iOS 8.0+, tvOS 9.0+ and macOS 10.10+
Bonus. With SwiftyStoreKit. Receipt validating is just like tapping a button:
Use this method to (optionally) refresh the receipt and perform validation in one step.
let appleValidator = AppleReceiptValidator(service: .production, sharedSecret: "your-shared-secret")
SwiftyStoreKit.verifyReceipt(using: appleValidator, forceRefresh: false) { result in
switch result {
case .success(let receipt):
print("Verify receipt success: \(receipt)")
case .error(let error):
print("Verify receipt failed: \(error)")
}
}
Now on the other hand. to the reviewers the purchased content is not delivering. So they think it's purchase validating.
How do you validate the purchase? deliver the content? please update your question. I'm sure i'll be helpful
Good Luck
I think there is no problem with your iOS code. From the Apple's response, they say that, your server is pointing to production environment of Apple InApp purchase and validating the receipts received from test environment of Apple InApp purchase used within App.
Apple has 2 environments for InApp purchases - Test & Production. Both the environments behave same. When you run the app on your iPhone to test by your QA or while you are debugging, it connects to Test environment. You are not charged in real when using Test environment. But the receipts generated are almost same as that of real production environment
Now when you submit the app to store, it will automatically make purchases from Production environment. Users are charged and real receipts are generated.
Your app is sending those receipts to server I think and your server is using the wrong InApp server environment to verify the receipts.
On your server make sure that the Apple InApp purchase environment URL is appropriately picked based on your InApp purchase receipt. If you or your team is testing the app, your server has to use Test URL and when the app is submitted to production, your server has to use Production URL of InApp Purchase.
I am using Xcode 8.0, Swift 3.0 and testing in app purchases in my iPad. I want to test in app purchases using sandbox user.
There is no account added in device's Setting
The Problem is I am not getting product list in response of product request code.
Please take a look on my code:
let PRODUCT_ID_MY_PRODUCT = "com.company.ProjectName.MyProduct"
// The ProducID in this code and ProducID on iTunes are the SAME. ✔️
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if productID == nil {
productID = PRODUCT_ID_MY_PRODUCT
}
SKPaymentQueue.default().add(self)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
startPurchases()
}
func startPurchases() {
if (SKPaymentQueue.canMakePayments())
{
let productIDs = NSSet(object: self.productID!)
let productsRequest:SKProductsRequest = SKProductsRequest(productIdentifiers: productIDs as! Set<String>)
productsRequest.delegate = self
productsRequest.start()
}
}
// Delegate Methods for SKProductsRequest
func productsRequest (_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
let count : Int = response.products.count
// THE PROBLEM IS HERE.. I AM GETTING COUNT IS ZERO.. MEANS response.products returning null ARRAY
if (count>0) {
let validProducts = response.products
for aProduct in validProducts {
print(aProduct.productIdentifier)
}
} else {
DispatchQueue.main.async(execute: {
UIAlertView(title: "Purchase !", message: "Product not available", delegate: nil, cancelButtonTitle: "OK").show()
return
})
}
}
So..... That's the problem: I am getting response.products null (no data in array) so Please help me to find the solution. You can see the comments in code:
// THE PROBLEM IS HERE.. I AM GETTING COUNT IS ZERO.. MEANS response.products returning null ARRAY
I created products over iTunes Connect. You can see the image below. All the products are in "Ready to Submit" state.
There is some warning on iTunes
Your first In-App Purchase must be submitted with a new app version.
Select it from the app’s In-App Purchases section and click Submit.
Once your binary has been uploaded and your first In-App Purchase
has been submitted for review, additional In-App Purchases can be
submitted using the table below.
And
I also created Sendbox user for testing In-App Purchases. See the image below:
Did I miss something? Or what is the error? And where is error? I want to test in app purchases using sandbox user
I fixed this. There are some points need to careful about. See below:
Make sure your developer account did the paid application contract. see image below:
Create Products on the iTunes Connect.
Implement In-App-Purchases code and configuration settings.
Create one build with Distribution profile.
Upload build on store. Add build to current version. Add In-App-Purchases to the version on iTunes Connect.
Then try to test, if still not then submit app once then cancel it. then after try to test on your device.
Make sure when you test with sandbox user, you need to sign-out from your already logged in account from device settings, and login with sandbox ID.
some screenshots might be helpful.
please check these settings
capabilities --> In-App purchase --> set to "ON"
and at developer.apple.com--> enable In-App purchase for App ID.
and please test app on Device instead of simulator.
I'm trying to do in-app purchases and everything works fine except Restore. Below is the code I have written:
func paymentQueueRestoreCompletedTransactionsFinished(queue:SKPaymentQueue!)
{
for transaction:AnyObject in queue.transactions
{
let trans : SKPaymentTransaction = transaction as SKPaymentTransaction
var identifier : NSString = trans.payment.productIdentifier
println(identifier)
}
}
The problem that I face here is I'm not getting the purchased identifier here. I think I have miswritten the code.
Riyazul, you need to look at the original transaction when you are restoring purchases.
The code you should need is:
var identifier : NSString = trans.originalTransaction.payment.productIdentifier
Let me know if it's still not working.
Add below two lines of code on your button click action
SKPaymentQueue.default().restoreCompletedTransactions()
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
Edit - for Swift 3:
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().restoreCompletedTransactions()