Swift How to handle Auto-renewable Subscription receipt and validation - swift

I am testing the auto renewable In-app purchases in swift, I found out that there are some strange problems with my code.
I am testing these functions in sandbox environment
User can purchase either one month, one year auto renewable subscription or permanent permission
App should check if the subscription is still valid every time when user open app, if not, lock all premium functions
User is able to restore the purchased plan, app should get the previous purchased type ie. one month, one year, or permanent.
After long research on the tutorials, I am still confused about the validation
I see that there are two ways to validate receipt, one is locally the other is on the server.
But I don't have a server, does that mean I can only validate it locally
Every time the auto-renewal subscription expires, the local receipt is not updated, so when I reopen the app I got a subscription expiration alert (The method I defined by my self for validation check ), when I click the restore button, the app restored successfully and receipt was updated
After 6 times manually restored and refresh the receipt (the sandbox user can only renew 6 times), when I click the restore button, the part transaction == .purchased is till called, and my app unlocks premium function, however when I reopen my app, my app alerts that the subscription is expired, which is it should.
My core problem is how can I check the validation of subscriptions with Apple every time when I open the app, I don't have a server, and I don't know why the receipt is not refreshing automatically
Here are some parts of my code, I call checkUserSubsriptionStatus() when I open the app, I am using TPInAppReceipt Library
class InAppPurchaseManager {
static var shared = InAppPurchaseManager()
init() {
}
public func getUserPurchaseType() -> PurchaseType {
if let receipt = try? InAppReceipt.localReceipt() {
var purchaseType: PurchaseType = .none
if let purchase = receipt.lastAutoRenewableSubscriptionPurchase(ofProductIdentifier: PurchaseType.oneMonth.productID) {
purchaseType = .oneMonth
}
if let purchase = receipt.lastAutoRenewableSubscriptionPurchase(ofProductIdentifier: PurchaseType.oneYear.productID) {
purchaseType = .oneYear
}
if receipt.containsPurchase(ofProductIdentifier: PurchaseType.permanent.productID) {
purchaseType = .permanent
}
return purchaseType
} else {
print("Receipt not found")
return .none
}
}
public func restorePurchase(in viewController: SKPaymentTransactionObserver) {
SKPaymentQueue.default().add(viewController)
if SKPaymentQueue.canMakePayments() {
SKPaymentQueue.default().restoreCompletedTransactions()
} else {
self.userIsNotAbleToPurchase()
}
}
public func checkUserSubsriptionStatus() {
DispatchQueue.main.async {
if let receipt = try? InAppReceipt.localReceipt() {
self.checkUserPermanentSubsriptionStatus(with: receipt)
}
}
}
private func checkUserPermanentSubsriptionStatus(with receipt: InAppReceipt) {
if let receipt = try? InAppReceipt.localReceipt() { //Check permsnent subscription
if receipt.containsPurchase(ofProductIdentifier: PurchaseType.permanent.productID) {
print("User has permament permission")
if !AppEngine.shared.currentUser.isVip {
self.updateAfterAppPurchased(withType: .permanent)
}
} else {
self.checkUserAutoRenewableSubsrption(with: receipt)
}
}
}
private func checkUserAutoRenewableSubsrption(with receipt: InAppReceipt) {
if receipt.hasActiveAutoRenewablePurchases {
print("Subsription still valid")
if !AppEngine.shared.currentUser.isVip {
let purchaseType = InAppPurchaseManager.shared.getUserPurchaseType()
updateAfterAppPurchased(withType: purchaseType)
}
} else {
print("Subsription expired")
if AppEngine.shared.currentUser.isVip {
self.subsrptionCheckFailed()
}
}
}
private func updateAfterAppPurchased(withType purchaseType: PurchaseType) {
AppEngine.shared.currentUser.purchasedType = purchaseType
AppEngine.shared.currentUser.energy += 5
AppEngine.shared.userSetting.hasViewedEnergyUpdate = false
AppEngine.shared.saveUser()
AppEngine.shared.notifyAllUIObservers()
}
public func updateAfterEnergyPurchased() {
AppEngine.shared.currentUser.energy += 3
AppEngine.shared.saveUser()
AppEngine.shared.notifyAllUIObservers()
}
public func purchaseApp(with purchaseType: PurchaseType, in viewController: SKPaymentTransactionObserver) {
SKPaymentQueue.default().add(viewController)
if SKPaymentQueue.canMakePayments() {
let paymentRequest = SKMutablePayment()
paymentRequest.productIdentifier = purchaseType.productID
SKPaymentQueue.default().add(paymentRequest)
} else {
self.userIsNotAbleToPurchase()
}
}
public func purchaseEnergy(in viewController: SKPaymentTransactionObserver) {
SKPaymentQueue.default().add(viewController)
let productID = "com.crazycat.Reborn.threePointOfEnergy"
if SKPaymentQueue.canMakePayments() {
let paymentRequest = SKMutablePayment()
paymentRequest.productIdentifier = productID
SKPaymentQueue.default().add(paymentRequest)
} else {
self.userIsNotAbleToPurchase()
}
}
}

If you do not have the possibility to use a server, you need to validate locally. Since you are already included TPInAppReceipt library, this is relatively easy.
To check if the user has an active premium product and what type it has, you can use the following code:
// Get all active purchases which are convertible to `PurchaseType`.
let premiumPurchases = receipt.activeAutoRenewableSubscriptionPurchases.filter({ PurchaseType(rawValue: $0.productIdentifier) != nil })
// It depends on how your premium access works, but if it doesn't matter what kind of premium the user has, it is enough to take one of the available active premium products.
// Note: with the possibility to share subscriptions via family sharing, the receipt can contain multiple active subscriptions.
guard let product = premiumPurchases.first else {
// User has no active premium product => lock all premium features
return
}
// To be safe you can use a "guard" or a "if let", but since we filtered for products conforming to PurchaseType, this shouldn't fail
let purchaseType = PurchaseType(rawValue: product.productIdentifier)!
// => Setup app corresponding to active premium product type
One point I notice in your code, which could lead to problems, is that you constantly add a new SKPaymentTransactionObserver. You should have one class conforming to SKPaymentTransactionObserver and add this only once on app start and not on every public call. Also, you need to remove it when you no longer need it (if you created it only once, you would do it in the deinit of your class, conforming to the observer protocol.
I assume this is the reason for point 2.
Technically, the behavior described in point 3 is correct because the method you are using asks the payment queue to restore all previously completed purchases (see here).
Apple states restoreCompletedTransactions() should only be used for the following scenarios (see here):
If you use Apple-hosted content, restoring completed transactions gives your app the transaction objects it uses to download the content.
If you need to support versions of iOS earlier than iOS 7, where the app receipt isn’t available, restore completed transactions instead.
If your app uses non-renewing subscriptions, your app is responsible for the restoration process.
For your case, it is recommended to use a SKReceiptRefreshRequest, which requests to update the current receipt.

Get the receipt every time when the app launches by calling the method in AppDelegate.
getAppReceipt(forTransaction: nil)
Now, below is the required method:
func getAppReceipt(forTransaction transaction: SKPaymentTransaction?) {
guard let receiptURL = receiptURL else { /* receiptURL is nil, it would be very weird to end up here */ return }
do {
let receipt = try Data(contentsOf: receiptURL)
receiptValidation(receiptData: receipt, transaction: transaction)
} catch {
// there is no app receipt, don't panic, ask apple to refresh it
let appReceiptRefreshRequest = SKReceiptRefreshRequest(receiptProperties: nil)
appReceiptRefreshRequest.delegate = self
appReceiptRefreshRequest.start()
// If all goes well control will land in the requestDidFinish() delegate method.
// If something bad happens control will land in didFailWithError.
}
}
Here is the method receiptValidation:
func receiptValidation(receiptData: Data?, transaction: SKPaymentTransaction?) {
guard let receiptString = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) else { return }
verify_in_app_receipt(with_receipt_string: receiptString, transaction: transaction)
}
Next is the final method that verifies receipt and gets the expiry date of subscription:
func verify_in_app_receipt(with_receipt_string receiptString: String, transaction: SKPaymentTransaction?) {
let params: [String: Any] = ["receipt-data": receiptString,
"password": "USE YOUR PASSWORD GENERATED FROM ITUNES",
"exclude-old-transactions": true]
// Below are the url's used for in app receipt validation
//appIsInDevelopment ? "https://sandbox.itunes.apple.com/verifyReceipt" : "https://buy.itunes.apple.com/verifyReceipt"
super.startService(apiType: .verify_in_app_receipt, parameters: params, files: [], modelType: SubscriptionReceipt.self) { (result) in
switch result {
case .Success(let receipt):
if let receipt = receipt {
print("Receipt is: \(receipt)")
if let _ = receipt.latest_receipt, let receiptArr = receipt.latest_receipt_info {
var expiryDate: Date? = nil
for latestReceipt in receiptArr {
if let dateInMilliseconds = latestReceipt.expires_date_ms, let product_id = latestReceipt.product_id {
let date = Date(timeIntervalSince1970: dateInMilliseconds / 1000)
if date >= Date() {
// Premium is valid
}
}
}
if expiryDate == nil {
// Premium is not purchased or is expired
}
}
}
case .Error(let message):
print("Error in api is: \(message)")
}
}
}

Related

Problem with in-app purchases in apple watch Swift

I have been trying in vain for several days to make a test purchase of a subscription from the watch, but nothing works out for me, all the methods that I found have been tried, and these are: apphud, rebooting devices, logging out with Apple ID, another Apple ID, test / not test apple id, sandbox is logged in, the appstore is also logged in, the card is linked, but, unfortunately, every time I try to buy, I get an error message: "Unable to Purchase App. Sign in with your Apple ID from the Apple Watch app on your iPhone".
Moreover, when I do all the same from the phone, everything works fine, and the purchase is made.
There is an option to try revenuecat, but honestly, I don't think it will be different from apphud. There is also an assumption that it is simply IMPOSSIBLE to make a test purchase from the watch.
Here is a piece of my code from in app manager (recipes are coming):
func startProducts() {
let bundle = appBundleID
let products = Set([
bundle + ".1m",
bundle + ".1y"
])
request = SKProductsRequest(productIdentifiers: Set(products))
request?.delegate = self
request?.start()
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
guard firstRequestFinish else {
return
}
firstRequestFinish = false
defer {
finishProducts()
}
if response.invalidProductIdentifiers.count > 0 {
print("Invalid Products IDs: \(response.invalidProductIdentifiers)")
}
self.allProducts = response.products
let subscriptions = (response.products.compactMap { item in
return IAProduct(id: item.productIdentifier,
title: item.localizedTitle,
price: item.price,
localePrice: item.localizedPrice,
locale: item.priceLocale)
}.sorted { item1, item2 in
return item1.price.doubleValue < item2.price.doubleValue
})
self.availableSubscriptions = subscriptions
if subscriptions.count > 0 {
print("Available products IDs: \(subscriptions.map({ item in return item.id }))")
}
self.monthProduct = subscriptions.first { item in
return item.id.contains(".1m")
}
self.yearProduct = subscriptions.first { item in
return item.id.contains(".1y")
}
delegate?.inappWasLoaded()
}
I would be glad for any advice! Many thanks to everyone who will take part in the discussion of this issue.

Having trouble with query-based Realms

Sorry, my subject isn't very specific.
I'm dealing with managing multiple users in my realm and having some problems. I have a user register with their email and name, then when realm logs in and creates a SyncUser, I create a new YpbUser object with the email, name, and SyncUser.current.identity in YpbUser.id.
When a user logs in, I want to use their email to look up whether there's an existing YpbUser for their email. I'm doing it this way so in the future if someone uses username/password and then uses Google auth (not yet implemented), those SyncUsers can get to the same YpbUser.
Below is my code for checking for an existing YpbUser, along with my realm setup code. The problem I'm having is that it usually, but not always, fails to find a YpbUser even if that user exists (I'm looking at the YpbUser list in realm studio and the user with that email is definitely there!)
Inspecting from within existingUser(for:in:), users is usually 0, but it sometimes non-zero.
I assume the issue lies somewhere in the fact that I'm pretty much just guessing on how to use SyncSubscription.observe.
Please help?
fileprivate func openRealmWithUser(user: SyncUser) {
DispatchQueue.main.async { [weak self] in
let config = user.configuration(realmURL: RealmConstants.realmURL, fullSynchronization: false, enableSSLValidation: true, urlPrefix: nil)
self?.realm = try! Realm(configuration: config)
let songSub = self?.realm?.objects(Song.self).subscribe()
let usersSub = self?.realm?.objects(YpbUser.self).subscribe()
self?.usersToken = usersSub?.observe(\.state, options: .initial) { state in
if !(self?.proposedUser.email.isEmpty)! {
self?.findYpbUser(in: (self?.realm)!)
}
}
self?.performSegue(withIdentifier: Storyboard.LoginToNewRequestSegue, sender: nil)
}
}
fileprivate func findYpbUser(in realm: Realm) {
if proposedUser != YpbUser() { // self.proposedUser.email gets set from the login method
let existingUser = YpbUser.existingUser(for: proposedUser, in: realm)
guard existingUser == nil else { // If we find the YpbUser, set as current:
try! realm.write {
pr("YpbUser found: \(existingUser!)")
YpbUser.current = existingUser }
return
}
pr("YpbUser not found.") // i.e., SyncUser set, but no YpbUser found. i.e., creating a new YpbUser
createNewYpbUser(for: proposedUser, in: realm)
}
}
extension YpbUser {
class func existingUser (for proposedUser: YpbUser, in realm: Realm) -> YpbUser? {
let users = realm.objects(YpbUser.self)
let usersWithThisEmail = users.filter("email = %#", proposedUser.email)
if let emailUser = usersWithThisEmail.first {
return emailUser
}
return nil
}
}

Xcode passcode - swift

How do I get a system access code?
I want to block the controller in the application.
I do not want the user to create a new code but I want to use the system code.
I saw that other applications have such feature
Example
You can't get the users passcode, however you can authenticate a user and have them use their TouchID/FaceID
For this, you'll want to use the LocalAuthentication framework.
Here's an example:
let myContext = LAContext()
let myLocalizedReasonString = <#String explaining why app needs authentication#>
var authError: NSError?
if #available(iOS 8.0, macOS 10.12.1, *) {
if myContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &authError) {
myContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: myLocalizedReasonString) { success, evaluateError in
if success {
// User authenticated successfully, take appropriate action
} else {
// User did not authenticate successfully, look at error and take appropriate action
}
}
} else {
// Could not evaluate policy; look at authError and present an appropriate message to user
}
} else {
// Fallback on earlier versions
}
Credit: Apple

CKContainer.discoverAllIdentities always fails

The CKContainer.discoverAllIdentities request always fails in my CloudKit app. It has continually failed over the course of several days.
A simplified version of the code that is failing (which results in the same error) is:
private func getContacts(completion: (([CKUserIdentity]?) -> Void)?) {
container.status(forApplicationPermission: .userDiscoverability) { [weak self] status, error in
if let error = error {
print(error)
}
switch status {
case .granted:
self?.discover(completion: completion)
default:
print("status not granted")
}
}
}
private func discover(completion: (([CKUserIdentity]?) -> Void)?) {
let op = CKDiscoverAllUserIdentitiesOperation()
op.qualityOfService = .userInitiated
op.discoverAllUserIdentitiesCompletionBlock = { error in
if let error = error {
print(error)
}
}
op.userIdentityDiscoveredBlock = { identity in
print(identity)
}
op.start()
}
It results in an error being passed to the op.discoverAllUserIdentitiesCompletionBlock. The description of the error in the log is:
<CKError 0x1c4a51a60: "Server Rejected Request" (15/2000); server message = "Internal server error"; uuid = F67453B9-712D-4E5E-9335-929123E3C978; container ID = "iCloud.com.huntermaximillionmonk.topdraw">
Previously, this operation would work, but only for certain iCloud users. Now it's not for both of my test users.
Problem:
This was a problem in iOS 11.0
Based on my testing:
This works ok in Xcode 9.2 / iOS 11.2.1 on the device (not simulator)
After resetting the simulator works for the first time, doesn't work subsequently, however on the device it works repeatedly.
Code:
let queue = OperationQueue()
func requestPermissions(for permissions: CKApplicationPermissions,
completionHandler: #escaping (CKApplicationPermissionStatus, Error?) -> ()) {
CKContainer.default().requestApplicationPermission(permissions) { status, error in
if let error = error {
print("Error for requesting \(permissions) - \(error)")
}
let statusMessage : String
switch status {
case .granted:
statusMessage = "Granted"
case .denied:
statusMessage = "Denied"
case .couldNotComplete:
statusMessage = "Could not complete"
case .initialState:
statusMessage = "Initial state"
}
print("Permission - \(statusMessage)")
completionHandler(status, error)
}
}
private func discoverAllUsers() {
let operation = CKDiscoverAllUserIdentitiesOperation()
operation.userIdentityDiscoveredBlock = { userIdentity in
print("userIdentity = \(userIdentity)")
}
operation.discoverAllUserIdentitiesCompletionBlock = { error in
if let error = error {
print("Discover all users Error: \(error) ")
}
else {
print("Discover all users completed successfully")
}
}
queue.addOperation(operation)
}
Edit:
Apple fixed this issue day after this answer was posted, coincidence?! I don't think so :)
This is not actually the answer to the question, but a fix that helped me to cross over this error. It will require you to change your app UI interaction and add ContactsUI framework to your project, moreover your user will be responsible for selecting a contact with iCloud related email.
Good news is that the method discoverUserIdentity is still works. So, you can use it to get CKUserIdentity from manually selected contact.
func addContact(_ contact:CNContact) {
var lookUpEmails = [CKUserIdentityLookupInfo]()
for email in contact.emailAddresses {
lookUpEmails.append(CKUserIdentityLookupInfo(emailAddress: (email.value as String)))
}
let checkUserOperation = CKDiscoverUserIdentitiesOperation()
checkUserOperation.userIdentityLookupInfos = lookUpEmails
checkUserOperation.userIdentityDiscoveredBlock = { [unowned self] (identity, info) -> Void in
if identity.hasiCloudAccount {
if let recordID = identity.userRecordID {
//do something with discovered user
}
checkUserOperation.cancel()
}
}
checkUserOperation.queuePriority = Operation.QueuePriority.high
CKContainer.default().add(checkUserOperation)
}
It might sound useless, but in my case, it helped me to solve the Server Rejected Request" (15/2000) error, to fix one of the features of my app and continue to use the other feature related code with less efforts than I thought.
I hope someone will find this helpful.
Just another data point on this that might help with the overall picture. I was still seeing this error on 11.2.5 when I used my own iCloud AppleID (with hundreds of contacts) while running a Test App that called discoverAllIdentitiesWithCompletionHandler. I'd get the dreaded
CKError 0x1c0051730: "Server Rejected Request" (15/2000); server message = "Internal server error".
When I switched to run the exact same code on my daughters iOS11.2.5 device (with just a handful of contacts) the code worked fine.
Leads me to believe there is some rate limiting going on when there are a lot of contacts with iOS11.
(P.S. No errors at all running on iOS10)

Firebase audience based on user properties

I need to test some features of my app with just a previously selected group of users.
I created an Audience where user_id exactly matches 123456. 123456 being my own ID.
In Remote Config I created a Condition that matches users in the Audience above.
Then I created a parameter in Remote Config called feature_available and for the condition, I set a return value of true. The Default value is false.
In my app I set up Firebase:
FIRApp.configure()
let remoteConfig = FIRRemoteConfig.remoteConfig()
if let remoteConfigSettings = FIRRemoteConfigSettings(developerModeEnabled: false) {
remoteConfig.configSettings = remoteConfigSettings
}
remoteConfig.setDefaultsFromPlistFileName("FirebaseRemoteConfigDefaults")
And set the user ID:
FIRAnalytics.setUserID("123456")
Then I fetch from Firebase:
var expirationDuration: Double
// If in developer mode cacheExpiration is set to 0 so each fetch will retrieve values from the server.
expirationDuration = remoteConfig.configSettings.isDeveloperModeEnabled ? 0 : 3600
remoteConfig.fetch(withExpirationDuration: TimeInterval(expirationDuration)) { (status, error) -> Void in
if status == .success {
remoteConfig.activateFetched()
} else {
assertionFailure("Firebase config not fetched. Error \(error!.localizedDescription)")
}
}
The last thing I do is to get the value from Firebase and check if I have the feature enable:
let featureIsAvailable = remoteConfig["feature_available"].boolValue
if featureIsAvailable { ... }
The problem is that every single time the value returns from Firebase it is false and I can't manage to get it to return the correct value that matches that Audience I created.
I also tried to do it setting a user property instead of using setUserID() and had the same result.
Any suggestions?
I've run into similar issues before, sometimes it can take a while for the fetch to finish. The check if the feature is available needs to be done when the config is successfully fixed. Something like this hopefully works for you as well:
var featureIsAvailable : Bool?
override func viewDidLoad() {
super.viewDidLoad()
configureRemoteConfig()
fetchConfig()
}
func configureRemoteConfig() {
remoteConfig = FIRRemoteConfig.remoteConfig()
// Create Remote Config Setting to enable developer mode.
// Fetching configs from the server is normally limited to 5 requests per hour.
// Enabling developer mode allows many more requests to be made per hour, so developers
// can test different config values during development.
let remoteConfigSettings = FIRRemoteConfigSettings(developerModeEnabled: true)
remoteConfig.configSettings = remoteConfigSettings!
remoteConfig.setDefaultsFromPlistFileName("RemoteConfigDefaults")
}
func fetchConfig() {
var expirationDuration: Double = 3600
// If in developer mode cacheExpiration is set to 0 so each fetch will retrieve values from
// the server.
if (self.remoteConfig.configSettings.isDeveloperModeEnabled) {
expirationDuration = 0
}
// cacheExpirationSeconds is set to cacheExpiration here, indicating that any previously
// fetched and cached config would be considered expired because it would have been fetched
// more than cacheExpiration seconds ago. Thus the next fetch would go to the server unless
// throttling is in progress. The default expiration duration is 43200 (12 hours).
remoteConfig.fetch(withExpirationDuration: expirationDuration) { (status, error) in
if (status == .success) {
print("Config fetched!")
self.remoteConfig.activateFetched()
let featureIsAvailable = self.remoteConfig["feature_available"]
if (featureIsAvailable.source != .static) {
self.featureIsAvailable = featureIsAvailable.boolValue
print("should the feature be available?", featureIsAvailable!)
}
} else {
print("Config not fetched")
print("Error \(error)")
}
self.checkIfFeatureIsAvailable()
}
}
func checkIfFeatureIsAvailable() {
if featureIsAvailable == false {
// Don't show new feature
} else {
// Show new feature
}
}
I sent an email to the Firebase team requesting support and they told me that the issue was a bug in Xcode 8(.0 and .1) using Swift.
Updating to the latest version released today, 8.2, fixed the issue.