Skip to content

Практические вопросы ​

English version: adopting.md

Вопросы, которые команда обычно задаёт, прежде чем добавить оплату в своё приложение: сколько весит, работает ли с серверным рендерингом, как работает с программами экранного доступа, можно ли перевести и какие браузеры поддерживаются.

Сколько весит ​

Замер от 2026-10-09: главная точка входа каждого пакета собрана и минифицирована через esbuild, потом сжата gzip. Учтены все экспорты точки входа. Зависимости (React, core, SDK провайдеров) в строки не входят. Ваш сборщик может выбросить неиспользуемые экспорты (tree-shaking), поэтому измеряйте и свою итоговую сборку.

Пакетgzip
@checkout-kit/core6.8 KiB
@checkout-kit/runtime-browser3.2 KiB
@checkout-kit/react1.0 KiB
@checkout-kit/ui (корневой JS)9.1 KiB
@checkout-kit/ui/styles.css (все импорты)6.2 KiB
Пакет провайдера1.0–1.9 KiB
@checkout-kit/webview-bridge2.1 KiB

Выбранный провайдер подгружается динамическим импортом, только когда он нужен. React, Stripe.js, Adyen Web и библиотеки для QR-кодов в состав набора не входят. См. список пакетов.

Рендеринг на сервере ​

В @checkout-kit/core нет кода для браузера, и npm run purity останавливает сборку с ошибкой, если он туда попадёт. @checkout-kit/react читает движок через useSyncExternalStore с серверным снимком состояния. Поэтому компонент, который показывает фазу или сумму, без проблем рендерится на сервере.

Браузер нужен только в двух местах:

  • createBrowserRuntime() читает window.location, чтобы собрать адрес возврата. Создавайте движок в браузере: либо на верхнем уровне файла, который импортирует только клиент, либо внутри эффекта.
  • <PaymentActionHost/> работает внутри эффекта. На сервере он рендерит пустой <div> и ничего не запускает.

Передавать состояние с сервера на клиент не нужно. Платёж в процессе хранится в session storage, и hydrate() подхватывает его после перенаправления. Вызывайте hydrate() в загрузчике маршрута (route loader), а не в эффекте — см. архитектуру.

Программы экранного доступа и клавиатура ​

О чём заботится сам набор:

  • У фреймов есть заголовок. Задайте понятный покупателю, например redirect: { frameTitle: () => 'Проверка банком' }. Программа экранного доступа читает этот заголовок, когда фокус переходит на страницу банка.
  • Раннер, который показывает код, помечает область кнопки копирования как «вежливую» живую область (polite live region), поэтому «Скопировано» зачитывается вслух. Для картинки QR-кода можно задать alt-текст.
  • Каждое поле, которое рисует набор, проходит через Field. У него есть подпись (label), id, а подсказка и ошибка связаны через aria-describedby. Ошибки полей озвучиваются вежливо; срочным сообщением (alert) бывает только ошибка платежа. См. UI-кит.
  • Всё сделано на настоящем HTML: кнопка — это <button>, ссылка — это <a>. Вкладки провайдеров используют роли вкладок, компоненты статуса и ошибок — живые области. Собирая свой интерфейс из компонентов, сохраняйте их подписи, описания и атрибуты состояния.

Что нужно сделать вам, потому что только вы знаете свою вёрстку:

  • Озвучивать состояние платежа. Фаза есть в снимке состояния. Вывести её в область aria-live="polite" — несколько строк, а без этого пользователь программы экранного доступа ничего не услышит. Набор не может сделать это за вас, потому что не управляет вашей вёрсткой.
  • Осознанно переводить фокус. Когда открывается шаг или платёж завершается, решите, куда должен перейти фокус.
  • Показывать ошибку рядом с полем. PaymentError.message написан для покупателя. Показывайте его там, куда смотрит покупатель, с role="alert".

В демо сделаны два последних пункта: ошибки показываются через ErrorText с role="alert", а ожидание имеет role="status". Озвучивание каждой смены фазы оставлено вашему приложению, потому что только оно знает, где в вёрстке этому место.

Языки ​

Любой текст, который набор показывает на экране, можно заменить:

ts
createBrowserRuntime({
  returnPath: '/payment/return',
  redirect: { frameTitle: () => t('checkout.bankAuthentication') },
  collectFields: { frameTitle: () => t('checkout.cardDetails') },
  display: {
    text: {
      copy: t('common.copy'),
      copied: t('common.copied'),
      openApp: t('checkout.openBankApp'),
    },
  },
})

С сообщениями, которые приходят от платежа, всё иначе. PaymentError.message — текст провайдера, обычно от банка покупателя. Не переводите его сами: «на карте недостаточно средств» сказал банк, и другая формулировка может изменить смысл.

Если нужны переведённые сообщения, выбирайте текст по PaymentError.code, а для незнакомого кода показывайте message. Именно для этого и нужен code, и поэтому ошибки здесь — обычные данные, а не классы исключений.

intent.amount хранится в минимальных единицах валюты (например, в копейках или центах). Чтобы показать правильное число знаков после запятой для каждой валюты, используйте formatMoney из core или Money из UI. См. руководство по UI.

Браузеры ​

Пакеты только в формате ESM, сборки CommonJS нет. Они публикуются в том же синтаксисе JavaScript, в котором написаны, поэтому до какой версии их транспилировать, решает ваш сборщик.

Самому набору при работе нужно очень немного современных возможностей браузера:

ВозможностьДля чегоПоддерживается с
crypto.randomUUIDid по умолчанию в движкеChrome 92, Firefox 95, Safari 15.4
AbortSignal.anyодновременное ожидание шага и статусаChrome 116, Firefox 124, Safari 17.4
Element.replaceChildrenочистка места для показаChrome 86, Firefox 78, Safari 14

AbortSignal.any на два года новее остальных, поэтому движок использует его, только если он есть, а иначе подключает свои обработчики. Телефон, отстающий на две версии, всё равно сможет заплатить. Минимальную версию тогда задают CSS container queries — Safari 16. crypto.randomUUID можно заменить: createCheckout({ uuid: () => myOwnId() }). Ему ещё нужен защищённый контекст (HTTPS), который у страницы оплаты и так есть.

@property и @starting-style используются там, где помогают, и игнорируются, где не поддерживаются. Без них теряется только типизированная CSS-переменная и плавное появление.

Если найдёте что-то ещё, что повышает минимальную версию браузера, сообщите об этом как об ошибке.

Два поведения браузеров влияют на настоящие платежи:

  • Блокировка сторонних cookie ломает некоторые страницы банков внутри фрейма. Если фрейм проверки банка в Safari остаётся пустым, выполните этот шаг во всём окне: runPendingAction({ surface: 'top' }). Плагин при этом менять не нужно.
  • Session storage — то, что переживает перенаправление всей страницы. В приватном окне он работает. Если данные сайта полностью запрещены — не работает, и hydrate() возвращает null. Поэтому ваш API должен позволять прочитать платёж и по его id.

Что не поддерживается ​

  • Нет CommonJS. Когда пакет публикуется в двух форматах, instanceof ошибается между двумя копиями одного класса, а платёжной библиотеке это недопустимо. Node 22 умеет require() для ESM-модулей.
  • Нет React ниже 19. Пакет для React настолько маленький, что его легко переписать под другой фреймворк: это один useSyncExternalStore и один эффект.
  • Нет перевода сообщений провайдера — см. выше.
  • Нет работы без сети. Для платежа всегда нужен сервер.

Apache-2.0 · Headless-движок, адаптеры провайдеров и запускаемые примеры.