moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,188 @@
# 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.**