skip to Main Content

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


  1. If a user has subscribed to the app but then unsubscribed, the following code still executes `subscribe()’

    Although it’s unclear what unsubscribe() and subscribe() do in the code, let’s say you want unsubscribe() to be executed when a user no longer has access to the subscription, then it seems your condition check is wrong.

    if !customerInfo.entitlements.active.isEmpty {
        print("ENTITLEMENTS IS EMPTY") <-- This is not executed
        self.unsubscribe()
    } else {
        print("ENTITLEMENTS IS NOT EMPTY") <-- Executed
        //...
        subscribe() <-- Executed Unlocks features
    }
    

    The above code should look like:

    if customerInfo.entitlements.active.isEmpty {
        // User no longer has any active subscription
        unsubscribe()
    } else {
        subscribe()
    }
    

    I’m not sure why customerInfo.activeSubscriptions still returns values even after the subscription is expired though…

    Login or Signup to reply.
  2. 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 on entitlements object.

    When you take a loot at the implementation of that property you will see the following:

    @objc var active: [String: EntitlementInfo] {
        return self.activeInAnyEnvironment
    }
    

    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 of active aka activeInAnyEnvironment. Especially in production.

    In our app, simply for our own convenience when developing the app, we have introduced the following extension.

    extension EntitlementInfos {
        /// Returns activeInAnyEnvironment for TestFlight and DEBUG build and activeInCurrentEnvironment for AppStore version.
        var activeForConfig: [String: EntitlementInfo] {
            switch Distribution.current {
            case .testFlight, .debug: activeInAnyEnvironment
            case .appStore: activeInCurrentEnvironment
            }
        }
    }
    

    The Distribution.current value is determined based on the compiler flags and AppStore receipt data.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search