<!-- Source: https://docs.infraio.xyz/vi/sdks/javascript -->
<!-- Last updated: 2026-10-04 -->

# JavaScript / SDK trình duyệt

`@lartech/infraio-checkout-js` là SDK duy nhất chúng tôi phát hành cho
đến nay. Nó chạy trong trình duyệt và mở trang thanh toán dựng sẵn.
Hiện chưa có SDK backend. Hãy gọi API trực tiếp; [Bắt đầu nhanh](https://docs.infraio.xyz/vi/get-started/quickstart)
có kèm helper ký HMAC.

- Phiên bản hiện tại: `0.1.1-beta.17` (pre-1.0; có thể có thay đổi minor breaking)
- Định dạng: **ESM** (`index.js`), **CJS** (`index.cjs`), **IIFE** (`index.global.js`)
- Types được bundle (`index.d.ts`)
- Không có runtime peer — không React, jQuery, hay dependency khác

> **Note:**
>
> **Không có server-side entry.** Helper xác thực chữ ký cho webhook
> không được bundle — hãy tự cài đặt với `crypto` (trang
> [xác thực chữ ký](https://docs.infraio.xyz/vi/webhooks/signature-verification) có code
> copy-paste trong 4 ngôn ngữ).

## Cài đặt

**npm**

```bash
npm install @lartech/infraio-checkout-js
```

**pnpm**

```bash
pnpm add @lartech/infraio-checkout-js
```

**yarn**

```bash
yarn add @lartech/infraio-checkout-js
```

**bun**

```bash
bun add @lartech/infraio-checkout-js
```

**CDN**

```html
<script src="https://unpkg.com/@lartech/infraio-checkout-js/dist/index.global.js"></script>
<script>
  const sdk = await InfraIo.loadInfraIo("pk_live_yourkeyhere");
  sdk.checkout({ sessionId, checkoutUrl });
</script>
```

## `loadInfraIo(publicKey, options?)`

Trả về `Promise<InfraIoInstance>`.

```ts
import { loadInfraIo } from "@lartech/infraio-checkout-js";

const sdk = await loadInfraIo("pk_live_yourkeyhere", {
  // Tùy chọn. Chỉ override khi trỏ đến môi trường không phải prod.
  checkoutUrl: "https://checkout-dev.infraio.xyz",
});
```

| Param | Type | Bắt buộc | Ghi chú |
| --- | --- | --- | --- |
| `publicKey` | `string` | ✓ | Phải khớp `pk_(live\|test)_…` |
| `options.checkoutUrl` | `string` | — | Override base checkout URL. Mặc định: `https://checkout.infraio.xyz`. Trang checkout tự động kết nối đến API tương ứng. |

## `sdk.checkout({ … })`

Mở trang thanh toán dựng sẵn. Trả về `void` (dùng callback cho trạng thái).

| Field | Type | Bắt buộc | Ghi chú |
| --- | --- | --- | --- |
| `sessionId` | `string` | ✓ | `session_key` trả về bởi `POST /b2b/v1/checkout-sessions/quick` |
| `checkoutUrl` | `string` | — | URL đầy đủ trả về bởi cùng endpoint. Nếu bỏ qua, SDK dựng từ `checkoutUrl` của `loadInfraIo()` (hoặc mặc định) + `sessionId` |
| `mode` | `"popup" \| "redirect" \| "embed"` | — | Mặc định `"popup"` |
| `container` | `string \| HTMLElement` | chỉ embed | CSS selector hoặc DOM element nơi iframe mount |
| `width` | `number` | — | Chỉ popup. Mặc định `560`. Kẹp `[320, 1280]` |
| `height` | `number` | — | Chỉ popup. Mặc định `780`. Kẹp `[400, 1000]` |
| `timeoutMs` | `number` | — | Chỉ popup. Timeout load iframe. Mặc định `30000`. Gửi `0` để tắt |
| `locale` | `string` | — | Tag BCP-47 forward dưới dạng `?locale=` đến trang checkout (`en`, `vi`, `ja`, `ko`, `es`, `pt-BR`, `ru`, `tr`, `zh-CN`, `zh-TW`) |
| `hideSummary` | `boolean` | — | Ẩn cột tóm tắt order. Mặc định `false` |
| `hideHeader` | `boolean` | — | Ẩn header InfraIO Pay và nút wallet-connect tích hợp sẵn. Mặc định `false`. Kết hợp với `walletAddress` để white-label đầy đủ |
| `walletAddress` | `string` | — | Kết nối sẵn ví của người mua. Yêu cầu `onSignRequest` |
| `walletChainId` | `number` | — | EVM chain ID cho ví được kết nối sẵn |
| `onReady` | `() => void` | — | Phát khi iframe interactive. Chỉ popup/embed |
| `onSignRequest` | `(req: { method: string; params: unknown[] }) => Promise<string>` | khi `walletAddress` set | SDK proxy wallet RPC đến handler của bạn; trả về hex đã ký |
| `onSuccess` | `({ sessionId }) => void` | — | Phát khi thanh toán thành công. **Không phải nguồn xác thực — webhook mới là** |
| `onCancel` | `() => void` | — | Phát khi người mua đóng popup/embed mà không thanh toán |
| `onError` | `(err: InfraIoError) => void` | — | Phát khi iframe load fail (chế độ popup + embed). Argument không hợp lệ được throw đồng bộ, **không** được giao qua đây. Chế độ redirect không có runtime error surface — failure được quan sát ở trang đã redirect đến. |

> **Warning:**
>
> `onSuccess` **không** phải nguồn xác thực. Nó có thể được phát ngay
> cả khi webhook sau đó xác định thanh toán đã fail (reorg testnet,
> timing phía người mua). Chỉ dùng nó cho UX (hiển thị "Thanks!",
> redirect). Luôn xác nhận qua webhook trước khi fulfillment.

## `sdk.close()`

Đóng programmatically popup hoặc embed đang mở. No-op cho chế độ
redirect.

```ts
sdk.checkout({ sessionId, mode: "popup", /* … */ });
// sau đó, ví dụ khi người dùng điều hướng đi
sdk.close();
```

## `sdk.openRefundRequest({ … })`

Mở **form refund-request do hệ thống host** cho một token dùng một lần
mà backend của bạn đã phát hành qua `POST /b2b/v1/merchants/{merchant_id}/refund-requests`.
Người mua điền địa chỉ đích refund + lý do + metadata (tùy chọn) trên
trang của chúng tôi; trang của bạn chỉ xử lý vòng đời mở/đóng. Trả về
một **hàm `close()`** — gọi nó để programmatically đóng popup hoặc
detach embed iframe. Trong chế độ redirect, hàm trả về là no-op.

> **Note:**
>
> Form sống tại `https://checkout.infraio.xyz/refund-request/:token`.
> Method này chỉ bọc URL đó trong popup / redirect / embed để người
> mua không bao giờ rời domain của bạn (trong popup / embed) hoặc
> quay lại nó tự động (trong redirect). Endpoint mà backend của bạn
> gọi để phát hành token là `POST /b2b/v1/merchants/{merchant_id}/refund-requests` — ký
> HMAC với khóa secret của bạn, cùng auth như phần còn lại của surface
> B2B. Xem [Khái niệm → Hoàn tiền](https://docs.infraio.xyz/vi/concepts/refunds#phat-hanh-qua-b2b-api).

```ts
import { loadInfraIo } from "@lartech/infraio-checkout-js";

const sdk = await loadInfraIo("pk_live_yourkeyhere");

// Phát hành token phía server, rồi giao nó cho SDK trong trình duyệt.
const { token } = await fetch("/api/mint-refund-token", { method: "POST" }).then(r => r.json());

const close = sdk.openRefundRequest({
  token,
  mode: "popup",
  onSuccess: ({ linkToken, refundId }) => {
    // Người mua đã submit form.
    // linkToken → trang trạng thái /r/:linkToken (chia sẻ cho người mua).
    // refundId  → dùng với B2B API để approve / reject.
    window.location.href = `/r/${linkToken}`;
  },
  onCancel: () => { /* người mua đã đóng mà không submit */ },
});

// Đóng popup programmatically sau đó nếu cần:
// close();
```

| Field | Type | Bắt buộc | Ghi chú |
| --- | --- | --- | --- |
| `token` | `string` | ✓ | Token `rfqt_…` trả về bởi `POST /b2b/v1/merchants/{merchant_id}/refund-requests` |
| `mode` | `"popup" \| "redirect" \| "embed"` | — | Mặc định `"popup"`. Cùng surface semantics như `sdk.checkout()` — xem [Ghi chú chế độ](#ghi-chu-che-do) |
| `container` | `string \| HTMLElement` | chỉ embed | CSS selector hoặc DOM element nơi iframe mount |
| `locale` | `string` | — | Tag BCP-47 forward dưới dạng `?locale=` (`en`, `vi`, `ja`, `ko`, `es`, `pt-BR`, `ru`, `tr`, `zh-CN`, `zh-TW`) |
| `hideHeader` | `boolean` | — | Ẩn header InfraIO Pay bên trong iframe. Trong popup/embed, SDK tự vẽ modal chrome riêng, nên header của trang thường không cần thiết. Mặc định `false` |
| `hideSummary` | `boolean` | — | Ẩn cột Order Summary, chỉ hiển thị form refund. Mặc định `false` |
| `walletAddress` | `string` | — | Điền sẵn trường ví đích (`?wallet_address=`). Cho phép một merchant đã biết ví người mua bỏ qua việc nhập lại thủ công |
| `onSuccess` | `(data: { linkToken: string; refundId: string }) => void` | — | Phát sau khi người mua submit form. `linkToken` → trang trạng thái `/r/:linkToken` để chia sẻ với người mua. `refundId` → dùng với B2B API để approve / reject |
| `onCancel` | `() => void` | — | Phát khi người mua đóng popup/embed mà không submit |
| `onError` | `(err: InfraIoError) => void` | — | Phát khi iframe load fail hoặc args không hợp lệ. Trường hợp token hết hạn / hủy được trang do hệ thống host xử lý, không qua `onError` |

**Trạng thái token được surface qua `onError`**

Nếu người mua mở một token đã cũ, bản thân trang xử lý hiển thị (render
prompt "Expired — request new link", v.v.), và SDK **không** phát
`onError` cho các trường hợp đó — người mua đang ở trong luồng form
và code của bạn không cần phản ứng. `onError` chỉ phát cho những thứ
code của bạn có thể xử lý (args sai, network failure khi load iframe).

> **Warning:**
>
> `openRefundRequest` ghi nhận intent refund — nó **không** di chuyển
> vốn. Sau `onSuccess`, bản ghi refund ở trạng thái `PENDING` (hoặc
> `APPROVED` nếu cấu hình merchant của bạn auto-approve refund của
> khách hàng). Bạn vẫn cần ký và broadcast giao dịch chuyển on-chain
> từ ví của bạn, rồi POST tx hash đến
> `POST /b2b/v1/refunds/:id/submit-tx`. Xem
> [Khái niệm → Hoàn tiền](https://docs.infraio.xyz/vi/concepts/refunds) cho vòng đời đầy đủ.

## Class Error

```ts
import { InfraIoError } from "@lartech/infraio-checkout-js";

sdk.checkout({
  sessionId,
  onError: (err: InfraIoError) => {
    switch (err.code) {
      case "invalid_request_error":   /* sessionId / args sai */ break;
      case "iframe_load_error":       /* iframe load fail */ break;
      case "iframe_timeout_error":    /* vượt timeoutMs */ break;
      case "already_open_error":      /* một checkout khác đang mở */ break;
      case "network_error":           /* vấn đề mạng tạm thời với origin checkout */ break;
      case "api_error":               /* backend trả về non-2xx cho call do SDK phát */ break;
    }
  },
});
```

`sdk.openRefundRequest()` chỉ throw `invalid_request_error` (thiếu
hoặc token / args không hợp lệ), đồng bộ. `iframe_load_error` (iframe
refund-request load fail) được giao **bất đồng bộ qua `onError`**,
không throw. Nó **không** phát `iframe_timeout_error` hay
`already_open_error` — popup refund-request không có load-timeout và
cho phép nhiều popup đồng thời.

## Ghi chú chế độ

### Popup
- Overlay căn giữa với backdrop nửa trong suốt tối
- `z-index: 2147483647` (max int32) — nằm trên mọi thứ khác
- Cuộn body bị khóa khi mở; phục hồi khi đóng
- Nút close nhận focus ban đầu; Tab bị bẫy trong popup
- Đóng bởi: nút close, Escape, click bên ngoài, `sdk.close()`. Tất cả
  đều gọi `onCancel`
- Trang checkout có thể yêu cầu resize qua `postMessage` — SDK kẹp
  trong giới hạn `width`/`height`

### Redirect
- Điều hướng cứng qua `window.location.href`
- Tự động nối thêm `?return_url=<current-page>` để người mua quay về
  nơi họ đến. Nếu `success_url` / `cancel_url` trên session đã cover
  điều này, round-trip bỏ qua `return_url`

### Embed
- iframe với `allow="payment; clipboard-write"` (chỉ thị HTML5 Feature
  Policy — không phải thuộc tính `sandbox`). iframe được serve từ
  origin checkout, nên popup ví và clipboard write từ phía người mua
  hoạt động mà không cần opt-in thêm.
- Container width 100%; height tự sizing qua postMessage
  `INFRAIO_RESIZE`, kẹp `[200, 2000]px`
- Không có CSS isolation ngoài biên iframe — style trang parent của
  bạn không bleed vào
- Luôn wire `onReady` để bạn có thể ẩn loading state của riêng mình
  khi checkout trở nên interactive

## TypeScript

Tất cả types được bundle. Các export hữu ích nhất:

```ts
import type {
  CheckoutOptions,
  RefundRequestOptions,
  LoadOptions,
  InfraIoInstance,
  InfraIoErrorCode,
} from "@lartech/infraio-checkout-js";
import { loadInfraIo, InfraIoError, VERSION } from "@lartech/infraio-checkout-js";
```

`VERSION` là chuỗi phiên bản của SDK — hữu ích trong bug report.
