I am trying to enable In-App Purchases in Swift 4 for iOS and I can not get the Display Name and Description to properly show up (or load) in the app when I test. I do get the error handing message that I put in if (products.count == 0) {. I can't get the Product Title and Product Description that I set up in AppStoreConnect to properly load when I test my app. Instead the code thinks that there are no products because it runs the code in the if (products.count == 0) { closure. I quadruple checked everything else (i.e. Bundle ID, Product ID, StoreKit's imported, the delegates are set up, everything on the AppStoreConnect side is set up, etc.). There are no Xcode Warnings and the In-App Purchase status is "Ready To Submit". Any suggestions?
var product: SKProduct?
var productID = "myProductID"
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
var products = response.products
if (products.count == 0) {
productTitle.text = "Warning" // GETS DISPLAYED
productDescripton.text = "Unable to connect to In-App Purchase." // GETS DISPLAYED
} else {
product = products[0]
productTitle.text = product?.localizedTitle
productDescripton.text = product?.localizedDescription
buyButton.isEnabled = true
buyButton.alpha = 1.0
}
let invalid = response.invalidProductIdentifiers
for product in invalid {
print("\(product)")
}
}
If you are a rookie at integrating In-App Purchases, then you should read this. If you have clean code and you think that you set everything up properly in App Store Connect, but still can't test properly. Specifically, if your products array is showing empty when it shouldn't be. Then, make sure that you have done the following:
1) Set up a Sandbox Tester via Users and Access in App Store Connect. This needs to be set up with an email that is not already used with iTunes, App Store, or Apple.
2) Filled out all of the necessary forms in Agreements, Tax, and Banking via App Store Connect. You'll know that everything is filled out when Paid Applications shows an Active Status.
3) Are testing on a real device that has been signed out of iTunes and App Store.
Common things that will cause this without warning are:
Your products aren't in the 'Ready to Submit' stage. This is most often from a missing screenshot, which is required even for Sandbox (you can upload a blank image for sandbox testing).
You haven't signed the 'Paid Applications' agreement in App Store Connect
Here's a good blog post that covers everything in a little more detail: Configuring In-app Products is Hard
Related
When I reinstall the app with my subscription secret unchanged ,
let receiptFileURL = Bundle.main.appStoreReceiptURL
let receiptData = try? Data(contentsOf: receiptFileURL!)
let receiptBase64String = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
guard let stringReceipt = receiptBase64String else { return }
Above section of code gives me the old and expired response before installing the new version. I swear whether I have logged in my sandbox account or not, the result gives me the response
transaction_id : 1000000558259116
original_transaction_id : 1000000558239264
productId : YP20190527_3
renewalDict : {
"auto_renew_product_id" = "YP20190527_3";
"auto_renew_status" = 0;
"expiration_intent" = 1;
"is_in_billing_retry_period" = 0;
"original_transaction_id" = 1000000558239264;
"product_id" = "YP20190527_3";
}
What should I do next if I want to simulate a completely new iPhone and proceed purchases? I guess this old response force me prompting the Apple sandbox login for all time
Updates: When I try to ping sandbox.itunes.apple.com , it gives 100% packet loss. When I login my sandbox account at iTunes and App Store Section of my iPhone setting , I cannot login even I have got correct password and email.
Still, the system status of Apple gives available service of Sandbox iTunes User? I am not sure whether my in-app subscription can be tested using sandbox account
https://developer.apple.com/system-status/
While visiting this link with browser, it say Service unavailable
Could you please tell us when I can test the in-app subscription now ?
SKPaymentQueue's restoreCompletedTransactions() does not update the appStoreReceiptURL receipt after completion.
I am working on a Mac app that will be supported via auto-renewing subscriptions. After restoring purchases finishes, I need the updated receipt because I will need to check the current subscription status (expiration date, etc.).
The only solution I could come up with was to refresh the receipt right after restore completed transactions finished, as shown below. This does appear to work. However, the user will be prompted to enter in their username and password twice, which is a less than a perfect experience. In addition, I am concerned that such behavior will lead to a rejection by app review.
public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
if !fileExistsAtAppStoreReceiptURL() {
print("No receipt.")
}
let refreshRequest = SKReceiptRefreshRequest(receiptProperties: nil)
refreshRequest.delegate = self
refreshRequest.start()
}
private func fileExistsAtAppStoreReceiptURL() -> Bool {
guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL else { return false }
return FileManager().fileExists(atPath: appStoreReceiptURL.path)
}
public func requestDidFinish(_ request: SKRequest) {
if request is SKReceiptRefreshRequest {
updateCachedReceiptFromAppStoreReceiptURL()
reverifyKnownPurchases()
notify { self.delegate?.inAppPurchaseController(self, restoreCompletedTransactionsDidFinishWith: .success) }
} else {
notify { self.delegate?.inAppPurchaseController(self, didFinishRequestingIndividualProductInformationWith: .success) }
}
}
Some other notes:
No receipt. does show up in the log.
Because this behavior occurs when using the sandbox environment, I don't know if the behavior is the same when the code executes in production.
The behavior did not change after an attempt with a newly-created test user.
Because I wanted to simulate a real-life situation, I restored purchases on a second Mac, and not the Mac that purchased the subscription.
The rest of the restore process appears to work correctly. paymentQueue:updatedTransactions: is called for each restored transaction with a transactionState of .restored.
SKPaymentQueue.default().finishTransaction(transaction) is called for each restored transaction.
SKPaymentQueue.default().add(self) is called in applicationDidFinishLaunching:.
Although I am making use of an auto-renewing subscription, it does not play a role. A receipt does not appear even if I make use of a non-consumable, for example.
I am not using a server.
The Restoring Purchased Products and the restoreCompletedTransactions() documentation neither explicitly confirm nor deny that the receipt is updated as a part of the restore process. Could this be the expected behavior?
Because I had a similar issue with an iOS test app earlier in the year, this may not be a Mac-only situation.
With all of that in mind, does anyone know how to properly handle such a situation? Has anyone had success with restore + refresh?
We're working on the app with auto-renewal subscription. We have just one option - monthly auto-renewal subscription with one week free trial. We also use "Subscription Status URL" to receive subscription notifications.
The app itself is similar to "TO DO LIST" applications, that can share tasks between multiple users. Thus, we keep data on the server.
Each time user loads the app or creates a task, data comes from the server with current_subscription_status parameter, e.g. we do subscription status validation on the server by simply checking receipt expiration date against current date on the server.
Currently we have only iOS version, but working on Android version as well. And user should be able to sing in to his/her account on different devices with different apple id.
The problem, we've met, is we don't receive actual purchase (subscription) notifications. E.g. when user taps "Start your free 1-week trial" button and subscribes we receive a notification (type INITIAL_BUY). After this one week trial period, we supposed to get another notification with type of something like "RENEWAL", but we receive nothing.
We contacted Apple developer support, but didn't get a real help. They just send links to Apple documentation. Where we found the following note (here is the link):
"To obtain up-to-date information as you process events, your app should verify the latest receipt with the App Store."
So, based on this, I have a question about a possible use case scenario:
User subscribes on iOS device for monthly auto-renewal subscription, e.g. he/she would like to be charged of his/her apple iTunes account. But, after initial purchase he/she closes (kills) the app and doesn't even open it on iOS device anymore. User downloads the app to Android device and use the app only on Android. So, App Store supposed to charge this user each month and send subscription status notifications to the server even if user never opened the app on his iOS device again. So, "...latest receipt verification..." never happens with the iOS app. How can this be implemented?
Tech details:
We use SwiftyStoreKit. These are two parts of in-app purchase implementation:
In AppDelegate we have:
// Auto-renewal complete transactions
SwiftyStoreKit.completeTransactions(atomically: true) { purchases in
for purchase in purchases {
if purchase.transaction.transactionState == .purchased || purchase.transaction.transactionState == .restored {
if purchase.needsFinishTransaction {
// Deliver content from server, then:
SwiftyStoreKit.finishTransaction(purchase.transaction)
}
}
}
}
Here is subscribe function called when user taps "Start free trial button":
func subscribe(completion: #escaping (_ response:Bool, _ message: String?) -> Void) {
SwiftyStoreKit.purchaseProduct(self.productId, atomically: true) { result in
if case .success(let purchase) = result {
if purchase.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(purchase.transaction)
}
let appleValidator = AppleReceiptValidator(service: self.env, sharedSecret: self.sharedSecret)
SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
if case .success(let receipt) = result {
let purchaseResult = SwiftyStoreKit.verifySubscription(
type: .autoRenewable,
productId: self.productId,
inReceipt: receipt)
switch purchaseResult {
case .purchased(let expiryDate, let receiptItems):
if let receiptItem = receiptItems.first {
// Send receipt to the server functionality
.............................
}
completion(true, nil)
case .expired(let expiryDate, let receiptItems):
completion(false, "Receipt has been expired")
case .notPurchased:
completion(false, "Purchase has not been processed")
}
} else {
// receipt verification error
completion(false, "ERROR OCCURED")
}
}
} else {
// purchase error
completion(false, "Canceled")
}
}}
I have a working implementation of gdx-pay for Android using the Google App Store. I did that following this: https://bitbucket.org/just4phil/gdxpayexample/src/.
I am now trying to get it working on iOS, but can't find any docs on how to do that. In the project linked above there is no "ios app store" or any changes made to the ios-project. Does anyone have a link to an example working with ios?
I tried some stuff out and got this far:
IOSResolver:
public class IOSResolver extends PlatformResolver {
public IOSResolver(MyGdxGame myGame) {
super(myGame);
PurchaseManagerConfig config = myGame.purchaseManagerConfig;
initializeIAP(null, myGame.purchaseObserver, config);
installIAP();
}
IOSLauncher:
#Override
protected IOSApplication createApplication() {
IOSApplicationConfiguration config = new IOSApplicationConfiguration();
config.orientationLandscape = false;
config.orientationPortrait = true;
config.useCompass = false;
config.useAccelerometer = false;
game = new MyGdxGame(new IOSPlatform());
return new IOSApplication(game, config);
}
#Override
public boolean didFinishLaunching(UIApplication application, UIApplicationLaunchOptions launchOptions) {
boolean finished = super.didFinishLaunching(application, launchOptions);
game.setPlatformResolver(new IOSResolver(game));
return finished;
}
Core:
// ---- IAP: define products ---------------------
purchaseManagerConfig = new PurchaseManagerConfig();
purchaseManagerConfig.addOffer(new Offer().setType(OfferType.ENTITLEMENT).setIdentifier(ios_productId));
This gives me the error:
[GdxPay/AppleIOS] Requesting product info for test.product.id
2017-02-20 16:58:58.089803 IOSLauncher[5611:2144625] [info] gdx-pay: calls purchasemanager.purchase()
[GdxPay/AppleIOS] Error purchasing product (wrong product info count returned: 0)!
com.badlogic.gdx.utils.GdxRuntimeException: java.lang.RuntimeException: Error purchasing product (wrong product info count returned: 0)!
at com.scene2d.space_camp.MyGdxGame$1.handlePurchaseError(MyGdxGame.java)
at com.badlogic.gdx.pay.ios.apple.PurchaseManageriOSApple$AppleProductsDelegatePurchase.didReceiveResponse(PurchaseManageriOSApple.java)
at com.badlogic.gdx.pay.ios.apple.PurchaseManageriOSApple$AppleProductsDelegatePurchase.$cb$productsRequest$didReceiveResponse$(PurchaseManageriOSApple.java)
at org.robovm.apple.uikit.UIApplication.main(UIApplication.java)
at org.robovm.apple.uikit.UIApplication.main(UIApplication.java)
at com.scene2d.tut.IOSLauncher.main(IOSLauncher.java)
Caused by: java.lang.RuntimeException: Error purchasing product (wrong product info count returned: 0)!
... 5 more
I registered the app on iTunes Connect and set up a product. However I did not post binaries, do I need to for testing IAP? It shows this message in iTunes Connect:
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.
Is there no way to test without uploading the actual app?
I had this same issue and was seeing the same error message.
I think the two keys to getting your IAPs work are the following:
Use a real device for testing, not an emulated one.
Sign in with a sandbox account, not a real one.
Create your sandbox account on itunesconnect.
Build your app and install it onto your ipad/iphone using xcode.
On your device, go into settings and logout. This is important.
Run your app and make a purchase. You will be prompted to login. Use your sandbox account.
I did this and it worked.
I'm implementing the In-App Purchase feature in my iOS app for the first time. I passed all test phases correctly and I submitted the app to review, but it was rejected for this reason:
Your app offers a content subscription but does not have a mechanism in place to support the requirement that the subscription content be available to the user on all of their iOS devices.
To fix the issue I have to:
include an optional user registration feature to deliver subscription content to all of the user's iOS devices.
My app don't require a user registration, so I'm wondering if it's really necessary.
Assuming the answer is "YES!", I don't know what really I have to do.
After the user registration, I need to save the receipt data on my host and then retrieve the data if the user change device?
Actually I get the receipt data in this way:
func validateReceipt() {
let receiptUrl = NSBundle.mainBundle().appStoreReceiptURL
let isSandox = receiptUrl?.absoluteString.containsString("sandboxReceipt")
if let receipt: NSData = NSData(contentsOfURL: receiptUrl!) {
let receiptdata: NSString = receipt.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0))
let transaction = GTLAppleTransactionBeanApiAppleTransactionBean()
transaction.receiptData = receiptdata as String
transaction.sandbox = isSandox
ReceiptService().validate(transaction, completion: validated)
} else {
receiptDetected = false
delegate.subscriptionUpdate()
}
}
The "GTLAppleTransactionBeanApiAppleTransactionBean()" is my GAE Bean and "ReceiptService()" call my GAE Endpoint service. This service check the receipt data with Apple.
A Swift example will be very much appreciated. :P
Sorry for my English.
Thanks, Alessio.