ios subsciptions don't auto renew in sandbox - ios

I have implemented auto renewing subscription in my app. As per documentation, this should auto renew every few minutes up to six times a day. But it's not happening for my app.
I look for the subscription expiration date in the app receipt, and this works the first time, but it doesn't work afterwards. Is the app receipt not being automatically updated in the sandbox environment? It was my understanding that it should.

For anyone dealing with this problem in 2019 or later.
I also experienced subscriptions not being automatically renewed.
In my case auto renewals stopped working after buying 6 times within 8 hours.
Details:
A one month subscription in the Sandbox lasts 5 minutes and is automatically renewed 6 times (the entire purchase expires in 30 minutes). After that you have to buy again and the same process starts over.
For automatic renewals there is however a limit of buying 6 times for every 8 hour period.
This limit is per test user per 8 hours, so you can simply use a new test users to get around this.
Under
Settings -> iTunes & App Store
there is now a new option for Sandbox Account where you can log out and log in for a new test user. This took me a long time to figure out.
The following can be used to get the receipt from apple
static func getReceipt() -> String? {
guard let url = Bundle.main.appStoreReceiptURL,
let _ = try? Data(contentsOf: url) else {
print("no receipt exists")
return nil
}
do {
let receipt = try Data(contentsOf: url)
print("receipt-data: \(receipt.base64EncodedString(options:[]))")
return receipt.base64EncodedString(options: [])
}
catch {
print("catch error")
return nil
}
}

Instead of attempting to 'Build and Run' your App each time, try to reopen the App by reopening it on the device directly. I found this way, I can refresh the subscription receipts, based upon the prior sandbox receipt.
It appears that each time you 'Build and Run' via Xcode that any pending subscription renewals are reset. Potentially rebuilding your App is the reason that the subscription auto-renew is reset.

Related

AppStore.sync() not restoring purchases

On an app that was using the old API for In-App Purchases (StoreKit 1). The app is already published on the App Store. The purchase is non-consumable.
While trying to migrate to StoreKit 2, I'm unable to restore purchases.
Specifically displaying and purchasing products works as expected, but when deleting and reinstalling the app, and then trying to restore purchases I can't do it.
I'm trying to restore them using the new APIs but it doesn't seem to be working.
What I have tried so far:
I'm listening for transaction updates during the whole lifetime of the app, with:
Task.detached {
for await result in Transaction.updates {
if case let .verified(safe) = result {
}
}
}
I have a button that calls this method, but other than prompting to log in again with the Apple ID it doesn't seem to have any effect at all:
try? await AppStore.sync()
This doesn't return any item
for await result in Transaction.currentEntitlements {
if case let .verified(transaction) = result {
}
}
This doesn't return any item
for await result in Transaction.all {
if case let .verified(transaction) = result {
}
}
As mentioned before I'm trying this after purchasing the item and deleting the app. So I'm sure it should be able to restore the purchase.
Am trying this both with a Configuration.storekit file on the simulator, and without it on a real device, in the Sandbox Environment.
Has anyone being able to restore purchases using StoreKit 2?
PD: I already filed a feedback report on Feedback Assistant, but so far the only thing that they have replied is:
Because StoreKit Testing in Xcode is a local environment, and the data is tied to the app, when you delete the app you're also deleting all the transaction data for that app in the Xcode environment. The code snippets provided are correct usage of the API.
So yes, using a Configuration.storekit file won't work on restoring purchases, but if I can't restore them on the Sandbox Environment I'm afraid that this won't work once released, leaving my users totally unable to restore what they have already purchased.
After releasing to the App Store and finally trying the app directly in production I can confirm that it works, but I have to say that it is not ideal to be unable to test this on the sandbox environment.
Also I feel the documentation was not clear enough, at least not for me.
Probably it is clear for other folks, but I was expecting the purchases to be restored automatically and get them on for await result in Transaction.updates, but this didn't work.
What did work was to check Transaction.currentEntitlements, and if the entitlement is not there, then do a sync() and check again.
This is the code that worked for me:
try? await AppStore.sync()
for await result in Transaction.currentEntitlements {
if case let .verified(transaction) = result {
// ...
}
}
Caveats
As mentioned before, this only works on release mode and doesn't work on debug mode without StoreKit Testing, that is without a Configuration.storekit.
If you want to use StoreKit Testing (using a Configuration.storekit) restoring purchases works (with this same approach), but only if you don't delete de app and reinstall again. By deleting the app you loose StoreKit Testing history.
As mentioned by #loremipsum, this can be tested on TestFlight too (for it being release mode).
Verified both iOS and macOS.

SwiftyStoreKit - How to properly restore auto renewable subscriptions?

So I have a really basic concept of auto-renewable subscriptions in the app which unlocks PRO functionality.
I also have a non-consumable purchase which is a "one-time purchase subscription" which unlocks the PRO functionality once and forever.
What I can't wrap my head around is, when I call restorePurchases (using SwiftyStoreKit) it brings in all purchases that were ever made by a user. Those may include multiple purchases of the same subscription which were expired long ago.
What I do next is I call a verifyPurchase method on each of those restored purchases and that method checks expiration date of each subscription that was ever purchased and if it is expired - it takes away the PRO from the user by clearing keychain (as it thinks that the current subscription got expired):
case .expired(let expiryDate, let items):
log.info("Subscription has expired on \(expiryDate)")
log.debug(items)
self.activeSubscription = nil
self.clearStore()
seal.reject(SubscriptionServiceError.expired(on: expiryDate))
}
So what currently happens in my app is even if a user has an active subscription, if he or she tries to restorePurchases, there is a chance that verifyPurchase last purchase to verify would be an expired subscription, resulting in a canceled PRO version of the app for the user, even with an active subscription on board.
What would be the best practice to avoid this mistake and always verify the one and only right subscription?
From the SwiftyStoreKit docs:
let purchaseResult = SwiftyStoreKit.verifySubscription(
ofType: .autoRenewable, // or .nonRenewing (see below)
productId: productId,
inReceipt: receipt)
switch purchaseResult {
case .purchased(let expiryDate, let items):
print("\(productId) is valid until \(expiryDate)\n\(items)\n")
case .expired(let expiryDate, let items):
print("\(productId) is expired since \(expiryDate)\n\(items)\n")
case .notPurchased:
print("The user has never purchased \(productId)")
}
It looks like you would pass in the product Id to determine if a specific subscription is still active.
You probably don't want to do this on every app launch so you should cache the result somewhere - all you need is probably the expiration date.

"Sign-in Required" popup doesn't appear after tapping on a purchase button

On running purchase UITests with XCUitest, sometimes after tapping on the purchase button in the IOS native pop-up
Sign-in Required
doesn't display for a long time (more than 2 min).
The tests are running on a real device(iPhone 6 ios 12).
I've tried to reproduce it manually with no success.
Every time I tried it manually it works fine and most of the time it works fine with the automatic test.
This is the code that waits for the alerts and handles with the alerts with "addUIInterruptionMonitor"
func PurchaseTest1(elementName: String) -> Bool {
if TestUtilities.wait(forMax: 120.0, condition: { return app.buttons[elementName].exists }, doPerIteration: { self.app.swipeDown() } ) == false {
return false
}
return true
}
It's waiting for an element to appear, after every iteration, it swipes down to call to "addUIInterruptionMonitor" to check if there is an iOS pop-up to handle.
Most of the time it works and the pop-up appear but sometimes pop-up doesn't appear at all (I can see in the screenshots that I have for every test).
I had a similar problem. I saw network error, and some time didn't see any errors. I would do the following steps, I hope, It will fix your issue.
reset your device; When I was debugging an issue, I realize there is some certificate related issue that causing this problem.
I think you are making apple purchase, Sandbox allows for a certain time for one user. You can't buy a continuous basis like running over and over again. May be 30 min before you can start purchase again using the same users
Under subscription, Make sure you don't have an active subscription, It can be already purchased and didn't show that button
I think the test will pass if you change device and use new apple sandbox users.

iOS in-app purchase sandbox mode testing logic

In my app there is a monthly auto-renewing subscription. We are doing server side validation based on expires_date from latest_receipt_info, then calculating and sending to app with daysLeft using this if daysLeft > 0, and then unlocking subscription features.
if(_backendDaysLeft) {
if(_backendDaysLeft.integerValue > 0) {
NSLog(#"Subs :ACTIVE");
status = SubscriptionStatusActive;
}
else {
NSLog(#"Subs :EXPIRED");
status = SubscriptionStatusExpired;
}
}
But in sandbox mode always daysLeft will be 0 in our tests, and so is always expired. In sandbox mode
If I change the condition to be greater than zero (>0) to >=0 for sandbox mode testing and when I submit the app if I revert back to >0, will the Apple review team also use sandbox mode to unlock it?
How do I tackle this situation?
Not really sure but looks like your server-end calculation of days-left could be wrong. In Sandbox mode, a monthly Auto-Renewing subscription would expire in just 5 mins and a renew would trigger. This goes on for some 5 or so times, so in any case, your subscription gets canceled after some 30 mins or so.
If you are calculating days-left, seems to me, that this calculation could be flawed as 30 mins are too less than 24 hrs.
A better approach would be to return the exact date-time the subscription expires and use that in your app to enable/disable features.
Sandbox subscriptions expire in ~5mins so "days left" is always 0.
Send back the expires_date and server timestamp to do the calculation on the client of time remaining.
Don't send back just an expires_date since the device time could be incorrect.

How to detect if user cancel auto-renewable subscriptions during the free trial period?

Apple's document does not seem to mention this. So if user cancels an auto-renewable subscription purchased during the free trial period, how do we detect?
In appstore receipt JSON there is this field: is_trial_period. But I think this is for indication of whether the free trial period is over.
The only thing I can think of is this NSBundle.mainBundle().appStoreReceiptURL?.path and if this is nil than that will indicate the user has not subscribed or cancel within the free trial period. But for sandbox testing, there is no way to do a cancel during free trial period to test this scenario.
Does anyone have a solid knowledge of this?
In order to support auto-renewing subscriptions, your app needs to periodically submit the app receipt obtained from NSBundle.mainBundle().appStoreReceiptURL?.path to Apple's receipt validation service.
Contained in the JSON response from this service is the latest_receipt_info array.
By examining this array you will be able to determine the currently active subscription(s).
If a user turns off auto-renewal before the expiration of the free trial then latest_receipt_info won't contain a purchase with an expires_date after the free trial end date
This means, that strictly speaking, you can't "detect a cancellation" as there is no "cancellation"; there just isn't a renewal at the end of the free trial period.
This is possible with the web hook feature in iTunes Connect.
When you set a path for the Subscription Status URL of your app the App Store server will call that URL when the subscription status changes.
Currently the following key events will trigger the call:
INITIAL_BUY Initial purchase of the subscription.
CANCEL Subscription was canceled by Apple customer support.
RENEWAL Automatic renewal was successful for an expired subscription.
INTERACTIVE_RENEWAL Customer renewed a subscription interactively after it lapsed, either by using your app’s interface or on the App
Store in account settings.
DID_CHANGE_RENEWAL_PREFERENCE Customer changed the plan that takes affect at the next subscription renewal.
DID_CHANGE_RENEWAL_STATUS Subscription has expired and customer resubscribed to the same or another plan.
More can be found here and here.
The correct way to do this is to check the auto-renew preference on the receipt. If you want to get notified of this even if the user doesn't open your app (or deletes it) you'll need to store and refresh the receipt on your server. There are 3 fields that you should be concerned with to detect a cancellation.
Expiration Date (lets you know if subscription is still active)
Auto-renew status (lets you know if the user "cancelled")
Cancellation Date (tells you why subscription cancelled by support)
You should check for receipts that are not expired, not cancelled and have an auto-renew status of "0". These will be users that are in a free trial, but have auto-renew turned off. Unfortunately, the App Store Connect Subscription Status Notifications don't report this to you.
Here's a good blog post that goes over a little more of the the details: iOS Subscriptions are Hard
Here is how I got the json data about receipt:
let sharedSecret = "..." //you can find this string on AppStoreConnect-IAP
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: [])
AF.request("https://sandbox.itunes.apple.com/verifyReceipt", method: .post, parameters: ["receipt-data":receiptString, "password": sharedSecret], encoding: JSONEncoding.default, headers: nil)
.responseJSON(completionHandler: { (response) -> Void in
print(response)
})
}
catch {
print("Couldn't read receipt data with error: " + error.localizedDescription) }
}
If you want a production URL, not the Sandbox, change the url to https://buy.itunes.apple.com/verifyReceipt
In the response you will find all data you need.
Note that you need to include this in the Podfile : pod 'Alamofire', '~> 5.2'

Resources