After launching a subscription app, one of the first walls developers hit is paywall conversion. The pricing seems right, users are engaged — but they're not tapping "Subscribe." Is it the design? The copy? The price anchoring? Without data, you're left guessing.
Superwall is an SDK built specifically for paywall A/B testing. It lets you swap paywall designs and copy from a dashboard without redeploying your app, then tells you exactly which variant converts better. This guide covers integrating Superwall into a Rork-generated React Native app, running your first A/B test, and avoiding the implementation pitfalls that trip up most developers.
What Superwall Does (and Why It Matters for React Native Apps)
Superwall decouples the paywall screen from your app's code. Instead of hardcoding your paywall UI in a component, you register "placements" — trigger points in your app where a paywall might appear — and the dashboard controls what gets shown to whom.
This solves three real problems:
- Changing paywall copy no longer requires an App Store submission
- You can run multiple variants simultaneously with automatic traffic splitting
- Every user interaction with the paywall is tracked: views, dismissals, conversions, and which placement triggered the paywall
The React Native SDK works well with Expo, though you'll need an Expo Development Build (not Expo Go) since Superwall uses native modules.
Setup: Superwall Account and App Registration
Create an account at superwall.com and register your app. You'll receive separate API keys for iOS and Android — keep both handy.
If you're using RevenueCat for subscription management (the recommended approach), connect it in the Superwall dashboard under Integrations before writing any code. Superwall will pull product and subscription status data from RevenueCat automatically.
Installing the SDK
npm install @superwall/superwall-reactnative
# or
yarn add @superwall/superwall-reactnativeSince Superwall uses native modules, you'll need a development build:
eas build --profile development --platform iosInitializing Superwall
In your app entry point (App.tsx or app/_layout.tsx):
import Superwall from '@superwall/superwall-reactnative';
import { Platform } from 'react-native';
const SUPERWALL_API_KEY = Platform.OS === 'ios'
? 'pk_ios_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
: 'pk_android_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
// Store these in environment variables in production:
// process.env.EXPO_PUBLIC_SUPERWALL_API_KEY_IOS
Superwall.configure(SUPERWALL_API_KEY);configure queues internally, so you don't need to await it before registering placements elsewhere in your app.
Triggering a Paywall with Placements
The core concept is the placement — a named trigger point you call when a user hits a premium feature gate.
import Superwall from '@superwall/superwall-reactnative';
const handlePremiumFeature = async () => {
await Superwall.shared.register('premium_feature_tapped');
// Code after register() only runs if the user is subscribed
// Superwall checks subscription status automatically
navigateToPremiumFeature();
};The string 'premium_feature_tapped' maps to a campaign in the Superwall dashboard. When a user hits this code path, Superwall checks: Is there an active campaign for this placement? Is this user in the test audience? Which variant should they see?
Use Different Placements for Different Contexts
This is where most developers leave conversion gains on the table. The placement that works best for someone trying to use the AI chat feature is different from the one that works for someone exploring the settings screen.
// Define placements that match where the user is in their journey
const placements = {
aiChatGate: 'ai_chat_gate', // Trying to use a gated feature
exportGate: 'export_feature_gate', // Trying to export data
settingsUpgrade: 'settings_upgrade', // Explicitly looking at upgrade options
onboardingEnd: 'onboarding_paywall', // Right after completing onboarding
};
// Usage: match the placement to the context
await Superwall.shared.register(placements.aiChatGate);Why this matters: a user who just tried to use AI and was blocked is primed for a feature-focused pitch ("Unlock AI for unlimited messages"). A user sitting in Settings is more likely to respond to a value-summary pitch ("Everything in one plan").
RevenueCat Integration: The PurchaseController
If you're using RevenueCat, implement a PurchaseController to delegate purchase handling to it. Superwall handles the display; RevenueCat handles the transaction.
import Superwall, {
PurchaseController,
PurchaseResult,
RestorationResult,
} from '@superwall/superwall-reactnative';
import Purchases from 'react-native-purchases';
class RCPurchaseController extends PurchaseController {
async purchaseFromAppStore(productId: string): Promise<PurchaseResult> {
try {
const { customerInfo } = await Purchases.purchaseStoreProduct(
{ productIdentifier: productId } as any
);
// Check if the expected entitlement is now active
if (customerInfo.entitlements.active['pro'] \!== undefined) {
return PurchaseResult.Purchased;
}
return PurchaseResult.Cancelled;
} catch (error: any) {
if (error.userCancelled) {
return PurchaseResult.Cancelled;
}
return PurchaseResult.Failed;
}
}
async restorePurchases(): Promise<RestorationResult> {
try {
const customerInfo = await Purchases.restorePurchases();
const hasActiveEntitlements =
Object.keys(customerInfo.entitlements.active).length > 0;
return hasActiveEntitlements
? RestorationResult.Restored
: RestorationResult.NothingToRestore;
} catch {
return RestorationResult.Failed;
}
}
}
// Pass the controller during configuration
const controller = new RCPurchaseController();
Superwall.configure(SUPERWALL_API_KEY, { purchaseController: controller });Replace 'pro' with your actual RevenueCat entitlement identifier. Getting PurchaseResult wrong is the most common integration bug — if you return Purchased when the entitlement isn't actually active, Superwall unlocks the content incorrectly. If you return Failed when the user actually cancelled, the UX breaks.
Setting Up an A/B Test in the Dashboard
Once your code is deployed, the actual test configuration happens in the dashboard.
Creating Paywall Variants
Under "Paywalls," create two versions. Superwall provides HTML/CSS templates you can customize directly. For your first test, change only one element — the headline. Two variables in one test means you can't attribute the result to either change confidently.
A simple first test:
- Variant A (control): "Upgrade to Premium"
- Variant B (test): "Remove the limits — go unlimited"
Campaign Configuration
Create a Campaign, attach it to your placement, and set the traffic split. 50/50 is standard for a first test. Under "Audience," you can target by:
- Session count (e.g., only users on their 3rd session or later)
- User attributes you set via
Superwall.shared.setUserAttributes() - Device and locale
Reading Results
Don't make decisions too early. Superwall shows confidence intervals alongside conversion rates. Practical guidelines:
- Wait until each variant has at least 100–200 paywall views
- Look for p < 0.05 before drawing conclusions
- Run tests for at least 7 days to capture weekly behavioral patterns
Forwarding Superwall Events to Your Analytics Stack
Superwall's own analytics are solid, but connecting them to your existing analytics tool lets you correlate paywall behavior with upstream events (like which onboarding step the user completed).
import Superwall, {
SuperwallEventInfo,
SuperwallDelegate,
} from '@superwall/superwall-reactnative';
class AnalyticsDelegate extends SuperwallDelegate {
async handleSuperwallEvent(eventInfo: SuperwallEventInfo): Promise<void> {
const { event } = eventInfo;
// Forward key events to your analytics platform
switch (event.type) {
case 'paywallOpen':
analytics.track('Paywall Viewed', {
placement: event.paywallInfo.identifier,
experimentId: event.paywallInfo.experiment?.id,
variantId: event.paywallInfo.experiment?.variant.id,
});
break;
case 'transactionComplete':
analytics.track('Subscription Started', {
placement: event.paywallInfo.identifier,
productId: event.transaction?.productIdentifier,
});
break;
case 'paywallClose':
analytics.track('Paywall Dismissed', {
placement: event.paywallInfo.identifier,
});
break;
}
}
}
Superwall.shared.delegate = new AnalyticsDelegate();Common Mistakes to Avoid
Paywall not showing in development
Superwall won't display anything if no active campaign exists for the placement you're calling. Confirm in the dashboard that your paywall is in Active state and that it's attached to a campaign targeting the correct placement.
Always seeing the same variant during testing
Superwall assigns variants based on user identity. To see a different variant on the same device, call Superwall.shared.reset() or use Preview Mode in the dashboard to force a specific variant.
Worrying about double charges with RevenueCat
With a proper PurchaseController, RevenueCat processes the transaction exactly once. Superwall tracks which paywall triggered the purchase but has no involvement in the actual charge. The risk of double-charging is in returning the wrong PurchaseResult — specifically, accidentally returning Purchased before the RevenueCat entitlement is confirmed active.
The Mindset Shift: Paywalls as a Product, Not a Checkbox
Most developers treat the paywall as something to build once and ship. Superwall makes it practical to treat it as a product — something to iterate on with real data the same way you iterate on any other screen.
A single headline change improving conversion by 20–30% is not unusual. Multiply that by the lifetime value of a subscriber, and a few hours of A/B testing setup pays for itself many times over.
Start small: integrate the SDK, add one placement around your most valuable gated feature, and run a single headline test. Once you see your first statistically significant result, you'll have a clear sense of how much headroom remains.