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

# JavaScript / 浏览器 SDK

`@lartech/infraio-checkout-js` 是我们目前发布的唯一 SDK。它运行在浏览器中,
并打开我们的托管收银台。目前还没有后端 SDK,请直接调用 API;
[快速开始](https://docs.infraio.xyz/zh-CN/get-started/quickstart) 提供了 HMAC 签名 helper。

- 当前版本:`0.1.1-beta.17`(1.0 之前;可能有小幅破坏性变更)
- 格式:**ESM** (`index.js`)、**CJS** (`index.cjs`)、**IIFE** (`index.global.js`)
- 已打包类型定义 (`index.d.ts`)
- 零运行时对等依赖 — 不依赖 React、jQuery 或其他库

> **Note:**
>
> **没有服务端入口**。Webhook 签名验证 helper 未打包 —
> 请用 `crypto` 自行实现([签名验证页](https://docs.infraio.xyz/zh-CN/webhooks/signature-verification)
> 提供了 4 种语言的可复制粘贴代码)。

## 安装

**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?)`

返回 `Promise<InfraIoInstance>`。

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

const sdk = await loadInfraIo("pk_live_yourkeyhere", {
  // 可选。仅当指向非生产环境时才覆盖。
  checkoutUrl: "https://checkout-dev.infraio.xyz",
});
```

| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `publicKey` | `string` | ✓ | 必须匹配 `pk_(live\|test)_…` |
| `options.checkoutUrl` | `string` | — | 覆盖结账基础 URL。默认:`https://checkout.infraio.xyz`。结账页会自动连接到对应的 API。 |

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

打开托管收银台。返回 `void`(状态通过回调获取)。

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `sessionId` | `string` | ✓ | 由 `POST /b2b/v1/checkout-sessions/quick` 返回的 `session_key` |
| `checkoutUrl` | `string` | — | 同一端点返回的完整 URL。如果省略,SDK 会根据 `loadInfraIo()` 的 `checkoutUrl`(或默认值)+ `sessionId` 构造 |
| `mode` | `"popup" \| "redirect" \| "embed"` | — | 默认 `"popup"` |
| `container` | `string \| HTMLElement` | 仅 embed | iframe 挂载的 CSS 选择器或 DOM 元素 |
| `width` | `number` | — | 仅 popup。默认 `560`。限制在 `[320, 1280]` |
| `height` | `number` | — | 仅 popup。默认 `780`。限制在 `[400, 1000]` |
| `timeoutMs` | `number` | — | 仅 popup。Iframe 加载超时。默认 `30000`。传 `0` 关闭 |
| `locale` | `string` | — | BCP-47 标签,作为 `?locale=` 转发到结账页(`en`、`vi`、`ja`、`ko`、`es`、`pt-BR`、`ru`、`tr`、`zh-CN`、`zh-TW`) |
| `hideSummary` | `boolean` | — | 隐藏订单摘要列。默认 `false` |
| `hideHeader` | `boolean` | — | 隐藏 InfraIO Pay 页头与内建的钱包连接按钮。默认 `false`。配合 `walletAddress` 可做完全白标 |
| `walletAddress` | `string` | — | 预连接买家钱包。需要同时提供 `onSignRequest` |
| `walletChainId` | `number` | — | 预连接钱包的 EVM chain ID |
| `onReady` | `() => void` | — | iframe 可交互后触发。仅 popup/embed |
| `onSignRequest` | `(req: { method: string; params: unknown[] }) => Promise<string>` | 设置 `walletAddress` 时必填 | SDK 把钱包 RPC 代理给你的 handler;返回已签名的十六进制 |
| `onSuccess` | `({ sessionId }) => void` | — | 支付成功时触发。**非权威 — Webhook 才是** |
| `onCancel` | `() => void` | — | 买家未支付就关闭弹窗/嵌入时触发 |
| `onError` | `(err: InfraIoError) => void` | — | iframe 加载失败时触发(popup + embed 模式)。参数非法会同步抛出,**不会**通过这里传递。Redirect 模式没有运行时错误表面 — 失败在跳转后的页面里观察。 |

> **Warning:**
>
> `onSuccess` **不是**权威。即便 Webhook 后续判定支付失败(测试网重组、
> 买家侧时序问题),它也可能触发。仅将其用于 UX(显示"感谢!"、
> 跳转)。履约前请始终通过 Webhook 确认。

## `sdk.close()`

通过代码关闭已打开的 popup 或 embed。redirect 模式下为空操作。

```ts
sdk.checkout({ sessionId, mode: "popup", /* … */ });
// 之后,例如用户导航离开时
sdk.close();
```

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

为你的后端通过 `POST /b2b/v1/merchants/{merchant_id}/refund-requests`
铸造的一次性 token 打开托管**退款申请表单**。买家在我们页面填写退款
目标地址 + 原因 +(可选)元数据;你的页面只处理打开/关闭生命周期。
返回**一个 `close()` 函数** — 调用它可通过代码关闭弹窗或卸载嵌入的
iframe。redirect 模式下返回的函数为空操作。

> **Note:**
>
> 表单位于 `https://checkout.infraio.xyz/refund-request/:token`。
> 此方法只是把该 URL 包装成 popup / redirect / embed,这样买家就不会
> 离开你的域名(popup / embed)或自动返回(redirect)。后端用来铸造
> token 的端点是 `POST /b2b/v1/merchants/{merchant_id}/refund-requests` —
> 用你的 secret key 做 HMAC 签名,与 B2B 表面其他接口的鉴权方式一致。
> 详见 [概念 → 退款](https://docs.infraio.xyz/zh-CN/concepts/refunds#mint-via-b2b-api)。

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

const sdk = await loadInfraIo("pk_live_yourkeyhere");

// 在服务端铸造 token,再把它交给浏览器中的 SDK。
const { token } = await fetch("/api/mint-refund-token", { method: "POST" }).then(r => r.json());

const close = sdk.openRefundRequest({
  token,
  mode: "popup",
  onSuccess: ({ linkToken, refundId }) => {
    // 买家已提交表单。
    // linkToken → /r/:linkToken 状态页(分享给买家)。
    // refundId  → 用于 B2B API 审批 / 驳回。
    window.location.href = `/r/${linkToken}`;
  },
  onCancel: () => { /* 买家未提交就关闭了 */ },
});

// 后续如需通过代码关闭弹窗:
// close();
```

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `token` | `string` | ✓ | `POST /b2b/v1/merchants/{merchant_id}/refund-requests` 返回的 `rfqt_…` token |
| `mode` | `"popup" \| "redirect" \| "embed"` | — | 默认 `"popup"`。与 `sdk.checkout()` 表面语义相同 — 参见 [模式说明](#mode-notes) |
| `container` | `string \| HTMLElement` | 仅 embed | iframe 挂载的 CSS 选择器或 DOM 元素 |
| `locale` | `string` | — | BCP-47 标签,作为 `?locale=` 转发(`en`、`vi`、`ja`、`ko`、`es`、`pt-BR`、`ru`、`tr`、`zh-CN`、`zh-TW`) |
| `hideHeader` | `boolean` | — | 隐藏 iframe 内的 InfraIO Pay 页头。在 popup/embed 中 SDK 自行绘制模态框架,所以页头通常没有必要。默认 `false` |
| `hideSummary` | `boolean` | — | 隐藏订单摘要列,仅显示退款表单。默认 `false` |
| `walletAddress` | `string` | — | 预填目标钱包字段 (`?wallet_address=`)。当商户已知买家钱包时,免去手动重新输入 |
| `onSuccess` | `(data: { linkToken: string; refundId: string }) => void` | — | 买家提交表单后触发。`linkToken` → `/r/:linkToken` 状态页,可分享给买家。`refundId` → 用于 B2B API 审批 / 驳回 |
| `onCancel` | `() => void` | — | 买家未提交就关闭弹窗/嵌入时触发 |
| `onError` | `(err: InfraIoError) => void` | — | iframe 加载失败或参数非法时触发。Token 过期 / 取消由托管页处理,不通过 `onError` |

**通过 `onError` 暴露的 token 状态**

如果买家打开的是过期 token,页面自身会处理展示(渲染"已过期 — 申请新链接"
提示等),SDK **不会**为这些情况触发 `onError` — 买家身处表单流程中,你
的代码无需响应。`onError` 只在你的代码可处理的情况下触发(参数错误、
加载 iframe 时网络失败)。

> **Warning:**
>
> `openRefundRequest` 记录退款意向 — 它**不会**转移资金。`onSuccess`
> 之后,退款记录处于 `PENDING`(如果你的商户配置自动批准客户退款,则
> 为 `APPROVED`)。你仍需要从资金库钱包签名并广播链上转账,然后将 tx
> hash 提交到 `POST /b2b/v1/refunds/:id/submit-tx`。完整生命周期见
> [概念 → 退款](https://docs.infraio.xyz/zh-CN/concepts/refunds)。

## Error 类

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

sdk.checkout({
  sessionId,
  onError: (err: InfraIoError) => {
    switch (err.code) {
      case "invalid_request_error":   /* sessionId / 参数无效 */ break;
      case "iframe_load_error":       /* iframe 加载失败 */ break;
      case "iframe_timeout_error":    /* 超过 timeoutMs */ break;
      case "already_open_error":      /* 已有一个 checkout 打开中 */ break;
      case "network_error":           /* 与结账 origin 通信的短时网络问题 */ break;
      case "api_error":               /* SDK 发起的调用上后端返回非 2xx */ break;
    }
  },
});
```

`sdk.openRefundRequest()` 只会同步抛出 `invalid_request_error`(缺少
或无效的 token / 参数)。`iframe_load_error`(退款申请 iframe 加载失败)
会**异步通过 `onError` 传递**,而不是被抛出。它**不会**触发
`iframe_timeout_error` 或 `already_open_error` — 退款申请弹窗没有加载
超时,并允许多个并发弹窗。

## 模式说明

### Popup
- 居中覆盖层,背后为半透明深色蒙版
- `z-index: 2147483647`(int32 最大值)— 位于其他一切之上
- 打开时锁定 body 滚动;关闭后恢复
- 关闭按钮获得初始焦点;Tab 被困在弹窗内
- 关闭方式:关闭按钮、Escape、点击外部、`sdk.close()`。以上方式都会触发 `onCancel`
- 结账页可通过 `postMessage` 请求调整大小 — SDK 在 `width`/`height` 范围内夹紧

### Redirect
- 通过 `window.location.href` 硬跳转
- 自动追加 `?return_url=<current-page>`,这样买家会回到出发处。如果
  会话上的 `success_url` / `cancel_url` 已覆盖这点,往返会忽略
  `return_url`

### Embed
- iframe 带 `allow="payment; clipboard-write"`(HTML5 Feature Policy
  指令 — 不是 `sandbox` 属性)。iframe 由结账 origin 提供,所以
  买家侧的钱包弹窗与剪贴板写入无需进一步授权即可工作。
- 容器宽度为 100%;高度通过 `INFRAIO_RESIZE` postMessage 自动调整,
  夹紧在 `[200, 2000]px`
- iframe 边界外没有 CSS 隔离 — 父页面样式不会渗入
- 始终接好 `onReady`,以便结账可交互时隐藏你自己的加载态

## TypeScript

所有类型已打包。最有用的导出:

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

`VERSION` 是 SDK 自己的版本字符串 — 提 bug 时有用。
