Flutter SDK · v1.1.0 · 0 ms Offline Cache
Ship paywalls without shipping an app update.
You design the paywall and decide when it appears in the MySuperWall dashboard. The SDK fetches it by trigger key and presents it in your app. Purchases stay with your own in-app purchase code.
handleTrigger in your apponPurchase); you run the purchase with RevenueCat, in_app_purchase or any other library.Quick start
From a new account to your first paywall in your app. Each step is done in the dashboard unless it says otherwise.
- Create an account and an appSign up, verify your email, then open Apps → New app. Enter a name, the platform and your bundle ID (iOS) or package name (Android). You land on the setup checklist, which tracks your progress.
- Add your productsOpen Products → New product and enter each subscription or purchase. The product ID must match your App Store or Google Play ID exactly. If you connected a store account, you can sync products instead.
- Create a paywallOpen Paywalls → New paywall, pick a template and edit the texts. On the Products tab, link your products to the template's price slots. Turn the paywall on when it is ready.
- Add a triggerOpen Triggers → New trigger. Choose a key such as
onboarding_finishand the paywall to show. Keep it simple at first; A/B tests and targeting are under Advanced options. - Connect the SDKIn your Flutter app, add the package, initialize it with your app key and call
handleTriggerwith the same trigger key. The code is below, with your real key on the app's Overview page. - Check it workedRun the app. The Overview page shows when the SDK has connected and when your first paywall was delivered. If nothing shows, see Troubleshooting.
// pubspec.yaml
dependencies:
mysuperwall_sdk: ^1.3.1
// main.dart
await MySuperWallClient.initialize(appKey: 'YOUR_APP_KEY');
// where the paywall should appear
await MySuperWallClient.instance.handleTrigger(
context,
triggerKey: 'premium',
onPurchase: (productId) async { /* run your purchase here */ },
);
clearCachedPaywalls() while testing.Install the SDK
Requires Flutter 3.35+ and Dart 3.9+.
flutter pub add mysuperwall_sdk:^1.3.1Initialize
Call initialize once, before runApp. Your app key is shown on the app's Overview page (SDK key) and in Settings.
import 'package:mysuperwall_sdk/mysuperwall_sdk.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await MySuperWallClient.initialize(
appKey: 'YOUR_APP_KEY',
);
runApp(const MyApp());
}
| Parameter | Default | Description |
|---|---|---|
appKey | none | Identifies your app. The SDK also sends your bundle ID / package name, so the app can be matched by those too. |
userId | auto | Your own user ID. If omitted, an anonymous UUID is generated and stored on the device. |
requestTimeout | 8 s | Timeout for backend requests. |
failOpenOnNetworkError | true | Controls whether initialization may continue when validation is unavailable. It does not authorize an offline paywall. Catch presentation request errors; cached content alone cannot authorize display. |
Afterwards use MySuperWallClient.instance anywhere. MySuperWallClient.isInitialized tells you whether setup completed.
Load store prices and prepare the paywall
After initialization, fetch real products from your existing billing provider and build localizedProducts keyed by exact product ID. Values must include displayPrice, priceString and currencyCode; include derived period prices when used. The snippet shows the SDK call, not a complete billing adapter. Wait for successful price and paywall loading before enabling the entry point. Currency follows the store account; it is not converted from device language. Do this once at startup and again only when prices or the language change: new prices or a new locale before every presentation discard the prepared paywall and make each opening slow.
await MySuperWallClient.instance.setProductData(localizedProducts);
await MySuperWallClient.instance.setPreparedPaywallPreferences(
preferredTriggerKeys: ['YOUR_TRIGGER_KEY'], warmPool: false,
);
await MySuperWallClient.instance.prefetchAllActivePaywalls();
await MySuperWallClient.instance.warmPreparedPaywalls();Add the host widget
Wrap your app in MySuperWallHost. It keeps paywalls pre-loaded in the background so they appear instantly. It is recommended; without it the SDK falls back to a normal route presentation.
MaterialApp(
builder: (context, child) => MySuperWallHost(child: child!),
home: const HomePage(),
);
Show a paywall
Call handleTrigger at the moment you want a paywall. The trigger key decides which paywall (or A/B variant) the user gets, based on your dashboard configuration.
Purchase and restore callbacks do not automatically show loading. Mount MySuperWallHost, then use this helper in your controller/service for both prepared and normal paywalls. Start loading before product lookup or billing; always clear it in finally and prevent repeated taps. Without the host, use your app's loading overlay above the paywall.
// Keep this helper and billing handlers in your existing controller/service.
// Mount MySuperWallHost in the app builder before presenting paywalls.
bool isStoreActionInProgress = false;
Future<void> runWithPurchaseLoading(Future<void> Function() action) async {
if (isStoreActionInProgress) return;
isStoreActionInProgress = true;
MySuperWallClient.setHostLoadingOverlayVisible(true);
try {
await action();
} finally {
MySuperWallClient.setHostLoadingOverlayVisible(false);
isStoreActionInProgress = false;
}
}
purchaseProduct and restorePurchases below are your existing asynchronous billing methods, not SDK APIs. Await the actual purchase or restore, handle cancellation/errors, update entitlements, and report only confirmed outcomes inside them. Use the same wrappers for presentPreparedPaywallIfAvailable callbacks.
final result = await MySuperWallClient.instance.handleTrigger(
context,
triggerKey: 'onboarding_finish',
onPurchase: (productId) async {
await runWithPurchaseLoading(() => purchaseProduct(productId));
},
onRestore: () async {
await runWithPurchaseLoading(restorePurchases);
},
onDismiss: () {},
);
if (!result.show) {
// No paywall for this user/trigger (disabled, holdout, offline...).
// Let the user continue.
}
PaywallResult fields: show, paywall, reason, message, statusCode, details. Always handle show == false; it is a normal outcome (for example holdout groups or audience rules).
| Callback | When |
|---|---|
onPurchase(productId) | User taps a purchase button. The SDK records purchase_started automatically. |
onRestore | User taps "Restore". |
onDismiss | Paywall closed. The SDK records the dismiss event automatically. |
onOpenTerms / onOpenPrivacy / onOpenUrl | User taps a legal or external link. Handle these callbacks to open the links in your app. |
Pass metadata: {...} to send extra context used for audience rules.
Handle purchases
The SDK records purchase_started and dismiss events itself. Report the outcome so your dashboard analytics show conversions and revenue:
Adapt your billing service to return a typed outcome: purchased, cancelled, pending or failed. A boolean false does not distinguish cancellation from error. Keep this adapter in your existing controller/service.
// Application adapter contract, not an SDK-provided billing service.
enum BillingOutcome { purchased, cancelled, pending, failed }
// Inside onPurchase, within runWithPurchaseLoading:
final client = MySuperWallClient.instance;
final outcome = await myPurchaseService.buy(productId); // BillingOutcome
switch (outcome) {
case BillingOutcome.purchased:
client.setSubscriptionStatus(true); // real entitlement confirmed
await client.trackPurchaseSuccess(productId: productId);
client.dismiss();
break;
case BillingOutcome.cancelled:
await client.trackPurchaseCancelled(productId: productId);
break;
case BillingOutcome.failed:
await client.trackPurchaseFailed(productId: productId);
break;
case BillingOutcome.pending:
break; // wait for the provider's actual terminal result
}
Follow-ups reuse your purchase callbacks. Omit optional paywallId and triggerKey overrides unless they identify the currently displayed paywall. The SDK records purchase context when a product button is tapped. Restore success must identify the current paywall. Report revenueUsd only when the actual USD amount is known.
dismiss() closes the visible paywall programmatically.
Product prices
Paywall templates show prices such as {{weeklyPriceText}}. The SDK has no built-in store connection, so pass the localized prices you already read from the store, keyed by product ID:
await MySuperWallClient.instance.setProductPrices({
'weekly_sku': '\$6.99',
'monthly_sku': '\$19.99',
});
For intro offers, per-day/week/month text and eligibility, use structured data:
await MySuperWallClient.instance.setStructuredProductData({
'weekly_sku': const MySuperWallProductDataEntry(
displayPrice: '\$6.99',
pricePerWeekText: '\$6.99',
introDisplayPrice: '\$1.99',
introEligible: true,
introUsed: false,
),
});
- If the user is intro-eligible, the price placeholders show the intro price; otherwise the regular price.
- Call this before showing a paywall. Pass
clearCachedPaywalls: trueif prices change while paywalls are already cached. - Product IDs must match the ones configured in the dashboard under Products.
Prefetching
Fetch paywalls ahead of time so they open without a spinner:
await MySuperWallClient.instance.prefetchPaywall(triggerKey: 'onboarding_finish');
// or everything that is active:
await MySuperWallClient.instance.prefetchAllActivePaywalls();
Paywalls are cached on disk. clearCachedPaywalls() removes them. With MySuperWallHost, one ready paywall is kept warm by default (Flutter SDK 1.2.0 defaults to none: call configurePreparedPaywallPool(maxCount: 1) after initialize); tune it with configurePreparedPaywallPool(maxCount: n) and setPreparedPaywallPreferences(preferredTriggerKeys: [...]).
Lifecycle triggers
Published Flutter 1.3.1 computes these fixed lifecycle keys during initialization. Call handleLifecycleTriggers after UI readiness; expanded automatic behavior below requires updated unpublished source.
| Key | Fires |
|---|---|
mysuperwall.install | First SDK use on this device |
mysuperwall.update | First launch after an app version change |
mysuperwall.session | Every session |
mysuperwall.session_3 / _5 / _10 / _20 / _50 | Once, at that session count |
mysuperwall.day_1 / _3 / _7 / _14 / _30 | Once, that many days after first SDK use |
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
MySuperWallClient.instance.handleLifecycleTriggers(context,
onPurchase: (id) => buy(id));
});
}
It shows at most one paywall per call (the first match). prefetchLifecycleTriggers() preloads them.
| Key | Fires |
|---|---|
mysuperwall.foreground | Each return from the background |
mysuperwall.session_N | Reach N total sessions, once |
mysuperwall.day_N | N days since first SDK use, once |
mysuperwall.active_minutes_N | N total active minutes, once; background time excluded |
// Updated source only. Register existing application-service handlers once.
client.setPresentationHandlers(
onPurchase: purchaseThroughExistingBilling,
onRestore: restoreThroughExistingBilling,
);
// Preserve your existing MaterialApp/GetMaterialApp builder.
MySuperWallHost(
lifecycleReady: onboardingComplete,
child: existingBuilderResult,
)
In the updated source, sessions start at SDK initialization or a return after at least 30 minutes in the background. Days start at first SDK use, not store installation. One-time signals expire 30 days after occurrence; history before collection began cannot be recovered. Automatic display waits for readiness and valid billing handlers. The host does not require purchase callbacks.
Read the complete lifecycle and recovery integration contract
Users & locale
MySuperWallClient.instance.setUserId('user_123'); // after login
MySuperWallClient.instance.setLocale('de'); // paywall language
Changing either value resets cached paywalls so the next one is fetched for the new identity. A/B variants are sticky per user, so use a stable userId for consistent experiments. Set the language once at startup and when the user changes it, never before each presentation.
Automation actions
Configure follow-up triggers on the paywall’s Follow-up tab. The default runner handles supported rules; do not install a custom handler just to enable recovery.
- Publish the source paywall and activate the target trigger and published paywall in the same app/platform. The built-in runner applies delay, once-per-session, cooldown, chain depth, premium and repeat protection.
- setAutomationActionHandler replaces the default runner. Use it only for an intentional custom runner that preserves every policy and billing callback; a delay-only handler is incomplete.
- The 2026-11-01 cancellation forwarding and prepared-presentation follow-up fixes are in updated source only, not published 1.3.1. Record the actual billing result and inspect your installed SDK before claiming recovery works.
Push notifications (optional)
Send notifications from the dashboard through your own Firebase project, for example a reminder after someone closes the paywall that opens a discount paywall. The SDK has no Firebase dependency: your app reads the FCM token and hands it over.
Requires Flutter SDK 1.3.0 or later.
- Connect Firebase. In the dashboard open Integrations → Push notifications and upload a Firebase service account key (role: Firebase Cloud Messaging Admin).
- Register the device. After the user allows notifications, pass the FCM token to the SDK, and again whenever it refreshes. It is registered for the current user and language, and again by itself when you change either.
- Handle taps. Give the notification data to the SDK when the user taps it. It reports the tap and returns the trigger to open, so a discount trigger shows your offer.
- Create a notification. In the dashboard open Notifications, choose when to send (for example after the paywall was closed), write the text per language and pick the trigger each reminder opens.
final messaging = FirebaseMessaging.instance;
await messaging.requestPermission();
final token = await messaging.getToken();
if (token != null) await MySuperWallClient.instance.setPushToken(token);
messaging.onTokenRefresh.listen(MySuperWallClient.instance.setPushToken);
// Taps: app in the background, and app started by the notification.
Future<void> openFrom(RemoteMessage message) async {
final key = await MySuperWallClient.instance
.trackNotificationOpened(message.data);
if (key == null || !context.mounted) return;
await MySuperWallClient.instance.prepareTrigger(key);
if (context.mounted) {
await MySuperWallClient.instance.handleTrigger(context, triggerKey: key);
}
}
FirebaseMessaging.onMessageOpenedApp.listen(openFrom);
final initial = await messaging.getInitialMessage();
if (initial != null) await openFrom(initial);
// Stop notifications (logout, or the user turned them off):
await MySuperWallClient.instance.clearPushToken();
Each device gets the text for its language (the SDK locale); devices without a matching language get the default one. Someone who just bought is skipped, and a purchase cancels pending notifications. Translating the text with DeepL is a Premium feature.
A notification's trigger is usually not one of your prepared paywalls. Wait until startup (prices and language) has finished and call prepareTrigger before presenting, otherwise the paywall can open as a blank page.
Android shows FCM notifications only while the app is in the background. Keep the WAKE_LOCK permission in your manifest (Firebase needs it) and request POST_NOTIFICATIONS on Android 13+.
iOS: enable the Push Notifications capability and the Remote notifications background mode, and upload your APNs key in the Firebase console (Project settings → Cloud Messaging).
Offline & errors
- Paywalls are cached on disk; a cached paywall opens without network. Stale cache is used only on transient failures, for at most 24 hours.
- With
failOpenOnNetworkError: true(default), a network failure returnsPaywallResult(show: false, reason: 'network_unavailable'). - Configuration problems throw
MySuperWallExceptionwithmessage,code,statusCodeanddetails. Wrap calls intry/catch. healthCheck()returnstrueif the backend is reachable.
API reference
| Method | Purpose |
|---|---|
MySuperWallClient.initialize({appKey, userId, httpClient, requestTimeout, failOpenOnNetworkError}) | Set up the SDK (static, once) |
handleTrigger(context, {triggerKey, metadata, onPurchase, onRestore, onDismiss, onOpenUrl, onOpenTerms, onOpenPrivacy}) | Resolve and show a paywall |
handleLifecycleTriggers(context, {...}) | Show the first matching lifecycle paywall |
prefetchPaywall({triggerKey, metadata}) | Fetch and cache one paywall |
prefetchAllActivePaywalls() | Fetch and cache all active paywalls |
setProductPrices(map) / setProductData(map) / setStructuredProductData(map) | Provide store prices |
trackPurchase / trackPurchaseSuccess / trackPurchaseFailed / trackPurchaseCancelled | Report purchase events |
trackDismiss({paywallId, ...}) | Report a dismiss (done automatically by handleTrigger) |
setUserId / setLocale | Change identity or language |
setAutomationActionHandler(handler) | Run dashboard-defined follow-up actions |
dismiss() | Close the visible paywall |
clearCachedPaywalls() | Remove disk cache |
healthCheck() | Check backend connectivity |
setPushToken(token) | Register the FCM token for this device |
clearPushToken() | Stop notifications for this device |
trackNotificationOpened(data) | Report a notification tap and get the trigger to open |
handleNotificationOpened(context, data, {...}) | Report the tap and show the notification's paywall |
AI agents
Connect Claude Code, Codex or Cursor to your account and let it build paywall drafts for you. The agent works through the MySuperWall MCP server. You review every draft and decide what goes live.
Live editor pairing (recommended)
Open a paywall in the editor and click AI agent. Add the editor MCP to your agent once, then paste the prompt with the pairing code shown in the editor. The agent edits the paywall in front of you: it picks or designs the template, writes the copy, links products, adds languages and checks its work with screenshots. No key is needed; the one-time code connects the agent to that editor tab only, and you can disconnect it at any time.
claude mcp add --transport http mysuperwall-editor https://mysuperwall.com/editor-mcp
# then, in Claude Code:
Connect to the mysuperwall-editor MCP with pairing code: ABCD-EFGH-JKLM
Connect an agent
- Create a keyOpen the account menu, choose AI agents and select New key. Pick the apps the agent may use and how long the key lasts. The key is shown once.
- Connect your agentThe page shows a ready command for Claude Code, Codex and Cursor. For Claude Code it looks like the command below.
- Ask for a paywallDescribe what you want. The agent saves drafts and gives you a preview link. Review them under Paywalls and activate the ones you like.
claude mcp add --transport http mysuperwall https://mysuperwall.com/mcp \
--header "Authorization: Bearer msw_pat_YOUR_KEY"
Example requests
- List my MySuperWall apps and show me what paywalls the first one has.
- Design a dark paywall from scratch for my fitness app with a weekly and a yearly plan, check how it looks, and fix anything that is clipped.
- Duplicate my live paywall, rewrite the headline to focus on saving time and add a Turkish version.
What the agent can do
| Tool | What it does |
|---|---|
list_apps, get_app | Read your apps with their products, paywalls and triggers. |
list_templates, get_template, get_template_guide | Read built-in and custom templates, and the rules for writing a new one. |
create_template, update_template | Save a template designed from scratch (HTML). Rejected with a reason if it breaks a rule. |
create_paywall, update_paywall, duplicate_paywall | Create or edit draft paywalls: content, products, languages. |
create_product, create_trigger | Register a store product, or add a draft trigger for a paywall. |
request_image_upload | Returns a one-time command to upload a local image (PNG, JPEG, WebP or GIF, up to 5 MB). |
preview_template, preview_paywall | Render a design to an image so the agent can check it. Uses sample prices. Available when screenshots are enabled on the server. |
Safety and limits
- A key only reaches the apps you chose, and it expires. Revoke it any time on the AI agents page.
- Agents cannot reach store credentials, billing, team members, SDK keys or your account.
- Templates are checked on save. Network calls, external scripts, redirects and iframes are not allowed. Template HTML has a 150 KB safety size check.
- Short-lived request throttling protects the service from abusive bursts.
- Every agent action appears in the activity log as
agent:key name.
Tips
Paste the short guide at /docs/agents.md into your project's CLAUDE.md or AGENTS.md so the agent knows how to work with MySuperWall. Ask it to preview after every significant change.
If something fails
- 401 Unauthorized
- The key was revoked, has expired or was copied incompletely. Create a new one.
- "This paywall is live"
- Agents cannot edit live paywalls. Ask it to duplicate the paywall and edit the copy.
- "Screenshots are not enabled"
- The server has no renderer configured. The agent can still give you the preview link.
- A template is rejected
- The message says which rule it broke. The agent can read
get_template_guidefor the full list.
Live SDK Debugger & Event Stream Console
Inspect real-time paywall trigger evaluations, audience targeting decisions, and conversion events as they occur on physical test devices.
The console allows you to:
- Filter Event Types: Switch between Triggers, Paywall Presentations, Purchases, Holdout allocations, and Diagnostic Warnings.
- Inspect Audience Rule Traces: See why a paywall was shown or skipped, which rule conditions passed, and the assigned A/B test variant.
- Interactive Trigger Simulator: Simulate trigger keys with custom user attributes directly in the dashboard before testing in the app.
Troubleshooting
- Nothing shows,
result.showis false - Check
result.reason. Common causes: the trigger key does not exist or has no paywall attached, the paywall is not published, the user is in a holdout group or fails an audience rule, or the device is offline. - "Prepared controller missing" in logs
- Harmless, but add
MySuperWallHostabove yourNavigatorso paywalls can be pre-built. unmounted_contexterror- The widget was disposed while the paywall was loading. Check
mountedbefore calling, or trigger from a stable parent. - Prices show as placeholders
- Call
setProductPrices/setStructuredProductDatabefore showing the paywall, and make sure product IDs match the dashboard. - Old design still appears after editing
- Paywalls are cached; the update arrives on the next fetch. Call
clearCachedPaywalls()while testing. - Different paywall for the same user
- Pass a stable
userId. Without one, a new install gets a new anonymous ID and can land in a different A/B variant.