---
title: Embedded Checkout
description: 在您的網站上嵌入支付表單，完全掌控 UI 體驗
---

# Embedded Checkout

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

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

## 運作流程

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

## 安裝 SDK

```bash
npm install recur-tw
```

```bash
yarn add recur-tw
```

```bash
pnpm add recur-tw
```

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

## React 整合

### 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. 建立訂閱按鈕

```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 整合

```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>
```

## 本地開發

  PAYUNi 金流不支援 `localhost` 作為嵌入式結帳的 domain。這是因為 PAYUNi SDK 需要驗證請求來源的安全性。

### 解決方案

有三種方式可以在本地開發時測試 Embedded 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 限制。

### 方案二：使用 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
```

### 方案三：部署到 Staging 環境

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

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

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

### 錯誤訊息說明

當您在 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 參考

### POST /v1/checkouts

建立 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"
}
```

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

## 下一步

- [電子發票](/guides/checkout/einvoice) - 支付表單會在啟用時自動收集買家的發票選擇
- [錯誤處理](/guides/checkout/error-handling) - 處理付款失敗與各種錯誤情況
- [Webhook 整合](/guides/webhooks) - 接收付款成功通知
- [Customer Portal](/guides/portal) - 讓客戶管理自己的訂閱
