> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fluz.app/llms.txt
> Use this file to discover all available pages before exploring further.

# 卡片信息展示

> 在你的页面中挂载由 Fluz 托管的查看器，向用户展示其 PAN、有效期和 CVV，而无需这些数据接触你的服务器或页面的 JavaScript。

<Note>
  本页假定你已阅读[安全元素概览](/build-a-platform/secure-elements-overview)——其中涵盖了铸造客户端令牌、加载 SDK 和样式化字段，这些内容在此同样适用。
</Note>

## 在线查看

<iframe
  src="https://demo.secure.fluz.app/"
  title="Fluz Secure Elements — Card Reveal 演示"
  loading="lazy"
  style={{
width: "100%",
height: "720px",
border: "1px solid #e5e5e5",
borderRadius: "8px",
}}
/>

该演示会自行铸造令牌并自动挂载查看器。若展示按钮无响应，可使用**Remint token & remount**，并通过**Reveal**、**Reveal CVV only**或**Mask**尝试下文介绍的字段级控制。[在单独标签页中打开 →](https://demo.secure.fluz.app/)

## 铸造展示令牌

调用[`POST /v1/client-token`](/build-a-platform/secure-elements-overview#mint-a-client-token)，传入`"purpose": "reveal"`以及你想展示的`virtualCardId`：

```json theme={null}
{
  "purpose": "reveal",
  "virtualCardId": "c107e50b-10f3-449c-92c0-609d9a8cfa2a"
}
```

将返回的`clientToken`和`loadToken`直接传递给下方的`createCardViewer`。

## 创建查看器

```js theme={null}
const viewer = createCardViewer({
  clientToken,
  loadToken,
  frameHostOrigin: "https://staging.secure.fluz.app",
  fields: ["pan", "expiry", { field: "cvv", individualReveal: false }],
});
```

`fields`控制按顺序渲染卡片的哪些部分——若省略，你将获得`["pan", "expiry", "cvv"]`。每一项要么是字段名本身，要么是一个`{ field, individualReveal }`对象；`"pan"`是`{ field: "pan", individualReveal: true }`的简写。仅支持`pan`、`expiry`和`cvv`这三个字段名——其他任何值都会在调用`createCardViewer`时同步抛出`FluzElementsError`（`error.code === "INVALID_FIELD"`），在你调用`mount()`之前就会报错。

| Field    | 展示前的掩码占位符                | 说明                               |
| :------- | :----------------------- | :------------------------------- |
| `pan`    | `•••• •••• •••• {last4}` | 后四位来自你的客户端令牌上的卡片元数据              |
| `expiry` | 真实的`MM/YYYY` —— 不做掩码     | 不视为敏感信息                          |
| `cvv`    | `•••`                    | 支持`individualReveal: false`（见下文） |

<Note>
  `individualReveal`在每个字段上默认为`true`。将其设为`false`可阻止单独展示该字段——参见[仅展示单个字段](#reveal-a-single-field)。它与`reveal()`相互独立，后者无论如何都会一次性展示所有字段。
</Note>

## 挂载

```js theme={null}
await viewer.mount(document.getElementById("card-viewer"));
```

`mount()`会将每个已配置字段各自作为一个受沙盒限制的 iframe 追加到你传入的容器元素中——默认`fields`会生成三个独立的 iframe，而非一个合并的 iframe——并返回一个在所有 iframe 完成握手后才会 resolve 的 Promise。若出现以下情况会以`FluzElementsError`拒绝：

* 你传给`createCardViewer`的`style`未通过校验（`error.code === "INVALID_STYLE"`）——参见[样式化字段](/build-a-platform/secure-elements-overview#styling-fields)
* 某个 iframe 未能在`mountTimeoutMs`内完成握手（`error.code === "MOUNT_TIMEOUT"`；默认 10 秒，可通过`createCardViewer({ ..., mountTimeoutMs })`配置）
* 某个 iframe 根本加载失败，或此查看器已被挂载（`error.code === "MOUNT_FAILED"`）——每个`CardViewer`实例只能挂载一次；如需再次挂载，请用`createCardViewer`创建新的实例

## 展示与掩码字段

```js theme={null}
await viewer.reveal(); // fetch and show every configured field at once
await viewer.reveal("cvv"); // fetch and show one field, if that field allows it
viewer.setMask("cvv", true); // re-mask a field that's already been revealed
viewer.setMask("cvv", false); // un-mask it again -- see below
```

* `reveal(field?)`——异步。从 Fluz 获取真实值并展示。无参数调用时，无视`individualReveal`，一次性获取并展示所有已配置字段。传入字段名时，只获取并展示该字段——若该字段配置了`individualReveal: false`，则以`error.code === "INDIVIDUAL_REVEAL_DISABLED"`拒绝。未挂载的查看器，或字段名不在`fields`中，则以`MOUNT_FAILED`拒绝。
* `setMask(field, masked, options?)`——同步、非异步。不会获取任何数据——仅切换当前展示状态：
  * `setMask(field, true)`将字段恢复为占位符，无论此前是否展示过真实值。
  * `setMask(field, false)`取消掩码——仅当该字段已被`reveal()`获取过真实值时才会展示真实值。若在任何`reveal()`之前调用，则字段仍保持占位符，因为尚无可展示的真实值。
  * `setMask(field, true, { hidden: true })`会将字段完全清空（空白，连占位符也不显示），而非显示圆点/后四位/有效期。`hidden`仅在`masked`为`true`时生效。
  * 不提供批量“全部掩码”的方法——若需重置整个查看器，请对`fields`中的每个字段分别调用`setMask`。
* `destroy()`——销毁所有 iframe 并分离查看器。组件卸载时调用，以免在组件消失后仍遗留已挂载的 iframe。

### 仅展示单个字段

若只需单独展示某个字段（例如该字段旁边的“显示 CVV”按钮），调用`reveal(field)`即可——默认每个字段都允许此操作，因此常见场景无需额外配置。

如果某个字段应当“绝不”单独展示，只能随整卡`reveal()`一同出现，可在`fields`中为其设置`{ field, individualReveal: false }`以选择退出：

```js theme={null}
const viewer = createCardViewer({
  clientToken,
  loadToken,
  frameHostOrigin: "https://staging.secure.fluz.app",
  fields: ["pan", "expiry", { field: "cvv", individualReveal: false }],
});

await viewer.reveal("cvv"); // rejects — error.code === "INDIVIDUAL_REVEAL_DISABLED"
await viewer.reveal(); // succeeds — reveals pan, expiry, and cvv together
```

针对选择退出的字段调用`reveal(field)`会在不联系`frame-host`的情况下以`FluzElementsError`（`code: "INDIVIDUAL_REVEAL_DISABLED"`）拒绝。无论如何，不带参数的`reveal()`总是会展示所有已挂载字段——`individualReveal`对其不起作用。

<Note>
  这是前端集成层面的选择，而非服务器强制的能力限制——它控制的是你的 UI 允许触发的行为，而不是授权可返回的数据范围。不要将`individualReveal: false`当作安全边界依赖。
</Note>

## 处理事件

```js theme={null}
const unsubscribeMount = viewer.onMount(() => {
  // all configured fields have finished rendering
});

const unsubscribeError = viewer.onError((error) => {
  // error.code, error.message
});
```

`onMount`在每个已配置字段都在 iframe 内完成渲染后触发一次。`onError`用于接收已经挂载的 iframe 内部发生的问题——如`reveal()`失败或触发限流——而`mount()`或`createCardViewer()`本身的问题则会直接以拒绝或抛错的形式呈现（见下文）。`onMount`与`onError`均返回一个取消订阅函数。

此能力可能产生的每个`FluzElementsError`及其出现位置：

| Code                         | 出现场景                                      | 含义                                                           |
| :--------------------------- | :---------------------------------------- | :----------------------------------------------------------- |
| `INVALID_FIELD`              | 由`createCardViewer`抛出                     | 某个`fields`项不是`pan`、`expiry`或`cvv`                            |
| `INVALID_FRAME_HOST_ORIGIN`  | 由`createCardViewer`抛出                     | `frameHostOrigin`不是已识别的 Fluz frame host                      |
| `INVALID_STYLE`              | 被`mount()`拒绝                              | 某个`style`值校验失败                                               |
| `MOUNT_TIMEOUT`              | 被`mount()`拒绝                              | 某个字段的 iframe 未在`mountTimeoutMs`内完成握手                         |
| `MOUNT_FAILED`               | 被`mount()`拒绝，或由`reveal()` / `setMask()`抛出 | 某个 iframe 加载失败；查看器已被挂载；或对未挂载/未知字段调用了`reveal()` / `setMask()` |
| `INDIVIDUAL_REVEAL_DISABLED` | 被`reveal(field)`拒绝                        | 该字段配置了`individualReveal: false`                              |
| `FIELD_ERROR`                | 发送给`onError`                              | 挂载后在 iframe 内部发生的`reveal()`失败（网络错误，或尚未允许展示）                  |
| `RATE_LIMITED`               | 发送给`onError`                              | 此授权的展示尝试过多——`error.message`包含重试等待时间                          |

## 完整示例

```html theme={null}
<div id="card-viewer"></div>

<script src="https://secure-cdn-staging.fluz.app/secure-elements/v0.1.0/index.global.js"></script>
<script>
  (async () => {
    const { createCardViewer } = FluzSecureElements;

    const res = await fetch("/mint-reveal-token", { method: "POST" });
    const { clientToken, loadToken } = await res.json();

    const viewer = createCardViewer({
      clientToken,
      loadToken,
      frameHostOrigin: "https://staging.secure.fluz.app",
      fields: ["pan", "expiry", { field: "cvv", individualReveal: false }],
      style: {
        fontFamily: "IBM Plex Mono",
        fontSize: "16px",
        color: "#1a1a1a",
      },
    });

    viewer.onError((error) => console.error(error.code, error.message));
    viewer.onMount(() => console.log("card viewer ready"));

    await viewer.mount(document.getElementById("card-viewer"));
  })();
</script>
```

`/mint-reveal-token`是你自有的后端路由——它会使用你的 Fluz OAuth 访问令牌调用`POST /v1/client-token`，详见[铸造客户端令牌](/build-a-platform/secure-elements-overview#mint-a-client-token)。

## 后续步骤

<CardGroup cols={2}>
  <Card title="安全元素概览" icon="book-open" href="/build-a-platform/secure-elements-overview">
    令牌铸造、SDK 加载、样式化与 CSP。
  </Card>

  {" "}

  <Card title="在线演示" icon="play" href="https://demo.secure.fluz.app/">
    展示、仅展示 CVV 与掩码，运行于 staging。
  </Card>

  <Card title="示例集成" icon="github" href="https://github.com/fluz-app/secure-elements-examples">
    可运行的纯 HTML 与 React 展示示例，含令牌铸造服务器。
  </Card>
</CardGroup>
