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.
{
"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"],
});| Capability | Lets a flow |
|---|---|
links | Open links in the browser or the app that handles them. |
notifications | Ask the user to allow notifications. |
tracking | Ask for App Tracking Transparency permission on iOS. |
attributes | Save answers as person attributes, like setPersonAttributes. |
A flow action you did not grant is skipped. Permission prompts need a declaration in your app:
notificationson Android 13 and later asks forPOST_NOTIFICATIONS, which your app has to declare, for example with"android": { "permissions": ["android.permission.POST_NOTIFICATIONS"] }inapp.json. Without it the flow is told the permission was not granted. Below Android 13 the answer is whether the user left notifications enabled.trackingneedsNSUserTrackingUsageDescriptionin your iOS Info.plist, for example underios.infoPlistinapp.json. Without it the request is refused. Android has no tracking prompt, so the request answersfalse.
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 withenabled: 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.