medusajs-payment-mollie
v0.2.1
Published
Mollie payment provider for Medusa
Downloads
520
Maintainers
Readme
medusajs-payment-mollie
Note: This package is currently in development. We encourage you to test the package in your environment and report any issues or improvements.
Table of Contents
Installation
To install the medusajs-payment-mollie
package, you can use either npm or yarn:
npm install medusajs-payment-mollie
yarn add medusajs-payment-mollie
Configuration
Backend Configuration
Add Mollie as a Payment Provider
In your Medusa server, update the medusa-config.js file to include Mollie as a payment provider:
modules: [
{
resolve: "@medusajs/medusa/payment",
options: {
providers: [
{
resolve: "medusajs-payment-mollie",
id: "mollie",
options: {
/**
* The ID assigned to the payment provider in `medusa-config.ts`.
* This ID will be used to construct the webhook URL for receiving events from the Mollie API.
*/
providerId: "mollie",
// Your Mollie API key (use either live or test key)
apiKey: process.env.MOLLIE_API_KEY,
// Default description for payments when not provided
paymentDescription: "mollie payment default description",
/**
* The base URL for the webhook. This will be used to construct the complete webhook URL for Mollie events.
* For example:
* webhookUrl: `https://example.com`
* providerId: `mollie`
* The final callback URL will be: `https://example.com/hooks/payment/mollie_mollie`
*
* The `webhookUrl` should always point to the domain where the Medusa backend is deployed.
*/
webhookUrl: "https://your-domain.com",
},
},
],
},
},
];
Create a Custom API Endpoint
Add a custom API endpoint to list available Mollie payment methods:
src/api/store/mollie/payment-methods/route.ts
import MollieClient from "@mollie/api-client";
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework";
export async function GET(req: MedusaRequest, res: MedusaResponse) {
const mollieClient = MollieClient({
apiKey: process.env.MOLLIE_API_KEY || "",
});
const methods = await mollieClient.methods.list();
res.status(200).json(methods);
}
Frontend Configuration
Handle the Place Order Logic
To complete the order after a successful Mollie transaction, create an API route for order completion and redirection:
/src/app/api/place-order/[cartId]/route.ts
import { sdk } from "@lib/config";
import { revalidateTag } from "next/cache";
import medusaError from "@lib/util/medusa-error";
import { getAuthHeaders, removeCartId } from "@lib/data/cookies";
type Params = { params: Promise<{ cartId: string }> };
export async function GET(_: Request, { params }: Params) {
try {
const cartId = (await params).cartId;
const cartRes = await sdk.store.cart
.complete(cartId, {}, getAuthHeaders())
.then((cartRes) => {
revalidateTag("cart");
return cartRes;
})
.catch(medusaError);
if (cartRes?.type === "order") {
const countryCode =
cartRes.order.shipping_address?.country_code?.toLowerCase();
removeCartId();
return Response.redirect(
new URL(
`/${countryCode}/order/confirmed/${cartRes?.order.id}`,
process.env.NEXT_PUBLIC_BASE_URL
)
);
}
return Response.redirect(new URL("/", process.env.NEXT_PUBLIC_BASE_URL));
} catch (error: any) {
return Response.json({ error: error?.message });
}
}
Update initiatePaymentSession
method
In your frontend component, update the initiatePaymentSession method to include Mollie. This ensures the payment session is correctly created.
/src/modules/checkout/components/payment.tsx
await initiatePaymentSession(cart, {
provider_id: providerId,
context: {
extra: {
// selected payment method or null
method,
// url to which the customer should be redirected after transaction is complete
redirectUrl,
/// description for the payment
paymentDescription: "new payment",
},
},
});
// import isMollie
import { isMollie as isMollieFunc } from "@lib/constants";
// handleSubmit
const handleSubmit = async () => {
setError("");
if (!cart || !selectedPaymentMethod) {
return;
}
setIsLoading(true);
try {
const shouldInputCard =
isStripeFunc(selectedPaymentMethod) && !activeSession;
if (!activeSession) {
let method: string | undefined = undefined;
let providerId = selectedPaymentMethod;
let redirectUrl: string | null = null;
if (isMollieFunc(selectedPaymentMethod)) {
const parts = selectedPaymentMethod.split("_");
method = parts.pop();
providerId = parts.join("_");
redirectUrl = `${process.env.NEXT_PUBLIC_BASE_URL}/api/place-order/${cart.id}`;
}
await initiatePaymentSession(cart, {
provider_id: providerId,
context: {
extra: {
method,
redirectUrl,
paymentDescription: "new payment",
},
},
});
}
if (!shouldInputCard) {
return router.push(pathname + "?" + createQueryString("step", "review"), {
scroll: false,
});
}
} catch (err: any) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
Mollie Payment Method Component
This component will render the Mollie payment options.
import { RadioGroup } from "@headlessui/react";
import { clx, Text } from "@medusajs/ui";
import Radio from "@modules/common/components/radio";
import { useEffect, useState } from "react";
const fetchMolliePaymentOptions = async () =>
fetch(
`${process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL}/store/mollie/payment-methods`,
{
method: "GET",
credentials: "include",
headers: {
"x-publishable-api-key":
process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "",
},
}
).then((res) => res.json());
type PaymentOption = {
description: string;
id: string;
image: {
svg: string;
};
};
const providerId = "pp_mollie_mollie";
export const MolliePaymentOptions = (props: {
selectedOptionId: string;
setSelectedOptionId: (value: string) => void;
}) => {
const { selectedOptionId, setSelectedOptionId } = props;
const [paymentOptions, setPaymentOptions] = useState<PaymentOption[]>([]);
useEffect(() => {
fetchMolliePaymentOptions()
.then((methods) => {
setPaymentOptions(methods);
})
.catch(console.log);
}, []);
return (
<div>
<RadioGroup
value={selectedOptionId}
onChange={(value: string) => setSelectedOptionId(value)}
>
{paymentOptions.map(({ description, id, image }) => (
<RadioGroup.Option
/// the prefix `pp_mollie_mollie_` should be same as the provider_id
value={`pp_mollie_mollie_${id}`}
key={id}
className={clx(
"flex flex-col gap-y-2 text-small-regular cursor-pointer py-4 border rounded-rounded px-8 mb-2 hover:shadow-borders-interactive-with-active",
{
"border-ui-border-interactive": selectedOptionId?.endsWith(id),
}
)}
>
<div className="flex items-center justify-between ">
<div className="flex items-center gap-x-4">
<Radio checked={selectedOptionId?.endsWith(id)} />
<Text className="text-base-regular">{description}</Text>
</div>
<span className="justify-self-end text-ui-fg-base">
<picture>
<img src={image.svg} alt={description} />
</picture>
</span>
</div>
</RadioGroup.Option>
))}
</RadioGroup>
</div>
);
};
Contributing
To contribute:
- Fork the repository.
- Create a new branch (
git checkout -b feat/your-feature
). - Make your changes.
- Commit your changes (
git commit -am 'Add new feature'
). - Push to your branch (
git push origin feat/your-feature
). - Create a new Pull Request.
If you encounter any issues, please open an issue on GitHub. If you have a fix, feel free to create a PR.