Практические вопросы
English version: adopting.md
Вопросы, которые команда обычно задаёт, прежде чем добавить оплату в своё приложение: сколько весит, работает ли с серверным рендерингом, как работает с программами экранного доступа, можно ли перевести и какие браузеры поддерживаются.
Сколько весит
Замер от 2026-10-09: главная точка входа каждого пакета собрана и минифицирована через esbuild, потом сжата gzip. Учтены все экспорты точки входа. Зависимости (React, core, SDK провайдеров) в строки не входят. Ваш сборщик может выбросить неиспользуемые экспорты (tree-shaking), поэтому измеряйте и свою итоговую сборку.
| Пакет | gzip |
|---|---|
@checkout-kit/core | 6.8 KiB |
@checkout-kit/runtime-browser | 3.2 KiB |
@checkout-kit/react | 1.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-bridge | 2.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". Озвучивание каждой смены фазы оставлено вашему приложению, потому что только оно знает, где в вёрстке этому место.
Языки
Любой текст, который набор показывает на экране, можно заменить:
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.randomUUID | id по умолчанию в движке | 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и один эффект. - Нет перевода сообщений провайдера — см. выше.
- Нет работы без сети. Для платежа всегда нужен сервер.