flutter_upi_gateway 1.0.0 copy "flutter_upi_gateway: ^1.0.0" to clipboard
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,
              ),
            ),
          ),
        ],
      ),
    );
  }
}
0
likes
150
points
248
downloads

Documentation

API reference

Publisher

verified publisherappifyhaven.website

Weekly Downloads

A production-quality Flutter plugin for initiating and handling UPI payments using standard upi://pay deep links across Android and iOS.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on flutter_upi_gateway

Packages that implement flutter_upi_gateway