Paystack plugin v2
A Medusa v2 plugin for Paystack payment integration.
Medusa Paystack Plugin v2
A robust Paystack payment integration plugin for Medusa v2.8.6+ that provides seamless payment processing using Paystack's payment infrastructure with support for the new payment session lifecycle.
✨ Features
- 🚀 Complete Payment Flow: Initialize, authorize, capture, and refund payments
- 🔐 Secure Transactions: Built-in webhook verification and secure API communication
- 🌍 Multi-Currency Support: NGN, USD, GHS, ZAR with automatic currency conversion
- 📱 Modern Payment Experience: Paystack Popup integration with retry mechanisms
- 🎯 Medusa v2 Compatible: Full support for new payment session lifecycle
- 🔄 Session Management: Intelligent session refresh and expiry handling
- 🛡️ Error Handling: Comprehensive error handling with user-friendly messages
📦 Installation
1. Install the Plugin
npm install @alexasomba/medusa-paystack-plugin-v2# oryarn add @alexasomba/medusa-paystack-plugin-v2
2. Environment Configuration
Add these environment variables to your file:
# Paystack ConfigurationPAYSTACK_SECRET_KEY=sk_test_your_secret_key_herePAYSTACK_PUBLIC_KEY=pk_test_your_public_key_herePAYSTACK_WEBHOOK_SECRET=your_webhook_secret_here
3. Medusa Configuration
Configure the plugin in your :
import { loadEnv, defineConfig } from '@medusajs/framework/utils'loadEnv(process.env.NODE_ENV || 'development', process.cwd())module.exports = defineConfig({projectConfig: {databaseUrl: process.env.DATABASE_URL,http: {storeCors: process.env.STORE_CORS!,adminCors: process.env.ADMIN_CORS!,authCors: process.env.AUTH_CORS!,jwtSecret: process.env.JWT_SECRET || "supersecret",cookieSecret: process.env.COOKIE_SECRET || "supersecret",}},modules: [{resolve: "@medusajs/payment",options: {providers: [{resolve: "@alexasomba/medusa-paystack-plugin-v2",id: "paystack",options: {secret_key: process.env.PAYSTACK_SECRET_KEY,public_key: process.env.PAYSTACK_PUBLIC_KEY,webhook_secret: process.env.PAYSTACK_WEBHOOK_SECRET,},},],},},],})
4. Build and Start
npm run buildnpm run dev
⚙️ Configuration
Environment Variables
Variable | Description | Required | Example |
---|---|---|---|
Your Paystack secret key | ✅ | ||
Your Paystack public key | ✅ | ||
Webhook secret for verification | ⚠️ Recommended |
Plugin Configuration
interface PaystackConfig {secret_key: string // Paystack secret keypublic_key: string // Paystack public keywebhook_secret?: string // Optional webhook secret}
🖥️ Frontend Integration
Next.js Storefront Integration
Install required dependencies in your storefront:
npm install @paystack/inline-js
1. Paystack Payment Component
// components/PaystackPayment.tsx"use client"import { useState } from "react"import { toast } from "@medusajs/ui"interface PaystackPaymentProps {session: anycart: anyonPaymentCompleted: (reference: string) => voidonPaymentFailed: (error: string) => void}export function PaystackPayment({session,cart,onPaymentCompleted,onPaymentFailed}: PaystackPaymentProps) {const [isLoading, setIsLoading] = useState(false)const initializePayment = async () => {try {setIsLoading(true)const { access_code, authorization_url } = session.dataif (!access_code) {throw new Error("Payment session not ready")}// Use Paystack Popupconst PaystackPop = (await import("@paystack/inline-js")).defaultconst popup = new PaystackPop()popup.resumeTransaction(access_code, {onClose: () => {setIsLoading(false)toast.warning("Payment was cancelled")},onSuccess: (transaction: any) => {setIsLoading(false)toast.success("Payment successful!")onPaymentCompleted(transaction.reference)},onError: (error: any) => {setIsLoading(false)let errorMessage = "Payment failed"if (error.message?.toLowerCase().includes('not found')) {errorMessage = "Payment session expired. Please try again."}toast.error(errorMessage)onPaymentFailed(errorMessage)}})} catch (error) {setIsLoading(false)onPaymentFailed(error.message)}}return (<buttononClick={initializePayment}disabled={isLoading}className="w-full bg-green-600 hover:bg-green-700 text-white font-medium py-3 px-4 rounded-lg">{isLoading ? "Processing..." : "Pay with Paystack"}</button>)}
2. Payment Session Hook
// hooks/use-paystack-session.tsximport { useState, useEffect } from "react"export function usePaystackSession({ session, cart, onSessionUpdate }) {const [isReady, setIsReady] = useState(false)const [isUpdating, setIsUpdating] = useState(false)useEffect(() => {if (session?.data) {const { authorization_url, access_code, session_expired, payment_completed } = session.dataconst isExpiredOrCompleted = session_expired === true || payment_completed === trueconst ready = !isExpiredOrCompleted &&((session.status === "requires_more" && (authorization_url || access_code)) ||session.status === "authorized")setIsReady(ready)}}, [session])const updateSession = async (customerData?: any) => {if (!cart?.payment_collection?.id) return falsesetIsUpdating(true)try {const response = await fetch(`${process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL}/store/payment-collections/${cart.payment_collection.id}/payment-sessions`,{method: "POST",headers: {"Content-Type": "application/json","x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY!,},body: JSON.stringify({provider_id: "pp_paystack_paystack",data: { email: customerData?.email || cart.email }}),})if (response.ok) {const result = await response.json()const paystackSession = result.payment_collection.payment_sessions?.find((s: any) => s.provider_id === 'pp_paystack_paystack')if (paystackSession && onSessionUpdate) {onSessionUpdate(paystackSession)return true}}return false} catch (error) {console.error('Session update failed:', error)return false} finally {setIsUpdating(false)}}return {isReady,isUpdating,updateSession,sessionData: session?.data,}}
3. Integration in Checkout
// In your checkout componentimport { PaystackPayment } from "./PaystackPayment"import { usePaystackSession } from "./use-paystack-session"export function CheckoutPayment({ cart }) {const paystackSession = cart.payment_collection?.payment_sessions?.find((session) => session.provider_id === "pp_paystack_paystack")const { isReady, updateSession } = usePaystackSession({session: paystackSession,cart,onSessionUpdate: (updatedSession) => {// Handle session update}})const handlePaymentCompleted = async (reference: string) => {// Complete the cart/orderconst response = await fetch(`/store/carts/${cart.id}/complete`, {method: "POST",headers: {"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY!,},})if (response.ok) {// Redirect to success pagewindow.location.href = "/order/confirmed"}}if (!paystackSession) {return <div>Paystack payment not available</div>}return (<PaystackPaymentsession={paystackSession}cart={cart}onPaymentCompleted={handlePaymentCompleted}onPaymentFailed={(error) => console.error("Payment failed:", error)}/>)}
🔧 Backend Features
Payment Session Lifecycle
The plugin correctly implements Medusa v2's payment session states:
- - Session created but awaiting customer email
- - Ready for payment (has authorization URL)
- - Payment completed successfully
- - Payment failed or session error
Session Refresh & Expiry Handling
The plugin automatically handles:
- Expired access codes
- Completed payment sessions
- Session recreation for retry scenarios
- Customer email validation
Webhook Support
Set up webhooks in your Paystack dashboard:
Webhook URL: https://yourdomain.com/store/paystack/webhook/routeEvents: charge.success, charge.failed
API Endpoints
The plugin provides these endpoints:
- - Webhook handler
- - Admin verification
- - Plugin status
💰 Supported Currencies
Currency | Code | Subunit | Example |
---|---|---|---|
Nigerian Naira | NGN | kobo | ₦100.00 |
US Dollar | USD | cents | $1.00 |
Ghanaian Cedi | GHS | pesewas | GH₵1.00 |
South African Rand | ZAR | cents | R1.00 |
🔍 Testing
Test Cards
Use these test cards in development:
# Successful PaymentCard: 4084 0840 8408 4081CVV: 408Expiry: 12/25# Declined PaymentCard: 4084 0840 8408 4096CVV: 408Expiry: 12/25
Development Workflow
- Use test API keys from Paystack dashboard
- Test with different scenarios (success, failure, expired sessions)
- Verify webhook delivery in Paystack dashboard
- Test session refresh and retry mechanisms
🛠️ Troubleshooting
Common Issues
"Not found" Error in Payment Popup
- This occurs when trying to reuse an expired/completed access code
- The plugin automatically handles this with session refresh
- Ensure your frontend implements the session refresh logic
Session Status Undefined Error
- Fixed in v1.3.3+ - plugin now always returns valid status
- Update to latest version if experiencing this
Customer Email Missing
- Ensure customer email is provided during session creation
- Plugin returns status until email is available
Webhook Not Triggering
- Verify webhook URL is publicly accessible
- Check webhook secret configuration
- Review Paystack dashboard for delivery logs
Debugging
Enable detailed logging:
LOG_LEVEL=debugNODE_ENV=development
This will show detailed logs for:
- Payment session creation
- Paystack API responses
- Session status changes
- Error details
📊 Migration from v1
If upgrading from v1, note these breaking changes:
- Provider ID: Now uses format
- Session Management: New lifecycle handling
- Configuration: Updated to Medusa v2 format
- Dependencies: Uses official Paystack SDK
Migration Steps
- Update configuration to new format
- Install new version:
- Update frontend integration to use new session states
- Test thoroughly with new session refresh logic
🚀 Version History
- v1.3.3 - Fixed session status handling and expiry management
- v1.3.0 - Added session refresh and error handling improvements
- v1.2.0 - Multi-currency support and official Paystack SDK
- v1.1.0 - Webhook support and admin integration
- v1.0.0 - Initial release for Medusa v2
📄 License
MIT License - see LICENSE file for details.
🤝 Contributing
- Fork the repository
- Create your feature branch ()
- Commit your changes ()
- Push to the branch ()
- Open a Pull Request
📞 Support
- 📧 Email: alex@asomba.com
- 💬 GitHub Issues: Report Issues
- 📖 Documentation: Medusa Docs
- 🌍 Community: Medusa Discord
⭐ Show Your Support
If this plugin helps your project, please give it a star on GitHub! ⭐
Built with ❤️ for the Medusa community