# Embedded Checkout (/guides/checkout/embedded-checkout)





Embedded Checkout [#embedded-checkout]

Embedded Checkout 讓您在自己的網站上嵌入支付表單，完全掌控使用者介面和體驗。

<Callout type="info">
  **適合場景**：

  * 需要完全控制結帳 UI/UX
  * 想讓結帳流程無縫融入您的網站
  * 有前端開發資源
  * 需要高度客製化
</Callout>

運作流程 [#運作流程]

```
1. 用戶點擊「訂閱」按鈕
   ↓
2. 前端呼叫您的後端 API
   ↓
3. 後端呼叫 POST /v1/checkouts 取得 SDK Token
   ↓
4. 前端使用 SDK Token 渲染支付表單
   ↓
5. 用戶填寫卡號並提交
   ↓
6. SDK 處理付款並回呼 onSuccess/onError
```

安裝 SDK [#安裝-sdk]

<Tabs items="['npm', 'yarn', 'pnpm', 'CDN']">
  <Tab value="npm">
    ```bash
    npm install recur-tw
    ```
  </Tab>

  <Tab value="yarn">
    ```bash
    yarn add recur-tw
    ```
  </Tab>

  <Tab value="pnpm">
    ```bash
    pnpm add recur-tw
    ```
  </Tab>

  <Tab value="CDN">
    ```html
    <script src="https://unpkg.com/recur-tw/dist/recur.umd.js"></script>
    ```
  </Tab>
</Tabs>

React 整合 [#react-整合]

1\. 設定 Provider [#1-設定-provider]

```tsx
// app/providers.tsx
'use client';

import { RecurProvider } from 'recur-tw';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <RecurProvider publishableKey={process.env.NEXT_PUBLIC_RECUR_PUBLISHABLE_KEY!}>
      {children}
    </RecurProvider>
  );
}
```

2\. 建立訂閱按鈕 [#2-建立訂閱按鈕]

```tsx
// components/SubscribeButton.tsx
'use client';

import { useSubscribe } from 'recur-tw';

export function SubscribeButton({ productId }: { productId: string }) {
  const { subscribe, isLoading, error } = useSubscribe({
    onSuccess: (result) => {
      console.log('訂閱成功！', result);
      window.location.href = '/success';
    },
    onError: (error) => {
      console.error('訂閱失敗：', error.message);
    },
  });

  return (
    <button
      onClick={() => subscribe({
        productId,
        customerEmail: 'user@example.com',
        customerName: '王小明',
      })}
      disabled={isLoading}
    >
      {isLoading ? '處理中...' : '立即訂閱'}
    </button>
  );
}
```

Vanilla JavaScript 整合 [#vanilla-javascript-整合]

```html
<div id="checkout-container"></div>

<script src="https://unpkg.com/recur-tw/dist/recur.umd.js"></script>
<script>
  const recur = RecurCheckout.init({
    publishableKey: 'pk_test_xxx',
  });

  recur.createEmbeddedCheckout({
    productId: 'prod_xxx',
    container: '#checkout-container',
    customerEmail: 'user@example.com',
    onSuccess: (result) => {
      console.log('成功！', result);
    },
    onError: (error) => {
      alert('錯誤：' + error.message);
    },
  });
</script>
```

本地開發 [#本地開發]

<Callout type="warning" title="重要：Localhost 限制">
  PAYUNi 金流不支援 `localhost` 作為嵌入式結帳的 domain。這是因為 PAYUNi SDK 需要驗證請求來源的安全性。
</Callout>

解決方案 [#解決方案]

有三種方式可以在本地開發時測試 Embedded Checkout：

<Steps>
  <Step>
    方案一：使用 Hosted Checkout（推薦） [#方案一使用-hosted-checkout推薦]

    在本地開發時使用 Hosted Checkout 模式。建立 Checkout Session 後會取得一個 `url` 欄位，直接導向該 URL 即可完成測試。

    ```typescript
    // 建立 Checkout Session
    const response = await fetch('/api/create-checkout', {
      method: 'POST',
      body: JSON.stringify({ productId: 'prod_xxx' }),
    });

    const { url } = await response.json();

    // 導向 Recur 託管的結帳頁面（在 localhost 也能正常運作）
    window.location.href = url;
    ```

    Hosted Checkout 頁面位於 `checkout.recur.tw`，不受 localhost 限制。
  </Step>

  <Step>
    方案二：使用 Tunneling 服務 [#方案二使用-tunneling-服務]

    使用 ngrok、Cloudflare Tunnel 或 localtunnel 等服務，將您的本地伺服器暴露到公開網路：

    **使用 ngrok：**

    ```bash
    # 安裝 ngrok
    npm install -g ngrok

    # 啟動 tunnel（假設您的開發伺服器在 port 5173）
    ngrok http 5173
    ```

    ngrok 會提供一個公開 URL（如 `https://abc123.ngrok.io`），使用這個 URL 存取您的應用程式即可正常使用 Embedded Checkout。

    **使用 Cloudflare Tunnel：**

    ```bash
    # 安裝 cloudflared
    brew install cloudflared

    # 啟動 tunnel
    cloudflared tunnel --url http://localhost:5173
    ```
  </Step>

  <Step>
    方案三：部署到 Staging 環境 [#方案三部署到-staging-環境]

    將您的應用程式部署到有正式 domain 的 staging 環境進行測試：

    * Vercel: `your-app.vercel.app`
    * Netlify: `your-app.netlify.app`
    * Railway: `your-app.up.railway.app`

    這些平台都提供免費的子網域，可以正常使用 Embedded Checkout。
  </Step>
</Steps>

錯誤訊息說明 [#錯誤訊息說明]

當您在 localhost 使用 Embedded Checkout 時，可能會看到以下錯誤：

```json
{
  "error": {
    "code": "localhost_not_supported",
    "message": "Embedded checkout is not supported on localhost"
  }
}
```

這表示 PAYUNi 拒絕了來自 localhost 的請求。請使用上述三種解決方案之一。

測試模式 [#測試模式]

使用 `pk_test_*` 開頭的 Publishable Key 時，SDK 會自動進入測試模式。在測試模式下：

* 支付表單頂部會顯示「🧪 測試模式」提示
* 可以展開查看測試卡號
* 不會產生真實交易

測試卡號 [#測試卡號]

| 卡號                    | 說明                |
| --------------------- | ----------------- |
| `4147-6310-0000-0001` | VISA 測試卡（授權成功）    |
| `3560-5110-0000-0001` | JCB 測試卡（授權成功）     |
| `4147-6310-0000-0002` | VISA 測試卡（3D 驗證失敗） |
| `3560-5110-0000-0002` | JCB 測試卡（3D 驗證失敗）  |

* **有效期**：任意未過期日期
* **CVV（背面末三碼）**：任意 3 位數

API 參考 [#api-參考]

POST /v1/checkouts [#post-v1checkouts]

建立 Checkout 並取得 SDK Token。

**請求參數：**

| 參數                     | 必填  | 說明                                                                                                  |
| ---------------------- | --- | --------------------------------------------------------------------------------------------------- |
| `productId`            | ✅\* | 商品 ID（與 `productSlug` 二擇一）                                                                          |
| `productSlug`          | ✅\* | 商品 Slug（與 `productId` 二擇一）                                                                          |
| `customerEmail`        | ❌   | 客戶 Email（結帳時必填）                                                                                     |
| `customerName`         | ❌   | 客戶姓名（結帳時必填）                                                                                         |
| `promotionCode`        | ❌   | 預先套用的優惠碼                                                                                            |
| `collectPaymentMethod` | ❌   | `always`（預設）或 `if_required`。詳見 [Hosted Checkout - 零元結帳](/guides/checkout/hosted-checkout#零元結帳免綁卡兌換) |
| `externalId`           | ❌   | 外部客戶 ID                                                                                             |

**回應：**

```json
{
  "checkout": {
    "id": "pi_xxx",
    "client_secret": "pi_xxx_secret_xxx",
    "status": "requires_payment_method",
    "amount": 990,
    "currency": "TWD",
    "requires_payment_method": true
  },
  "product": {
    "id": "prod_xxx",
    "name": "Pro 方案",
    "price": 990,
    "interval": "month"
  },
  "sdk_token": "xxx",
  "sdk_token_expires_at": "2024-01-01T12:30:00Z"
}
```

<Callout type="info">
  **零元結帳**：當 `promotionCode` 折扣 100% 且 `collectPaymentMethod` 為 `if_required` 時，回應的 `requires_payment_method` 會是 `false`，且 `sdk_token` 為 `null`。SDK 的 `pay` 端點會直接完成訂閱，不需要信用卡。詳見 [Hosted Checkout - 零元結帳](/guides/checkout/hosted-checkout#零元結帳免綁卡兌換)。
</Callout>

下一步 [#下一步]

* [錯誤處理](/guides/checkout/error-handling) - 處理付款失敗與各種錯誤情況
* [Webhook 整合](/guides/webhooks) - 接收付款成功通知
* [Customer Portal](/guides/portal) - 讓客戶管理自己的訂閱
