> ## 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>
  本頁假設您已閱讀[Secure Elements 概觀](/build-a-platform/secure-elements-overview)——其中涵蓋了建立用戶端權杖、載入 SDK，以及欄位樣式設定，這些同樣適用於此處。
</Note>

## 線上預覽

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

此示範會自動建立權杖並掛載檢視器。若揭示按鈕無回應，請使用**重新建立權杖並重新掛載**，並使用**揭示**、**僅揭示 CVV**或**遮罩**來試用下方介紹的欄位層級控制。[在新分頁開啟 →](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` 這三個欄位名稱——其它任何值都會在您呼叫 `mount()` 之前，由 `createCardViewer` 立即同步擲回 `FluzElementsError`（`error.code === "INVALID_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` 而言，是三個獨立的 frame，而非單一整合的 frame——並回傳在每個 frame 完成握手後才會解決的 promise。若發生以下情況，會以 `FluzElementsError` 拒絕：

* 您傳給 `createCardViewer` 的 `style` 未通過驗證（`error.code === "INVALID_STYLE"`）——參見[欄位樣式設定](/build-a-platform/secure-elements-overview#styling-fields)
* 某個 frame 未能在 `mountTimeoutMs` 內完成握手（`error.code === "MOUNT_TIMEOUT"`；預設 10 秒，可透過 `createCardViewer({ ..., mountTimeoutMs })` 設定）
* 某個 frame 完全載入失敗，或此檢視器已被掛載過（`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()`——拆除所有 frame 並解除掛載檢視器。於元件卸載時呼叫，避免在元件消失後仍遺留已掛載的 frame。

### 僅揭示單一欄位

若要僅揭示單一欄位（例如該欄位旁的「顯示 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` 會在每個設定的欄位皆已於 frame 內渲染後觸發一次。`onError` 會在已掛載的 frame「內部」發生問題時觸發——例如 `reveal()` 失敗或遭遇速率限制——而非 `mount()` 或 `createCardViewer()` 本身的問題；後者會直接拒絕或擲回（見下表）。`onMount` 與 `onError` 皆會回傳取消訂閱函式。

此功能可能產生的每個 `FluzElementsError` 以及其出現位置：

| 代碼                           | 出現位置                                          | 意義                                                           |
| :--------------------------- | :-------------------------------------------- | :----------------------------------------------------------- |
| `INVALID_FIELD`              | 由 `createCardViewer` 擲回                       | 某個 `fields` 項目不是 `pan`、`expiry` 或 `cvv`                      |
| `INVALID_FRAME_HOST_ORIGIN`  | 由 `createCardViewer` 擲回                       | `frameHostOrigin` 不是已辨識的 Fluz frame 主機                       |
| `INVALID_STYLE`              | 由 `mount()` 拒絕                                | 某個 `style` 值未通過驗證                                            |
| `MOUNT_TIMEOUT`              | 由 `mount()` 拒絕                                | 某欄位的 frame 未能在 `mountTimeoutMs` 內完成握手                        |
| `MOUNT_FAILED`               | 由 `mount()` 拒絕，或由 `reveal()` / `setMask()` 擲回 | 某個 frame 載入失敗；檢視器已被掛載；或以未掛載或未知欄位呼叫了 `reveal()` / `setMask()` |
| `INDIVIDUAL_REVEAL_DISABLED` | 由 `reveal(field)` 拒絕                          | 該欄位被設定為 `individualReveal: false`                            |
| `FIELD_ERROR`                | 傳遞給 `onError`                                 | `reveal()` 在掛載後於 frame 內失敗（網路錯誤，或揭示尚不可用）                     |
| `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 access token 呼叫 `POST /v1/client-token`，詳見[建立用戶端權杖](/build-a-platform/secure-elements-overview#mint-a-client-token)。

## 下一步

<CardGroup cols={2}>
  <Card title="Secure Elements 概觀" 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>
