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
package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx
DELETED
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the `/[locale]/flows/[slug]/results/[payload]` cold-load route
|
|
3
|
-
* (#4770).
|
|
4
|
-
*
|
|
5
|
-
* AC3 — a payload encoding a known answer set renders exactly the offer
|
|
6
|
-
* names and matched reasons `recommend()` returns for those same
|
|
7
|
-
* answers — asserted against the SAME fixture (`FIXTURE_CONFIG` +
|
|
8
|
-
* `KNOWN_ANSWERS`) `recommend()` itself would score.
|
|
9
|
-
* AC4 — four cases `redirect()` to `/[locale]/flows/[slug]` and render no
|
|
10
|
-
* results markup: invalid payload, stale-version payload, a
|
|
11
|
-
* flowSlug-mismatched payload, and a non-recommending flow.
|
|
12
|
-
*
|
|
13
|
-
* `next/navigation`'s `redirect` is mocked to throw (mirrors the sibling
|
|
14
|
-
* `thanks/[slug]` route's `notFound` test pattern) so a redirect is
|
|
15
|
-
* observable via `.rejects.toThrow` without a real Next.js router. `next-intl`
|
|
16
|
-
* is mocked to an identity translator — `ResultsClient`'s only string comes
|
|
17
|
-
* from the `flow` namespace's `whyThis` key, which this test does not assert
|
|
18
|
-
* on by literal text.
|
|
19
|
-
*/
|
|
20
|
-
import { render, screen } from "@testing-library/react";
|
|
21
|
-
import userEvent from "@testing-library/user-event";
|
|
22
|
-
import type { FlowConfig } from "@working-theory/validation";
|
|
23
|
-
import {
|
|
24
|
-
encodePermalink,
|
|
25
|
-
PERMALINK_VERSION,
|
|
26
|
-
recommend,
|
|
27
|
-
resolveLocalizedString,
|
|
28
|
-
} from "@working-theory/validation";
|
|
29
|
-
|
|
30
|
-
const mockGet = jest.fn();
|
|
31
|
-
jest.mock("@working-theory/api-client", () => ({
|
|
32
|
-
__esModule: true,
|
|
33
|
-
flows: { get: (...args: unknown[]) => mockGet(...args) },
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
const mockRedirect = jest.fn((url: string) => {
|
|
37
|
-
throw new Error(`NEXT_REDIRECT:${url}`);
|
|
38
|
-
});
|
|
39
|
-
jest.mock("next/navigation", () => ({
|
|
40
|
-
__esModule: true,
|
|
41
|
-
redirect: (url: string) => mockRedirect(url),
|
|
42
|
-
}));
|
|
43
|
-
|
|
44
|
-
jest.mock("next-intl", () => ({
|
|
45
|
-
__esModule: true,
|
|
46
|
-
useTranslations: () => (key: string) => key,
|
|
47
|
-
useMessages: () => ({}),
|
|
48
|
-
}));
|
|
49
|
-
|
|
50
|
-
import Page, { generateMetadata } from "../page";
|
|
51
|
-
|
|
52
|
-
const SLUG = "ailk-recommender";
|
|
53
|
-
|
|
54
|
-
const KNOWN_ANSWERS = { team_size: "solo", wants_seo: "yes" };
|
|
55
|
-
|
|
56
|
-
function fixtureConfig(overrides: Partial<FlowConfig> = {}): FlowConfig {
|
|
57
|
-
return {
|
|
58
|
-
slug: SLUG,
|
|
59
|
-
defaultLocale: "en",
|
|
60
|
-
mode: "recommending",
|
|
61
|
-
steps: [
|
|
62
|
-
{
|
|
63
|
-
key: "team",
|
|
64
|
-
type: "segmentation",
|
|
65
|
-
copy: { title: { en: "Tell us about your team" } },
|
|
66
|
-
questions: [
|
|
67
|
-
{
|
|
68
|
-
name: "team_size",
|
|
69
|
-
options: [
|
|
70
|
-
{ value: "solo", label: { en: "Solo" } },
|
|
71
|
-
{ value: "agency", label: { en: "Agency" } },
|
|
72
|
-
],
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
name: "wants_seo",
|
|
76
|
-
options: [
|
|
77
|
-
{ value: "yes", label: { en: "Yes" } },
|
|
78
|
-
{ value: "no", label: { en: "No" } },
|
|
79
|
-
],
|
|
80
|
-
},
|
|
81
|
-
],
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
offers: [
|
|
85
|
-
{
|
|
86
|
-
key: "free_oss",
|
|
87
|
-
copy: {
|
|
88
|
-
name: { en: "Free" },
|
|
89
|
-
description: { en: "Self-serve, open source." },
|
|
90
|
-
},
|
|
91
|
-
price: { kind: "label", label: { en: "Free" } },
|
|
92
|
-
rules: [
|
|
93
|
-
{
|
|
94
|
-
when: { answer: "team_size", eq: "solo" },
|
|
95
|
-
reason: { en: "You're a solo builder." },
|
|
96
|
-
},
|
|
97
|
-
],
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
key: "seo_addon",
|
|
101
|
-
copy: {
|
|
102
|
-
name: { en: "SEO Add-on" },
|
|
103
|
-
description: { en: "AEO + SEO tuning." },
|
|
104
|
-
},
|
|
105
|
-
price: { kind: "label", label: { en: "Add-on" } },
|
|
106
|
-
rules: [
|
|
107
|
-
{
|
|
108
|
-
when: { answer: "wants_seo", eq: "yes" },
|
|
109
|
-
reason: { en: "You want more organic reach." },
|
|
110
|
-
},
|
|
111
|
-
],
|
|
112
|
-
},
|
|
113
|
-
],
|
|
114
|
-
recommendation: {
|
|
115
|
-
key: "recommendation",
|
|
116
|
-
copy: { title: { en: "Your recommendation" } },
|
|
117
|
-
},
|
|
118
|
-
...overrides,
|
|
119
|
-
} as FlowConfig;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
beforeEach(() => {
|
|
123
|
-
mockGet.mockReset();
|
|
124
|
-
mockRedirect.mockClear();
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
async function expectRedirectTo(
|
|
128
|
-
payload: string,
|
|
129
|
-
expectedUrl: string,
|
|
130
|
-
): Promise<void> {
|
|
131
|
-
await expect(
|
|
132
|
-
Page({
|
|
133
|
-
params: Promise.resolve({ locale: "en", slug: SLUG, payload }),
|
|
134
|
-
}),
|
|
135
|
-
).rejects.toThrow(`NEXT_REDIRECT:${expectedUrl}`);
|
|
136
|
-
expect(mockRedirect).toHaveBeenCalledWith(expectedUrl);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
describe("AC4 — invalid / stale-version / mismatched / non-recommending all redirect to step 1", () => {
|
|
140
|
-
it("redirects on an invalid payload", async () => {
|
|
141
|
-
await expectRedirectTo("not-a-valid-payload!!!", `/en/flows/${SLUG}`);
|
|
142
|
-
// Never fetched the flow config for an undecodable payload.
|
|
143
|
-
expect(mockGet).not.toHaveBeenCalled();
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("redirects on a stale-version payload", async () => {
|
|
147
|
-
const stalePayload = encodePermalink({
|
|
148
|
-
version: PERMALINK_VERSION - 1,
|
|
149
|
-
flowSlug: SLUG,
|
|
150
|
-
answers: KNOWN_ANSWERS,
|
|
151
|
-
});
|
|
152
|
-
await expectRedirectTo(stalePayload, `/en/flows/${SLUG}`);
|
|
153
|
-
expect(mockGet).not.toHaveBeenCalled();
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("redirects when the payload's own flowSlug differs from the route's [slug]", async () => {
|
|
157
|
-
const mismatchedPayload = encodePermalink({
|
|
158
|
-
version: PERMALINK_VERSION,
|
|
159
|
-
flowSlug: "a-different-flow",
|
|
160
|
-
answers: KNOWN_ANSWERS,
|
|
161
|
-
});
|
|
162
|
-
await expectRedirectTo(mismatchedPayload, `/en/flows/${SLUG}`);
|
|
163
|
-
expect(mockGet).not.toHaveBeenCalled();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("redirects when the served flow is not a recommending flow (no offers)", async () => {
|
|
167
|
-
const payload = encodePermalink({
|
|
168
|
-
version: PERMALINK_VERSION,
|
|
169
|
-
flowSlug: SLUG,
|
|
170
|
-
answers: KNOWN_ANSWERS,
|
|
171
|
-
});
|
|
172
|
-
const {
|
|
173
|
-
offers: _offers,
|
|
174
|
-
mode: _mode,
|
|
175
|
-
...nonRecommending
|
|
176
|
-
} = fixtureConfig();
|
|
177
|
-
mockGet.mockResolvedValueOnce({ ok: true, body: nonRecommending });
|
|
178
|
-
|
|
179
|
-
await expectRedirectTo(payload, `/en/flows/${SLUG}`);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("redirects when the served flow declares mode !== 'recommending'", async () => {
|
|
183
|
-
const payload = encodePermalink({
|
|
184
|
-
version: PERMALINK_VERSION,
|
|
185
|
-
flowSlug: SLUG,
|
|
186
|
-
answers: KNOWN_ANSWERS,
|
|
187
|
-
});
|
|
188
|
-
mockGet.mockResolvedValueOnce({
|
|
189
|
-
ok: true,
|
|
190
|
-
body: fixtureConfig({ mode: "qualifying" }),
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
await expectRedirectTo(payload, `/en/flows/${SLUG}`);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("no results markup renders on any redirect path (renders nothing before throwing)", async () => {
|
|
197
|
-
await expect(
|
|
198
|
-
Page({
|
|
199
|
-
params: Promise.resolve({
|
|
200
|
-
locale: "en",
|
|
201
|
-
slug: SLUG,
|
|
202
|
-
payload: "garbage",
|
|
203
|
-
}),
|
|
204
|
-
}),
|
|
205
|
-
).rejects.toThrow();
|
|
206
|
-
expect(screen.queryByTestId("flow-results")).not.toBeInTheDocument();
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
describe("AC3 — a valid payload against a recommending flow renders exactly recommend()'s fits", () => {
|
|
211
|
-
it("renders exactly the offer names and matched reasons recommend() itself returns for the same answers", async () => {
|
|
212
|
-
const user = userEvent.setup();
|
|
213
|
-
const config = fixtureConfig();
|
|
214
|
-
const payload = encodePermalink({
|
|
215
|
-
version: PERMALINK_VERSION,
|
|
216
|
-
flowSlug: SLUG,
|
|
217
|
-
answers: KNOWN_ANSWERS,
|
|
218
|
-
});
|
|
219
|
-
mockGet.mockResolvedValueOnce({ ok: true, body: config });
|
|
220
|
-
|
|
221
|
-
// The SAME fixture (`config` + `KNOWN_ANSWERS`) drives BOTH the mocked
|
|
222
|
-
// server response AND this expectation — `recommend()` is the one
|
|
223
|
-
// source of truth for "which fits, in what order, with which reason,"
|
|
224
|
-
// never a second hand-typed list that could silently drift from it.
|
|
225
|
-
const expected = recommend(
|
|
226
|
-
{
|
|
227
|
-
key: SLUG,
|
|
228
|
-
offers: config.offers,
|
|
229
|
-
pairings: config.pairings,
|
|
230
|
-
rateTable: config.rateTable,
|
|
231
|
-
scoring: config.scoring,
|
|
232
|
-
},
|
|
233
|
-
KNOWN_ANSWERS,
|
|
234
|
-
);
|
|
235
|
-
expect(expected.fits.length).toBeGreaterThan(0);
|
|
236
|
-
|
|
237
|
-
const element = await Page({
|
|
238
|
-
params: Promise.resolve({ locale: "en", slug: SLUG, payload }),
|
|
239
|
-
});
|
|
240
|
-
render(element);
|
|
241
|
-
|
|
242
|
-
expect(mockRedirect).not.toHaveBeenCalled();
|
|
243
|
-
expect(screen.getByTestId("flow-results")).toBeInTheDocument();
|
|
244
|
-
expect(
|
|
245
|
-
screen.getByRole("heading", {
|
|
246
|
-
name: resolveLocalizedString(
|
|
247
|
-
config.recommendation!.copy.title,
|
|
248
|
-
"en",
|
|
249
|
-
config.defaultLocale,
|
|
250
|
-
),
|
|
251
|
-
}),
|
|
252
|
-
).toBeInTheDocument();
|
|
253
|
-
|
|
254
|
-
for (const fit of expected.fits) {
|
|
255
|
-
const offer = config.offers!.find((o) => o.key === fit.offerKey)!;
|
|
256
|
-
const name = resolveLocalizedString(
|
|
257
|
-
offer.copy.name,
|
|
258
|
-
"en",
|
|
259
|
-
config.defaultLocale,
|
|
260
|
-
);
|
|
261
|
-
// #4842 FR1 — the top fit's name is ALSO the safe-default side panel's
|
|
262
|
-
// `offerName`, so the FIRST fit renders exactly twice (panel + card)
|
|
263
|
-
// and every other fit exactly once.
|
|
264
|
-
expect(screen.getAllByText(name)).toHaveLength(
|
|
265
|
-
fit.offerKey === expected.fits[0]!.offerKey ? 2 : 1,
|
|
266
|
-
);
|
|
267
|
-
if (offer.copy.description) {
|
|
268
|
-
expect(
|
|
269
|
-
screen.getByText(
|
|
270
|
-
resolveLocalizedString(
|
|
271
|
-
offer.copy.description,
|
|
272
|
-
"en",
|
|
273
|
-
config.defaultLocale,
|
|
274
|
-
),
|
|
275
|
-
),
|
|
276
|
-
).toBeInTheDocument();
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// The matched reason renders inside the "Why this?" Popover
|
|
280
|
-
// disclosure — reveal it (mirrors packages/ui/src/blocks/__tests__/
|
|
281
|
-
// RecommendationCard.test.tsx's own click-to-reveal pattern).
|
|
282
|
-
await user.click(
|
|
283
|
-
screen.getByTestId(`recommendation-card-${fit.offerKey}-why-trigger`),
|
|
284
|
-
);
|
|
285
|
-
expect(
|
|
286
|
-
await screen.findByText(
|
|
287
|
-
resolveLocalizedString(fit.reason, "en", config.defaultLocale),
|
|
288
|
-
),
|
|
289
|
-
).toBeInTheDocument();
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
describe("generateMetadata", () => {
|
|
295
|
-
it("resolves the page title from the compiled recommendation marker", async () => {
|
|
296
|
-
const payload = encodePermalink({
|
|
297
|
-
version: PERMALINK_VERSION,
|
|
298
|
-
flowSlug: SLUG,
|
|
299
|
-
answers: KNOWN_ANSWERS,
|
|
300
|
-
});
|
|
301
|
-
mockGet.mockResolvedValueOnce({ ok: true, body: fixtureConfig() });
|
|
302
|
-
|
|
303
|
-
const metadata = await generateMetadata({
|
|
304
|
-
params: Promise.resolve({ locale: "en", slug: SLUG, payload }),
|
|
305
|
-
});
|
|
306
|
-
expect(metadata.title).toBe("Your recommendation");
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it("returns an empty metadata object for an invalid payload", async () => {
|
|
310
|
-
const metadata = await generateMetadata({
|
|
311
|
-
params: Promise.resolve({
|
|
312
|
-
locale: "en",
|
|
313
|
-
slug: SLUG,
|
|
314
|
-
payload: "garbage",
|
|
315
|
-
}),
|
|
316
|
-
});
|
|
317
|
-
expect(metadata).toEqual({});
|
|
318
|
-
expect(mockGet).not.toHaveBeenCalled();
|
|
319
|
-
});
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
// ─── #4842 AC-5 — the `?checkout=` return states (additive; the cases above
|
|
323
|
-
// are the regression floor and stay as they were) ───────────────────────────
|
|
324
|
-
|
|
325
|
-
describe("AC-5 (#4842) — ?checkout= return states render over the same recommendation", () => {
|
|
326
|
-
function validPayload(): string {
|
|
327
|
-
return encodePermalink({
|
|
328
|
-
version: PERMALINK_VERSION,
|
|
329
|
-
flowSlug: SLUG,
|
|
330
|
-
answers: KNOWN_ANSWERS,
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
async function renderWithSearch(
|
|
335
|
-
checkout: string | undefined,
|
|
336
|
-
): Promise<ReturnType<typeof recommend>> {
|
|
337
|
-
const config = fixtureConfig();
|
|
338
|
-
mockGet.mockResolvedValueOnce({ ok: true, body: config });
|
|
339
|
-
const expected = recommend(
|
|
340
|
-
{
|
|
341
|
-
key: SLUG,
|
|
342
|
-
offers: config.offers,
|
|
343
|
-
pairings: config.pairings,
|
|
344
|
-
rateTable: config.rateTable,
|
|
345
|
-
scoring: config.scoring,
|
|
346
|
-
},
|
|
347
|
-
KNOWN_ANSWERS,
|
|
348
|
-
);
|
|
349
|
-
const element = await Page({
|
|
350
|
-
params: Promise.resolve({ locale: "en", slug: SLUG, payload: validPayload() }),
|
|
351
|
-
searchParams: Promise.resolve(checkout === undefined ? {} : { checkout }),
|
|
352
|
-
});
|
|
353
|
-
render(element);
|
|
354
|
-
expect(mockRedirect).not.toHaveBeenCalled();
|
|
355
|
-
return expected;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function expectIdenticalFits(expected: ReturnType<typeof recommend>): void {
|
|
359
|
-
expect(expected.fits.length).toBeGreaterThan(0);
|
|
360
|
-
for (const fit of expected.fits) {
|
|
361
|
-
expect(screen.getByTestId(`recommendation-card-${fit.offerKey}`)).toBeInTheDocument();
|
|
362
|
-
}
|
|
363
|
-
expect(screen.getAllByTestId(/^recommendation-card-[a-z_]+$/)).toHaveLength(
|
|
364
|
-
expected.fits.length,
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
it("?checkout=success renders the success status line AND the identical fit cards", async () => {
|
|
369
|
-
const expected = await renderWithSearch("success");
|
|
370
|
-
expect(screen.getByTestId("flow-results-return-state")).toHaveTextContent("checkoutSuccess");
|
|
371
|
-
expect(screen.getByRole("status")).toBeInTheDocument();
|
|
372
|
-
expectIdenticalFits(expected);
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
it("?checkout=cancelled renders the cancelled status line AND the identical fit cards", async () => {
|
|
376
|
-
const expected = await renderWithSearch("cancelled");
|
|
377
|
-
expect(screen.getByTestId("flow-results-return-state")).toHaveTextContent("checkoutCancelled");
|
|
378
|
-
expectIdenticalFits(expected);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it("an unrecognised value renders no status line; the fits are unchanged", async () => {
|
|
382
|
-
const expected = await renderWithSearch("bogus");
|
|
383
|
-
expect(screen.queryByTestId("flow-results-return-state")).not.toBeInTheDocument();
|
|
384
|
-
expectIdenticalFits(expected);
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
it("a return param never changes which redirect guard fires (invalid payload still redirects to step 1)", async () => {
|
|
388
|
-
await expect(
|
|
389
|
-
Page({
|
|
390
|
-
params: Promise.resolve({ locale: "en", slug: SLUG, payload: "not-a-valid-payload!!!" }),
|
|
391
|
-
searchParams: Promise.resolve({ checkout: "success" }),
|
|
392
|
-
}),
|
|
393
|
-
).rejects.toThrow(`NEXT_REDIRECT:/en/flows/${SLUG}`);
|
|
394
|
-
expect(mockGet).not.toHaveBeenCalled();
|
|
395
|
-
});
|
|
396
|
-
});
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* /[locale]/flows/[slug]/results/[payload] — the cold-load results route
|
|
3
|
-
* (#4770).
|
|
4
|
-
*
|
|
5
|
-
* Server-fetches the flow config exactly as `flows/[slug]/page.tsx` does
|
|
6
|
-
* (`flows.get(slug)`, always dynamically rendered — the config is DB-backed
|
|
7
|
-
* live data).
|
|
8
|
-
* Decodes the versioned, answers-only permalink (`decodePermalink`,
|
|
9
|
-
* `@working-theory/validation`) and `redirect()`s to `/[locale]/flows/[slug]`
|
|
10
|
-
* — step 1, never an error page — on ANY of: an invalid payload (FR6), a
|
|
11
|
-
* stale-version payload (FR6), a payload whose own `flowSlug` does not match
|
|
12
|
-
* this route's `[slug]` (D2/FR6), or a served flow that is not a
|
|
13
|
-
* recommending register (D8 — `mode !== "recommending" || !offers`, checked
|
|
14
|
-
* against the field `recommend()` is about to be handed rather than
|
|
15
|
-
* inferring the compiler's five-keys-travel-together guarantee).
|
|
16
|
-
*
|
|
17
|
-
* On a valid payload against a recommending flow, renders `ResultsClient`,
|
|
18
|
-
* which runs the already-shipped pure `recommend()` in the browser and
|
|
19
|
-
* renders the returned fits through the existing `RecommendationCard`
|
|
20
|
-
* (D10 — a minimal render; Epic #4759 recomposes it against the LOCKED
|
|
21
|
-
* DESIGN.md).
|
|
22
|
-
*
|
|
23
|
-
* `?checkout=success|cancelled` (#4842 FR7/D6) is read only after every
|
|
24
|
-
* guard above and rendered by `ResultsClient` as a one-line status over the
|
|
25
|
-
* identical recommendation.
|
|
26
|
-
*/
|
|
27
|
-
import { flows } from "@working-theory/api-client";
|
|
28
|
-
import {
|
|
29
|
-
decodePermalink,
|
|
30
|
-
resolveLocalizedString,
|
|
31
|
-
} from "@working-theory/validation";
|
|
32
|
-
import type { Metadata } from "next";
|
|
33
|
-
import { redirect } from "next/navigation";
|
|
34
|
-
import type { ReactElement } from "react";
|
|
35
|
-
|
|
36
|
-
import type { ResultsReturnState } from "./ResultsClient";
|
|
37
|
-
import { ResultsClient } from "./ResultsClient";
|
|
38
|
-
|
|
39
|
-
export const dynamic = "force-dynamic";
|
|
40
|
-
|
|
41
|
-
interface Props {
|
|
42
|
-
params: Promise<{ locale: string; slug: string; payload: string }>;
|
|
43
|
-
/** FR7 (#4842) — `?checkout=success|cancelled` after Stripe's hosted page. */
|
|
44
|
-
searchParams?: Promise<Record<string, string | string[] | undefined>>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* D6 (#4842) — the return micro-state, validated against the two literals;
|
|
49
|
-
* anything else (absent, an array, an unknown value) renders no status line.
|
|
50
|
-
*/
|
|
51
|
-
function resolveReturnState(
|
|
52
|
-
value: string | string[] | undefined,
|
|
53
|
-
): ResultsReturnState | undefined {
|
|
54
|
-
return value === "success" || value === "cancelled" ? value : undefined;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
58
|
-
const { locale, slug, payload } = await params;
|
|
59
|
-
|
|
60
|
-
const decoded = decodePermalink(payload);
|
|
61
|
-
if (!decoded.ok || decoded.value.flowSlug !== slug) return {};
|
|
62
|
-
|
|
63
|
-
const res = await flows.get(slug);
|
|
64
|
-
if (!res.ok) return {};
|
|
65
|
-
|
|
66
|
-
const { recommendation } = res.body;
|
|
67
|
-
if (!recommendation) return {};
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
title: resolveLocalizedString(
|
|
71
|
-
recommendation.copy.title,
|
|
72
|
-
locale,
|
|
73
|
-
res.body.defaultLocale,
|
|
74
|
-
),
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export default async function Page({
|
|
79
|
-
params,
|
|
80
|
-
searchParams,
|
|
81
|
-
}: Props): Promise<ReactElement> {
|
|
82
|
-
const { locale, slug, payload } = await params;
|
|
83
|
-
|
|
84
|
-
const decoded = decodePermalink(payload);
|
|
85
|
-
if (!decoded.ok) {
|
|
86
|
-
redirect(`/${locale}/flows/${slug}`);
|
|
87
|
-
}
|
|
88
|
-
if (decoded.value.flowSlug !== slug) {
|
|
89
|
-
redirect(`/${locale}/flows/${slug}`);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const res = await flows.get(slug);
|
|
93
|
-
if (!res.ok) {
|
|
94
|
-
redirect(`/${locale}/flows/${slug}`);
|
|
95
|
-
}
|
|
96
|
-
if (res.body.mode !== "recommending" || res.body.offers === undefined) {
|
|
97
|
-
redirect(`/${locale}/flows/${slug}`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// FR7 — read AFTER every redirect guard above, so a return param never
|
|
101
|
-
// changes which guard fires or where it sends the visitor.
|
|
102
|
-
const returnState = resolveReturnState((await searchParams)?.checkout);
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
<ResultsClient
|
|
106
|
-
config={res.body}
|
|
107
|
-
locale={locale}
|
|
108
|
-
answers={decoded.value.answers}
|
|
109
|
-
slug={slug}
|
|
110
|
-
payload={payload}
|
|
111
|
-
{...(returnState !== undefined ? { returnState } : {})}
|
|
112
|
-
/>
|
|
113
|
-
);
|
|
114
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ListingApplicationClient — the REFERENCE MOUNT for the founder application
|
|
5
|
-
* (#5048).
|
|
6
|
-
*
|
|
7
|
-
* This file is the whole per-site cost of the consumer surface, and it is
|
|
8
|
-
* deliberately this short. Before it, a site had to hand-write three proxy
|
|
9
|
-
* routes, a form that saved on every advance, and a decision about where a
|
|
10
|
-
* bearer token lives. What is left is: bind `projectListings` to the section,
|
|
11
|
-
* and decide what to do with the token when `start` mints one.
|
|
12
|
-
*
|
|
13
|
-
* IT IS NOT A BFF, and a site must not copy it into one (the placement
|
|
14
|
-
* decision's D4). `apps/api` is called DIRECTLY: an agent-mediated caller
|
|
15
|
-
* (#5039) cannot use any site's proxy, so the direct path has to be safe on
|
|
16
|
-
* its own terms regardless — and it is, because `siteId` is resolved
|
|
17
|
-
* server-side from the public site key rather than taken from the body. A
|
|
18
|
-
* proxy would be convenience, not a security boundary.
|
|
19
|
-
*
|
|
20
|
-
* TOKEN CUSTODY (#5048 (a)). The token is caller-held. This mount reads a
|
|
21
|
-
* resume link's `id` and `token` query parameters and passes them straight to
|
|
22
|
-
* the section; when `start` mints a new pair it replaces the URL's query
|
|
23
|
-
* parameters with them, so a founder who reloads or navigates back keeps their
|
|
24
|
-
* draft. That is a deliberate choice with a cost: a token in a URL lands in
|
|
25
|
-
* browser history and in any `Referer` the page sends. It is the accepted cost
|
|
26
|
-
* of a resume link that has to survive being EMAILED and opened in a cold
|
|
27
|
-
* browser (samhenry #316 D6b), which is the constraint that rules out an
|
|
28
|
-
* httpOnly cookie entirely. A site with different constraints replaces this
|
|
29
|
-
* one function; nothing else here changes.
|
|
30
|
-
*
|
|
31
|
-
* NO ANTI-AUTOMATION CONTROLS. No honeypot, no timing check, deliberately —
|
|
32
|
-
* see #316 D6b and #5039.
|
|
33
|
-
*
|
|
34
|
-
* SHIPS NO COPY, AND NO ROUTE. Every string comes from the `config` and
|
|
35
|
-
* `messages` a caller passes in. There is deliberately no `page.tsx` beside
|
|
36
|
-
* this file: a route would need a question set, and a question set is the
|
|
37
|
-
* SITE's content — shipping a placeholder one here would put founder-readable
|
|
38
|
-
* copy in the template, which is exactly what this unit exists to stop. A site
|
|
39
|
-
* adds its own route, imports this wrapper, and passes its own config.
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
import { projectListings } from "@working-theory/api-client";
|
|
43
|
-
import { ListingApplicationFlow } from "@working-theory/ui";
|
|
44
|
-
import type {
|
|
45
|
-
FlowStepperMessages,
|
|
46
|
-
ListingApplicationDraftClient,
|
|
47
|
-
ListingApplicationMessages,
|
|
48
|
-
} from "@working-theory/ui";
|
|
49
|
-
import type { ListingApplicationConfig } from "@working-theory/validation";
|
|
50
|
-
import { useRouter, useSearchParams } from "next/navigation";
|
|
51
|
-
import type { ReactElement, ReactNode } from "react";
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* `projectListings` satisfies the section's structural client contract as-is —
|
|
55
|
-
* `start`, `patchDraft` and `submit` already have the right shapes, and the
|
|
56
|
-
* section defines the interface rather than importing this package so that
|
|
57
|
-
* `packages/ui` stays transport-agnostic (#3957 D2).
|
|
58
|
-
*/
|
|
59
|
-
const client: ListingApplicationDraftClient = projectListings;
|
|
60
|
-
|
|
61
|
-
/** The query parameters an emailed resume link carries. */
|
|
62
|
-
const DRAFT_ID_PARAM = "draft";
|
|
63
|
-
const RESUME_TOKEN_PARAM = "token";
|
|
64
|
-
|
|
65
|
-
interface Props {
|
|
66
|
-
config: ListingApplicationConfig;
|
|
67
|
-
locale: string;
|
|
68
|
-
messages: FlowStepperMessages & ListingApplicationMessages;
|
|
69
|
-
/** Rendered under the live tile — the per-listing privacy notice goes here. */
|
|
70
|
-
tileFooter?: ReactNode;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function ListingApplicationClient({
|
|
74
|
-
config,
|
|
75
|
-
locale,
|
|
76
|
-
messages,
|
|
77
|
-
tileFooter,
|
|
78
|
-
}: Props): ReactElement {
|
|
79
|
-
const router = useRouter();
|
|
80
|
-
const params = useSearchParams();
|
|
81
|
-
|
|
82
|
-
const draftId = params.get(DRAFT_ID_PARAM);
|
|
83
|
-
const resumeToken = params.get(RESUME_TOKEN_PARAM);
|
|
84
|
-
const resume =
|
|
85
|
-
draftId && resumeToken ? { id: draftId, resumeToken } : undefined;
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<ListingApplicationFlow
|
|
89
|
-
config={config}
|
|
90
|
-
locale={locale}
|
|
91
|
-
messages={messages}
|
|
92
|
-
client={client}
|
|
93
|
-
{...(resume ? { resume } : {})}
|
|
94
|
-
{...(tileFooter !== undefined ? { tileFooter } : {})}
|
|
95
|
-
onDraftStarted={({ id, resumeToken: token }) => {
|
|
96
|
-
// Put the pair in the URL so a reload or a back-navigation resumes
|
|
97
|
-
// rather than starting a second draft. `replace`, not `push`: the
|
|
98
|
-
// founder should not have to walk back through one history entry per
|
|
99
|
-
// step. The site's own email send goes here too — the same two values
|
|
100
|
-
// are what its resume link carries.
|
|
101
|
-
const next = new URLSearchParams(params.toString());
|
|
102
|
-
next.set(DRAFT_ID_PARAM, id);
|
|
103
|
-
next.set(RESUME_TOKEN_PARAM, token);
|
|
104
|
-
router.replace(`?${next.toString()}`, { scroll: false });
|
|
105
|
-
}}
|
|
106
|
-
/>
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export default ListingApplicationClient;
|