easy_iap 0.2.0
easy_iap: ^0.2.0 copied to clipboard
Simple and elegant in-app purchase SDK for Flutter. Unified API for iOS App Store and Google Play Store with functional patterns (fold, when) and multi-language support.
/// Easy IAP Example
///
/// This example demonstrates the core features of the easy_iap package.
/// Copy and use directly in your project.
///
/// ## Key Features
/// - Simple initialization with EasyIap.init()
/// - IapBuilder for reactive UI based on ownership
/// - fold/when patterns for result handling
/// - Smart error handling with retry support
library;
import 'package:flutter/material.dart';
import 'package:easy_iap/easy_iap.dart';
// -----------------------------------------------------------------------------
// Configuration
// -----------------------------------------------------------------------------
/// Demo mode: When true, uses MockEasyIap for UI testing without real purchases
const kDemoMode = true;
/// EasyIap instance (initialized in main)
late final dynamic iap;
// -----------------------------------------------------------------------------
// Main
// -----------------------------------------------------------------------------
void main() async {
WidgetsFlutterBinding.ensureInitialized();
if (kDemoMode) {
// Demo mode: Use MockEasyIap for testing
final mockIap = MockEasyIap(
products: [
IapProduct.subscription('pro_monthly'),
IapProduct.subscription('pro_yearly'),
IapProduct.nonConsumable('remove_ads'),
IapProduct.consumable('gems_100'),
],
delay: const Duration(milliseconds: 800),
);
await mockIap.initialize();
iap = mockIap;
} else {
// Production: Use real EasyIap
// final realIap = await EasyIap.init(
// products: [
// IapProduct.subscription('pro_monthly'),
// IapProduct.subscription('pro_yearly'),
// IapProduct.nonConsumable('remove_ads'),
// IapProduct.consumable('gems_100'),
// ],
// );
// iap = realIap;
}
runApp(const App());
}
// -----------------------------------------------------------------------------
// App
// -----------------------------------------------------------------------------
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Easy IAP Example',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: const HomeScreen(),
);
}
}
// -----------------------------------------------------------------------------
// Home Screen
// -----------------------------------------------------------------------------
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Easy IAP'),
actions: [
IconButton(
onPressed: _showSubscriptionStatus,
icon: const Icon(Icons.card_membership),
tooltip: 'Subscription Status',
),
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
// Demo mode indicator
if (kDemoMode)
Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.orange.shade200),
),
child: Row(
children: [
Icon(Icons.science, color: Colors.orange.shade700),
const SizedBox(width: 12),
Expanded(
child: Text(
'Demo Mode - Using MockEasyIap',
style: TextStyle(color: Colors.orange.shade700),
),
),
],
),
),
// Subscriptions Section
_SectionHeader(title: 'Subscriptions'),
_ProductCard(
productId: 'pro_monthly',
title: 'Pro Monthly',
description: 'Unlock all features',
price: '\$9.99/month',
icon: Icons.star,
color: Colors.indigo,
),
_ProductCard(
productId: 'pro_yearly',
title: 'Pro Yearly',
description: 'Save 40% with annual plan',
price: '\$69.99/year',
icon: Icons.workspace_premium,
color: Colors.purple,
badge: 'BEST VALUE',
),
const SizedBox(height: 24),
// One-time Purchases Section
_SectionHeader(title: 'One-time Purchases'),
_ProductCard(
productId: 'remove_ads',
title: 'Remove Ads',
description: 'Enjoy ad-free experience forever',
price: '\$4.99',
icon: Icons.block,
color: Colors.green,
),
const SizedBox(height: 24),
// Consumables Section
_SectionHeader(title: 'Consumables'),
_ProductCard(
productId: 'gems_100',
title: '100 Gems',
description: 'Use gems to unlock special content',
price: '\$0.99',
icon: Icons.diamond,
color: Colors.cyan,
isConsumable: true,
),
const SizedBox(height: 24),
// Restore Purchases
Center(
child: TextButton.icon(
onPressed: _restorePurchases,
icon: const Icon(Icons.restore),
label: const Text('Restore Purchases'),
),
),
const SizedBox(height: 32),
],
),
);
}
void _showSubscriptionStatus() {
final hasSubscription = iap.hasActiveSubscription;
final ownedProducts = iap.ownedProductIds;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Purchase Status'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_statusRow(
'Subscription',
hasSubscription ? 'Active' : 'None',
hasSubscription ? Colors.green : Colors.grey,
),
const Divider(),
const Text(
'Owned Products:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
if (ownedProducts.isEmpty)
const Text('No products owned',
style: TextStyle(color: Colors.grey))
else
...ownedProducts.map((id) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
const Icon(Icons.check_circle,
size: 16, color: Colors.green),
const SizedBox(width: 8),
Text(id),
],
),
)),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
],
),
);
}
Widget _statusRow(String label, String value, Color color) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
value,
style: TextStyle(color: color, fontWeight: FontWeight.bold),
),
),
],
),
);
}
Future<void> _restorePurchases() async {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AlertDialog(
content: Row(
children: [
CircularProgressIndicator(),
SizedBox(width: 16),
Text('Restoring purchases...'),
],
),
),
);
final receipts = await iap.restore();
if (mounted) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
receipts.isEmpty
? 'No purchases to restore'
: 'Restored ${receipts.length} purchase(s)',
),
),
);
setState(() {}); // Refresh UI
}
}
}
// -----------------------------------------------------------------------------
// Section Header
// -----------------------------------------------------------------------------
class _SectionHeader extends StatelessWidget {
const _SectionHeader({required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.grey.shade700,
),
),
);
}
}
// -----------------------------------------------------------------------------
// Product Card
// -----------------------------------------------------------------------------
class _ProductCard extends StatefulWidget {
const _ProductCard({
required this.productId,
required this.title,
required this.description,
required this.price,
required this.icon,
required this.color,
this.badge,
this.isConsumable = false,
});
final String productId;
final String title;
final String description;
final String price;
final IconData icon;
final Color color;
final String? badge;
final bool isConsumable;
@override
State<_ProductCard> createState() => _ProductCardState();
}
class _ProductCardState extends State<_ProductCard> {
bool _loading = false;
bool get _isOwned => iap.isOwned(widget.productId);
Future<void> _purchase() async {
setState(() => _loading = true);
final result = await iap.purchase(widget.productId);
if (mounted) {
setState(() => _loading = false);
// Using when pattern for success/cancelled/failure handling
result.when(
success: (receipt) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Purchased: ${receipt.product.id}'),
backgroundColor: Colors.green,
),
);
},
cancelled: () {
// User cancelled - usually ignore
},
failure: (failure) => _handleFailure(failure),
);
}
}
void _handleFailure(IapFailure failure) {
if (failure.shouldIgnore) return;
if (failure.canRetry) {
// Show retry dialog for network errors
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Purchase Failed'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(failure.displayMessage),
const SizedBox(height: 12),
Text(
'Please check your connection and try again.',
style: TextStyle(color: Colors.grey.shade600, fontSize: 13),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () {
Navigator.pop(context);
_purchase();
},
child: const Text('Retry'),
),
],
),
);
} else if (failure.requiresUserAction) {
// Payment settings issue
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(failure.displayMessage),
backgroundColor: Colors.orange,
action: SnackBarAction(
label: 'Settings',
textColor: Colors.white,
onPressed: () {
// Open payment settings
},
),
),
);
} else {
// Other errors
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(failure.displayMessage),
backgroundColor: Colors.red,
),
);
}
}
@override
Widget build(BuildContext context) {
final isOwned = _isOwned;
return Card(
margin: const EdgeInsets.only(bottom: 12),
clipBehavior: Clip.antiAlias,
child: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// Icon
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: widget.color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(widget.icon, color: widget.color, size: 28),
),
const SizedBox(width: 16),
// Title & Description
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.title,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
widget.description,
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 13,
),
),
],
),
),
// Price & Button
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (!isOwned || widget.isConsumable)
Text(
widget.price,
style: TextStyle(
fontWeight: FontWeight.bold,
color: widget.color,
),
),
const SizedBox(height: 8),
if (isOwned && !widget.isConsumable)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.green.shade200),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.check_circle,
size: 16,
color: Colors.green.shade700,
),
const SizedBox(width: 4),
Text(
'Owned',
style: TextStyle(
color: Colors.green.shade700,
fontWeight: FontWeight.w500,
fontSize: 13,
),
),
],
),
)
else
SizedBox(
height: 36,
child: FilledButton(
onPressed: _loading ? null : _purchase,
style: FilledButton.styleFrom(
backgroundColor: widget.color,
padding: const EdgeInsets.symmetric(horizontal: 16),
),
child: _loading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text('Buy'),
),
),
],
),
],
),
),
// Badge
if (widget.badge != null)
Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: widget.color,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(8),
),
),
child: Text(
widget.badge!,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
);
}
}
// -----------------------------------------------------------------------------
// IapBuilder Example (commented)
// -----------------------------------------------------------------------------
/*
/// Example: Using IapBuilder for reactive UI
///
/// ```dart
/// IapBuilder(
/// iap: iap,
/// productId: 'remove_ads',
/// owned: () => const AdFreeContent(),
/// notOwned: () => const ContentWithAds(),
/// loading: () => const LoadingSpinner(),
/// )
/// ```
///
/// Example: Using SubscriptionBuilder
///
/// ```dart
/// SubscriptionBuilder(
/// iap: iap,
/// subscribed: (subscriptions) => PremiumContent(subscriptions),
/// notSubscribed: () => const UpgradePrompt(),
/// )
/// ```
///
/// Example: Using PurchaseListener
///
/// ```dart
/// PurchaseListener(
/// iap: iap,
/// onPurchase: (receipt) {
/// analytics.logPurchase(receipt);
/// showSuccessAnimation();
/// },
/// child: const YourContent(),
/// )
/// ```
*/