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



Vložte zabezpečená pole pro kartu přímo do své pokladny. Formulář i odesílací tlačítko máte ve své režii; Tapaya zajišťuje
validaci karty, tokenizaci a 3-D Secure. Vložené platby jsou dostupné v ostrém provozu pro schválené obchodníky a pro testování
je k dispozici sandbox. Tento průvodce používá Tapaya.js **0.2.0** a Node.js SDK **0.3.0**.

<EmbeddedPaymentFlowDiagram />

<Callout title="Platební metody" type="info">
  Embedded Checkout podporuje karty, Apple Pay a Google Pay. Pro karty použijte vlastní tlačítko Zaplatit a pro Apple Pay
  a Google Pay oficiální tlačítka peněženek. Viz [Přidání Apple Pay a Google Pay](#add-apple-pay-and-google-pay).
</Callout>

## Než začnete [#before-you-start]

### Instalace SDK [#install-the-sdk]

Jeden balíček obsahuje serverového klienta, SDK pro prohlížeč i React hook:

<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>

| Import                       | Kde                                                                                                                      |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `@tapayadot/checkout`        | Pouze na vašem serveru. Import v sestavení pro prohlížeč vyhodí chybu, takže se váš API klíč nikdy nedostane na stránku. |
| `@tapayadot/checkout/client` | Vaše stránka pokladny.                                                                                                   |
| `@tapayadot/checkout/react`  | Vaše stránka pokladny, pokud používáte React.                                                                            |

### Konfigurace serveru [#configure-your-server]

Vložené platby nepotřebují návratové URL. Serverové příklady níže jsou ve verzích &#x2A;*HTTP (fetch)**, **Node.js SDK**
a **cURL**. Všechny volají stejné API obchodníka a pole pro kartu v prohlížeči nechávají beze změny.

|                     | Node.js SDK                                                                                                                                                                | HTTP (fetch) a cURL                                                                                                   |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| **Klíč**            | `TAPAYA_SECRET_KEY`, který načítá `new Tapaya()`                                                                                                                           | `TAPAYA_CHECKOUT_API_KEY`                                                                                             |
| **Prostředí**       | `TAPAYA_ENVIRONMENT=sandbox` nebo `production`. Bez této proměnné klíč `tapaya_live_` vybere produkční prostředí a u ostatních klíčů se ve výchozím stavu použije sandbox. | `TAPAYA_CHECKOUT_API_URL`: `https://api.sandbox.tapaya.com` nebo `https://api.tapaya.com`                             |
| **Režim platformy** | Předávejte `merchantToken` objednávky v každém volání nebo ho nastavte pomocí `new Tapaya(key, { merchantToken })`                                                         | Posílejte `X-Tapaya-Merchant-Token` v každém požadavku. Pomocná funkce pro fetch ho načítá z `TAPAYA_MERCHANT_TOKEN`. |

Checkout klíč obchodníka nic dalšího nepotřebuje. V režimu platformy použijte Secret key organizace a backend
obsluhující více obchodníků nechte vybírat token podle autentizované objednávky. Viz [Autentizace
Checkoutu](/online/integration/api_integration#authentication). Client secrety jsou specifické pro každou platbu.

Dokončete [onboarding](/merchants/onboarding) s online platbami a ověřte, že je obchodník v
[nastavení Checkoutu](/online/integration/checkout_settings) ve stavu **Ready**.

### Povolení Tapaya ve vaší CSP [#allow-tapaya-in-your-csp]

Loader stahuje Tapaya.js **0.2.0** z `https://js.tapaya.com/checkout/0.2.0/checkout.js`; verze prohlížečového skriptu
je nezávislá na verzi Node.js SDK. Přidejte origin assetů Tapaya do své stávající Content Security
Policy:

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

Prohlížeč dostane publishable key a client secret specifický pro danou platbu. Váš tajný API klíč obchodníka
zůstává na vašem serveru a údaje o kartě i skripty poskytovatele plateb zůstávají uvnitř rámce Tapaya. Pro lokální testování
hostujte Tapaya.js sami a jeho origin předejte loaderu, například
`loadTapaya(publishableKey, { apiOrigin, origin: 'http://localhost:4191' })`.

## Jak to funguje [#how-it-works]

1. Vaše stránka připojí pole pro kartu od Tapaya a nasměruje je na endpoint na vašem serveru.
2. Při načtení stránky a při platbě zákazníka volají pole pro kartu tento endpoint. Ten vrátí platbu objednávky
   jako `clientSecret`: přihlašovací údaj pro jednu platbu, pomocí kterého pole pro kartu potvrdí platbu přímo
   u Tapaya. Kód vaší stránky ho nikdy nečte.
3. Vaše stránka sleduje `outcome` platby. Když se platba dokončí, váš server ji ověří a teprve potom
   objednávku vyřídíte.

## Přijetí platby [#accept-a-payment]

<div className="fd-steps [&_h3]:fd-step">
  ### Získání nastavení pro prohlížeč [#get-the-browser-settings]

  Načtěte nastavení pro prohlížeč jednou na serveru a přidejte je do konfigurace svého frontendu.

  <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">
      Tuto pomocnou funkci mějte na serveru. Kontroluje chyby HTTP, na požádání považuje chybějící platbu objednávky za `null`
      a požadavky automaticky neopakuje.

      ```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 vrací `publishableKey` a `environment`. Hodnotu `apiOrigin` nastavte podle URL API, které jste volali; není to pole
      odpovědi s konfigurací.
    </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">
      Spusťte tento příkaz na svém serveru nebo v důvěryhodném terminálu. V režimu platformy přidejte
      `--header "X-Tapaya-Merchant-Token: $TAPAYA_MERCHANT_TOKEN"` ke každému příkazu cURL v tomto průvodci.

      ```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"
      ```

      Jako konfiguraci pro prohlížeč použijte vrácený `publishableKey` a origin vaší `TAPAYA_CHECKOUT_API_URL`.
    </TabsContent>
  </Tabs>

  Obě hodnoty můžete bezpečně vložit do stránky. Publishable key má předponu `pk_test_`, pokud používáte testovací klíče brány, a `pk_live_`
  u ostrých klíčů brány. Loaderu vždy předávejte `apiOrigin` vrácený vaším serverem, aby prohlížeč i server
  zůstaly ve stejném prostředí.

  ### Přidání polí pro kartu [#add-the-card-fields]

  Přidejte kontejner do svého stávajícího formuláře pokladny:

  ```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>
  ```

  Načtěte Tapaya a připojte pole. `prepare` je endpoint, který přidáte v dalším kroku.

  ```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')
  ```

  Po připojení pole zavolají `prepare`, aby navázala na existující platbu objednávky. Opětovné načtení stránky během platby
  nikdy nevede k další platbě: stránka místo toho zobrazí aktuální stav platby.

  ### Vrácení platby objednávky [#return-the-orders-payment]

  Připojte tento handler k `POST /api/payments`. `getCheckoutOrder()` je funkce vaší vlastní aplikace: autentizuje
  zákazníka, zkontroluje jeho přístup k objednávce a vrátí její uložené `id`, `amount`, `currency` a `status`. Na tuto routu
  uplatněte svou obvyklou ochranu proti 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">
      Než vytvoříte pokus, načtěte existující platbu. Nevyřešenou platbu použijte znovu a další pokus vytvořte
      pouze po zamítnutí nebo zrušení s `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' } },
      	);
      }
      ```

      Používejte kód měny velkými písmeny a uložené ID objednávky o délce nejvýše 200 znaků. Po zahájení platby
      údaje objednávky neměňte. Při vypršení časového limitu znovu načtěte platbu objednávky a teprve pak rozhodněte, zda je potřeba nový pokus.
      Nikdy negenerujte nový klíč jen proto, že se ztratila odpověď.
    </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()` vrací platbu objednávky. Vytvoří první pokus, nevyřešený pokus vrátí beze změny a po zamítnutí
      vytvoří další pokus. Klíče idempotence si odvozuje sám, takže jeho volání při každém načtení stránky a
      odeslání nikdy nevytvoří druhou platbu. Jakmile platba začne, zafixujte celkovou částku: `prepare()` vyhodí
      `TapayaOrderMismatchError`, pokud částka nebo měna objednávky už neodpovídají její platbě.
    </TabsContent>

    <TabsContent value="curl">
      Nejprve vyhledejte platbu objednávky. Tyto příklady používají objednávku na 121,00 EUR uloženou na vašem serveru. `404` znamená,
      že žádná platba nebyla nalezena; jiná selhání HTTP neopravňují k vytvoření dalšího pokusu.

      ```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"
      ```

      Pokud žádná platba neexistuje a objednávka může platbu stále přijmout, vytvořte první pokus:

      ```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"}'
      ```

      Existující nevyřešenou platbu použijte znovu. Po `declined` nebo `cancelled` vytvořte další pokus pouze tehdy, když je
      `retryAllowed` rovno `true`. Pro tento pokus zopakujte POST se stejnými údaji objednávky a klíčem
      `order-1001:after:<previous-payment-id>`, kde zástupný text nahradíte vráceným `id`. Tento klíč používejte i pro
      opakované pokusy o požadavek. Po vypršení časového limitu objednávku znovu vyhledejte; nepředpokládejte, že POST selhal.

      cURL ukazuje volání API. Autentizovaný handler `POST /api/payments` vaší aplikace musí polím pro kartu vracet
      `{ clientSecret, status }`. Stav z API převeďte podle tabulky níže a pošlete
      `clientSecret: null`, když vaše objednávka už platbu nepřijímá. Tyto odpovědi nikdy nevkládejte do sdílených logů terminálu.
    </TabsContent>
  </Tabs>

  REST API vrací stavy platby. Node SDK je převádí na `status`, který pole pro kartu očekávají; integrace přes fetch a cURL
  používají stejný převod ve svém vlastním endpointu prepare:

  | `status` v API                                                                      | `status` endpointu prepare |
  | ----------------------------------------------------------------------------------- | -------------------------- |
  | `successful`                                                                        | `completed`                |
  | `declined` nebo `cancelled` s `retryAllowed: false`                                 | `ended`                    |
  | `ready`, `pending`, `action_required` nebo zamítnutí či zrušení, které lze opakovat | `open`                     |

  <Accordions>
    <Accordion title="Pravidla pro váš endpoint prepare">
      * **Částku berte z uložené objednávky,** v celých nejmenších jednotkách měny, nikdy z prohlížeče. Jakmile platba začne, zafixujte
        celkovou částku.
      * **Odpovídejte i u dokončených objednávek.** Objednávka, která je už zaplacená nebo kterou dokončila vaše plánovaná úloha, stále vrací
        `status` své platby, takže opětovné načtení zobrazí výsledek místo chyby. Nikdy nedostane client secret,
        takže zákazník nemůže zaplatit objednávku, za kterou už platbu nepřijímáte.
      * **Jedna úspěšná platba na objednávku.** Každé zamítnutí nastaví `retryAllowed`, takže zákazník může zkusit jinou kartu pro
        stejnou objednávku. Limit je sdílený s [Hosted Checkout](/online/integration/api_integration#one-payment-per-order).
      * **Chraňte client secret.** Posílejte ho pouze zákazníkovi, který je oprávněn tuto objednávku zaplatit, a nikdy ho nelogujte.
      * **Počítejte s prostým požadavkem POST.** Pole pro kartu posílají POST na `prepare` na vašem vlastním originu s cookies a bez těla. Odmítají
        přesměrování a nikdy požadavek automaticky neopakují. Pro vlastní hlavičky, CSRF tokeny nebo tělo požadavku předejte místo URL
        funkci, která vrací stejný JSON: `prepare: async () => (await fetch(…)).json()`.
    </Accordion>
  </Accordions>

  ### Odeslání z vašeho handleru [#submit-from-your-handler]

  Přidejte toto do stejného skriptu. Váš handler zvaliduje formulář a zavolá `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()` zavolá pro platbu `prepare`, potom zvaliduje kartu, provede 3-D Secure a potvrdí platbu
  u Tapaya.

  ### Zobrazení výsledku [#show-the-outcome]

  `onChange` dostane `snapshot` platby při každé jeho změně, včetně načtení stránky s platbou, která
  už je dokončená. Řiďte se hodnotou `snapshot.outcome`:

  | `outcome`   | Co zobrazit                                                                                                                               |
  | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
  | `open`      | Tlačítko Zaplatit, aktivní, dokud je `canSubmit` true. Po zamítnutí zůstává `open` a další `submit()` dostane nový pokus.                 |
  | `pending`   | Indikátor průběhu. Výsledek zatím není znám, takže nelze nic zaplatit znovu. SDK kontroluje stav automaticky, dokud je stránka viditelná. |
  | `completed` | Vaši potvrzovací stránku. Tam váš server objednávku ověří.                                                                                |
  | `ended`     | Novou pokladnu. Pro tuto objednávku už není možný žádný další pokus.                                                                      |

  Když se zobrazení vaší pokladny odpojuje (unmount), zavolejte `payment.destroy()`. V Reactu použijte `usePayment()` z
  `@tapayadot/checkout/react`. Přijímá stejné možnosti a navíc `publishableKey` a `apiOrigin` a vrací
  `fieldRef` pro kontejner, aktuální `snapshot` a `submit()`.

  ### Ověření před vyřízením [#verify-before-fulfilling]

  Výsledek v prohlížeči slouží jen k zobrazení. Na potvrzovací stránce načtěte platbu objednávky na svém serveru a objednávku vyřiďte
  pouze tehdy, když je platba dokončená:

  <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()` nikdy nevytváří platbu a před prvním pokusem vrací `null`. Vyhodí
      `TapayaOrderMismatchError`, když se částka nebo měna liší od vaší objednávky.
    </TabsContent>

    <TabsContent value="curl">
      Aktuální platbu objednávky načtěte poté, co prohlížeč skončí, nebo když kontrolujete nejistý výsledek:

      ```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"
      ```

      Před vyřízením ukázkové objednávky ověřte `merchantOrderId: "order-1001"`, `amount: 12100`, `currency: "EUR"`
      a `status: "successful"`. Vrácené `id` platby uložte ke své objednávce. Samotná odpověď HTTP 200 platbu neprokazuje.
    </TabsContent>
  </Tabs>

  Zajistěte, aby `markOrderPaid()` zaznamenala platbu jen jednou, i když je volána opakovaně, a vyřízení spouštějte z tohoto
  uloženého stavu.

  <Callout title="Odsouhlasení objednávek, které zákazník opustil" type="warn">
    Zákazník může zavřít kartu prohlížeče dříve, než se otevře potvrzovací stránka. Spusťte plánovanou úlohu, která pro nezaplacené objednávky volá `verify()` nebo
    čte `GET /merchant/payments?merchantOrderId=...`, se stejnými kontrolami. Zamítnuté a
    zrušené pokusy umožňují nový pokus, takže nezaplacená objednávka zůstává `open`: uvolněte ji podle vlastního harmonogramu, a
    to jen tehdy, když její `paymentStatus` není `pending`. Neúspěšný požadavek nebo vypršení časového limitu není zamítnutí, proto objednávku nechte
    rezervovanou, dokud platba nemá známý výsledek.
  </Callout>
</div>

## Přidání Apple Pay a Google Pay [#add-apple-pay-and-google-pay]

Peněženky se zobrazují uvnitř rámce Tapaya vedle polí pro kartu. Aby se zobrazily, ověřte, že:

* Peněženky jsou povolené v [nastavení Checkoutu](/online/integration/checkout_settings#wallets-and-appearance).
* **Apple Pay:** obchod má [aktivní doménu obchodu](/online/integration/checkout_settings#register-apple-pay-shop-domains).
* **Google Pay v produkčním prostředí:** je nakonfigurované Google Pay merchant ID.
* Zařízení, prohlížeč a peněženka zákazníka danou metodu podporují.

Předejte `wallets` s celkovou částkou pokladny v nejmenších jednotkách měny. Tapaya zobrazí dostupné metody a nepodporované nebo
vypnuté peněženky zůstanou skryté. Použijte tento kód místo příkladu `createPayment()` pouze pro karty:

```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')
```

Celková částka zobrazená v prohlížeči musí pocházet z vašeho serveru, který ji při přípravě platby i tak sám
vypočítá a ověří.

### Kliknutí na peněženku [#wallet-clicks]

Oficiální tlačítko peněženky otevře nativní okno peněženky na základě kliknutí zákazníka. `onWalletClick` zvaliduje vaši
pokladnu a rezervuje její platbu ve chvíli, kdy se na toto tlačítko klikne, nikdy při připojení ani při výběru metody. Bez tohoto
callbacku používají peněženky `prepare` nebo `clientSecret`. Pokud objednávku rezervujete až při kliknutí, zajistěte, aby callback
`prepare` volaný při připojení navázal na existující objednávku, aniž by vytvořil novou.

Vlastní tlačítko Zaplatit nechte viditelné pouze tehdy, když je `snapshot.paymentMethod` rovno `card`; peněženky místo
`payment.submit()` používají svá značková tlačítka. Výsledky plateb peněženkou procházejí stejným postupem ověření a obnovy jako karty. Zavření
okna peněženky ponechá pokladnu dostupnou; nejistá platba zůstane ve stavu pending a zablokuje další platbu.

### Aktualizace celkové částky [#update-the-total]

Dokud nejsou vyplněné povinné údaje pokladny, předávejte `wallets: { amount, currency, disabled: true }`. Celkovou částku
aktualizujte nebo tlačítka povolte bez opětovného připojení:

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

Předáním `wallets: null` peněženky skryjete, dokud nebude celková částka připravena. Tapaya odmítne potvrzení, pokud se připravená platba
liší od celkové částky peněženky. React hook `usePayment()` tyto možnosti přijímá a celkovou částku aktualizuje, když se
`wallets` změní.

### Vlastní výběr metody [#your-own-method-selector]

Pokud výběr metody spravuje vaše stránka, předejte `paymentMethod: 'card'`, `'apple_pay'` nebo `'google_pay'`, aby se vykreslila
pouze tato metoda. Nedostupná vybraná peněženka zobrazí popisek `walletUnavailable` a nahlásí
`snapshot.error.code: 'wallet_unavailable'`, pokud lze bezpečně zahájit jinou platbu; nabídněte ve svém
výběru jinou metodu. Výběr zamkněte, dokud je `snapshot.busy` true nebo je platba ve stavu `pending` či `action_required`.

### Rámce a CSP [#frames-and-csp]

SDK přidává ke svému rámci `allow="payment"`. Pokud je váš obchod uvnitř jiného rámce, musí platby povolit i tento vnější
rámec. Apple Pay napříč originy vyžaduje Safari 17 nebo novější. Skripty poskytovatelů peněženek zůstávají uvnitř rámce Tapaya, takže
vaše stránka nepotřebuje ve své CSP žádné další originy poskytovatelů.

## Řešení vypršeného přístupu prohlížeče [#handle-expired-browser-access]

Client secret vyprší po 24 hodinách a `clientSecret` je pak `null`. Platba, která nebyla nikdy odeslána, je
pak zrušena a další `prepare()` zahájí pro stejnou objednávku nový pokus. Čekající platba zůstává v prohlížeči `pending`
a SDK nadále volá váš endpoint, dokud váš server nenahlásí její výsledek jako `status`. Čtení na serveru
stále funguje, takže ji vaše plánovaná úloha v každém případě dokončí.

## Diagnostika chyb serveru [#diagnose-server-errors]

Node SDK vyhazuje u odmítnutých požadavků na API `PaymentsApiError`. Na svém serveru zkontrolujte jeho `status`, `code`, `message`,
`fieldErrors` a `requestId`. Přihlašovací údaje a client secrety nezapisujte do logů a zákazníkovi zobrazujte vlastní
texty.

## Přizpůsobení vaší značce [#match-your-branding]

Pole pro kartu používají jako barvu zvýraznění při fokusu (`colorFocus`) výslednou barvu pokladny obchodníka z [nastavení
Checkoutu](/online/integration/checkout_settings#wallets-and-appearance).
Hodnoty, které předáte v `appearance.variables`, ji přepíšou, a odesílají se pouze proměnné, které nastavíte.
