Command Palette

Search for a command to run...

Интеграция Robokassa в витрину магазина

Для интеграции платёжного провайдера Robokassa в storefront Medusa DTC Starter от Gorgo начните с добавления необходимых UI-компонентов. Таким образом провайдер будет отображаться на странице оформления заказа наряду с другими доступными методами оплаты.

Когда покупатель выбирает Robokassa, оформление заказа инициализирует платёжную сессию, а провайдер создаёт её на стороне Robokassa и возвращает адрес для оплаты. Кнопка оплаты отправляет покупателя на эту страницу, где он сможет выбрать предпочтительный способ оплаты.

После завершения оплаты Robokassa одновременно отправляет вебхук и перенаправляет покупателя обратно в витрину. То событие, которое придёт первым, завершит корзину и создаст новый заказ в Medusa.

Для витрины на Next.js необходимо внести следующие изменения:

Шаг 1: Настройте платёжный провайдер

Чтобы сделать Robokassa доступным в качестве способа оплаты на странице оформления заказа витрины магазина, необходимо добавить её конфигурацию в маппинг платёжных провайдеров в файле с константами вашей витрины. Этот маппинг определяет, как каждый провайдер отображается в интерфейсе.

Откройте и добавьте следующий код:

Структура проекта Medusa Storefront после обновления файла с константами

src/lib/constants.tsx
1export type PaymentInfo = {
2 title: string
3 subtitle?: string
4 icon: React.JSX.Element
5}
6
7export const paymentInfoMap: Record<string, PaymentInfo> = {
8 // ...
9 pp_robokassa_robokassa: {
10 title: "Robokassa",
11 subtitle: "Cards, SBP",
12 icon: <Robokassa />,
13 },
14 // ...
15}
16
17// ...
18
19export const isRobokassa = (providerId?: string) => {
20 return providerId?.startsWith("pp_robokassa")
21}

Вы расширяете объект , добавляя в него запись . Эта запись определяет заголовок, подпись и иконку, которые будут отображаться для Robokassa на странице оформления заказа, а также служит резервным значением, когда для локали нет перевода.

Вспомогательная функция проверяет, принадлежит ли переданный к Robokassa. Она используется диспетчером ниже и функцией сборки данных платёжной сессии из следующего шага.

Добавьте компонент для Robokassa:

src/modules/common/icons/robokassa.tsx
1import type React from "react"
2
3import type { IconProps } from "types/icon"
4
5const Robokassa: React.FC<IconProps> = ({
6 size = "16",
7 color: _color,
8 ...attributes
9}) => {
10 return (
11 <svg
12 width={size}
13 height={size}
14 viewBox="54 129 404 254"
15 role="img"
16 fill="none"
17 xmlns="http://www.w3.org/2000/svg"
18 {...attributes}
19 >
20 <title>Robokassa icon</title>
21 <path d="M239 129L54 383H274L458 129H239Z" fill="#333339" />
22 </svg>
23 )
24}
25
26export default Robokassa

Этот компонент передаётся как в записи . Он отображает иконку способа оплаты Robokassa на странице оформления заказа.


Шаг 2: Инициализируйте платёжную сессию

Чтобы перенаправить покупателя в Robokassa, платёжная сессия должна быть корректно инициализирована с необходимыми параметрами, включая адрес возврата после оплаты и корзину для формирования онлайн-чеков.

Откройте тот же файл и добавьте вспомогательную функцию активной локали, адрес возврата и функцию сборки данных сессии:

Структура проекта Medusa Storefront после обновления файла с константами

src/lib/constants.tsx
1/**
2 * Локаль, в которой отображалось оформление заказа. `<html lang>` хранит её,
3 * поэтому страница оплаты и возврат с неё остаются на том языке, на котором
4 * покупатель ушёл из магазина.
5 */
6const activeLocale = () =>
7 typeof document === "undefined"
8 ? defaultLocale
9 : document.documentElement.lang || defaultLocale
10
11const paymentReturnUrl = (providerId: string, cartId: string) => {
12 const origin =
13 typeof window === "undefined" ? getBaseURL() : window.location.origin
14
15 return `${origin}/api/payment-return?provider=${providerId}&cart_id=${cartId}&locale=${activeLocale()}`
16}
17
18const paymentSessionDataBuilders: Array<{
19 test: (providerId?: string) => boolean | undefined
20 isReady?: (cart: HttpTypes.StoreCart) => boolean
21 build: (cart: HttpTypes.StoreCart) => Record<string, unknown>
22}> = [
23 {
24 test: (providerId) => isRobokassa(providerId),
25 // Robokassa формирует чек из корзины и берёт из неё адрес покупателя.
26 // Инициализация сессии до того, как оформление заказа собрало эти данные,
27 // создаёт платёж без чека.
28 isReady: (cart) => Boolean(cart.email && cart.shipping_address),
29 build: (cart) => {
30 const returnUrl = paymentReturnUrl("robokassa", cart.id)
31
32 return {
33 SuccessUrl2: returnUrl,
34 SuccessUrl2Method: "GET",
35 FailUrl2: returnUrl,
36 FailUrl2Method: "GET",
37 EMail: cart.email,
38 // Собственная страница оплаты Robokassa говорит по-русски или по-английски.
39 Culture: activeLocale() === "ru" ? "ru" : "en",
40 cart,
41 }
42 },
43 },
44]

Robokassa нужна активная локаль не только для адреса возврата, поэтому она вынесена в отдельную функцию , а не встроена напрямую. передаёт и , оба указывающие на один и тот же адрес возврата, их , email покупателя в и для языка, на котором откроется собственная страница оплаты Robokassa.

Оба адреса указывают на API-роут вашей витрины и используются как для успешных, так и для неудачных попыток оплаты.

Объект включается в данные инициализации для формирования чека в соответствии с Федеральным законом № 54.

удерживает сессию, пока в корзине не появятся email и адрес доставки, поскольку для чека нужен адрес покупателя.


Шаг 3: Создайте кнопку оплаты

В витрине для каждого платёжного провайдера необходим отдельный компонент кнопки оплаты. Используя данные платёжной сессии, он перенаправляет покупателя на страницу оплаты Robokassa. Сам заказ оформляется позже, когда покупатель вернётся через , либо раньше по вебхуку, если тот придёт первым.

Создайте файл и добавьте следующий код:

Структура проекта Medusa Storefront после создания файла для кнопки оплаты Robokassa

src/modules/checkout/components/payment-button/providers/robokassa.tsx
1"use client"
2
3import type { HttpTypes } from "@medusajs/types"
4import { useTranslations } from "next-intl"
5import { useState } from "react"
6import { PaymentButtonShell, type PaymentProviderButtonProps } from "../shared"
7
8const paymentUrl = (cart: HttpTypes.StoreCart) => {
9 const session = cart.payment_collection?.payment_sessions?.find((s) =>
10 s.provider_id.startsWith("pp_robokassa")
11 )
12
13 return (session?.data as { paymentUrl?: string } | undefined)?.paymentUrl
14}
15
16/**
17 * Robokassa принимает оплату на собственной странице. Сессия, созданная
18 * оформлением заказа, хранит подписанный адрес, куда отправить покупателя, а
19 * заказ оформляется, когда он вернётся через `/api/payment-return`, либо по
20 * вебхуку, если тот придёт раньше.
21 */
22const RobokassaPaymentButton = ({
23 cart,
24 notReady,
25 label,
26 cartUpdating,
27 "data-testid": dataTestId,
28}: PaymentProviderButtonProps) => {
29 const t = useTranslations("Errors")
30 const [submitting, setSubmitting] = useState(false)
31 const [errorMessage, setErrorMessage] = useState<string | null>(null)
32
33 const handlePayment = () => {
34 const url = paymentUrl(cart)
35
36 if (!url) {
37 setErrorMessage(t("paymentFailed"))
38 return
39 }
40
41 setSubmitting(true)
42 window.location.assign(url)
43 }
44
45 return (
46 <PaymentButtonShell
47 label={label}
48 onClick={handlePayment}
49 disabled={notReady}
50 loading={submitting || cartUpdating}
51 errorMessage={errorMessage}
52 errorTestId="robokassa-payment-error-message"
53 data-testid={dataTestId ?? "submit-order-button"}
54 />
55 )
56}
57
58export default RobokassaPaymentButton

Этот компонент находит Robokassa в активной корзине и извлекает значение . Когда покупатель нажимает кнопку оплаты, он перенаправляется на этот URL для завершения оплаты на странице Robokassa.

Если отсутствует, кнопка показывает сообщение об ошибке вместо перехода.


Шаг 4: Зарегистрируйте кнопку оплаты

Импортируйте кнопку и в , затем добавьте для неё в :

Структура проекта Medusa Storefront после обновления файла для компонента кнопки оплаты

src/modules/checkout/components/payment-button/index.tsx
1import {
2 isManual,
3 isPaymentSessionReady,
4 isRobokassa,
5 isStripeLike,
6} from "@lib/constants"
7// ...
8import RobokassaPaymentButton from "./providers/robokassa"
9
10// ...
11
12 switch (true) {
13 // ...
14 case isRobokassa(activePaymentMethod):
15 return <RobokassaPaymentButton {...providerProps} />
16 default:
17 // ...
18 }

Родительский компонент использует , чтобы определить, нужно ли отобразить для текущей сессии, иначе выполнение переходит к следующей ветке.


Шаг 5: Обработайте возврат с оплаты

После того как покупатель завершает оплату на странице Robokassa, он перенаправляется обратно на витрину магазина. Robokassa сообщает статус платежа и через это перенаправление, и через вебхук, поэтому общий API-роут проверяет, есть ли у корзины заказ, прежде чем оформлять его самому.

Откройте со следующим содержимым:

Структура проекта Medusa Storefront после создания файла для API route

src/app/api/payment-return/route.ts
1type ReturnContext = {
2 cartId: string
3 locale: string
4 origin: string
5}
6
7/**
8 * Обрабатывает возврат от провайдера, который принимает оплату на собственной
9 * странице и возвращает покупателя только с идентификатором корзины —
10 * Robokassa в их числе.
11 *
12 * Вебхук провайдера и этот редирект соревнуются друг с другом, и какой из них
13 * ни пришёл бы первым, он оформляет заказ, поэтому перед оформлением
14 * проверяется, нет ли у корзины уже заказа.
15 */
16async function redirectProviderReturn({
17 cartId,
18 locale,
19 origin,
20}: ReturnContext) {
21 const cart = await sdk.client
22 .fetch<HttpTypes.StoreCartResponse>(`/store/carts/${cartId}`, {
23 method: "GET",
24 query: { fields: "id,order_link.order_id" },
25 headers: { ...(await getAuthHeaders()) },
26 cache: "no-store",
27 })
28 .then(({ cart }) => cart)
29 .catch(() => null)
30
31 if (!cart) {
32 return NextResponse.redirect(
33 `${origin}/${locale}/cart?error=payment_failed`
34 )
35 }
36
37 const orderId = (cart as unknown as { order_link?: { order_id?: string } })
38 .order_link?.order_id
39
40 if (orderId) {
41 const orderCacheTag = await getCacheTag("orders")
42 revalidateTag(orderCacheTag)
43 await removeCartId()
44
45 return NextResponse.redirect(
46 `${origin}/${locale}/order/${orderId}/confirmed`
47 )
48 }
49
50 // Покупатель отменил оплату, либо она была отклонена, поэтому он возвращается
51 // на страницу оформления заказа с нетронутой корзиной и может выбрать способ
52 // оплаты заново.
53 await setCartId(cartId)
54
55 try {
56 await placeOrder(cartId)
57 } catch (error) {
58 unstable_rethrow(error)
59
60 return NextResponse.redirect(
61 `${origin}/${locale}/checkout?error=payment_failed`
62 )
63 }
64
65 return NextResponse.redirect(
66 `${origin}/${locale}/checkout?error=payment_failed`
67 )
68}

Затем в функции направьте возврат Robokassa в :

src/app/api/payment-return/route.ts
1if (provider === "robokassa") {
2 return redirectProviderReturn({ cartId, locale, origin })
3 }

Этот API-роут обрабатывает возврат от Robokassa после попытки оплаты. Он всегда получает актуальное состояние корзины без кэша, чтобы не пропустить изменения, внесённые во время оплаты.

Если у корзины уже есть заказ, значит подтверждение уже пришло по вебхуку. API-роут обновляет кэш заказа, удаляет cookie корзины и отправляет покупателя на страницу подтверждения. Это гарантирует корректное завершение платёжного процесса и сохранение актуальных данных в витрине.

Иначе API-роут пытается оформить заказ сам. В случае успеха покупатель оказывается на странице подтверждения заказа. Если же возникает ошибка, покупатель возвращается на оформление заказа с и может выбрать способ оплаты заново.


Шаг 6: Добавьте подписи провайдера в локали витрины

Добавьте в и в каждую другую локаль под :

messages/ru.json
1{
2 "PaymentMethods": {
3 // ...
4 "pp_robokassa_robokassa": {
5 "title": "Робокасса",
6 "subtitle": "Карты, СБП"
7 }
8 }
9}

Если для локали нет записи, используются английские и из в Шаге 1.


Полный пример кода интеграции в витрину

Полный код витрины находится в , собранной на основе Medusa DTC Starter от Gorgo. В README примера перечислены все файлы, из которых состоит интеграция, включая бэкенд.

Изменено 23 сентября 2026 г.·Редактировать страницу