paypal_checkout_flutter
A complete Flutter package for PayPal payments using the native PayPal Mobile SDK (Android v2.3.0 / iOS v2.0.1). Type-safe Dart ↔ Kotlin/Swift communication via Pigeon.
No WebView. Opens the system browser or processes cards directly with the native SDK.
Features
Native SDK (Pigeon)
| Feature | Method | Backend required |
|---|---|---|
| PayPal Checkout | pay() |
Yes |
| PayPal Checkout (no backend) | payDirect() |
No |
| Pay Later (financing) | pay() + payLater |
Yes |
| Card payment | payWithCard() |
Yes |
| Card payment (no backend) | payWithCardDirect() |
No |
| Vault PayPal account | vaultPaypal() |
Yes |
| Vault card | vaultCard() |
Yes |
| Vault PayPal (no backend) | vaultPaypalDirect() |
No |
| Vault card (no backend) | vaultCardDirect() |
No |
REST API — Orders & Payments
| Endpoint | Method |
|---|---|
| Create order | createOrder()* |
| Get order details | getOrderDetails() |
| Update order (PATCH) | updateOrder() |
| Capture order | captureOrder()* |
| Authorize order | authorizeOrder() |
| Capture authorization | captureAuthorization() |
| Void authorization | voidAuthorization() |
| Refund capture | refund() |
REST API — Catalog Products (4/4 endpoints)
| Endpoint | Method |
|---|---|
| Create product | createProduct() |
| List products | listProducts() |
| Show product details | getProductDetails() |
| Update product | updateProduct() |
REST API — Billing Plans (7/7 endpoints)
| Endpoint | Method |
|---|---|
| Create plan | createPlan() |
| List plans | listPlans() |
| Show plan details | getPlanDetails() |
| Update plan | updatePlan()** |
| Activate plan | activatePlan()** |
| Deactivate plan | deactivatePlan()** |
| Update pricing schemes | updatePlanPricing() |
REST API — Subscriptions (10/10 endpoints)
| Endpoint | Method |
|---|---|
| Create subscription | createSubscription() |
| Show subscription details | getSubscriptionDetails() |
| List subscriptions | listSubscriptions() |
| Update subscription | updateSubscription() |
| Revise subscription | reviseSubscription() |
| Activate subscription | activateSubscription() |
| Suspend subscription | suspendSubscription() |
| Cancel subscription | cancelSubscription() |
| Capture payment | captureSubscriptionPayment() |
| List transactions | listSubscriptionTransactions() |
* Available via PaypalOrderService and internally used by payDirect()/payWithCardDirect().
** Available via PaypalSubscriptionService directly.
- Full 3D Secure support for card payments
- Clean architecture: entities, repositories, mappers
Either<Failure, Success>with dartz for error handling- 177 unit tests with full coverage (257 as of v0.2.0)
Requirements
- Android:
minSdk 23,compileSdk 34, Java 17 - iOS: iOS 16.0+
- Flutter:
>=1.17.0 - A PayPal app (developer.paypal.com)
Installation
dependencies:
paypal_checkout_flutter: ^0.2.0
Android Setup
Add the deep link intent filter in your AndroidManifest.xml:
<activity
android:name=".MainActivity"
android:launchMode="singleTop">
<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="com.example.myapp" android:host="paypalpay" />
</intent-filter>
</activity>
Quick Start
Initialize (once at app startup)
import 'package:paypal_checkout_flutter/paypal_checkout_flutter.dart';
final paypal = FlutterPaypalPayment();
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await paypal.init(PaypalConfig(
clientId: 'YOUR_CLIENT_ID',
environment: PaypalEnvironment.sandbox,
returnUrl: 'com.example.myapp://paypalpay',
));
runApp(MyApp());
}
Usage Examples
1. PayPal Checkout (with backend)
Your server creates the order via PayPal Orders API and returns the orderId.
final result = await paypal.pay(
PaymentRequest(orderId: 'ORDER_ID_FROM_BACKEND'),
);
result.fold(
(failure) => print('Error: ${failure.message}'),
(success) => print('Paid! Order: ${success.orderId}'),
);
2. PayPal Checkout (no backend)
Creates the order, opens checkout, and captures — all from Flutter.
Note: Requires your
clientSecret. Not recommended for production.
final result = await paypal.payDirect(
clientSecret: 'YOUR_SECRET',
params: PaymentParams(
amount: '25.00',
currencyCode: 'USD',
description: 'Product X purchase',
),
);
3. Card Payment
Charge a card directly without PayPal login. Supports 3D Secure automatically.
final result = await paypal.payWithCard(
CardPaymentRequest(
orderId: 'ORDER_ID',
card: PaymentCard(
number: '4111111111111111',
expirationMonth: '12',
expirationYear: '2028',
securityCode: '123',
),
),
);
3b. PaypalCardForm Widget
Drop-in PayPal-styled card form. Shows animated 3D card preview, auto-detects card network (Visa, Mastercard, Amex, Discover), and validates all fields client-side before calling onSubmit.
import 'package:paypal_checkout_flutter/paypal_checkout_flutter.dart';
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(ctx).viewInsets.bottom,
),
child: SingleChildScrollView(
child: PaypalCardForm(
amount: '35.20',
currency: 'USD',
submitButtonText: 'Pay \$35.20',
requireCardholderName: false, // optional name field
requireBillingPostalCode: false, // optional ZIP field
onSubmit: (card) async {
Navigator.of(ctx).pop();
final result = await paypal.payWithCard(
CardPaymentRequest(orderId: 'ORDER_ID', card: card),
);
result.fold(
(err) => showError(err.message),
(ok) => showSuccess(ok.orderId),
);
},
onError: (message) => showError(message),
),
),
),
);
4. Pay Later (Financing)
final result = await paypal.pay(
PaymentRequest(
orderId: 'ORDER_ID',
fundingSource: PaypalFundingSource.payLater,
),
);
5. Vault: Save PayPal Account
final result = await paypal.vaultPaypal(
VaultPaypalRequest(setupTokenId: 'SETUP_TOKEN_FROM_BACKEND'),
);
6. Vault: Save Card
final result = await paypal.vaultCard(
VaultCardRequest(
setupTokenId: 'SETUP_TOKEN_FROM_BACKEND',
card: PaymentCard(
number: '4111111111111111',
expirationMonth: '12',
expirationYear: '2028',
securityCode: '123',
),
),
);
7. Refund (Total or Partial)
// Full refund
final result = await paypal.refund(
clientSecret: 'YOUR_SECRET',
captureId: 'CAPTURE_ID',
);
// Partial refund ($5.00)
final partial = await paypal.refund(
clientSecret: 'YOUR_SECRET',
captureId: 'CAPTURE_ID',
amount: '5.00',
currencyCode: 'USD',
);
8. Order Authorization Flow
// Authorize (hold funds)
final auth = await paypal.authorizeOrder(
clientSecret: 'YOUR_SECRET',
orderId: 'ORDER_ID',
);
// Capture later
final capture = await paypal.captureAuthorization(
clientSecret: 'YOUR_SECRET',
authorizationId: 'AUTH_ID',
);
// Or void
final voided = await paypal.voidAuthorization(
clientSecret: 'YOUR_SECRET',
authorizationId: 'AUTH_ID',
);
9. Update Order (Shipping/Tracking)
final result = await paypal.updateOrder(
clientSecret: 'YOUR_SECRET',
orderId: 'ORDER_ID',
patchOperations: [
{
'op': 'add',
'path': '/purchase_units/@reference_id==\'default\'/shipping/trackers',
'value': [
{
'carrier': 'FEDEX',
'tracking_number': '1234567890',
'status': 'SHIPPED',
}
],
}
],
);
Subscriptions API
10. Create a Product
final result = await paypal.createProduct(
clientSecret: 'YOUR_SECRET',
product: {
'name': 'Premium Plan',
'description': 'Access to all features',
'type': 'SERVICE',
'category': 'SOFTWARE',
},
);
result.fold(
(failure) => print('Error: ${failure.message}'),
(product) => print('Product created: ${product['id']}'),
);
11. List Products
final result = await paypal.listProducts(
clientSecret: 'YOUR_SECRET',
pageSize: 10,
page: 1,
totalRequired: true,
);
result.fold(
(failure) => print('Error: ${failure.message}'),
(data) {
final products = data['products'] as List;
print('Total: ${data['total_items']}, Found: ${products.length}');
},
);
12. Get Product Details
final result = await paypal.getProductDetails(
clientSecret: 'YOUR_SECRET',
productId: 'PROD-XXXX',
);
13. Update Product
final result = await paypal.updateProduct(
clientSecret: 'YOUR_SECRET',
productId: 'PROD-XXXX',
patchOperations: [
{'op': 'replace', 'path': '/description', 'value': 'New description'},
],
);
14. Create a Billing Plan
final result = await paypal.createPlan(
clientSecret: 'YOUR_SECRET',
plan: {
'product_id': 'PROD-XXXX',
'name': 'Monthly Plan',
'billing_cycles': [
{
'frequency': {'interval_unit': 'MONTH', 'interval_count': 1},
'tenure_type': 'REGULAR',
'sequence': 1,
'total_cycles': 0,
'pricing_scheme': {
'fixed_price': {'value': '9.99', 'currency_code': 'USD'},
},
}
],
'payment_preferences': {
'auto_bill_outstanding': true,
'payment_failure_threshold': 3,
},
},
);
15. List Plans
final result = await paypal.listPlans(
clientSecret: 'YOUR_SECRET',
productId: 'PROD-XXXX', // optional filter
pageSize: 10,
);
16. Update Plan Pricing
final result = await paypal.updatePlanPricing(
clientSecret: 'YOUR_SECRET',
planId: 'P-XXXX',
pricingSchemes: [
{
'billing_cycle_sequence': 1,
'pricing_scheme': {
'fixed_price': {'value': '14.99', 'currency_code': 'USD'},
},
}
],
);
17. Create a Subscription
final result = await paypal.createSubscription(
clientSecret: 'YOUR_SECRET',
subscription: {
'plan_id': 'P-XXXX',
'subscriber': {
'name': {'given_name': 'John', 'surname': 'Doe'},
'email_address': 'john@example.com',
},
'application_context': {
'return_url': 'https://example.com/return',
'cancel_url': 'https://example.com/cancel',
},
},
);
18. List Subscriptions
final result = await paypal.listSubscriptions(
clientSecret: 'YOUR_SECRET',
planIds: 'P-XXXX',
statuses: 'ACTIVE',
pageSize: 20,
);
19. Manage Subscription Lifecycle
// Activate
await paypal.activateSubscription(
clientSecret: 'YOUR_SECRET',
subscriptionId: 'I-XXXX',
reason: 'Reactivating after pause',
);
// Suspend
await paypal.suspendSubscription(
clientSecret: 'YOUR_SECRET',
subscriptionId: 'I-XXXX',
reason: 'Customer requested pause',
);
// Cancel
await paypal.cancelSubscription(
clientSecret: 'YOUR_SECRET',
subscriptionId: 'I-XXXX',
reason: 'Customer requested cancellation',
);
// Revise (change plan)
final revised = await paypal.reviseSubscription(
clientSecret: 'YOUR_SECRET',
subscriptionId: 'I-XXXX',
revisionDetails: {'plan_id': 'P-NEW-PLAN'},
);
20. Capture Outstanding Payment
final result = await paypal.captureSubscriptionPayment(
clientSecret: 'YOUR_SECRET',
subscriptionId: 'I-XXXX',
captureRequest: {
'note': 'Charging outstanding balance',
'capture_type': 'OUTSTANDING_BALANCE',
'amount': {'currency_code': 'USD', 'value': '10.00'},
},
);
21. List Subscription Transactions
final result = await paypal.listSubscriptionTransactions(
clientSecret: 'YOUR_SECRET',
subscriptionId: 'I-XXXX',
startTime: '2026-01-01T00:00:00Z',
endTime: '2026-04-18T23:59:59Z',
);
result.fold(
(failure) => print('Error: ${failure.message}'),
(data) {
final txns = data['transactions'] as List;
for (final txn in txns) {
print('${txn['id']}: ${txn['status']} — ${txn['amount_with_breakdown']}');
}
},
);
Using the Service Directly
For advanced usage, you can use PaypalSubscriptionService or PaypalOrderService directly:
final service = PaypalSubscriptionService(
config: PaypalConfig(
clientId: 'YOUR_CLIENT_ID',
environment: PaypalEnvironment.sandbox,
returnUrl: 'com.example.myapp://paypalpay',
),
clientSecret: 'YOUR_SECRET',
);
try {
// Plan lifecycle methods only available via service
await service.updatePlan('P-XXXX', patchOperations: [...]);
await service.activatePlan('P-XXXX');
await service.deactivatePlan('P-XXXX');
} finally {
service.dispose();
}
Dependency Injection
GetIt
final getIt = GetIt.instance;
Future<void> configureDependencies() async {
final paypal = FlutterPaypalPayment();
await paypal.init(PaypalConfig(
clientId: 'YOUR_CLIENT_ID',
environment: PaypalEnvironment.sandbox,
returnUrl: 'com.example.myapp://paypalpay',
));
getIt.registerSingleton<FlutterPaypalPayment>(paypal);
}
Riverpod
final paypalProvider = Provider<FlutterPaypalPayment>((ref) {
throw UnimplementedError('Initialized in main');
});
// In main:
runApp(
ProviderScope(
overrides: [paypalProvider.overrideWithValue(paypal)],
child: MyApp(),
),
);
Architecture
lib/
├── paypal_checkout_flutter.dart # Public exports
└── src/
├── flutter_paypal_payment_plugin.dart # Public API (FlutterPaypalPayment)
├── domain/
│ ├── entities/ # PaypalConfig, PaymentRequest, PaymentCard, etc.
│ └── repositories/ # Abstract contracts
├── data/
│ ├── repositories/ # Implementation delegating to Pigeon
│ ├── mappers/ # Dart ↔ Pigeon message mappers
│ └── services/ # PaypalOrderService, PaypalSubscriptionService
└── generated/ # Auto-generated Pigeon code
├── analytics/
│ └── paypal_subscription_analytics.dart # MRR/ARR/ARPU/Churn
├── events/
│ ├── paypal_event_bus.dart # Reactive streams
│ └── paypal_events.dart # Typed event classes
├── logger/
│ └── paypal_logger.dart # Structured logging
└── webhooks/
├── paypal_webhook_event.dart # Typed webhook models
└── paypal_webhook_helper.dart # Verify & parse webhooks
android/src/main/kotlin/
└── FlutterPaypalPaymentPlugin.kt # Native implementation (PayPal Android SDK)
ios/Classes/
└── PaypalCheckoutFlutterPlugin.swift # Native implementation (PayPal iOS SDK)
Error Handling
All methods return Either<Failure, Success>. Use .fold() to handle both cases:
result.fold(
(failure) {
// failure.code — e.g. 'NOT_INITIALIZED', 'CAPTURE_ERROR'
// failure.message — human-readable description
print('${failure.code}: ${failure.message}');
},
(success) {
// Handle success
},
);
Error Code Reference
| Code | When it occurs |
|---|---|
NOT_INITIALIZED |
init() was not called before pay() or payWithCard() |
AUTH_ERROR |
OAuth2 token request failed (bad credentials or network) |
CREATE_ORDER_ERROR |
Order creation failed |
CAPTURE_ERROR |
Order capture failed after buyer approval |
GET_ORDER_ERROR |
getOrderDetails() failed |
UPDATE_ORDER_ERROR |
updateOrder() PATCH failed |
AUTHORIZE_ERROR |
authorizeOrder() failed (AUTHORIZE intent) |
CAPTURE_AUTHORIZATION_ERROR |
captureAuthorization() failed |
VOID_AUTHORIZATION_ERROR |
voidAuthorization() failed |
REFUND_ERROR |
refundCapture() failed |
SETUP_TOKEN_ERROR |
createSetupToken() failed (vaulting flow) |
PAYMENT_TOKEN_ERROR |
createPaymentToken() failed (vaulting flow) |
CREATE_PRODUCT_ERROR |
createProduct() failed (subscriptions) |
CREATE_PLAN_ERROR |
createPlan() failed |
GET_PLAN_ERROR |
getPlan() failed |
UPDATE_PLAN_ERROR |
updatePlan() failed |
LIST_PLANS_ERROR |
listPlans() failed |
UPDATE_PRICING_ERROR |
updatePlanPricing() failed |
CREATE_SUBSCRIPTION_ERROR |
createSubscription() failed |
GET_SUBSCRIPTION_ERROR |
getSubscription() failed |
SUBSCRIPTION_ACTION_ERROR |
suspendSubscription() / cancelSubscription() / activateSubscription() failed |
CAPTURE_SUBSCRIPTION_ERROR |
captureSubscriptionPayment() failed |
UPDATE_SUBSCRIPTION_ERROR |
updateSubscription() failed |
LIST_SUBSCRIPTIONS_ERROR |
listSubscriptions() failed |
LIST_TRANSACTIONS_ERROR |
listSubscriptionTransactions() failed |
LIST_PRODUCTS_ERROR |
listProducts() failed |
GET_PRODUCT_ERROR |
getProduct() failed |
UPDATE_PRODUCT_ERROR |
updateProduct() failed |
VALIDATION_ERROR |
Invalid input (e.g. malformed order ID) |
UNKNOWN_ERROR |
Unexpected error not covered above |
v0.2.0 — New Features
Event Stream Architecture
Every payment lifecycle event is published on a strongly-typed broadcast stream. Subscribe in any widget without changing your business logic:
final paypal = FlutterPaypalPayment();
@override
void initState() {
super.initState();
// Listen to checkout completions
paypal.events.checkoutCompleted.listen((event) {
print('Paid! orderId=${event.result.orderId}');
});
// Listen to failures
paypal.events.checkoutFailed.listen((event) {
print('Failed: ${event.failure.message}');
});
}
@override
void dispose() {
paypal.dispose(); // closes all stream controllers
super.dispose();
}
Available streams on paypal.events:
| Stream | Emits when… |
|---|---|
checkoutStarted |
pay() begins |
checkoutCompleted |
pay() succeeds |
checkoutCancelled |
Buyer cancels in browser |
checkoutFailed |
pay() returns failure |
cardCheckoutCompleted |
payWithCard() succeeds |
cardCheckoutFailed |
payWithCard() returns failure |
vaultCompleted |
vaultPaypal()/vaultCard() succeeds |
vaultFailed |
Vault returns failure |
subscriptionCreated |
createSubscription() succeeds |
subscriptionCancelled |
cancelSubscription() succeeds |
subscriptionSuspended |
suspendSubscription() succeeds |
subscriptionActivated |
activateSubscription() succeeds |
Multiple Funding Sources
Pass a fundingSource to any checkout to restrict the payment instrument:
await paypal.pay(
PaymentRequest(
orderId: 'ORDER_ID',
fundingSource: PaypalFundingSource.venmo, // or .credit, .debit, .payLater
),
);
Available values: PaypalFundingSource.paypal, .payLater, .venmo, .credit, .debit.
Pay Later Offer
Fetch promotional financing offers for a buyer before checkout:
final result = await paypal.getPayLaterOffer(
clientSecret: 'YOUR_SECRET',
amount: '150.00',
currencyCode: 'USD',
buyerCountryCode: 'US', // optional
);
result.fold(
(failure) => print('No offer: ${failure.message}'),
(offer) => print('Monthly payment: ${offer['monthly_payment']}'),
);
UI Components
PaypalCheckoutButton
Drop-in branded button with animated press, loading state, and dark mode support:
PaypalCheckoutButton(
fundingSource: PaypalFundingSource.paypal,
isLoading: _processing,
onPressed: () async {
setState(() => _processing = true);
await paypal.pay(PaymentRequest(orderId: orderId));
setState(() => _processing = false);
},
)
PaypalPayLaterBanner
Inline promotional banner that auto-calculates 4-instalment amounts:
PaypalPayLaterBanner(
amount: 120.00,
currencyCode: 'USD',
onLearnMoreTap: () => launchUrl(Uri.parse('https://www.paypal.com/paylater')),
)
PaypalVaultButton
Branded "Save payment method" button for vault flows:
PaypalVaultButton(
isLoading: _saving,
label: 'Save card for later',
onPressed: () async {
await paypal.vaultCard(VaultCardRequest(...));
},
)
Structured Logging
All internal plugin operations emit structured logs. Configure globally:
// Suppress debug logs in production
PaypalLogger.minLevel = PaypalLogLevel.warning;
// Forward logs to your analytics / Crashlytics
PaypalLogger.customHandler = (level, tag, message, [error, stackTrace]) {
FirebaseCrashlytics.instance.log('[$level] $tag: $message');
return true; // return true to suppress the default print()
};
Log levels: debug, info, warning, error, none.
Webhook Framework
Parse an incoming webhook
final event = PaypalWebhookHelper.parse(requestBody);
print('${event.eventTypeName} — ${event.resource['id']}');
// Or safe variant that returns null on error
final event = PaypalWebhookHelper.tryParse(requestBody);
Local signature verification (HMAC-SHA256)
final valid = PaypalWebhookHelper.verifySignatureLocal(
webhookId: 'WH-ID-FROM-DASHBOARD',
transmissionId: request.headers['paypal-transmission-id']!,
transmissionTime: request.headers['paypal-transmission-time']!,
certUrl: request.headers['paypal-cert-url']!,
authAlgo: request.headers['paypal-auth-algo']!,
actualSignature: request.headers['paypal-transmission-sig']!,
webhookSecret: 'YOUR_WEBHOOK_SECRET',
body: requestBody,
);
Server-side verification via PayPal API
final valid = await PaypalWebhookHelper.verifyViaApi(
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_SECRET',
environment: PaypalEnvironment.sandbox,
webhookId: 'WH-ID-FROM-DASHBOARD',
headers: {
'paypal-transmission-id': request.headers['paypal-transmission-id']!,
'paypal-transmission-time': request.headers['paypal-transmission-time']!,
'paypal-cert-url': request.headers['paypal-cert-url']!,
'paypal-auth-algo': request.headers['paypal-auth-algo']!,
'paypal-transmission-sig': request.headers['paypal-transmission-sig']!,
},
body: requestBody,
);
Supported event types (28 total): CHECKOUT.ORDER.*, PAYMENT.CAPTURE.*, PAYMENT.AUTHORIZATION.*, PAYMENT.SALE.*, BILLING.SUBSCRIPTION.*, PAYMENT.SALE.*, VAULT.*, and more.
Subscription Analytics
Compute SaaS metrics from a list of subscription maps returned by listSubscriptions():
final subs = await paypal.listSubscriptions(
clientSecret: secret,
statuses: 'ACTIVE,CANCELLED',
pageSize: 100,
);
subs.fold((err) => null, (data) {
final subscriptions = data['subscriptions'] as List<Map<String, dynamic>>;
final report = PaypalSubscriptionAnalytics.revenueReport(subscriptions);
print('MRR: \$${report.mrr.toStringAsFixed(2)}');
print('ARR: \$${report.arr.toStringAsFixed(2)}');
print('ARPU: \$${report.arpu.toStringAsFixed(2)}');
print('Churn rate: ${(report.churnRate * 100).toStringAsFixed(1)}%');
print('Active: ${report.activeSubscriptions}');
});
All metrics normalize billing intervals (daily, weekly, monthly, annual) to a per-month MRR.
Swift Package Manager (iOS)
A Package.swift manifest is now included at the package root. Flutter 3.24+ will auto-detect it. To opt in manually, add to your ios/Podfile:
# Keep CocoaPods as primary (default)
# SPM will be used automatically by Flutter 3.24+
No changes required — existing CocoaPods integrations continue to work.
v0.3.0 — Enterprise Features
Federated Plugin Architecture
PaypalPlatform provides an abstract interface for federated plugin implementations. Third-party packages can implement PaypalPlatform to add new platform targets (e.g., macOS, Windows) without forking the plugin.
// Custom platform implementation
class MyPaypalPlatform extends PaypalPlatform {
@override
Future<Either<PaymentFailure, Unit>> initialize(PaypalConfig config) async { ... }
// ...
}
// Register before first use
PaypalPlatform.instance = MyPaypalPlatform();
Web Platform Support
Use PaypalWebCheckout for REST-based checkout on Flutter Web (redirect flow — no native SDK required):
final checkout = PaypalWebCheckout(
config: config,
clientSecret: 'YOUR_CLIENT_SECRET',
);
final result = await checkout.createOrder(
amount: '50.00',
currencyCode: 'USD',
returnUrl: 'https://yourapp.com/success',
cancelUrl: 'https://yourapp.com/cancel',
);
result.fold(
(f) => print('Error: ${f.message}'),
(data) {
final approveUrl = data['approveUrl']!;
// Redirect buyer to approveUrl, then capture:
final captured = await checkout.captureOrder(orderId: data['orderId']!);
},
);
JS SDK Loader
Load the PayPal JavaScript SDK lazily (idempotent, singleton):
await PaypalJsSdkLoader.ensureLoaded(
clientId: 'YOUR_CLIENT_ID',
environment: PaypalEnvironment.sandbox,
currency: 'USD',
fundingSources: ['paypal', 'paylater'],
);
// Check status
if (PaypalJsSdkLoader.isLoaded) {
print('SDK version: ${PaypalJsSdkLoader.sdkVersion}');
}
Funding Eligibility
Check which PayPal funding sources are available for a given buyer — with TTL caching:
final result = await paypal.checkFundingEligibility(
clientSecret: 'YOUR_CLIENT_SECRET',
currencyCode: 'USD',
buyerCountryCode: 'US',
);
result.fold(
(f) => print('Error: ${f.message}'),
(eligibility) {
if (eligibility.payLaterEligible) showPayLaterBadge();
if (eligibility.venmoEligible) showVenmoOption();
// List all eligible sources
for (final source in eligibility.eligibleSources) {
print('Eligible: $source');
}
},
);
// Or use the static API directly
await PaypalFundingEligibility.check(
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_SECRET',
environment: PaypalEnvironment.sandbox,
currencyCode: 'USD',
);
// Cache is valid for 5 minutes by default
PaypalFundingEligibility.cacheDuration = const Duration(minutes: 10);
PaypalFundingEligibility.clearCache();
Pay Later Offer Service
Fetch structured Pay Later financing offers for a given amount:
final result = await PayLaterOfferService.getOffer(
clientId: 'CLIENT_ID',
clientSecret: 'SECRET',
environment: PaypalEnvironment.sandbox,
amount: '500.00',
currencyCode: 'USD',
buyerCountryCode: 'US',
);
result.fold(
(f) => print(f.message),
(offer) {
print(offer.summary); // "4 payments of $125.00"
print(offer.formattedMonthly); // "$125.00"
print(offer.installments); // 4
print(offer.disclosure); // Legal text
},
);
Marketplace / Commerce Platform
Multi-seller checkout via PayPal Commerce Platform:
final service = PaypalMarketplaceService(
config: config,
clientSecret: 'SECRET',
partnerMerchantId: 'PARTNER_PAYER_ID',
);
// 1. Onboard a seller
final referral = await service.createPartnerReferral(
merchantEmail: 'seller@example.com',
trackingId: 'seller_unique_id',
returnUrl: 'https://yourapp.com/onboarding/complete',
);
referral.fold(
(f) => print(f.message),
(r) => redirect(r.actionUrl), // Send seller to PayPal onboarding
);
// 2. Check onboarding status
final status = await service.getSellerStatus(merchantId: 'MERCHANT_PAYER_ID');
status.fold(
(f) => print(f.message),
(s) => print('Fully onboarded: ${s.isFullyOnboarded}'),
);
// 3. Create a marketplace order with platform fee
final order = await service.createMarketplaceOrder(
amount: '100.00',
currencyCode: 'USD',
sellerMerchantId: 'SELLER_PAYER_ID',
platformFee: '5.00',
returnUrl: 'https://yourapp.com/success',
cancelUrl: 'https://yourapp.com/cancel',
);
// 4. Capture for the seller
await service.captureForMerchant(
orderId: 'ORDER_ID',
sellerMerchantId: 'SELLER_PAYER_ID',
);
service.dispose();
Subscription Widget
Display subscription details with status badge and action buttons:
PaypalSubscriptionWidget(
subscriptionData: subscriptionJson, // raw map from getSubscriptionDetails()
onCancel: () => _cancelSubscription(),
onSuspend: () => _suspendSubscription(),
onActivate: () => _activateSubscription(),
showActions: true,
backgroundColor: Colors.white,
borderRadius: 12,
)
Status badge colors: ACTIVE (green), SUSPENDED (amber), CANCELLED (red), EXPIRED (grey), APPROVAL_PENDING (blue).
Debug Overlay
A floating debug panel — automatically hidden in release builds:
// 1. Create controller
final debugController = PaypalDebugController();
// 2. Wire up event streams
paypal.events.checkoutStarted.listen(debugController.recordCheckoutEvent);
paypal.events.checkoutCompleted.listen(debugController.recordCheckoutEvent);
paypal.events.checkoutFailed.listen(debugController.recordCheckoutEvent);
// 3. Wrap your UI
PaypalDebugOverlay(
controller: debugController,
child: MyApp(),
)
// 4. Record SDK initialization
debugController.recordInit(env: 'sandbox');
// 5. Record custom events
debugController.recordEvent(
type: 'CAPTURE_STARTED',
summary: 'Capturing order',
detail: 'orderId: ORDER-123',
);
Trace Log Level
Ultra-verbose trace level for debugging raw HTTP traffic — never enable in production:
PaypalLogger.minLevel = PaypalLogLevel.trace;
Log levels (most to least verbose): trace, debug, info, warning, error, none.
Enhanced Event Bus
Four new event streams in v0.3.0:
| Stream | Emits when… |
|---|---|
cardPaymentStarted |
Card payment submitted to SDK |
vaultStarted |
Vault operation begins |
refundCompleted |
refund() succeeds |
refundFailed |
refund() returns failure |
paypal.events.refundCompleted.listen((e) {
print('Refunded ${e.refundId} for capture ${e.captureId}');
});
paypal.events.cardPaymentStarted.listen((e) {
print('Card payment started for order ${e.orderId}');
});
Revenue Segmentation Analytics
Two new analytics methods and a growth trend utility:
final subs = [...]; // from listSubscriptions()
// MRR grouped by plan ID (sorted descending)
final byPlan = PaypalSubscriptionAnalytics.revenueByPlan(subs);
byPlan.forEach((planId, mrr) => print('$planId: \$$mrr'));
// MRR grouped by calendar month (YYYY-MM, sorted ascending)
final byMonth = PaypalSubscriptionAnalytics.revenueByMonth(subs);
byMonth.forEach((month, mrr) => print('$month: \$$mrr'));
// Month-over-month growth trend
final trend = PaypalSubscriptionAnalytics.revenueTrend(subs);
for (final t in trend) {
print('${t.month}: \$${t.mrr.toStringAsFixed(2)} '
'(${t.growthPercent?.toStringAsFixed(1) ?? 'N/A'}% MoM)');
print(t.isGrowth ? '↑' : t.isDecline ? '↓' : '—');
}
Migration Guide: v0.2.x → v0.3.x
pubspec.yaml: Bump version to ^0.3.0.
Logger: PaypalLogLevel.trace is now the lowest level (index 0). Existing minLevel comparisons still work — no code changes needed unless you compare enum .index values directly.
Event bus: All new streams (cardPaymentStarted, vaultStarted, refundCompleted, refundFailed) are broadcast streams — subscribe normally. Existing streams are unchanged.
Exports: All new types (FundingEligibilityResult, PayLaterOffer, PaypalMarketplaceService, PaypalWebCheckout, PaypalSubscriptionWidget, PaypalDebugOverlay, PaypalPlatform, etc.) are now exported from paypal_checkout_flutter.dart — no additional imports needed.
Support
If this package helps you, consider supporting its development:
License
BSD-3-Clause — See LICENSE for details.