mcpField
R
recur-portal

recur-portal

github.com/recur-tw開発060 回インストール4 週間前に更新
リポジトリ

recur-portal について

Recur Customer Portal の導入フローを実装するスキルです。購読者がサポートに連絡せずに契約や請求を管理できるよう、サーバー側のセッション作成、Next.js 連携、REST API の直接利用を扱います。

主な機能

  1. 01サーバー側でのポータルセッション作成
  2. 02IDまたはメールによる顧客識別
  3. 03Next.js のルートとアクションへの統合
  4. 04REST API の直接利用
  5. 05認証とセッションのセキュリティ指針

活用シーン

  1. 01セルフサービスのアカウントポータルへのリンク追加
  2. 02購読者による支払い方法の更新
  3. 03契約の解約やプラン変更の有効化
  4. 04ポータルでの請求履歴の表示

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/recur-tw/skills --skill recur-portal

インストール後、Claude Code で/recur-portalを入力して読み込まれたか確認します。

詳細

Recur カスタマーポータルの統合

サブスクライバーがサポートに問い合わせずにサブスクリプションを自分で管理できるようにする、Recur のカスタマーポータルの実装を支援します。

カスタマーポータルとは?

カスタマーポータルは、顧客が次の操作を行えるホスト型のページです。

  • 有効なサブスクリプションと請求履歴の確認
  • 支払い方法の更新
  • サブスクリプションのキャンセルまたは再開
  • プランの切り替え(アップグレード/ダウングレード)

使用する場面

シナリオ解決策
「アカウント管理ページを追加する」ポータルセッションを作成してリダイレクトする
「ユーザーがカードを更新できるようにする」ポータルで支払い方法の更新に対応する
「ユーザーがサブスクリプションをキャンセルする必要がある」ポータルでセルフサービスのキャンセルを提供する
「請求履歴を表示する」ポータルに請求書と支払いを表示する

クイックスタート:ポータルセッションの作成

ポータルセッションはサーバーサイドで作成します(Secret Key が必要です)。

サーバー SDK を使用する
import { Recur } from 'recur-tw/server'

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

// Create portal session - identify customer by email, ID, or externalId
const session = await recur.portal.sessions.create({
  email: '[email protected]',  // or customer: 'cus_xxx' or externalId: 'user_123'
  returnUrl: 'https://yourapp.com/account',
})

// Redirect customer to portal
redirect(session.url)
顧客の識別

次のいずれかの方法で顧客を識別できます(優先順位順)。

// By Recur customer ID (highest priority)
await recur.portal.sessions.create({
  customer: 'cus_xxx',
  returnUrl: 'https://yourapp.com/account',
})

// By your system's user ID
await recur.portal.sessions.create({
  externalId: 'user_123',
  returnUrl: 'https://yourapp.com/account',
})

// By email (lowest priority)
await recur.portal.sessions.create({
  email: '[email protected]',
  returnUrl: 'https://yourapp.com/account',
})

Next.js の実装

API ルート(App Router)
// app/api/portal/route.ts
import { Recur } from 'recur-tw/server'
import { auth } from '@/lib/auth'  // Your auth solution
import { NextResponse } from 'next/server'

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

export async function POST() {
  const session = await auth()

  if (!session?.user?.email) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  try {
    const portalSession = await recur.portal.sessions.create({
      email: session.user.email,
      returnUrl: `${process.env.NEXT_PUBLIC_APP_URL}/account`,
    })

    return NextResponse.json({ url: portalSession.url })
  } catch (error) {
    console.error('Portal session error:', error)
    return NextResponse.json(
      { error: 'Failed to create portal session' },
      { status: 500 }
    )
  }
}
サーバーアクション
// app/actions/portal.ts
'use server'

import { Recur } from 'recur-tw/server'
import { auth } from '@/lib/auth'
import { redirect } from 'next/navigation'

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

export async function openPortal() {
  const session = await auth()

  if (!session?.user?.email) {
    throw new Error('Unauthorized')
  }

  const portalSession = await recur.portal.sessions.create({
    email: session.user.email,
    returnUrl: `${process.env.NEXT_PUBLIC_APP_URL}/account`,
  })

  redirect(portalSession.url)
}
ポータルボタンコンポーネント
// components/portal-button.tsx
'use client'

import { useState } from 'react'

export function PortalButton() {
  const [isLoading, setIsLoading] = useState(false)

  const handleClick = async () => {
    setIsLoading(true)
    try {
      const response = await fetch('/api/portal', { method: 'POST' })
      const { url, error } = await response.json()

      if (error) throw new Error(error)

      window.location.href = url
    } catch (error) {
      console.error('Failed to open portal:', error)
      alert('無法開啟帳戶管理頁面,請稍後再試')
    } finally {
      setIsLoading(false)
    }
  }

  return (
    <button onClick={handleClick} disabled={isLoading}>
      {isLoading ? '載入中...' : '管理訂閱'}
    </button>
  )
}
ボタンでサーバーアクションを使用する
// components/portal-button-action.tsx
'use client'

import { openPortal } from '@/app/actions/portal'

export function PortalButton() {
  return (
    <form action={openPortal}>
      <button type="submit">管理訂閱</button>
    </form>
  )
}

ポータルセッションのレスポンス

interface PortalSession {
  id: string              // Session ID (e.g., 'portal_sess_xxx')
  object: 'portal.session'
  url: string             // URL to redirect customer to
  customer: string        // Customer ID
  returnUrl: string       // URL to return after portal exit
  status: 'active' | 'expired'
  expiresAt: string       // ISO 8601 (sessions last 1 hour)
  accessedAt: string | null
  createdAt: string
}

REST API を直接使用する

SDK を使用しない場合:

const response = await fetch('https://api.recur.tw/v1/portal/sessions', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    email: '[email protected]',
    return_url: 'https://yourapp.com/account',
  }),
})

const { url } = await response.json()
// Redirect to url

よくあるパターン

ポータルリンク付きアカウントページ
// app/account/page.tsx
import { auth } from '@/lib/auth'
import { PortalButton } from '@/components/portal-button'

export default async function AccountPage() {
  const session = await auth()

  return (
    <div>
      <h1>帳戶設定</h1>
      <p>Email: {session?.user?.email}</p>

      <section>
        <h2>訂閱管理</h2>
        <p>管理您的訂閱、更新付款方式、查看帳單記錄</p>
        <PortalButton />
      </section>
    </div>
  )
}
ポータルへのアクセス制御
// Only show portal button if user has subscription
function SubscriptionSection({ hasSubscription }: { hasSubscription: boolean }) {
  if (!hasSubscription) {
    return (
      <div>
        <p>您目前沒有訂閱</p>
        <a href="/pricing">查看方案</a>
      </div>
    )
  }

  return (
    <div>
      <p>您目前的訂閱:Pro 方案</p>
      <PortalButton />
    </div>
  )
}

ポータルの設定

Recur Dashboard → Settings → Customer Portal でポータルの動作を設定します。

  • 既定のリターン URL:ポータルを離れた後のリダイレクト先
  • 許可する操作:キャンセル、支払いの更新、プランの切り替えを有効/無効にする
  • ブランディング:カスタムロゴとカラー

セキュリティに関する注意

  1. サーバーサイドのみ:ポータルセッションには Secret Key(sk_xxx)が必要です
  2. 有効期間が短い:セッションは 1 時間で期限切れになります
  3. 期限切れまでは再利用可能:URL はその 1 時間は引き続き機能するため、必要なときに作成し、ログに記録したりメールで送信したりしないでください
  4. ユーザーを確認する:ポータルセッションを作成する前に、必ずユーザーを認証してください

エラー処理

import { RecurAPIError } from 'recur-tw/server'

let session
try {
  session = await recur.portal.sessions.create({
    email: userEmail,
    returnUrl: returnUrl,
  })
} catch (error) {
  if (error instanceof RecurAPIError && error.code === 'not_found') {
    // No Recur customer matches this email / externalId / customer id,
    // usually because they have not bought anything yet
    redirect('/pricing')
  }
  // code 'bad_request': neither returnUrl nor a default return URL
  // (Dashboard → Settings → Customer Portal) is set
  throw error
}
redirect(session.url)

redirect() は try の外に置いてください。Next.js では例外をスローして動作するため、外側の catch によって処理が捕捉されます。

関連スキル

  • /recur-quickstart - SDK の初期設定
  • /recur-checkout - 購入フローの実装
  • /recur-entitlements - サブスクリプションへのアクセス確認

コメント