Display a paywall

Present the paywall or flow assigned to a placement and react to how it ended.

The initial release is observer-only

SDK-started purchases are temporarily unavailable. Paywalls and flows show, but a purchase started from one is refused and the paywall shows it as failed. Keep your own purchase path, as described in Build a custom purchase screen.

Use this page to show a paywall or a multi-screen flow, such as an onboarding survey, from the React Native SDK. A paywall is a flow with one screen. A placement is the stable slug your app asks for, such as onboarding or settings-upsell. You publish paywalls and assign them to placements in Studio, as described in Paywall locations.

Add the renderer

Paywalls are drawn natively, on iOS and on Android, by a renderer that is not part of the default build. Turn it on with the renderer option of the Voidhash config plugin, then rebuild the app.

app.json
{
  "expo": {
    "plugins": [["@voidhash/react-native", { "renderer": true }]]
  }
}

Run npx expo prebuild (or npx expo run:ios / npx expo run:android) so the native projects pick it up. Without the renderer, presenting fails with FLOW_RENDERER_MISSING and the SDK makes no paywall requests.

Choose what flows may do

Purchases, restores, closing and analytics always work. Anything else a flow can do is off until you grant it with the flowCapabilities client option. By default a flow may only open links.

export const voidhash = createVoidhashClient("vh_pk_…", {
  flowCapabilities: ["links", "notifications", "attributes"],
});
CapabilityLets a flow
linksOpen links in the browser or the app that handles them.
notificationsAsk the user to allow notifications.
trackingAsk for App Tracking Transparency permission on iOS.
attributesSave answers as person attributes, like setPersonAttributes.

A flow action you did not grant is skipped. Permission prompts need a declaration in your app:

  • notifications on Android 13 and later asks for POST_NOTIFICATIONS, which your app has to declare, for example with "android": { "permissions": ["android.permission.POST_NOTIFICATIONS"] } in app.json. Without it the flow is told the permission was not granted. Below Android 13 the answer is whether the user left notifications enabled.
  • tracking needs NSUserTrackingUsageDescription in your iOS Info.plist, for example under ios.infoPlist in app.json. Without it the request is refused. Android has no tracking prompt, so the request answers false.

Present it

Ask for a placement with client.presentFlow, and the SDK presents whichever paywall or flow is assigned to it. The promise resolves when the flow ends and never rejects.

const result = await voidhash.client.presentFlow("settings-upsell");

switch (result.status) {
  case "purchased":
  case "restored":
    await refreshAccess();
    break;
  case "finished":
    handleOnboarding(result.result, result.variables);
    break;
  case "closed":
    break;
  case "failed":
    showOwnUpgradeScreen();
    break;
}

purchased and restored mean a purchase or restore started from the paywall completed, and the paywall was dismissed. purchased carries the store productId. finished means the flow reached one of its finish actions: result is the name you gave that action in Studio and variables holds the flow's variables, such as the answers to a survey. closed means the user closed it, you called dismissFlow(), or another flow was presented in its place. A failed or cancelled purchase does not end the flow, so the user can try again.

In components, the usePresentFlow hook wraps the same call and tells you while a flow is on screen.

const { present, isPresenting } = voidhash.usePresentFlow("settings-upsell");

<Button
  disabled={isPresenting}
  title="See plans"
  onPress={async () => {
    const result = await present();
    if (result.status === "failed") router.push("/plans");
  }}
/>;

You can also close the flow from your own code, and the pending presentFlow resolves as closed.

await voidhash.client.dismissFlow();

Purchases and restores started from a flow go through the SDK like any other purchase. After a successful purchase, Voidhash refreshes the person snapshot and dismisses the paywall.

Resume where the user left

A flow saves its progress on the device as the user moves through it, for each person and flow. When the same flow is presented again, it resumes with those answers instead of starting over. Finishing the flow or completing a purchase from it clears the saved progress, and so does switching to another person with identify or reset.

Fall back when nothing was shown

failed carries an error whose code says why nothing was shown:

  • FLOW_NOT_ASSIGNED: no published paywall is assigned to the placement. This is the normal answer for a placement you have not set up yet.
  • FLOW_UNAVAILABLE: the device has never loaded this placement and cannot reach Voidhash.
  • FLOW_RENDERER_MISSING: the build does not include the renderer. See Add the renderer.
  • FLOW_UNSUPPORTED_PACKAGE: the paywall was published for a newer SDK. Update the SDK.
  • FLOW_PRESENTATION_FAILED: there was nothing to present from, or the paywall could not be drawn.
  • FLOW_DISABLED: the client was created with enabled: false.

Keep a fallback for important entry points so the customer still has a way to upgrade. Once a placement has been shown, the device keeps its paywall, so it shows again offline.

Preloading

A placement the device has shown before is downloaded again at launch, so the next presentation does not wait on the network. Pass placements you have not shown yet with the preloadPlacements client option to download them at launch too.

export const voidhash = createVoidhashClient("vh_pk_…", {
  preloadPlacements: ["onboarding"],
});

Nothing is preloaded when the build does not include the renderer.

Analytics

Events a flow tracks are captured with paywall_location and paywall_id properties, and each screen it shows is recorded as a $screen event, so flows appear in your funnels next to your own screens.

Next steps