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



Đặt các trường nhập thẻ bảo mật ngay trong trang thanh toán của riêng bạn. Bạn quản lý biểu mẫu và nút gửi; Tapaya xử lý việc kiểm tra
thẻ, tokenization và 3-D Secure. Thanh toán nhúng đã hoạt động chính thức cho các merchant đã được phê duyệt, và có sẵn sandbox
để kiểm thử. Hướng dẫn này dùng Tapaya.js **0.2.0** và Node.js SDK **0.3.0**.

<EmbeddedPaymentFlowDiagram />

<Callout title="Phương thức thanh toán" type="info">
  Embedded Checkout hỗ trợ thẻ, Apple Pay và Google Pay. Dùng nút thanh toán của riêng bạn cho thẻ và các nút ví
  chính thức cho Apple Pay và Google Pay. Xem [Thêm Apple Pay và Google Pay](#add-apple-pay-and-google-pay).
</Callout>

## Trước khi bắt đầu [#before-you-start]

### Cài đặt SDK [#install-the-sdk]

Một gói duy nhất chứa client cho máy chủ, SDK cho trình duyệt và 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                       | Dùng ở đâu                                                                                                                                 |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `@tapayadot/checkout`        | Chỉ trên máy chủ của bạn. Import gói này trong bản build cho trình duyệt sẽ gây lỗi, nên API key của bạn không bao giờ đến được trang web. |
| `@tapayadot/checkout/client` | Trang thanh toán của bạn.                                                                                                                  |
| `@tapayadot/checkout/react`  | Trang thanh toán của bạn, nếu bạn dùng React.                                                                                              |

### Cấu hình máy chủ [#configure-your-server]

Thanh toán nhúng không cần URL trả về. Các ví dụ máy chủ bên dưới có các phiên bản &#x2A;*HTTP (fetch)**, **Node.js SDK**
và **cURL**, tất cả đều gọi cùng một merchant API và không thay đổi các trường nhập thẻ trong trình duyệt.

|                     | Node.js SDK                                                                                                                                           | HTTP (fetch) và cURL                                                                                          |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Key**             | `TAPAYA_SECRET_KEY`, được `new Tapaya()` đọc                                                                                                          | `TAPAYA_CHECKOUT_API_KEY`                                                                                     |
| **Môi trường**      | `TAPAYA_ENVIRONMENT=sandbox` hoặc `production`. Nếu không có biến này, key `tapaya_live_` sẽ chọn production, còn các key khác mặc định dùng sandbox. | `TAPAYA_CHECKOUT_API_URL`: `https://api.sandbox.tapaya.com` hoặc `https://api.tapaya.com`                     |
| **Chế độ nền tảng** | Truyền `merchantToken` của đơn hàng vào mọi lệnh gọi, hoặc đặt nó bằng `new Tapaya(key, { merchantToken })`                                           | Gửi `X-Tapaya-Merchant-Token` trong mọi yêu cầu. Hàm hỗ trợ fetch đọc giá trị này từ `TAPAYA_MERCHANT_TOKEN`. |

Checkout key của merchant không cần thêm gì khác. Ở chế độ nền tảng, hãy dùng Secret key của tổ chức, và để backend
phục vụ nhiều merchant chọn token từ đơn hàng gắn với phiên đăng nhập. Xem [Xác thực
Checkout](/online/integration/api_integration#authentication). Client secret là riêng cho từng khoản thanh toán.

Hoàn tất [onboarding](/merchants/onboarding) với thanh toán trực tuyến và kiểm tra rằng merchant ở trạng thái **Ready** trong
[Cài đặt Checkout](/online/integration/checkout_settings).

### Cho phép Tapaya trong CSP [#allow-tapaya-in-your-csp]

Trình tải lấy Tapaya.js **0.2.0** từ `https://js.tapaya.com/checkout/0.2.0/checkout.js`; phiên bản script trình duyệt
độc lập với phiên bản Node.js SDK. Thêm origin tài nguyên của Tapaya vào Content Security
Policy hiện có của bạn:

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

Trình duyệt nhận publishable key và client secret riêng của khoản thanh toán. Merchant API key bí mật của bạn
vẫn nằm trên máy chủ, còn dữ liệu thẻ và các script của nhà cung cấp dịch vụ thanh toán nằm bên trong frame của Tapaya. Để kiểm thử cục bộ,
hãy tự phục vụ Tapaya.js và truyền origin của nó cho trình tải, ví dụ
`loadTapaya(publishableKey, { apiOrigin, origin: 'http://localhost:4191' })`.

## Cách hoạt động [#how-it-works]

1. Trang của bạn gắn các trường nhập thẻ của Tapaya và trỏ chúng đến một endpoint trên máy chủ của bạn.
2. Khi trang tải và khi khách hàng thanh toán, các trường nhập thẻ gọi endpoint đó. Endpoint trả về khoản thanh toán
   của đơn hàng dưới dạng `clientSecret`: một thông tin xác thực dùng cho một khoản thanh toán, mà các trường nhập thẻ dùng để xác nhận thanh toán trực tiếp với
   Tapaya. Mã trang của bạn không bao giờ đọc giá trị này.
3. Trang của bạn theo dõi `outcome` của khoản thanh toán. Khi thanh toán hoàn tất, máy chủ của bạn xác minh khoản thanh toán trước khi bạn
   hoàn tất đơn hàng.

## Nhận một khoản thanh toán [#accept-a-payment]

<div className="fd-steps [&_h3]:fd-step">
  ### Lấy cài đặt cho trình duyệt [#get-the-browser-settings]

  Lấy cài đặt cho trình duyệt một lần trên máy chủ và thêm chúng vào cấu hình frontend của bạn.

  <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">
      Giữ hàm hỗ trợ này trên máy chủ của bạn. Hàm kiểm tra lỗi HTTP, coi khoản thanh toán không tồn tại của đơn hàng là `null` khi được yêu cầu,
      và không tự động thử lại.

      ```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 trả về `publishableKey` và `environment`. Đặt `apiOrigin` từ URL API mà bạn đã gọi; đây không phải là một trường
      trong phản hồi cấu hình.
    </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">
      Chạy lệnh này trên máy chủ của bạn hoặc trong một terminal đáng tin cậy. Ở chế độ nền tảng, hãy thêm
      `--header "X-Tapaya-Merchant-Token: $TAPAYA_MERCHANT_TOKEN"` vào mọi lệnh cURL trong hướng dẫn này.

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

      Dùng `publishableKey` được trả về và origin của `TAPAYA_CHECKOUT_API_URL` làm cấu hình cho trình duyệt.
    </TabsContent>
  </Tabs>

  Cả hai giá trị đều an toàn khi đưa vào trang của bạn. Publishable key dùng `pk_test_` với key của cổng thanh toán thử nghiệm và `pk_live_`
  với key của cổng thanh toán chính thức. Luôn truyền `apiOrigin` do máy chủ của bạn trả về cho trình tải, để trình duyệt và máy chủ
  luôn ở cùng một môi trường.

  ### Thêm các trường nhập thẻ [#add-the-card-fields]

  Thêm một container vào biểu mẫu thanh toán hiện có của bạn:

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

  Tải Tapaya và gắn các trường. `prepare` là endpoint bạn thêm ở bước tiếp theo.

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

  Khi các trường được gắn, chúng gọi `prepare` để tiếp tục khoản thanh toán hiện có của đơn hàng. Việc tải lại trang trong khi thanh toán không bao giờ
  trừ tiền lần nữa: thay vào đó, trang hiển thị trạng thái hiện tại của khoản thanh toán.

  ### Trả về khoản thanh toán của đơn hàng [#return-the-orders-payment]

  Kết nối trình xử lý này với `POST /api/payments`. `getCheckoutOrder()` là hàm của chính ứng dụng bạn: xác thực
  khách hàng, kiểm tra quyền truy cập vào đơn hàng của họ, và trả về `id`, `amount`, `currency` và `status` đã lưu của đơn hàng. Áp dụng
  cơ chế bảo vệ CSRF thông thường của bạn cho route này.

  <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">
      Đọc khoản thanh toán hiện có trước khi tạo một lần thử. Dùng lại khoản thanh toán chưa có kết quả, và chỉ tạo lần thử khác
      sau khi bị từ chối hoặc bị hủy với `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' } },
      	);
      }
      ```

      Dùng mã tiền tệ viết hoa và ID đơn hàng đã lưu dài tối đa 200 ký tự. Giữ nguyên thông tin đơn hàng
      sau khi thanh toán bắt đầu. Khi hết thời gian chờ, hãy đọc lại khoản thanh toán của đơn hàng trước khi quyết định có cần lần thử mới hay không.
      Không bao giờ tạo key mới chỉ vì một phản hồi bị mất.
    </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()` trả về khoản thanh toán của đơn hàng. Phương thức này tạo lần thử đầu tiên, trả về nguyên trạng một lần thử chưa có kết quả, và
      tạo lần thử tiếp theo sau khi bị từ chối. Nó tự suy ra idempotency key, nên việc gọi nó mỗi lần tải trang và
      mỗi lần gửi không bao giờ tạo lần trừ tiền thứ hai. Hãy cố định tổng tiền khi thanh toán đã bắt đầu: `prepare()` ném
      `TapayaOrderMismatchError` nếu số tiền hoặc loại tiền tệ của đơn hàng không còn khớp với khoản thanh toán.
    </TabsContent>

    <TabsContent value="curl">
      Trước tiên, hãy tra cứu khoản thanh toán của đơn hàng. Các ví dụ này dùng một đơn hàng 121,00 EUR được lưu trên máy chủ của bạn. `404` nghĩa là
      không tìm thấy khoản thanh toán nào; các lỗi HTTP khác không cho phép tạo thêm lần thử.

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

      Nếu chưa có khoản thanh toán nào và đơn hàng vẫn có thể nhận thanh toán, hãy tạo lần thử đầu tiên:

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

      Dùng lại khoản thanh toán hiện có chưa có kết quả. Sau `declined` hoặc `cancelled`, chỉ tạo lần thử khác khi
      `retryAllowed` là `true`. Với lần thử đó, lặp lại POST với cùng thông tin đơn hàng và key
      `order-1001:after:<previous-payment-id>`, thay phần giữ chỗ bằng `id` được trả về. Dùng lại key đó cho
      các lần thử lại. Sau khi hết thời gian chờ, hãy tra cứu lại đơn hàng; đừng mặc định rằng POST đã thất bại.

      cURL minh họa các lệnh gọi API. Trình xử lý `POST /api/payments` yêu cầu đăng nhập trong ứng dụng của bạn phải trả về
      `{ clientSecret, status }` cho các trường nhập thẻ. Ánh xạ trạng thái API theo bảng bên dưới, và gửi
      `clientSecret: null` khi đơn hàng của bạn không còn nhận thanh toán. Không bao giờ đưa các phản hồi này vào log terminal dùng chung.
    </TabsContent>
  </Tabs>

  REST API trả về các trạng thái thanh toán. Node SDK ánh xạ chúng sang `status` mà các trường nhập thẻ cần; các tích hợp fetch và cURL
  áp dụng cùng cách ánh xạ trong endpoint prepare của riêng mình:

  | `status` của API                                                                         | `status` của endpoint prepare |
  | ---------------------------------------------------------------------------------------- | ----------------------------- |
  | `successful`                                                                             | `completed`                   |
  | `declined` hoặc `cancelled`, với `retryAllowed: false`                                   | `ended`                       |
  | `ready`, `pending`, `action_required`, hoặc một lần bị từ chối hay bị hủy có thể thử lại | `open`                        |

  <Accordions>
    <Accordion title="Quy tắc cho endpoint prepare của bạn">
      * **Lấy số tiền từ đơn hàng đã lưu,** tính bằng số nguyên theo đơn vị nhỏ nhất của tiền tệ, không bao giờ lấy từ trình duyệt. Hãy cố định tổng tiền khi
        thanh toán đã bắt đầu.
      * **Vẫn phản hồi cho các đơn hàng đã có kết quả cuối cùng.** Một đơn hàng đã được thanh toán, hoặc đã được tác vụ định kỳ của bạn xử lý xong, vẫn trả về
        `status` của khoản thanh toán, để khi tải lại trang sẽ hiển thị kết quả thay vì lỗi. Đơn hàng đó không bao giờ nhận client secret,
        nên khách hàng không thể thanh toán cho một đơn hàng mà bạn không còn nhận thanh toán.
      * **Một khoản thanh toán thành công cho mỗi đơn hàng.** Mỗi lần bị từ chối đều đặt `retryAllowed`, để khách hàng có thể thử thẻ khác cho
        cùng đơn hàng. Giới hạn này được dùng chung với [Hosted Checkout](/online/integration/api_integration#one-payment-per-order).
      * **Bảo vệ client secret.** Chỉ gửi nó cho khách hàng được phép thanh toán đơn hàng này, và không bao giờ ghi nó vào log.
      * **Chuẩn bị cho một POST trống.** Các trường nhập thẻ gửi POST đến `prepare` trên origin của chính bạn, kèm cookie và không có phần thân. Chúng từ chối
        chuyển hướng và không bao giờ tự động thử lại yêu cầu. Để dùng header tùy chỉnh, token CSRF hoặc phần thân yêu cầu, hãy truyền một
        hàm trả về cùng JSON đó thay cho URL: `prepare: async () => (await fetch(…)).json()`.
    </Accordion>
  </Accordions>

  ### Gửi thanh toán từ trình xử lý của bạn [#submit-from-your-handler]

  Thêm đoạn này vào cùng script. Trình xử lý của bạn kiểm tra biểu mẫu và gọi `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()` gọi `prepare` cho khoản thanh toán, sau đó kiểm tra thẻ, chạy 3-D Secure và xác nhận thanh toán
  với Tapaya.

  ### Hiển thị kết quả [#show-the-outcome]

  `onChange` nhận `snapshot` của khoản thanh toán mỗi khi nó thay đổi, kể cả khi trang tải với một khoản thanh toán
  đã có kết quả cuối cùng. Hãy làm theo `snapshot.outcome`:

  | `outcome`   | Hiển thị gì                                                                                                                                        |
  | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `open`      | Nút thanh toán, được bật khi `canSubmit` là true. Khi bị từ chối, trạng thái vẫn là `open`, và lần gọi `submit()` tiếp theo sẽ có một lần thử mới. |
  | `pending`   | Chỉ báo đang xử lý. Kết quả chưa được biết, nên không thể thanh toán lại. SDK tự động kiểm tra trạng thái khi trang đang hiển thị.                 |
  | `completed` | Trang xác nhận của bạn. Máy chủ của bạn xác minh đơn hàng tại đó.                                                                                  |
  | `ended`     | Một lượt checkout mới. Không thể thực hiện thêm lần thử nào cho đơn hàng này.                                                                      |

  Gọi `payment.destroy()` khi giao diện checkout của bạn bị gỡ (unmount). Với React, hãy dùng `usePayment()` từ
  `@tapayadot/checkout/react`. Hook này nhận cùng các tùy chọn, cộng thêm `publishableKey` và `apiOrigin`, và trả về một
  `fieldRef` cho container, `snapshot` hiện tại và `submit()`.

  ### Xác minh trước khi hoàn tất đơn hàng [#verify-before-fulfilling]

  Kết quả trên trình duyệt chỉ để hiển thị. Trên trang xác nhận, hãy đọc khoản thanh toán của đơn hàng trên máy chủ, và chỉ hoàn tất đơn hàng
  khi khoản thanh toán đã hoàn tất:

  <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()` không bao giờ tạo khoản thanh toán và trả về `null` trước lần thử đầu tiên. Phương thức này ném
      `TapayaOrderMismatchError` khi số tiền hoặc loại tiền tệ khác với đơn hàng của bạn.
    </TabsContent>

    <TabsContent value="curl">
      Truy xuất khoản thanh toán hiện tại của đơn hàng sau khi trình duyệt hoàn tất, hoặc khi kiểm tra một kết quả chưa chắc chắn:

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

      Trước khi hoàn tất đơn hàng ví dụ, hãy xác minh `merchantOrderId: "order-1001"`, `amount: 12100`, `currency: "EUR"`
      và `status: "successful"`. Lưu `id` của khoản thanh toán được trả về cùng với đơn hàng. Chỉ riêng HTTP 200 không chứng minh việc thanh toán.
    </TabsContent>
  </Tabs>

  Hãy để `markOrderPaid()` ghi nhận khoản thanh toán đúng một lần, kể cả khi được gọi nhiều lần, và kích hoạt việc hoàn tất đơn hàng từ
  trạng thái đã lưu đó.

  <Callout title="Đối soát các đơn hàng có khách hàng rời đi" type="warn">
    Khách hàng có thể đóng tab trước khi trang xác nhận mở ra. Hãy chạy một tác vụ định kỳ gọi `verify()` hoặc
    đọc `GET /merchant/payments?merchantOrderId=...` cho các đơn hàng chưa thanh toán, với cùng các bước kiểm tra. Các lần thử bị từ chối và
    bị hủy đều cho phép một lần thử mới, nên đơn hàng chưa thanh toán vẫn ở trạng thái `open`: hãy giải phóng đơn hàng theo lịch của riêng bạn, và
    chỉ khi `paymentStatus` của nó không phải là `pending`. Một yêu cầu thất bại hoặc hết thời gian chờ không phải là bị từ chối, vì vậy hãy tiếp tục giữ đơn hàng
    cho đến khi khoản thanh toán có kết quả xác định.
  </Callout>
</div>

## Thêm Apple Pay và Google Pay [#add-apple-pay-and-google-pay]

Ví xuất hiện bên trong frame của Tapaya, cạnh các trường nhập thẻ. Để ví hiển thị, hãy kiểm tra rằng:

* Ví đã được bật trong [Cài đặt Checkout](/online/integration/checkout_settings#wallets-and-appearance).
* **Apple Pay:** cửa hàng có một [tên miền cửa hàng đang hoạt động](/online/integration/checkout_settings#register-apple-pay-shop-domains).
* **Google Pay trên production:** đã cấu hình Google Pay merchant ID.
* Thiết bị, trình duyệt và ví của khách hàng hỗ trợ phương thức này.

Truyền `wallets` với tổng tiền thanh toán tính bằng đơn vị nhỏ nhất của tiền tệ. Tapaya hiển thị các phương thức khả dụng, còn các ví không được hỗ trợ hoặc
bị tắt sẽ được ẩn. Dùng đoạn này thay cho ví dụ `createPayment()` chỉ dành cho thẻ:

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

Tổng tiền hiển thị trên trình duyệt phải đến từ máy chủ của bạn, nơi vẫn tính toán và kiểm tra số tiền
khi chuẩn bị khoản thanh toán.

### Khi nhấn nút ví [#wallet-clicks]

Nút ví chính thức mở cửa sổ thanh toán gốc của ví từ thao tác nhấn của khách hàng. `onWalletClick` kiểm tra
checkout của bạn và giữ chỗ khoản thanh toán khi nút đó được nhấn, không bao giờ khi gắn trường hay khi chọn phương thức. Nếu không có
callback này, ví dùng `prepare` hoặc `clientSecret`. Nếu bạn chỉ giữ chỗ đơn hàng khi nhấn, hãy để callback
`prepare` lúc gắn trường tiếp tục một đơn hàng hiện có mà không tạo đơn hàng mới.

Chỉ hiển thị nút thanh toán của riêng bạn khi `snapshot.paymentMethod` là `card`; ví dùng các nút mang thương hiệu của chúng
thay cho `payment.submit()`. Kết quả từ ví dùng cùng luồng xác minh và khôi phục như thẻ. Việc đóng
cửa sổ thanh toán của ví vẫn giữ checkout khả dụng; một khoản thanh toán chưa chắc chắn vẫn ở trạng thái chờ và chặn một lần trừ tiền khác.

### Cập nhật tổng tiền [#update-the-total]

Truyền `wallets: { amount, currency, disabled: true }` khi các thông tin checkout bắt buộc chưa đầy đủ. Cập nhật
tổng tiền hoặc bật các nút mà không cần gắn lại:

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

Truyền `wallets: null` để ẩn ví cho đến khi tổng tiền sẵn sàng. Tapaya từ chối xác nhận nếu khoản thanh toán đã chuẩn bị
khác với tổng tiền trong ví. Hook React `usePayment()` chấp nhận các tùy chọn này và cập nhật tổng tiền khi
`wallets` thay đổi.

### Bộ chọn phương thức của riêng bạn [#your-own-method-selector]

Nếu trang của bạn tự quản lý bộ chọn phương thức, hãy truyền `paymentMethod: 'card'`, `'apple_pay'` hoặc `'google_pay'` để chỉ hiển thị
phương thức đó. Một ví đã chọn nhưng không khả dụng sẽ hiển thị nhãn `walletUnavailable` và báo
`snapshot.error.code: 'wallet_unavailable'` khi có thể bắt đầu một khoản thanh toán khác một cách an toàn; hãy đưa ra phương thức khác trong
bộ chọn của bạn. Khóa bộ chọn khi `snapshot.busy` là true hoặc khoản thanh toán đang ở trạng thái `pending` hay `action_required`.

### Frame và CSP [#frames-and-csp]

SDK thêm `allow="payment"` vào frame của nó. Nếu cửa hàng của bạn nằm trong một frame khác, frame bên ngoài đó cũng phải cho phép
thanh toán. Apple Pay khác origin (cross-origin) yêu cầu Safari 17 trở lên. Các script của nhà cung cấp ví nằm bên trong frame của Tapaya, nên
trang của bạn không cần thêm origin nào của nhà cung cấp vào CSP.

## Xử lý quyền truy cập trình duyệt đã hết hạn [#handle-expired-browser-access]

Client secret hết hạn sau 24 giờ, và khi đó `clientSecret` là `null`. Một khoản thanh toán chưa từng được gửi khi đó
sẽ bị hủy, và lần gọi `prepare()` tiếp theo bắt đầu một lần thử mới cho cùng đơn hàng. Một khoản thanh toán đang chờ vẫn ở trạng thái `pending`
trong trình duyệt, và SDK tiếp tục gọi endpoint của bạn cho đến khi máy chủ của bạn báo kết quả của nó dưới dạng `status`. Việc đọc
từ máy chủ vẫn hoạt động, nên dù thế nào tác vụ định kỳ của bạn cũng sẽ xử lý xong khoản thanh toán đó.

## Chẩn đoán lỗi máy chủ [#diagnose-server-errors]

Node SDK ném `PaymentsApiError` cho các yêu cầu API bị từ chối. Kiểm tra `status`, `code`, `message`,
`fieldErrors` và `requestId` của lỗi trên máy chủ. Không để thông tin xác thực và client secret xuất hiện trong log, và hiển thị
nội dung dành cho khách hàng do chính bạn soạn.

## Khớp với thương hiệu của bạn [#match-your-branding]

Các trường nhập thẻ dùng màu checkout đang có hiệu lực của merchant từ [Cài đặt
Checkout](/online/integration/checkout_settings#wallets-and-appearance) làm màu nhấn khi focus (`colorFocus`).
Các giá trị bạn truyền trong `appearance.variables` sẽ ghi đè màu này, và chỉ những biến bạn đặt mới được gửi đi.
