paypal_payment_flutter

Pub License GitHub stars

Overview

A comprehensive Flutter plugin for integrating PayPal payments into your Flutter application. This plugin supports both Android and iOS platforms and provides access to PayPal's Orders API, Payments API (v1 & v2), Transactions API, and Checkout flows.

Features

  • Orders API: Create, capture, and authorize orders
  • Card Payments: Accept credit and debit card payments with 3D Secure support
  • Payments API V1: Create, execute, capture, void, and refund payments
  • Payments API V2: Capture, void, and refund authorized payments
  • Transactions API: List and search transaction history
  • Checkout API: Simplified checkout flows with native WebView integration
  • Manual Flows: Build custom checkout experiences without using Checkout API

Installation

Add this to your package's pubspec.yaml file:

dependencies:
  paypal_payment_flutter: ^0.0.3

Then run:

flutter pub get

Setup

1. Android Manifest Setup

To handle the redirect from the PayPal payment flow back to your app, you need to configure a custom URL scheme in your AndroidManifest.xml file. This allows your app to open automatically when the payment is complete.

  1. Open android/app/src/main/AndroidManifest.xml.
  2. Locate the <activity> tag with the android:name=".MainActivity".
  3. Add the following <intent-filter> inside the <activity> tag:
<intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="itheamc" android:host="paypal" />
</intent-filter>

2. Get PayPal Credentials

  1. Go to PayPal Developer Dashboard
  2. Create a new app or select an existing one
  3. Copy your Client ID and Secret
  4. Use Sandbox credentials for testing, Live credentials for production

3. Create .env File

Create a .env file in the root of your Flutter project:

PAYPAL_CLIENT_ID=your_client_id_here
PAYPAL_CLIENT_SECRET=your_client_secret_here

Example:

PAYPAL_CLIENT_ID=AX9HZkDVbFQDVyOQBO_t8oLscdNk9YTeXQ8lnKBoE0ns7Z_YTH70qPR0njSHge2QjqcNS3IkP3kxPB0b
PAYPAL_CLIENT_SECRET=EEpm782H_lewazmVlIlotj-3W8YNeEYXKFAmHYJk-4M9bsE1uC5Ptv8K6862Oc0MowwknHXg4_kb4bQX

Important: Add .env to your .gitignore file to keep your credentials secure:

# .gitignore
.env

4. Add .env to Assets

Update your pubspec.yaml to include the .env file as an asset:

flutter:
  assets:
    - .env

5. Initialize PayPal

Initialize the PayPal plugin in your app's main function or before using any PayPal APIs:

import 'package:paypal_payment_flutter/paypal_payment_flutter.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Initialize PayPal
  // By default it will be initialize with sandbox environment
  await PaypalPayment.instance.initialize();
  
  // For production, use:
  // await PaypalPayment.instance.initialize(Environment.production);
  
  runApp(MyApp());
}

The initialize() method automatically:

  • Loads environment variables from the .env file
  • Retrieves PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET
  • Configures the HTTP service with your credentials
  • Initializes the native PayPal SDK on Android and iOS

Note: The plugin uses a custom environment variable loader that reads from the .env file. No additional packages like flutter_dotenv are required.

API Usage

Orders API

The Orders API allows you to create and manage orders for checkout flows.

Create an Order

import 'package:paypal_payment_flutter/paypal_payment_flutter.dart';

Future<void> createOrder() async {
  
  await PaypalPayment.instance.orders.createOrder(
    intent: OrderIntent.capture,
    purchaseUnits: [
      PurchaseUnit(
        referenceId: 'default',
        amount: Amount(
          currencyCode: 'USD',
          value: '100.00',
        ),
      ),
    ],
    onInitiated: () {
      print('Order creation Initiated');
    },
    onSuccess: (response) {
      print('Order created: ${response.id}');
      print('Status: ${response.status}');
      // Extract approval URL for manual checkout
      final approvalUrl = response.links
          ?.firstWhere((link) => link.rel == 'approve')
          .href;
      print('Approval URL: $approvalUrl');
    },
    onError: (error) {
      print('Error creating order: $error');
    },
  );
  
}

Capture an Order

Future<void> captureOrder(String orderId) async {
  
  await PaypalPayment.instance.orders.captureOrder(
    orderId: orderId,
    onInitiated: () {
      print('Capture Initiated');
    },
    onSuccess: (response) {
      print('Order captured: ${response.id}');
      print('Status: ${response.status}');
    },
    onError: (error) {
      print('Error capturing order: $error');
    },
  );
  
}

Authorize an Order

Future<void> authorizeOrder(String orderId) async {
  
  await PaypalPayment.instance.orders.authorizeOrder(
    orderId: orderId,
    onInitiated: () {
      print('Authorization Initiated');
    },
    onSuccess: (response) {
      print('Order authorized: ${response.id}');
    },
    onError: (error) {
      print('Error authorizing order: $error');
    },
  );
  
}

Payments API V1

The Payments API V1 provides comprehensive payment management capabilities.

Create a Payment

Future<void> createPayment() async {
  
  await PaypalPayment.instance.payments.v1.createPayment(
    intent: PaymentIntent.sale,
    payer: Payer(paymentMethod: "paypal"),
    transactions: [
      Transaction(
        amount: TransactionAmount(
          total: "10.00",
          currency: "USD",
        ),
        description: "Payment for services",
      ),
    ],
    redirectUrls: RedirectUrls(
      returnUrl: "https://yourapp.com/return",
      cancelUrl: "https://yourapp.com/cancel",
    ),
    onInitiated: () {
      print('Create Payment Initiated');
    },
    onSuccess: (response) {
      print('Payment created: ${response.id}');
      // Get approval URL
      final approvalUrl = response.links
          ?.firstWhere((link) => link.rel == 'approval_url')
          .href;
      print('Approval URL: $approvalUrl');
    },
    onError: (error) {
      print('Error: $error');
    },
  );
  
}

Execute a Payment

After user approves the payment, execute it:

Future<void> executePayment(String paymentId, String payerId) async {
  
  await PaypalPayment.instance.payments.v1.executePayment(
    paymentId: paymentId,
    payerId: payerId,
    onInitiated: () {
      print('Executed Initiated');
    },
    onSuccess: (response) {
      print('Payment executed: ${response.id}');
      print('State: ${response.state}');
    },
    onError: (error) {
      print('Error: $error');
    },
  );
  
}

Capture Authorized Payment

Future<void> captureAuthorizedPayment(String authorizationId) async {
  
  await PaypalPayment.instance.payments.v1.captureAuthorizedPayment(
    authorizationId: authorizationId,
    amount: TransactionAmount(
      total: "10.00",
      currency: "USD",
    ),
    onInitiated: () {
      print('Capture initiated');
    },
    onSuccess: (response) {
      print('Payment captured: ${response.id}');
    },
    onError: (error) {
      print('Error: $error');
    },
  );
  
}

Void Authorized Payment

Future<void> voidAuthorizedPayment(String authorizationId) async {
  
  await PaypalPayment.instance.payments.v1.voidAuthorizedPayment(
    authorizationId: authorizationId,
    onInitiated: () {
      print('Void Initiated');
    },
    onSuccess: (response) {
      print('Authorization voided: ${response.id}');
    },
    onError: (error) {
      print('Error: $error');
    },
  );
  
}

Refund a Payment

Future<void> refundCapturedPayment(String captureId) async {
  
  await PaypalPayment.instance.payments.v1.refundCapturedPayment(
    captureId: captureId,
    amount: TransactionAmount(
      total: "5.00",
      currency: "USD",
    ),
    onInitiated: () {
      print('Refund Initiated');
    },
    onSuccess: (response) {
      print('Refund processed: ${response.id}');
    },
    onError: (error) {
      print('Error: $error');
    },
  );
  
}

Get Payment Details

Future<void> getPaymentDetails(String paymentId) async {
  
  await PaypalPayment.instance.payments.v1.getPaymentDetails(
    paymentId: paymentId,
    onInitiated: () {
      print('Payment Details Request Initiated');
    },
    onSuccess: (response) {
      print('Payment ID: ${response.id}');
      print('State: ${response.state}');
    },
    onError: (error) {
      print('Error: $error');
    },
  );
  
}

Payments API V2

The Payments API V2 provides modern payment authorization and capture flows.

Capture Authorized Payment (V2)

Future<void> captureAuthorizedPaymentV2(String authorizationId) async {
  
  await PaypalPayment.instance.payments.v2.captureAuthorizedPayment(
    authorizationId: authorizationId,
    amount: Amount(
      currencyCode: "USD",
      value: "10.00",
    ),
    onInitiated: () {
      print('Capture Authorization Initiated');
    },
    onSuccess: (response) {
      print('Payment captured: ${response.id}');
      print('Status: ${response.status}');
    },
    onError: (error) {
      print('Error: $error');
    },
  );
  
}

Void Authorized Payment (V2)

Future<void> voidAuthorizedPaymentV2(String authorizationId) async {
  
  await PaypalPayment.instance.payments.v2.voidAuthorizedPayment(
    authorizationId: authorizationId,
    onInitiated: () {
      print('Void Authorization Initiated');
    },
    onSuccess: (response) {
      print('Authorization voided: ${response.id}');
    },
    onError: (error) {
      print('Error: $error');
    },
  );
  
}

Refund Captured Payment (V2)

Future<void> refundCapturedPaymentV2(String captureId) async {
  
  await PaypalPayment.instance.payments.v2.refundCapturedPayment(
    captureId: captureId,
    amount: Amount(
      currencyCode: "USD",
      value: "5.00",
    ),
    onInitiated: () {
      print('Refund Capture Payment Initiated');
    },
    onSuccess: (response) {
      print('Refund processed: ${response.id}');
    },
    onError: (error) {
      print('Error: $error');
    },
  );
  
}

Transactions API

List and search transaction history.

Future<void> listTransactions() async {
  
  final startDate = DateTime.now().subtract(Duration(days: 30));
  final endDate = DateTime.now();

  await PaypalPayment.instance.transactions.listTransactions(
    startDate: startDate,
    endDate: endDate,
    onInitiated: () {
      print('Loading Initiated');
    },
    onSuccess: (response) {
      print('Total transactions: ${response.transactionDetails?.length}');
      response.transactionDetails?.forEach((transaction) {
        print('Transaction ID: ${transaction.transactionInfo?.transactionId}');
        print('Amount: ${transaction.transactionInfo?.transactionAmount?.value}');
      });
    },
    onError: (error) {
      print('Error: $error');
    },
  );
  
}

Card Payment API

The Card Payment API allows you to accept credit and debit card payments directly. It supports 3D Secure authentication and handles the complete payment flow.

⚠️ Security Notice Card details are highly sensitive. Never log, store, or cache card information. Always handle card data according to PCI-DSS guidelines.

Card Payment Models

CardDetail: Contains the card information required for payment.

import 'package:paypal_payment_flutter/paypal_payment_flutter.dart';

final cardDetail = CardDetail(
  number: '4111111111111111',           // Card number (13-19 digits)
  expirationMonth: '12',                // Expiration month (MM format)
  expirationYear: '2025',               // Expiration year (YYYY format)
  securityCode: '123',                  // CVV/CVC (3-4 digits)
  cardholderName: 'John Doe',           // Name on card
  billingAddress: BillingAddress(
    countryCode: 'US',                  // Required: ISO 3166-1 alpha-2 code
    streetAddress: '123 Main Street',   // Optional: Street address
    extendedAddress: 'Apt 4B',          // Optional: Apartment/Suite
    locality: 'San Francisco',          // Optional: City
    region: 'CA',                       // Optional: State/Province
    postalCode: '94105',                // Optional: ZIP/Postal code
  ),
);

SCA (Strong Customer Authentication): Controls when 3D Secure authentication is performed.

enum SCA {
  whenRequired,  // Default: Only when mandated by regulation
  always,        // Always perform 3D Secure
}

Checkout with Card Payment

The simplest way to accept card payments is using the checkoutOrderWithCard method, which handles the entire flow:

Future<void> checkoutWithCard() async {
  // Define card details
  final cardDetail = CardDetail(
    number: '4111111111111111',
    expirationMonth: '12',
    expirationYear: '2025',
    securityCode: '123',
    cardholderName: 'John Doe',
    billingAddress: BillingAddress(
      countryCode: 'US',
      streetAddress: '123 Main Street',
      locality: 'San Francisco',
      region: 'CA',
      postalCode: '94105',
    ),
  );

  // Process card payment
  await PaypalPayment.instance.checkout.checkoutOrderWithCard(
    intent: OrderIntent.capture,
    purchaseUnits: [
      PurchaseUnit(
        referenceId: 'default',
        amount: Amount(
          currencyCode: 'USD',
          value: '100.00',
        ),
      ),
    ],
    cardDetail: cardDetail,
    sca: SCA.whenRequired,  // Optional: defaults to whenRequired
    onInitiated: () {
      print('Card payment initiated');
    },
    onSuccess: (orderId) {
      print('Payment successful! Order ID: $orderId');
      // Order is automatically captured
    },
    onError: (error) {
      print('Payment failed: $error');
    },
  );
}

Manual Card Payment Flow

For more control, you can manually handle each step:

import 'package:paypal_payment_flutter/paypal_payment_flutter.dart';

class ManualCardPayment extends StatefulWidget {
  @override
  _ManualCardPaymentState createState() => _ManualCardPaymentState();
}

class _ManualCardPaymentState extends State<ManualCardPayment> {
  String? orderId;

  // Step 1: Create the order
  Future<void> createOrder() async {
    await PaypalPayment.instance.orders.createOrder(
      intent: OrderIntent.capture,
      purchaseUnits: [
        PurchaseUnit(
          referenceId: 'default',
          amount: Amount(
            currencyCode: 'USD',
            value: '100.00',
          ),
        ),
      ],
      onSuccess: (response) {
        setState(() {
          orderId = response.id;
        });
        
        // Step 2: Initiate card payment
        if (orderId != null) {
          initiateCardPayment(orderId!);
        }
      },
      onError: (error) {
        print('Order creation failed: $error');
      },
    );
  }

  // Step 2: Initiate card payment request
  Future<void> initiateCardPayment(String orderId) async {
    final cardDetail = CardDetail(
      number: '4111111111111111',
      expirationMonth: '12',
      expirationYear: '2025',
      securityCode: '123',
      cardholderName: 'John Doe',
      billingAddress: BillingAddress(countryCode: 'US'),
    );

    await PaypalPayment.instance.orders.initiateCardPaymentRequest(
      orderId: orderId,
      cardDetail: cardDetail,
      sca: SCA.whenRequired,
      onSuccess: (orderId, status, didAttemptThreeDSecure) {
        print('Card payment approved');
        print('3D Secure attempted: $didAttemptThreeDSecure');
        
        // Step 3: Capture the order
        if (orderId != null) {
          captureOrder(orderId);
        }
      },
      onFailure: (orderId, reason, code, correlationId) {
        print('Payment failed: $reason (Code: $code)');
        print('Correlation ID: $correlationId');
      },
      onCancel: (orderId) {
        print('Payment canceled by user');
      },
      onError: (error) {
        print('Error: $error');
      },
    );
  }

  // Step 3: Capture the order
  Future<void> captureOrder(String orderId) async {
    await PaypalPayment.instance.orders.captureOrder(
      orderId: orderId,
      onSuccess: (response) {
        print('Order captured successfully!');
        print('Capture ID: ${response.id}');
        print('Status: ${response.status}');
      },
      onError: (error) {
        print('Capture failed: $error');
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Card Payment')),
      body: Center(
        child: ElevatedButton(
          onPressed: createOrder,
          child: Text('Pay with Card'),
        ),
      ),
    );
  }
}

Test Card Numbers

Use these test cards in the PayPal Sandbox environment:

Card Type Number CVV Expiry
Visa 4111111111111111 123 Any future
Visa 4032039594896467 123 Any future
MasterCard 5555555555554444 123 Any future
MasterCard 2223000048410010 123 Any future
Amex 378282246310005 1234 Any future
Discover 6011111111111117 123 Any future

Note: For production, use real card details. Test cards only work in Sandbox mode.

Checkout API

The Checkout API provides simplified payment flows with built-in WebView integration.

Checkout with Order

Future<void> checkoutWithOrder(BuildContext context) async {
  
  await PaypalPayment.instance.checkout.checkoutOrder(
    intent: OrderIntent.capture,
    purchaseUnits: [
      PurchaseUnit(
        referenceId: 'default',
        amount: Amount(
          currencyCode: 'USD',
          value: '100.00',
        ),
      ),
    ],
    onInitiated: () {
      print('Order Checkout Process Initiated');
    },
    onSuccess: (orderId) {
      print('Order completed: $orderId');
      // Order is automatically captured
    },
    onError: (error) {
      print('Checkout error: $error');
    },
  );
  
}

Checkout with Payment (V1)

Future<void> checkoutWithPayment(BuildContext context) async {
  
  await PaypalPayment.instance.checkout.checkoutPayment(
    context,
    intent: PaymentIntent.sale,
    payer: Payer(paymentMethod: "paypal"),
    transactions: [
      Transaction(
        amount: TransactionAmount(
          total: "10.00",
          currency: "USD",
        ),
        description: "Payment for services",
      ),
    ],
    redirectUrls: RedirectUrls(
      returnUrl: "https://yourapp.com/return",
      cancelUrl: "https://yourapp.com/cancel",
    ),
    onInitiated: () {
      print('Payment Checkout Process Initiated');
    },
    onSuccess: (paymentId) {
      print('Payment completed: $paymentId');
      // Payment is automatically executed
    },
    onError: (error) {
      print('Checkout error: $error');
    },
  );
  
}

Manual Checkout Flow (Without Checkout API)

If you prefer to build a custom checkout experience without using the Checkout API, you can manually handle the flow:

Option 1: Manual Order Flow

import 'package:url_launcher/url_launcher.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:paypal_payment_flutter/paypal_payment_flutter.dart';

class ManualOrderCheckout extends StatefulWidget {
  @override
  _ManualOrderCheckoutState createState() => _ManualOrderCheckoutState();
}

class _ManualOrderCheckoutState extends State<ManualOrderCheckout> {
  String? orderId;
  String? approvalUrl;

  // Step 1: Create the order
  Future<void> createOrder() async {
    await PaypalPayment.instance.orders.createOrder(
      intent: OrderIntent.capture,
      purchaseUnits: [
        PurchaseUnit(
          referenceId: 'default',
          amount: Amount(
            currencyCode: 'USD',
            value: '100.00',
          ),
        ),
      ],
      onSuccess: (response) {
        setState(() {
          orderId = response.id;
          // Extract approval URL
          approvalUrl = response.links
              ?.firstWhere((link) => link.rel == 'approve')
              .href;
        });
        
        // Step 2: Open approval URL in your custom WebView or browser
        if (approvalUrl != null) {
          openApprovalUrl(approvalUrl!);
        }
      },
      onError: (error) {
        print('Error: $error');
      },
    );
  }

  // Step 2: Open approval URL
  void openApprovalUrl(String url) {
    // Option A: Use WebView
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => CustomWebView(
          url: url,
          onApproved: () {
            // Step 3: Capture the order after approval
            captureOrder();
          },
        ),
      ),
    );

    // Option B: Use external browser
    // launchUrl(Uri.parse(url));
  }

  // Step 3: Capture the order
  Future<void> captureOrder() async {
    if (orderId == null) return;

    await PaypalPayment.instance.orders.captureOrder(
      orderId: orderId!,
      onSuccess: (response) {
        print('Order captured successfully!');
        print('Capture ID: ${response.id}');
        // Handle success
      },
      onError: (error) {
        print('Capture error: $error');
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Manual Checkout')),
      body: Center(
        child: ElevatedButton(
          onPressed: createOrder,
          child: Text('Pay with PayPal'),
        ),
      ),
    );
  }
}

// Custom WebView widget
class CustomWebView extends StatelessWidget {
  final String url;
  final VoidCallback onApproved;

  const CustomWebView({
    required this.url,
    required this.onApproved,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('PayPal Checkout')),
      body: WebViewWidget(
        controller: WebViewController()
          ..setJavaScriptMode(JavaScriptMode.unrestricted)
          ..setNavigationDelegate(
            NavigationDelegate(
              onUrlChange: (change) {
                // Monitor URL changes to detect approval
                if (change.url?.contains('yourapp.com/return') ?? false) {
                  Navigator.pop(context);
                  onApproved();
                }
              },
            ),
          )
          ..loadRequest(Uri.parse(url)),
      ),
    );
  }
}

Option 2: Manual Payment Flow (V1)

class ManualPaymentCheckout extends StatefulWidget {
  @override
  _ManualPaymentCheckoutState createState() => _ManualPaymentCheckoutState();
}

class _ManualPaymentCheckoutState extends State<ManualPaymentCheckout> {
  String? paymentId;
  String? approvalUrl;

  // Step 1: Create payment
  Future<void> createPayment() async {
    await PaypalPayment.instance.payments.v1.createPayment(
      intent: PaymentIntent.sale,
      payer: Payer(paymentMethod: "paypal"),
      transactions: [
        Transaction(
          amount: TransactionAmount(
            total: "10.00",
            currency: "USD",
          ),
        ),
      ],
      redirectUrls: RedirectUrls(
        returnUrl: "https://yourapp.com/return",
        cancelUrl: "https://yourapp.com/cancel",
      ),
      onSuccess: (response) {
        setState(() {
          paymentId = response.id;
          approvalUrl = response.links
              ?.firstWhere((link) => link.rel == 'approval_url')
              .href;
        });

        if (approvalUrl != null) {
          openApprovalUrl(approvalUrl!);
        }
      },
      onError: (error) {
        print('Error: $error');
      },
    );
  }

  // Step 2: Open approval URL (similar to above)
  void openApprovalUrl(String url) {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => CustomWebView(
          url: url,
          onApproved: (Uri returnUri) {
            // Extract PayerID from return URL
            final payerId = returnUri.queryParameters['PayerID'];
            if (payerId != null) {
              executePayment(payerId);
            }
          },
        ),
      ),
    );
  }

  // Step 3: Execute payment
  Future<void> executePayment(String payerId) async {
    if (paymentId == null) return;

    await PaypalPayment.instance.payments.v1.executePayment(
      paymentId: paymentId!,
      payerId: payerId,
      onSuccess: (response) {
        print('Payment executed successfully!');
        // Handle success
      },
      onError: (error) {
        print('Execution error: $error');
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Manual Payment')),
      body: Center(
        child: ElevatedButton(
          onPressed: createPayment,
          child: Text('Pay with PayPal'),
        ),
      ),
    );
  }
}

Complete Example

See the example directory for a complete working application demonstrating all APIs.

Platform Support

Platform Support
Android
iOS
Web
macOS
Windows
Linux

Requirements

  • Flutter SDK: >=3.3.0
  • Dart SDK: ^3.10.0
  • Android: minSdkVersion 21
  • iOS: 14.0+ (Required by the latest Paypal iOS SDK)

Testing

The plugin includes comprehensive unit and widget tests. Run tests with:

flutter test

Additional Resources

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Support

For issues, questions, or contributions, please visit the GitHub repository.