create-ailk 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/component-catalog.md +210 -52
- package/dist/cli.js +0 -0
- package/dist/lib/apply-module-patches.d.ts +47 -0
- package/dist/lib/apply-module-patches.js +370 -0
- package/dist/lib/extract-module-bundle.d.ts +12 -3
- package/dist/lib/extract-module-bundle.js +236 -24
- package/dist/lib/fetch-module.d.ts +3 -38
- package/dist/lib/fetch-module.js +26 -8
- package/dist/lib/module-license-gate.d.ts +2 -0
- package/dist/lib/module-license-gate.js +1 -0
- package/dist/lib/paid-paths.d.ts +16 -0
- package/dist/lib/paid-paths.js +90 -0
- package/dist/module-architecture.d.ts +139 -23
- package/dist/module-architecture.js +668 -25
- package/dist/parse-args.d.ts +2 -2
- package/dist/parse-args.js +3 -1
- package/dist/programmatic.d.ts +2 -1
- package/dist/programmatic.js +15 -4
- package/dist/surfaces.d.ts +12 -36
- package/dist/surfaces.js +35 -5
- package/dist/sync-routes.js +31 -1
- package/package.json +14 -15
- package/templates/.claude/agents/web.md +2 -3
- package/templates/.claude/rules/architecture.md +5 -5
- package/templates/.claude/skills/README.md +2 -1
- package/templates/.env.example +21 -9
- package/templates/CONVENTIONS.md +6 -7
- package/templates/apps/api/.env.example +9 -0
- package/templates/apps/api/CLAUDE.md +35 -4
- package/templates/apps/api/package.json +0 -1
- package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
- package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
- package/templates/apps/api/src/config/index.ts +19 -0
- package/templates/apps/api/src/config/modules.ts +12 -14
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +13 -0
- package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
- package/templates/apps/api/src/lib/slice-load.ts +90 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
- package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
- package/templates/apps/api/src/openapi/index.ts +9 -3
- package/templates/apps/api/src/openapi/spec.ts +505 -65
- package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
- package/templates/apps/api/src/routes/content/index.ts +38 -11
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +28 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +144 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +478 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +717 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +35 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +484 -0
- package/templates/apps/api/src/routes/project-listings/index.ts +52 -10
- package/templates/apps/api/src/routes/project-listings/me.ts +86 -0
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +38 -0
- package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
- package/templates/apps/api/src/server.ts +471 -133
- package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
- package/templates/apps/api/src/services/consent.ts +236 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
- package/templates/apps/api/src/services/listing-config.ts +58 -0
- package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
- package/templates/apps/api/src/services/project-listings.ts +602 -29
- package/templates/apps/api/src/vercel-handler.ts +60 -26
- package/templates/apps/mcp/.env.example +8 -0
- package/templates/apps/mcp/CLAUDE.md +2 -2
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
- package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
- package/templates/apps/mcp/src/config/modules.ts +14 -13
- package/templates/apps/mcp/src/server.ts +8 -4
- package/templates/apps/mcp/src/tools/index.ts +7 -22
- package/templates/apps/web/.env.example +15 -0
- package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/layout.tsx +13 -1
- package/templates/apps/web/app/llms.txt/route.ts +12 -9
- package/templates/apps/web/jest.config.cjs +20 -13
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
- package/templates/apps/web/lib/site-brand.tsx +4 -1
- package/templates/apps/web/lib/site-theme.ts +74 -0
- package/templates/apps/web/next.config.mjs +1 -3
- package/templates/apps/web/package.json +1 -6
- package/templates/apps/web/public/android-chrome-192x192.png +0 -0
- package/templates/apps/web/public/android-chrome-512x512.png +0 -0
- package/templates/apps/web/public/apple-touch-icon.png +0 -0
- package/templates/apps/web/public/favicon-16x16.png +0 -0
- package/templates/apps/web/public/favicon-32x32.png +0 -0
- package/templates/apps/web/public/favicon.ico +0 -0
- package/templates/apps/web/public/favicon.svg +6 -3
- package/templates/apps/web/public/lockup-horizontal.svg +4 -0
- package/templates/apps/web/public/logomark.svg +4 -0
- package/templates/apps/web/public/site.webmanifest +2 -2
- package/templates/apps/web/public/wordmark.svg +4 -0
- package/templates/content/_site.mdx +12 -0
- package/templates/database/CHANGELOG.md +94 -0
- package/templates/database/inbox/schema.prisma +165 -0
- package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
- package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
- package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
- package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
- package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
- package/templates/database/package.json +1 -1
- package/templates/database/scripts/db-generate-locked.sh +0 -0
- package/templates/package.json +1 -1
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
- package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
- package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
- package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
- package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
- package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
- package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
- package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
- package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
- package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
- package/templates/apps/api/src/__tests__/server.test.ts +0 -253
- package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
- package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
- package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
- package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
- package/templates/apps/api/src/bin/seed-presets.ts +0 -58
- package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
- package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
- package/templates/apps/api/src/lib/stripe.ts +0 -52
- package/templates/apps/api/src/lib/tenant-db.ts +0 -225
- package/templates/apps/api/src/lib/ws-token.ts +0 -91
- package/templates/apps/api/src/middleware/tenant.ts +0 -99
- package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
- package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
- package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
- package/templates/apps/api/src/routes/billing/index.ts +0 -36
- package/templates/apps/api/src/routes/billing/portal.ts +0 -182
- package/templates/apps/api/src/routes/billing/read.ts +0 -75
- package/templates/apps/api/src/routes/billing/usage.ts +0 -153
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
- package/templates/apps/api/src/routes/checkout/index.ts +0 -13
- package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
- package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
- package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
- package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
- package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
- package/templates/apps/api/src/routes/flows/README.md +0 -147
- package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
- package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
- package/templates/apps/api/src/routes/flows/index.ts +0 -202
- package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
- package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
- package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
- package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
- package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
- package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
- package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
- package/templates/apps/api/src/routes/schedule/index.ts +0 -249
- package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
- package/templates/apps/api/src/routes/slack/actions.ts +0 -177
- package/templates/apps/api/src/routes/slack/index.ts +0 -38
- package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
- package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
- package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
- package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
- package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
- package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
- package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
- package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
- package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
- package/templates/apps/api/src/routes/webhooks/README.md +0 -80
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
- package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
- package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
- package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
- package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
- package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
- package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
- package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
- package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
- package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
- package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
- package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
- package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
- package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
- package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
- package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
- package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
- package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
- package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
- package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
- package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
- package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
- package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
- package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
- package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
- package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
- package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
- package/templates/apps/api/src/services/flow-engine.ts +0 -1278
- package/templates/apps/api/src/services/lead-promotion.ts +0 -176
- package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
- package/templates/apps/api/src/services/playbook-compile.ts +0 -398
- package/templates/apps/api/src/services/playbook-render.ts +0 -263
- package/templates/apps/api/src/services/project-listing-decision.ts +0 -490
- package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
- package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
- package/templates/apps/api/src/services/recommender-capture.ts +0 -835
- package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
- package/templates/apps/api/src/services/scheduling/index.ts +0 -63
- package/templates/apps/api/src/services/scheduling/types.ts +0 -88
- package/templates/apps/api/src/services/tenant-context.ts +0 -451
- package/templates/apps/api/src/services/usage-metering.ts +0 -699
- package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
- package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
- package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
- package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
- package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
- package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
- package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
- package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
- package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
- package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
- package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
- package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
- package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
- package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
- package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
- package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
- package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
- package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
- package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
- package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
- package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
- package/templates/apps/web/content/en/blog/README.txt +0 -25
- package/templates/apps/web/content/en/docs/index.mdx +0 -28
- package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
- package/templates/apps/web/lib/blog-list-data.ts +0 -95
- package/templates/apps/web/lib/blog-post-data.ts +0 -79
- package/templates/apps/web/lib/source.ts +0 -34
- package/templates/apps/web/public/logomark-dark.svg +0 -4
- package/templates/apps/web/public/logomark-light.svg +0 -4
- package/templates/apps/web/source.config.ts +0 -78
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* /[locale]/waitlist — the operator dashboard's list + rollup view
|
|
3
|
-
* (#4665 D10, AC1).
|
|
4
|
-
*
|
|
5
|
-
* RSC: reads `GET /v1/waitlist-experiments` through
|
|
6
|
-
* `@working-theory/api-client`'s `waitlistExperiments.list`, forwarding the
|
|
7
|
-
* caller's cookie session (`../../../../lib/server-api.js`, D9). A 401/403
|
|
8
|
-
* renders `ForbiddenState` (no leak of why — §2.2 Security); every other
|
|
9
|
-
* error rethrows to the route's error boundary.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { waitlistExperiments } from "@working-theory/api-client";
|
|
13
|
-
import { PageHeader } from "@working-theory/ui/blocks";
|
|
14
|
-
import { Container, Heading, Text } from "@working-theory/ui/primitives";
|
|
15
|
-
import type { WaitlistMode, WaitlistStatus } from "@working-theory/validation";
|
|
16
|
-
import type { Metadata } from "next";
|
|
17
|
-
import { getTranslations } from "next-intl/server";
|
|
18
|
-
import type { ReactElement } from "react";
|
|
19
|
-
|
|
20
|
-
import { ApiError, forwardedCookieInit, unwrapApiResult } from "../../../../lib/server-api";
|
|
21
|
-
|
|
22
|
-
import { ExperimentsTable } from "./components/ExperimentsTable";
|
|
23
|
-
import { ForbiddenState } from "./components/ForbiddenState";
|
|
24
|
-
import { OfferFunnelTable } from "./components/OfferFunnelTable";
|
|
25
|
-
import { RollupTicker } from "./components/RollupTicker";
|
|
26
|
-
import { formatCount, formatDateTime, formatRate } from "./format";
|
|
27
|
-
|
|
28
|
-
interface Props {
|
|
29
|
-
params: Promise<{ locale: string }>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
33
|
-
const { locale } = await params;
|
|
34
|
-
const t = await getTranslations({ locale, namespace: "waitlistDashboard.list" });
|
|
35
|
-
return { title: t("title"), description: t("description") };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export default async function WaitlistListPage({ params }: Props): Promise<ReactElement> {
|
|
39
|
-
const { locale } = await params;
|
|
40
|
-
const t = await getTranslations({ locale, namespace: "waitlistDashboard" });
|
|
41
|
-
|
|
42
|
-
let data;
|
|
43
|
-
try {
|
|
44
|
-
data = await unwrapApiResult(waitlistExperiments.list(await forwardedCookieInit()));
|
|
45
|
-
} catch (err) {
|
|
46
|
-
if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
|
|
47
|
-
return (
|
|
48
|
-
<ForbiddenState
|
|
49
|
-
headline={t("common.forbiddenHeadline")}
|
|
50
|
-
description={t("common.forbiddenDescription")}
|
|
51
|
-
/>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
throw err;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const statusLabels: Record<WaitlistStatus, string> = {
|
|
58
|
-
live: t("status.live"),
|
|
59
|
-
closed: t("status.closed"),
|
|
60
|
-
draft: t("status.draft"),
|
|
61
|
-
};
|
|
62
|
-
const modeLabels: Record<WaitlistMode, string> = {
|
|
63
|
-
"lead-capture": t("mode.lead-capture"),
|
|
64
|
-
qualifying: t("mode.qualifying"),
|
|
65
|
-
paid: t("mode.paid"),
|
|
66
|
-
// #4767 — the register's fourth mode; the engine/UI that reads it are
|
|
67
|
-
// sibling children of Epic #4758 (out of scope here). A placeholder
|
|
68
|
-
// label only, so `Record<WaitlistMode, …>` stays exhaustive.
|
|
69
|
-
recommending: t("mode.recommending"),
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const rows = data.experiments.map((experiment) => ({
|
|
73
|
-
experiment,
|
|
74
|
-
raw: formatCount(experiment.counts.raw, locale),
|
|
75
|
-
qualified:
|
|
76
|
-
experiment.counts.qualified === null
|
|
77
|
-
? t("common.unavailable")
|
|
78
|
-
: formatCount(experiment.counts.qualified, locale),
|
|
79
|
-
rate: formatRate(experiment.counts.qualificationRate, locale),
|
|
80
|
-
updated: formatDateTime(experiment.updatedAt, locale),
|
|
81
|
-
}));
|
|
82
|
-
|
|
83
|
-
// #4839 — one per-offer funnel per recommending experiment (the register's
|
|
84
|
-
// `offers[]` order), rendered under the experiments table.
|
|
85
|
-
const offerFunnels = data.experiments.flatMap((experiment) =>
|
|
86
|
-
experiment.counts.offers
|
|
87
|
-
? [
|
|
88
|
-
{
|
|
89
|
-
experimentId: experiment.id,
|
|
90
|
-
experimentKey: experiment.key,
|
|
91
|
-
rows: experiment.counts.offers.map((offer) => ({
|
|
92
|
-
offerKey: offer.offerKey,
|
|
93
|
-
recommended: formatCount(offer.recommended, locale),
|
|
94
|
-
saved: formatCount(offer.saved, locale),
|
|
95
|
-
checkedOut: formatCount(offer.checkedOut, locale),
|
|
96
|
-
})),
|
|
97
|
-
},
|
|
98
|
-
]
|
|
99
|
-
: [],
|
|
100
|
-
);
|
|
101
|
-
const offerLabels = {
|
|
102
|
-
captionPrefix: t("list.offers.captionPrefix"),
|
|
103
|
-
columnOffer: t("list.offers.columnOffer"),
|
|
104
|
-
columnRecommended: t("list.offers.columnRecommended"),
|
|
105
|
-
columnSaved: t("list.offers.columnSaved"),
|
|
106
|
-
columnCheckedOut: t("list.offers.columnCheckedOut"),
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
return (
|
|
110
|
-
<main className="min-h-screen bg-surface text-fg" data-testid="waitlist-list-page">
|
|
111
|
-
<PageHeader />
|
|
112
|
-
<Container className="flex flex-col gap-6 py-section">
|
|
113
|
-
<div>
|
|
114
|
-
<Heading level={1}>{t("list.title")}</Heading>
|
|
115
|
-
<Text as="p" variant="body-sm" tone="muted" className="mt-1">
|
|
116
|
-
{t("list.description")}
|
|
117
|
-
</Text>
|
|
118
|
-
</div>
|
|
119
|
-
|
|
120
|
-
<RollupTicker
|
|
121
|
-
ariaLabel={t("list.rollupAriaLabel")}
|
|
122
|
-
labels={{
|
|
123
|
-
live: t("list.rollup.live"),
|
|
124
|
-
closed: t("list.rollup.closed"),
|
|
125
|
-
draft: t("list.rollup.draft"),
|
|
126
|
-
raw: t("list.rollup.raw"),
|
|
127
|
-
qualified: t("list.rollup.qualified"),
|
|
128
|
-
rate: t("list.rollup.rate"),
|
|
129
|
-
sites: t("list.rollup.sites"),
|
|
130
|
-
}}
|
|
131
|
-
values={{
|
|
132
|
-
live: formatCount(data.rollup.experiments.live, locale),
|
|
133
|
-
closed: formatCount(data.rollup.experiments.closed, locale),
|
|
134
|
-
draft: formatCount(data.rollup.experiments.draft, locale),
|
|
135
|
-
raw: formatCount(data.rollup.raw, locale),
|
|
136
|
-
qualified:
|
|
137
|
-
data.rollup.qualified === null
|
|
138
|
-
? t("common.unavailable")
|
|
139
|
-
: formatCount(data.rollup.qualified, locale),
|
|
140
|
-
rate: formatRate(data.rollup.qualificationRate, locale),
|
|
141
|
-
...(data.scope === "all"
|
|
142
|
-
? { sites: formatCount(data.rollup.sites, locale) }
|
|
143
|
-
: {}),
|
|
144
|
-
}}
|
|
145
|
-
/>
|
|
146
|
-
|
|
147
|
-
<ExperimentsTable
|
|
148
|
-
locale={locale}
|
|
149
|
-
rows={rows}
|
|
150
|
-
labels={{
|
|
151
|
-
caption: t("list.table.caption"),
|
|
152
|
-
columnKey: t("list.table.columnKey"),
|
|
153
|
-
columnMode: t("list.table.columnMode"),
|
|
154
|
-
columnStatus: t("list.table.columnStatus"),
|
|
155
|
-
columnWaitlists: t("list.table.columnWaitlists"),
|
|
156
|
-
columnRaw: t("list.table.columnRaw"),
|
|
157
|
-
columnQualified: t("list.table.columnQualified"),
|
|
158
|
-
columnRate: t("list.table.columnRate"),
|
|
159
|
-
columnUpdated: t("list.table.columnUpdated"),
|
|
160
|
-
emptyHeadline: t("list.table.emptyHeadline"),
|
|
161
|
-
emptyDescription: t("list.table.emptyDescription"),
|
|
162
|
-
statusLabel: (status) => statusLabels[status as WaitlistStatus],
|
|
163
|
-
modeLabel: (mode) => modeLabels[mode as WaitlistMode],
|
|
164
|
-
}}
|
|
165
|
-
/>
|
|
166
|
-
|
|
167
|
-
{offerFunnels.map((funnel) => (
|
|
168
|
-
<OfferFunnelTable
|
|
169
|
-
key={funnel.experimentId}
|
|
170
|
-
experimentId={funnel.experimentId}
|
|
171
|
-
experimentKey={funnel.experimentKey}
|
|
172
|
-
rows={funnel.rows}
|
|
173
|
-
labels={offerLabels}
|
|
174
|
-
/>
|
|
175
|
-
))}
|
|
176
|
-
</Container>
|
|
177
|
-
</main>
|
|
178
|
-
);
|
|
179
|
-
}
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* /blog catch-all route. With `content/blog/` empty, the index renders the
|
|
3
|
-
* BLOG_EMPTY_SENTINEL placeholder (testable contract).
|
|
4
|
-
*
|
|
5
|
-
* Q5 deferred post-MVP: no `/docs` companion ships in Phase 0 — only the
|
|
6
|
-
* `docsPageJsonLd` builder is exported from `@working-theory/fumadocs-schema`.
|
|
7
|
-
*/
|
|
8
|
-
import { blogListJsonLd, blogPostingJsonLd } from "@working-theory/fumadocs-schema";
|
|
9
|
-
import { JsonLdScript, emptyStateVisible } from "@working-theory/page-renderer";
|
|
10
|
-
import { BlogList, BlogPost } from "@working-theory/ui";
|
|
11
|
-
import type { Metadata } from "next";
|
|
12
|
-
import { notFound } from "next/navigation";
|
|
13
|
-
import { getTranslations } from "next-intl/server";
|
|
14
|
-
import type { ComponentType, ReactElement } from "react";
|
|
15
|
-
|
|
16
|
-
import { toBlogListData } from "../../../../lib/blog-list-data";
|
|
17
|
-
import { toBlogPostData } from "../../../../lib/blog-post-data";
|
|
18
|
-
import { getOgImageUrl } from "../../../../lib/og";
|
|
19
|
-
import { blogSource as defaultBlogSource } from "../../../../lib/source";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Sentinel string the empty-state placeholder must contain. Tests assert
|
|
23
|
-
* on this exact value to detect the empty-blog branch without coupling to
|
|
24
|
-
* copy. Exported so the integration test can import it directly.
|
|
25
|
-
*/
|
|
26
|
-
export const BLOG_EMPTY_SENTINEL = "ailk-blog-empty-placeholder";
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Frontmatter contract — must match `source.config.ts` `defineDocs.docs.schema`.
|
|
30
|
-
*/
|
|
31
|
-
interface BlogFrontmatter {
|
|
32
|
-
title: string;
|
|
33
|
-
description: string;
|
|
34
|
-
date?: string;
|
|
35
|
-
updated?: string;
|
|
36
|
-
image?: string;
|
|
37
|
-
author?: { name: string; avatar?: string };
|
|
38
|
-
tags?: string[];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Post `page.data`: frontmatter plus the Fumadocs-generated `body` (compiled
|
|
43
|
-
* MDX component) and `toc` (table of contents). The body renders through the
|
|
44
|
-
* `BlogPost` section's `children` slot (issue #2714).
|
|
45
|
-
*/
|
|
46
|
-
interface BlogPostPageData extends BlogFrontmatter {
|
|
47
|
-
body?: ComponentType;
|
|
48
|
-
toc?: Array<{ title: unknown; url: string; depth: number }>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Minimal blog-source interface covering the subset consumed by this route.
|
|
53
|
-
* Tests inject a stub without mocking lib/source at the module level.
|
|
54
|
-
*/
|
|
55
|
-
interface BlogSource {
|
|
56
|
-
getPages: () => Array<{ url: string; data: unknown }>;
|
|
57
|
-
getPage: (slug: string[]) => { data: unknown } | undefined;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
interface BlogRouteProps {
|
|
61
|
-
params: Promise<{ locale: string; slug?: string[] }>;
|
|
62
|
-
/**
|
|
63
|
-
* Blog source override for testing. Defaults to the real Fumadocs blogSource.
|
|
64
|
-
* Inject a stub to avoid mocking lib/source at the module level.
|
|
65
|
-
*/
|
|
66
|
-
blogSource?: BlogSource;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export async function generateMetadata(
|
|
70
|
-
props: BlogRouteProps,
|
|
71
|
-
): Promise<Metadata> {
|
|
72
|
-
const { slug } = await props.params;
|
|
73
|
-
const source = props.blogSource ?? defaultBlogSource;
|
|
74
|
-
|
|
75
|
-
if (slug && slug.length > 0) {
|
|
76
|
-
const page = source.getPage(slug);
|
|
77
|
-
if (page) {
|
|
78
|
-
const data = page.data as unknown as BlogFrontmatter;
|
|
79
|
-
const ogImageUrl = getOgImageUrl({
|
|
80
|
-
title: data.title,
|
|
81
|
-
schemaType: "BlogPosting",
|
|
82
|
-
});
|
|
83
|
-
return {
|
|
84
|
-
openGraph: { images: [ogImageUrl] },
|
|
85
|
-
twitter: { images: [ogImageUrl] },
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const ogImageUrl = getOgImageUrl({ title: "Blog", schemaType: "BlogPosting" });
|
|
91
|
-
return {
|
|
92
|
-
openGraph: { images: [ogImageUrl] },
|
|
93
|
-
twitter: { images: [ogImageUrl] },
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export default async function BlogRoute(
|
|
98
|
-
props: BlogRouteProps,
|
|
99
|
-
): Promise<ReactElement> {
|
|
100
|
-
const { locale, slug } = await props.params;
|
|
101
|
-
const t = await getTranslations("blog");
|
|
102
|
-
const source = props.blogSource ?? defaultBlogSource;
|
|
103
|
-
|
|
104
|
-
if (slug && slug.length > 0) {
|
|
105
|
-
const page = source.getPage(slug);
|
|
106
|
-
if (!page) notFound();
|
|
107
|
-
|
|
108
|
-
// Fumadocs' `PageData` is opaque; the frontmatter contract is enforced
|
|
109
|
-
// by the `defineDocs` schema in source.config.ts at build time.
|
|
110
|
-
const data = page.data as unknown as BlogPostPageData;
|
|
111
|
-
const jsonLd = blogPostingJsonLd({
|
|
112
|
-
slug: slug.join("/"),
|
|
113
|
-
title: data.title,
|
|
114
|
-
description: data.description,
|
|
115
|
-
locale,
|
|
116
|
-
...(data.date !== undefined && { publishedAt: data.date }),
|
|
117
|
-
...(data.updated !== undefined && { updatedAt: data.updated }),
|
|
118
|
-
...(data.image !== undefined && { image: data.image }),
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// The compiled MDX body renders through the section's `children` slot so it
|
|
122
|
-
// keeps full formatting; the section maps the rest of the post (#2714).
|
|
123
|
-
const MDXBody = data.body;
|
|
124
|
-
|
|
125
|
-
return (
|
|
126
|
-
<>
|
|
127
|
-
<JsonLdScript data={jsonLd} />
|
|
128
|
-
<main>
|
|
129
|
-
<BlogPost sectionName="blog-post" data={toBlogPostData(data)}>
|
|
130
|
-
{MDXBody ? <MDXBody /> : null}
|
|
131
|
-
</BlogPost>
|
|
132
|
-
</main>
|
|
133
|
-
</>
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const pages = source.getPages();
|
|
138
|
-
|
|
139
|
-
const indexJsonLd = blogListJsonLd({
|
|
140
|
-
slug: "blog",
|
|
141
|
-
title: "Blog",
|
|
142
|
-
description: "Writing on AEO and AX-first software.",
|
|
143
|
-
locale,
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
if (pages.length === 0) {
|
|
147
|
-
// #4154: uniform with every other contentless surface — the empty-blog
|
|
148
|
-
// placeholder is an authoring affordance, so it renders outside production
|
|
149
|
-
// and 404s inside it rather than serving a stock page to a visitor.
|
|
150
|
-
if (!emptyStateVisible()) notFound();
|
|
151
|
-
|
|
152
|
-
return (
|
|
153
|
-
<>
|
|
154
|
-
<JsonLdScript data={indexJsonLd} />
|
|
155
|
-
<main>
|
|
156
|
-
<h1>{t("heading")}</h1>
|
|
157
|
-
<p
|
|
158
|
-
data-testid="blog-empty-placeholder"
|
|
159
|
-
data-sentinel={BLOG_EMPTY_SENTINEL}
|
|
160
|
-
>
|
|
161
|
-
{t("emptyState")} <code>{"content/blog/"}</code>{" "}
|
|
162
|
-
{t("emptyStateTrailing")}
|
|
163
|
-
</p>
|
|
164
|
-
</main>
|
|
165
|
-
</>
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return (
|
|
170
|
-
<>
|
|
171
|
-
<JsonLdScript data={indexJsonLd} />
|
|
172
|
-
<main>
|
|
173
|
-
<h1>{t("heading")}</h1>
|
|
174
|
-
<BlogList sectionName="blog-list" data={toBlogListData(pages)} />
|
|
175
|
-
</main>
|
|
176
|
-
</>
|
|
177
|
-
);
|
|
178
|
-
}
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for purchaseAction Server Action.
|
|
3
|
-
*
|
|
4
|
-
* Acceptance criteria verified here:
|
|
5
|
-
* - purchaseAction(validInput) returns { url: string } on success
|
|
6
|
-
* - purchaseAction(invalidInput) returns Zod validation errors
|
|
7
|
-
* - purchaseAction calls @working-theory/api-client checkout helper
|
|
8
|
-
* - Parity shape: purchaseAction returns the same response shape as ailkPurchase
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { purchaseAction } from "../purchase.actions.js";
|
|
12
|
-
|
|
13
|
-
// Mock @working-theory/api-client
|
|
14
|
-
jest.mock("@working-theory/api-client", () => ({
|
|
15
|
-
checkout: {
|
|
16
|
-
createSession: jest.fn(),
|
|
17
|
-
},
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
|
-
import { checkout } from "@working-theory/api-client";
|
|
21
|
-
const mockCreateSession = checkout.createSession as jest.Mock;
|
|
22
|
-
|
|
23
|
-
afterEach(() => {
|
|
24
|
-
jest.clearAllMocks();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe("purchaseAction — valid input", () => {
|
|
28
|
-
it("returns { url } on success", async () => {
|
|
29
|
-
mockCreateSession.mockResolvedValueOnce({
|
|
30
|
-
ok: true,
|
|
31
|
-
body: {
|
|
32
|
-
sessionId: "cs_test_abc123",
|
|
33
|
-
url: "https://checkout.stripe.com/pay/cs_test_abc123",
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const result = await purchaseAction({
|
|
38
|
-
priceId: "price_test_123",
|
|
39
|
-
quantity: 1,
|
|
40
|
-
successUrl: "https://example.com/success",
|
|
41
|
-
cancelUrl: "https://example.com/cancel",
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
expect(result).toEqual({
|
|
45
|
-
url: "https://checkout.stripe.com/pay/cs_test_abc123",
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("returns { url } when quantity is omitted", async () => {
|
|
50
|
-
mockCreateSession.mockResolvedValueOnce({
|
|
51
|
-
ok: true,
|
|
52
|
-
body: {
|
|
53
|
-
sessionId: "cs_test_xyz",
|
|
54
|
-
url: "https://checkout.stripe.com/pay/cs_test_xyz",
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const result = await purchaseAction({
|
|
59
|
-
priceId: "price_test_abc",
|
|
60
|
-
successUrl: "https://example.com/success",
|
|
61
|
-
cancelUrl: "https://example.com/cancel",
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
expect(result).toEqual({
|
|
65
|
-
url: "https://checkout.stripe.com/pay/cs_test_xyz",
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("calls checkout.createSession with mapped payload", async () => {
|
|
70
|
-
mockCreateSession.mockResolvedValueOnce({
|
|
71
|
-
ok: true,
|
|
72
|
-
body: {
|
|
73
|
-
sessionId: "cs_test_def",
|
|
74
|
-
url: "https://checkout.stripe.com/pay/cs_test_def",
|
|
75
|
-
},
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
await purchaseAction({
|
|
79
|
-
priceId: "price_plan_pro",
|
|
80
|
-
quantity: 2,
|
|
81
|
-
successUrl: "https://example.com/success",
|
|
82
|
-
cancelUrl: "https://example.com/cancel",
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
expect(mockCreateSession).toHaveBeenCalledWith({
|
|
86
|
-
productCode: "price_plan_pro",
|
|
87
|
-
quantity: 2,
|
|
88
|
-
successUrl: "https://example.com/success",
|
|
89
|
-
cancelUrl: "https://example.com/cancel",
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe("purchaseAction — invalid input (Zod errors)", () => {
|
|
95
|
-
it("returns validation errors when priceId is missing", async () => {
|
|
96
|
-
const result = await purchaseAction({
|
|
97
|
-
successUrl: "https://example.com/success",
|
|
98
|
-
cancelUrl: "https://example.com/cancel",
|
|
99
|
-
} as never);
|
|
100
|
-
|
|
101
|
-
expect(result).toHaveProperty("errors");
|
|
102
|
-
expect(mockCreateSession).not.toHaveBeenCalled();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("returns validation errors when priceId is empty", async () => {
|
|
106
|
-
const result = await purchaseAction({
|
|
107
|
-
priceId: "",
|
|
108
|
-
successUrl: "https://example.com/success",
|
|
109
|
-
cancelUrl: "https://example.com/cancel",
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
expect(result).toHaveProperty("errors");
|
|
113
|
-
expect(mockCreateSession).not.toHaveBeenCalled();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("returns validation errors when priceId contains invalid characters", async () => {
|
|
117
|
-
// priceId must be min length 1; test with a non-string
|
|
118
|
-
const result = await purchaseAction({
|
|
119
|
-
// eslint-disable-next-line no-restricted-syntax -- Phase 2/3 follow-up: pre-existing test-hygiene violation
|
|
120
|
-
priceId: 12345 as unknown as string,
|
|
121
|
-
successUrl: "https://example.com/success",
|
|
122
|
-
cancelUrl: "https://example.com/cancel",
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
expect(result).toHaveProperty("errors");
|
|
126
|
-
expect(mockCreateSession).not.toHaveBeenCalled();
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("returns validation errors when quantity is not a positive integer", async () => {
|
|
130
|
-
const result = await purchaseAction({
|
|
131
|
-
priceId: "price_test",
|
|
132
|
-
quantity: 0,
|
|
133
|
-
successUrl: "https://example.com/success",
|
|
134
|
-
cancelUrl: "https://example.com/cancel",
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
expect(result).toHaveProperty("errors");
|
|
138
|
-
expect(mockCreateSession).not.toHaveBeenCalled();
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe("purchaseAction — parity shape with ailkPurchase", () => {
|
|
143
|
-
it("success shape matches ailkPurchase success shape: { url: string }", async () => {
|
|
144
|
-
mockCreateSession.mockResolvedValueOnce({
|
|
145
|
-
ok: true,
|
|
146
|
-
body: {
|
|
147
|
-
sessionId: "cs_test_parity",
|
|
148
|
-
url: "https://checkout.stripe.com/pay/cs_test_parity",
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
const result = await purchaseAction({
|
|
153
|
-
priceId: "price_parity",
|
|
154
|
-
successUrl: "https://example.com/success",
|
|
155
|
-
cancelUrl: "https://example.com/cancel",
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
expect(typeof (result as { url: string }).url).toBe("string");
|
|
159
|
-
});
|
|
160
|
-
});
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
// Per-segment error boundary for the /dev/purchase (checkout) route.
|
|
3
|
-
// Does not make any claim about whether payment was processed.
|
|
4
|
-
import type { ReactElement } from "react";
|
|
5
|
-
|
|
6
|
-
import { ErrorFallback } from "../../../error-fallback";
|
|
7
|
-
|
|
8
|
-
interface ErrorProps {
|
|
9
|
-
error: Error & { digest?: string };
|
|
10
|
-
reset: () => void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export default function PurchaseError({
|
|
14
|
-
error,
|
|
15
|
-
reset,
|
|
16
|
-
}: ErrorProps): ReactElement {
|
|
17
|
-
return (
|
|
18
|
-
<ErrorFallback
|
|
19
|
-
error={error}
|
|
20
|
-
reset={reset}
|
|
21
|
-
heading="Checkout couldn't complete"
|
|
22
|
-
body="Something interrupted your purchase attempt. Please try again to continue."
|
|
23
|
-
/>
|
|
24
|
-
);
|
|
25
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-restricted-syntax -- i18n-allow: dev-only route, not in customer navigation (ADR 0019) */
|
|
2
|
-
"use client";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* /dev/purchase — demo route for the purchaseAction Server Action.
|
|
6
|
-
*
|
|
7
|
-
* SD-3d: /dev/-prefixed routes are not in customer-visible navigation.
|
|
8
|
-
* This page exists to exercise the Server Action / MCP tool parity pair
|
|
9
|
-
* in a browser context during Phase 3 development.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { useState } from "react";
|
|
13
|
-
|
|
14
|
-
import { purchaseAction } from "./purchase.actions";
|
|
15
|
-
import type { PurchaseActionResult } from "./purchase.actions";
|
|
16
|
-
|
|
17
|
-
export default function DevPurchasePage() {
|
|
18
|
-
const [priceId, setPriceId] = useState("price_test_123");
|
|
19
|
-
const [result, setResult] = useState<PurchaseActionResult | null>(null);
|
|
20
|
-
const [pending, setPending] = useState(false);
|
|
21
|
-
|
|
22
|
-
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
23
|
-
e.preventDefault();
|
|
24
|
-
setPending(true);
|
|
25
|
-
setResult(null);
|
|
26
|
-
try {
|
|
27
|
-
const res = await purchaseAction({ priceId });
|
|
28
|
-
setResult(res);
|
|
29
|
-
if ("url" in res) {
|
|
30
|
-
window.location.href = res.url;
|
|
31
|
-
}
|
|
32
|
-
} finally {
|
|
33
|
-
setPending(false);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<main style={{ padding: "2rem", maxWidth: 480 }}>
|
|
39
|
-
<h1>Dev: Purchase (Server Action smoke test)</h1>
|
|
40
|
-
<p style={{ fontSize: "0.875rem", color: "#666" }}>
|
|
41
|
-
/dev/ — not in customer navigation
|
|
42
|
-
</p>
|
|
43
|
-
|
|
44
|
-
<form
|
|
45
|
-
onSubmit={(e) => void handleSubmit(e)}
|
|
46
|
-
style={{
|
|
47
|
-
display: "flex",
|
|
48
|
-
flexDirection: "column",
|
|
49
|
-
gap: "1rem",
|
|
50
|
-
marginTop: "1.5rem",
|
|
51
|
-
}}
|
|
52
|
-
>
|
|
53
|
-
<label>
|
|
54
|
-
<span
|
|
55
|
-
style={{
|
|
56
|
-
display: "block",
|
|
57
|
-
marginBottom: "0.25rem",
|
|
58
|
-
fontWeight: 500,
|
|
59
|
-
}}
|
|
60
|
-
>
|
|
61
|
-
Price ID
|
|
62
|
-
</span>
|
|
63
|
-
<input
|
|
64
|
-
type="text"
|
|
65
|
-
value={priceId}
|
|
66
|
-
onChange={(e) => setPriceId(e.target.value)}
|
|
67
|
-
placeholder="price_test_..."
|
|
68
|
-
required
|
|
69
|
-
style={{
|
|
70
|
-
width: "100%",
|
|
71
|
-
padding: "0.5rem",
|
|
72
|
-
border: "1px solid #ccc",
|
|
73
|
-
borderRadius: 4,
|
|
74
|
-
}}
|
|
75
|
-
/>
|
|
76
|
-
</label>
|
|
77
|
-
|
|
78
|
-
<button
|
|
79
|
-
type="submit"
|
|
80
|
-
disabled={pending}
|
|
81
|
-
style={{
|
|
82
|
-
padding: "0.5rem 1rem",
|
|
83
|
-
background: "#0070f3",
|
|
84
|
-
color: "#fff",
|
|
85
|
-
border: "none",
|
|
86
|
-
borderRadius: 4,
|
|
87
|
-
cursor: pending ? "not-allowed" : "pointer",
|
|
88
|
-
}}
|
|
89
|
-
>
|
|
90
|
-
{pending ? "Creating session…" : "Start checkout"}
|
|
91
|
-
</button>
|
|
92
|
-
</form>
|
|
93
|
-
|
|
94
|
-
{result && (
|
|
95
|
-
<div style={{ marginTop: "1.5rem" }}>
|
|
96
|
-
{"errors" in result ? (
|
|
97
|
-
<div>
|
|
98
|
-
<strong>Validation errors:</strong>
|
|
99
|
-
<ul>
|
|
100
|
-
{result.errors.map((e, i) => (
|
|
101
|
-
<li key={i}>
|
|
102
|
-
{e.path.join(".") || "input"}: {e.message}
|
|
103
|
-
</li>
|
|
104
|
-
))}
|
|
105
|
-
</ul>
|
|
106
|
-
</div>
|
|
107
|
-
) : (
|
|
108
|
-
<div>
|
|
109
|
-
<strong>Checkout URL:</strong>{" "}
|
|
110
|
-
<a href={result.url}>{result.url}</a>
|
|
111
|
-
</div>
|
|
112
|
-
)}
|
|
113
|
-
</div>
|
|
114
|
-
)}
|
|
115
|
-
</main>
|
|
116
|
-
);
|
|
117
|
-
}
|