1.4.0 (2026-11-01): Lifecycle history, deferred follow-up offers and expanded motion. Use this version for the new APIs below. Existing billing remains app-owned. Device billing and motion verification are not implied by publication.
1.3.1 delivery behavior (2026-10-30): The Flutter SDK validates delivery with the server before every presentation, including prepared views. Disabled triggers/paywalls and holdout decisions must not open a native/default fallback. Cached content is not shown on network failure. Hidden prepared views detach after loading; animations and media pause while hidden/backgrounded. Keep
MySuperWallHost. Prewarming avoids HTML loading work, but a network decision is still required. Dashboard changes take effect on the next delivery request, not by closing an already-open screen.
Beautiful paywalls. Delivered in Flutter.
Design in MySuperWall. Publish remotely. Show the right paywall in your app — with your products, your pricing, and your existing billing integration.
Open dashboard → · Documentation · Example app · Report an issue
Design. Publish. Learn.
| Make it yours | Show it with purpose | Keep improving |
|---|---|---|
| AI-assisted design — create and refine paywalls with your coding agent connected to MySuperWall. | Audience targeting — configure rules and variants in the dashboard, then present by trigger key. | A/B experiments — compare SDK-reported displays and purchase outcomes in the dashboard. |
| Remote publishing — update published paywall designs and content without a new app release. | Localized experiences — select paywall language independently from the audience’s country. | Recovery flows — connect a second paywall with bounded chains, cooldowns and repeat protection. |
Made to fit your app
- Keep your billing. Connect RevenueCat,
in_app_purchase, or your own store integration through purchase and restore callbacks. - Prepare before the moment. Cache manifests, prefetch paywalls and keep views ready with
MySuperWallHost. - Handle connectivity gracefully. Use available cached paywalls; the default transient-network fallback lets your app continue when a paywall cannot be loaded.
- Use the dependencies you already have. Compatible
package_info_plusanddevice_info_plusversions are accepted across multiple major releases.
Dashboard features depend on your MySuperWall account plan. Store billing and entitlement decisions remain in your application.
From dashboard to your first paywall
- Create an app, add your store product IDs and design a paywall.
- Publish it and connect it to a trigger such as
onboarding_finish. - Integrate the SDK using your app key, then present that trigger.
1. Install
dependencies:
mysuperwall_sdk: ^1.4.0
Requires Flutter 3.35+ / Dart 3.9+. Supports iOS and Android.
2. Initialize
import 'package:flutter/material.dart';
import 'package:mysuperwall_sdk/mysuperwall_sdk.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await MySuperWallClient.initialize(
appKey: 'YOUR_APP_KEY', // Dashboard → your app → Settings
locale: 'en', // your app's language, set once here
);
runApp(const MyApp());
}
Pass the app language to initialize, not before each presentation: a later setLocale with a different value (for example tr while the device default is tr-tr) clears the cached manifest and the prepared paywall.
Add the host to your existing MaterialApp or GetMaterialApp builder. Preserve any builder your app already uses.
builder: (context, child) => MySuperWallHost(
child: child ?? const SizedBox.shrink(),
),
3. Prepare once at startup
When your store prices are available (for example after RevenueCat returns the current offering), push them once and warm the paywall you show most often. MySuperWallHost keeps one prepared paywall by default, so the next handleTrigger can reuse the loaded HTML after server validation.
final client = MySuperWallClient.instance;
await client.setProductPrices(localizedPrices); // from your billing provider
await client.setPreparedPaywallPreferences(
preferredTriggerKeys: const ['premium'],
warmPool: false,
);
await client.prefetchAllActivePaywalls();
await client.warmPreparedPaywalls();
Do this at startup and again only when prices or the language change. Calling setProductPrices or setLocale with new values before every presentation discards the prepared paywall and rebuilds it, which makes every opening slow.
4. Present
Purchase and restore callbacks do not automatically start a loading indicator. Mount MySuperWallHost in your existing app builder and keep this helper in your existing controller/service. Show loading before product lookup or billing, await completion, and always clear it in finally. The shared guard prevents repeated purchase/restore taps.
// 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 mean your existing asynchronous billing methods; they are not SDK methods. They must await billing, handle cancellation/errors, report confirmed outcomes, and update entitlements. Apply the same wrappers to presentPreparedPaywallIfAvailable callbacks when using prepared presentation. If you do not mount MySuperWallHost, use your app's existing loading overlay above the paywall.
final result = await MySuperWallClient.instance.handleTrigger(
context,
triggerKey: 'onboarding_finish',
onPurchase: (productId) async {
await runWithPurchaseLoading(() => purchaseProduct(productId));
},
onRestore: () async {
await runWithPurchaseLoading(restorePurchases);
},
);
if (!result.show) {
// No matching paywall, holdout, or an unavailable delivery.
// Continue with your application's normal flow.
}
After your billing provider confirms a completed purchase:
await MySuperWallClient.instance.trackPurchaseSuccess(
productId: confirmedProductId,
);
A button tap is not a successful purchase. The SDK presents the paywall and exposes callbacks; your billing provider completes the transaction.
The details, when you need them
Localized product prices & introductory offers
Read localized prices from your store integration and pass them by product ID once they are loaded (see Prepare once at startup), not before every presentation. The strings below are illustrative; use actual store values in production.
await MySuperWallClient.instance.setProductPrices({
'weekly_sku': '\$6.99',
'monthly_sku': '\$19.99',
});
For introductory offers and per-period text:
await MySuperWallClient.instance.setStructuredProductData({
'weekly_sku': const MySuperWallProductDataEntry(
displayPrice: '\$6.99',
pricePerWeekText: '\$6.99',
introDisplayPrice: '\$1.99',
introEligible: true,
introUsed: false,
),
});
Pass eligibility from your billing provider. Price placeholders use the introductory price when eligible and the regular price otherwise.
Identity, language & audience country
final client = MySuperWallClient.instance;
client.setUserId('YOUR_AUTHENTICATED_USER_ID');
client.setLocale('tr');
client.setCountry('TR');
client.setSubscriptionStatus(hasActiveEntitlement);
An anonymous user ID is generated when omitted during initialization. Language and country are separate settings. Regional languages fall back to the base translation on compatible backends. Identity and locale changes invalidate the active manifest scope and cancel pending default recovery.
Recovery paywalls & restore completion
handleTrigger runs configured follow-up rules with once-per-session, cooldown, chain-depth and repeat protection. The 1.4.0 update also binds these rules in presentPreparedPaywallIfAvailable, using a mounted MySuperWallHost context when no presentation context is supplied. In 1.3.1, a legacy dismiss second screen requires a freshly validated manifest; a cached target alone is insufficient.
Pass real entitlement state through setSubscriptionStatus(bool) for rules using skipIfPremium. After a confirmed restore:
MySuperWallClient.instance.trackRestoreSuccess(
paywallId: displayedPaywallId,
);
Report actual billing outcomes: call trackPurchaseCancelled for user cancellation, trackPurchaseFailed for errors, and trackPurchaseSuccess for confirmed success. A generic failure cannot be interpreted as a cancellation. When callbacks are reused by a follow-up, omit optional purchase paywallId/triggerKey overrides unless they identify the currently displayed paywall; the SDK records the purchase context on the button tap. Hardcoding the first paywall's IDs attributes later outcomes to the wrong screen. Cooldown is a minimum interval between successful follow-up attempts; it is not added to the first attempt's delay.
Purchase success rules follow host-confirmed completion calls. Existing setAutomationActionHandler integrations take precedence over the default runner, preventing double presentation.
Caching, prefetching & lifecycle triggers
The SDK caches content on disk. Use prefetching and MySuperWallHost to prepare views before presentation. Every presentation still requires a successful server delivery decision; offline cached content cannot authorize display.
Built-in lifecycle triggers include:
mysuperwall.install,mysuperwall.update,mysuperwall.sessionmysuperwall.session_3/5/10/20/50mysuperwall.day_1/3/7/14/30
Automatic lifecycle presentation reuses the purchase, restore and link handlers supplied to handleTrigger or presentPreparedPaywallIfAvailable. Manual presentation semantics are unchanged: omitted callbacks remain omitted. For automatic presentation, a bundle inferred from a manual call is usable only while its supplied context remains mounted; prepared calls without a context cannot register a safe owner. Explicit setPresentationHandlers registers app-level service/controller handlers, takes precedence over inferred callbacks, and remains valid until SDK disposal or replacement. The host only controls display readiness:
MySuperWallHost(
lifecycleReady: onboardingComplete,
child: child ?? const SizedBox.shrink(),
)
If an automatic lifecycle paywall must appear before any manual presentation has registered billing handlers, register the existing application handlers once with client.setPresentationHandlers(onPurchase: buy, onRestore: restorePurchases). Automatic display waits until a purchase handler is available; no billing callbacks are required on the host. Dismiss callbacks remain specific to each manual presentation. The SDK supports mysuperwall.foreground and configurable mysuperwall.session_N, mysuperwall.day_N, and mysuperwall.active_minutes_N keys. Active minutes exclude background time. A new session is counted on SDK initialization and on a foreground return after at least 30 minutes in the background. Days are measured from the SDK's first initialization.
Call handleLifecycleTriggers(context, onPurchase: ..., onRestore: ...) after the UI is ready to evaluate these events manually. One-time events are retained for up to 30 days until a matching paywall is presented; the recurring session event is evaluated for the current initialization. Session/active-time checkpoints retain 30 days plus a baseline. Custom rules created later use the recorded crossing date, rather than the rule creation date. Continuous active intervals are compacted; active-time crossing dates are interpolated within the recorded interval (checkpoint resolution is normally one minute). Crossings before history collection began cannot be recovered and are reported as historical_timestamp_unavailable; no synthetic date is assigned. Debugger lifecycle events distinguish received, pending, matched, shown, rejected and expired states.
Fetching or preparing a paywall is separate from displaying it. Analytics reflects SDK-reported activity; it is not independent proof of screen visibility or a store transaction.
Dependency compatibility
| Dependency | Accepted range |
|---|---|
package_info_plus |
>=9.0.0 <11.0.0 |
device_info_plus |
>=11.5.0 <14.0.0 |
Your app can retain compatible constraints such as ^9.0.0 and ^11.5.0. Pub selects versions satisfying both your app and the SDK. Other dependency and platform/toolchain requirements also apply. Future major versions require API review before extending the upper bound.
Advanced: optional store-proof verification
Optional store-proof verification
Your billing integration still handles purchase, acknowledgment/consumption and entitlements. After it reports a completed purchase, pass the store-issued proof:
Future<void> reportCompletedApplePurchase({
required String productId,
required String transactionId,
}) async {
await MySuperWallClient.instance.trackPurchaseSuccess(
productId: productId,
proof: StorePurchaseProof.apple(transactionId: transactionId),
onVerification: (result) {
// A transaction proof is not authorization to grant a user entitlement.
// result.retryable identifies transport/server/rate-limit failures.
},
);
}
Use StorePurchaseProof.googleProduct(purchaseToken: token) for Google one-time
products or StorePurchaseProof.googleSubscription(purchaseToken: token) for
subscriptions. Supply the purchase token, not the Google order ID or full
JSON receipt. Apple needs the transaction ID, not its base64 app receipt.
Extract these values from your own billing provider's completed-purchase result;
the SDK does not depend on RevenueCat or any particular billing package.
Existing trackPurchaseSuccess calls remain compatible and send unverified
analytics when no proof is supplied. onVerification is optional and receives
verification separately from the billing outcome. The original Future<void>
return type is preserved. A verification failure never marks a successful store
payment as a billing failure.
To retry only verification without another reported analytics event:
Future<PurchaseVerificationResult> retryAppleVerification({
required String productId,
required String transactionId,
}) => MySuperWallClient.instance.verifyPurchase(
productId: productId,
proof: StorePurchaseProof.apple(transactionId: transactionId),
);
Initialize with the dashboard appKey; verification requires an explicit key.
Configure the matching app's encrypted Apple/Google store connection first.
Requests use the existing HTTP client and timeout. Network/timeouts/server errors
get at most three attempts with bounded backoff; 4xx responses (including 429)
are returned without an immediate retry. No proof is persisted or logged by the
verification service. There is no durable offline retry queue; retry from the
billing integration when connectivity returns. Never put proofs in analytics
metadata or logs.
PurchaseVerificationResult includes verified, duplicate, environment,
amountUsd, reason, statusCode and retryable. Sandbox is determined by the
store. A null amount is unknown, not zero; Google amounts remain unknown when the
purchase API does not provide an actual charge. Do not combine direct-store and
RevenueCat totals for the same transactions. Backend contract:
backend/docs/purchase-verification-api.md in the MySuperWall backend repository.
Push notifications (optional, 1.3.0+)
Connect your Firebase project under Integrations → Push notifications, then create notifications under Notifications (for example "1 hour after a paywall is closed", opening a discount paywall). The SDK has no Firebase dependency: your app reads the FCM token and hands it over.
final messaging = FirebaseMessaging.instance;
await messaging.requestPermission();
final token = await messaging.getToken();
if (token != null) await client.setPushToken(token);
messaging.onTokenRefresh.listen(client.setPushToken);
// Tapping a notification shows the paywall of its trigger.
Future<void> openFrom(RemoteMessage message) async {
final key = await client.trackNotificationOpened(message.data);
if (key == null || !context.mounted) return;
await client.prepareTrigger(key); // its paywall is usually not prepared yet
if (context.mounted) await client.handleTrigger(context, triggerKey: key);
}
FirebaseMessaging.onMessageOpenedApp.listen(openFrom);
final initial = await messaging.getInitialMessage();
if (initial != null) await openFrom(initial);
- The token is registered for the current user and language and registered again by itself after
setUserId/setLocale(at most once a day otherwise). The notification text is chosen from that language. handleNotificationOpened(context, data, {...})doestrackNotificationOpenedplushandleTriggerin one call. Use the two-step form when you present paywalls with your own purchase callbacks.- Wait for your startup work (
setProductData,setLocale, warming) to finish before presenting a tapped notification's paywall, and callprepareTriggerfirst, otherwise the paywall can open as a blank page. - Call
clearPushToken()to stop notifications for the device. People who just bought are skipped on the server. - Android: keep
WAKE_LOCKin your manifest and requestPOST_NOTIFICATIONS; notifications show only while the app is in the background. iOS: enable the Push Notifications capability and upload your APNs key to Firebase.
Build something worth subscribing to.
Create your MySuperWall account →
Integration documentation · Changelog · GitHub
Illustrated paywalls in the header demonstrate visual possibilities, rather than bundled templates. Licensed under MIT.
SDK log level
SDK logging defaults to MySuperWallLogLevel.warning (warnings and errors),
including in release builds. Configure it during initialization:
await MySuperWallClient.initialize(
appKey: 'YOUR_APP_KEY',
logLevel: MySuperWallLogLevel.warning,
);
Use all for every SDK message, error for errors only, or none to disable
SDK logging. Change it at runtime with
MySuperWallClient.setLogLevel(MySuperWallLogLevel.none).
This setting does not change your application's own logs.
Paywall lifecycle
Read the current state of a trigger or listen for changes:
final sdk = MySuperWallClient.instance;
final state = sdk.paywallStateFor('location');
void onPaywallStatesChanged() {
final state = sdk.paywallStateFor('location');
// Update your application's controller using this state.
}
sdk.paywallStates.addListener(onPaywallStatesChanged);
// Remove the listener when your application's controller is disposed:
sdk.paywallStates.removeListener(onPaywallStatesChanged);
States are idle, loading, ready, presenting, visible, dismissed, and
failed. They describe each trigger, including background preparation.
paywall_shown is emitted at most once per presentation, after the opening
transition finishes and the WebView reports paint readiness, while the paywall
is active and the app is in the foreground. A readiness timeout alone does not
count as an impression. This does not delay the opening animation.
Deferred follow-up offers (local, unreleased source)
The Follow-up tab supports elapsed minutes, additional active-use minutes, next
session and next foreground return relative to the source event. Eligibility can
exclude the first session and require minimum sessions/days. Offers persist per
app/user for their configured validity (default 1,440 minutes), with per-rule UTC
daily limits and cancellation after confirmed purchase/restore. They never open
UI while the app is backgrounded or another paywall/billing overlay is active.
The host evaluates eligible offers at foreground readiness and approximately
every 15 seconds while active. Reliable restart presentation requires explicit
application-service billing handlers. Old SDKs skip advanced rules because only
updated source advertises deferred_follow_up_v1; included in 1.4.0.
See the scheduling contract.
Native presentation animation
Custom slide styles use the configured duration (0–4,000 ms) and curve on entry and exit in the updated Flutter source. System mode uses platform/SDK defaults; None has no transition, so the editor disables duration/curve for those modes. Reduced-motion settings suppress SDK transitions. The updated prepared path reverses its animation before detaching; programmatic route dismissal uses pop and waits for the reverse transition before permitting another presentation. Kapatılabilir / dismissable controls user back/edge gestures; the paywall's own close button and explicit application dismiss calls can still close it. The prepared back-interceptor respects the same policy. Dismiss analytics are emitted once per route close in the SDK wrapper. These SDK corrections remain local and unpublished; code analysis is not device animation verification.
Additional motion styles (Flutter 1.4.0 source)
Custom styles now share one PaywallMotion implementation across prepared and
route presentation; the WebView is retained as a stable child. No blur, screenshots,
HTML animation injection or repeated view creation is used for SDK transitions.
| Editor style | Wire value | Recommended entry/exit duration |
|---|---|---|
| Soft Rise | fade_slide_up |
340 ms |
| Soft Fade | fade |
220 ms |
| Scale In | scale_up |
320 ms |
| Scale Settle | scale_down |
300 ms |
| Slide From Left | from_left |
300 ms |
New choices select the smooth cubic curve (0.22, 1, 0.36, 1) and the recommended
duration. Existing paywall configurations are not automatically changed. An
additional easeInOutCubic curve is available for balanced motion. Scale changes
are deliberately small (0.96→1 and 1.035→1); Soft Rise moves 24 logical pixels and
fades. Opacity is clamped even for overshooting legacy curves. Existing slide
aliases remain backward compatible. Unknown effects fall back to System in older
SDKs; they do not implement the new effects. System still uses platform/SDK defaults,
None has no animation, and reduced-motion preferences disable SDK transitions.
Native-device smoothness and frame timing have not been verified in this change.
Libraries
- mysuperwall_sdk
- Remote mobile paywalls with cached targeting and host-managed purchases.
