Skip to main content
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:
PropertyTypeDescription
placementString?Where the paywall was shown (e.g., “settings”, “onboarding”, “feature_gate”)
customDataMap<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

MethodDescription
onPaywallPresentedCalled when the paywall is displayed to the user
onPaywallActionCalled when the user interacts with a paywall element
onPaywallPurchaseStartedCalled when the user initiates a purchase from the paywall
onPaywallPurchaseCompletedCalled when a purchase is successfully completed
onPaywallPurchaseFailedCalled when a purchase fails or is cancelled
onPaywallDismissedCalled 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.