Files
waggle-os/apps/www/SESIJA-E-MANIFEST.md
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

189 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Sesija E Manifest — Clerk auth + Stripe-Clerk linkage + branding fixes
**Date:** Started 2026-05-03, last entry 2026-05-04
**Branch:** main (SUPPORT mode — work main directly, commit per logical milestone)
**Brief:** CC Sesija E — Clerk auth + Stripe-Clerk linkage + FR Pass8-A logo fix
**Cost cap:** $15 hard / $12 halt
**Status:** §5.0§5.3 COMPLETE · §5.4 / §5.5 / §5.6 PENDING
---
## Commit ledger (4 commits to date — extends through §5.6)
| # | SHA | Phase | Description |
|---|---|---|---|
| 1 | `4365897` | §5.0 | Scaffold Clerk integration (deps + middleware + ClerkProvider in layout) |
| 2 | `d281a86` | §5.1 + §5.2 | Wire Clerk auth UI in navbar + account page (`SignedIn` / `SignedOut`, `/account` `UserProfile`) |
| 3 | `0147d6c` | §5.3 Phase A | Provision Stripe test-mode prices via CLI — Path A idempotent reuse, no new products created |
| 4 | `a087cf6` | §5.3 Phase C | Connect Stripe Customer to Clerk user.publicMetadata via lazy-create pattern; add webhook handler |
| 5 | (this) | §5.3 Phase E | Manifest + smoke-test instructions |
---
## §5.3 Stripe catalog state (test mode, account `acct_1SzHlbC0mmjh4oEM`)
| Lookup key | Price ID | Amount | Product | Metadata |
|---|---|---|---|---|
| `pro_monthly` | `price_1TNZfkC0mmjh4oEMGAZ2PDbc` | $19/mo | `prod_UMIG4B7V0Ke6zQ` (Waggle Pro) | tier=pro, billing=monthly |
| `pro_annual` | `price_1TTN4FC0mmjh4oEMyaEX40Kl` | $190/yr | `prod_UMIG4B7V0Ke6zQ` (Waggle Pro) | tier=pro, billing=annual |
| `teams_monthly` | `price_1TNZfpC0mmjh4oEMH10c02YB` | $49/mo | `prod_UMIGZ99xtazCAs` (Waggle Teams) | tier=teams, billing=monthly, seat_minimum=3 |
| `teams_annual` | `price_1TTN4HC0mmjh4oEMsgKwV1MY` | $490/yr | `prod_UMIGZ99xtazCAs` (Waggle Teams) | tier=teams, billing=annual, seat_minimum=3 |
**Archived in Phase A** (older 2026-04-08 duplicates, no longer active):
- `prod_UG0WP8RCBvaiQ2` (Waggle Teams 2026-04-08, wrong amount)
- `prod_UG0Wk4DLVpsRwf` (Waggle Basic 2026-04-08, no longer in tier roadmap)
Recreating these IDs on a new Stripe account: see `apps/www/.env.local.example` for the CLI provisioning command template.
---
## §5.3 Phase B — Local webhook listener (Marko-side, separate terminal)
For local dev webhook testing, run in a terminal separate from `next dev`:
```powershell
cd D:\Projects\waggle-os
stripe listen --forward-to localhost:3001/api/webhooks/stripe --print-secret
```
Stripe CLI prints the signing secret (`whsec_...`) on first run — paste
it into `apps/www/.env.local` as `STRIPE_WEBHOOK_SECRET=whsec_...`. Leave
the listener running for the duration of the test session; the secret
itself is stable across restarts of the same `stripe listen` invocation.
**Why CC didn't run this:** `stripe listen` is a long-running process. CC
sessions can't host indefinitely-blocking foreground commands without
losing the rest of the conversation, so this is documented for Marko to
run instead. The webhook handler at `apps/www/app/api/webhooks/stripe/route.ts`
is fully implemented and waiting to receive deliveries once the secret
is pasted and the listener is up.
**Production webhook setup is Marko-side ponedeljak 14:00:**
1. Stripe Dashboard → Developers → Webhooks → "Add endpoint".
2. Endpoint URL: `https://waggle-os.ai/api/webhooks/stripe` (or whichever production deploy URL).
3. Subscribe to events: `checkout.session.completed`, `customer.subscription.updated`, `customer.subscription.deleted`.
4. Copy the new endpoint's signing secret into the production env as
`STRIPE_WEBHOOK_SECRET=whsec_...` (production secret is **different**
from the local `stripe listen` secret).
5. Live key swap: replace `STRIPE_SECRET_KEY=sk_test_...` with
`STRIPE_SECRET_KEY=sk_live_...` in production env. The route accepts
both `sk_test_*` and `sk_live_*` automatically — no code changes
required.
---
## §5.3 Phase E — End-to-end smoke test
**Pre-conditions** (run once, then leave running):
| | Action |
|---|---|
| 1 | `STRIPE_SECRET_KEY=sk_test_*` (real value, not placeholder) is in `apps/www/.env.local` |
| 2 | `STRIPE_WEBHOOK_SECRET=whsec_*` (from `stripe listen --print-secret`) is in `apps/www/.env.local` |
| 3 | All 4 `STRIPE_PRICE_*` IDs in `apps/www/.env.local` (already done in §5.3 Phase A) |
| 4 | Terminal 1 — Next.js dev server running. Either `cd D:\Projects\waggle-os\apps\www && npx next dev -H 0.0.0.0 -p 3001`, or from repo root `npm run dev -w apps/www -- -H 0.0.0.0 -p 3001` (`-w` is the npm workspace flag; `--` separates npm args from forwarded script args). |
| 5 | Terminal 2 — `stripe listen --forward-to localhost:3001/api/webhooks/stripe` is running |
**Browser walkthrough:**
1. Visit `http://localhost:3001` → click **Sign up** in the navbar → complete Clerk sign-up (any test email, any password ≥ 8 chars).
2. Land on `/pricing` (or scroll to the pricing section on `/`).
3. On the **Pro** tier card, click "Start free trial" (or whatever the CTA is — see `landing.pricing.tiers.pro.cta` in `messages/en.json`).
4. The button currently POSTs to `/api/stripe/checkout` and opens the returned URL in a new tab. (§5.4 will migrate this to a GET-based redirect flow.)
5. On Stripe Checkout, use test card `4242 4242 4242 4242` (any future expiry, any 3-digit CVC, any ZIP).
6. After payment, Stripe redirects to `/account?checkout=success&session_id=cs_...`.
**Verify state:**
| Check | Where | Expected |
|---|---|---|
| Clerk user metadata | Clerk Dashboard → Users → your user → "Public metadata" tab | `stripeCustomerId: cus_...` · `subscriptionTier: pro` · `subscriptionStatus: active` |
| Stripe customer | Stripe Dashboard test mode → Customers → newest customer | `metadata.clerkUserId = <your Clerk user id>` |
| Webhook delivery | Terminal 2 (`stripe listen` log) | `checkout.session.completed [200]` · `customer.subscription.created [200]` · `customer.subscription.updated [200]` |
| Subscription on customer | Stripe Dashboard → Customers → your customer → Subscriptions | One active subscription on the price you selected (Pro monthly = `price_1TNZfkC0mmjh4oEMGAZ2PDbc`) |
| Checkout Session metadata | Stripe Dashboard → Payments → newest session → Metadata | `clerkUserId`, `tier`, `billing` all populated |
If any check fails: see Troubleshooting below.
---
## §5.3 Troubleshooting (smoke-test failure modes)
| Symptom | Likely cause | Fix |
|---|---|---|
| Pricing button click → 401 alert "Sign in required" | You haven't signed up / signed in yet | Sign up first via the navbar, then retry |
| Pricing button click → 503 "configuration required" | `STRIPE_SECRET_KEY` is still placeholder (`REPLACE_AFTER_ROTATION`) | Paste real `sk_test_*` from `stripe config --list` into `apps/www/.env.local`, restart `next dev` |
| Pricing button → 503 "No active Stripe price found" | `lookup_key` not set on the price (or env var is wrong) | Re-run §5.3 Phase A `stripe prices update` for the missing tier |
| Checkout completes but Clerk metadata stays empty | Webhook listener not running OR signature secret mismatch | Confirm `stripe listen` is up, secret in `.env.local` matches `--print-secret` output, restart `next dev` after pasting |
| `checkout.session.completed [400]` in `stripe listen` log | "Signature verification failed" — listener restarted with a new secret while old one is still in env | Copy fresh secret from listener's startup line, paste into `.env.local`, restart `next dev` |
| Webhook fires but Clerk metadata doesn't update | Session metadata missing `clerkUserId` (rare — only if checkout was created outside this app) | Inspect the offending session's metadata in Stripe Dashboard; the route always sets it on new sessions |
| Stripe customer created but Clerk `stripeCustomerId` not saved | Clerk publicMetadata write failed silently | Check `next dev` logs for Clerk errors; verify `CLERK_SECRET_KEY` is the freshly rotated key, not stale |
---
## §5.3 Phase D — Why no Clerk webhook is needed
The **lazy-create pattern** attaches a Stripe Customer at first paid
checkout, not at user creation. As a result:
- No `user.created` Clerk webhook handler is needed.
- `apps/www/app/api/webhooks/clerk/` is **not** created in §5.3.
- `CLERK_WEBHOOK_SECRET` in `.env.local` stays as
`REPLACE_AFTER_WEBHOOK_REGISTERED` indefinitely (or remove the line).
**Trade-off:** a user who signs up and never reaches checkout has no
`stripeCustomerId` in their publicMetadata. That's correct — they
shouldn't have a Stripe customer record until they convert. The
checkout route's `ensureStripeCustomer` handles the gap on first pay.
If we later decide to support pre-paid features (e.g. trial without a
card), pre-creating Stripe customers eagerly via Clerk webhook becomes
useful. For now, lazy-create is simpler and avoids the
"orphan Stripe customer" failure mode.
---
## Out of scope (locked for §5.4+)
- **Pricing.tsx CTA gating** — §5.4 will migrate from `POST` + `window.open` to:
- Signed-in users: GET `/api/stripe/checkout?tier=...&billing=...` → 303 to Stripe Checkout
- Signed-out users: open `<SignUpButton>` modal with `forceRedirectUrl=/api/stripe/checkout?tier=...&billing=...` so they land on Stripe Checkout immediately after sign-up
- **POST backward-compat shim removal** — once §5.4 migrates Pricing.tsx, the POST handler in `/api/stripe/checkout/route.ts` is dead code and should be deleted.
- **FR Pass8-A logo fix** — §5.5 (filename mismatch in HeroVisual asset reference, root cause isolated in §5.0 brief).
- **§5.6 final verification** — full landing-page screenshot diff vs. Sesija D baseline + visual confirmation that Hive DS hasn't regressed.
- **Live keys + production webhook endpoint** — Marko-side ponedeljak 14:00.
---
## Day-2 polish backlog (post §5.6)
| Item | Severity | Source | Notes |
|---|---|---|---|
| Remove POST shim from `/api/stripe/checkout` | low | §5.3 Phase C | Once §5.4 migrates Pricing.tsx to GET, the POST handler is dead code — delete the function + its `runCheckout` POST branch. |
| Stripe customer email sync | medium | observed in route | `ensureStripeCustomer` sets email from Clerk's `primaryEmailAddress` once. If user changes email in Clerk, Stripe customer email goes stale. Consider a Clerk `user.updated` webhook OR periodic sync (low priority — Stripe's email is mainly used for receipts, which can be customized). |
| `subscriptionStatus: 'paused'` collapsed to `canceled` | low | `webhooks/stripe/route.ts` `mapStatus` | Stripe's `paused` is not technically canceled. If we ever support paid-tier pausing in the UI, distinguish. Current collapse is the "UI shows it as inactive" semantic. |
| Webhook idempotency | medium | best practice | Stripe can deliver the same event twice (rare but documented). Current handlers are idempotent for the metadata fields they touch (last-write wins is fine for status). If we add side effects like email sends, gate on `event.id` dedup table. |
| Production env vars | high (pre-launch) | §5.3 Phase B | Replace test-mode keys with `sk_live_*` + production `whsec_*`. Production prices may differ from test-mode prices — re-provision via CLI on the live account, capture new IDs, update production env. |
| Subscription receipts customization | low | Stripe default | Stripe sends receipts to `customer.email` by default. Verify with Marko whether the brand wants Stripe-default receipts or a custom outbound. |
---
## §5.3 acceptance criteria
| # | Criterion | Status | Evidence |
|---|---|---|---|
| 1 | Stripe catalog has 2 active products (Pro + Teams) with proper lookup_keys + metadata, no duplicates added | ✅ | Path A applied; `stripe products list --active=true \| grep -i waggle` shows exactly 2 |
| 2 | All 4 prices have `lookup_key` matching `${tier}_${billing}` and `metadata[tier]` + `metadata[billing]` set | ✅ | `stripe prices list \| jq '.data[] \| select(.lookup_key \| startswith("pro_") or startswith("teams_"))'` confirms |
| 3 | `apps/www/.env.local` has all 4 real `STRIPE_PRICE_*` IDs (no placeholders) | ✅ | gitignored; verified by Marko paste of secret + this commit's env update |
| 4 | `apps/www/.env.local.example` documents the 4 keys + CLI provisioning command | ✅ | Commit `0147d6c` |
| 5 | `/api/stripe/checkout` enforces auth, lazy-creates Stripe Customer, resolves price by lookup_key with env-pin fallback | ✅ | Commit `a087cf6` |
| 6 | `/api/webhooks/stripe` verifies signature + handles 3 event types + maps to Clerk publicMetadata | ✅ | Commit `a087cf6` |
| 7 | No Clerk webhook handler created (lazy-create pattern) | ✅ | `apps/www/app/api/webhooks/clerk/` does not exist |
| 8 | TypeScript compiles clean (`npx tsc --noEmit`) | ✅ | Verified post-Phase-C |
| 9 | Manifest documents Phase B (local listener) + Phase E (smoke test) + production webhook plan | ✅ | This document |
| 10 | Smoke test executable end-to-end | ⏸ Pending | Requires Marko to run with `stripe listen` — see Phase E walkthrough above |
**9 / 10 PASS · 1 pending Marko-side smoke run.**