---
title: 前端整合
description: 使用 Recur SDK Web Component 整合 Customer Portal 按鈕
---

# 前端整合

Recur SDK 提供 `<recur-portal>` Web Component，讓您可以快速在前端加入 Customer Portal 按鈕。

## 安裝 SDK

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

```typescript
// 在應用程式入口點引入
import 'recur-tw';
```

```html
<script src="https://cdn.recur.tw/sdk/recur.umd.js"></script>
```

## `<recur-portal>` 組件

### 使用方式一：直接提供 Portal URL

適合 Server-Side Rendering (SSR) 的應用，在後端取得 Portal URL 後直接傳入：

```html
<recur-portal portal-url="https://portal.recur.tw/s/ps_xxxxx">
  管理訂閱
</recur-portal>
```

```tsx
// Next.js Server Component 範例
import { Recur } from 'recur-tw/server';

const recur = new Recur(process.env.RECUR_SECRET_KEY!);

export default async function AccountPage() {
  const session = await recur.portal.sessions.create({
    customer: 'cus_xxxxx',
    returnUrl: 'https://your-site.com/account',
  });

  return (
    <recur-portal portal-url={session.url}>
      管理訂閱
    </recur-portal>
  );
}
```

### 使用方式二：透過 API 動態建立

適合 Client-Side Rendering (CSR) 的應用，點擊時呼叫您的後端 API 建立 Session：

```html
<recur-portal
  api-endpoint="/api/portal/create"
  customer-id="cus_xxxxx">
  管理訂閱
</recur-portal>
```

  使用 `api-endpoint` 模式時，組件會在點擊後 POST 到指定端點，期望回應 `{ url: "..." }` 格式。

## 屬性說明

| 屬性 | 必填 | 說明 |
|------|------|------|
| `portal-url` | ❌* | 直接提供 Portal URL |
| `api-endpoint` | ❌* | 後端 API 端點（用於動態建立 Session） |
| `customer-id` | ❌ | 客戶 ID（傳送給 API） |
| `return-url` | ❌ | 返回 URL（傳送給 API） |
| `button-text` | ❌ | 按鈕文字（預設：插槽內容或「管理訂閱」） |
| `button-style` | ❌ | 按鈕樣式：`primary`、`outline`、`gradient`、`link` |
| `target` | ❌ | 設為 `_blank` 在新視窗開啟 |
| `disabled` | ❌ | 禁用按鈕 |

  `portal-url` 和 `api-endpoint` 至少需要提供一個。

## 按鈕樣式

```html
<!-- Primary（預設） -->
<recur-portal portal-url="..." button-style="primary">
  管理訂閱
</recur-portal>

<!-- Outline -->
<recur-portal portal-url="..." button-style="outline">
  管理訂閱
</recur-portal>

<!-- Gradient -->
<recur-portal portal-url="..." button-style="gradient">
  管理訂閱
</recur-portal>

<!-- Link -->
<recur-portal portal-url="..." button-style="link">
  管理訂閱
</recur-portal>
```

## 事件處理

### `portal-redirect`

在導向 Portal 前觸發：

```javascript
document.querySelector('recur-portal').addEventListener('portal-redirect', (event) => {
  console.log('Redirecting to:', event.detail.url);
  // 可用於追蹤分析
  analytics.track('portal_opened');
});
```

### `portal-error`

發生錯誤時觸發：

```javascript
document.querySelector('recur-portal').addEventListener('portal-error', (event) => {
  console.error('Portal error:', event.detail.message);
  // 顯示錯誤訊息給用戶
  alert('無法開啟訂閱管理頁面，請稍後再試');
});
```

## React 整合

### 使用 Web Component

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

import { useEffect, useRef } from 'react';
import 'recur-tw';

interface PortalButtonProps {
  portalUrl?: string;
  apiEndpoint?: string;
  customerId?: string;
}

export function PortalButton({ portalUrl, apiEndpoint, customerId }: PortalButtonProps) {
  const ref = useRef<HTMLElement>(null);

  useEffect(() => {
    const element = ref.current;
    if (!element) return;

    const handleError = (e: Event) => {
      const { message } = (e as CustomEvent).detail;
      console.error('Portal error:', message);
    };

    element.addEventListener('portal-error', handleError);
    return () => element.removeEventListener('portal-error', handleError);
  }, []);

  return (
    <recur-portal
      ref={ref}
      portal-url={portalUrl}
      api-endpoint={apiEndpoint}
      customer-id={customerId}
    >
      管理訂閱
    </recur-portal>
  );
}
```

### 使用自訂按鈕

如果您想使用自己的 UI 元件，可以直接呼叫 SDK 方法：

```tsx
'use client';

import { useState } from 'react';
import { RecurCheckout } from 'recur-tw/vanilla';

export function CustomPortalButton({ customerId }: { customerId: string }) {
  const [loading, setLoading] = useState(false);

  const handleClick = async () => {
    setLoading(true);

    try {
      const recur = RecurCheckout.init({ publishableKey: 'pk_xxx' });

      const session = await recur.createPortalSession('/api/portal/create', {
        customerId,
      });

      window.location.href = session.url;
    } catch (error) {
      console.error('Failed to open portal:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <button
      onClick={handleClick}
      disabled={loading}
      className="btn btn-primary"
    >
      {loading ? '載入中...' : '管理訂閱'}
    </button>
  );
}
```

## Vue 整合

```vue
<template>
  <recur-portal
    :portal-url="portalUrl"
    @portal-redirect="onRedirect"
    @portal-error="onError"
  >
    管理訂閱
  </recur-portal>
</template>

<script setup lang="ts">
import 'recur-tw';

defineProps<{
  portalUrl: string;
}>();

const onRedirect = (event: CustomEvent) => {
  console.log('Redirecting to:', event.detail.url);
};

const onError = (event: CustomEvent) => {
  console.error('Portal error:', event.detail.message);
};
</script>
```

## 後端 API 範例

當使用 `api-endpoint` 模式時，您需要建立對應的後端 API：

```typescript
// app/api/portal/create/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { Recur } from 'recur-tw/server';
import { auth } from '@/lib/auth';

const recur = new Recur(process.env.RECUR_SECRET_KEY!);

export async function POST(request: NextRequest) {
  // 驗證用戶
  const session = await auth();
  if (!session?.user) {
    return NextResponse.json({ error: { message: 'Unauthorized' } }, { status: 401 });
  }

  // 從請求中取得參數（可選）
  const body = await request.json().catch(() => ({}));

  // 建立 Portal Session
  const portalSession = await recur.portal.sessions.create({
    customer: session.user.recurCustomerId,
    returnUrl: body.returnUrl || `${process.env.NEXT_PUBLIC_URL}/account`,
  });

  // 返回 URL（組件期望的格式）
  return NextResponse.json({
    url: portalSession.url,
  });
}
```

## TypeScript 支援

```typescript
// 宣告 Web Component 類型
declare global {
  namespace JSX {
    interface IntrinsicElements {
      'recur-portal': React.DetailedHTMLProps<
        React.HTMLAttributes<HTMLElement> & {
          'portal-url'?: string;
          'api-endpoint'?: string;
          'customer-id'?: string;
          'return-url'?: string;
          'button-text'?: string;
          'button-style'?: 'primary' | 'outline' | 'gradient' | 'link';
          'target'?: string;
          'disabled'?: boolean;
        },
        HTMLElement
      >;
    }
  }
}
```

## 下一步

- [後端整合](/guides/portal/server-integration) - Server SDK 詳細說明
- [Portal API 參考](/api-reference/portal) - 完整 API 文件
- [Webhook 整合](/guides/webhooks) - 接收訂閱變更通知
