paypal_checkout_flutter 0.1.1 copy "paypal_checkout_flutter: ^0.1.1" to clipboard
paypal_checkout_flutter: ^0.1.1 copied to clipboard

Flutter PayPal integration — native SDK checkout, cards, vault, subscriptions, plans, products, orders & refunds. No WebView. Includes event streams, webhook helpers, analytics, and UI components.

example/lib/main.dart

// ignore_for_file: avoid_print

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

// ─── Entry point ───────────────────────────────────────────────────────────────

void main() {
  runApp(const PaypalDemoApp());
}

// ─── App ───────────────────────────────────────────────────────────────────────

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'PayPal Plugin Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF003087)),
        useMaterial3: true,
        fontFamily: 'Roboto',
      ),
      home: const HomeScreen(),
    );
  }
}

// ─── Shared state ──────────────────────────────────────────────────────────────

/// Fill in your PayPal sandbox credentials here.
const _kClientId =
    'YOUR_SANDBOX_CLIENT_ID';
const _kClientSecret =
    'YOUR_SANDBOX_CLIENT_SECRET';
const _kReturnUrl = 'com.example.demo://paypalpay';

final _paypal = FlutterPaypalPayment();
bool _initialized = false;

Future<String?> _ensureInit() async {
  if (_initialized) return null;
  final result = await _paypal.init(
    PaypalConfig(
      clientId: _kClientId,
      environment: PaypalEnvironment.sandbox,
      returnUrl: _kReturnUrl,
      debugMode: true,
    ),
  );
  return result.fold(
    (f) => f.message,
    (_) {
      _initialized = true;
      return null;
    },
  );
}

// ─── Home Screen ───────────────────────────────────────────────────────────────

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF4F6FB),
      appBar: AppBar(
        title: const Text(
          'PayPal Plugin Demo',
          style: TextStyle(fontWeight: FontWeight.w700),
        ),
        centerTitle: true,
        backgroundColor: Colors.white,
        foregroundColor: const Color(0xFF001C64),
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
      ),
      body: ListView(
        padding: const EdgeInsets.all(20),
        children: [
          // ── Credentials notice ──
          Container(
            padding: const EdgeInsets.all(14),
            decoration: BoxDecoration(
              color: const Color(0xFFFFF8E1),
              borderRadius: BorderRadius.circular(12),
              border: Border.all(color: const Color(0xFFFFCC02)),
            ),
            child: const Row(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Icon(Icons.info_outline, color: Color(0xFFE65100), size: 18),
                SizedBox(width: 8),
                Expanded(
                  child: Text(
                    'Replace _kClientId / _kClientSecret at the top of main.dart '
                    'with your PayPal sandbox credentials before testing real flows.',
                    style:
                        TextStyle(fontSize: 12, color: Color(0xFF5D4037)),
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(height: 24),

          _SectionTitle('Card UI Themes'),
          const SizedBox(height: 12),
          _FeatureCard(
            icon: Icons.palette_outlined,
            title: 'PayPal Theme',
            subtitle: 'Classic light paysheet with blue gradient card',
            color: const Color(0xFF003087),
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(
                builder: (_) => const CardFormDemoScreen(
                  theme: PaypalCardFormTheme.paypal,
                  themeName: 'PayPal',
                ),
              ),
            ),
          ),
          _FeatureCard(
            icon: Icons.dark_mode_outlined,
            title: 'Dark Theme',
            subtitle: 'Night mode with violet accent',
            color: const Color(0xFF7B61FF),
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(
                builder: (_) => const CardFormDemoScreen(
                  theme: PaypalCardFormTheme.dark,
                  themeName: 'Dark',
                ),
              ),
            ),
          ),
          _FeatureCard(
            icon: Icons.auto_awesome_outlined,
            title: 'Aurora Theme',
            subtitle: 'Deep purple to teal gradient',
            color: const Color(0xFF5E35B1),
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(
                builder: (_) => const CardFormDemoScreen(
                  theme: PaypalCardFormTheme.aurora,
                  themeName: 'Aurora',
                ),
              ),
            ),
          ),
          _FeatureCard(
            icon: Icons.star_outline,
            title: 'Gold Theme',
            subtitle: 'Midnight obsidian with gold accents',
            color: const Color(0xFFD4AF37),
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(
                builder: (_) => const CardFormDemoScreen(
                  theme: PaypalCardFormTheme.gold,
                  themeName: 'Gold',
                ),
              ),
            ),
          ),

          const SizedBox(height: 24),
          _SectionTitle('Theme Comparison'),
          const SizedBox(height: 12),
          _FeatureCard(
            icon: Icons.compare_outlined,
            title: 'Compare All Themes',
            subtitle: 'Swipe between all 4 card form themes side by side',
            color: const Color(0xFF009CDE),
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const ThemeComparisonScreen()),
            ),
          ),

          const SizedBox(height: 24),
          _SectionTitle('Payment Flows'),
          const SizedBox(height: 12),
          _FeatureCard(
            icon: Icons.credit_card_outlined,
            title: 'Card Payment (with backend)',
            subtitle: 'payWithCard — provide your own orderId',
            color: const Color(0xFF00875A),
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const CardPaymentScreen()),
            ),
          ),
          _FeatureCard(
            icon: Icons.flash_on_outlined,
            title: 'Card Payment (no backend)',
            subtitle: 'payWithCardDirect — creates order internally',
            color: const Color(0xFF006FCF),
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(
                  builder: (_) => const CardPaymentDirectScreen()),
            ),
          ),
          _FeatureCard(
            icon: Icons.account_balance_wallet_outlined,
            title: 'PayPal Checkout (with backend)',
            subtitle: 'pay — provide your own orderId',
            color: const Color(0xFF009CDE),
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const PaypalCheckoutScreen()),
            ),
          ),
          _FeatureCard(
            icon: Icons.bolt_outlined,
            title: 'PayPal Checkout (no backend)',
            subtitle: 'payDirect — creates order internally',
            color: const Color(0xFF0070BA),
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(
                  builder: (_) => const PaypalCheckoutDirectScreen()),
            ),
          ),

          const SizedBox(height: 24),
          _SectionTitle('Vault (Save Payment Method)'),
          const SizedBox(height: 12),
          _FeatureCard(
            icon: Icons.lock_outline,
            title: 'Vault Card',
            subtitle: 'Save a card for future payments',
            color: const Color(0xFFAB47BC),
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const VaultCardScreen()),
            ),
          ),
          _FeatureCard(
            icon: Icons.account_circle_outlined,
            title: 'Vault PayPal Account',
            subtitle: 'Save a PayPal account for future payments',
            color: const Color(0xFF7E57C2),
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const VaultPaypalScreen()),
            ),
          ),
          const SizedBox(height: 32),
        ],
      ),
    );
  }
}

// ─── Shared widgets ────────────────────────────────────────────────────────────

class _SectionTitle extends StatelessWidget {
  const _SectionTitle(this.title);
  final String title;

  @override
  Widget build(BuildContext context) {
    return Text(
      title,
      style: const TextStyle(
        fontSize: 13,
        fontWeight: FontWeight.w700,
        color: Color(0xFF6C7378),
        letterSpacing: 0.8,
      ),
    );
  }
}

class _FeatureCard extends StatelessWidget {
  const _FeatureCard({
    required this.icon,
    required this.title,
    required this.subtitle,
    required this.color,
    required this.onTap,
  });

  final IconData icon;
  final String title;
  final String subtitle;
  final Color color;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.only(bottom: 12),
      child: Material(
        color: Colors.white,
        borderRadius: BorderRadius.circular(14),
        elevation: 0,
        child: InkWell(
          borderRadius: BorderRadius.circular(14),
          onTap: onTap,
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
            child: Row(
              children: [
                Container(
                  width: 44,
                  height: 44,
                  decoration: BoxDecoration(
                    color: color.withValues(alpha: 0.12),
                    borderRadius: BorderRadius.circular(10),
                  ),
                  child: Icon(icon, color: color, size: 22),
                ),
                const SizedBox(width: 14),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        title,
                        style: const TextStyle(
                          fontWeight: FontWeight.w600,
                          fontSize: 14,
                          color: Color(0xFF1A1D27),
                        ),
                      ),
                      const SizedBox(height: 2),
                      Text(
                        subtitle,
                        style: const TextStyle(
                          fontSize: 12,
                          color: Color(0xFF6C7378),
                        ),
                      ),
                    ],
                  ),
                ),
                Icon(Icons.chevron_right,
                    color: color.withValues(alpha: 0.6), size: 20),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class _ResultBanner extends StatelessWidget {
  const _ResultBanner({required this.result});

  final String? result;

  @override
  Widget build(BuildContext context) {
    if (result == null) return const SizedBox.shrink();
    final isSuccess = result!.startsWith('✓');
    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
      padding: const EdgeInsets.all(14),
      decoration: BoxDecoration(
        color: isSuccess
            ? const Color(0xFFE6F4EA)
            : const Color(0xFFFFEBEE),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(
          color: isSuccess
              ? const Color(0xFF4CAF50)
              : const Color(0xFFEF5350),
        ),
      ),
      child: Row(
        children: [
          Icon(
            isSuccess ? Icons.check_circle_outline : Icons.error_outline,
            color: isSuccess
                ? const Color(0xFF2E7D32)
                : const Color(0xFFC62828),
            size: 18,
          ),
          const SizedBox(width: 8),
          Expanded(
            child: Text(
              result!,
              style: TextStyle(
                color: isSuccess
                    ? const Color(0xFF2E7D32)
                    : const Color(0xFFC62828),
                fontSize: 13,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

// ─── Card Form Demo Screen (theme showcase) ────────────────────────────────────

class CardFormDemoScreen extends StatefulWidget {
  const CardFormDemoScreen({
    super.key,
    required this.theme,
    required this.themeName,
  });

  final PaypalCardFormTheme theme;
  final String themeName;

  @override
  State<CardFormDemoScreen> createState() => _CardFormDemoScreenState();
}

class _CardFormDemoScreenState extends State<CardFormDemoScreen> {
  String? _result;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: widget.theme.backgroundColor,
      appBar: AppBar(
        title: Text('${widget.themeName} Theme'),
        backgroundColor: widget.theme.backgroundColor,
        foregroundColor: widget.theme.primaryColor,
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
      ),
      body: Column(
        children: [
          _ResultBanner(result: _result),
          Expanded(
            child: SingleChildScrollView(
              padding: const EdgeInsets.all(20),
              child: PaypalCardForm(
                theme: widget.theme,
                amount: '49.99',
                currency: 'USD',
                submitButtonText: 'Pay \$49.99',
                requireCardholderName: true,
                requireBillingPostalCode: true,
                onSubmit: (card) async {
                  // Simulate a short processing delay
                  await Future.delayed(const Duration(seconds: 2));
                  setState(() {
                    _result =
                        '✓ Card validated: •••• •••• •••• ${card.number.substring(card.number.length - 4)}'
                        '  exp ${card.expirationMonth}/${card.expirationYear}';
                  });
                },
                onError: (msg) {
                  setState(() => _result = '✗ Error: $msg');
                },
              ),
            ),
          ),
        ],
      ),
    );
  }
}

// ─── Theme Comparison Screen ───────────────────────────────────────────────────

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

  @override
  State<ThemeComparisonScreen> createState() =>
      _ThemeComparisonScreenState();
}

class _ThemeComparisonScreenState extends State<ThemeComparisonScreen> {
  static const _themes = [
    (PaypalCardFormTheme.paypal, 'PayPal'),
    (PaypalCardFormTheme.dark, 'Dark'),
    (PaypalCardFormTheme.aurora, 'Aurora'),
    (PaypalCardFormTheme.gold, 'Gold'),
  ];

  int _page = 0;
  final _controller = PageController();

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

  @override
  Widget build(BuildContext context) {
    final current = _themes[_page];
    return Scaffold(
      backgroundColor: current.$1.backgroundColor,
      appBar: AppBar(
        title: Text(current.$2),
        backgroundColor: current.$1.backgroundColor,
        foregroundColor: current.$1.primaryColor,
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
        actions: [
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Text(
              '${_page + 1} / ${_themes.length}',
              style: TextStyle(color: current.$1.secondaryColor),
            ),
          ),
        ],
      ),
      body: Column(
        children: [
          // Theme tab bar
          Padding(
            padding:
                const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
            child: Row(
              children: List.generate(_themes.length, (i) {
                final selected = i == _page;
                return Expanded(
                  child: GestureDetector(
                    onTap: () {
                      _controller.animateToPage(i,
                          duration: const Duration(milliseconds: 350),
                          curve: Curves.easeInOut);
                    },
                    child: AnimatedContainer(
                      duration: const Duration(milliseconds: 250),
                      margin: EdgeInsets.only(right: i < _themes.length - 1 ? 8 : 0),
                      padding: const EdgeInsets.symmetric(vertical: 8),
                      decoration: BoxDecoration(
                        color: selected
                            ? current.$1.accentColor
                            : current.$1.inputFillColor,
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: Text(
                        _themes[i].$2,
                        textAlign: TextAlign.center,
                        style: TextStyle(
                          fontSize: 12,
                          fontWeight: FontWeight.w600,
                          color: selected
                              ? current.$1.buttonTextColor
                              : current.$1.secondaryColor,
                        ),
                      ),
                    ),
                  ),
                );
              }),
            ),
          ),

          Expanded(
            child: PageView.builder(
              controller: _controller,
              onPageChanged: (i) => setState(() => _page = i),
              itemCount: _themes.length,
              itemBuilder: (_, i) {
                final (theme, _) = _themes[i];
                return AnimatedContainer(
                  duration: const Duration(milliseconds: 350),
                  color: theme.backgroundColor,
                  child: SingleChildScrollView(
                    padding: const EdgeInsets.symmetric(horizontal: 20),
                    child: PaypalCardForm(
                      theme: theme,
                      amount: '99.00',
                      currency: 'USD',
                      requireCardholderName: true,
                      onSubmit: (_) async {},
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

// ─── Card Payment Screen (with backend / own orderId) ─────────────────────────

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

  @override
  State<CardPaymentScreen> createState() => _CardPaymentScreenState();
}

class _CardPaymentScreenState extends State<CardPaymentScreen> {
  final _orderIdController =
      TextEditingController(text: 'PASTE_YOUR_ORDER_ID');
  String? _result;
  bool _loading = false;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF4F6FB),
      appBar: AppBar(
        title: const Text('Card Payment (with backend)'),
        backgroundColor: Colors.white,
        foregroundColor: const Color(0xFF001C64),
        elevation: 0,
        surfaceTintColor: Colors.transparent,
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Padding(
            padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
            child: TextField(
              controller: _orderIdController,
              decoration: const InputDecoration(
                labelText: 'Order ID (from your backend)',
                border: OutlineInputBorder(),
              ),
            ),
          ),
          _ResultBanner(result: _result),
          Expanded(
            child: SingleChildScrollView(
              padding: const EdgeInsets.all(20),
              child: PaypalCardForm(
                amount: '25.00',
                currency: 'USD',
                isLoading: _loading,
                onSubmit: (card) async {
                  final initError = await _ensureInit();
                  if (initError != null) {
                    setState(() => _result = '✗ Init error: $initError');
                    return;
                  }
                  setState(() => _loading = true);
                  final result = await _paypal.payWithCard(
                    CardPaymentRequest(
                      orderId: _orderIdController.text.trim(),
                      card: card,
                    ),
                  );
                  setState(() {
                    _loading = false;
                    _result = result.fold(
                      (f) => '✗ ${f.message} (${f.code})',
                      (s) => '✓ Paid! Order: ${s.orderId}  '
                          '3DS: ${s.didAttemptThreeDSecureAuthentication}',
                    );
                  });
                },
                onError: (msg) =>
                    setState(() => _result = '✗ Form error: $msg'),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

// ─── Card Payment Direct Screen (no backend) ──────────────────────────────────

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

  @override
  State<CardPaymentDirectScreen> createState() =>
      _CardPaymentDirectScreenState();
}

class _CardPaymentDirectScreenState
    extends State<CardPaymentDirectScreen> {
  final _amountController = TextEditingController(text: '35.00');
  final _currencyController = TextEditingController(text: 'USD');
  String? _result;
  bool _loading = false;

  @override
  void dispose() {
    _amountController.dispose();
    _currencyController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF4F6FB),
      appBar: AppBar(
        title: const Text('Card Payment (no backend)'),
        backgroundColor: Colors.white,
        foregroundColor: const Color(0xFF001C64),
        elevation: 0,
        surfaceTintColor: Colors.transparent,
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Padding(
            padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
            child: Row(
              children: [
                Expanded(
                  flex: 2,
                  child: TextField(
                    controller: _amountController,
                    keyboardType: TextInputType.number,
                    decoration: const InputDecoration(
                      labelText: 'Amount',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: TextField(
                    controller: _currencyController,
                    decoration: const InputDecoration(
                      labelText: 'Currency',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
              ],
            ),
          ),
          _ResultBanner(result: _result),
          Expanded(
            child: SingleChildScrollView(
              padding: const EdgeInsets.all(20),
              child: PaypalCardForm(
                theme: PaypalCardFormTheme.dark,
                amount: _amountController.text,
                currency: _currencyController.text,
                isLoading: _loading,
                requireCardholderName: true,
                onSubmit: (card) async {
                  final initError = await _ensureInit();
                  if (initError != null) {
                    setState(() => _result = '✗ Init error: $initError');
                    return;
                  }
                  setState(() => _loading = true);
                  final result = await _paypal.payWithCardDirect(
                    clientSecret: _kClientSecret,
                    params: PaymentParams(
                      amount: _amountController.text,
                      currencyCode: _currencyController.text,
                      description: 'Demo purchase',
                    ),
                    buildRequest: (orderId) => CardPaymentRequest(
                      orderId: orderId,
                      card: card,
                    ),
                  );
                  setState(() {
                    _loading = false;
                    _result = result.fold(
                      (f) => '✗ ${f.message} (${f.code})',
                      (s) => '✓ Paid! Order: ${s.orderId}',
                    );
                  });
                },
                onError: (msg) =>
                    setState(() => _result = '✗ Form error: $msg'),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

// ─── PayPal Checkout Screen (with backend) ────────────────────────────────────

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

  @override
  State<PaypalCheckoutScreen> createState() => _PaypalCheckoutScreenState();
}

class _PaypalCheckoutScreenState extends State<PaypalCheckoutScreen> {
  final _orderIdController =
      TextEditingController(text: 'PASTE_YOUR_ORDER_ID');
  String? _result;
  bool _loading = false;

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

  Future<void> _pay() async {
    setState(() {
      _loading = true;
      _result = null;
    });
    final initError = await _ensureInit();
    if (initError != null) {
      setState(() {
        _loading = false;
        _result = '✗ Init error: $initError';
      });
      return;
    }
    final result = await _paypal.pay(
      PaymentRequest(orderId: _orderIdController.text.trim()),
    );
    setState(() {
      _loading = false;
      _result = result.fold(
        (f) => '✗ ${f.message} (${f.code})',
        (s) => '✓ Paid! Order: ${s.orderId}  Payer: ${s.payerId}',
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF4F6FB),
      appBar: AppBar(
        title: const Text('PayPal Checkout (with backend)'),
        backgroundColor: Colors.white,
        foregroundColor: const Color(0xFF001C64),
        elevation: 0,
        surfaceTintColor: Colors.transparent,
      ),
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const Text(
              'Create an order on your backend and paste the Order ID below.',
              style: TextStyle(color: Color(0xFF6C7378)),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _orderIdController,
              decoration: const InputDecoration(
                labelText: 'Order ID',
                border: OutlineInputBorder(),
              ),
            ),
            _ResultBanner(result: _result),
            const SizedBox(height: 8),
            SizedBox(
              height: 52,
              child: ElevatedButton.icon(
                onPressed: _loading ? null : _pay,
                icon: _loading
                    ? const SizedBox(
                        width: 18,
                        height: 18,
                        child:
                            CircularProgressIndicator(strokeWidth: 2.5),
                      )
                    : const Icon(Icons.account_balance_wallet_outlined),
                label: const Text('Open PayPal Checkout'),
                style: ElevatedButton.styleFrom(
                  backgroundColor: const Color(0xFF003087),
                  foregroundColor: Colors.white,
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// ─── PayPal Checkout Direct Screen (no backend) ───────────────────────────────

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

  @override
  State<PaypalCheckoutDirectScreen> createState() =>
      _PaypalCheckoutDirectScreenState();
}

class _PaypalCheckoutDirectScreenState
    extends State<PaypalCheckoutDirectScreen> {
  final _amountController = TextEditingController(text: '50.00');
  final _currencyController = TextEditingController(text: 'USD');
  final _descController =
      TextEditingController(text: 'Demo purchase');
  String? _result;
  bool _loading = false;

  @override
  void dispose() {
    _amountController.dispose();
    _currencyController.dispose();
    _descController.dispose();
    super.dispose();
  }

  Future<void> _pay() async {
    setState(() {
      _loading = true;
      _result = null;
    });
    final initError = await _ensureInit();
    if (initError != null) {
      setState(() {
        _loading = false;
        _result = '✗ Init error: $initError';
      });
      return;
    }
    final result = await _paypal.payDirect(
      clientSecret: _kClientSecret,
      params: PaymentParams(
        amount: _amountController.text,
        currencyCode: _currencyController.text,
        description: _descController.text.isEmpty
            ? null
            : _descController.text,
      ),
    );
    setState(() {
      _loading = false;
      _result = result.fold(
        (f) => '✗ ${f.message} (${f.code})',
        (s) => '✓ Paid & captured! Order: ${s.orderId}',
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF4F6FB),
      appBar: AppBar(
        title: const Text('PayPal Checkout (no backend)'),
        backgroundColor: Colors.white,
        foregroundColor: const Color(0xFF001C64),
        elevation: 0,
        surfaceTintColor: Colors.transparent,
      ),
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Row(
              children: [
                Expanded(
                  flex: 2,
                  child: TextField(
                    controller: _amountController,
                    keyboardType: TextInputType.number,
                    decoration: const InputDecoration(
                      labelText: 'Amount',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: TextField(
                    controller: _currencyController,
                    decoration: const InputDecoration(
                      labelText: 'Currency',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 12),
            TextField(
              controller: _descController,
              decoration: const InputDecoration(
                labelText: 'Description (optional)',
                border: OutlineInputBorder(),
              ),
            ),
            _ResultBanner(result: _result),
            const SizedBox(height: 8),
            SizedBox(
              height: 52,
              child: ElevatedButton.icon(
                onPressed: _loading ? null : _pay,
                icon: _loading
                    ? const SizedBox(
                        width: 18,
                        height: 18,
                        child:
                            CircularProgressIndicator(strokeWidth: 2.5),
                      )
                    : const Icon(Icons.bolt),
                label: const Text('Pay with PayPal'),
                style: ElevatedButton.styleFrom(
                  backgroundColor: const Color(0xFF0070BA),
                  foregroundColor: Colors.white,
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// ─── Vault Card Screen ────────────────────────────────────────────────────────

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

  @override
  State<VaultCardScreen> createState() => _VaultCardScreenState();
}

class _VaultCardScreenState extends State<VaultCardScreen> {
  final _setupTokenController =
      TextEditingController(text: 'PASTE_SETUP_TOKEN');
  String? _result;
  bool _loading = false;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF4F6FB),
      appBar: AppBar(
        title: const Text('Vault Card'),
        backgroundColor: Colors.white,
        foregroundColor: const Color(0xFF001C64),
        elevation: 0,
        surfaceTintColor: Colors.transparent,
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Padding(
            padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                const Text(
                  'Create a setup token via the PayPal API and paste it below, '
                  'or use vaultCardDirect to skip the backend step.',
                  style:
                      TextStyle(fontSize: 12, color: Color(0xFF6C7378)),
                ),
                const SizedBox(height: 12),
                TextField(
                  controller: _setupTokenController,
                  decoration: const InputDecoration(
                    labelText: 'Setup Token',
                    border: OutlineInputBorder(),
                  ),
                ),
              ],
            ),
          ),
          _ResultBanner(result: _result),
          Expanded(
            child: SingleChildScrollView(
              padding: const EdgeInsets.all(20),
              child: PaypalCardForm(
                theme: PaypalCardFormTheme.aurora,
                submitButtonText: 'Save Card',
                isLoading: _loading,
                onSubmit: (card) async {
                  final initError = await _ensureInit();
                  if (initError != null) {
                    setState(() => _result = '✗ Init error: $initError');
                    return;
                  }
                  setState(() => _loading = true);
                  final result = await _paypal.vaultCard(
                    VaultCardRequest(
                      setupTokenId: _setupTokenController.text.trim(),
                      card: card,
                    ),
                  );
                  setState(() {
                    _loading = false;
                    _result = result.fold(
                      (f) => '✗ ${f.message} (${f.code})',
                      (s) => '✓ Vaulted! Setup token: ${s.setupTokenId}  '
                          'Status: ${s.status}',
                    );
                  });
                },
                onError: (msg) =>
                    setState(() => _result = '✗ Form error: $msg'),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

// ─── Vault PayPal Screen ───────────────────────────────────────────────────────

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

  @override
  State<VaultPaypalScreen> createState() => _VaultPaypalScreenState();
}

class _VaultPaypalScreenState extends State<VaultPaypalScreen> {
  String? _result;
  bool _loading = false;

  Future<void> _vault() async {
    setState(() {
      _loading = true;
      _result = null;
    });
    final initError = await _ensureInit();
    if (initError != null) {
      setState(() {
        _loading = false;
        _result = '✗ Init error: $initError';
      });
      return;
    }
    final result = await _paypal.vaultPaypalDirect(
      clientSecret: _kClientSecret,
    );
    setState(() {
      _loading = false;
      _result = result.fold(
        (f) => '✗ ${f.message} (${f.code})',
        (s) =>
            '✓ PayPal account vaulted! Token: ${s.setupTokenId}  Status: ${s.status}',
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF4F6FB),
      appBar: AppBar(
        title: const Text('Vault PayPal Account'),
        backgroundColor: Colors.white,
        foregroundColor: const Color(0xFF001C64),
        elevation: 0,
        surfaceTintColor: Colors.transparent,
      ),
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const Text(
              'Opens a PayPal checkout sheet where the user authorizes saving '
              'their account. No card form required.',
              style: TextStyle(color: Color(0xFF6C7378)),
            ),
            _ResultBanner(result: _result),
            const SizedBox(height: 16),
            SizedBox(
              height: 52,
              child: ElevatedButton.icon(
                onPressed: _loading ? null : _vault,
                icon: _loading
                    ? const SizedBox(
                        width: 18,
                        height: 18,
                        child: CircularProgressIndicator(
                          strokeWidth: 2.5,
                          color: Colors.white,
                        ),
                      )
                    : const Icon(Icons.account_circle_outlined),
                label: const Text('Save PayPal Account'),
                style: ElevatedButton.styleFrom(
                  backgroundColor: const Color(0xFF7E57C2),
                  foregroundColor: Colors.white,
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
0
likes
140
points
116
downloads

Documentation

API reference

Publisher

verified publisherignacio-manchu.com

Weekly Downloads

Flutter PayPal integration — native SDK checkout, cards, vault, subscriptions, plans, products, orders & refunds. No WebView. Includes event streams, webhook helpers, analytics, and UI components.

Repository (GitHub)
View/report issues

Topics

#paypal #payments #checkout #subscriptions #in-app-purchase

License

BSD-3-Clause (license)

Dependencies

crypto, dartz, flutter, http, plugin_platform_interface

More

Packages that depend on paypal_checkout_flutter

Packages that implement paypal_checkout_flutter