This is a production bug where users who have previously been subscribed still have access to all Pro features due to customerInfo.activeSubscriptions
returning all productIdentifiers
of previous subscriptions.
Also customerInfo.entitlements
always returns value.
I have added
print("ENTITLEMENTS (customerInfo.entitlements)")
print("ACTIVE SUBS (customerInfo.activeSubscriptions)")
This returns
ENTITLEMENTS ["pro": <EntitlementInfo: "
ACTIVE SUBS ["com.appname.com.promonthly", "com.appname.com.proyearly"]
I have tried using
customerInfo.entitlements.active
But I get the same result.
I have also tried removing cached info with
Purchases.shared.invalidateCustomerInfoCache()
Also when opening Debug > StoreKit, not transactions are show.
This is the code to verify the subscription status.
func verifyIAPReceipt() {
Purchases.shared.invalidateCustomerInfoCache()
Purchases.shared.getCustomerInfo { (customerInfo, error) in
if error == nil {
if let customerInfo = customerInfo {
if customerInfo.entitlements.active.isEmpty {
print("ENTITLEMENTS IS EMPTY") <-- This is not executed
self.unsubscribe()
} else {
print("ENTITLEMENTS IS NOT EMPTY") <-- Executed
print("ENTITLEMENTS (customerInfo.entitlements)")
print("ACTIVE SUBS (customerInfo.activeSubscriptions)")
if customerInfo.activeSubscriptions.isEmpty == false {
print("Active Subscriptions is NOT empty") <-- Executed
for s in customerInfo.activeSubscriptions {
if s == "com.app.come.promonthly" || s == "com.app.com.proyearly" {
subscribe() <-- Executed Unlocks features
}
}
} else if customerInfo.activeSubscriptions.isEmpty == true {
print("Active Subscriptions are empty")
self.unsubscribe()
} else {
print("Active Subscription and profile is subscribed")
}
}
}
} else {
print("ERROR GETTING CUSTOMER INFO TO VERIFY RECEIPTS")
}
}
}
When using fetchPolicy: .fetchCurrent
it is returning active subscriptions
Purchases.shared.getCustomerInfo(fetchPolicy: .fetchCurrent, completion: { (customerInfo, error) in
if error != nil {
print("FETCH POLICY ERROR:(error)")
}
if customerInfo != nil {
print("FETCH POLICY ACTIVE SUBSCRIPTIONS:(customerInfo!.activeSubscriptions)")
print("FETCH POLICY ACTIVE ENTITLEMENTS:(customerInfo!.entitlements)")
print("FETCH POLICY ACTIVE ALL PURCHASED PRODUCT ID'S:(customerInfo!.allPurchasedProductIdentifiers)")
}
})
Output
FETCH POLICY ACTIVE ENTITLEMENTS:["pro": <EntitlementInfo: "
FETCH POLICY ACTIVE ENTITLEMENTS:<RCEntitlementInfos: self.all=[:],
self.active=[:],self.verification=VerificationResult.notRequested>
FETCH POLICY ACTIVE ALL PURCHASED PRODUCT
ID’S:["com.appname.com.promonthly", "com.appname.com.proyearly"]
This is the subscribe code
func subscribe() {
print("Subscribe")
//Revenue Cat
if let packages = offering?.availablePackages {
for p in packages {
if p.storeProduct.productIdentifier == selectedproductbundle {
Purchases.shared.purchase(package: p) { (transaction, customerInfo, error, userCancelled) in
print("PURCHASE")
if userCancelled {
print("User cancelled purchase")
return
}
if let err = error {
if let error = error as? RevenueCat.ErrorCode {
print(error.errorCode)
print("ERROR: (error.errorUserInfo)")
switch error {
case .purchaseNotAllowedError:
errorDescription = "Purchases not allowed on this device."
showError.toggle()
case .purchaseInvalidError:
errorDescription = "Purchase invalid, check payment source."
default: break
}
}
} else if customerInfo?.activeSubscriptions.isEmpty == false {
print("Unlocked Pro 🎉")
// Update profile
print("Customer INFO: (customerInfo!)")
print("Entitlements: (customerInfo!.entitlements.all)")
if customerInfo != nil {
for s in customerInfo!.activeSubscriptions {
if s == "com.appname.com.promonthly" || s == "com.appname.com.proyearly" {
subscribeToPro()
}
}
}
} else {
print("PURCHASE WITH: (String(describing: transaction?.productIdentifier)) && (String(describing: customerInfo?.activeSubscriptions.count))")
}
}
}
}
}
}
I have tested via simulator and on device.
2
Answers
Although it’s unclear what
unsubscribe()
andsubscribe()
do in the code, let’s say you wantunsubscribe()
to be executed when a user no longer has access to the subscription, then it seems your condition check is wrong.The above code should look like:
I’m not sure why
customerInfo.activeSubscriptions
still returns values even after the subscription is expired though…Assuming you have your entitlements configured correctly in the RevenueCat dashboard, they should be always returning correct data.
One thing that have cought my eye in your code is that you are trying to call the
.active
property onentitlements
object.When you take a loot at the implementation of that property you will see the following:
Which means that any user that was previously part of the TestFlight group and was able to do any purchase in the sandbox, could still be granted that entitlement.
This is not safe approach if you really want to support just the customers that have actually purchased the subscription.
To solution here is to use the
activeInCurrentEnvironment
exclusively – instead ofactive
akaactiveInAnyEnvironment
. Especially in production.In our app, simply for our own convenience when developing the app, we have introduced the following extension.
The
Distribution.current
value is determined based on the compiler flags and AppStore receipt data.