Skip to content

Paywall integration

Superwall and Wire AI on React Native

Superwall decides when a paywall shows and which one, from a campaign you edit remotely. Wire AI decides what happens in the minutes before that, and it finishes holding a set of answers about the person. Connecting the two is one handoff: write the answers into Superwall as user attributes, wait for that write, then register the placement so the campaign evaluates against what the onboarding just learned instead of an empty attribute set. Miss the await and your audience filters run one session behind, forever.

Install

terminal
$
npxexpo install expo-superwall
$
npminstall @wireai/activation

API surface on this page read against @wireai/activation 0.13.5 (npm latest, read 2026-08-04) and expo-superwall 1.2.0 (npm latest, read 2026-08-02). These are the versions the calls below were checked against, not a claim that this exact file is running in production somewhere.

Hand the answers over, then register

WireOnboarding calls onComplete with an answers object keyed by question key, plus the raw thread if you want to parse it yourself. Values come back as unknown, so cast at the boundary rather than trusting the shape downstream.

useUser().update takes a function over the previous attributes, so spread rather than overwrite. Superwall reads those attributes when registerPlacement runs, which is why the await is load bearing and not cosmetic.

components/OnboardingScreen.tsx
1import { usePlacement, useUser } from "expo-superwall";
2import { WireOnboarding, useWireActivation } from "@wireai/activation";
3
4import { wireConfig } from "@/lib/wire";
5import { StaticOnboarding } from "@/components/StaticOnboarding";
6
7export function OnboardingScreen({ onDone }: { onDone: () => void }) {
8  const { update } = useUser();
9  const { track } = useWireActivation(wireConfig);
10
11  const { registerPlacement } = usePlacement({
12    onPresent: () => { void track("paywall_seen", { via: "superwall" }); },
13    onDismiss: (_info, result) => {
14      void track("paywall_dismissed", { result: String(result) });
15      onDone(); // the user still has to get into the app
16    },
17  });
18
19  return (
20    <WireOnboarding
21      config={wireConfig}
22      onComplete={async ({ answers }) => {
23        await update((prev) => ({
24          ...prev,
25          goal: String(answers.goal ?? "unknown"),
26          experience: String(answers.experience ?? "unknown"),
27        }));
28
29        await registerPlacement({
30          placement: "post_onboarding",
31          feature() { onDone(); },
32        });
33      }}
34      fallbackFlow={<StaticOnboarding onDone={onDone} />}
35    />
36  );
37}

Keep the two systems on one session

Both the placement result and the onboarding funnel are about the same person in the same app-open. Reporting the paywall beats through wire.track puts them in one stream keyed on the current session id, so the experiment engine can compare a variant that showed the paywall early against one that showed it late without you joining two exports by hand.

The same call is what re-arms a review or questionnaire gate, because a successful track bumps decision revalidation and a subscribed gate re-fetches.

What breaks

The failure modes when Superwall and Wire AI share an app. None of them throws, which is why they survive code review.

feature() never runs when the user dismisses without buying

registerPlacement's feature callback fires when the user has access. Dismiss the paywall and it does not fire at all. Put your navigation in feature() only and a non-buyer sits on the onboarding screen looking at nothing. Handle onDismiss as a first-class path, which is what the snippet above does.

Register after the attribute update resolves

Superwall evaluates audience filters at register time. Fire registerPlacement in the same tick as update() and the campaign sees the previous attribute set, so the segment you just discovered targets nobody until the next open. Await the update.

Superwall attributes are not a dumping ground for answers

Push the two or three fields a campaign actually filters on. Everything else belongs in the Wire event stream, where it is already stored against the session and does not become a permanent user property you have to migrate later.

answers values are unknown by design

OnboardingResult types answers as Record<string, unknown> because the backend can add a question without breaking your build. Cast at the boundary and give every read a fallback, or a new question shape crashes the paywall handoff instead of degrading.

Questions people actually ask

Can Wire AI decide which Superwall paywall shows?

Indirectly, and that is the right seam. Wire writes the attributes, Superwall's campaign rules read them. Your paywall targeting stays in Superwall where you can edit it without a release.

Does this work with the bare React Native SDK too?

The handoff is the same: set the user attributes, wait, then register. expo-superwall is the wrapper this page was written against, so the hook names come from it.

What if the onboarding fails before it reaches the paywall?

Pass fallbackFlow and the kit renders your existing static onboarding in place after retries are exhausted. The user still finishes and still reaches the placement.

Sources

Wire it into your app

Analytics is free to 299k events a month, forever, on any app. 15 founder seats get the whole engine free, 4 filled.

Written by Malik Chohra. 9 years React Native. Shipped an app at 9M monthly users, and took a consumer app from a 4.3 to a 4.9 App Store rating.

Next: RevenueCat · PostHog