Migrating from RevenueCat

We have prepared this guide to help you move your infrastructure seamlessly from RevenueCat to Qonversion. Although this guide is comprehensive and should enable you to transfer the entire infrastructure smoothly, feel free to reach out to support if you have any questions.

  1. Configure dashboard
  2. Install SDKs
  3. User-base migration

Configure dashboard

First and foremost, register at Qonversion. Registration will take you through an onboarding process, where you'll have to fill out information about your project and insert links β€” all to help you add your project to Qonversion. We highly recommend following this flow and not skipping any settings during registration. We'll also cover the scenario if you don't fill these out.

Store Settings

First, let's set up your Stores in the Qonversion dashboard. To do this, gather the necessary data from RevenueCat.

Apple App Store settings

To configure the Apple App Store in Qonversion, you will need the following fields:

  1. App-Specific Shared Secret: Copy this value from the App Store Connect App-specific Shared Secret field in the App Settings app settings in the RevenueCat dashboard. See the screen below. Here's a more detailed guide on how to obtain this.
  2. App Store ID: You'll find your App Store ID in the URL of your App Store Connect account. For example, if your URL is something like https://apps.apple.com/us/app/example-app/id123456789, then 123456789 is your app’s Apple ID.


Google Play Console Settings

Here's what you have in RevenueCat:


To set up your Google Play account in Qonversion, you will need the following:

  1. Android Package Name: This is the applicationID found in your app-level build.gradle file. You can either copy it from there or use the Google play package from your RevenueCat settings.
  2. Service Account Credentials JSON: attach the same file you used for Service Account Credentials JSON at RevenueCat. Here's more on how you can get Google Play Service Account Key.

πŸ‘

Configure Stores in Qonversion

Once you have all the necessary data from RevenueCat, follow the steps to set up Stores in Qonversion

Products, Entitlements and Remote Configs

After you've filled the store data, it's time to set up Offerings, Products, and Entitlements. Here's another way to describe these elements:

RevenueCatQonversion
EntitlementsEntitlements
PackagesProducts
OfferingsOfferings

Entitlements

Let's start with Entitlements. Similar to how you set up Entitlements in RevenueCat, you need to set up Entitlements in Qonversion.

Here’s how it was done in RevenueCat:


Here’s how to do it in Qonversion:

Navigate to the Entitlements and click the Create Entitlement button.


Products

Next, you need to create Products similar to how you created in RevenueCat:


To create Products in Qonversion, go to Entitlements β†’ Products and click Create Product.


  • Qonversion Product ID is your unique product identifier in Qonversion that corresponds to a unique product on the Apple App Store and Google Play Store. Qonversion SDK will use it to make purchases.

  • AppStore Product ID – product identifier on Apple App Store. Learn more on how to create subscriptions on iOS.

  • Google Play Product ID – product identifier on Google Play Console. Learn more on how to set up Android in-app products .

  • Google Play Base Plan ID - identifier of the base plan for Google Play Product. This is used for subscription products. If you're creating an in-app, leave this field empty for in-apps. In Qonversion, you will set up different products for the different base plans of the same product.

  • Associated Entitlements - choose the entitlements that should be granted once this product is purchased.

Offerings

Offerings are designed as a remote configuration to present particular products on specific paywalls. Moreover, you can change the products without additional app releases.

For example, you can offer one set of products on a paywall immediately after an onboarding flow is completed and another with discounts if a user has not converted later.

Here’s how it was done in RevenueCat:


To create Offerings in Qonversion, go to Remote Config β†’ Offerings and click Create Offering.


Server-to-server notifications

To seamlessly implement server-to-server notifications, use RevenueCat S2S URLs as a temporary proxy in our system. This allows Qonversion to process new requests while still sending notifications to your existing users.

Configure Apple App Store

  1. Navigate to your Qonversion project settings. Copy the server-to-server notification URL for Apple App Store.
  2. Sign in to App Store Connect and select your app.
  3. Navigate to the App Information section.
  1. Copy the RevenueCat URL you used for server-to-server notifications and replace it with the URL from step 1.
  2. Set up the RevenueCat URL as a Proxy URL in Qonversion in project settings.

See more information here.

Google Developer Notifications

  1. Enable access to API here.

  2. Navigate to project settings, then click the "Connect to Google" button.

  3. You need to copy generated Google Developer Notifications Topic ID.

  1. Open the Google Play Console for your application, then navigate to Monetization setup.
  2. In the Real-time developer notifications tab, insert your topic ID, click on Save changes, and then the Send Test Notification button.

It takes up to 24 hours for your Google service credentials to work correctly. Make sure to wait for at least 24 hours after you have created the credentials before connecting to Google Real-Time Developer Notifications.

See more information here.

Install SDKs

Check out the links below for the SDK you need and follow the steps provided:

β†’ iOS SDK
β†’ Android SDK
β†’ Flutter SDK
β†’ React Native SDK
β†’ Cordova Plugin
β†’ Unity SDK
β†’ Web
β†’ Kids Mode Qonversion SDKs

Read more about Installing the SDKs.

When you're done with the setup, it's time to get into the code!

Launch SDK

Firstly, you need to initialize the SDK. Here's how you did it in RevenueCat:

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

    Purchases.logLevel = .debug
    Purchases.configure(withAPIKey: <public_apple_api_key>, appUserID: <app_user_id>)

}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  	// Override point for customization after application launch.
    
  	RCPurchases.logLevel = RCLogLevelDebug;
        [RCPurchases configureWithAPIKey:<public_apple_api_key> appUserID:<app_user_id>];
    
  	return YES;
}
public class App extends Application {
  @Override
  public void onCreate() {
    super.onCreate()
    Purchases.setDebugLogsEnabled(true);
    Purchases.configure(new PurchasesConfiguration.Builder(this, <public_google_api_key>).build());
  }
}
class App : Application() {
  override fun onCreate() {
    super.onCreate()
    Purchases.debugLogsEnabled = true
    Purchases.configure(PurchasesConfiguration.Builder(this, <public_google_api_key>).build())
  }
}
import 'dart:io' show Platform;

//...

Future<void> initPlatformState() async {
  await Purchases.setDebugLogsEnabled(true);
  
  PurchasesConfiguration configuration = PurchasesConfiguration(<public_api_key>);
  await Purchases.configure(configuration);
}
Purchases.setLogLevel(Purchases.LOG_LEVEL.DEBUG);
await Purchases.configure({ apiKey: <public_api_key> });
Purchases.PurchasesConfiguration.Builder builder = Purchases.PurchasesConfiguration.Builder.Init(<public_api_key>);
Purchases.PurchasesConfiguration purchasesConfiguration =
    .SetAppUserId(<app_user_id>)
    .Build();
purchases.Configure(purchasesConfiguration);
document.addEventListener("deviceready", onDeviceReady, false);

function onDeviceReady() {
    Purchases.setDebugLogsEnabled(true);
  	Purchases.configureWith({ apiKey: <public_api_key> });
}

Here's how you'll do it for Qonversion:

import Qonversion

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    let config = Qonversion.Configuration(projectKey: "projectKey", launchMode: .subscriptionManagement)
    Qonversion.initWithConfig(config)
  	return true
}
#import "Qonversion.h"
  
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    QONConfiguration *configuration = [[QONConfiguration alloc] initWithProjectKey:@"projectKey" launchMode:QONLaunchModeSubscriptionManagement];
    [Qonversion initWithConfig:configuration];
    
    return YES;
}
import com.qonversion.android.sdk.Qonversion;
import com.qonversion.android.sdk.QonversionConfig;
import com.qonversion.android.sdk.Qonversion.dto.QLaunchMode;

public class App extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        final QonversionConfig qonversionConfig = new QonversionConfig.Builder(
                this,
                "projectKey",
                QLaunchMode.SubscriptionManagement
        ).build();
        Qonversion.initialize(qonversionConfig);
    }
}
import com.qonversion.android.sdk.Qonversion
import com.qonversion.android.sdk.QonversionConfig
import com.qonversion.android.sdk.Qonversion.dto.QLaunchMode

public class App : Application {
    override fun onCreate() {
        super.onCreate()
        val qonversionConfig = QonversionConfig.Builder(
            this,
            "projectKey",
            QLaunchMode.SubscriptionManagement
        ).build()
        Qonversion.initialize(qonversionConfig)
    }
}
import 'package:qonversion_flutter/qonversion_flutter.dart';

final config = new QonversionConfigBuilder(
  'projectKey',
  QLaunchMode.subscriptionManagement
).build();
Qonversion.initialize(config);
import Qonversion, {
  QonversionConfigBuilder,
  LaunchMode,
} from 'react-native-qonversion';

const config = new QonversionConfigBuilder(
  'projectKey',
  LaunchMode.SUBSCRIPTION_MANAGEMENT
).build();
Qonversion.initialize(config);
using QonversionUnity;

private void Start()
{
    QonversionConfig config = new QonversionConfigBuilder(
            "projectKey",
            LaunchMode.SubscriptionManagement
        ).Build();
    Qonversion.Initialize(config);
}
const config = new Qonversion.ConfigBuilder(
  'projectKey',
  Qonversion.LaunchMode.SUBSCRIPTION_MANAGEMENT,
).build();
Qonversion.initialize(config);

Displaying products

In RevenueCat you did it this way:

Purchases.shared.getOfferings { (offerings, error) in
		if let packages = offerings?.current?.availablePackages {
				self.display(packages)
		}
}
[[RCPurchases sharedPurchases] getOfferingsWithCompletion:^(RCOfferings *offerings, NSError *error) {
  if (offerings.current && offerings.current.availablePackages.count != 0) {
    // Display packages for sale
  } else if (error) {
    // optional error handling
  }
}];
Purchases.getSharedInstance().getOfferings(new ReceiveOfferingsCallback() {
  @Override
  public void onReceived(@NonNull Offerings offerings) {
    if (offerings.getCurrent() != null) {
      List<Package> availablePackages = offerings.getCurrent().getAvailablePackages();
      // Display packages for sale
    }
  }
  
  @Override
  public void onError(@NonNull PurchasesError error) {
    // An error occurred
  }
});
Purchases.sharedInstance.getOfferingsWith({ error ->
  // An error occurred
}) { offerings ->
  offerings.current?.availablePackages?.takeUnless { it.isNullOrEmpty() }?.let {
    // Display packages for sale
  }
}
try {
  Offerings offerings = await Purchases.getOfferings();
  if (offerings.current != null && offerings.current.availablePackages.isNotEmpty) {
    // Display packages for sale
  }
} on PlatformException catch (e) {
	// optional error handling
}
try {
  const offerings = await Purchases.getOfferings();
  if (offerings.current !== null && offerings.current.availablePackages.length !== 0) {
    // Display packages for sale
  }
} catch (e) {
Β 
}
var purchases = GetComponent<Purchases>();
purchases.GetOfferings((offerings, error) =>
{
  if (offerings.Current != null && offerings.Current.AvailablePackages.Count != 0){
    // Display packages for sale
  }
});
func displayUpsellScreen() {
  Purchases.getOfferings(
      offerings => {
        if (offerings.current !== null && offerings.current.availablePackages.length !== 0) {  
			    // Display packages for sale
        }
      },
      error => {

      }
  );
}

Here's how you'll do it for Qonversion:

Qonversion.shared().offerings { (offerings, error) in
  if let products = offerings?.main?.products {
    // Display products for sale
  }
}
[[Qonversion sharedInstance] offerings:^(QONOfferings * _Nullable offerings, NSError * _Nullable error) {
  if (offerings.main.products.count > 0) {
    // Display products for sale
  }
}];
Qonversion.getSharedInstance().offerings(new QonversionOfferingsCallback() {
    @Override
    public void onSuccess(@NotNull QOfferings offerings) {
        if (offerings.getMain() != null && !offerings.getMain().getProducts().isEmpty()) {
            // Display products for sale
        }
    }
    @Override
    public void onError(@NotNull QonversionError error) {
        // handle error here
    }
});
Qonversion.shared.offerings(object: QonversionOfferingsCallback {
    override fun onSuccess(offerings: QOfferings) {
        val mainOffering = offerings.main
        if (mainOffering != null && mainOffering.products.isNotEmpty()) {
            // Display products for sale
        }
    }
    override fun onError(error: QonversionError) {
        // handle error here
    }
})
try {
  final QOfferings offerings = await Qonversion.getSharedInstance().offerings();
  final List<QProduct> products = offerings.main.products;
  if (products.isNotEmpty) {
    // Display your products
  }
} catch (e) {
  print(e);
}
try {
  const offerings = await Qonversion.getSharedInstance().offerings();
  if (offerings.main != null && offerings.main.products.length > 0) {
    // Display products for sale
  }
} catch (e) {
  // handle error here
}
Qonversion.GetSharedInstance().Offerings((offerings, error) =>
{
   if (error == null)
   {
            Offering mainOffering = offerings.Main;
            if(mainOffering != null)
            {
                List<Product> products = mainOffering.Products;
                if (products.Any())
                {
                    // Display your products
                }
            }
   }
   else
   {
       // Handle the error  
       Debug.Log("Error" + error.ToString());
   }
});
try {
  const offerings = await Qonversion.getSharedInstance().offerings();
  if (offerings.main != null && offerings.main.products.length > 0) {
    // Display products for sale
  }
} catch (e) {
  // handle error here
}

See more about Offerings

Making purchases

RevenueCat:

Purchases.shared.purchase(package: package) { (transaction, customerInfo, error, userCancelled) in
  if customerInfo.entitlements["your_entitlement_id"]?.isActive == true {
    // Unlock that great "pro" content              
  }
}
[[RCPurchases sharedPurchases] purchasePackage:package withCompletion:^(RCStoreTransaction *transaction, RCCustomerInfo *customerInfo, NSError *error, BOOL cancelled) {
  if (customerInfo.entitlements[@"your_entitlement_id"].isActive) {
    // Unlock that great "pro" content
  }
}];
Purchases.getSharedInstance().purchase(
	new PurchaseParams.Builder(this, aPackage).build(), 
	new PurchaseCallback() {
    @Override
    public void onCompleted(@NonNull StoreTransaction storeTransaction, @NonNull CustomerInfo customerInfo) {
			if (customerInfo.getEntitlements().get(<my_entitlement_identifier>).isActive()) {
				// Unlock that great "pro" content
			}
		}

		@Override
		public void onError(@NonNull PurchasesError purchasesError, boolean b) {
			// No purchase
		}
	}
);
Purchases.sharedInstance.purchaseWith(
  PurchaseParams.Builder(this, aPackage).build(),
  onError = { error, userCancelled -> /* No purchase */ },
  onSuccess = { storeTransaction, customerInfo ->
    if (customerInfo.entitlements["my_entitlement_identifier"]?.isActive == true) {
      // Unlock that great "pro" content
    }
  }
)
try {
  CustomerInfo customerInfo = await Purchases.purchasePackage(package);
  if (customerInfo.entitlements.all["my_entitlement_identifier"].isActive) {
    // Unlock that great "pro" content
  }
} on PlatformException catch (e) {
  var errorCode = PurchasesErrorHelper.getErrorCode(e);
  if (errorCode != PurchasesErrorCode.purchaseCancelledError) {
    showError(e);  	          
  }
}
try {
  const { customerInfo } = await Purchases.purchasePackage(package);
  if (
    typeof customerInfo.entitlements.active["my_entitlement_identifier"] !==
    "undefined"
  ) {
    // Unlock that great "pro" content
  }
} catch (e) {
  if (!e.userCancelled) {
    showError(e);
  }
}
Purchases purchases = GetComponent<Purchases>();
purchases.PurchasePackage(package, (productIdentifier, customerInfo, userCancelled, error) =>
{
  if (customerInfo.Entitlements.Active.ContainsKey("my_entitlement_identifier")) {
    // Unlock that great "pro" content
  }
});

Qonversion:

Purchases.purchasePackage(package, ({ productIdentifier, customerInfo }) => {
    if (typeof customerInfo.entitlements.active.my_entitlement_identifier !== "undefined") {
      // Unlock that great "pro" content
    }
  },
  ({error, userCancelled}) => {
    // Error making purchase
  }
);
[[Qonversion sharedInstance] purchaseProduct:product completion:^(NSDictionary<NSString *,QONEntitlement *> * _Nonnull entitlements,
                                                                  NSError * _Nullable error,
                                                                  BOOL cancelled) {
  QONEntitlement *premiumEntitlement = entitlements[@"premium"];
  
  if (premiumEntitlement && premiumEntitlement.isActive) {
    // Flow for success state
  }
}];
final QPurchaseModel purchaseModel = product.toPurchaseModel();
Qonversion.getSharedInstance().purchase(this, purchaseModel, new QonversionEntitlementsCallback() {
    @Override
    public void onSuccess(@NotNull Map<String, QEntitlement> entitlements) {
        QEntitlement premiumEntitlement = entitlements.get("premium");

        if (premiumEntitlement != null && premiumEntitlement.isActive()) {
            // Handle active entitlement here
        }
    }

    @Override
    public void onError(@NotNull QonversionError error) {
        // Handle error here
        if (error.getCode() == QonversionErrorCode.CanceledPurchase) {
            // Purchase canceled by the user  
        }
    }
});
val purchaseModel = product.toPurchaseModel()
Qonversion.shared.purchase(this, purchaseModel, callback = object: QonversionEntitlementsCallback {
    override fun onSuccess(entitlements: Map<String, QEntitlement>) {
        val premiumEntitlement = entitlements["premium"]
        if (premiumEntitlement != null && premiumEntitlement.isActive) {
            // Handle active entitlement here
        }
    }

    override fun onError(error: QonversionError) {
        // Handle error here
        if (error.code === QonversionErrorCode.CanceledPurchase) {
            // Purchase canceled by the user
        }
    }
})
try {
  final QPurchaseModel purchaseModel = product.toPurchaseModel();
  final Map<String, QEntitlement> entitlements = await Qonversion.getSharedInstance().purchase(purchaseModel);
} on QPurchaseException catch (e) {
  if (e.isUserCancelled) {
    // Purchase canceled by the user
  }
  print(e);
}
try {
  const purchaseModel: PurchaseModel = product.toPurchaseModel();
  const entitlements: Map<string, Entitlement> = await Qonversion.getSharedInstance().purchase(purchaseModel);
} catch (e) {
  if (e.userCanceled) {
    // Purchase canceled by the user
  }
  console.log(e);
}
PurchaseModel purchaseModel = product.ToPurchaseModel();
Qonversion.GetSharedInstance().Purchase(purchaseModel, (entitlements, error, isCancelled) =>
{
      if (error == null)
      {
          if (entitlements.TryGetValue("premium", out Entitlement premium) && premium.IsActive)
          {
             // Handle the active entitlement here
          }
      }
      else
      {
        // Handle the error  
        Debug.Log("Error" + error.ToString());
      }
});
try {
  const purchaseModel: PurchaseModel = product.toPurchaseModel();
  const entitlements = await Qonversion.getSharedInstance().purchase(purchaseModel);
} catch (e) {
  if (e.userCanceled) {
    // Purchase canceled by the user
  }
  console.log(e);
}

Restore purchases

If your app has a restore purchases feature on paywalls, just use the Qonversion restore() function when users want to restore their purchases.

Qonversion.shared().restore { [weak self] (entitlements, error) in
  if let error = error {
    // Handle error
  }
  
  if let entitlement: Qonversion.Entitlement = entitlements["plus"], entitlement.isActive {
    // Restored and entitlement is active 
  }

[[Qonversion sharedInstance] restore:^(NSDictionary<NSString *, QONEntitlement *> * _Nonnull result, NSError * _Nullable error) {
  if (error) {
    // Handle error
  }
  QONEntitlement *entitlement = result[@"active"];
  if (entitlement && entitlement.isActive) {
    // Restored and entitlement is active
  }
}];
Qonversion.getSharedInstance().restore(new QonversionEntitlementsCallback() {
    @Override
    public void onSuccess(@NotNull Map<String, QEntitlement> entitlements) {
        QEntitlement premiumEntitlement = entitlements.get("premium");

        if (premiumEntitlement != null && premiumEntitlement.isActive()) {
            // handle active entitlement here
        }
    }

    @Override
    public void onError(@NotNull QonversionError error) {
        // handle error here
    }
});
Qonversion.shared.restore(object : QonversionEntitlementsCallback {
    override fun onSuccess(entitlements: Map<String, QEntitlement>) {
        val premiumEntitlement = entitlements["premium"]
        if (premiumEntitlement != null && premiumEntitlement.isActive) {
            // handle active entitlement here
        }
    }

    override fun onError(error: QonversionError) {
        // handle error here
    }
})
try {
  final Map<String, QEntitlement> entitlements = await Qonversion.getSharedInstance().restore();
} catch (e) {
  print(e);
}
try {
  const entitlements: Map<string, Entitlement> = await Qonversion.getSharedInstance().restore();
 } catch (e) {
  console.log(e);
}
Qonversion.GetSharedInstance().Restore((entitlements, error) =>
{
      if (error == null)
      {
         // Handle entitlements here
      }
      else
      {
        // Handle the error  
        Debug.Log("Error" + error.ToString());
      }
});
try {
    const entitlements = await Qonversion.getSharedInstance().restore();
} catch (e) {
    console.log(e);
}

User status

To check the customer status in Qonversion, you need to use Entitlements. In RevenueCat, you Customer info and entitlements.

RevenueCat:

Purchases.shared.getCustomerInfo { (customerInfo, error) in
    // access latest customerInfo
		if customerInfo.entitlements[<your_entitlement_id>]?.isActive == true {
  			// user has access to "your_entitlement_id"                
    }
}

[[RCPurchases sharedPurchases] customerInfoWithCompletion:^(RCCustomerInfo * customerInfo, NSError * error) {
    // access latest customerInfo
  	if (customerInfo.entitlements[@<your_entitlement_id>].isActive) {
 				// user has access to "your_entitlement_id"
		}
}];
Purchases.sharedInstance.getCustomerInfo({ error -> /* Optional error handling */ }) { customerInfo ->
	  // access latest customerInfo
	  if (customerInfo.entitlements[<your_entitlement_id>]?.isActive == true) {
				// user has access to "your_entitlement_id"                
		}
}
Purchases.getSharedInstance().getCustomerInfo(new ReceiveCustomerInfoCallback() {
	  @Override
  	public void onReceived(@NonNull CustomerInfo customerInfo) {
    		// access latest customerInfo
		    if (customerInfo.getEntitlements().get(<your_entitlement_id>).isActive()) {
						// user has access to "your_entitlement_id"
				}
	  }
  
    @Override
    public void onError(@NonNull PurchasesError error) {

    }
});
try {
  CustomerInfo customerInfo = await Purchases.getCustomerInfo();
  // access latest customerInfo
  if (customerInfo.entitlements.all[<my_entitlement_identifier>].isActive) {
    // Grant user "pro" access
  }
} on PlatformException catch (e) {
  // Error fetching customer info
}
try {
  const customerInfo = await Purchases.getCustomerInfo();
  // access latest customerInfo
  if(typeof customerInfo.entitlements.active[<my_entitlement_identifier>] !== "undefined") {
	  // Grant user "pro" access
	}
} catch (e) {
 // Error fetching customer info
}
var purchases = GetComponent<Purchases>();
purchases.GetCustomerInfo((customerInfo, error) =>
{
  // access latest customerInfo
  if (customerInfo.Entitlements.Active.ContainsKey(<my_entitlement_identifier>)) {
  	// Unlock that great "pro" content
	}
});
Purchases.getCustomerInfo(
  customerInfo => {
    // access latest customerInfo
    if(typeof customerInfo.entitlements.active[<my_entitlement_identifier>] !== "undefined") {
			  // Grant user "pro" access
		}
  },
  error => {
    // Error fetching customer info
  }
);

Qonversion:

Qonversion.shared().checkEntitlements { (entitlements, error) in
  if let error = error {
    // handle error
    return
  }
  
  if let premium: Qonversion.Entitlement = entitlements["premium"], premium.isActive {
    // unlock feature
  }
}
[[Qonversion sharedInstance] checkEntitlements:^(NSDictionary<NSString *, QONEntitlement *> * _Nonnull entitlements,
                                NSError * _Nullable error) {
    QONEntitlement *premiumEntitlement = entitlements[@"premium"];
    if (premiumEntitlement && premiumEntitlement.isActive) {
      // unlock feature
    }
}];
Qonversion.getSharedInstance().checkEntitlements(new QonversionEntitlementsCallback() {
    @Override
    public void onSuccess(@NotNull Map<String, QEntitlement> entitlements) {
        final QEntitlement premiumEntitlement = entitlements.get("premium");

        if (premiumEntitlement != null && premiumEntitlement.isActive()) {
            // unlock feature
        }
    }

    @Override
    public void onError(@NotNull QonversionError error) {
        // handle error here
    }
});
Qonversion.shared.checkEntitlements(object: QonversionEntitlementsCallback {
    override fun onSuccess(entitlements: Map<String, QEntitlement>) {
        val premiumEntitlement = entitlements["premium"]
        if (premiumEntitlement != null && premiumEntitlement.isActive) {
            // unlock feature
        }
    }

    override fun onError(error: QonversionError) {
        // handle error here
    }
})
try {
  final Map<String, QEntitlement> entitlements = await Qonversion.getSharedInstance().checkEntitlements();
  final premium = entitlements['premium'];
  if (premium != null && premium.isActive) {
    // unlock feature
  }
} catch (e) {
  print(e);
}
try {
  const entitlements = await Qonversion.getSharedInstance().checkEntitlements();

  const premiumEntitlement = entitlements.get('premium');
  if (premiumEntitlement != null) {
    // unlock feature
  }
} catch (e) {
  // handle error here
}
Qonversion.GetSharedInstance().CheckEntitlements((entitlements, error) =>
{
    if (error == null)
    {
        if (entitlements.TryGetValue("premium", out Entitlement premium) && premium.IsActive)
        {
            // unlock feature
        }
    }
    else
    {
        // Handle the error  
        Debug.Log("Error" + error.ToString());
    }
});
try {
    const entitlements = await Qonversion.getSharedInstance().checkEntitlements();

    const premiumEntitlement = entitlements.get('premium');
    if (premiumEntitlement != null) {
        // unlock feature
    }
} catch (e) {
    // handle error here
}

Authorization system

πŸ“˜

Cross-device and cross-platform access

Set up this section only if you need cross-device and cross-platform access and your project includes an authorization system.

Qonversion lets you identify your signed-in users and unlock premium access across multiple devices. Use the identify() method to link a user to your signed-in subscriber. Call this method every time you want to use User Identity. For example, when a user logs in.
User Identity provides a convenient way of managing premium access of your existing subscribers, including the following cases:

  • A user reinstalls your app for any reason. Using the same User ID allows you to provide premium access linked to previously purchased products.
  • A user logs in on several devices. You can provide premium access based on a subscription purchased on one of his devices.
  • A user logs in on iOS and Android versions of your app. You can provide premium access based on a subscription purchased on one of the platforms.

Logging in

When a user logs into his account, call identify().

Qonversion.shared().identify("your_custom_user_id")

// or the following option, if you want to get notified about the result.

Qonversion.shared().identify("your_custom_user_id") { user, error in
  // use user if necessary
}
[[Qonversion sharedInstance] identify:@"your_custom_user_id"];

// or the following option, if you want to get notified about the result.

[[Qonversion sharedInstance] identify:@"your_custom_user_id" completion:^(QONUser * _Nullable user, NSError * _Nullable error) {
  // use user if necessary
}];
Qonversion.getSharedInstance().identify("your_custom_user_id");

// or the following option, if you want to get notified about the result.

Qonversion.getSharedInstance().identify("your_custom_user_id", new QonversionUserCallback() {
    @Override
    public void onSuccess(@NonNull QUser user) {
        // use user if necessary
    }

    @Override
    public void onError(@NonNull QonversionError error) {
        // handle error here
    }
});
Qonversion.shared.identify("your_custom_user_id")

// or the following option, if you want to get notified about the result.

Qonversion.shared.identify("your_custom_user_id", object : QonversionUserCallback {
    override fun onSuccess(user: QUser) {
        // use user if necessary
    }

    override fun onError(error: QonversionError) {
        // handle error here
    }
})
try {
  final userInfo = await Qonversion.getSharedInstance().identify("your_custom_user_id");
  // use userInfo if necessary
} catch (e) {
  // handle error here
}
try {
  const userInfo = await Qonversion.getSharedInstance().identify('your_custom_user_id');
  // use userInfo if necessary
} catch (e) {
  // handle error here
}
Qonversion.GetSharedInstance().Identify("your_custom_user_id")

// or the following option, if you want to get notified about the result.

Qonversion.GetSharedInstance().Identify("your_custom_user_id", (userInfo, error) =>
{
    if (error == null)
    {
        // use userInfo if necessary
    }
    else
    {
        // Handle the error  
        Debug.Log("Error" + error.ToString());
    }
});
try {
  const userInfo = await Qonversion.getSharedInstance().identify('your_custom_user_id');
  // use userInfo if necessary
} catch (e) {
  // handle error here
}
curl --location --request POST 'https://api.qonversion.io/v3/identities/new_identities_user' \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data-raw '{
    "user_id": "QON_38a2d811afd54a433587620f8696266e"
}'

{
    "id": "new_identities_user",
    "user_id": "QON_38a2d811afd54a433587620f8696266e"
}

🚧

Use the unique user ID stored in your database

Always use unique user ID values. Otherwise, a user can get matched to another user's entitlements status.

To check entitlements for identified users, you can call the checkEntitlements function, which we discussed above.

Logging out

You need to call the logout() method to handle entitlements for an unauthorized user.
Call this method when a user logs out within your app:

Qonversion.shared().logout()
[[Qonversion sharedInstance] logout];
Qonversion.getSharedInstance().logout();
Qonversion.shared.logout()
Qonversion.getSharedInstance().logout();
Qonversion.getSharedInstance().logout();
Qonversion.GetSharedInstance().Logout();
Qonversion.getSharedInstance().logout();

When a user logs back into his account, don't forget to use identify() method again.

User-base migration

Those steps have to be done to keep the system working smoothly for your current user base. The necessity of steps depends on the complexity of your system. The simplest system is represented by a single platform infrastructure (Apple, Google, or Stripe). The complex one includes a few platforms with authorization system, your backend infrastructure, and WebHooks handler.

Client-side migration

Follow these steps to sync user status and retrieve the latest data from Google and Apple on your current device. Π‘all the syncHistoricalData() method right after Qonversion SDK initialization to synchronize all the device-related data and ensure that none of the entitlements has been missed.

User-database migration

Once you have the app version with Qonversion SDK up and running, it's time to proceed to the server-side migration.

  1. Prepare files with AppStore with Base64 encoded receipt data or Google Play with purchase tokens.
    If you do not have receipt data or purchase tokens on your side but are already running your subscription management with a third-party vendor, you should ask the vendor’s support team for the required data file.
  2. Then simply share the files with us using our support chat and provide CSV files.

Release

Once you're done with these steps go ahead and release your app. For complex systems with cross-platform access and authorization setups, make sure you're in touch with us and our team has verified your user database migration.

πŸ‘

You're good to go

Check out the detailed RevenueCat and Qonversion feature comparison and don't hesitate to contact us if you have questions.