credo_flutter 1.0.0 copy "credo_flutter: ^1.0.0" to clipboard
credo_flutter: ^1.0.0 copied to clipboard

Official Credo checkout SDK for Flutter.

example/lib/main.dart

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:credo_flutter/credo_flutter.dart';

void main() {
  CredoCheckout.configure(
    publicKey: 'pk_demo_xxx',
    environment: CredoEnvironment.demo,
    debug: true,
  );

  runApp(const CredoExampleApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Credo Flutter Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int _currentIndex = 0;

  final _pages = const [ProductPage(), UrlCheckoutPage(), CustomCheckoutPage()];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _pages[_currentIndex],
      bottomNavigationBar: NavigationBar(
        selectedIndex: _currentIndex,
        onDestinationSelected: (index) => setState(() => _currentIndex = index),
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.shopping_bag_outlined),
            selectedIcon: Icon(Icons.shopping_bag),
            label: 'Product',
          ),
          NavigationDestination(
            icon: Icon(Icons.link_outlined),
            selectedIcon: Icon(Icons.link),
            label: 'URL Checkout',
          ),
          NavigationDestination(
            icon: Icon(Icons.widgets_outlined),
            selectedIcon: Icon(Icons.widgets),
            label: 'Custom',
          ),
        ],
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Product Page - Demonstrates CredoCheckout.pay()
// ─────────────────────────────────────────────────────────────────────────────

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

  @override
  State<ProductPage> createState() => _ProductPageState();
}

class _ProductPageState extends State<ProductPage> {
  bool _loading = false;

  Future<void> _buyProduct() async {
    setState(() => _loading = true);

    final result = await CredoCheckout.pay(
      context,
      PaymentInitConfig(
        amount: 250000,
        email: 'customer@example.com',
        currency: Currency.ngn,
        bearer: FeeBearer.customer,
        reference: _generateRef(),
      ),
    );

    if (!mounted) return;
    setState(() => _loading = false);

    _showResultSheet(context, result);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Shop'), centerTitle: true),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Container(
              height: 280,
              decoration: BoxDecoration(
                color: Colors.grey.shade100,
                borderRadius: BorderRadius.circular(16),
              ),
              child: const Center(
                child: Icon(Icons.headphones, size: 120, color: Colors.indigo),
              ),
            ),
            const SizedBox(height: 24),
            const Text(
              'Wireless Headphones',
              style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            Text(
              'Premium sound quality with active noise cancellation',
              style: TextStyle(fontSize: 16, color: Colors.grey.shade600),
            ),
            const SizedBox(height: 16),
            const Text(
              '₦2,500.00',
              style: TextStyle(
                fontSize: 28,
                fontWeight: FontWeight.bold,
                color: Colors.indigo,
              ),
            ),
            const SizedBox(height: 32),
            FilledButton(
              onPressed: _loading ? null : _buyProduct,
              style: FilledButton.styleFrom(
                padding: const EdgeInsets.symmetric(vertical: 16),
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(12),
                ),
              ),
              child: _loading
                  ? const SizedBox(
                      width: 24,
                      height: 24,
                      child: CircularProgressIndicator(
                        strokeWidth: 2,
                        color: Colors.white,
                      ),
                    )
                  : const Text('Buy Now', style: TextStyle(fontSize: 16)),
            ),
            const SizedBox(height: 16),
            Text(
              'Uses CredoCheckout.pay() to initialize and open checkout',
              textAlign: TextAlign.center,
              style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
            ),
          ],
        ),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// URL Checkout Page - Demonstrates CredoCheckout.open()
// ─────────────────────────────────────────────────────────────────────────────

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

  @override
  State<UrlCheckoutPage> createState() => _UrlCheckoutPageState();
}

class _UrlCheckoutPageState extends State<UrlCheckoutPage> {
  final _controller = TextEditingController();
  bool _loading = false;

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  Future<void> _openCheckout() async {
    final url = _controller.text.trim();
    if (url.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Please enter a checkout URL')),
      );
      return;
    }

    setState(() => _loading = true);

    final result = await CredoCheckout.open(context, checkoutUrl: url);

    if (!mounted) return;
    setState(() => _loading = false);

    _showResultSheet(context, result);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('URL Checkout'), centerTitle: true),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Container(
              padding: const EdgeInsets.all(20),
              decoration: BoxDecoration(
                color: Colors.indigo.shade50,
                borderRadius: BorderRadius.circular(16),
              ),
              child: Column(
                children: [
                  Icon(Icons.link, size: 48, color: Colors.indigo.shade400),
                  const SizedBox(height: 12),
                  const Text(
                    'Open with Checkout URL',
                    style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    'Paste a checkout URL from your backend to open the payment page directly.',
                    textAlign: TextAlign.center,
                    style: TextStyle(color: Colors.grey.shade600),
                  ),
                ],
              ),
            ),
            const SizedBox(height: 24),
            TextField(
              controller: _controller,
              decoration: InputDecoration(
                labelText: 'Checkout URL',
                hintText: 'https://pay.credodemo.com/v4/...',
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(12),
                ),
                prefixIcon: const Icon(Icons.link),
              ),
              keyboardType: TextInputType.url,
              textInputAction: TextInputAction.done,
              onSubmitted: (_) => _openCheckout(),
            ),
            const SizedBox(height: 24),
            FilledButton(
              onPressed: _loading ? null : _openCheckout,
              style: FilledButton.styleFrom(
                padding: const EdgeInsets.symmetric(vertical: 16),
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(12),
                ),
              ),
              child: _loading
                  ? const SizedBox(
                      width: 24,
                      height: 24,
                      child: CircularProgressIndicator(
                        strokeWidth: 2,
                        color: Colors.white,
                      ),
                    )
                  : const Text('Open Checkout', style: TextStyle(fontSize: 16)),
            ),
            const SizedBox(height: 16),
            Text(
              'Uses CredoCheckout.open() — no SDK configuration required',
              textAlign: TextAlign.center,
              style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
            ),
          ],
        ),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Custom Checkout Page - Demonstrates CredoWebview widget
// ─────────────────────────────────────────────────────────────────────────────

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

  @override
  State<CustomCheckoutPage> createState() => _CustomCheckoutPageState();
}

class _CustomCheckoutPageState extends State<CustomCheckoutPage> {
  final _controller = TextEditingController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _openInModal() {
    final url = _controller.text.trim();
    if (url.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Please enter a checkout URL')),
      );
      return;
    }

    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      useSafeArea: true,
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
      ),
      builder: (sheetContext) => _CheckoutSheet(
        checkoutUrl: url,
        onResult: (result) {
          Navigator.pop(sheetContext);
          _showResultSheet(context, result);
        },
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Custom'), centerTitle: true),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Container(
              padding: const EdgeInsets.all(20),
              decoration: BoxDecoration(
                color: Colors.teal.shade50,
                borderRadius: BorderRadius.circular(16),
              ),
              child: Column(
                children: [
                  Icon(Icons.widgets, size: 48, color: Colors.teal.shade400),
                  const SizedBox(height: 12),
                  const Text(
                    'CredoWebview Widget',
                    style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    'Embed the checkout WebView anywhere — in a modal, a custom page, or inline. You control the presentation.',
                    textAlign: TextAlign.center,
                    style: TextStyle(color: Colors.grey.shade600),
                  ),
                ],
              ),
            ),
            const SizedBox(height: 24),
            TextField(
              controller: _controller,
              decoration: InputDecoration(
                labelText: 'Checkout URL',
                hintText: 'https://pay.credodemo.com/v4/...',
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(12),
                ),
                prefixIcon: const Icon(Icons.link),
              ),
              keyboardType: TextInputType.url,
              textInputAction: TextInputAction.done,
              onSubmitted: (_) => _openInModal(),
            ),
            const SizedBox(height: 24),
            FilledButton(
              onPressed: _openInModal,
              style: FilledButton.styleFrom(
                padding: const EdgeInsets.symmetric(vertical: 16),
                backgroundColor: Colors.teal,
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(12),
                ),
              ),
              child: const Text(
                'Open in Modal Sheet',
                style: TextStyle(fontSize: 16),
              ),
            ),
            const SizedBox(height: 16),
            Text(
              'Uses CredoWebview widget inside a modal bottom sheet',
              textAlign: TextAlign.center,
              style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
            ),
          ],
        ),
      ),
    );
  }
}

/// A modal sheet that wraps CredoWebview with a custom header.
class _CheckoutSheet extends StatefulWidget {
  final String checkoutUrl;
  final ValueChanged<CheckoutResult> onResult;

  const _CheckoutSheet({
    required this.checkoutUrl,
    required this.onResult,
  });

  @override
  State<_CheckoutSheet> createState() => _CheckoutSheetState();
}

class _CheckoutSheetState extends State<_CheckoutSheet> {
  bool _isLoading = true;
  bool _resultDelivered = false;

  void _handleResult(CheckoutResult result) {
    if (_resultDelivered) return;
    _resultDelivered = true;
    widget.onResult(result);
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: MediaQuery.of(context).size.height * 0.92,
      child: Column(
        children: [
          // Custom header
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
            decoration: BoxDecoration(
              color: Theme.of(context).colorScheme.surface,
              borderRadius:
                  const BorderRadius.vertical(top: Radius.circular(16)),
            ),
            child: Row(
              children: [
                IconButton(
                  icon: const Icon(Icons.close),
                  onPressed: () =>
                      _handleResult(CheckoutResult.cancelled()),
                ),
                const Expanded(
                  child: Text(
                    'Checkout',
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.w600,
                    ),
                  ),
                ),
                const SizedBox(width: 48),
              ],
            ),
          ),
          const Divider(height: 1),

          // WebView
          Expanded(
            child: Stack(
              children: [
                CredoWebview(
                  checkoutUrl: widget.checkoutUrl,
                  onResult: _handleResult,
                  onLoadingChanged: (loading) =>
                      setState(() => _isLoading = loading),
                ),
                if (_isLoading)
                  const ColoredBox(
                    color: Colors.white,
                    child: Center(child: CircularProgressIndicator()),
                  ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Result Sheet - Shows checkout result
// ─────────────────────────────────────────────────────────────────────────────

class ResultSheet extends StatelessWidget {
  final CheckoutResult result;

  const ResultSheet({super.key, required this.result});

  @override
  Widget build(BuildContext context) {
    final (icon, color, title) = switch (result.status) {
      CheckoutStatus.success => (
        Icons.check_circle,
        Colors.green,
        'Payment Successful',
      ),
      CheckoutStatus.failed => (Icons.error, Colors.red, 'Payment Failed'),
      CheckoutStatus.cancelled => (Icons.cancel, Colors.orange, 'Cancelled'),
      CheckoutStatus.unknown => (Icons.help, Colors.grey, 'Unknown Status'),
    };

    return Padding(
      padding: const EdgeInsets.all(24),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(icon, size: 64, color: color),
          const SizedBox(height: 16),
          Text(
            title,
            style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          if (result.transaction != null) ...[
            const SizedBox(height: 16),
            _InfoRow(
              label: 'Amount',
              value:
                  '${result.transaction!.currency ?? ''} ${result.transaction!.amount ?? '-'}',
            ),
            _InfoRow(
              label: 'Transaction Reference',
              value: result.transaction?.transRef ?? '-',
            ),
            if (result.transaction!.message != null)
              _InfoRow(label: 'Message', value: result.transaction!.message!),
          ],
          if (result.hasFieldErrors) ...[
            const SizedBox(height: 16),
            ...result.fieldErrors!.entries.map(
              (e) => _InfoRow(label: e.key, value: e.value),
            ),
          ],
          const SizedBox(height: 24),
          SizedBox(
            width: double.infinity,
            child: FilledButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('Done'),
            ),
          ),
        ],
      ),
    );
  }
}

class _InfoRow extends StatelessWidget {
  final String label;
  final String value;

  const _InfoRow({required this.label, required this.value});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label, style: TextStyle(color: Colors.grey.shade600)),
          Flexible(
            child: Text(
              value,
              textAlign: TextAlign.end,
              style: const TextStyle(fontWeight: FontWeight.w500),
            ),
          ),
        ],
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Shared helpers
// ─────────────────────────────────────────────────────────────────────────────

void _showResultSheet(BuildContext context, CheckoutResult result) {
  showModalBottomSheet(
    context: context,
    builder: (context) => ResultSheet(result: result),
  );
}

String _generateRef({int length = 20}) {
  const chars =
      'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  final random = Random.secure();
  return List.generate(
    length,
    (_) => chars[random.nextInt(chars.length)],
  ).join();
}
0
likes
150
points
69
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Official Credo checkout SDK for Flutter.

Homepage
Repository (GitHub)
View/report issues

Topics

#payments #checkout #flutter #fintech #sdk

License

BSD-3-Clause (license)

Dependencies

flutter, http, webview_flutter

More

Packages that depend on credo_flutter