flutter_in_app_purchase_helper 0.1.0
flutter_in_app_purchase_helper: ^0.1.0 copied to clipboard
A Flutter in-app purchase helper that handles product lookup, purchase flows, restores and transaction completion.
import 'package:flutter/material.dart';
import 'package:flutter_in_app_purchase_helper/flutter_in_app_purchase_helper.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'In-App Purchase Helper Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const PaymentScreen(),
);
}
}
class PaymentScreen extends StatefulWidget {
const PaymentScreen({super.key});
@override
State<PaymentScreen> createState() => _PaymentScreenState();
}
class _PaymentScreenState extends State<PaymentScreen> {
final FlutterInAppPurchaseHelper _helper = FlutterInAppPurchaseHelper();
List<ProductDetails> _products = <ProductDetails>[];
/// The selected product itself, rather than a title that has to be kept in
/// sync with a second field.
ProductDetails? _selected;
bool _busy = false;
@override
void initState() {
super.initState();
_initializePurchases();
}
Future<void> _initializePurchases() async {
// Replace these with the product IDs you configured in Play Console and
// App Store Connect.
await _helper.initialize(
productIds: <String>{'PRODUCT_ID_1', 'PRODUCT_ID_2'},
onProductsFetched: (List<ProductDetails> products) {
if (!mounted) {
return;
}
setState(() => _products = products);
},
onPurchaseSuccess: (PurchaseDetails purchase) {
// Verify `purchase.verificationData` against your server before
// unlocking anything in a production app.
setState(() => _busy = false);
_show('Unlocked ${purchase.productID}');
},
onPurchasePending: (PurchaseDetails purchase) {
_show('Waiting for approval…');
},
onPurchaseCanceled: (PurchaseDetails purchase) {
setState(() => _busy = false);
},
onPurchaseError: (String error) {
setState(() => _busy = false);
_show('Error: $error');
},
);
}
/// Purchase updates can arrive after this screen is popped.
void _show(String message) {
if (!mounted) {
return;
}
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(message)));
}
Future<void> _buy() async {
final ProductDetails? product = _selected;
if (product == null) {
_show('Please select a plan.');
return;
}
setState(() => _busy = true);
final bool sent = await _helper.buyProduct(product, (String error) {
setState(() => _busy = false);
_show('Error: $error');
});
if (!sent) {
setState(() => _busy = false);
}
}
@override
void dispose() {
_helper.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Choose a plan'),
actions: <Widget>[
// The App Store requires a restore entry point for non-consumables.
TextButton(
onPressed: () => _helper.restorePurchases(onError: _show),
child: const Text('Restore'),
),
],
),
body: _products.isEmpty
? const Center(child: Text('No products available.'))
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _products.length,
itemBuilder: (BuildContext context, int index) {
final ProductDetails product = _products[index];
return PlanCard(
product: product,
isActive: _selected?.id == product.id,
onTap: () => setState(
() => _selected =
_selected?.id == product.id ? null : product,
),
);
},
),
bottomNavigationBar: Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
onPressed: _busy || _selected == null ? null : _buy,
child: _busy
? const SizedBox.square(
dimension: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Subscribe'),
),
),
);
}
}
class PlanCard extends StatelessWidget {
const PlanCard({
super.key,
required this.product,
required this.isActive,
required this.onTap,
});
final ProductDetails product;
final bool isActive;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Card(
elevation: isActive ? 8 : 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: isActive
? Theme.of(context).colorScheme.primary
: Colors.transparent,
width: 2,
),
),
child: ListTile(
onTap: onTap,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
leading: Icon(
isActive ? Icons.check_circle : Icons.circle_outlined,
color: Theme.of(context).colorScheme.primary,
),
title: Text(product.title, overflow: TextOverflow.ellipsis),
subtitle: Text(product.description, overflow: TextOverflow.ellipsis),
trailing: Text(
product.price,
style: Theme.of(context).textTheme.titleMedium,
),
),
);
}
}