Search for a command to run...
To integrate the YooKassa payment provider in a Next.js storefront, start by adding the required UI components. So, the provider is displayed on the checkout page alongside other available payment methods.
When YooKassa is selected, the storefront should call with the necessary parameters. This will create a payment session through the YooKassa API and prepare the customer for redirection. The Place Order button should then send the customer to the YooKassa payment page, where he can select his preferred payment method.
Once the payment is completed, YooKassa will concurrently send a webhook and redirect the customer back to the storefront. Whichever arrives first will complete the cart and create a new order in Medusa.
To make YooKassa available as a payment method on the storefront checkout page, you must add its configuration to the payment provider mapping in your storefront's constants file. This mapping determines how each payment provider is displayed in the UI.
export const paymentInfoMap: Record<string,{ title: string; icon: React.ReactNode }> = {// ... other providerspp_yookassa_yookassa: {title: "YooKassa",icon: <CreditCard />,}}// Helper to check if a provider is YooKassaexport const isYookassa = (providerId?: string) => {return providerId?.startsWith("pp_yookassa")}
You extend the object to include a entry. This entry defines the title and the icon that will be shown for YooKassa on the checkout page.
The helper function checks whether a given belongs to YooKassa. This is useful when rendering provider-specific UI-components.
When integrating YooKassa, you need to adjust your cookie policy to allow cross-domain payment redirects. Some payment providers require more permissive cookie settings so the payment session can be preserved when the customer is redirected back to the storefront.
Open and update the cookie configuration as follows:
export const setCartId = async (cartId: string) => {cookies.set("_medusa_cart_id", cartId, {// ... other cookie settingssameSite: "lax", // Changed from "strict" for payment redirects})}
This helper function stores the cart ID in a cookie named .
The option is set to instead of . This change ensures the cookie is sent with cross-site requests during the YooKassa redirect flow, preventing the payment session from being lost.
To redirect a customer to YooKassa, the payment session must be properly initialized with the required parameters, including the return URL for the post-payment callback and the shopping cart for generating receipts.
Open and update the payment initialization logic to include YooKassa's redirect URL and cart:
await initiatePaymentSession(cart, {provider_id: selectedPaymentMethod,data: {confirmation: {type: "redirect",return_url: `${getBaseURL()}/api/capture-payment/${cart?.id}?country_code=${countryCode}`},cart: cart}})
When initiating a payment for YooKassa, pass a object with and a . YooKassa will later provide a to which the customer should be redirected.
The points back to your storefront's capture endpoint and is used for both successful and failed payment attempts.
The object is included to build a compliant receipt in accordance with Federal Law No. 54.
Medusa storefront requires a dedicated payment button component for each payment provider to handle the checkout flow after the customer confirms his order. This component leverages the payment session data and navigates the customer to the YooKassa payment page.
Open and add the following code:
const PaymentButton: React.FC<PaymentButtonProps> = ({cart,"data-testid\": dataTestId,}) => {// ...switch (true) {// ... other casescase isYookassa(paymentSession?.provider_id):return (<YookassaPaymentButtonnotReady={notReady}cart={cart}data-testid={dataTestId}/>)default:return <Button disabled>Select a payment method</Button>}}// ... other payment button's componentsconst YookassaPaymentButton = ({cart,notReady}: {cart: HttpTypes.StoreCartnotReady: boolean"data-testid"?: string}) => {const [submitting, setSubmitting] = useState(false)const [errorMessage, setErrorMessage] = useState<string | null>(null)const router = useRouter()const paymentSession = cart.payment_collection?.payment_sessions?.find(session => session.provider_id === "pp_yookassa_yookassa")const handlePayment = () => {setSubmitting(true)const confirmation = paymentSession?.data?.confirmation as IConfirmationif (confirmation?.confirmation_url) {router.push(confirmation.confirmation_url)}}return (<><Buttondisabled={notReady}isLoading={submitting}onClick={handlePayment}size="large"data-testid="submit-order-button">Place an order and pay</Button><ErrorMessageerror={errorMessage}data-testid="manual-payment-error-message"/></>)}
This component locates the YooKassa in the active cart and extracts . When the Place an order and pay button is clicked, the customer is redirected to this URL to complete the payment on the YooKassa page.
If the is missing, the component displays an error message instead of proceeding. The state provides visual feedback while the redirection is being prepared.
The parent uses to determine whether to render the for the current session; otherwise, it shows a disabled Select a payment method button.
After the customer completes payment on the YooKassa page, he is redirected back to the storefront. You need an API route to handle this callback, verify the payment status, and complete the cart.
Create the file with the following content:
import { NextRequest, NextResponse } from "next/server"import { revalidateTag } from "next/cache"import {getCacheTag,getAuthHeaders,removeCartId} from "@lib/data/cookies"import { sdk } from "@lib/config"import { placeOrder } from "@lib/data/cart"type Params = Promise<{ cartId: string }>export async function GET(req: NextRequest, { params }: { params: Params }) {const { cartId } = await paramsconst { origin, searchParams } = req.nextUrlconst countryCode = searchParams.get("country_code") || ""const headers = { ...(await getAuthHeaders()) }// Retrieve fresh cart valuesconst cartCacheTag = await getCacheTag("carts")revalidateTag(cartCacheTag)const { cart } = await sdk.store.cart.retrieve(cartId, {fields: "id, order_link.order_id"},headers)if (!cart) {return NextResponse.redirect(`${origin}/${countryCode}`)}const orderId = (cart as unknown as Record<string, any>).order_link?.order_idif (!orderId) {await placeOrder(cartId)// Fail when payment not authorizedreturn NextResponse.redirect(`${origin}/${countryCode}/checkout?step=review&error=payment_failed`)}const orderCacheTag = await getCacheTag("orders")revalidateTag(orderCacheTag)removeCartId()return NextResponse.redirect(`${origin}/${countryCode}/order/${orderId}/confirmed`)}
This API route handles the redirect from YooKassa after a payment attempt. It retrieves the latest state of the cart to ensure any updates made during payment are reflected.
If the cart does not contain an associated order ID, the route tries to place an order. If successful, the customer is redirected to the order confirmation page. If any error happens during cart completion, the customer is redirected back to the checkout page indicating an error, and he can proceed the checkout once again.
When the payment is successful, the route revalidates the cached cart and order data, removes the cart cookie, and redirects the customer to the order confirmation page. This ensures a consistent post-payment experience while keeping storefront data up to date.
You can refer to the modifications made in the Medusa Next.js Starter Template, which are located in the directory.
The complete integration diff can be viewed in the comparison page, open the tab, and explore the differences under the directory. Or run diff in the terminal:
git clone https://github.com/sergkoudi/medusa-payment-yookassacd medusa-payment-yookassagit diff v0.0.3...main -- examples/medusa-storefront