> ## 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 與共用的 client-token 流程，這兩者在此同樣適用。
</Note>

## 實際體驗

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

此示範會自行產生權杖並自動掛載欄位。輸入任何可通過 Luhn 檢查的卡號、填入持卡人姓名並提交。若表單停止回應，請使用 **Remint token & remount**。 [在新分頁開啟 →](https://demo.secure.fluz.app/collect/)

## 產生 tokenization 權杖

呼叫 [`POST /v1/client-token`](/build-a-platform/secure-elements-overview#mint-a-client-token) 並傳入 `"purpose": "tokenization"`：

```json theme={null}
{
  "purpose": "tokenization"
}
```

不同於 reveal 權杖，這個不需要 `virtualCardId`。但你的 access token 需要不同的 scope —— `MANAGE_PAYMENT`，而不是 `CREATE_VIRTUALCARD` —— 並且它的有效時間較長（預設 30 分鐘），因為使用者填寫卡片表單通常比點擊 reveal 需要更久的時間。

## 繪製欄位

```js theme={null}
const inputs = renderFieldsForTokenization({
  clientToken,
  loadToken,
  frameHostOrigin: "https://staging.secure.fluz.app",
});
```

`renderFieldsForTokenization` 沒有 `fields` 選項 —— PAN、到期日與 CVV 會一同以單一合併的 frame 掛載，因為 CVV 驗證會依品牌而異（Amex 的 CVV 為 4 碼；其他品牌為 3 碼），而這只在該欄位能得知旁邊 frame 輸入的卡號時才可運作。你無法像 `createCardViewer` 的欄位那樣各自獨立掛載。

| 選項                          | 必填 | 詳細說明                                                                              |
| :-------------------------- | :- | :-------------------------------------------------------------------------------- |
| `clientToken` / `loadToken` | 是  | 來自以上的 mint 呼叫                                                                     |
| `frameHostOrigin`           | 否  | 與 [Card Reveal](/build-a-platform/card-reveal) 相同的允許清單來源規則 —— 未提供則預設為正式環境         |
| `style`                     | 否  | 與 `createCardViewer` 相同的 `{ color, fontSize, fontFamily, fontWeight }` —— 見下方注意事項 |
| `mountTimeoutMs`            | 否  | 預設 10 秒                                                                           |
| `submitTimeoutMs`           | 否  | 預設 15 秒 —— 參見 [提交](#submit)                                                       |
| `excludedCardBrands`        | 否  | 例如 `["amex"]` —— 參見 [追蹤欄位狀態](#track-field-state)                                  |

<Warning>
  透過 `style.fontFamily` 傳入的 Google Font 會在 [Card Reveal](/build-a-platform/card-reveal) 的欄位中顯示，但在此能力中會被靜默略過 —— 這些欄位在供應商託管的 vault iframe 中渲染，無法載入外部 CSS。只有系統字型允許清單（`system-ui`、`Arial`、`Georgia`、`monospace` 等）能在此實際套用字型。
</Warning>

## 掛載

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

與 [Card Reveal](/build-a-platform/card-reveal#mount-it) 的行為相同：若為 `INVALID_STYLE`、`MOUNT_TIMEOUT` 或 `MOUNT_FAILED`（frame 載入失敗，或此實例已被掛載）則會以 `FluzElementsError` 拒絕。`frameHostOrigin` 會在你呼叫 `renderFieldsForTokenization` 時同步驗證，與 `createCardViewer` 相同 —— 未被識別的來源會在到達 `mount()` 前就拋出 `INVALID_FRAME_HOST_ORIGIN`。

## 追蹤欄位狀態

```js theme={null}
inputs.onChange((field, state) => {
  // field: "pan" | "expiry" | "cvv"
  // state: { isEmpty, isValid, isDirty, brand? }
});
```

在 frame 內每次鍵入都會觸發。`brand` 只會出現在 `pan` 的狀態中，依目前輸入的數字判斷：`amex`、`visa`、`mastercard`、`discover`、`diners` 或 `jcb`。使用 `isValid` 來控制你自己的提交按鈕並驅動即時驗證訊息 —— 這些欄位都不會將底層值曝露給你的頁面。

`excludedCardBrands`（例如 `["amex"]`）不會阻止輸入 —— 一旦偵測到符合的品牌，會將 `pan` 的 `isValid` 強制為 `false`，因此使用者仍可輸入卡號，但在換成其他卡片前 `submit()` 不會成功。

## 提交

持卡人姓名與帳單地址請在你的頁面以一般輸入欄位收集 —— SDK 不會將它們渲染在 Fluz 託管的 frame 中，因為它們並非卡片資料。自行處理它們是否影響你的 PCI DSS 範疇，取決於你更廣泛的持卡人資料環境；請與你的 QSA 確認。

```js theme={null}
await inputs.submit({
  cardholderName: "Jane Doe",
  billingAddress: {
    line1: "123 Main St",
    line2: "Apt 4", // optional
    city: "Austin",
    state: "TX", // optional
    zipCode: "78701",
    country: "US",
  },
  isBackupPayment: false, // optional
});
```

`cardholderName` 只在第一個空白處切分為名字/姓氏 —— `"Mary Ann Smith"` 會變成名字 `"Mary"`、姓氏 `"Ann Smith"`；單一字的姓名會同時作為名與姓。若要重用帳戶中既有的地址而非收集新地址，請傳入 `billingAddress: { userAddressId: "<uuid>" }`。

<Note>
  `submit()` 幾乎不會拒絕，且不會因為拒絕（decline）而丟錯。它只會在同步情況下拋出 `MOUNT_FAILED`（尚未掛載）或 `SUBMIT_FAILED`（"a submit() call is already in progress" —— 當前正有請求進行時會忽略第二次呼叫）。其他所有結果 —— 成功、拒絕、驗證失敗、逾時 —— 都會正常 resolve，並透過下方的回呼傳遞。
</Note>

## 處理結果

```js theme={null}
inputs.onSuccess((result) => {
  // result.bankCardId, brand, last4, expirationMonth, expirationYear,
  // cardholderName, billingAddress, createdAt
});

inputs.onDeclined((decline) => {
  // decline.code, decline.message
});

inputs.onError((error) => {
  // error.code, error.message
});
```

`onSuccess` 會在卡片成功新增為資金來源後觸發。`onDeclined` 則在處理方拒絕卡片時觸發 —— 這仍是正常且可預期的結果，不是錯誤：

| 拒絕代碼                    | 意義                         |
| :---------------------- | :------------------------- |
| `CARD_DECLINED`         | 一般性拒絕                      |
| `INSUFFICIENT_FUNDS`    | 因資金不足而被拒                   |
| `CARD_EXPIRED`          | 卡片已過期                      |
| `CARD_INVALID`          | 無法驗證卡片                     |
| `CVV_MISMATCH`          | 安全碼不符                      |
| `AVS_MISMATCH`          | 帳單地址不符                     |
| `CONTACT_BANK`          | 被拒 —— 請聯絡發卡銀行              |
| `DUPLICATE_CARD`        | 此卡已存在於帳戶中                  |
| `PREPAID_REJECTED`      | 不接受預付卡                     |
| `FRAUD_FILTER`          | 被詐欺過濾規則擋下                  |
| `BIN_BLOCKED`           | 卡片的 BIN 已被封鎖               |
| `EXPANDED_BIN_REQUIRED` | 卡片需要 Fluz 目前尚未具備的擴充 BIN 資料 |
| `KYB_GATE`              | 帳戶尚未符合新增卡片的資格              |
| `TRUST_STATUS_FAILED`   | 帳戶不具新增卡片的資格                |
| `DEVICE_BLOCKED`        | 此裝置不具新增卡片的資格               |
| `MAX_CARDS_REACHED`     | 帳戶已達卡片數量上限                 |
| `DECLINED_OTHER`        | 未對應之拒絕原因的總稱                |

`onError` 則用於所有非正常拒絕的情況：

| 錯誤代碼                        | 出現位置                               | 意義                                                                                                                                                                                                     |
| :-------------------------- | :--------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INVALID_FRAME_HOST_ORIGIN` | 由 `renderFieldsForTokenization` 拋出 | `frameHostOrigin` 並非被 Fluz 認可的 frame host                                                                                                                                                              |
| `INVALID_STYLE`             | 被 `mount()` 拒絕                     | 某個 `style` 值未通過驗證                                                                                                                                                                                      |
| `MOUNT_TIMEOUT`             | 被 `mount()` 拒絕                     | frame 未能在 `mountTimeoutMs` 內完成握手                                                                                                                                                                       |
| `MOUNT_FAILED`              | 被 `mount()` 拒絕 / 由 `submit()` 拋出   | frame 載入失敗、已被掛載，或在 `mount()` resolve 前呼叫了 `submit()`                                                                                                                                                   |
| `SUBMIT_FAILED`             | 由 `submit()` 拋出，或傳遞至 `onError`     | 在已有進行中的 `submit()` 時再次呼叫；或透過 `onError` 傳遞，表示欄位值驗證失敗（`VALIDATION_FAILED`）、環境未開通 collect 能力（`COLLECT_UNAVAILABLE`）、Fluz 後端拒絕請求（`FUNDING_SOURCE_UNAUTHORIZED`），或發生非預期的處理方錯誤（`FUNDING_SOURCE_UNAVAILABLE`） |
| `SUBMIT_TIMEOUT`            | 傳遞至 `onError`                      | 在 `submitTimeoutMs`（預設 15 秒）內未收到提交結果                                                                                                                                                                   |
| `FIELD_ERROR`               | 傳遞至 `onError`                      | 底層卡片保管庫欄位回報內部錯誤                                                                                                                                                                                        |
| `RATE_LIMITED`              | 傳遞至 `onError`                      | 此授權嘗試提交次數過多                                                                                                                                                                                            |

## 清理

```js theme={null}
inputs.destroy();
```

移除 frame 並解除所有監聽器。請在卸載時呼叫，或在產生新權杖以重試前呼叫。

## 完整範例

```html theme={null}
<div id="card-fields"></div>
<input id="cardholder-name" placeholder="Name on card" />
<button id="submit-button">Add card</button>

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

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

    const inputs = renderFieldsForTokenization({
      clientToken,
      loadToken,
      frameHostOrigin: "https://staging.secure.fluz.app",
      excludedCardBrands: ["amex"],
      style: { fontFamily: "system-ui", fontSize: "16px", color: "#1a1a1a" },
    });

    inputs.onChange((field, state) => console.log(field, state));
    inputs.onDeclined((decline) => alert(decline.message));
    inputs.onError((error) => console.error(error.code, error.message));
    inputs.onSuccess((result) => console.log("card added", result.bankCardId));

    await inputs.mount(document.getElementById("card-fields"));

    const submitButton = document.getElementById("submit-button");
    submitButton.addEventListener("click", async () => {
      submitButton.disabled = true;
      try {
        await inputs.submit({
          cardholderName: document.getElementById("cardholder-name").value,
          billingAddress: { userAddressId: "<existing-address-uuid>" },
        });
      } catch (error) {
        console.error(error.code, error.message);
      } finally {
        submitButton.disabled = false;
      }
    });
  })();
</script>
```

`/mint-tokenization-token` 是你自有的後端路由 —— 會以 `"purpose": "tokenization"` 搭配你的 Fluz OAuth access token 呼叫 `POST /v1/client-token`。

## 下一步

<CardGroup cols={2}>
  <Card title="Secure Elements 概觀" icon="book-open" href="/build-a-platform/secure-elements-overview">
    權杖產生、SDK 載入與 CSP。
  </Card>

  {" "}

  <Card title="Card Reveal" icon="eye" href="/build-a-platform/card-reveal">
    另一項 Secure Elements 能力 —— 向使用者顯示其自己的卡片明細。
  </Card>

  {" "}

  <Card title="即時示範" icon="play" href="https://demo.secure.fluz.app/collect/">
    試用連接到 staging 的新增卡片表單。
  </Card>

  <Card title="範例整合" icon="github" href="https://github.com/fluz-app/secure-elements-examples">
    可執行的純 HTML 與 React 安全卡片輸入範例，並附帶產生權杖的伺服器。
  </Card>
</CardGroup>
