Skip to main content

Overview

The Payout component enables secure collection of payment methods using Stripe Elements. It supports credit/debit cards and SEPA direct debit, with built-in 3D Secure authentication handling.
A Stripe publishable API key is required for the Payout component to function. Never use your secret key in client-side code.

Prerequisites

1. Install Required Packages

npm install @stripe/react-stripe-js @stripe/stripe-js

2. Get Stripe API Keys

1

Create Stripe Account

Sign up at stripe.com and complete account verification.
2

Navigate to API Keys

Go to Developers > API Keys in the Stripe Dashboard.
3

Copy Publishable Key

Copy your Publishable key (starts with pk_test_ for test mode or pk_live_ for production).
The publishable key is safe to expose in client-side code. The secret key (starts with sk_) should never be used in frontend applications.
4

Configure Webhook (Optional)

For production, set up webhook endpoints to handle payment events on your backend.

Environment Variables

Store your Stripe publishable key securely:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
Use pk_test_... for development and testing, and pk_live_... for production. Never commit API keys to version control.

Basic Usage

Card Payment Method

PaymentForm.tsx
import { useRef, useState } from 'react';
import { Payout, PayoutRef } from '@adoptaunabuelo/react-components';

export default function PaymentForm() {
  const payoutRef = useRef<PayoutRef>(null);
  const [isLoading, setIsLoading] = useState(false);

  const handleSubmit = async () => {
    const paymentMethod = await payoutRef.current?.getPaymentMethod();
    
    if (paymentMethod) {
      // Send payment method ID to your backend
      console.log('Payment method ID:', paymentMethod.id);
      // Process payment on your backend
    }
  };

  return (
    <div>
      <Payout
        ref={payoutRef}
        stripeKey={process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!}
        paymentOption="card"
        design="primary"
        onLoading={setIsLoading}
        onSetupConfirmed={() => {
          console.log('3D Secure authentication completed');
        }}
      />
      
      <button onClick={handleSubmit} disabled={isLoading}>
        {isLoading ? 'Processing...' : 'Submit Payment'}
      </button>
    </div>
  );
}

SEPA Direct Debit

SepaForm.tsx
import { useRef } from 'react';
import { Payout, PayoutRef } from '@adoptaunabuelo/react-components';

export default function SepaForm() {
  const payoutRef = useRef<PayoutRef>(null);

  const handleSubmit = async () => {
    const paymentMethod = await payoutRef.current?.getPaymentMethod();
    
    if (paymentMethod) {
      // Send to backend for SEPA debit setup
      console.log('SEPA method:', paymentMethod);
    }
  };

  return (
    <div>
      <Payout
        ref={payoutRef}
        stripeKey={process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!}
        paymentOption="sepa_debit"
        design="secondary"
        userData={{
          email: '[email protected]' // Required for SEPA
        }}
        placeholderEmail="Correo electrónico"
        placeholderName="Nombre del titular"
      />
      
      <button onClick={handleSubmit}>
        Add Bank Account
      </button>
    </div>
  );
}

Component Props

Required Props

stripeKey
string
required
Your Stripe publishable API key (starts with pk_test_ or pk_live_).
paymentOption
'card' | 'sepa_debit'
required
Payment method type to collect:
  • "card": Credit/debit card with 3D Secure support
  • "sepa_debit": SEPA direct debit (European bank accounts)

Optional Props

design
'primary' | 'secondary'
default:"primary"
Visual design variant for the form inputs.
stripeConfirmUrl
string
3D Secure confirmation URL from Stripe SetupIntent. When provided, opens authentication modal automatically.
userData
{ email?: string }
Pre-fill user data. Email is required for SEPA direct debit.
placeholderName
string
Custom placeholder for cardholder/account holder name input.
placeholderEmail
string
Custom placeholder for email input (SEPA only).
cardStyle
CSSProperties
Custom CSS styles for the payment form container.
error
boolean
When true, displays error state styling on the form.
onLoading
(loading: boolean) => void
Callback fired when loading state changes (during payment method collection or 3D Secure flow).
onSetupConfirmed
() => void
Callback fired when 3D Secure authentication completes successfully.

Component Methods

Access component methods via ref:

getPaymentMethod()

Collects and returns the payment method from Stripe.
const paymentMethod = await payoutRef.current?.getPaymentMethod();

if (paymentMethod) {
  console.log(paymentMethod.id);        // pm_abc123...
  console.log(paymentMethod.type);      // "card" or "sepa_debit"
  console.log(paymentMethod.card);      // Card details (if card)
  console.log(paymentMethod.sepa_debit); // IBAN details (if SEPA)
}
Returns: Promise<PaymentMethod | undefined>

3D Secure Authentication

The component automatically handles 3D Secure (SCA) authentication:
1

User Submits Card

User enters card details and you call getPaymentMethod().
2

Backend Creates SetupIntent

Your backend creates a Stripe SetupIntent and may receive a confirmation URL if 3DS is required.
const setupIntent = await stripe.setupIntents.create({
  payment_method: paymentMethod.id,
  confirm: true,
  return_url: 'https://yourapp.com/payment/complete'
});

if (setupIntent.next_action?.redirect_to_url?.url) {
  // 3D Secure required
  return { confirmUrl: setupIntent.next_action.redirect_to_url.url };
}
3

Pass Confirmation URL to Component

Update the component with the confirmation URL:
const [confirmUrl, setConfirmUrl] = useState<string>();

<Payout
  ref={payoutRef}
  stripeKey={stripeKey}
  stripeConfirmUrl={confirmUrl}
  paymentOption="card"
  onSetupConfirmed={() => {
    console.log('3D Secure completed!');
    // Finalize payment on backend
  }}
/>
4

User Authenticates

The component opens a modal with the bank’s 3D Secure page. After authentication, onSetupConfirmed is called.

Backend Integration

You’ll need a backend endpoint to handle payment method processing:
server.js (Node.js example)
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

app.post('/api/create-setup-intent', async (req, res) => {
  try {
    const { paymentMethodId, customerId } = req.body;
    
    // Attach payment method to customer
    await stripe.paymentMethods.attach(paymentMethodId, {
      customer: customerId,
    });
    
    // Create SetupIntent for 3D Secure if needed
    const setupIntent = await stripe.setupIntents.create({
      payment_method: paymentMethodId,
      customer: customerId,
      confirm: true,
      return_url: `${process.env.APP_URL}/payment/complete`,
    });
    
    res.json({
      success: true,
      confirmUrl: setupIntent.next_action?.redirect_to_url?.url,
    });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

Complete Example with Backend

CompletePaymentFlow.tsx
import { useRef, useState } from 'react';
import { Payout, PayoutRef } from '@adoptaunabuelo/react-components';

export default function CompletePaymentFlow() {
  const payoutRef = useRef<PayoutRef>(null);
  const [loading, setLoading] = useState(false);
  const [confirmUrl, setConfirmUrl] = useState<string>();
  const [error, setError] = useState<string>();

  const handleSubmit = async () => {
    try {
      setLoading(true);
      setError(undefined);
      
      // Step 1: Get payment method from Stripe
      const paymentMethod = await payoutRef.current?.getPaymentMethod();
      
      if (!paymentMethod) {
        throw new Error('Failed to collect payment method');
      }
      
      // Step 2: Send to backend
      const response = await fetch('/api/create-setup-intent', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          paymentMethodId: paymentMethod.id,
          customerId: 'cus_abc123', // Your Stripe customer ID
        }),
      });
      
      const data = await response.json();
      
      // Step 3: Handle 3D Secure if needed
      if (data.confirmUrl) {
        setConfirmUrl(data.confirmUrl);
      } else {
        // Payment method set up successfully
        console.log('Payment method added!');
      }
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      <Payout
        ref={payoutRef}
        stripeKey={process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!}
        stripeConfirmUrl={confirmUrl}
        paymentOption="card"
        design="primary"
        error={!!error}
        onLoading={setLoading}
        onSetupConfirmed={() => {
          console.log('3D Secure authentication successful!');
          setConfirmUrl(undefined);
          // Verify on backend that setup is complete
        }}
      />
      
      {error && <p style={{ color: 'red' }}>{error}</p>}
      
      <button onClick={handleSubmit} disabled={loading}>
        {loading ? 'Processing...' : 'Add Payment Method'}
      </button>
    </div>
  );
}

Styling

The component uses Stripe Elements with custom styling. Font family is set to Poppins:
<Payout
  stripeKey={stripeKey}
  paymentOption="card"
  cardStyle={{
    padding: '20px',
    border: '1px solid #e0e0e0',
    borderRadius: '8px',
  }}
/>
Stripe Elements inherit CSS variables. Customize globally:
globals.css
:root {
  --primary-color: #007bff;
}

.StripeElement {
  padding: 12px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.StripeElement--focus {
  border-color: var(--primary-color);
}

.StripeElement--invalid {
  border-color: #dc3545;
}

Testing

Test Cards

Use Stripe’s test cards in development:
Card NumberDescription3D Secure
4242 4242 4242 4242Visa - SucceedsNo
4000 0027 6000 3184Visa - Requires 3DSYes
4000 0000 0000 9995Visa - DeclinedNo
5555 5555 5555 4444Mastercard - SucceedsNo
  • Use any future expiration date (e.g., 12/34)
  • Use any 3-digit CVC (e.g., 123)
  • Use any postal code

Test SEPA Accounts

IBANResult
DE89370400440532013000Success
DE62370400440532013001Fails

Troubleshooting

”Invalid API key” error

  • Verify you’re using the publishable key (starts with pk_), not the secret key
  • Check the key matches your environment (test vs. production)
  • Ensure environment variable is properly loaded

3D Secure modal doesn’t appear

  • Ensure stripeConfirmUrl prop is set with the confirmation URL from your backend
  • Check browser console for errors
  • Verify your backend is properly creating SetupIntents

SEPA form doesn’t accept email

Email is required for SEPA direct debit. Pass it via the userData prop:
<Payout
  paymentOption="sepa_debit"
  userData={{ email: '[email protected]' }}
/>

Security Best Practices

  • Never expose your Stripe secret key in client-side code
  • Always validate payment methods on your backend before processing
  • Use HTTPS in production
  • Enable Stripe’s fraud detection tools (Radar)
  • Set up webhook signature verification

Next Steps

Payout Component

View full component documentation and examples

Stripe Documentation

Official Stripe API documentation

Optional Dependencies

Learn about other optional peer dependencies

Build docs developers (and LLMs) love