Оплата на React
@checkout-kit/react подключает платёжный движок к приложению на React 19. Он не заменяет API вашего сервера и ваши поля формы. Необязательный UI-кит даёт готовое оформление, но ваша собственная дизайн-система так же хорошо работает с теми же хуками.
Установка из npm
npm install @checkout-kit/core @checkout-kit/runtime-browser @checkout-kit/provider-paypal @checkout-kit/react @checkout-kit/ui react@^19 react-dom@^19Пакет в npm · Все варианты установки
Полный небольшой пример
Скопируйте два файла в свой проект под именами quickstart-engine.ts и quickstart.tsx. Это настоящие файлы из репозитория, их проверяет сборка TypeScript, поэтому они всегда рабочие. Ещё понадобятся маршруты PayPal на вашем сервере. Создайте движок один раз в браузере и запустите приложение этим кодом.
Настройка движка:
import { createCheckout, defineProvider } from '@checkout-kit/core'
import { createBrowserRuntime } from '@checkout-kit/runtime-browser'
import type { PayPalConfig } from '@checkout-kit/provider-paypal'
/** Call once in the browser. baseUrl points at the merchant's PayPal API. */
export const createPayPalCheckout = (baseUrl = '/api', returnPath = '/checkout/return') => {
const runtime = createBrowserRuntime({
returnPath,
security: { allowInsecureLocalhost: import.meta.env.DEV },
})
const engine = createCheckout({
providers: [
defineProvider({
id: 'paypal',
config: { baseUrl, credentials: 'include' } satisfies PayPalConfig,
load: () => import('@checkout-kit/provider-paypal'),
}),
],
runners: runtime.runners,
storage: runtime.storage,
returnUrl: runtime.returnUrl,
defaultProviderId: 'paypal',
})
return {
engine,
runtime,
async pay(planId: string) {
// Recovery retains the original intent/key if the merchant API was unavailable.
const restored = await engine.hydrate(runtime.readReturnParams())
return restored ?? engine.pay({ input: { planId }, instrument: { kind: 'none' } })
},
}
}Экран на React:
import type { CheckoutEngine, PaymentResult } from '@checkout-kit/core'
import { CheckoutProvider, CheckoutRoot, PaymentActionHost, useCheckout } from '@checkout-kit/react'
import { Button, Money, Panel } from '@checkout-kit/ui'
import '@checkout-kit/ui/styles.css'
type Pay = (planId: string) => Promise<PaymentResult>
function Payment({ planId, pay }: { planId: string; pay: Pay }) {
const { engine, phase, intent, error, isBusy, isLocked, isSettled } = useCheckout()
const hasFinalOutcome = intent && ['succeeded', 'declined', 'canceled'].includes(intent.status)
return (
<Panel title="Checkout">
{intent && <Money amount={intent.amount} currency={intent.currency} />}
<p role="status">{phase}</p>
{error && <p role="alert">{error.message}</p>}
<Button busy={isBusy} disabled={isLocked} onClick={() => void pay(planId)}>
Pay with PayPal
</Button>
{phase === 'failed' && (
<Button variant="secondary" onClick={() => void pay(planId)}>
Retry the same payment
</Button>
)}
{isSettled && hasFinalOutcome && (
<Button variant="secondary" onClick={() => engine.reset()}>
Start over
</Button>
)}
<PaymentActionHost />
</Panel>
)
}
export function PayPalCheckout({
engine,
planId,
pay,
}: {
engine: CheckoutEngine
planId: string
pay: Pay
}) {
return (
<CheckoutProvider engine={engine}>
<CheckoutRoot theme="auto">
<Payment planId={planId} pay={pay} />
</CheckoutRoot>
</CheckoutProvider>
)
}Как работает пример:
- цену назначает сервер по
planId, аMoneyтолько показывает её; PaymentActionHostвыполняет шаг подтверждения в PayPal;- если результат неясен (например, оборвалась сеть), кнопка «попробовать снова» повторяет восстановление или ту же попытку, поэтому деньги не спишутся дважды;
- кнопка «начать заново» появляется только тогда, когда у платежа подтверждён окончательный статус.
Сколько живёт движок
Создавайте один движок на одну оплату и вне отрисовки (не в теле компонента). Если создавать новый движок при каждой отрисовке, теряются подписки и платёж, который уже идёт.
Пока шаг выполняется, PaymentActionHost должен оставаться на экране. Если он пропадёт, шаг остановится.
CheckoutProviderпередаёт движок хукам внутри себя.CheckoutRootдобавляет CSS-тему, плотность и атрибуты платформы.
Если вместо @checkout-kit/ui у вас свой интерфейс, нужен только CheckoutProvider.
Читайте только нужное состояние
| Хук | Что возвращает |
|---|---|
useCheckoutEngine() | Команды движка. Компонент не перерисовывается при смене состояния. |
useCheckoutSnapshot() | Текущее состояние: фаза, платёж, шаг, ошибка, провайдер и его возможности. |
useCheckout() | Состояние плюс движок и флаги isBusy, isSettled и isLocked. |
useCheckoutSelector(selector, isEqual?) | Только выбранную вами часть. Перерисовка — только когда эта часть меняется. Можно передать свою функцию сравнения. |
usePaymentState({ isDirty, isValidating }) | Состояние, которое учитывает и вашу форму (изменена, идёт проверка). |
import { useCheckoutSelector } from '@checkout-kit/react'
const total = useCheckoutSelector(
(s) => ({ amount: s.intent?.amount, currency: s.intent?.currency }),
(a, b) => a.amount === b.amount && a.currency === b.currency,
)Правила и подводные камни:
- селектор должен только читать данные, без побочных эффектов;
isSettledзначит «движок остановился». Сюда входят и ошибки, и неясный результат. Это не значит «оплачено»;isLockedтоже равенtrueв окончательных состояниях. Не отключайте кнопку повтора навсегда только по нему — добавьте своё условие для восстановления или новой попытки.
Дополнительные шаги и восстановление
PaymentActionHostсам запускает шаг (autoRunвключён по умолчанию).surfaceпозволяет выбрать, где показывать шаг, если провайдер это поддерживает.onSettled— только для интерфейса: показать сообщение, перейти на другую страницу. Выдача заказа — задача сервера.- При
autoRun={false}вызывайтеengine.runPendingActionсами и передавайте место для показа (mount).
Компонент не даёт запустить один и тот же шаг дважды, даже если провайдер присылает несколько шагов подряд с одинаковым id.
Компоненты React и UI можно рендерить на сервере (SSR), но браузерный runtime создавайте только в браузере. См. работа в браузере и решение проблем.