# Embedded Checkout (/uk/online/integration/embedded_checkout)



Розмістіть захищені поля картки у власному оформленні замовлення. Форма та кнопка надсилання належать вам; Tapaya виконує валідацію
картки, токенізацію та 3-D Secure. Вбудовані платежі працюють для схвалених мерчантів, а для тестування доступна пісочниця
(sandbox). Цей посібник використовує Tapaya.js **0.2.0** і Node.js SDK **0.3.0**.

<EmbeddedPaymentFlowDiagram />

<Callout title="Способи оплати" type="info">
  Embedded Checkout підтримує картки, Apple Pay і Google Pay. Для карток використовуйте власну кнопку оплати, а для Apple Pay і Google Pay
  офіційні кнопки гаманців. Див. [Додавання Apple Pay і Google Pay](#add-apple-pay-and-google-pay).
</Callout>

## Перш ніж почати [#before-you-start]

### Встановіть SDK [#install-the-sdk]

Один пакет містить серверний клієнт, браузерний SDK і React-хук:

<CodeBlockTabs defaultValue="npm" groupId="package-manager">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      Bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```sh
    npm install @tapayadot/checkout
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```sh
    pnpm add @tapayadot/checkout
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```sh
    bun add @tapayadot/checkout
    ```
  </CodeBlockTab>
</CodeBlockTabs>

| Імпорт                       | Де використовувати                                                                                                      |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `@tapayadot/checkout`        | Лише на вашому сервері. Імпорт у браузерній збірці спричиняє виняток, тож ваш API-ключ ніколи не потрапляє на сторінку. |
| `@tapayadot/checkout/client` | На вашій сторінці оформлення замовлення.                                                                                |
| `@tapayadot/checkout/react`  | На вашій сторінці оформлення замовлення, якщо ви використовуєте React.                                                  |

### Налаштуйте свій сервер [#configure-your-server]

Вбудованим платежам URL повернення не потрібні. Наведені нижче серверні приклади мають версії &#x2A;*HTTP (fetch)**, **Node.js SDK**
і **cURL**, які всі викликають той самий API мерчанта й не змінюють поля картки в браузері.

|                     | Node.js SDK                                                                                                                                                  | HTTP (fetch) і cURL                                                                                                   |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
| **Ключ**            | `TAPAYA_SECRET_KEY`, який зчитує `new Tapaya()`                                                                                                              | `TAPAYA_CHECKOUT_API_KEY`                                                                                             |
| **Середовище**      | `TAPAYA_ENVIRONMENT=sandbox` або `production`. Без цієї змінної ключ `tapaya_live_` вибирає робоче середовище, а інші ключі типово використовують пісочницю. | `TAPAYA_CHECKOUT_API_URL`: `https://api.sandbox.tapaya.com` або `https://api.tapaya.com`                              |
| **Режим платформи** | Передавайте `merchantToken` замовлення в кожен виклик або задайте його через `new Tapaya(key, { merchantToken })`                                            | Надсилайте `X-Tapaya-Merchant-Token` у кожному запиті. Допоміжна функція fetch зчитує його з `TAPAYA_MERCHANT_TOKEN`. |

Checkout-ключу мерчанта більше нічого не потрібно. У режимі платформи використовуйте Secret key організації, а бекенд,
який обслуговує кількох мерчантів, має брати токен з автентифікованого замовлення. Див. [автентифікацію
Checkout](/online/integration/api_integration#authentication). Client secret видається окремо для кожного платежу.

Пройдіть [онбординг](/merchants/onboarding) з онлайн-платежами та перевірте в
[налаштуваннях Checkout](/online/integration/checkout_settings), що мерчант має статус **Ready**.

### Дозвольте Tapaya у своїй CSP [#allow-tapaya-in-your-csp]

Завантажувач отримує Tapaya.js **0.2.0** з `https://js.tapaya.com/checkout/0.2.0/checkout.js`; версія браузерного скрипта
не залежить від версії Node.js SDK. Додайте origin ресурсів Tapaya до своєї наявної Content Security
Policy:

```text
script-src https://js.tapaya.com
frame-src https://js.tapaya.com
```

Браузер отримує publishable key і client secret конкретного платежу. Ваш секретний API-ключ мерчанта
залишається на вашому сервері, а дані картки та скрипти платіжного провайдера залишаються у фреймі Tapaya. Для локального тестування
розмістіть Tapaya.js на власному сервері та передайте його origin завантажувачу, наприклад
`loadTapaya(publishableKey, { apiOrigin, origin: 'http://localhost:4191' })`.

## Як це працює [#how-it-works]

1. Ваша сторінка розміщує поля картки Tapaya та спрямовує їх на ендпоінт на вашому сервері.
2. Під час завантаження сторінки та коли клієнт платить, поля картки викликають цей ендпоінт. Він повертає платіж
   замовлення як `clientSecret`: облікові дані для одного платежу, якими поля картки підтверджують платіж безпосередньо в
   Tapaya. Код вашої сторінки ніколи його не зчитує.
3. Ваша сторінка стежить за `outcome` платежу. Коли платіж завершено, ваш сервер перевіряє його, перш ніж ви
   виконаєте замовлення.

## Прийом платежу [#accept-a-payment]

<div className="fd-steps [&_h3]:fd-step">
  ### Отримайте налаштування для браузера [#get-the-browser-settings]

  Один раз отримайте налаштування для браузера на своєму сервері та додайте їх до конфігурації свого фронтенду.

  <Tabs defaultValue="fetch" groupId="checkout-server" className="border-0 bg-transparent">
    <TabsList>
      <TabsTrigger value="fetch">
        HTTP (fetch)
      </TabsTrigger>

      <TabsTrigger value="node">
        Node.js SDK
      </TabsTrigger>

      <TabsTrigger value="curl">
        cURL
      </TabsTrigger>
    </TabsList>

    <TabsContent value="fetch">
      Тримайте цю допоміжну функцію на своєму сервері. Вона перевіряє HTTP-помилки, за запитом повертає `null`, якщо платежу замовлення немає,
      і не виконує повторних спроб автоматично.

      ```js title="server/tapaya.js"
      const apiOrigin = new URL(process.env.TAPAYA_CHECKOUT_API_URL).origin;
      const apiKey = process.env.TAPAYA_CHECKOUT_API_KEY;
      const merchantToken = process.env.TAPAYA_MERCHANT_TOKEN;

      export async function paymentRequest(path, { method = 'GET', body, key, allowMissing = false } = {}) {
      	const response = await fetch(new URL(path, apiOrigin), {
      		method,
      		headers: {
      			Authorization: `Bearer ${apiKey}`,
      			...(merchantToken ? { 'X-Tapaya-Merchant-Token': merchantToken } : {}),
      			...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
      			...(key ? { 'Idempotency-Key': key } : {}),
      		},
      		...(body === undefined ? {} : { body: JSON.stringify(body) }),
      		redirect: 'error',
      		cache: 'no-store',
      		signal: AbortSignal.timeout(35000),
      	});
      	if (allowMissing && response.status === 404) return null;
      	if (!response.ok) throw new Error(`Payment request failed (${response.status})`);
      	return response.json();
      }

      const { publishableKey } = await paymentRequest('/merchant/payments/configuration');
      export const config = { publishableKey, apiOrigin };
      ```

      API повертає `publishableKey` і `environment`. Задайте `apiOrigin` на основі URL API, який ви викликали; це не поле
      відповіді з конфігурацією.
    </TabsContent>

    <TabsContent value="node">
      ```ts title="server/config.ts"
      import { Tapaya } from '@tapayadot/checkout'

      const tapaya = new Tapaya()
      const { publishableKey, apiOrigin } = await tapaya.checkout.configuration()
      // Sandbox: publishableKey: 'pk_test_…', apiOrigin: 'https://api.sandbox.tapaya.com'
      // Production: publishableKey: 'pk_live_…', apiOrigin: 'https://api.tapaya.com'
      ```
    </TabsContent>

    <TabsContent value="curl">
      Виконайте це на своєму сервері або в довіреному терміналі. У режимі платформи додайте
      `--header "X-Tapaya-Merchant-Token: $TAPAYA_MERCHANT_TOKEN"` до кожної команди cURL у цьому посібнику.

      ```bash
      curl "$TAPAYA_CHECKOUT_API_URL/merchant/payments/configuration" \
      	--fail-with-body --silent --show-error --max-time 35 \
      	--header "Authorization: Bearer $TAPAYA_CHECKOUT_API_KEY"
      ```

      Використовуйте повернений `publishableKey` і origin вашого `TAPAYA_CHECKOUT_API_URL` як конфігурацію для браузера.
    </TabsContent>
  </Tabs>

  Обидва значення можна безпечно включати у свою сторінку. Publishable key використовують `pk_test_` з тестовими ключами шлюзу та `pk_live_`
  з ключами шлюзу робочого режиму. Завжди передавайте завантажувачу `apiOrigin`, повернений вашим сервером, щоб браузер і сервер
  працювали в тому самому середовищі.

  ### Додайте поля картки [#add-the-card-fields]

  Додайте контейнер до своєї наявної форми оформлення замовлення:

  ```html title="checkout.html"
  <form id="checkout">
  	<!-- Your other checkout inputs go here. -->
  	<div id="card-fields"></div>
  	<button type="submit" disabled>Pay</button>
  	<p id="message" role="status"></p>
  </form>
  ```

  Завантажте Tapaya та розмістіть поля. `prepare` є ендпоінтом, який ви додасте на наступному кроці.

  ```ts title="checkout.ts"
  import { loadTapaya } from '@tapayadot/checkout/client'
  import { config } from './config' // { publishableKey, apiOrigin } from your server

  const form = document.querySelector<HTMLFormElement>('#checkout')!
  const button = form.querySelector<HTMLButtonElement>('button')!
  const message = document.querySelector<HTMLElement>('#message')!
  const tapaya = await loadTapaya(config.publishableKey, { apiOrigin: config.apiOrigin })
  const payment = tapaya.createPayment({
  	prepare: '/api/payments',
  	onChange: ({ canSubmit, outcome }) => {
  		button.disabled = !canSubmit
  		if (outcome === 'completed') location.assign('/checkout/confirmation')
  		if (outcome === 'ended') message.textContent = 'This payment cannot be completed. Start a new checkout.'
  	},
  })
  await payment.mount('#card-fields')
  ```

  Коли поля розміщуються, вони викликають `prepare`, щоб продовжити наявний платіж замовлення. Перезавантаження під час платежу ніколи
  не призводить до повторного стягнення оплати: натомість сторінка показує поточний стан платежу.

  ### Поверніть платіж замовлення [#return-the-orders-payment]

  Підключіть цей обробник до `POST /api/payments`. `getCheckoutOrder()` є функцією вашого власного застосунку: автентифікуйте
  клієнта, перевірте доступ до його замовлення та поверніть збережені `id`, `amount`, `currency` і `status` цього замовлення. Застосуйте
  до маршруту свій звичайний захист від CSRF.

  <Tabs defaultValue="fetch" groupId="checkout-server" className="border-0 bg-transparent">
    <TabsList>
      <TabsTrigger value="fetch">
        HTTP (fetch)
      </TabsTrigger>

      <TabsTrigger value="node">
        Node.js SDK
      </TabsTrigger>

      <TabsTrigger value="curl">
        cURL
      </TabsTrigger>
    </TabsList>

    <TabsContent value="fetch">
      Перш ніж створювати спробу, прочитайте наявний платіж. Повторно використовуйте невизначений платіж, а нову спробу створюйте
      лише після відмови або скасування з `retryAllowed: true`.

      ```js title="server/payments.js"
      import { paymentRequest } from './tapaya.js';
      import { getCheckoutOrder } from './orders.js';

      export async function findPayment(order) {
      	const query = new URLSearchParams({ merchantOrderId: order.id });
      	const payment = await paymentRequest(`/merchant/payments?${query}`, { allowMissing: true });
      	if (payment && (payment.merchantOrderId !== order.id ||
      		payment.amount !== order.amount || payment.currency !== order.currency)) {
      		throw new Error('Payment does not match the order');
      	}
      	return payment;
      }

      export function browserStatus(payment) {
      	if (payment.status === 'successful') return 'completed';
      	if (['declined', 'cancelled'].includes(payment.status) && !payment.retryAllowed) return 'ended';
      	return 'open';
      }

      export async function POST(request) {
      	const order = await getCheckoutOrder(request);
      	const payable = order.status === 'pending_payment';
      	let payment = await findPayment(order);
      	if (payable && (!payment ||
      		(['declined', 'cancelled'].includes(payment.status) && payment.retryAllowed))) {
      		// Derive the same key on retries, including after a lost creation response.
      		let key = `${order.id}:${payment ? `after:${payment.id}` : 'initial'}`;
      		if (/[^\x21-\x7e]/.test(key)) {
      			const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(key));
      			key = `prepare:${Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('')}`;
      		}
      		payment = await paymentRequest('/merchant/payments', {
      			method: 'POST', key,
      			body: { merchantOrderId: order.id, amount: order.amount, currency: order.currency },
      		});
      		if (payment.merchantOrderId !== order.id || payment.amount !== order.amount || payment.currency !== order.currency) {
      			throw new Error('Payment does not match the order');
      		}
      	}
      	if (!payment) return Response.json({ error: 'Order cannot accept payment' }, { status: 409 });
      	return Response.json(
      		{ clientSecret: payable ? payment.clientSecret : null, status: browserStatus(payment) },
      		{ headers: { 'Cache-Control': 'no-store' } },
      	);
      }
      ```

      Використовуйте код валюти у верхньому регістрі та збережений ID замовлення довжиною щонайбільше 200 символів. Не змінюйте дані замовлення
      після початку платежу. У разі тайм-ауту знову прочитайте платіж замовлення, перш ніж вирішувати, чи потрібна нова спроба.
      Ніколи не генеруйте новий ключ лише тому, що відповідь було втрачено.
    </TabsContent>

    <TabsContent value="node">
      ```ts title="server/payments.ts"
      import { Tapaya } from '@tapayadot/checkout'
      import type { PreparedPayment } from '@tapayadot/checkout'
      import { getCheckoutOrder } from './orders'

      const tapaya = new Tapaya()

      export async function POST(request: Request) {
      	const order = await getCheckoutOrder(request)
      	const details = { merchantOrderId: order.id, amount: order.amount, currency: order.currency }
      	// Only an order awaiting payment gets a payable session. Other orders report their payment's outcome.
      	const payable = order.status === 'pending_payment'
      	const session = payable
      		? await tapaya.checkout.sessions.prepare(details)
      		: await tapaya.checkout.sessions.verify(details)
      	if (!session) return Response.json({ error: 'Order cannot accept payment' }, { status: 409 })
      	// The card fields read both values. Your page code never handles them.
      	return Response.json(
      		{ clientSecret: payable ? session.clientSecret : null, status: session.status } satisfies PreparedPayment,
      		{ headers: { 'Cache-Control': 'no-store' } },
      	)
      }
      ```

      `prepare()` повертає платіж замовлення. Він створює першу спробу, повертає невизначену спробу без змін і
      створює наступну спробу після відмови. Він сам формує idempotency key, тому виклик під час кожного завантаження сторінки та
      надсилання ніколи не призводить до другого стягнення оплати. Зафіксуйте суму, щойно почнеться платіж: `prepare()` викидає
      `TapayaOrderMismatchError`, якщо сума або валюта замовлення більше не відповідають його платежу.
    </TabsContent>

    <TabsContent value="curl">
      Спочатку знайдіть платіж замовлення. Ці приклади використовують замовлення на 121,00 EUR, збережене на вашому сервері. `404` означає,
      що платіж не знайдено; інші HTTP-помилки не дають підстав створювати ще одну спробу.

      ```bash
      curl --get "$TAPAYA_CHECKOUT_API_URL/merchant/payments" \
      	--data-urlencode 'merchantOrderId=order-1001' \
      	--fail-with-body --silent --show-error --max-time 35 \
      	--header "Authorization: Bearer $TAPAYA_CHECKOUT_API_KEY"
      ```

      Якщо платежу немає, а замовлення ще може прийняти оплату, створіть першу спробу:

      ```bash
      curl --request POST "$TAPAYA_CHECKOUT_API_URL/merchant/payments" \
      	--fail-with-body --silent --show-error --max-time 35 \
      	--header "Authorization: Bearer $TAPAYA_CHECKOUT_API_KEY" \
      	--header 'Content-Type: application/json' \
      	--header 'Idempotency-Key: order-1001:initial' \
      	--data '{"merchantOrderId":"order-1001","amount":12100,"currency":"EUR"}'
      ```

      Повторно використовуйте наявний невизначений платіж. Після `declined` або `cancelled` створюйте нову спробу лише тоді, коли
      `retryAllowed` має значення `true`. Для цієї спроби повторіть POST з тими самими даними замовлення та ключем
      `order-1001:after:<previous-payment-id>`, замінивши заповнювач поверненим `id`. Використовуйте цей ключ і для
      повторних спроб. Після тайм-ауту знову знайдіть замовлення; не вважайте, що POST не вдався.

      cURL демонструє виклики API. Автентифікований обробник `POST /api/payments` вашого застосунку має повертати
      `{ clientSecret, status }` для полів картки. Зіставте статус API за таблицею нижче й надсилайте
      `clientSecret: null`, коли ваше замовлення більше не приймає оплату. Ніколи не записуйте ці відповіді в спільні журнали термінала.
    </TabsContent>
  </Tabs>

  REST API повертає стани платежу. Node SDK зіставляє їх зі `status`, якого очікують поля картки; інтеграції через fetch і cURL
  застосовують те саме зіставлення у власному ендпоінті prepare:

  | `status` API                                                                                          | `status` ендпоінта prepare |
  | ----------------------------------------------------------------------------------------------------- | -------------------------- |
  | `successful`                                                                                          | `completed`                |
  | `declined` або `cancelled` з `retryAllowed: false`                                                    | `ended`                    |
  | `ready`, `pending`, `action_required` або відмова чи скасування, після яких дозволено повторну спробу | `open`                     |

  <Accordions>
    <Accordion title="Правила для вашого ендпоінта prepare">
      * **Беріть суму зі збереженого замовлення** в цілих мінімальних одиницях, ніколи з браузера. Зафіксуйте суму, щойно
        почнеться платіж.
      * **Відповідайте і для завершених замовлень.** Для замовлення, яке вже оплачено або яке завершило ваше заплановане завдання, ендпоінт однаково повертає
        `status` його платежу, тож перезавантаження показує результат, а не помилку. Таке замовлення ніколи не отримує client secret,
        тож клієнт не може оплатити замовлення, за яке ви більше не приймаєте оплату.
      * **Один успішний платіж на замовлення.** Кожна відмова встановлює `retryAllowed`, тож клієнт може спробувати іншу картку для
        того самого замовлення. Це обмеження спільне з [Hosted Checkout](/online/integration/api_integration#one-payment-per-order).
      * **Захищайте client secret.** Надсилайте його лише клієнту, уповноваженому оплатити це замовлення, і ніколи не записуйте його в журнали.
      * **Очікуйте POST без тіла.** Поля картки надсилають POST на `prepare` у вашому власному origin з cookie і без тіла. Вони відхиляють
        переспрямування й ніколи не повторюють запит автоматично. Для власних заголовків, CSRF-токенів чи тіла запиту передайте замість URL
        функцію, яка повертає той самий JSON: `prepare: async () => (await fetch(…)).json()`.
    </Accordion>
  </Accordions>

  ### Надсилайте оплату зі свого обробника [#submit-from-your-handler]

  Додайте це до того самого скрипта. Ваш обробник валідує вашу форму та викликає `submit()`:

  ```ts title="checkout.ts"
  form.addEventListener('submit', async (event) => {
  	event.preventDefault()
  	if (!form.reportValidity() || !payment.snapshot.canSubmit) return
  	await payment.submit().catch(() => {
  		// The card fields show the reason. The shopper can try again when canSubmit allows it.
  	})
  })
  ```

  `submit()` викликає `prepare` для платежу, потім валідує картку, виконує 3-D Secure і підтверджує платіж
  у Tapaya.

  ### Покажіть результат [#show-the-outcome]

  `onChange` отримує `snapshot` платежу щоразу, коли він змінюється, зокрема коли сторінка завантажується з платежем, який
  уже завершено. Орієнтуйтеся на `snapshot.outcome`:

  | `outcome`   | Що показувати                                                                                                                                  |
  | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
  | `open`      | Кнопку оплати, активну, поки `canSubmit` має значення true. Після відмови стан залишається `open`, і наступний `submit()` отримує нову спробу. |
  | `pending`   | Індикатор виконання. Результат ще невідомий, тож повторно оплатити нічого не можна. SDK автоматично перевіряє статус, поки сторінка видима.    |
  | `completed` | Вашу сторінку підтвердження. Там ваш сервер перевіряє замовлення.                                                                              |
  | `ended`     | Нове оформлення замовлення. Для цього замовлення більше спроб неможливо.                                                                       |

  Викликайте `payment.destroy()`, коли компонент оформлення замовлення демонтується. Для React використовуйте `usePayment()` з
  `@tapayadot/checkout/react`. Він приймає ті самі параметри плюс `publishableKey` і `apiOrigin` і повертає
  `fieldRef` для контейнера, поточний `snapshot` і `submit()`.

  ### Перевіряйте перед виконанням замовлення [#verify-before-fulfilling]

  Результат у браузері призначений лише для відображення. На сторінці підтвердження прочитайте платіж замовлення на своєму сервері й виконуйте замовлення
  лише тоді, коли платіж завершено:

  <Tabs defaultValue="fetch" groupId="checkout-server" className="border-0 bg-transparent">
    <TabsList>
      <TabsTrigger value="fetch">
        HTTP (fetch)
      </TabsTrigger>

      <TabsTrigger value="node">
        Node.js SDK
      </TabsTrigger>

      <TabsTrigger value="curl">
        cURL
      </TabsTrigger>
    </TabsList>

    <TabsContent value="fetch">
      ```js title="server/confirmation.js"
      import { findPayment } from './payments.js';

      const order = await getCheckoutOrder(request);
      const payment = await findPayment(order); // Checks order ID, amount, and currency.
      if (payment?.status === 'successful') await markOrderPaid(order.id, payment.id);
      ```
    </TabsContent>

    <TabsContent value="node">
      ```ts title="server/confirmation.ts"
      const order = await getCheckoutOrder(request)
      const session = await tapaya.checkout.sessions.verify({
      	merchantOrderId: order.id,
      	amount: order.amount,
      	currency: order.currency,
      })
      if (session?.status === 'completed') await markOrderPaid(order.id, session.id)
      ```

      `verify()` ніколи не створює платіж і повертає `null` до першої спроби. Він викидає
      `TapayaOrderMismatchError`, коли сума або валюта відрізняються від вашого замовлення.
    </TabsContent>

    <TabsContent value="curl">
      Отримайте поточний платіж замовлення після завершення в браузері або під час перевірки невизначеного результату:

      ```bash
      curl --get "$TAPAYA_CHECKOUT_API_URL/merchant/payments" \
      	--data-urlencode 'merchantOrderId=order-1001' \
      	--fail-with-body --silent --show-error --max-time 35 \
      	--header "Authorization: Bearer $TAPAYA_CHECKOUT_API_KEY"
      ```

      Перш ніж виконати приклад замовлення, перевірте `merchantOrderId: "order-1001"`, `amount: 12100`, `currency: "EUR"`
      і `status: "successful"`. Збережіть повернений `id` платежу разом із замовленням. Сама лише відповідь HTTP 200 не доводить оплату.
    </TabsContent>
  </Tabs>

  Подбайте, щоб `markOrderPaid()` записувала платіж лише один раз, навіть якщо її викликають повторно, і запускайте виконання замовлення на основі цього
  збереженого стану.

  <Callout title="Звірка замовлень, які клієнти залишили" type="warn">
    Клієнт може закрити вкладку до того, як відкриється сторінка підтвердження. Запустіть заплановане завдання, яке для неоплачених замовлень викликає `verify()` або
    читає `GET /merchant/payments?merchantOrderId=...` з тими самими перевірками. Відхилені та
    скасовані спроби дозволяють нову спробу, тож неоплачене замовлення залишається `open`: знімайте з нього резервування за власним розкладом, і
    лише тоді, коли його `paymentStatus` не має значення `pending`. Невдалий запит чи тайм-аут не є відмовою, тому тримайте замовлення
    зарезервованим, доки результат платежу не стане відомим.
  </Callout>
</div>

## Додавання Apple Pay і Google Pay [#add-apple-pay-and-google-pay]

Гаманці з'являються у фреймі Tapaya поруч із полями картки. Щоб вони відобразилися, перевірте, що:

* Гаманці увімкнено в [налаштуваннях Checkout](/online/integration/checkout_settings#wallets-and-appearance).
* **Apple Pay:** магазин має [активний домен магазину](/online/integration/checkout_settings#register-apple-pay-shop-domains).
* **Google Pay у робочому середовищі:** налаштовано Google Pay merchant ID.
* Пристрій, браузер і гаманець клієнта підтримують цей спосіб оплати.

Передайте `wallets` із сумою оформлення замовлення в мінімальних одиницях. Tapaya показує доступні способи оплати, а непідтримувані чи
вимкнені гаманці залишаються прихованими. Використовуйте цей приклад замість прикладу `createPayment()` лише для карток:

```ts title="checkout-wallets.ts"
const payment = tapaya.createPayment({
	prepare: '/api/payments',
	wallets: { amount: order.totalInMinorUnits, currency: order.currency },
	onWalletClick: async () => {
		if (!form.reportValidity()) throw new Error('Complete the checkout details.')
		// Validate the cart and reserve the order on your server.
		const response = await fetch('/api/payments', { method: 'POST' })
		if (!response.ok) throw new Error('Could not prepare the payment.')
		return response.json() // { clientSecret, status }
	},
	onChange: (snapshot) => {
		button.hidden = snapshot.paymentMethod !== 'card'
		button.disabled = !snapshot.canSubmit
		if (snapshot.outcome === 'completed') location.assign('/checkout/confirmation')
	},
})
await payment.mount('#card-fields')
```

Сума, що відображається в браузері, має надходити з вашого сервера, який однаково обчислює та валідує суму,
коли готує платіж.

### Натискання кнопок гаманців [#wallet-clicks]

Офіційна кнопка гаманця відкриває нативне платіжне вікно гаманця після натискання клієнтом. `onWalletClick` валідує ваше
оформлення замовлення та резервує його платіж, коли натиснуто цю кнопку, ніколи під час розміщення полів чи вибору способу оплати. Без цієї
функції callback гаманці використовують `prepare` або `clientSecret`. Якщо ви резервуєте замовлення лише після натискання, подбайте, щоб функція callback
`prepare`, яка викликається під час розміщення, продовжувала наявне замовлення, не створюючи нового.

Залишайте власну кнопку оплати видимою лише тоді, коли `snapshot.paymentMethod` має значення `card`; гаманці використовують свої фірмові кнопки
замість `payment.submit()`. Результати оплати гаманцем проходять той самий процес перевірки та відновлення, що й картки. Закриття
платіжного вікна гаманця залишає оформлення замовлення доступним; невизначений платіж залишається в очікуванні й блокує повторне стягнення оплати.

### Оновлення суми [#update-the-total]

Передавайте `wallets: { amount, currency, disabled: true }`, поки обов'язкові дані оформлення замовлення не заповнено. Оновлюйте
суму або вмикайте кнопки без повторного розміщення:

```ts
payment.update({ wallets: { amount: 2450, currency: 'EUR', disabled: false } })
```

Передайте `wallets: null`, щоб приховати гаманці, доки сума не буде готова. Tapaya відмовляє в підтвердженні, якщо підготовлений платіж
відрізняється від суми в гаманці. React-хук `usePayment()` приймає ці параметри й оновлює суму, коли
змінюється `wallets`.

### Власний вибір способу оплати [#your-own-method-selector]

Якщо вибір способу оплати реалізує ваша сторінка, передайте `paymentMethod: 'card'`, `'apple_pay'` або `'google_pay'`, щоб відобразити
лише цей спосіб. Недоступний вибраний гаманець показує напис `walletUnavailable` і повідомляє
`snapshot.error.code: 'wallet_unavailable'`, коли можна безпечно почати інший платіж; запропонуйте у своєму
виборі інший спосіб оплати. Блокуйте вибір, поки `snapshot.busy` має значення true або платіж має стан `pending` чи `action_required`.

### Фрейми та CSP [#frames-and-csp]

SDK додає до свого фрейму `allow="payment"`. Якщо ваш магазин розміщено в іншому фреймі, цей зовнішній фрейм також має дозволяти
оплату. Apple Pay між різними origin потребує Safari 17 або новішої версії. Скрипти провайдерів гаманців залишаються у фреймі Tapaya, тож
вашій сторінці не потрібні додаткові origin провайдерів у CSP.

## Обробка простроченого доступу браузера [#handle-expired-browser-access]

Термін дії client secret спливає через 24 години, і тоді `clientSecret` має значення `null`. Платіж, який так і не було надіслано,
тоді скасовується, а наступний `prepare()` починає нову спробу для того самого замовлення. Платіж в очікуванні залишається `pending`
у браузері, а SDK продовжує викликати ваш ендпоінт, доки ваш сервер не повідомить його результат як `status`. Читання на сервері
й далі працює, тож ваше заплановане завдання в будь-якому разі його завершить.

## Діагностика серверних помилок [#diagnose-server-errors]

Node SDK викидає `PaymentsApiError` для відхилених запитів до API. Перевіряйте його `status`, `code`, `message`,
`fieldErrors` і `requestId` на своєму сервері. Не допускайте потрапляння облікових даних і client secret до журналів і показуйте власний
текст для клієнтів.

## Відповідність вашому брендингу [#match-your-branding]

Поля картки використовують чинний колір Checkout мерчанта з [налаштувань
Checkout](/online/integration/checkout_settings#wallets-and-appearance) як акцент фокуса (`colorFocus`).
Значення, які ви передаєте в `appearance.variables`, його перевизначають, і надсилаються лише задані вами змінні.
