# Prompt-to-PR: Ajouter une page de tarification

> SOP pour générer une page de tarification de production avec bascule mensuelle/annuelle, matrice de fonctionnalités et liens Stripe Checkout dans un projet Next.js Tailwind.

**Type:** Playbook  
**Tools:** Cursor, Claude Code, Codex, Windsurf  
**Stack:** Next.js, TypeScript, Tailwind  
**Difficulty:** easy  
**Updated:** 2026-06-08

---

L'une des pages les plus générées dans les sessions de codage IA. Ce playbook contraint l'agent à produire de véritables liens Stripe Checkout câblés plutôt que des boutons factices.

## 1. Exigence

Ajouter une page `/pricing` avec deux ou trois niveaux, une bascule de facturation mensuelle/annuelle (annuelle donne une réduction de 20 %), une matrice de comparaison des fonctionnalités et une intégration Stripe Checkout. Les prix doivent provenir de variables d'environnement, ne pas être codés en dur dans JSX.

## 2. Première invite

```txt title="First Prompt"
Add a /pricing page to this Next.js 15 App Router project with Tailwind.

Requirements:
1. Create `src/app/pricing/page.tsx` — a Server Component that renders
   `<PricingCards>`.
2. Create `src/components/PricingCards.tsx` — a Client Component with:
   - Monthly/annual toggle (useState). Annual prices are monthly × 0.8.
   - Three tiers: Hobby (free), Pro, Business. Read plan data from
     `src/config/pricing.ts` — not hardcoded in JSX.
   - A feature matrix table below the cards showing which features each tier
     includes. Use checkmarks and dashes; do not use emojis in code.
   - A "Get started" CTA for Hobby (links to /sign-up) and "Subscribe" for
     paid tiers.
3. Create `src/config/pricing.ts` exporting a `PLANS` array. Each plan has:
   { id, name, monthlyPriceUsd, stripePriceIdMonthly, stripePriceIdAnnually,
     features: string[], highlighted: boolean }
   Read Stripe price IDs from env vars:
     NEXT_PUBLIC_STRIPE_PRICE_PRO_MONTHLY
     NEXT_PUBLIC_STRIPE_PRICE_PRO_ANNUALLY
     NEXT_PUBLIC_STRIPE_PRICE_BIZ_MONTHLY
     NEXT_PUBLIC_STRIPE_PRICE_BIZ_ANNUALLY
4. Create `src/app/api/checkout/route.ts` (POST). Accept `{ priceId, userId }`.
   Create a Stripe Checkout Session (mode: "subscription") with:
   - success_url: NEXT_PUBLIC_APP_URL + /dashboard?checkout=success
   - cancel_url: NEXT_PUBLIC_APP_URL + /pricing
   Return `{ url }`.
5. In PricingCards, the Subscribe button POSTs to /api/checkout and redirects
   to the returned url.
6. Install `stripe` package. Use env var STRIPE_SECRET_KEY.
```

## 3. Modifications de fichiers attendues

```txt
package.json                             (stripe)
src/app/pricing/page.tsx                 (new — Server Component shell)
src/components/PricingCards.tsx          (new — Client Component with toggle)
src/config/pricing.ts                    (new — plan definitions)
src/app/api/checkout/route.ts            (new — Stripe Checkout Session)
.env.local.example                       (STRIPE_* and NEXT_PUBLIC_* vars)
```

## 4. Liste de vérification

- `STRIPE_SECRET_KEY` n'est jamais importé dans un composant client ni exposé via `NEXT_PUBLIC_`.
- La session Checkout utilise `mode: "subscription"` et non `mode: "payment"` pour la facturation récurrente.
- Les IDs de prix annuels sont des objets Stripe Price séparés — pas une remise calculée dans le code.
- La bascule montre clairement les économies annuelles (par exemple « Économisez 20 % »).
- Le tableau `PLANS` est la source unique de vérité — la matrice de fonctionnalités réutilise les mêmes données, pas un tableau codé en dur séparé.
- Gestion des erreurs sur le POST checkout : renvoie une erreur 500 avec un message si Stripe échoue.
- Aucun `console.log` des clés Stripe ou des données client laissé dans le code.

## 5. Commandes de test

```bash
bun dev

# Visit http://localhost:3000/pricing
# Toggle monthly/annual — confirm prices update correctly
# Confirm annual = monthly * 0.8

# Test checkout endpoint with a test price ID
curl -X POST http://localhost:3000/api/checkout \
  -H "Content-Type: application/json" \
  -d '{"priceId":"price_test_xxx","userId":"user_1"}' | jq .url
# Expect a Stripe Checkout URL

bun tsc --noEmit
```

## 6. Échecs courants

- **`STRIPE_SECRET_KEY` divulgué côté client** — l'agent importe `stripe` dans `PricingCards.tsx`. Déplacez tous les appels Stripe vers la route API.
- **La bascule annuelle affiche le même prix que mensuel** — l'agent calcule `price * 0.8` mais oublie d'utiliser l'état `isAnnual`. Confirmez que l'état de la bascule est passé à l'affichage du prix.
- **Checkout renvoie 500 avec « No such price »** — la variable d'environnement de l'ID de prix n'est pas définie ou le mauvais environnement (test vs live). Vérifiez avec `stripe prices retrieve <id>` dans la Stripe CLI.
- **`success_url` est relative** — Stripe nécessite une URL absolue. Préfixez avec `NEXT_PUBLIC_APP_URL`.

## 7. Invite de correction

```txt title="Fix Prompt"
The annual/monthly toggle renders correctly visually but the Subscribe button
always sends the monthly priceId regardless of which toggle state is active.

In PricingCards.tsx, the `priceId` passed to the checkout POST must be
`isAnnual ? plan.stripePriceIdAnnually : plan.stripePriceIdMonthly`.
Check that the isAnnual state variable is read at the point the button's
onClick handler calls the checkout API.
```

## 8. Description de la PR

```md title="PR description"
## Feature: Pricing page with Stripe Checkout

- `/pricing` page with monthly/annual toggle and three tiers (Hobby, Pro, Business)
- Feature matrix driven from `src/config/pricing.ts` — one source of truth
- POST `/api/checkout` creates a Stripe Subscription Checkout Session
- Price IDs read from env vars — no hardcoded Stripe IDs in code
- Annual plan = 20% discount via separate Stripe Price objects

**Required env vars**: `STRIPE_SECRET_KEY`, `NEXT_PUBLIC_STRIPE_PRICE_*`,
`NEXT_PUBLIC_APP_URL` (see `.env.local.example`)
```