> For the complete documentation index, see [llms.txt](https://docs.loomgate.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.loomgate.io/integrations/browser.md).

# Form thanh toán (loomgate.js)

Gắn form thẻ vào trang checkout và xác nhận thanh toán trong trình duyệt.

`loomgate.js` hiện form thẻ (kèm Apple Pay, Google Pay khi tên miền đã được kích hoạt), xác nhận thanh toán, chạy bước 3-D Secure và tự xử lý các tình huống như thẻ bị từ chối hay số tiền đã đổi.

## Nạp loomgate.js

{% tabs %}
{% tab title="Thẻ script" %}

```html
<script src="https://api.loomgate.io/partner/v1/loomgate.js"></script>
<script>
  window.Loomgate.loadLoomgate('pk_live_…').then((loomgate) => {
    // ...
  });
</script>
```

Script tự suy ra địa chỉ API từ URL của chính nó.
{% endtab %}

{% tab title="npm" %}

```js
import { loadLoomgate } from '@loompay/loomgate-js-sdk';

const loomgate = await loadLoomgate('pk_live_…', {
  apiBaseUrl: 'https://api.loomgate.io', // tuỳ chọn: đây là mặc định, chỉ truyền khi gọi API khác
  locale: 'en', // 'en' (mặc định), mã ngôn ngữ khác, hoặc 'auto' theo trình duyệt
  appearance: { theme: { appearance: 'light', accentColor: 'blue' } },
});
```

{% endtab %}
{% endtabs %}

Gọi `loadLoomgate()` một lần cho mỗi trang.

## Gắn form thẻ

```html
<div id="loomgate-payment"></div>
<div id="loomgate-branding"></div>
```

```js
const session = loomgate.payment({ clientSecret: 'lg_cs_…' });
await session.mount({ payment: '#loomgate-payment', branding: '#loomgate-branding' });
```

* `branding` là **bắt buộc**: đây là phần thương hiệu của đối tác thanh toán, thiếu nó thì form không thanh toán được và `mount()` báo `mount_target_missing`.
* `mount()` nhận selector CSS hoặc phần tử DOM. Gọi hai lần vẫn chỉ có một form.
* Form thẻ không thu email, tên và địa chỉ: bạn tự thu trên trang checkout và truyền vào `confirm()`.

### Sự kiện

`session.on(tên, handler)` trả về hàm hủy đăng ký.

| Sự kiện  | Khi nào                                                                | Dữ liệu                                                                                                                                 |
| -------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `ready`  | Form vẽ xong lần đầu                                                   | —                                                                                                                                       |
| `change` | Người mua nhập thẻ / đổi phương thức, hoặc số tiền của payment vừa đổi | `{ complete, amountTotal, currency }`. `complete` để bật nút Pay; `amountTotal` (đơn vị nhỏ nhất) là số tiền sẽ bị trừ, hãy hiển thị nó |
| `error`  | Form không tải được                                                    | `{ type, code, message }`                                                                                                               |

## Xác nhận thanh toán

```js
payButton.addEventListener('click', () => {
  // Gọi confirm() ngay trong handler click, KHÔNG await gì trước nó:
  // Apple Pay và Google Pay cần đúng cú click của người mua.
  session
    .confirm({
      billingDetails: {
        email: 'buyer@example.com',
        name: 'Jane Doe',
        address: { line1: '1 Market St', city: 'San Francisco', state: 'CA', postal_code: '94105', country: 'US' },
      },
      returnUrl: 'https://shop.example/checkout/return', // tuỳ chọn, phải là https://
    })
    .then((result) => {
      if (result.error) {
        showError(result.error.message); // tiếng Anh, hiển thị thẳng cho người mua được
        return;
      }
      // result = { status: 'succeeded' | 'processing', paymentIntentId }
      location.assign('/orders/1042/thanks');
    });
});
```

* `billingDetails.address` cần `line1`, `postal_code`, `country` (ISO 3166-1 alpha-2); `line2`, `city`, `state` tuỳ chọn. Độ dài tối đa: email 254, `postal_code` 20, các chuỗi khác 200. Thiếu hoặc sai → `validation_error` kèm `issues[]`, chưa gọi mạng.
* `confirm()` **không bao giờ throw**: mọi lỗi nằm trong `result.error`. Bấm Pay hai lần chỉ gửi một yêu cầu.
* Thẻ bị từ chối: `loomgate.js` tự release để người mua thử thẻ khác ngay trên form đó.
* Số tiền đã đổi (`payment_intent_updated`): `loomgate.js` tải lại form với số tiền mới và phát `change`; người mua xem lại và bấm Pay lần nữa.
* `processing` **chưa phải đã thu tiền**: chỉ giao hàng khi máy chủ nhận webhook `payment_intent.succeeded`.

### Lỗi thường gặp

| `code`                                                         | Nghĩa                                                  | Nên làm                                                    |
| -------------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------- |
| `payment_method_declined`                                      | Thẻ bị từ chối (có thể kèm `declineCode`)              | Mời người mua dùng thẻ khác                                |
| `payment_failed`                                               | Bước xác thực (3-D Secure) thất bại hoặc bị hủy        | Mời thử lại                                                |
| `payment_canceled`                                             | Người mua đóng cửa sổ ví                               | Không cần làm gì                                           |
| `payment_intent_updated`                                       | Số tiền đã đổi từ lúc form tải; chưa trừ tiền          | Hiển thị số tiền mới, cho bấm Pay lại                      |
| `payment_intent_not_pending`                                   | Payment đã xong, đã hủy, hoặc đã dùng hết số lần thử   | Tạo payment mới, cho người mua bắt đầu lại                 |
| `payment_details_missing`                                      | Payment thiếu người mua, mã đơn hoặc địa chỉ giao hàng | Cập nhật payment từ máy chủ rồi cho bấm lại                |
| `rate_limited`                                                 | Quá nhiều lần thử trong 10 phút                        | Thử lại sau ít phút                                        |
| `request_timeout`, `payment_processing`, `next_action_timeout` | Chưa rõ kết quả                                        | Kiểm tra trạng thái đơn trên máy chủ trước khi cho thử lại |
| `payment_form_unavailable`                                     | Không tải được form thẻ (mạng, CSP)                    | Gọi lại `mount()`                                          |

## Xác nhận từ máy chủ

Khi nền tảng của bạn bắt buộc xác nhận ở máy chủ:

```js
// 1. Trình duyệt, trong handler click: chỉ tạo token
const { confirmationToken, error } = await session.createConfirmationToken(billingDetails);
// gửi confirmationToken + billingDetails lên máy chủ

// 2. Máy chủ: POST /partner/v1/payment_intents/{id}/confirm (secret key)
//    - 402 card_error  → POST /partner/v1/payment_intents/{id}/release, báo bị từ chối
//    - 409 payment_intent_updated → báo tổng tiền đã đổi, tạo session mới để hiện lại form
//    - 200 → trả next_action?.client_secret về trình duyệt

// 3. Trình duyệt, nếu có next_action:
const next = await session.handleNextAction(nextActionClientSecret); // { status } | { error }
```

`session.handleNextAction()` tự release khi bước tiếp theo thất bại. Khi cần release từ trình duyệt, gọi `session.release()`; chỉ mời thử lại khi nó trả `status: 'pending'`.

## Vòng đời session

* `session.destroy()` gỡ form và dừng mọi việc đang chạy. Session đã destroy không mount lại được: tạo session mới bằng `loomgate.payment(...)`.
* Thời gian chờ tối đa: tải form 60 giây, xác nhận 180 giây, release 10 giây. Quá hạn trả `request_timeout`.

## Content-Security-Policy

Form thẻ được tải trong iframe từ máy chủ của đối tác thanh toán. Nếu trang của bạn có CSP, cần cho phép:

* `connect-src` và (khi dùng thẻ script) `script-src`: `https://api.loomgate.io`;
* `script-src` và `frame-src`: tên miền của form thẻ. Liên hệ Loomgate để lấy danh sách hiện hành.

Thiếu các mục này thì form không hiện và `mount()` báo `payment_form_unavailable`.

## Quyền riêng tư

`loomgate.js` thu dữ liệu thiết bị của người mua để chống gian lận. Chính sách quyền riêng tư của bạn phải nêu điều này. Xem [Chi tiết giao dịch](/concepts/payment-details.md).

## Ví dụ đầy đủ

[Kho ví dụ loomgate-examples](https://github.com/loomgroup/loomgate-examples) có trang checkout chạy được cho từng cách nạp `loomgate.js`, kèm phần máy chủ tạo payment:

* [`examples/js`](https://github.com/loomgroup/loomgate-examples/tree/main/examples/js): JS SDK cài từ npm, đóng gói bằng Vite.
* [`examples/js-cdn`](https://github.com/loomgroup/loomgate-examples/tree/main/examples/js-cdn): thẻ script `https://api.loomgate.io/partner/v1/loomgate.js`, không cần bundler. Nên dùng cách này khi bạn không có bundler.
* [`examples/js-umd`](https://github.com/loomgroup/loomgate-examples/tree/main/examples/js-umd): bản build UMD tải từ jsDelivr, ghim phiên bản và kiểm tra bằng SRI.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.loomgate.io/integrations/browser.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
