Purchase issue with Revenue Cat (SwiftUI) "The receipt is not valid" - ios

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.

Related

Why is my Google AdMob GDPR consent form not presented in my iOS app

I am in the process of implementing UMP SDK into my iOS app. I have setup the GDPR and IDFA messages in the Google AdMob dashboard's Privacy and Messaging section. I am having trouble getting the GDPR message to show up. The IDFA and iOS' ATT messages work perfectly.
Below is the code that I am using. I have tested this on both simulator and physical device. Also, I am located in the EU.
static func trackingConsentFlow(completion: #escaping () -> Void) {
let umpParams = UMPRequestParameters()
let debugSettings = UMPDebugSettings()
debugSettings.geography = UMPDebugGeography.EEA
umpParams.debugSettings = debugSettings
umpParams.tagForUnderAgeOfConsent = false
UMPConsentInformation
.sharedInstance
.requestConsentInfoUpdate(with: umpParams,
completionHandler: { error in
if error != nil {
print("MYERROR #1 \(String(describing: error))")
completion()
} else {
let formStatus = UMPConsentInformation.sharedInstance.formStatus
print("FORM STATUS: \(formStatus)")
if formStatus == .available {
loadForm(completion)
} else {
completion()
}
}
})
}
private static func loadForm(_ completion: #escaping () -> Void) {
UMPConsentForm.load(completionHandler: { form, loadError in
if loadError != nil {
print("MYERROR #2 \(String(describing: loadError))")
completion()
} else {
print("CONSENT STATUS: \(UMPConsentInformation.sharedInstance.consentStatus)")
if UMPConsentInformation
.sharedInstance.consentStatus == .required {
guard let rootViewController = UIApplication.shared.currentUIWindow()?.rootViewController else {
return completion()
}
form?.present(from: rootViewController, completionHandler: { dismissError in
if UMPConsentInformation
.sharedInstance.consentStatus == .obtained {
completion()
}
})
}
}
})
}
Just to be clear:
With this code I am able to show the IDFA message, after which the AppTrackingTransparency alert is shown. But I am expecting to also see the GDPR consent form.
For anyone wondering the same thing. The GDPR message was not appearing because I had not finalised my Admob account setup. I had not added my payment method. After adding it (and sending the app for review in Admob) the GDPR message started to appear.

Continue handling receipts after log in

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)
}
}

Check if an Auto-Renewable Subscription is still valid

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.

Login with Twitter on iOS

I have a problem with twitter login on iOS. If the user has account set in Settings application, I get the following error:
[TwitterKit] did encounter error with message "User's system account
credentials are invalid.": Error Domain=TWTRLogInErrorDomain Code=7
"The system account credentials are no longer valid and will need to
be updated in the Settings app." UserInfo={NSLocalizedDescription=The
system account credentials are no longer valid and will need to be
updated in the Settings app., NSLocalizedRecoverySuggestion=The user
has been prompted to visit the Settings app.}
And the credentials are valid, I double check that.
If there are no account in Settings app, my login goes as expected.
The method I call is:
Twitter.sharedInstance().logInWithCompletion {(session, error) -> Void in
Twitter.sharedInstance().logIn { session, error in
if (session != nil)
{
print("signed in as \(session!.userName)");
let client = TWTRAPIClient.withCurrentUser()
let request = client.urlRequest(withMethod: "GET",
url: "https://api.twitter.com/1.1/account/verify_credentials.json",
parameters: ["include_email": "true", "skip_status": "true"],
error: nil)
client.sendTwitterRequest(request)
{ response, data, connectionError in
print(response)
}
}
else
{
print("error: \(error!.localizedDescription)");
}
}
}
this helps me in swift3
I think the following method should also work.
Twitter.sharedInstance().logIn(withCompletion: {(session: TWTRSession, error: Error) -> Void in
if session {
print("#\(session.userName()) logged in! (\(session.userID()))")
}
else {
print("error: \(error!.localizedDescription)")
}
})
It takes the account credentials from the Settings Twitter app.
More to look into:
- AppDelegate.m
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]) -> Bool {
//Twitter
Twitter.sharedInstance().start(with: kTwitterConsumerKey, consumerSecret: kTwitterConsumerSecret)
Fabric.with([Twitter.sharedInstance()])
return true
}
Info.plist
Solved my problem by using method:
Twitter.sharedInstance().logInWithMethods(TWTRLoginMethod.WebBased, completion: {(session, error) -> Void in
But still think other method should work as well.
I did it like using the below code, Might be it Help
TWTRTwitter.sharedInstance().logIn(with: self, completion: { session, error in
if (session != nil) {
let client = TWTRAPIClient.withCurrentUser()
}
}

Firebase - Deleting and reinstalling app does not un-authenticate a user

After authenticating a user with the following code (below is a trimmed version of my code, so only the successful login logic is shown)...
let firebaseReference = Firebase(url: "https://MY-FIREBASE.firebaseio.com")
FBSession.openActiveSessionWithReadPermissions(["public_profile", "user_friends"], allowLoginUI: true,
completionHandler: { session, state, error in
if state == FBSessionState.Open {
let accessToken = session.accessTokenData.accessToken
firebaseReference.authWithOAuthProvider("facebook", token: accessToken,
withCompletionBlock: { error, authData in
if error != nil {
// Login failed.
} else {
// Logged in!
println("Logged in! \(authData)")
}
})
}
})
}
(I.e. Launching and running the app, logging in successfully).
If you then delete the app and reinstall it on the same device, this call - which I am using in the app delegate to determine if a user is logged in - will always return that they are logged in.
if firebaseReference.authData == nil {
// Not logged in
} else {
// Logged in
}
Why is that? I would have thought deleting the app and reinstalling it should wipe all data.
If you reset the Content and Settings in the iOS simulator, and the install the app, the firebaseReference.authData property will once again be nil.
The Firebase authentication session is persisted on the user's device in the iOS keychain. The keychain data for the application is not removed when the application is uninstalled.
If you're looking to manually clear the data, you can store some additional metadata along with your application and manually call FirebaseRef.unauth() to clear the persisted session. See #4747404: Delete keychain items when an app is uninstalled for an additional reference.
Adding below code at the end of didFinishLaunchingWithOptions function (before return true) of AppDelegate works swiftly.
Swifty way
let userDefaults = UserDefaults.standard
if userDefaults.value(forKey: "appFirstTimeOpend") == nil {
//if app is first time opened then it will be nil
userDefaults.setValue(true, forKey: "appFirstTimeOpend")
// signOut from Auth
do {
try Auth.auth().signOut()
}catch {
}
// go to beginning of app
} else {
//go to where you want
}
Swift 3.*
let userDefaults = NSUserDefaults.standardUserDefaults()
if userDefaults.valueForKey("appFirstTimeOpend") == nil {
//if app is first time opened then it will be nil
userDefaults.setValue(true, forKey: "appFirstTimeOpend")
// signOut from FIRAuth
do {
try FIRAuth.auth()?.signOut()
}catch {
}
// go to beginning of app
} else {
//go to where you want
}
For swift 4 the same Answer:
let userDefaults = UserDefaults.standard
if userDefaults.value(forKey: "appFirstTimeOpend") == nil {
//if app is first time opened then it will be nil
userDefaults.setValue(true, forKey: "appFirstTimeOpend")
// signOut from FIRAuth
do {
try Auth.auth().signOut()
}catch {
}
// go to beginning of app
} else {
//go to where you want
}
Use below extension :
extension AppDelegate{
func signOutOldUser(){
if let _ = UserDefaults.standard.value(forKey: "isNewuser"){}else{
do{
UserDefaults.standard.set(true, forKey: "isNewuser")
try Auth.auth().signOut()
}catch{}
}
}
}
and call this in '...didFinishLaunchingWithOptions...' method of Appdelegate:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FirebaseApp.configure()
signOutOldUser()
return true
}

Resources