PayPal Payment Provider
PayPal payment provider for Medusa v2 with support for payment capture, refunds, and webhook integration
Overview
The medusa-plugin-paypal is a payment provider integration for PayPal designed specifically for Medusa v2 commerce platforms. This plugin enables seamless PayPal payment processing with support for payment capture and webhook integration.
System Requirements
Before installing the plugin, ensure your system meets the following requirements:
- Medusa server: v2.10.4 or later
- Node.js: v20 or later
Installation
Install the plugin using npm:
npm i medusa-plugin-paypal
Backend Configuration
Configure the plugin in your medusa-config.js file within the modules section:
const { loadEnv, defineConfig, Modules } = require("@medusajs/framework/utils");
modules: [
[Modules.PAYMENT]: {
resolve: "@medusajs/medusa/payment",
options: {
providers: [
{
resolve: "medusa-plugin-paypal/providers/paypal-payment",
id: "payment-paypal",
options: {
intent: "CAPTURE",
clientId: process.env.PAYPAL_CLIENT_ID,
clientSecret: process.env.PAYPAL_CLIENT_SECRET,
sandbox: true,
webhookId: process.env.PAYPAL_WEBHOOK_ID,
}
}
]
}
}
]
Environment Variables
Add the following environment variables to your .env file:
PAYPAL_CLIENT_ID=your_paypal_client_id
PAYPAL_CLIENT_SECRET=your_paypal_client_secret
PAYPAL_WEBHOOK_ID=your_paypal_webhook_id
Frontend Implementation
Step 1: PayPal Wrapper Component
Create /src/modules/checkout/components/payment-wrapper/paypal-wrapper.tsx:
"use client"
import { HttpTypes } from "@medusajs/types"
import { PayPalScriptProvider, ReactPayPalScriptOptions } from "@paypal/react-paypal-js"
type PaypalWrapperProps = {
paymentSession: HttpTypes.StorePaymentSession
clientId: string
children: React.ReactNode
}
const PaypalWrapper: React.FC<PaypalWrapperProps> = ({
paymentSession,
clientId,
children,
}) => {
const initialOptions: ReactPayPalScriptOptions = {
clientId,
currency: paymentSession.currency_code.toLocaleUpperCase(),
intent: "capture",
components: "buttons",
debug: false
}
return (
<PayPalScriptProvider deferLoading={false} options={initialOptions}>
{children}
</PayPalScriptProvider>
)
}
export default PaypalWrapper
Step 2: Payment Wrapper Integration
Update /src/modules/checkout/components/payment-wrapper/index.tsx:
if (isPaypal(paymentSession?.provider_id) && paymentSession) {
return (
<PaypalWrapper
paymentSession={paymentSession}
clientId={paypalClientId}
>
{children}
</PaypalWrapper>
)
}
Step 3: Payment Button Component
Add PayPal button logic to /src/modules/checkout/components/payment-button/index.tsx:
const PaypalPaymentButton = ({
cart,
notReady,
"data-testid": dataTestId,
}: {
cart: HttpTypes.StoreCart
notReady: boolean
"data-testid"?: string
}) => {
const [submitting, setSubmitting] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const onPaymentCompleted = async () => {
await placeOrder()
}
const handlePayment = async (
_data: OnApproveData,
actions: OnApproveActions
) => {
setSubmitting(true)
await actions?.order?.capture()
.then((authorization) => {
if (authorization.status !== "COMPLETED") {
setSubmitting(false)
setErrorMessage(`An error occurred, status: ${authorization.status}`)
return
}
onPaymentCompleted()
})
.catch(() => {
setErrorMessage("An unknown error occurred, please try again.")
setSubmitting(false)
})
}
const session = cart.payment_collection?.payment_sessions?.find(
(s) => s.status === "pending"
)
return (
<>
<PayPalButtons
disabled={submitting}
createOrder={async () => session?.data.id as string}
onApprove={handlePayment}
onError={(err) => {
console.error("PayPal Checkout onError", err)
}}
style={styles}
/>
<ErrorMessage
error={errorMessage}
data-testid="paypal-payment-error-message"
/>
</>
)
}
Key Features
- CAPTURE Intent - Immediate payment authorization
- Sandbox Mode - Testing environment support
- Webhook Integration - Payment event notifications
- Currency-Aware - Automatic currency handling
- Error Handling - Comprehensive error management