flutter_upi_gateway 1.0.0
flutter_upi_gateway: ^1.0.0 copied to clipboard
A production-quality Flutter plugin for initiating and handling UPI payments using standard upi://pay deep links across Android and iOS.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_upi_gateway/flutter_upi_gateway.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
FlutterUpiGateway.enableDebugLogging(true);
runApp(const FlutterUpiExampleApp());
}
class FlutterUpiExampleApp extends StatelessWidget {
const FlutterUpiExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter UPI Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF0052CC),
brightness: Brightness.light,
),
),
home: const UpiHomeScreen(),
);
}
}
class UpiHomeScreen extends StatefulWidget {
const UpiHomeScreen({super.key});
@override
State<UpiHomeScreen> createState() => _UpiHomeScreenState();
}
class _UpiHomeScreenState extends State<UpiHomeScreen> {
final _formKey = GlobalKey<FormState>();
final _vpaController = TextEditingController(text: 'merchant@upi');
final _nameController = TextEditingController(text: 'Demo Store');
final _amountController = TextEditingController(text: '1.00');
final _txnIdController = TextEditingController();
final _noteController = TextEditingController(text: 'Test Order Payment');
bool _isAvailable = false;
bool _isLoadingApps = false;
bool _isProcessingPayment = false;
List<UpiApp> _installedApps = [];
UpiApp? _selectedApp;
UpiPaymentResult? _lastPaymentResult;
String? _generatedUri;
String? _vpaValidationMsg;
@override
void initState() {
super.initState();
_txnIdController.text = 'ORD_${DateTime.now().millisecondsSinceEpoch}';
_checkAvailabilityAndLoadApps();
}
Future<void> _checkAvailabilityAndLoadApps() async {
setState(() => _isLoadingApps = true);
final available = await FlutterUpiGateway.isAvailable();
final apps = await FlutterUpiGateway.getInstalledUpiApps();
setState(() {
_isAvailable = available;
_installedApps = apps;
_selectedApp = apps.isNotEmpty ? apps.first : null;
_isLoadingApps = false;
});
_validateVpaInput(_vpaController.text);
}
void _validateVpaInput(String value) {
setState(() {
_vpaValidationMsg = FlutterUpiGateway.validateVpa(value);
});
}
void _generateUriAndQr() {
if (!_formKey.currentState!.validate()) return;
final request = UpiPaymentRequest(
payeeVpa: _vpaController.text.trim(),
payeeName: _nameController.text.trim(),
amount: double.parse(_amountController.text.trim()),
transactionId: _txnIdController.text.trim(),
transactionNote: _noteController.text.trim(),
);
final uri = FlutterUpiGateway.buildPaymentUri(request);
setState(() {
_generatedUri = uri.toString();
});
}
Future<void> _payWithChooser() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_isProcessingPayment = true;
_lastPaymentResult = null;
});
try {
final request = UpiPaymentRequest(
payeeVpa: _vpaController.text.trim(),
payeeName: _nameController.text.trim(),
amount: double.parse(_amountController.text.trim()),
transactionId: _txnIdController.text.trim(),
transactionNote: _noteController.text.trim(),
);
final result = await FlutterUpiGateway.pay(request);
setState(() {
_lastPaymentResult = result;
});
} on UpiException catch (e) {
if (e.code == UpiErrorCode.noUpiAppAvailable) {
_showErrorSnackBar('No UPI payment application (Google Pay, PhonePe, Paytm, BHIM) is installed on this device or emulator.');
} else {
_showErrorSnackBar('UPI Error (${e.code.name}): ${e.message}');
}
} catch (e) {
_showErrorSnackBar('Unexpected error: $e');
} finally {
setState(() => _isProcessingPayment = false);
}
}
Future<void> _payWithSelectedApp() async {
if (!_formKey.currentState!.validate()) return;
if (_selectedApp == null) {
_showErrorSnackBar('Please select a UPI app from the list.');
return;
}
setState(() {
_isProcessingPayment = true;
_lastPaymentResult = null;
});
try {
final request = UpiPaymentRequest(
payeeVpa: _vpaController.text.trim(),
payeeName: _nameController.text.trim(),
amount: double.parse(_amountController.text.trim()),
transactionId: _txnIdController.text.trim(),
transactionNote: _noteController.text.trim(),
);
final result = await FlutterUpiGateway.payWithApp(
request,
appId: _selectedApp!.id,
);
setState(() {
_lastPaymentResult = result;
});
} on UpiException catch (e) {
if (e.code == UpiErrorCode.selectedAppNotAvailable) {
_showErrorSnackBar('The selected app (${_selectedApp?.name}) is not installed or could not be launched.');
} else {
_showErrorSnackBar('UPI Error (${e.code.name}): ${e.message}');
}
} catch (e) {
_showErrorSnackBar('Unexpected error: $e');
} finally {
setState(() => _isProcessingPayment = false);
}
}
void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red.shade700,
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 4),
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('Flutter UPI Integration'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _checkAvailabilityAndLoadApps,
tooltip: 'Refresh Apps',
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// System Availability Banner
Card(
elevation: 0,
color: _isAvailable
? Colors.green.shade50
: Colors.amber.shade50,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
Icon(
_isAvailable ? Icons.check_circle : Icons.warning_amber_rounded,
color: _isAvailable ? Colors.green.shade700 : Colors.amber.shade800,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_isAvailable ? 'UPI Ready' : 'No UPI Apps Installed',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: _isAvailable ? Colors.green.shade900 : Colors.amber.shade900,
),
),
const SizedBox(height: 2),
Text(
_isAvailable
? '${_installedApps.length} installed UPI application(s) detected.'
: 'No UPI apps (Google Pay, PhonePe, Paytm, BHIM) found on this device/emulator. Test on a physical Android device or install a UPI app.',
style: theme.textTheme.bodySmall,
),
],
),
),
],
),
),
),
const SizedBox(height: 16),
// Installed UPI Apps Section
Text('Available UPI Applications', style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
if (_isLoadingApps)
const Center(child: CircularProgressIndicator())
else if (_installedApps.isEmpty)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'No installed UPI apps found. On a real Android device, installed apps like Google Pay, PhonePe, Paytm, or BHIM will appear here.',
textAlign: TextAlign.center,
),
)
else
SizedBox(
height: 90,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _installedApps.length,
itemBuilder: (context, index) {
final app = _installedApps[index];
final isSelected = _selectedApp?.id == app.id;
return GestureDetector(
onTap: () => setState(() => _selectedApp = app),
child: Container(
width: 80,
margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isSelected ? theme.colorScheme.primaryContainer : Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? theme.colorScheme.primary : Colors.grey.shade300,
width: isSelected ? 2 : 1,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
app.iconBytes != null
? Image.memory(app.iconBytes!, width: 36, height: 36)
: Icon(Icons.account_balance_wallet, size: 36, color: theme.colorScheme.primary),
const SizedBox(height: 6),
Text(
app.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
],
),
),
);
},
),
),
const SizedBox(height: 20),
// Transaction Parameters Form
Text('Payment Request Parameters', style: theme.textTheme.titleMedium),
const SizedBox(height: 12),
TextFormField(
controller: _vpaController,
decoration: InputDecoration(
labelText: 'Payee UPI ID (VPA)',
hintText: 'merchant@upi',
prefixIcon: const Icon(Icons.alternate_email),
suffixIcon: _vpaValidationMsg == null
? const Icon(Icons.check_circle, color: Colors.green)
: const Icon(Icons.error_outline, color: Colors.red),
errorText: _vpaValidationMsg,
border: const OutlineInputBorder(),
),
onChanged: _validateVpaInput,
validator: (val) => FlutterUpiGateway.validateVpa(val ?? ''),
),
const SizedBox(height: 12),
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Payee Name',
prefixIcon: Icon(Icons.store),
border: OutlineInputBorder(),
),
validator: (val) => val == null || val.trim().isEmpty ? 'Enter payee name' : null,
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: TextFormField(
controller: _amountController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(
labelText: 'Amount (INR)',
prefixIcon: Icon(Icons.currency_rupee),
border: OutlineInputBorder(),
),
validator: (val) {
final parsed = double.tryParse(val ?? '');
if (parsed == null || parsed <= 0) return 'Invalid amount';
return null;
},
),
),
const SizedBox(width: 12),
Expanded(
child: TextFormField(
controller: _txnIdController,
decoration: const InputDecoration(
labelText: 'Transaction ID',
prefixIcon: Icon(Icons.numbers),
border: OutlineInputBorder(),
),
),
),
],
),
const SizedBox(height: 12),
TextFormField(
controller: _noteController,
decoration: const InputDecoration(
labelText: 'Transaction Note',
prefixIcon: Icon(Icons.note),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 20),
// Actions
if (_isProcessingPayment)
const Center(child: CircularProgressIndicator())
else ...[
ElevatedButton.icon(
onPressed: _payWithChooser,
icon: const Icon(Icons.payment),
label: const Text('Pay via System Chooser (Any App)'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
backgroundColor: theme.colorScheme.primary,
foregroundColor: Colors.white,
),
),
const SizedBox(height: 10),
OutlinedButton.icon(
onPressed: _selectedApp != null ? _payWithSelectedApp : null,
icon: const Icon(Icons.launch),
label: Text('Pay with ${_selectedApp?.name ?? "Selected App"}'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
),
),
const SizedBox(height: 10),
TextButton.icon(
onPressed: _generateUriAndQr,
icon: const Icon(Icons.qr_code),
label: const Text('Generate UPI URI & QR Payload'),
),
],
const SizedBox(height: 20),
// Generated URI Preview
if (_generatedUri != null) ...[
Card(
elevation: 1,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Generated Payment URI', style: theme.textTheme.titleSmall),
IconButton(
icon: const Icon(Icons.copy, size: 18),
onPressed: () {
Clipboard.setData(ClipboardData(text: _generatedUri!));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('URI copied to clipboard.')),
);
},
),
],
),
SelectableText(
_generatedUri!,
style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
),
],
),
),
),
const SizedBox(height: 16),
],
// Payment Result Section
if (_lastPaymentResult != null) _buildResultCard(theme, _lastPaymentResult!),
],
),
),
),
);
}
Widget _buildResultCard(ThemeData theme, UpiPaymentResult result) {
Color statusColor;
IconData statusIcon;
switch (result.status) {
case UpiPaymentStatus.success:
statusColor = Colors.green;
statusIcon = Icons.check_circle;
break;
case UpiPaymentStatus.cancelled:
statusColor = Colors.orange;
statusIcon = Icons.cancel;
break;
case UpiPaymentStatus.failure:
statusColor = Colors.red;
statusIcon = Icons.error;
break;
case UpiPaymentStatus.pending:
statusColor = Colors.blue;
statusIcon = Icons.hourglass_empty;
break;
default:
statusColor = Colors.grey;
statusIcon = Icons.help_outline;
break;
}
return Card(
elevation: 3,
shape: RoundedRectangleBorder(
side: BorderSide(color: statusColor, width: 2),
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(statusIcon, color: statusColor, size: 28),
const SizedBox(width: 10),
Text(
'Payment Result: ${result.status.name.toUpperCase()}',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: statusColor,
),
),
],
),
const Divider(height: 24),
_buildResultRow('Status', result.status.name),
if (result.transactionId != null)
_buildResultRow('Transaction ID', result.transactionId!),
if (result.approvalReferenceNumber != null)
_buildResultRow('Approval Ref No (RRN)', result.approvalReferenceNumber!),
if (result.responseCode != null)
_buildResultRow('Response Code', result.responseCode!),
if (result.message != null)
_buildResultRow('Message', result.message!),
if (result.rawResponse != null)
_buildResultRow('Raw Response', result.rawResponse!, isMonospace: true),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(6),
),
child: const Text(
'Note: Always verify high-value payment settlement on your server backend.',
style: TextStyle(fontSize: 11, fontStyle: FontStyle.italic),
),
),
],
),
),
);
}
Widget _buildResultRow(String label, String value, {bool isMonospace = false}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 140,
child: Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
),
),
Expanded(
child: SelectableText(
value,
style: TextStyle(
fontSize: 13,
fontFamily: isMonospace ? 'monospace' : null,
),
),
),
],
),
);
}
}