Identifying Users

This section covers subscription transfer and event handling during user authentication.

Overview

Accurately identifying users within your application is essential for delivering personalized experiences and managing user-specific data. The Purchasely SDK provides robust tools for user identification.

A subscription made with Apple or Google must be linked to an identifier as neither Apple or Google provides the information about the user account. Purchasely only has access to the purchase receipt and the transaction id. To be able to inform your application if the device has an active subscription, Purchasely must link the subscription to an identifier like a user id (provided by you) or an anonymous user id (created by Purchasely)

Anonymous users

Why Using Anonymous User Identification?

In scenarios where user registration isn't required (e.g., news apps), and you wish to offer in-app purchases, Apple mandates that users should not be forced to register to subscribe. Consequently, managing purchases made by anonymous users becomes necessary.

Purchasely helps you combine the power of Server to Server notifications (S2S) with anonymous purchases.

🚧

Exception for consumables and non-consumables

Note that consumable and non-consumable items cannot be transferred from an anonymous user to an authenticated user.

Handling anonymous purchasing

The Purchasely SDK automatically generates and assigns an anonymous_user_id to each user, maintaining consistency as long as the app remains installed on the device.

  1. Purchase Initiation: When an anonymous user makes a purchase, the Purchasely SDK generates an anonymous_user_id.
  2. Webhook Event: After validating the store receipt, an event containing the anonymous_user_id is sent to the webhook.
  3. Database Association: The association between the anonymous_user_id and the Product & Plan purchased must be stored in your own database.
  4. SDK Access: Use the SDK method to retrieve the anonymous_user_id and verify the user's entitlements in your backend.

1. Proceeding with an anonymous Purchase

If no UserId has been attached to the user by the app to Purchasely SDK, the user will be treated as an anonymous user.

When an anonymous user makes an In-App Purchase, the platform will automatically associate it to the anonymous_user_id for the user. This anonymous_user_id is tied to this particular device and remains consistent as long as the app remains installed on the device and will be the key for your backend to authorize the purchase on the device.

2. Getting the anonymous_user_id from the webhook

When an event occurs on a subscription made by an anonymous user, the webhook message carries the user anonymous_user_id generated by Purchasely.

{
  "api_version": 3,
  "content_id": "<content id you provided through the sdk>",
  "environment": "SANDBOX",
  "event_created_at": "2021-11-22T09:23:38.559Z",
  "event_created_at_ms": 1637573018559,
  "event_name": "ACTIVATE",
  "is_family_shared": false,
  "offer_type": "NONE",
  "original_purchased_at": "2021-11-22T09:23:36.000Z",
  "original_purchased_at_ms": 1637573016000,
  "plan": "<plan vendorID defined in the Purchasely console>",
  "product": "<product vendorID define in the Purchasely console>",
  "purchased_at": "2021-11-22T09:23:36.000Z",
  "purchased_at_ms": 1637573016000,
  "purchasely_one_time_purchase_id": "otp_XXXXXXXFFFFFFFFF",
  "store": "APPLE_APP_STORE",
  "store_app_bundle_id": "<app bundle id defined in the store console>",
  "store_country": "US",
  "store_original_transaction_id": "100000099999999",
  "store_product_id": "<store product id defined in the store console>",
  "store_transaction_id": "100000099999999",
  "anonymous_user_id": "<anonymous user id generated by Purchasely>"
}

3. Attaching the purchase to the anonymous user

Purchases and corresponding entitlements must be attached to the anonymous user and stored in your database, using the anonymous_user_id.

Every subsequent purchase or event for this particular user will carry the same anonymous_user_id. In other words, when a particular event (renewal / cancellation / expiration etc...) happens on an anonymous user subscription, it will carry the same anonymous_user_id.

4. Retrieve user entitlements & user_anonymous_id in the app

Entitlements are managed by the developer's backend and directly attached to the anonymous_user_id. Your app can retrieve the anonymous_user_id by calling the following method of the SDK :

Purchasely.anonymousUserId
Purchasely.anonymousUserId
Purchasely.getAnonymousUserId();
Purchasely.anonymousUserId;
Purchasely.getAnonymousUserId((anonymousId) => {
	console.log("Purchasely anonymous Id: " + anonymousId);
});
private PurchaselyRuntime.Purchasely _purchasely;

_purchasely.GetAnonymousUserId();

🚧

Enhanced operational efficiency and Support

To ease customer support, we advise you to display both anonymous_user_id and user_id inside your app (e.g. in the section "My Subscriptions", in the settings or in the contact-us mail). This will help you identify the user in the Purchasely Console or in your own logs.

📘

Unlock content from Subscription Status

If you don't want to manage anonymous users you could use our mobile API to check subscription status and unlock the content locally.

Authenticate users

To authenticate an anonymous user, just provides your user id. Purchasely will save this user id for all sessions moving forward until you call Purchasely.userLogout() or the user uninstall the application.
You can call the following method whenever you want and as much as you want, no network connection is required, the user id is saved directly if the SDK detect it has changed.

Purchasely.userLogin(with: "123456789")
Purchasely.userLogin("123456789")
Purchasely.userLogin('123456789');
Purchasely.userLogin('123456789');
Purchasely.userLogin("123456789", (shouldRefresh) => {
	if (shouldRefresh) {
		// You should call your backend to refresh user entitlements
	}
});
Purchasely.UserLogin("123456789", (shouldRefresh) => {
	if (shouldRefresh) {
		// You should call your backend to refresh user entitlements
	}
});

Authenticate from Purchasely Screen

Every presentation, has a Already subscribed? Sign-in button to let your customers connect to unlock a feature / access a content.
This button is displayed if you did not set a user id with Purchasely.userLogin(). see SDK implementation for further information.

To intercept this event, you can setup a global handler that passes you the source paywall controller / fragment above which to display login.

  • IF the user signs in AND has a subscription, you can dismiss the paywall controller
  • IF the user signs in AND doesn't have a subscription, dismiss your login controller and notify the SDK by calling the isLoggedIn closure with true
  • IF the user cancels sign-in, dismiss your login controller and notify the SDK by calling the isLoggedIn closure with false
Purchasely.setPaywallActionsInterceptor { [weak self] (action, parameters, presentationInfo, proceed) in

	switch action {

	// Intercept the tap on login
	case .login:
		// When the user has completed the process
		// Pass true to reload the paywall or dismiss the paywall if the user already has an active subscription
		self?.presentLogin(above: presentationInfo?.controller) { (loggedIn) in
			proceed(loggedIn)
		}
	default:
		proceed(true)
		break
	}
}
Purchasely.setPaywallActionsInterceptor { info, action, parameters, processAction ->
    if (info?.activity == null) return@setPaywallActionsInterceptor

    when(action) {
        PLYPresentationAction.LOGIN -> {
            // Call your method to display your view 
            // and return boolean result to userLoggedIn
            presentLogin(info.activity) { userLoggedIn ->
    		// Don't forget to notify the SDK by calling `processAction`
    		processAction(userLoggedIn)
            }
        }
        else -> {
            Log.d("PLYActionInterceptor", action.value + " " + parameters)
            processAction(true)
        }
    }
}
Purchasely.setPaywallActionInterceptorCallback((result) => {
    if (result.action === PLYPaywallAction.LOGIN) {
      console.log('User wants to login');
      //Present your own screen for user to log in
      Purchasely.closePaywall();
      Purchasely.userLogin('MY_USER_ID');
      //Call this method to update Purchasely Paywall
      Purchasely.onProcessAction(true);
    } else {
      Purchasely.onProcessAction(true);
    }
});
Purchasely.setPaywallActionInterceptorCallback(
          (PaywallActionInterceptorResult result) {
    if (result.action == PLYPaywallAction.login) {
      print('User wants to login');
      //Present your own screen for user to log in
      Purchasely.closePaywall();
      Purchasely.userLogin('MY_USER_ID');
      //Call this method to update Purchasely Paywall
      Purchasely.onProcessAction(true);
    } else {
      Purchasely.onProcessAction(true);
    }
 });
Purchasely.setPaywallActionInterceptorCallback((result) => {
   if (result.action === PLYPaywallAction.LOGIN) {
      console.log('User wants to login');
      //Present your own screen for user to log in
      Purchasely.closePaywall();
      Purchasely.userLogin('MY_USER_ID');
      //Call this method to update Purchasely Paywall
      Purchasely.onProcessAction(true);
    } else {
      Purchasely.onProcessAction(true);
    }
  });
private PurchaselyRuntime.Purchasely _purchasely;

...
_purchasely.SetPaywallActionInterceptor(OnPaywallActionIntercepted);
...

private void OnPaywallActionIntercepted(PaywallAction action)
{
    Log($"Purchasely Paywall Action Intercepted. Action: {action.action}.");
}

🚧

Update your Screen

Do not forget to call Purchasely.userLogin("YOUR_USER_ID") before returning the result to Purchasely to properly update the presentation without the sign-in button.

Transferring Subscriptions

The process can be automatic or manual, depending on your application's requirements.

Automatic Transfer

The Purchasely SDK can automatically transfer subscriptions from an anonymous user to an authenticated user if the purchase was made with Purchasely and the user did not re-install the application before logging in. That implies sending events to your backend and wait for it to confirm before you try to refresh your entitlements. This is why you have a closure that passes a boolean telling you if the entitlements of the logged in user should be refreshed.

When calling the Purchasely.userLogin method, a callback informs you if a transfer was made and if you should update user entitlements.

Purchasely.userLogin(with: "123456789") { (shouldRefreshCredentials) in
    if (shouldRefreshCredentials) {
        // You should call your backend to refresh user entitlements
    }
}
Purchasely.userLogin("123456789") { refresh ->
    if (refresh) {
        // You should call your backend to refresh user entitlements
    }
}
Purchasely.userLogin('123456789').then((refresh) => {
  if (refresh) {
    // You should call your backend to refresh user entitlements
  }
});
Purchasely.userLogin('123456789').then((refresh) => {
  if (refresh) {
    //call your backend to refresh user information
  }
});
Purchasely.userLogin("123456789", (shouldRefresh) => {
	if (shouldRefresh) {
		// You should call your backend to refresh user entitlements
	}
});
Purchasely.UserLogin("123456789", (shouldRefresh) => {
	if (shouldRefresh) {
		// You should call your backend to refresh user entitlements
	}
});

Manual Transfer

If you need to manually, from your code based on your own logic, transfer a subscription from an anonymous user or user A to an authenticated user B, you must call the Purchasely.synchronize() method.

To provide users with the option to retrieve their subscription, you must set up a button in your application that triggers the Purchasely.restoreAllPurchases() method.

Both methods work the same way, the main difference is that synchronize() will not trigger any Apple pop-in for user to login and is an automatic process done by your application.
restoreAllPurchasely() may trigger an Apple pop-in and should only be called from a user action.

Webhook events

When a subscription transfer occurs, your webhook will receive two transactional events: an ACTIVATE event for the connected user and a DEACTIVATE event for the anonymous user.

The webhook and integrations you activated will also receive 2 events, a SUBSCRIPTION_RECEIVED for the connected user and a SUBSCRIPTION_TRANSFERRED for the anonymous user.

{
  "plan": "<plan vendorID defined in the Purchasely console>",
  "store": "APPLE_APP_STORE",
  "product": "<product vendorID define in the Purchasely console>",
  "anonymous_user_id": "<anonymous user id originally generated by Purchasely>",
  "event_name": "DEACTIVATE",
  "offer_type": "NONE",
  "api_version": 3,
  "environment": "SANDBOX",
  "purchased_at": "2021-11-07T17:44:17.000Z",
  "store_country": "FR",
  "next_renewal_at": "2021-11-07T17:47:17.000Z",
  "purchased_at_ms": 1636307057000,
  "event_created_at": "2021-11-07T17:43:35.225Z",
  "is_family_shared": false,
  "store_product_id": "<store product id defined in the store console>",
  "next_renewal_at_ms": 1636307237000,
  "event_created_at_ms": 1636307015225,
  "previous_offer_type": "NONE",
  "store_app_bundle_id": "<app bundle id defined in the store console>",
  "subscription_status": "AUTO_RENEWING",
  "store_transaction_id": "100000099999999",
  "original_purchased_at": "2021-11-07T17:41:18.000Z",
  "original_purchased_at_ms": 1636306878000,
  "effective_next_renewal_at": "2021-11-07T17:47:17.000Z",
  "purchasely_subscription_id": "subs_XFJFJEBFFU757FUJH",
  "effective_next_renewal_at_ms": 1636307237000,
  "store_original_transaction_id": "10000009999999"
}
{
  "plan": "<plan vendorID defined in the Purchasely console>",
  "store": "APPLE_APP_STORE",
  "product": "<product vendorID define in the Purchasely console>",
  "user_id": "<user you provided through the SDK when the user logged in>",
  "event_name": "ACTIVATE",
  "offer_type": "NONE",
  "api_version": 3,
  "environment": "SANDBOX",
  "purchased_at": "2021-11-07T17:44:17.000Z",
  "store_country": "FR",
  "next_renewal_at": "2021-11-07T17:47:17.000Z",
  "purchased_at_ms": 1636307057000,
  "event_created_at": "2021-11-07T17:43:35.225Z",
  "is_family_shared": false,
  "store_product_id": "<store product id defined in the store console>",
  "next_renewal_at_ms": 1636307237000,
  "event_created_at_ms": 1636307015225,
  "previous_offer_type": "NONE",
  "store_app_bundle_id": "<app bundle id defined in the store console>",
  "subscription_status": "AUTO_RENEWING",
  "store_transaction_id": "100000099999999",
  "original_purchased_at": "2021-11-07T17:41:18.000Z",
  "original_purchased_at_ms": 1636306878000,
  "effective_next_renewal_at": "2021-11-07T17:47:17.000Z",
  "purchasely_subscription_id": "subs_XFJFJEBFFU757FUJH",
  "effective_next_renewal_at_ms": 1636307237000,
  "store_original_transaction_id": "10000009999999"
}

What’s Next

Learn more about the different types of attributes