pricing-page について
既存アプリに合わせて、プラン定義、機能アクセス制御、Dodo Payments の決済、料金 UI を含む料金システムを構築するコーディングスキルです。未対応の決済プロバイダーを置き換えず、開発者の導入作業を支援します。
主な機能
- 01アプリの構成と決済プロバイダーの確認
- 02プランとクレジットパックの定義
- 03プラン別の機能アクセス制御
- 04Dodo Payments の決済と請求ポータル
- 05料金 UI の生成とフロー検証
活用シーン
- 01既存アプリへの有料プラン追加
- 02プランに応じた API と UI の機能制限
- 03Dodo Payments の決済と請求管理の設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ tushaarmehtaa/ tushar-skills --skill pricing-pageインストール後、Claude Code で/pricing-pageを入力して読み込まれたか確認します。
同梱ファイルreferences/guide.md
詳細
料金システム一式を構築します。プランの定義、機能の利用制限ロジック、Dodo Payments のチェックアウト、フロントエンドの料金コンポーネントを作成します。まずプロジェクトを読み込み、既存のスタックに組み込みます。
フェーズ 1:プロジェクトを把握する
作業を始める前に、コードベースを読み込みます。
1.1 スタックの検出
- フレームワーク:Next.js など、何を使用していますか?
- データベース:どの ORM/クライアントを使用していますか?
usersテーブルの構成はどうなっていますか? - 認証:API ルートでは現在のユーザーをどのように特定していますか?
- 既存の決済:
package.jsonで決済プロバイダーを確認します。@dodopayments/sdk→ Dodo Payments(以下のフェーズ 4 を使用)stripe→ 停止して、次のように報告します:「サポート対象外のプロバイダーです。このスキルで実装できるのは Dodo Payments のみです。Stripe は変更せず、Stripe 専用のワークフローを使用してください。」@lemonsqueezy/lemonsqueezy.js→ 停止して、次のように報告します:「サポート対象外のプロバイダーです。このスキルで実装できるのは Dodo Payments のみです。Lemon Squeezy は変更せず、Lemon Squeezy 専用のワークフローを使用してください。」- なし → Dodo Payments をセットアップするかユーザーに確認します
既存のサポート対象外プロバイダーと並行して、Dodo をインストールしてはいけません。ファイルや依存関係を変更する前に停止してください。
1.2 ユーザーへの確認
I'll scaffold pricing for your [framework] app.
Quick decisions:
1. How many tiers? (e.g., Free + Pro, or Free + Pro + Enterprise)
2. What's the pricing model? (flat rate / credits / per-seat / usage-based)
3. Monthly billing, annual, or both?
4. What features are gated behind paid? (or let me suggest based on the codebase)
5. Confirm Dodo Payments? (This skill does not implement Stripe or Lemon Squeezy.)
フェーズ 2:プランの定義
プランの定義を一元管理できるようにします。ユーザーの回答に合わせて調整してください。
// config/pricing.ts
export type Plan = 'free' | 'pro' | 'enterprise';
export const PLANS = {
free: {
name: 'Free',
price: 0,
description: 'Get started',
features: ['[Feature 1]', '[Feature 2]'],
limits: {
// Fill based on codebase — e.g., creditsPerMonth: 50
},
cta: 'Get Started',
ctaHref: '/signup',
},
pro: {
name: 'Pro',
priceMonthly: 0, // fill from user
priceAnnual: 0, // fill from user
description: 'For serious users',
features: ['Everything in Free', '[Pro Feature 1]', '[Pro Feature 2]'],
limits: {
// creditsPerMonth: 500, etc.
},
cta: 'Upgrade to Pro',
highlighted: true,
badge: 'Most Popular',
},
} as const;
クレジット制のアプリの場合は、プランの定義と合わせてクレジットパックも定義します。
フェーズ 3:機能の利用制限
利用制限のある機能を実行する前に、アクセス権を確認するユーティリティを作成します。これが強制適用のレイヤーです。それ以外はすべて表示にすぎません。
// lib/feature-gate.ts
type Plan = 'free' | 'pro' | 'enterprise';
// Define which plans can access which features
// Populate based on what actually exists in the codebase
const FEATURE_ACCESS: Record<string, Plan[]> = {
'api-access': ['pro', 'enterprise'],
'export-data': ['pro', 'enterprise'],
'custom-domain': ['enterprise'],
'priority-support': ['enterprise'],
};
export function canAccess(userPlan: Plan, feature: string): boolean {
return FEATURE_ACCESS[feature]?.includes(userPlan) ?? false;
}
API ルートでは、負荷の高い処理を行う前に確認します。
export async function POST(req: Request) {
const user = await getAuthUser(req);
if (!canAccess(user.plan, 'api-access')) {
return Response.json(
{ error: 'This feature requires Pro.', upgradeUrl: '/pricing' },
{ status: 403 }
);
}
// ... rest of handler
}
UI では機能を隠すのではなく、ロックされた状態を表示します。ユーザーがその機能の存在を知る必要があります。
function ExportButton({ userPlan }: { userPlan: Plan }) {
if (!canAccess(userPlan, 'export-data')) {
return (
<button
onClick={() => router.push('/pricing')}
className="opacity-60"
title="Upgrade to Pro to export"
>
🔒 Export — Pro only
</button>
);
}
return <button onClick={handleExport}>Export</button>;
}
フェーズ 4:Dodo Payments の統合
環境変数
DODO_API_KEY= # From Dodo dashboard
DODO_WEBHOOK_SECRET= # whsec_... format — see the dodo-webhook skill
DODO_PRODUCT_ID= # Product ID for Pro plan
APP_URL= # Frontend URL for checkout redirect
インストール:npm install @dodopayments/sdk
チェックアウト作成エンドポイント
// app/api/payments/create-checkout/route.ts
import DodoPayments from '@dodopayments/sdk';
const dodo = new DodoPayments({ bearerToken: process.env.DODO_API_KEY });
export async function POST(req: Request) {
const user = await getAuthUser(req);
const { planId } = await req.json();
const checkout = await dodo.payments.create({
payment_link: true,
customer: { email: user.email },
product_cart: [{ product_id: process.env.DODO_PRODUCT_ID!, quantity: 1 }],
metadata: {
userId: user.id, // REQUIRED — webhook uses this
planId, // which plan they're buying
},
return_url: `${process.env.APP_URL}/checkout/success?plan=${planId}`,
});
return Response.json({ checkout_url: checkout.payment_link });
}
**Webhook がユーザーを特定するために、メタデータを使用します。**メタデータに userId が含まれていないと、Webhook は正しいアカウントを更新できません。dodo-webhook スキルを使用して、Webhook ハンドラーを設定してください。
カスタマーポータル
プランの管理、解約、請求書の履歴確認には、ユーザーを Dodo のホスト型請求ポータルに誘導します。
// app/api/billing/portal/route.ts
export async function GET(req: Request) {
const user = await getAuthUser(req);
const portal = await dodo.customerPortal.create({
customer_id: user.dodoCustomerId,
return_url: `${process.env.APP_URL}/settings/billing`,
});
return Response.json({ portal_url: portal.url });
}
ユーザー設定に、このエンドポイントにアクセスする「請求を管理」リンクを追加します。
チェックアウト成功ページ
/checkout/success を作成します。これは、支払い後に Dodo がユーザーをリダイレクトするページです。Webhook の到着はリダイレクトから数秒遅れる場合があるため、プランが更新されるまでポーリングします。
// app/checkout/success/page.tsx
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
export default function CheckoutSuccess() {
const [plan, setPlan] = useState<string | null>(null);
const router = useRouter();
useEffect(() => {
// Poll until plan updates (webhook may lag by a few seconds)
let attempts = 0;
const interval = setInterval(async () => {
const res = await fetch('/api/auth/me');
const user = await res.json();
if (user.plan !== 'free' || attempts > 10) {
setPlan(user.plan);
clearInterval(interval);
}
attempts++;
}, 1000);
return () => clearInterval(interval);
}, []);
if (!plan) return <p>Confirming your upgrade...</p>;
return (
<div>
<h1>You're on {plan}.</h1>
<p>Your account has been upgraded.</p>
<a href="/dashboard">Go to dashboard →</a>
</div>
);
}
フェーズ 5:料金 UI コンポーネント
レスポンシブな料金コンポーネントを生成します。1 つのプランを目立たせるデザインにしてください。同じ重みで 3 つのプランを提示すると、ユーザーが決められないまま離脱することがよくあります。
// components/pricing-cards.tsx
'use client';
import { PLANS } from '@/config/pricing';
interface PricingCardsProps {
currentPlan?: string;
onUpgrade?: (planId: string) => void;
}
export function PricingCards({ currentPlan, onUpgrade }: PricingCardsProps) {
const handleUpgrade = async (planId: string) => {
const res = await fetch('/api/payments/create-checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ planId }),
});
const { checkout_url } = await res.json();
window.location.href = checkout_url;
};
return (
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{Object.entries(PLANS).map(([planId, plan]) => (
<div
key={planId}
className={`rounded-xl border p-6 ${
'highlighted' in plan && plan.highlighted
? 'border-black shadow-xl'
: 'border-gray-200'
}`}
>
{'badge' in plan && plan.badge && (
<span className="text-xs font-bold uppercase tracking-widest text-black">
{plan.badge}
</span>
)}
<h3 className="mt-2 text-xl font-bold">{plan.name}</h3>
<p className="mt-1 text-3xl font-bold">
{'price' in plan ? (plan.price === 0 ? 'Free' : `$${plan.price}/mo`) : `$${plan.priceMonthly}/mo`}
</p>
<ul className="mt-4 space-y-2">
{plan.features.map((f) => (
<li key={f} className="flex items-center gap-2 text-sm text-gray-700">
<span className="text-green-500">✓</span> {f}
</li>
))}
</ul>
<div className="mt-6">
{currentPlan === planId ? (
<div className="py-2 text-center text-sm text-gray-400">Current plan</div>
) : (
<button
onClick={() => handleUpgrade(planId)}
className={`w-full rounded-lg py-2 text-sm font-medium ${
'highlighted' in plan && plan.highlighted
? 'bg-black text-white'
: 'border border-gray-300 text-gray-700 hover:border-black'
}`}
>
{plan.cta}
</button>
)}
</div>
</div>
))}
</div>
);
}
フェーズ 6:検証
Flow 1: Feature Gating
[ ] Free user hits gated endpoint → 403 with upgradeUrl
[ ] Pro user hits same endpoint → proceeds normally
[ ] Gated UI shows locked state, links to /pricing
Flow 2: Checkout
[ ] "Upgrade" button creates checkout session
[ ] Redirects to Dodo-hosted checkout page
[ ] userId is in checkout metadata
[ ] After payment, redirects to /checkout/success
Flow 3: Webhook (handled by the dodo-webhook skill)
[ ] Webhook verified and processed
[ ] User plan updated in database
[ ] Success page reflects new plan after polling
Flow 4: Billing Portal
[ ] "Manage billing" link accessible in settings
[ ] Opens Dodo customer portal
[ ] Returns to app after portal actions
Flow 5: Edge Cases
[ ] Users without dodoCustomerId don't crash portal link
[ ] Checkout success polling stops after plan updates
[ ] Env vars in .env.example, not hardcoded
料金設定の心理学、A/B テストのアイデア、高度な機能利用制限のパターンについては、references/guide.md を参照してください。