
Introduction
Payments are a critical part of many mobile applications, from subscriptions to one-time purchases and marketplaces. When building Flutter apps, developers often integrate Stripe and PayPal because they are reliable, widely supported, and flexible across regions. However, payment integration requires careful architecture to ensure security, compliance, and a smooth user experience. In this comprehensive guide, you will learn how payment gateways work in Flutter, implement both Stripe and PayPal with production-ready code, handle subscriptions and webhooks, and follow security best practices that protect your users and your business.
Understanding Payment Architecture
Before writing code, you must understand how payment flows work in mobile applications. Flutter apps should never process or store raw card details directly. Instead, payments are handled through secure SDKs that tokenize sensitive data and backend services that create and verify transactions.
Payment Flow Diagram
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Flutter │ │ Backend │ │ Payment │ │ Bank │
│ App │ │ Server │ │ Gateway │ │ Network │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │ │
│ 1. Request │ │ │
│ payment intent │ │ │
│──────────────────>│ │ │
│ │ │ │
│ │ 2. Create payment │ │
│ │ intent │ │
│ │──────────────────>│ │
│ │ │ │
│ │ 3. Return client │ │
│ │ secret │ │
│ │<──────────────────│ │
│ │ │ │
│ 4. Client secret │ │ │
│<──────────────────│ │ │
│ │ │ │
│ 5. Show payment │ │ │
│ sheet (SDK) │ │ │
│───────────────────────────────────────>│ │
│ │ │ │
│ │ │ 6. Process │
│ │ │ payment │
│ │ │──────────────────>│
│ │ │ │
│ │ │ 7. Payment result │
│ │ │<──────────────────│
│ │ │ │
│ 8. Success/ │ │ │
│ failure │ │ │
│<──────────────────────────────────────│ │
│ │ │ │
│ │ 9. Webhook event │ │
│ │<──────────────────│ │
│ │ │ │
│ │ 10. Update order │ │
│ │ status │ │
Integrating Stripe in Flutter
Stripe provides an official Flutter package that handles card input, validation, and PCI compliance automatically. The integration requires both client-side (Flutter) and server-side (backend) components.
Project Setup
# pubspec.yaml
dependencies:
flutter_stripe: ^10.1.1
dio: ^5.4.0 # For API calls
# iOS: Add to ios/Podfile
platform :ios, '13.0'
# Android: Update android/app/build.gradle
minSdkVersion 21
Initialize Stripe
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_stripe/flutter_stripe.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Load environment variables
await dotenv.load(fileName: '.env');
// Initialize Stripe with publishable key
Stripe.publishableKey = dotenv.env['STRIPE_PUBLISHABLE_KEY']!;
// Optional: Set merchant identifier for Apple Pay
Stripe.merchantIdentifier = 'merchant.com.yourapp';
// Optional: Enable URL scheme for 3D Secure
await Stripe.instance.applySettings();
runApp(const MyApp());
}
Payment Service Implementation
// lib/services/stripe_payment_service.dart
import 'package:dio/dio.dart';
import 'package:flutter_stripe/flutter_stripe.dart';
class StripePaymentService {
final Dio _dio;
final String _baseUrl;
StripePaymentService({
required String baseUrl,
Dio? dio,
}) : _baseUrl = baseUrl,
_dio = dio ?? Dio();
/// Create a payment intent on the server
Future createPaymentIntent({
required int amount, // Amount in cents
required String currency,
String? customerId,
Map? metadata,
}) async {
try {
final response = await _dio.post(
'$_baseUrl/payments/create-intent',
data: {
'amount': amount,
'currency': currency,
'customer_id': customerId,
'metadata': metadata,
},
);
return PaymentIntentResult(
clientSecret: response.data['client_secret'],
paymentIntentId: response.data['payment_intent_id'],
ephemeralKey: response.data['ephemeral_key'],
customerId: response.data['customer_id'],
);
} on DioException catch (e) {
throw PaymentException(
message: e.response?.data['error'] ?? 'Failed to create payment intent',
code: 'CREATE_INTENT_FAILED',
);
}
}
/// Initialize and present the payment sheet
Future processPayment({
required String clientSecret,
required String merchantName,
String? customerId,
String? ephemeralKey,
bool allowsDelayedPaymentMethods = false,
}) async {
try {
// Initialize the payment sheet
await Stripe.instance.initPaymentSheet(
paymentSheetParameters: SetupPaymentSheetParameters(
paymentIntentClientSecret: clientSecret,
merchantDisplayName: merchantName,
customerId: customerId,
customerEphemeralKeySecret: ephemeralKey,
allowsDelayedPaymentMethods: allowsDelayedPaymentMethods,
style: ThemeMode.system,
appearance: const PaymentSheetAppearance(
colors: PaymentSheetAppearanceColors(
primary: Color(0xFF6772E5),
),
shapes: PaymentSheetShape(
borderRadius: 12,
shadow: PaymentSheetShadowParams(color: Colors.black12),
),
),
billingDetails: const BillingDetails(
address: Address(
city: null,
country: null,
line1: null,
line2: null,
postalCode: null,
state: null,
),
),
billingDetailsCollectionConfiguration:
const BillingDetailsCollectionConfiguration(
name: CollectionMode.automatic,
email: CollectionMode.automatic,
address: AddressCollectionMode.automatic,
),
),
);
// Present the payment sheet
await Stripe.instance.presentPaymentSheet();
return PaymentResult(
success: true,
message: 'Payment completed successfully',
);
} on StripeException catch (e) {
if (e.error.code == FailureCode.Canceled) {
return PaymentResult(
success: false,
message: 'Payment cancelled',
cancelled: true,
);
}
throw PaymentException(
message: e.error.localizedMessage ?? 'Payment failed',
code: e.error.code.toString(),
);
}
}
/// Process a card payment directly (for custom UI)
Future processCardPayment({
required String clientSecret,
required CardDetails cardDetails,
BillingDetails? billingDetails,
}) async {
try {
// Create card payment method
await Stripe.instance.dangerouslyUpdateCardDetails(cardDetails);
// Confirm the payment
final paymentIntent = await Stripe.instance.confirmPayment(
paymentIntentClientSecret: clientSecret,
data: PaymentMethodParams.card(
paymentMethodData: PaymentMethodData(
billingDetails: billingDetails,
),
),
);
if (paymentIntent.status == PaymentIntentsStatus.Succeeded) {
return PaymentResult(
success: true,
message: 'Payment successful',
paymentIntentId: paymentIntent.id,
);
} else if (paymentIntent.status == PaymentIntentsStatus.RequiresAction) {
// 3D Secure authentication required
final result = await Stripe.instance.handleNextAction(clientSecret);
return PaymentResult(
success: result.status == PaymentIntentsStatus.Succeeded,
message: result.status == PaymentIntentsStatus.Succeeded
? 'Payment successful'
: 'Authentication required',
requiresAction: result.status != PaymentIntentsStatus.Succeeded,
);
}
return PaymentResult(
success: false,
message: 'Payment requires further action',
);
} on StripeException catch (e) {
throw PaymentException(
message: e.error.localizedMessage ?? 'Payment failed',
code: e.error.code.toString(),
);
}
}
/// Setup for future payments (save card)
Future setupFuturePayment({
required String customerId,
}) async {
try {
final response = await _dio.post(
'$_baseUrl/payments/create-setup-intent',
data: {'customer_id': customerId},
);
final clientSecret = response.data['client_secret'];
await Stripe.instance.initPaymentSheet(
paymentSheetParameters: SetupPaymentSheetParameters(
setupIntentClientSecret: clientSecret,
merchantDisplayName: 'Your App',
customerId: customerId,
),
);
await Stripe.instance.presentPaymentSheet();
return SetupIntentResult(
success: true,
setupIntentId: response.data['setup_intent_id'],
);
} on StripeException catch (e) {
throw PaymentException(
message: e.error.localizedMessage ?? 'Setup failed',
code: e.error.code.toString(),
);
}
}
}
// Data classes
class PaymentIntentResult {
final String clientSecret;
final String paymentIntentId;
final String? ephemeralKey;
final String? customerId;
PaymentIntentResult({
required this.clientSecret,
required this.paymentIntentId,
this.ephemeralKey,
this.customerId,
});
}
class PaymentResult {
final bool success;
final String message;
final bool cancelled;
final bool requiresAction;
final String? paymentIntentId;
PaymentResult({
required this.success,
required this.message,
this.cancelled = false,
this.requiresAction = false,
this.paymentIntentId,
});
}
class SetupIntentResult {
final bool success;
final String setupIntentId;
SetupIntentResult({
required this.success,
required this.setupIntentId,
});
}
class PaymentException implements Exception {
final String message;
final String code;
PaymentException({required this.message, required this.code});
@override
String toString() => 'PaymentException: $message (code: $code)';
}
Backend Implementation (Node.js)
// server/routes/payments.js
const express = require('express');
const Stripe = require('stripe');
const router = express.Router();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
// Create payment intent
router.post('/create-intent', async (req, res) => {
const { amount, currency, customer_id, metadata } = req.body;
try {
// Validate amount
if (!amount || amount < 50) {
return res.status(400).json({ error: 'Invalid amount' });
}
// Create or retrieve customer
let customer = customer_id;
if (!customer) {
const newCustomer = await stripe.customers.create({
metadata: { user_id: req.user?.id },
});
customer = newCustomer.id;
}
// Create ephemeral key for mobile SDK
const ephemeralKey = await stripe.ephemeralKeys.create(
{ customer },
{ apiVersion: '2023-10-16' }
);
// Create payment intent
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: currency || 'usd',
customer,
automatic_payment_methods: { enabled: true },
metadata: {
...metadata,
user_id: req.user?.id,
},
});
res.json({
client_secret: paymentIntent.client_secret,
payment_intent_id: paymentIntent.id,
ephemeral_key: ephemeralKey.secret,
customer_id: customer,
});
} catch (error) {
console.error('Create intent error:', error);
res.status(500).json({ error: error.message });
}
});
// Create setup intent (save card for later)
router.post('/create-setup-intent', async (req, res) => {
const { customer_id } = req.body;
try {
const setupIntent = await stripe.setupIntents.create({
customer: customer_id,
automatic_payment_methods: { enabled: true },
});
res.json({
client_secret: setupIntent.client_secret,
setup_intent_id: setupIntent.id,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Webhook handler
router.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const sig = req.headers['stripe-signature'];
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
await handlePaymentSuccess(paymentIntent);
break;
case 'payment_intent.payment_failed':
const failedIntent = event.data.object;
await handlePaymentFailure(failedIntent);
break;
case 'customer.subscription.created':
case 'customer.subscription.updated':
const subscription = event.data.object;
await handleSubscriptionUpdate(subscription);
break;
case 'customer.subscription.deleted':
const deletedSub = event.data.object;
await handleSubscriptionCancelled(deletedSub);
break;
case 'invoice.payment_succeeded':
const invoice = event.data.object;
await handleInvoicePaid(invoice);
break;
case 'invoice.payment_failed':
const failedInvoice = event.data.object;
await handleInvoiceFailed(failedInvoice);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.json({ received: true });
});
async function handlePaymentSuccess(paymentIntent) {
const { id, amount, currency, customer, metadata } = paymentIntent;
// Update order status in database
await Order.updateOne(
{ paymentIntentId: id },
{
status: 'paid',
paidAt: new Date(),
amount: amount / 100,
currency,
}
);
// Send confirmation email, update inventory, etc.
console.log(`Payment ${id} succeeded for customer ${customer}`);
}
module.exports = router;
Integrating PayPal in Flutter
PayPal integration typically uses a WebView-based checkout flow or the PayPal SDK. The WebView approach provides maximum compatibility across platforms.
PayPal Payment Service
// lib/services/paypal_payment_service.dart
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class PayPalPaymentService {
final Dio _dio;
final String _baseUrl;
PayPalPaymentService({
required String baseUrl,
Dio? dio,
}) : _baseUrl = baseUrl,
_dio = dio ?? Dio();
/// Create a PayPal order
Future createOrder({
required double amount,
required String currency,
required String description,
Map? metadata,
}) async {
try {
final response = await _dio.post(
'$_baseUrl/paypal/create-order',
data: {
'amount': amount,
'currency': currency,
'description': description,
'metadata': metadata,
},
);
return PayPalOrderResult(
orderId: response.data['order_id'],
approvalUrl: response.data['approval_url'],
captureUrl: response.data['capture_url'],
);
} on DioException catch (e) {
throw PaymentException(
message: e.response?.data['error'] ?? 'Failed to create PayPal order',
code: 'PAYPAL_CREATE_FAILED',
);
}
}
/// Capture payment after user approval
Future capturePayment(String orderId) async {
try {
final response = await _dio.post(
'$_baseUrl/paypal/capture-order',
data: {'order_id': orderId},
);
return PayPalCaptureResult(
success: response.data['status'] == 'COMPLETED',
transactionId: response.data['transaction_id'],
status: response.data['status'],
);
} on DioException catch (e) {
throw PaymentException(
message: e.response?.data['error'] ?? 'Failed to capture payment',
code: 'PAYPAL_CAPTURE_FAILED',
);
}
}
}
class PayPalOrderResult {
final String orderId;
final String approvalUrl;
final String captureUrl;
PayPalOrderResult({
required this.orderId,
required this.approvalUrl,
required this.captureUrl,
});
}
class PayPalCaptureResult {
final bool success;
final String? transactionId;
final String status;
PayPalCaptureResult({
required this.success,
this.transactionId,
required this.status,
});
}
PayPal Checkout WebView
// lib/widgets/paypal_checkout_view.dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class PayPalCheckoutView extends StatefulWidget {
final String approvalUrl;
final String returnUrl;
final String cancelUrl;
final Function(String orderId) onSuccess;
final VoidCallback onCancel;
final Function(String error) onError;
const PayPalCheckoutView({
super.key,
required this.approvalUrl,
required this.returnUrl,
required this.cancelUrl,
required this.onSuccess,
required this.onCancel,
required this.onError,
});
@override
State createState() => _PayPalCheckoutViewState();
}
class _PayPalCheckoutViewState extends State {
late final WebViewController _controller;
bool _isLoading = true;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageStarted: (_) => setState(() => _isLoading = true),
onPageFinished: (_) => setState(() => _isLoading = false),
onNavigationRequest: _handleNavigation,
onWebResourceError: (error) {
widget.onError(error.description);
},
),
)
..loadRequest(Uri.parse(widget.approvalUrl));
}
NavigationDecision _handleNavigation(NavigationRequest request) {
final url = request.url;
// Check for success return URL
if (url.startsWith(widget.returnUrl)) {
final uri = Uri.parse(url);
final token = uri.queryParameters['token'];
final payerId = uri.queryParameters['PayerID'];
if (token != null) {
widget.onSuccess(token);
} else {
widget.onError('Missing payment token');
}
return NavigationDecision.prevent;
}
// Check for cancel URL
if (url.startsWith(widget.cancelUrl)) {
widget.onCancel();
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('PayPal Checkout'),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: widget.onCancel,
),
),
body: Stack(
children: [
WebViewWidget(controller: _controller),
if (_isLoading)
const Center(child: CircularProgressIndicator()),
],
),
);
}
}
PayPal Backend (Node.js)
// server/routes/paypal.js
const express = require('express');
const axios = require('axios');
const router = express.Router();
const PAYPAL_API = process.env.PAYPAL_MODE === 'live'
? 'https://api-m.paypal.com'
: 'https://api-m.sandbox.paypal.com';
async function getAccessToken() {
const auth = Buffer.from(
`${process.env.PAYPAL_CLIENT_ID}:${process.env.PAYPAL_CLIENT_SECRET}`
).toString('base64');
const response = await axios.post(
`${PAYPAL_API}/v1/oauth2/token`,
'grant_type=client_credentials',
{
headers: {
Authorization: `Basic ${auth}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
return response.data.access_token;
}
router.post('/create-order', async (req, res) => {
const { amount, currency, description, metadata } = req.body;
try {
const accessToken = await getAccessToken();
const response = await axios.post(
`${PAYPAL_API}/v2/checkout/orders`,
{
intent: 'CAPTURE',
purchase_units: [
{
amount: {
currency_code: currency || 'USD',
value: amount.toFixed(2),
},
description,
custom_id: metadata?.order_id,
},
],
application_context: {
brand_name: 'Your App',
landing_page: 'NO_PREFERENCE',
user_action: 'PAY_NOW',
return_url: `${process.env.APP_URL}/paypal/success`,
cancel_url: `${process.env.APP_URL}/paypal/cancel`,
},
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
);
const approvalLink = response.data.links.find(link => link.rel === 'approve');
res.json({
order_id: response.data.id,
approval_url: approvalLink?.href,
status: response.data.status,
});
} catch (error) {
console.error('PayPal create order error:', error.response?.data || error);
res.status(500).json({ error: 'Failed to create PayPal order' });
}
});
router.post('/capture-order', async (req, res) => {
const { order_id } = req.body;
try {
const accessToken = await getAccessToken();
const response = await axios.post(
`${PAYPAL_API}/v2/checkout/orders/${order_id}/capture`,
{},
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
);
const capture = response.data.purchase_units[0]?.payments?.captures?.[0];
res.json({
status: response.data.status,
transaction_id: capture?.id,
amount: capture?.amount,
});
} catch (error) {
console.error('PayPal capture error:', error.response?.data || error);
res.status(500).json({ error: 'Failed to capture payment' });
}
});
// PayPal webhook handler
router.post('/webhook', async (req, res) => {
const event = req.body;
// Verify webhook signature (important for production)
// See PayPal docs for verification implementation
switch (event.event_type) {
case 'CHECKOUT.ORDER.APPROVED':
console.log('Order approved:', event.resource.id);
break;
case 'PAYMENT.CAPTURE.COMPLETED':
const captureId = event.resource.id;
const orderId = event.resource.supplementary_data?.related_ids?.order_id;
await handlePayPalPaymentSuccess(orderId, captureId);
break;
case 'PAYMENT.CAPTURE.DENIED':
await handlePayPalPaymentFailed(event.resource);
break;
}
res.json({ received: true });
});
module.exports = router;
Unified Payment UI
// lib/screens/checkout_screen.dart
import 'package:flutter/material.dart';
import '../services/stripe_payment_service.dart';
import '../services/paypal_payment_service.dart';
import '../widgets/paypal_checkout_view.dart';
enum PaymentMethod { stripe, paypal }
class CheckoutScreen extends StatefulWidget {
final double amount;
final String currency;
final String description;
const CheckoutScreen({
super.key,
required this.amount,
required this.currency,
required this.description,
});
@override
State createState() => _CheckoutScreenState();
}
class _CheckoutScreenState extends State {
PaymentMethod _selectedMethod = PaymentMethod.stripe;
bool _isProcessing = false;
final _stripeService = StripePaymentService(baseUrl: 'https://api.yourapp.com');
final _paypalService = PayPalPaymentService(baseUrl: 'https://api.yourapp.com');
Future _processPayment() async {
setState(() => _isProcessing = true);
try {
if (_selectedMethod == PaymentMethod.stripe) {
await _processStripePayment();
} else {
await _processPayPalPayment();
}
} catch (e) {
_showError(e.toString());
} finally {
setState(() => _isProcessing = false);
}
}
Future _processStripePayment() async {
// Create payment intent
final intent = await _stripeService.createPaymentIntent(
amount: (widget.amount * 100).toInt(), // Convert to cents
currency: widget.currency.toLowerCase(),
);
// Process payment
final result = await _stripeService.processPayment(
clientSecret: intent.clientSecret,
merchantName: 'Your App',
customerId: intent.customerId,
ephemeralKey: intent.ephemeralKey,
);
if (result.success) {
_showSuccess('Payment successful!');
Navigator.of(context).pop(true);
} else if (result.cancelled) {
// User cancelled, do nothing
} else {
_showError(result.message);
}
}
Future _processPayPalPayment() async {
// Create PayPal order
final order = await _paypalService.createOrder(
amount: widget.amount,
currency: widget.currency,
description: widget.description,
);
// Navigate to PayPal checkout
if (!mounted) return;
final success = await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => PayPalCheckoutView(
approvalUrl: order.approvalUrl,
returnUrl: 'https://yourapp.com/paypal/success',
cancelUrl: 'https://yourapp.com/paypal/cancel',
onSuccess: (orderId) async {
Navigator.pop(context);
// Capture the payment
final capture = await _paypalService.capturePayment(orderId);
if (capture.success) {
_showSuccess('Payment successful!');
Navigator.of(context).pop(true);
} else {
_showError('Payment capture failed');
}
},
onCancel: () => Navigator.pop(context),
onError: (error) {
Navigator.pop(context);
_showError(error);
},
),
),
);
}
void _showSuccess(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.green,
),
);
}
void _showError(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Checkout')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Order summary
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Order Summary',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(widget.description),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Total'),
Text(
'${widget.currency} ${widget.amount.toStringAsFixed(2)}',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
],
),
),
),
const SizedBox(height: 24),
// Payment method selection
Text(
'Payment Method',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
_PaymentMethodTile(
icon: Icons.credit_card,
title: 'Credit / Debit Card',
subtitle: 'Powered by Stripe',
selected: _selectedMethod == PaymentMethod.stripe,
onTap: () => setState(() => _selectedMethod = PaymentMethod.stripe),
),
const SizedBox(height: 8),
_PaymentMethodTile(
icon: Icons.account_balance_wallet,
title: 'PayPal',
subtitle: 'Pay with your PayPal account',
selected: _selectedMethod == PaymentMethod.paypal,
onTap: () => setState(() => _selectedMethod = PaymentMethod.paypal),
),
const Spacer(),
// Pay button
ElevatedButton(
onPressed: _isProcessing ? null : _processPayment,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: _isProcessing
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(
'Pay ${widget.currency} ${widget.amount.toStringAsFixed(2)}',
),
),
],
),
),
);
}
}
class _PaymentMethodTile extends StatelessWidget {
final IconData icon;
final String title;
final String subtitle;
final bool selected;
final VoidCallback onTap;
const _PaymentMethodTile({
required this.icon,
required this.title,
required this.subtitle,
required this.selected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(
color: selected ? Theme.of(context).primaryColor : Colors.grey.shade300,
width: selected ? 2 : 1,
),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(icon, size: 32),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
Text(subtitle, style: TextStyle(color: Colors.grey.shade600)),
],
),
),
if (selected)
Icon(Icons.check_circle, color: Theme.of(context).primaryColor),
],
),
),
);
}
}
Handling Subscriptions
Subscriptions require server-side management with webhook handling for status updates:
// lib/services/subscription_service.dart
class SubscriptionService {
final Dio _dio;
final String _baseUrl;
SubscriptionService({required String baseUrl, Dio? dio})
: _baseUrl = baseUrl,
_dio = dio ?? Dio();
Future createSubscription({
required String priceId,
String? customerId,
}) async {
try {
final response = await _dio.post(
'$_baseUrl/subscriptions/create',
data: {
'price_id': priceId,
'customer_id': customerId,
},
);
return SubscriptionResult(
subscriptionId: response.data['subscription_id'],
clientSecret: response.data['client_secret'],
status: response.data['status'],
);
} on DioException catch (e) {
throw PaymentException(
message: e.response?.data['error'] ?? 'Failed to create subscription',
code: 'SUBSCRIPTION_FAILED',
);
}
}
Future cancelSubscription(String subscriptionId) async {
await _dio.post(
'$_baseUrl/subscriptions/cancel',
data: {'subscription_id': subscriptionId},
);
}
Future getSubscriptionStatus() async {
final response = await _dio.get('$_baseUrl/subscriptions/status');
return SubscriptionStatus.fromJson(response.data);
}
}
Common Mistakes to Avoid
Mistake 1: Trusting Client-Side Payment Confirmation
// WRONG - Never trust client-side success
await Stripe.instance.presentPaymentSheet();
// DO NOT assume payment succeeded here!
await unlockPremiumFeatures(); // Dangerous!
// CORRECT - Verify on backend via webhook
// Client shows "Processing..." until backend confirms via webhook
await Stripe.instance.presentPaymentSheet();
await showProcessingDialog();
// Wait for push notification or poll backend for confirmation
Mistake 2: Exposing Secret Keys in Flutter
// WRONG - Secret key in Flutter code
Stripe.instance.secretKey = 'sk_live_xxx'; // NEVER DO THIS!
// CORRECT - Only use publishable key in Flutter
Stripe.publishableKey = 'pk_live_xxx'; // Safe
// Secret key stays on server only
Mistake 3: Not Implementing Webhooks
// WRONG - Only checking payment status after presentPaymentSheet
// This misses async events like 3D Secure failures, refunds, disputes
// CORRECT - Implement comprehensive webhook handling
switch (event.type) {
case 'payment_intent.succeeded':
case 'payment_intent.payment_failed':
case 'charge.refunded':
case 'charge.dispute.created':
// Handle all payment lifecycle events
}
Mistake 4: Missing Idempotency Keys
// WRONG - Duplicate charges if user retries
await stripe.paymentIntents.create({ amount: 1000 });
// CORRECT - Use idempotency key
await stripe.paymentIntents.create(
{ amount: 1000 },
{ idempotencyKey: `order_${orderId}` }
);
Stripe vs PayPal Comparison
| Feature | Stripe | PayPal |
|---|---|---|
| Setup Complexity | Low (official SDK) | Medium (WebView) |
| Card Payments | Excellent | Good |
| Wallet Support | Apple Pay, Google Pay | PayPal, Venmo |
| Subscriptions | Native support | Supported |
| International | Good | Excellent |
| Fees | 2.9% + $0.30 | 2.9% + $0.30 |
| Best For | Developer experience | Brand recognition |
Conclusion
Integrating payment gateways in Flutter requires careful architecture, secure backend coordination, and reliable SDK usage. By combining Stripe's modern APIs with PayPal's global reach, you can offer flexible and trustworthy payment options to your users. Always process payments server-side, implement webhooks for reliable status updates, and never store sensitive card data in your Flutter app.
For building scalable Flutter backends, read Fullstack Flutter with Serverpod: Getting Started Guide. For secure authentication patterns, see OAuth2, JWT, and Session Tokens Explained. Reference the official Stripe Flutter documentation and PayPal developer documentation for the latest API updates.
1 Comment