I would like to check the Auto Renewable Subscription status whenever I open the app.
This is to make sure that the user is still subscribed to the service. How do I achieve this?
Any thoughts? Thank you
P.S.: I am using SwiftyStoreKit
Here is several ways to do receipt validation to check is user granted to subscription. Here is two ways of doing it correctly:
Do receipt validation locally as it is written here.
Do receipt validation remotely as it is written here. It is mentioned that receipt should not be sent to App Store within an app. Short summary:
Your app sends receipt to your backend.
Your backend sends receipt to Apple backend for validation.
Your backend gets response from the apple.
Your backend sends result back to your app is receipt valid or invalid.
In both ways you will get list of in-app purchases. It will contain expired subscriptions as well. You would need to go through all subscriptions and check expiration dates. If it is still valid you must grant user with subscription.
As I understand you are using SwiftyStoreKit and here is open task for local receipt validation.
You can check with this function. its works with swift4
func receiptValidation() {
let SUBSCRIPTION_SECRET = "yourpasswordift"
let receiptPath = Bundle.main.appStoreReceiptURL?.path
if FileManager.default.fileExists(atPath: receiptPath!){
var receiptData:NSData?
do{
receiptData = try NSData(contentsOf: Bundle.main.appStoreReceiptURL!, options: NSData.ReadingOptions.alwaysMapped)
}
catch{
print("ERROR: " + error.localizedDescription)
}
//let receiptString = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
let base64encodedReceipt = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions.endLineWithCarriageReturn)
print(base64encodedReceipt!)
let requestDictionary = ["receipt-data":base64encodedReceipt!,"password":SUBSCRIPTION_SECRET]
guard JSONSerialization.isValidJSONObject(requestDictionary) else { print("requestDictionary is not valid JSON"); return }
do {
let requestData = try JSONSerialization.data(withJSONObject: requestDictionary)
let validationURLString = "https://sandbox.itunes.apple.com/verifyReceipt" // this works but as noted above it's best to use your own trusted server
guard let validationURL = URL(string: validationURLString) else { print("the validation url could not be created, unlikely error"); return }
let session = URLSession(configuration: URLSessionConfiguration.default)
var request = URLRequest(url: validationURL)
request.httpMethod = "POST"
request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringCacheData
let task = session.uploadTask(with: request, from: requestData) { (data, response, error) in
if let data = data , error == nil {
do {
let appReceiptJSON = try JSONSerialization.jsonObject(with: data)
print("success. here is the json representation of the app receipt: \(appReceiptJSON)")
// if you are using your server this will be a json representation of whatever your server provided
} catch let error as NSError {
print("json serialization failed with error: \(error)")
}
} else {
print("the upload task returned an error: \(error)")
}
}
task.resume()
} catch let error as NSError {
print("json serialization failed with error: \(error)")
}
}
}
I wanted to provide an alternative solution that uses the RevenueCat SDK for those who still stumble upon this question.
AppDelegate.swift
Configure the RevenueCat Purchases SDK with your api key an optional user identifier.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Purchases.configure(withAPIKey: "<...>", appUserID: "<...>")
...
return true
}
Subscription status function
The function below checks the PurchaserInfo to see if the user still has an active "entitlement" (or you can check for an active product ID directly).
func subscriptionStatus(completion: #escaping (Bool)-> Void) {
Purchases.shared.purchaserInfo { (info, error) in
// Check if the purchaserInfo contains the pro feature ID you configured
completion(info?.activeEntitlements.contains("pro_feature_ID") ?? false)
// Alternatively, you can directly check if there is a specific product ID
// that is active.
// completion(info?.activeSubscriptions.contains("product_ID") ?? false)
}
}
Getting subscription status
You can call the above function as often as needed, since the result is cached by the Purchases SDK it will return synchronously in most cases and not require a network request.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
subscriptionStatus { (subscribed) in
if subscribed {
// Show that great pro content
}
}
}
If you're using SwiftyStoreKit, the RevenueCat syntax is fairly similar and there is a migration guide available to help switch over.
Yet another solution to handle auto-renewable iOS subscription using Qonversion SDK.
AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Qonversion.launch(withKey: "yourProjectKey")
return true
}
Get subscription status
Link App Store subscription to Qonversion Product, and link the Product to Permission.
Then you just need to trigger checkPermissions method at the start of your app to check if a user's subscription is still valid. This method will check the user receipt and will return the current permissions. And then for the still-active subscription, you can get the details if the subscriber has turned-off auto-renewal, if he is in grace period (billing retry state), etc.
Qonversion.checkPermissions { (permissions, error) in
if let error = error {
// handle error
return
}
if let premium = permissions["premium"], premium.isActive {
switch premium.renewState {
case .willRenew, .nonRenewable:
// .willRenew is the state of an auto-renewable subscription
// .nonRenewable is the state of consumable/non-consumable IAPs that could unlock lifetime access
break
case .billingIssue:
// Grace period: permission is active, but there was some billing issue.
// Prompt the user to update the payment method.
break
case .cancelled:
// The user has turned off auto-renewal for the subscription, but the subscription has not expired yet.
// Prompt the user to resubscribe with a special offer.
break
default: break
}
}
}
You can check our sample app that demonstrates auto-renewable subscription implementation here.
Related
This question is specific to StoreKit receipt validation & restore purchase testing. I made a restore purchase by calling
SKPaymentQueue.default().restoreCompletedTransactions()
After which I got a callback with updated transactions list and internally refreshes the receipt.
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { }
Meanwhile when I tested the receipt inside paymentQueueRestoreCompletedTransactionsFinished,
fileprivate func loadReceipt() -> String? {
// Get the receipt if it's available
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("Receipt :", receiptString)
return receiptString
} catch {
print("Couldn't read receipt data with error: " + error.localizedDescription)
}
}
return nil
}
Now, If I validate the above receipt using https://sandbox.itunes.apple.com/verifyReceipt API it gave error code 220002 which means Receipt is malformed. Also neither expiry time of receipt is set.
Is the stored receipt loading correctly? How do we make sure a valid receipt is stored ?
I have been trying to test In App purchases (Auto Renewable Subscriptions) to be specific and I always see "The receipt is not valid"
In this regard, as soon as the purchase completes I would like to write true to a bool value called premium
I have first tried to check whether the user is already subscribed at app launch and do the same logic if the user is subscribed (unlock premium)
Here is my code for the same
application didFinishlaunchingWithOptions
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
// Init other stuff
Purchases.configure(withAPIKey: Config.REVENUE_CAT_API_KEY)
checkAllPurchases()
}
checkAllPurchases()
func checkAllPurchases(){
Purchases.shared.purchaserInfo { (purchaseInfo, err) in
print("Purchase info :", purchaseInfo?.entitlements.all)
if(err != nil){
if purchaseInfo?.entitlements["allaccess"]?.isActive == true {
UserDefaults.standard.setValue(true, forKey: "premium")
}
}
else{
self.purchaseError = err?.localizedDescription ?? ""
//print(err?.localizedDescription)
}
}
}
purchase()
This gets called when the buy button is clicked
func purchase (productId : String?){
guard productId != nil else {
return
}
var skProduct : SKProduct?
Purchases.shared.products([productId!]) { (skProducts) in
if !skProducts.isEmpty{
skProduct = skProducts[0]
print("SKProduct:", skProducts[0].productIdentifier)
Purchases.shared.purchaseProduct(skProduct!) { (transaction, purchaseInfo, error, userCancelled) in
// If successfull purchase
if (error == nil && !userCancelled){
UserDefaults.standard.setValue(true, forKey: "premium")
}
else if (error != nil && !userCancelled){
self.purchaseError = error?.localizedDescription ?? ""
if let err = error as NSError? {
print("Error: \(err.userInfo)")
print("Message: \(err.localizedDescription)")
print("Underlying Error: \(String(describing: err.userInfo[NSUnderlyingErrorKey]))")
}
}
}
}
}
}
There is an open issue Here but seems like it is only the case with StoreKit file and not the physical sandbox testing
I have this issue for both the cases and now I don't know how to test my in app purchases
Usually the 'receipt is not valid' error indicates an error with some iOS / Xcode configuration. I would confirm:
You've followed the StoreKit test guide (valid for simulator and device): https://docs.revenuecat.com/docs/apple-app-store#ios-14-only-testing-on-the-simulator
You're using the latest RevenueCat Purchases SDK
You've re-uploaded the StoreKit certificate after making any changes to products or code-signing
You've confirmed the Bundle ID and shared secret are set correctly for your app
All of your products in the StoreKit file are listed in the RevenueCat dashboard
Additionally, if you're using any other third-party purchasing SDK's they could be interfering with the validation process.
I implemented auto renew subscriptions in my app. By default app tries to use production URL for handling receipts, if code returns 27001, app will use sandbox URL. So if I run app from XCode and sandbox account is specified in the iPhone Setting (iTunes/AppStore), app tries at second attempt to load product details for sandbox and does it successfully. Because product array is not empty, I am able to buy a subscription after confirmation prompt. It looks like code working as expected but not in all conditions.
App Review Team does the following: they log off from real and sandbox account in the device settings. In this case if they run app, app shows prompt to log in, because in the ViewController app checks for status of subscriptions. They press Cancel. Next they go to BuySubscriptionViewController and press Cancel again. So at this moment product array is empty and it is not possible to buy a product.
I added the following condition:
#IBAction func buySub(_ sender: Any) {
if (product_mysub != nil) {
StoreManager.shared.buy(product: product_mysub)
} else {
requestData()
}
}
So if a product wasn't found I am asking user to log in again if he tries to buy it.
func requestData() {
let receiptManager: ReceiptManager = ReceiptManager()
receiptManager.startValidatingReceipts()
}
from ReceiptManager.swift:
func startValidatingReceipts() {
do {
_ = try self.getReceiptURL()?.checkResourceIsReachable()
do {
let receiptData = try Data(contentsOf: self.getReceiptURL()!)
self.validateData(data: receiptData)
print("Receipt exists")
} catch {
print("Not able to get data from URL")
}
} catch {
guard UserDefaults.standard.bool(forKey: "didRefreshReceipt") == false else {
print("Stopping after second attempt")
return
}
UserDefaults.standard.set(true, forKey: "didRefreshReceipt")
let receiptRequest = SKReceiptRefreshRequest()
receiptRequest.delegate = self
receiptRequest.start()
print("Receipt URL Doesn't exist", error.localizedDescription)
}
}
If users taps on a button again, app doesn't ask him to log in again and gets data about products. If user taps third time on the button, app shows confirmation prompt to buy a product. How can I continue purchase flow after entering email and password without asking user to press the button 2 times yet?
extension ReceiptManager: SKRequestDelegate {
func requestDidFinish(_ request: SKRequest) {
// this func is not executed after entering account details in the prompt and pressing OK
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("Error refreshing receipt", error.localizedDescription)
}
}
I am using Parse and I want to be able to access the current user on my watch. There is a guide on sharing data between a host app and extensions but the function enableDataSharing(withApplicationGroupIdentifier:, containingApplication:) is marked as available. I guess I misunderstood and Watch Extensions are not considered App Extensions. Is there another way to access the current user on the Watch?
So after a lot of digging and pursuing multiple options I found out that Watch Apps are completely separate from iOS Apps and can no longer share KeyChain Access. This is probably why the built in Parse solution no longer works. Fortunately Watch Connectivity is pretty secure so I think it's safe to send the session token over from iOS and use PFUser.become to log in.
In the watch app:
guard WCSession.default.isReachable else {
print("Phone is not reachable")
return
}
WCSession.default.sendMessage(["Request": "SessionToken"],
replyHandler: (user, error) in {
guard let token = response["Response"] as? String else { return }
// there is a version of PFUser.become() with a success callback as well
PFUser.become(inBackground: token)
}
in iOS (class that conforms to WCSessionDelegate protocol):
public func session(_ session: WCSession,
didReceiveMessage message: [String: Any],
replyHandler: #escaping ([String: Any]) -> Void) {
// Respond to 'Request' message with sessionToken
if let _ = message["Request"], let sessionToken = PFUser.current()?.sessionToken {
replyHandler(["Response": sessionToken])
} else {
// Either user is not logged in, or this was not a 'Request' message
}
}
In my iOS app users complete transactions which I need to post back to the server. I've created a function to do this:
static let configurationParam = NSURLSessionConfiguration.defaultSessionConfiguration()
static var manager = Alamofire.Manager(configuration: configurationParam)
func postItemToServer(itemToPost:DemoItem) {
let webServiceCallUrl = "..."
var itemApiModel:[String: AnyObject] = [
"ItemId": 123,
"ItemName": itemToPost.Name!,
//...
]
ApiManager.manager.request(.POST, webServiceCallUrl, parameters: itemApiModel, encoding: .JSON)
.validate()
.responseJSON { response in
switch response.result {
case .Success:
print("post success")
case .Failure:
print("SERVER RESPONSE: \(response.response?.statusCode)")
}
}
}
Currently I call this once a transaction is complete:
//...
if(transactionCompleted!) {
let apiManager = ApiManager()
apiManager.postItemToServer(self.item)
self.senderViewController!.performSegueWithIdentifier("TransactionCompletedSegue", sender: self)
}
//...
Where DemoItem is a CoreData object.
This all works as expected. However I need the ability to retry the POST request if it fails. For example if the network connection is down at the point of trying post to the server I need to automatically post the data once it becomes active again - at which point there may be several DemoItem's which need to be synced.
I'm new to Swift. In a similar Xamarin app I had a status column in my SQLite database which I set to 'AwaitingSync'. I then had an async timer that ran every 30 seconds, queried the DB for any items which had status='AwaitingSync' and then tried to post them if they existed. If it succeed it updated the status in the DB. I could implement something along the same lines here - but I was never really happy with that implementation as I had a DB query every 30 seconds even if nothing had changed.
Finally, it needs to be still work if the app is terminated. For example any items which weren't synced before the app is killed should sync once the app is resumed. What's the best way to approach this?
Edit
Based on Tom's answer I've created the following:
class SyncHelper {
let serialQueue = dispatch_queue_create("com.mycompany.syncqueue", DISPATCH_QUEUE_SERIAL)
let managedContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
func StartSync() {
//Run on serial queue so it can't be called twice at once
dispatch_async(serialQueue, {
//See if there are any items pending to sync
if let itemsToSync = self.GetItemsToSync() {
//Sync all pending items
for itemToSync in itemsToSync {
self.SyncItemToServer(itemToSync)
}
}
})
}
private func GetItemsToSync() -> [DemoItem]? {
var result:[DemoItem]?
do {
let fetchRequest = NSFetchRequest(entityName: "DemoItem")
fetchRequest.predicate = NSPredicate(format: "awaitingSync = true", argumentArray: nil)
result = try managedContext.executeFetchRequest(fetchRequest) as? [DemoItem]
} catch {
//Handle error...
}
return result
}
private func SyncItemToServer(itemToSync:DemoItem) {
let apiManager = ApiManager()
//Try to post to the server
apiManager.postItemToServer(itemToSync:DemoItem, completionHandler: { (error) -> Void in
if let _ = error {
//An error has occurred - nothing need to happen as it will be picked up when the network is restored
print("Sync failed")
} else {
print("Sync success")
itemToSync.awaitingSync = false
do {
try self.managedContext.save()
} catch {
//Handle error...
}
}
})
}
}
I then call this when ever a transaction is completed:
//...
if(transactionCompleted!) {
let syncHelper = SyncHelper()
syncHelper.StartSync()
}
//...
And then finally I've used Reachability.swift to start the sync every time the network connection resumes:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var reachability:Reachability?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
//...
//Setup the sync for when the network connection resumes
do {
reachability = try Reachability.reachabilityForInternetConnection()
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "reachabilityChanged:",
name: ReachabilityChangedNotification,
object: reachability)
try reachability!.startNotifier()
} catch {
print("Unable to create Reachability")
}
return true
}
func reachabilityChanged(note: NSNotification) {
let reachability = note.object as! Reachability
if reachability.isReachable() {
print("Network reachable")
let syncHelper = SyncHelper()
syncHelper.StartSync()
} else {
print("Not reachable")
}
}
}
This all seems to be working. Is this approach ok and have I missed anything which would improve it? The only gap I can see is if the network connectivity is active however the server throws an error for some reason - I guess I could then add a button for the user to retry any pending items.
Firstly, if your concern is whether the network connection is working, you shouldn't be polling at intervals. You should be using iOS's network reachability API to get notified when the network status changes. Apple provides a simple implementation of this and there are numerous alternative implementations online.
Since a sync status value should be a boolean flag, it's not as if a fetch request is a heavy-duty operation, especially if you use reachability. Not only should the fetch request be fast, you can update the flag after the fact in a single step-- use NSBatchUpdateRequest to set the flag to false on every instance you just sent to the server.
If you want to get the sync status out of the persistent store (not a bad idea since it's metadata), you'll need to maintain your own list of unsynced objects. The best way to do this is by tracking the objectID of the managed objects awaiting sync. That would be something like:
Get the objectID of a newly changed managed object
Convert that to an NSURL using NSManagedObjectID's URIRepresentation() method.
Put the NSURL on a list that you save somewhere, so it'll persist.
You can save the list in a file, in user defaults, or in the persistent store's own metadata.
When it's time to sync, you'd do something like:
Get an NSURL from your list
Convert that into an NSManagedObjectID using managedObjectIDForURIRepresentation(url:NSURL) (which is on NSPersistentStoreCoordinator)
Get the managed object for that ID objectWithID: on NSManagedObjectContext.
Sync that object's data.
Then on a successful sync, remove entries from the list.