Get from zero Adapty setup to displaying a paywall and processing purchases in under 60 minutes.
Pre-requisites
Before you begin, make sure you have:
- A working Flutter app with iOS deployment configured
- Flutter SDK 3.0 or later
- iOS development environment (Xcode, CocoaPods)
- At least one in-app purchase product created in App Store Connect
- An Adapty account with your app added to the dashboard, including at least one product, paywall, and placement
Project files
To follow along with this tutorial, download the project files from GitHub. The starter
branch contains the app without Adapty integration, while the main
branch contains the finished project.
Install the Adapty SDK in your project
Add the Adapty Flutter SDK to your pubspec.yaml
file:
dependencies:
flutter:
sdk: flutter
# Add the following to your dependencies list
adapty_flutter: ^3.10.0
Run flutter pub get
to install the dependencies.
iOS Configuration
Since Flutter apps require native iOS configuration for in-app purchases, run the following:
cd ios && pod install && cd ..
Initialize Adapty in your app
First, add these constants to your existing constants management system:
class AppConstants {
static const String apiKey = "API key goes here";
static const String accessLevelId = "premium";
static const String placementId = "on_tap_history";
}
🔐 Find your public SDK key: Adapty Dashboard > Settings > API Keys
Next, initialize Adapty in your main.dart
file:
// Add these import statements to the existing ones:
import 'package:adapty_flutter/adapty_flutter.dart';
import 'constants.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
try {
// Initialize Adapty with API key
await Adapty().activate(configuration: AdaptyConfiguration(
apiKey: AppConstants.apiKey,
));
// Set up profile update listener
Adapty().didUpdateProfileStream.listen((profile) {
profileManager.onProfileUpdate(profile);
});
} catch (e) {
throw Exception('Failed to initialize Adapty: $e');
}
runApp(FocusJournalApp(profileManager: profileManager));
}
This code configures and activates the Adapty SDK.
Set up profile handling
Next, edit the file lib/providers/profile_manager.dart
to add Adapty integration.
The primary changes are the addition of the Adapty profile property, the call to update the profile, and the updating of premium status based on the contents of the profile object.
// Add these imports to the existing ones:
import 'package:adapty_flutter/adapty_flutter.dart';
import '../constants.dart';
class ProfileManager extends ChangeNotifier {
// Add this property to your list of properties
AdaptyProfile? _customerProfile;
...
// Add this getter
AdaptyProfile? get customerProfile => _customerProfile;
...
Future<void> refreshProfile() async {
_setLoading(true);
_setError(null);
try {
// Remove the network call delay and replace with:
final profile = await Adapty().getProfile();
_customerProfile = profile;
_updatePremiumStatus();
_setLoading(false);
} catch (e) {
_setError("Failed to refresh profile: $e");
_setLoading(false);
}
}
// Add this function to your profile_manager
void _updatePremiumStatus() {
if (_customerProfile == null) {
_isPremium = false;
return;
}
final accessLevel = _customerProfile!.accessLevels[AppConstants.accessLevelId];
if (accessLevel == null) {
_isPremium = false;
return;
}
// Check for active, grace period, or lifetime access
_isPremium = accessLevel.isActive ||
accessLevel.isInGracePeriod ||
accessLevel.isLifetime;
notifyListeners();
}
// Add this function to your profile_manager:
void onProfileUpdate(AdaptyProfile profile) {
_customerProfile = profile;
_updatePremiumStatus();
}
// All other functions in starter remain the same
}
Create the paywall view
Create a file in lib/views
called paywall_view.dart
with the following contents:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:adapty_flutter/adapty_flutter.dart';
import '../providers/profile_manager.dart';
import '../constants.dart';
class PaywallView extends StatefulWidget {
const PaywallView({super.key});
@override
State<PaywallView> createState() => _PaywallViewState();
}
class _PaywallViewState extends State<PaywallView> {
// AdaptyPaywall stored for potential future use (analytics, etc.)
List<AdaptyPaywallProduct>? _products;
bool _isLoading = true;
bool _isPurchasing = false;
String? _error;
@override
void initState() {
super.initState();
_loadPaywall();
}
Future<void> _loadPaywall() async {
try {
setState(() {
_isLoading = true;
_error = null;
});
final paywall = await Adapty().getPaywall(
placementId: AppConstants.placementId,
);
final products = await Adapty().getPaywallProducts(paywall: paywall);
setState(() {
_products = products;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = 'Failed to load paywall: $e';
_isLoading = false;
});
}
}
Future<void> _makePurchase(AdaptyPaywallProduct product) async {
setState(() {
_isPurchasing = true;
_error = null;
});
try {
final purchaseResult = await Adapty().makePurchase(product: product);
switch (purchaseResult) {
case AdaptyPurchaseResultSuccess(profile: final profile):
if (mounted) {
context.read<ProfileManager>().onProfileUpdate(profile);
// Show success and close paywall
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Purchase successful! Welcome to Premium!'),
backgroundColor: Colors.green,
duration: Duration(seconds: 3),
),
);
Navigator.of(
context,
).pop(true); // Return true to indicate successful purchase
}
break;
case AdaptyPurchaseResultUserCancelled():
case AdaptyPurchaseResultPending():
}
} catch (e) {
if (mounted) {
setState(() {
_isPurchasing = false;
});
// Check if it's a user cancellation (don't show error for cancellations)
final errorMessage = e.toString();
if (errorMessage.contains('USER_CANCELLED')) {
// User cancelled - just reset the state, don't show error
return;
}
// For actual errors, show the error message
setState(() {
_error = 'Purchase failed: $e';
});
}
}
}
Future<void> _restorePurchases() async {
setState(() {
_isPurchasing = true;
_error = null;
});
try {
final profile = await Adapty().restorePurchases();
if (mounted) {
context.read<ProfileManager>().onProfileUpdate(profile);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Purchases restored successfully!'),
backgroundColor: Colors.green,
duration: Duration(seconds: 3),
),
);
Navigator.of(context).pop(true);
}
} catch (e) {
if (mounted) {
setState(() {
_error = 'Restore failed: $e';
_isPurchasing = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Upgrade to Premium'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _buildPaywallContent(),
);
}
Widget _buildPaywallContent() {
if (_error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 16),
Text(
_error ?? 'Unknown error',
textAlign: TextAlign.center,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
const SizedBox(height: 16),
ElevatedButton(onPressed: _loadPaywall, child: const Text('Retry')),
],
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Premium features list
Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Icon(Icons.star, size: 48, color: Colors.amber.shade600),
const SizedBox(height: 16),
const Text(
'Unlock Premium Features',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 24),
_buildFeatureItem(
icon: Icons.history,
title: 'View Entry History',
description: 'Access all your past journal entries',
),
_buildFeatureItem(
icon: Icons.cloud_sync,
title: 'Cloud Sync',
description: 'Keep your entries synced across devices',
),
_buildFeatureItem(
icon: Icons.insights,
title: 'Insights & Analytics',
description: 'Track your journaling patterns',
),
],
),
),
),
const SizedBox(height: 24),
// Products list
if (_products != null && _products!.isNotEmpty) ...[
const Text(
'Choose Your Plan',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
..._products!.map((product) => _buildProductCard(product)),
],
const SizedBox(height: 24),
// Restore purchases button
TextButton(
onPressed: _isPurchasing ? null : _restorePurchases,
child: _isPurchasing
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Restore Purchases'),
),
],
),
);
}
Widget _buildFeatureItem({
required IconData icon,
required String title,
required String description,
}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Icon(icon, color: Theme.of(context).colorScheme.primary, size: 24),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(fontWeight: FontWeight.w600),
),
Text(
description,
style: TextStyle(color: Colors.grey.shade600, fontSize: 14),
),
],
),
),
],
),
);
}
Widget _buildProductCard(AdaptyPaywallProduct product) {
return Card(
margin: const EdgeInsets.symmetric(vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.localizedTitle,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
if (product.localizedDescription.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
product.localizedDescription,
style: TextStyle(color: Colors.grey.shade600),
),
],
],
),
),
Text(
product.price.localizedString ?? 'Price unavailable',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
],
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _isPurchasing ? null : () => _makePurchase(product),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: _isPurchasing
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Text('Subscribe', style: TextStyle(fontSize: 16)),
),
],
),
),
);
}
}
The above sets up a basic paywall view with subscription options and pricing. It’s designed to showcase the basics of interfacing with the Adapty SDK to get information on products and present them to the user. You also have the option of using our Paywall Builder feature to design and deploy paywalls from the Adapty dashboard without requiring a new submission to the App or Play stores.

Update the main journal interface to show a paywall
Edit the home_view.dart file to call the paywall instead of simulating a purchase:
class _HomeViewState extends State<HomeView> {
...
void _viewHistory() {
final profileManager = context.read<ProfileManager>();
if (profileManager.isPremium) {
Navigator.of(context).pushNamed('/history');
} else {
// Remove the call to show the mock dialog and replace with:
_showPaywall();
}
}
// Add this function
void _showPaywall() async {
if (!mounted) return;
final result = await Navigator.of(context).pushNamed('/paywall');
// If purchase was successful, refresh profile and navigate to history
if (result == true && mounted) {
final profileManager = context.read<ProfileManager>();
await profileManager.refreshProfile();
if (profileManager.isPremium && mounted) {
Navigator.of(context).pushNamed('/history');
}
}
}
...
}
class FocusJournalApp extends StatelessWidget {
...
Widget build(BuildContext context) {
return ChangeNotifierProvider<ProfileManager>.value(
...
child: MaterialApp(
...
routes: {
...
// Add this route to the Paywall View under the route for the History View
'/paywall': (context) => const PaywallView(),
},
}
}
Check for purchases on app launch
The initialization code we added to main.dart
handles checking subscription status when the app launches. The ProfileManager
automatically updates the premium status based on the user’s access levels.
Here’s how the purchase flow works:
- App Launch:
refreshProfile()
gets the latest subscription status - Profile Updates: The SDK automatically notifies your app when purchases are made
- Access Control: The
isPremium
flag controls which features are available
Test your integration
Build and run your app:
flutter build ios --no-codesign
The expected flow is as follows:
- Load subscription status from Adapty on launch
- Show premium features to subscribers
- Display paywalls to non-subscribers
- Process purchases and unlock content immediately
Cross-Platform Considerations
While this guide focuses on iOS, the Flutter implementation works on Android too. For Android deployment, you’ll need:
- Google Play Console setup for in-app purchases
- Android-specific product configuration in Adapty
- Additional gradle configuration for billing permissions
The core Dart code remains identical across platforms.
Congrats!
At this point, your Flutter app:
- Loads paywalls from Adapty using your remote configuration
- Lets users purchase subscriptions through native iOS interfaces
- Unlocks premium features based on access levels
- Handles subscription status automatically across app launches
- Uses Flutter best practices with Provider state management
Learn more about Flutter best practices with Adapty in our documentation.