Skip to main content

swichpay_flutter 2.1.0 copy "swichpay_flutter: ^2.1.0" to clipboard
swichpay_flutter: ^2.1.0 copied to clipboard

Flutter SDK for integrating Swich payment services.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:swichpay_flutter/swichpay.dart';
import 'package:switchpay_flutter_example/gradiant_button.dart';

// Static QR, UnionPay, and Tokenization commented out for now - not
// something this storefront wants to offer yet.
const _checkoutCategories = [
  PaymentCategory.ewallet,
  PaymentCategory.biller,
  PaymentCategory.bankAccount,
  PaymentCategory.visaMasterCardPayment,
  // PaymentCategory.unionPayCardPayment,
  PaymentCategory.dqrcPayment,
  // PaymentCategory.sqrcPayment,
  PaymentCategory.rtpNowPayment,
  // PaymentCategory.tokenization,
  PaymentCategory.rtpEwalletPayment,
];

// Mutable (not const) so the "Sandbox Credentials" fields below can change them at runtime for
// testing - editing either calls [_initializeSwichPay] again, so a change takes effect on the
// very next checkout attempt without restarting the app.
String _sandboxClientId = '4f744abad406438aa40c6e4afaf8b015';

// The OAuth client_credentials secret - matches native's PaymentConfig.secretKey. Required for
// the payment sheet / session-based checkout; the SDK exchanges this for a bearer token itself.
// NOT the same value as _sandboxSecretHashKey below - mixing them up gets an "invalid_client"
// 401 from the token endpoint.
String _sandboxClientSecret =
    '52dbc39a18b146f5a72687e490c8a0687973412fbdcf43c28a8ced01cf7e72e9';

// Only required for SwichPay.presentCardCheckout (the direct/session-less card flow) - matches
// this same sandbox merchant's native PaymentConfig.secretHashKey. A different secret than
// _sandboxClientSecret above.
const _sandboxSecretHashKey = '79A9528E04383A1A';

// clientId/referrer must match what's registered with SwichPay for this merchant; the checkout
// API validates both on every request. That's the whole integration - no backend proxy to stand
// up; the SDK exchanges clientId/clientSecret for an OAuth token itself and manages sessions
// internally.
void _initializeSwichPay() {
  SwichPay.initialize(
    clientId: _sandboxClientId,
    clientSecret: _sandboxClientSecret,
    referrer: 'http://127.0.0.1:5500/',
    merchantId: 1178,
    appId: 'com.yourapp',
    environment: Environment.sandbox,
    secretHashKey: _sandboxSecretHashKey,
  );
}

void main() {
  _initializeSwichPay();
  runApp(const SwichPayExampleApp());
}

class SwichPayExampleApp extends StatelessWidget {
  const SwichPayExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Swich Pay',
      theme: ThemeData(
        colorSchemeSeed: const Color(0xFF2563EB),
        scaffoldBackgroundColor: Colors.white,
        useMaterial3: true,
      ),
      home: const CheckoutPage(),
    );
  }
}

/// Identifies which checkout button is currently in flight - see
/// `_CheckoutPageState._activeCheckout`.
enum _CheckoutAction {
  checkout,
  wallet,
  bankTransfer,
  oneBill,
  qr,
  rtpNow,
  rtpEwallet,
  card,
}

/// A dummy storefront product - just enough (name/price/icon) to drive the
/// add-to-cart demo below.
class _Product {
  const _Product({required this.name, required this.price, required this.icon});

  final String name;
  final int price;
  final IconData icon;
}

class CheckoutPage extends StatefulWidget {
  const CheckoutPage({super.key});

  @override
  State<CheckoutPage> createState() => _CheckoutPageState();
}

class _CheckoutPageState extends State<CheckoutPage> {
  String _status = 'No payment attempted yet.';

  /// Which checkout button is currently in flight, if any - `null` means
  /// none. Was a single shared `bool` before, which made every button's
  /// `GradientPayButton(loading: ...)` light up together since they all
  /// read the same flag; keyed per-button now so only the one actually
  /// tapped shows its loader.
  _CheckoutAction? _activeCheckout;

  // Dummy catalog - prices kept small (1-10 PKR each) so a cart full of
  // everything still makes for a quick sandbox test. A small enough cart
  // total can get rejected server-side with HTTP 400, code "0012"
  // ("invalid minimum amount") - a real backend validation, not an SDK
  // bug, confirmed live - add more items to the cart to clear it.
  static const _products = [
    _Product(name: 'Wireless Earbuds', price: 10, icon: Icons.headphones),
    _Product(name: 'Phone Case', price: 5, icon: Icons.phone_iphone),
    _Product(name: 'USB Cable', price: 3, icon: Icons.cable),
    _Product(name: 'Screen Protector', price: 4, icon: Icons.shield_outlined),
  ];

  /// Cart quantity per product name - absent/`0` means not in the cart.
  final Map<String, int> _cartQuantities = {};

  // Customer details that used to be hardcoded into _cartDetails() -
  // pre-filled with those same values so checkout still works untouched,
  // but now editable via the "Your Details" form below.
  final _nameController = TextEditingController(text: 'anas arif');
  final _emailController = TextEditingController(text: 'anas@gmail.com');
  final _phoneController = TextEditingController(text: '03032290204');
  final _cnicController = TextEditingController(text: '4220153560816');
  final _customerTokenController = TextEditingController(
    text: 'E02C50B2-6697-4845-AF9A-3F34DE8C59E5',
  );

  // Pre-filled with the current default client id/secret - editing either updates the matching
  // top-level _sandboxClientId/_sandboxClientSecret and re-runs SwichPay.initialize(), so a
  // change takes effect on the next checkout without restarting the app.
  final _clientIdController = TextEditingController(text: _sandboxClientId);
  final _clientSecretController = TextEditingController(
    text: _sandboxClientSecret,
  );

  @override
  void initState() {
    super.initState();
    SwichPay.onResult((result) {
      setState(() => _status = 'Result: ${result.status} (${result.message})');
    });
    _clientIdController.addListener(() {
      _sandboxClientId = _clientIdController.text.trim();
      _initializeSwichPay();
    });
    _clientSecretController.addListener(() {
      _sandboxClientSecret = _clientSecretController.text.trim();
      _initializeSwichPay();
    });
  }

  @override
  void dispose() {
    _nameController.dispose();
    _emailController.dispose();
    _phoneController.dispose();
    _cnicController.dispose();
    _customerTokenController.dispose();
    _clientIdController.dispose();
    _clientSecretController.dispose();
    super.dispose();
  }

  int get _cartTotal => _products.fold(
    0,
    (sum, product) =>
        sum + product.price * (_cartQuantities[product.name] ?? 0),
  );

  int get _cartItemCount =>
      _cartQuantities.values.fold(0, (sum, qty) => sum + qty);

  void _incrementQuantity(_Product product) {
    setState(() {
      _cartQuantities[product.name] = (_cartQuantities[product.name] ?? 0) + 1;
    });
  }

  void _decrementQuantity(_Product product) {
    setState(() {
      final current = _cartQuantities[product.name] ?? 0;
      if (current <= 1) {
        _cartQuantities.remove(product.name);
      } else {
        _cartQuantities[product.name] = current - 1;
      }
    });
  }

  /// Cart contents rendered as a single `item` string (e.g. `"Wireless
  /// Earbuds x2, Phone Case x1"`) since [PaymentDetails.item] only takes one
  /// line of text - a real storefront would send its own order/line-item
  /// breakdown instead.
  String get _cartSummary => _products
      .where((p) => (_cartQuantities[p.name] ?? 0) > 0)
      .map((p) => '${p.name} x${_cartQuantities[p.name]}')
      .join(', ');

  PaymentDetails _cartDetails() {
    // A fresh timestamp per checkout attempt - reusing the same
    // billReferenceNo/email across repeated test runs had the gateway
    // treating them as the same order/customer instead of a new one.
    final unique = DateTime.now().millisecondsSinceEpoch;
    final emailBase = _emailController.text.trim();
    final atIndex = emailBase.indexOf('@');
    final uniqueEmail = atIndex == -1
        ? '$emailBase+$unique'
        : '${emailBase.substring(0, atIndex)}+$unique${emailBase.substring(atIndex)}';
    return PaymentDetails(
      orderNumber: 'ORD$unique',
      customerTransactionId: 'new-$unique',
      billReferenceNo: 'test-$unique',
      merchantName: 'anas1 Merchant',
      consumerName: _nameController.text.trim(),
      amount: _cartTotal,
      currency: 'PKR',
      email: uniqueEmail,
      msisdn: _phoneController.text.trim(),
      cnic: _cnicController.text.trim(),
      item: _cartSummary,
      successRedirectUrl: 'https://example.com/success',
      failedRedirectUrl: 'https://example.com/failure',
      attempts: 5,
      expiryMinutes: 15,
      merchantCustomerToken: _customerTokenController.text.trim().isEmpty
          ? null
          : _customerTokenController.text.trim(),
    );
  }

  /// Shows [result] both as the persistent `_status` line (which can be
  /// scrolled out of view) and as an immediate, color-coded SnackBar so
  /// it's visible no matter where the user's scrolled to when a checkout
  /// finishes. Prefers [PaymentResult.message] (the actual API/vendor
  /// text - e.g. a real decline reason or exception detail) over the
  /// bare `status` string, since "Result: failed (null)" or a raw status
  /// word on its own isn't actually informative.
  void _showResult(PaymentResult? result) {
    final message = switch (result) {
      null => 'Sheet dismissed.',
      PaymentResult(:final message?) when message.trim().isNotEmpty => message,
      PaymentResult(:final status) => status,
    };
    setState(() => _status = message);
    if (!mounted) return;
    ScaffoldMessenger.of(context)
      ..hideCurrentSnackBar()
      ..showSnackBar(
        SnackBar(
          content: Text(message),
          backgroundColor: result == null
              ? null
              : (result.isSuccess
                    ? Colors.green.shade700
                    : Colors.red.shade700),
          duration: const Duration(seconds: 5),
        ),
      );
  }

  /// Every checkout button (session-based or direct) needs at least one
  /// product in the cart - shows an error instead of silently no-op-ing
  /// when the user taps one with an empty cart.
  bool _requireProductSelected() {
    if (_cartItemCount > 0) return true;
    ScaffoldMessenger.of(context)
      ..hideCurrentSnackBar()
      ..showSnackBar(const SnackBar(content: Text('Select a product first.')));
    return false;
  }

  Future<void> _checkout() async {
    if (!_requireProductSelected() || _activeCheckout != null) return;
    setState(() => _activeCheckout = _CheckoutAction.checkout);
    try {
      final result = await SwichPay.presentPaymentSheet(
        context: context,
        details: _cartDetails(),
        categories: _checkoutCategories,
      );
      if (!mounted) return;
      _showResult(result);
    } finally {
      if (mounted) setState(() => _activeCheckout = null);
    }
  }

  /// Direct (session-less) checkouts - each skips the "Choose Payment
  /// Method" list/payment session entirely and posts straight to its own
  /// single-category endpoint. [presenter] is one of
  /// `SwichPay.present*Checkout`. [action] identifies which button
  /// triggered this, so only that one shows its loader.
  Future<void> _directCheckout(
    _CheckoutAction action,
    Future<PaymentResult?> Function({
      required BuildContext context,
      required PaymentDetails details,
    })
    presenter,
  ) async {
    if (!_requireProductSelected() || _activeCheckout != null) return;
    setState(() => _activeCheckout = action);
    try {
      final result = await presenter(context: context, details: _cartDetails());
      if (!mounted) return;
      _showResult(result);
    } finally {
      if (mounted) setState(() => _activeCheckout = null);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Swich Pay')),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(24),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Text(_status, textAlign: TextAlign.center),
              const SizedBox(height: 24),
              const Text(
                'Sandbox Credentials (testing)',
                style: TextStyle(fontWeight: FontWeight.w600),
              ),
              const SizedBox(height: 12),
              TextField(
                controller: _clientIdController,
                decoration: const InputDecoration(
                  labelText: 'Client ID',
                  border: OutlineInputBorder(),
                ),
              ),
              const SizedBox(height: 12),
              TextField(
                controller: _clientSecretController,
                obscureText: true,
                decoration: const InputDecoration(
                  labelText: 'Client Secret',
                  border: OutlineInputBorder(),
                ),
              ),
              const SizedBox(height: 24),
              const Text(
                'Your Details',
                style: TextStyle(fontWeight: FontWeight.w600),
              ),
              const SizedBox(height: 12),
              TextField(
                controller: _nameController,
                decoration: const InputDecoration(
                  labelText: 'Name',
                  border: OutlineInputBorder(),
                ),
              ),
              const SizedBox(height: 12),
              TextField(
                controller: _emailController,
                keyboardType: TextInputType.emailAddress,
                decoration: const InputDecoration(
                  labelText: 'Email',
                  border: OutlineInputBorder(),
                ),
              ),
              const SizedBox(height: 12),
              TextField(
                controller: _phoneController,
                keyboardType: TextInputType.phone,
                decoration: const InputDecoration(
                  labelText: 'Mobile Number',
                  border: OutlineInputBorder(),
                ),
              ),
              const SizedBox(height: 12),
              TextField(
                controller: _cnicController,
                keyboardType: TextInputType.number,
                decoration: const InputDecoration(
                  labelText: 'CNIC',
                  border: OutlineInputBorder(),
                ),
              ),
              const SizedBox(height: 12),
              TextField(
                controller: _customerTokenController,
                decoration: const InputDecoration(
                  labelText: 'Merchant Customer Token (optional)',
                  helperText:
                      'A stable id for this customer in your own system - '
                      'set this to get their saved cards/wallets back in '
                      'the session.',
                  border: OutlineInputBorder(),
                ),
              ),
              const SizedBox(height: 24),
              const Text(
                'Products',
                style: TextStyle(fontWeight: FontWeight.w600),
              ),
              const SizedBox(height: 12),
              for (final product in _products) ...[
                _ProductTile(
                  product: product,
                  quantity: _cartQuantities[product.name] ?? 0,
                  onAdd: () => _incrementQuantity(product),
                  onIncrement: () => _incrementQuantity(product),
                  onDecrement: () => _decrementQuantity(product),
                ),
                const SizedBox(height: 12),
              ],
              const SizedBox(height: 12),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Text(
                    'Cart ($_cartItemCount item${_cartItemCount == 1 ? '' : 's'})',
                    style: const TextStyle(fontWeight: FontWeight.w600),
                  ),
                  Text(
                    'PKR $_cartTotal',
                    style: const TextStyle(
                      fontWeight: FontWeight.w700,
                      fontSize: 16,
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 16),
              GradientPayButton(
                label: _cartItemCount == 0
                    ? 'Add items to checkout'
                    : 'Checkout - PKR $_cartTotal',
                loading: _activeCheckout == _CheckoutAction.checkout,
                onPressed: _checkout,
              ),
              const SizedBox(height: 24),
              const Text(
                'Direct checkouts (no payment session)',
                style: TextStyle(fontWeight: FontWeight.w600),
              ),
              const SizedBox(height: 12),
              GradientPayButton(
                label: 'Pay with Wallet',
                loading: _activeCheckout == _CheckoutAction.wallet,
                onPressed: () => _directCheckout(
                  _CheckoutAction.wallet,
                  SwichPay.presentEwalletCheckout,
                ),
              ),
              const SizedBox(height: 12),
              GradientPayButton(
                label: 'Pay with Bank Transfer',
                loading: _activeCheckout == _CheckoutAction.bankTransfer,
                onPressed: () => _directCheckout(
                  _CheckoutAction.bankTransfer,
                  SwichPay.presentBankTransferCheckout,
                ),
              ),
              const SizedBox(height: 12),
              GradientPayButton(
                label: 'Pay via 1Bill',
                loading: _activeCheckout == _CheckoutAction.oneBill,
                onPressed: () => _directCheckout(
                  _CheckoutAction.oneBill,
                  SwichPay.presentOneBillCheckout,
                ),
              ),
              const SizedBox(height: 12),
              GradientPayButton(
                label: 'Pay with QR',
                loading: _activeCheckout == _CheckoutAction.qr,
                onPressed: () => _directCheckout(
                  _CheckoutAction.qr,
                  SwichPay.presentQrCheckout,
                ),
              ),
              const SizedBox(height: 12),
              GradientPayButton(
                label: 'RTP Now',
                loading: _activeCheckout == _CheckoutAction.rtpNow,
                onPressed: () => _directCheckout(
                  _CheckoutAction.rtpNow,
                  SwichPay.presentRtpNowCheckout,
                ),
              ),
              const SizedBox(height: 12),
              GradientPayButton(
                label: 'RTP E-Wallet',
                loading: _activeCheckout == _CheckoutAction.rtpEwallet,
                onPressed: () => _directCheckout(
                  _CheckoutAction.rtpEwallet,
                  SwichPay.presentRtpEwalletCheckout,
                ),
              ),
              const SizedBox(height: 12),
              GradientPayButton(
                label: 'Pay with Card',
                loading: _activeCheckout == _CheckoutAction.card,
                onPressed: () => _directCheckout(
                  _CheckoutAction.card,
                  SwichPay.presentCardCheckout,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _ProductTile extends StatelessWidget {
  const _ProductTile({
    required this.product,
    required this.quantity,
    required this.onAdd,
    required this.onIncrement,
    required this.onDecrement,
  });

  final _Product product;
  final int quantity;
  final VoidCallback onAdd;
  final VoidCallback onIncrement;
  final VoidCallback onDecrement;

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: EdgeInsets.zero,
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Row(
          children: [
            Container(
              width: 48,
              height: 48,
              decoration: BoxDecoration(
                color: const Color(0xFF2563EB).withValues(alpha: 0.1),
                borderRadius: BorderRadius.circular(10),
              ),
              alignment: Alignment.center,
              child: Icon(product.icon, color: const Color(0xFF2563EB)),
            ),
            const SizedBox(width: 12),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    product.name,
                    style: const TextStyle(fontWeight: FontWeight.w600),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    'PKR ${product.price}',
                    style: TextStyle(color: Colors.grey.shade600),
                  ),
                ],
              ),
            ),
            if (quantity == 0)
              OutlinedButton(onPressed: onAdd, child: const Text('Add to Cart'))
            else
              Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  IconButton(
                    onPressed: onDecrement,
                    icon: const Icon(Icons.remove_circle_outline),
                    color: const Color(0xFF2563EB),
                  ),
                  Text(
                    '$quantity',
                    style: const TextStyle(fontWeight: FontWeight.w700),
                  ),
                  IconButton(
                    onPressed: onIncrement,
                    icon: const Icon(Icons.add_circle_outline),
                    color: const Color(0xFF2563EB),
                  ),
                ],
              ),
          ],
        ),
      ),
    );
  }
}
0
likes
0
points
295
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter SDK for integrating Swich payment services.

Homepage

License

unknown (license)

Dependencies

card_scanner, crypto, dio, flutter, gal, path_provider, qr_flutter, share_plus, webview_flutter

More

Packages that depend on swichpay_flutter