The AppDNA Flutter SDK provides a server-driven paywall module that presents paywalls configured in the AppDNA Console. Paywalls are delivered via the config bundle, rendered natively, and automatically handle purchase flows through the billing module.
Paywall Module
Access the paywall module through AppDNA.paywall, which returns an AppDNAPaywallModule instance:
final paywall = AppDNA.paywall;
Present a Paywall
Present a specific paywall by its ID:
await AppDNA.paywall.present(
"premium_paywall",
context: PaywallContext(placement: "settings"),
);
Or via the static method:
await AppDNA.presentPaywall("premium_paywall");
Paywalls are configured in the AppDNA Console under Monetization > Paywalls. The paywall ID you pass to present() must match the ID configured in the Console.
PaywallContext
The PaywallContext class provides additional context about where the paywall was triggered:
| Property | Type | Description |
|---|
placement | String? | Where the paywall was shown (e.g., “settings”, “onboarding”, “feature_gate”) |
customData | Map<String, dynamic>? | Custom key-value pairs for paywall personalization |
Example with Context
await AppDNA.paywall.present(
"premium_paywall",
context: PaywallContext(
placement: "feature_gate",
customData: {
"feature_name": "advanced_analytics",
"user_tier": "free",
},
),
);
The placement value is used for analytics to track conversion rates across different paywall placements. Use consistent placement names across your app for accurate reporting in the Console.
AppDNAPaywallDelegate
Implement the AppDNAPaywallDelegate abstract class to receive paywall lifecycle callbacks:
abstract class AppDNAPaywallDelegate {
void onPaywallPresented(String paywallId);
void onPaywallAction(String paywallId, String action);
void onPaywallPurchaseStarted(String paywallId, String productId);
void onPaywallPurchaseCompleted(
String paywallId,
String productId,
Map<String, dynamic> transaction,
);
void onPaywallPurchaseFailed(String paywallId, String error);
void onPaywallDismissed(String paywallId);
}
Delegate Methods
| Method | Description |
|---|
onPaywallPresented | Called when the paywall is displayed to the user |
onPaywallAction | Called when the user interacts with a paywall element |
onPaywallPurchaseStarted | Called when the user initiates a purchase from the paywall |
onPaywallPurchaseCompleted | Called when a purchase is successfully completed |
onPaywallPurchaseFailed | Called when a purchase fails or is cancelled |
onPaywallDismissed | Called when the user closes the paywall |
Set the Delegate
AppDNA.paywall.setDelegate(myPaywallDelegate);
Example Implementation
class MyPaywallHandler implements AppDNAPaywallDelegate {
@override
void onPaywallPresented(String paywallId) {
print("Paywall presented: $paywallId");
}
@override
void onPaywallAction(String paywallId, String action) {
print("Paywall action: $action");
// Handle custom actions like "learn_more", "terms_of_service"
}
@override
void onPaywallPurchaseStarted(String paywallId, String productId) {
print("Purchase started: $productId");
// Show loading indicator
}
@override
void onPaywallPurchaseCompleted(
String paywallId,
String productId,
Map<String, dynamic> transaction,
) {
print("Purchase completed: $productId");
// Unlock premium features, dismiss paywall
}
@override
void onPaywallPurchaseFailed(String paywallId, String error) {
print("Purchase failed: $error");
// Show error to user
}
@override
void onPaywallDismissed(String paywallId) {
print("Paywall dismissed: $paywallId");
}
}
Full Example
import 'package:appdna_sdk/appdna_sdk.dart';
import 'package:flutter/material.dart';
class PremiumFeatureScreen extends StatefulWidget {
const PremiumFeatureScreen({super.key});
@override
State<PremiumFeatureScreen> createState() => _PremiumFeatureScreenState();
}
class _PremiumFeatureScreenState extends State<PremiumFeatureScreen>
implements AppDNAPaywallDelegate {
bool _isPremium = false;
@override
void initState() {
super.initState();
AppDNA.paywall.setDelegate(this);
_checkAccess();
}
Future<void> _checkAccess() async {
final active = await AppDNA.billing.hasActiveSubscription();
setState(() => _isPremium = active);
}
Future<void> _showPaywall() async {
await AppDNA.paywall.present(
"premium_paywall",
context: PaywallContext(
placement: "feature_gate",
customData: {"feature": "advanced_analytics"},
),
);
}
@override
void onPaywallPresented(String paywallId) {}
@override
void onPaywallAction(String paywallId, String action) {}
@override
void onPaywallPurchaseStarted(String paywallId, String productId) {}
@override
void onPaywallPurchaseCompleted(
String paywallId,
String productId,
Map<String, dynamic> transaction,
) {
setState(() => _isPremium = true);
}
@override
void onPaywallPurchaseFailed(String paywallId, String error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Purchase failed: $error")),
);
}
@override
void onPaywallDismissed(String paywallId) {}
@override
Widget build(BuildContext context) {
if (!_isPremium) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("This feature requires a premium subscription."),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _showPaywall,
child: const Text("Unlock Premium"),
),
],
),
),
);
}
return const Scaffold(
body: Center(
child: Text("Premium content here"),
),
);
}
}
Paywalls depend on the billing module being properly configured. Ensure AppDNABillingProvider is set in your AppDNAOptions and that products are configured in both the App Store / Google Play and the AppDNA Console.