stripe_smart_checkout 0.1.1
stripe_smart_checkout: ^0.1.1 copied to clipboard
Modern Flutter package for Stripe Checkout integration with support for one-time payments and subscriptions. Clean architecture, type-safe, cross-platform.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:stripe_smart_checkout/stripe_smart_checkout.dart';
void main() {
runApp(const StripeCheckoutExampleApp());
}
class StripeCheckoutExampleApp extends StatelessWidget {
const StripeCheckoutExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Stripe Smart Checkout Example',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const CheckoutExampleScreen(),
);
}
}
class CheckoutExampleScreen extends StatefulWidget {
const CheckoutExampleScreen({super.key});
@override
State<CheckoutExampleScreen> createState() => _CheckoutExampleScreenState();
}
class _CheckoutExampleScreenState extends State<CheckoutExampleScreen> {
// Configure your Stripe keys here
late final StripeCheckoutConfig _stripeConfig;
@override
void initState() {
super.initState();
_stripeConfig = const StripeCheckoutConfig(
publicKey:
'pk_test_51SdSchSjB5swWNpegdfh2fsOYxy9VDFpDdT0UwOHjOvfUDggpDJz8Mz1RdvkqDz4e6m1850DKMJoExkJHsGwHQcJ00bDmrZwHT',
secretKey:
'sk_test_51SdSchSjB5swWNpeJMcojb4Tchbw41J1MWfbtgdH7Ip0E3u3gB4U0DimOh0X2Qia27oublEjZcHBSMhANyHYXIBR00yxbdFjv8',
isTestMode: true,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Stripe Smart Checkout Demo'),
centerTitle: true,
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
// Package Info Card
Card(
color: Colors.blue.shade50,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'๐ Stripe Smart Checkout',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Text(
'A complete Flutter package for Stripe Checkout with support for both one-time payments and recurring subscriptions.',
),
const SizedBox(height: 16),
const Text(
'โ๏ธ Setup Required:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
const Text(
'1. Get your Stripe API keys from dashboard.stripe.com/apikeys\n'
'2. Replace the placeholder keys in main.dart (lines 37-38)\n'
'3. Tap any product below to start a checkout!',
style: TextStyle(fontSize: 12),
),
],
),
),
),
const SizedBox(height: 24),
// One-Time Payments Section
Text(
'๐ณ One-Time Payments',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Text(
'Single transaction checkout for products and services',
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 16),
_buildProductCard(
context,
icon: Icons.school,
name: 'Flutter Course',
description: 'Advanced Flutter Development Masterclass',
price: 49.99,
isSubscription: false,
),
_buildProductCard(
context,
icon: Icons.book,
name: 'Clean Architecture Book',
description: 'Comprehensive Guide to Software Architecture',
price: 29.99,
isSubscription: false,
),
_buildProductCard(
context,
icon: Icons.laptop,
name: 'Premium Development Laptop',
description: 'High-Performance Laptop for Developers',
price: 1499.99,
isSubscription: false,
),
const SizedBox(height: 32),
// Subscriptions Section
Text(
'๐ Subscription Plans',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Text(
'Recurring payments with flexible billing intervals',
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 16),
_buildProductCard(
context,
icon: Icons.star_outline,
name: 'Starter Plan',
description: 'Weekly subscription - Perfect for trying out',
price: 9.99,
isSubscription: true,
billingPeriod: 'week',
trialDays: 7,
),
_buildProductCard(
context,
icon: Icons.star_half,
name: 'Pro Plan',
description: 'Monthly subscription with advanced features',
price: 29.99,
isSubscription: true,
billingPeriod: 'month',
),
_buildProductCard(
context,
icon: Icons.star,
name: 'Premium Plan',
description: 'Annual subscription - Save 20%!',
price: 299.99,
isSubscription: true,
billingPeriod: 'year',
trialDays: 14,
),
const SizedBox(height: 32),
// Test Cards Info
Card(
color: Colors.amber.shade50,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'๐งช Test Cards',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 8),
const Text(
'Use these Stripe test cards:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
_buildTestCardRow('4242 4242 4242 4242', 'Success'),
_buildTestCardRow(
'4000 0000 0000 9995', 'Insufficient funds'),
_buildTestCardRow('4000 0000 0000 0002', 'Card declined'),
const SizedBox(height: 8),
const Text(
'โข Use any future expiration date (e.g., 12/34)\n'
'โข Use any 3-digit CVC\n'
'โข Use any billing ZIP code',
style: TextStyle(fontSize: 12),
),
],
),
),
),
],
),
);
}
Widget _buildProductCard(
BuildContext context, {
required IconData icon,
required String name,
required String description,
required double price,
required bool isSubscription,
String? billingPeriod,
int? trialDays,
}) {
final priceLabel = isSubscription
? '\$${price.toStringAsFixed(2)}/${billingPeriod ?? 'month'}'
: '\$${price.toStringAsFixed(2)}';
final trialLabel = trialDays != null ? ' โข $trialDays-day trial' : '';
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
onTap: () => _handleCheckout(
context,
name: name,
description: description,
price: price,
isSubscription: isSubscription,
billingPeriod: billingPeriod,
trialDays: trialDays,
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSubscription
? Colors.purple.shade50
: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: isSubscription ? Colors.purple : Colors.blue,
size: 32,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
description,
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 13,
),
),
const SizedBox(height: 8),
Row(
children: [
Text(
priceLabel,
style: TextStyle(
color: Colors.green.shade700,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
if (trialLabel.isNotEmpty)
Text(
trialLabel,
style: TextStyle(
color: Colors.orange.shade700,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
),
Icon(
Icons.arrow_forward_ios,
size: 16,
color: Colors.grey.shade400,
),
],
),
),
),
);
}
Widget _buildTestCardRow(String cardNumber, String result) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
const Text('โข ', style: TextStyle(fontSize: 12)),
Text(
cardNumber,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
Text(
' โ $result',
style: const TextStyle(fontSize: 12),
),
],
),
);
}
void _handleCheckout(
BuildContext context, {
required String name,
required String description,
required double price,
required bool isSubscription,
String? billingPeriod,
int? trialDays,
}) {
// Determine checkout mode
final mode =
isSubscription ? CheckoutMode.subscription : CheckoutMode.payment;
// Create checkout item
final CheckoutItem item;
if (isSubscription) {
// Convert billing period to interval
BillingInterval interval = BillingInterval.month;
if (billingPeriod == 'week') {
interval = BillingInterval.week;
} else if (billingPeriod == 'year') {
interval = BillingInterval.year;
} else if (billingPeriod == 'day') {
interval = BillingInterval.day;
}
item = CheckoutItem.subscription(
name: name,
price: price,
description: description,
billingInterval: interval,
trialPeriodDays: trialDays,
);
} else {
item = CheckoutItem.payment(
name: name,
price: price,
quantity: 1,
description: description,
);
}
// Navigate to checkout
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => StripeCheckoutWidget(
config: _stripeConfig,
amount: price,
currency: 'usd',
mode: mode,
items: [item],
customerEmail: '[email protected]',
onSuccess: (session) {
// Pop the checkout screen first
//Navigator.of(context).pop();
// Add a small delay to ensure navigation animation completes
Future.delayed(const Duration(milliseconds: 300), () {
if (context.mounted) {
_showSuccessDialog(
context, session, name, price, isSubscription);
}
});
},
onError: (error) {
// Pop the checkout screen first
Navigator.of(context).pop();
// Add a small delay to ensure navigation animation completes
Future.delayed(const Duration(milliseconds: 300), () {
if (context.mounted) {
_showErrorDialog(context, error);
}
});
},
),
),
);
}
void _showSuccessDialog(
BuildContext context,
CheckoutSession session,
String productName,
double amount,
bool isSubscription,
) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
Icon(Icons.check_circle, color: Colors.green.shade600, size: 28),
const SizedBox(width: 8),
const Text('Success!'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isSubscription
? 'Your subscription has been activated successfully!'
: 'Payment completed successfully!',
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 16),
_buildInfoRow('Product', productName),
_buildInfoRow('Amount', '\$${amount.toStringAsFixed(2)}'),
_buildInfoRow('Session ID', session.id),
_buildInfoRow('Status', session.paid ? 'Paid' : 'Pending'),
if (isSubscription)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
'You can manage your subscription in the Stripe Dashboard.',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Done'),
),
],
),
);
}
void _showErrorDialog(BuildContext context, StripeCheckoutException error) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
Icon(Icons.error, color: Colors.red.shade600, size: 28),
const SizedBox(width: 8),
const Text('Payment Failed'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Unable to process your payment.',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 16),
_buildInfoRow('Error', error.message),
if (error is StripePaymentException && error.errorCode != null)
_buildInfoRow('Code', error.errorCode!),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.amber.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'๐ก Troubleshooting:',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.amber.shade900,
),
),
const SizedBox(height: 4),
Text(
_getErrorSuggestion(error),
style: TextStyle(
fontSize: 12,
color: Colors.amber.shade900,
),
),
],
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
),
],
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 80,
child: Text(
'$label:',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13,
),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(fontSize: 13),
),
),
],
),
);
}
String _getErrorSuggestion(StripeCheckoutException error) {
if (error is StripeNetworkException) {
return 'Check your internet connection and API keys configuration.';
} else if (error is StripePaymentException) {
if (error.errorCode?.contains('insufficient') ?? false) {
return 'The card has insufficient funds. Try a different card.';
} else if (error.errorCode?.contains('declined') ?? false) {
return 'The card was declined. Please use a different payment method.';
}
return 'Please check your card details and try again.';
}
return 'Please try again or contact support if the issue persists.';
}
}