create-ailk 0.5.0 → 0.6.1
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/README.md +55 -8
- package/component-catalog.json +336 -27
- package/component-catalog.md +25 -12
- package/dist/cli.js +54 -5
- package/dist/copy.d.ts +14 -0
- package/dist/copy.js +38 -1
- package/dist/lib/api-boot-assertion.d.ts +46 -0
- package/dist/lib/api-boot-assertion.js +70 -0
- package/dist/lib/extract-module-bundle.d.ts +20 -1
- package/dist/lib/extract-module-bundle.js +64 -5
- package/dist/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/lib/module-license-gate.d.ts +6 -1
- package/dist/lib/module-license-gate.js +6 -1
- package/dist/lib/pro-callout.js +1 -1
- package/dist/module-architecture.d.ts +1 -0
- package/dist/module-architecture.js +253 -127
- package/dist/modules/commerce/0.6.0.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
- package/dist/modules/workspace/0.6.0.tar.gz +0 -0
- package/dist/programmatic.js +14 -10
- package/dist/surfaces.js +105 -7
- package/dist/template-manifest.d.ts +33 -0
- package/dist/template-manifest.js +99 -16
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +1 -0
- package/package.json +3 -2
- package/site-templates/marketing-site/DESIGN.md +148 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
- package/site-templates/marketing-site/ailk-template.json +1 -1
- package/site-templates/marketing-site-free/DESIGN.md +148 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
- package/site-templates/minimalism-site/DESIGN.md +148 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
- package/site-templates/minimalism-site/ailk-template.json +2 -1
- package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
- package/site-templates/saas-product/DESIGN.md +148 -0
- package/site-templates/simple-service/DESIGN.md +148 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
- package/site-templates/simple-service/ailk-template.json +10 -0
- package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
- package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
- package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/simple-service/provenance.json +4 -0
- package/templates/.env.example +72 -7
- package/templates/CLAUDE.md +2 -3
- package/templates/DESIGN.md +155 -0
- package/templates/README.md +42 -8
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
- package/templates/apps/api/.env.example +13 -0
- package/templates/apps/api/CLAUDE.md +26 -2
- package/templates/apps/api/jest.config.cjs +3 -0
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
- package/templates/apps/api/src/__tests__/server.test.ts +471 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +2 -0
- package/templates/apps/api/src/config/index.ts +62 -0
- package/templates/apps/api/src/config/modules.ts +7 -3
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +238 -2
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
- package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
- package/templates/apps/api/src/lib/content-adapter.ts +57 -31
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
- package/templates/apps/api/src/lib/error-serializer.ts +23 -3
- package/templates/apps/api/src/lib/outbound-http.ts +7 -2
- package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
- package/templates/apps/api/src/lib/site.ts +208 -9
- package/templates/apps/api/src/lib/stripe.ts +66 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +182 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
- package/templates/apps/api/src/openapi/spec.ts +543 -2
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
- package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
- package/templates/apps/api/src/routes/auth/user.ts +61 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
- package/templates/apps/api/src/routes/checkout/index.ts +34 -0
- package/templates/apps/api/src/routes/checkout/session.ts +139 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
- package/templates/apps/api/src/routes/content/get-page.ts +94 -12
- package/templates/apps/api/src/routes/content/index.ts +13 -73
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
- package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
- package/templates/apps/api/src/routes/content/search.ts +129 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
- package/templates/apps/api/src/routes/leads/index.ts +100 -13
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
- package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
- package/templates/apps/api/src/routes/requests/index.ts +54 -0
- package/templates/apps/api/src/routes/webhooks/README.md +265 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
- package/templates/apps/api/src/server.ts +439 -49
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
- package/templates/apps/api/src/services/abandon-email.ts +8 -2
- package/templates/apps/api/src/services/aeo-score.ts +49 -12
- package/templates/apps/api/src/services/audit-log.ts +668 -0
- package/templates/apps/api/src/services/consent.ts +18 -0
- package/templates/apps/api/src/services/default-tenant.ts +70 -31
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
- package/templates/apps/api/src/services/dunning-policy.ts +294 -0
- package/templates/apps/api/src/services/entitlements.ts +80 -0
- package/templates/apps/api/src/services/lead-promotion.ts +13 -14
- package/templates/apps/api/src/services/lead-routing.ts +161 -2
- package/templates/apps/api/src/services/leads-inbox.ts +23 -2
- package/templates/apps/api/src/services/listing-config.ts +27 -9
- package/templates/apps/api/src/services/project-listings.ts +201 -6
- package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
- package/templates/apps/api/src/services/request-confirm.ts +646 -0
- package/templates/apps/api/src/services/revalidation.ts +173 -0
- package/templates/apps/api/src/services/verified-email.ts +71 -0
- package/templates/apps/api/src/vercel-handler.ts +75 -35
- package/templates/apps/mcp/.env.example +17 -0
- package/templates/apps/mcp/CLAUDE.md +57 -24
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
- package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
- package/templates/apps/mcp/__tests__/index.test.ts +39 -0
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +10 -7
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
- package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
- package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
- package/templates/apps/mcp/__tests__/server.test.ts +67 -14
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
- package/templates/apps/mcp/jest.config.cjs +13 -0
- package/templates/apps/mcp/package.json +2 -0
- package/templates/apps/mcp/src/abuse/index.ts +159 -24
- package/templates/apps/mcp/src/config/modules.ts +10 -5
- package/templates/apps/mcp/src/http-transport.ts +102 -29
- package/templates/apps/mcp/src/index.ts +73 -11
- package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
- package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
- package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
- package/templates/apps/mcp/src/lib/prisma.ts +39 -0
- package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
- package/templates/apps/mcp/src/server.ts +59 -35
- package/templates/apps/mcp/src/tools/audit.ts +26 -2
- package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
- package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
- package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
- package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
- package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
- package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
- package/templates/apps/mcp/src/tools/index.ts +6 -4
- package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
- package/templates/apps/mcp/src/tools/purchase.ts +128 -45
- package/templates/apps/mcp/src/tools/read_content.ts +20 -6
- package/templates/apps/mcp/src/tools/search_content.ts +11 -4
- package/templates/apps/web/.env.example +71 -5
- package/templates/apps/web/CLAUDE.md +44 -0
- package/templates/apps/web/PRODUCT.md +86 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +63 -8
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/layout.tsx +133 -87
- package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
- package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
- package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
- package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
- package/templates/apps/web/app/api/revalidate/route.ts +194 -0
- package/templates/apps/web/auth.ts +407 -8
- package/templates/apps/web/jest.config.cjs +8 -4
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +135 -20
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
- package/templates/apps/web/lib/auth-email.ts +318 -0
- package/templates/apps/web/lib/auth-env.ts +85 -0
- package/templates/apps/web/lib/auth-routes.ts +55 -0
- package/templates/apps/web/lib/meeting-slugs.ts +53 -0
- package/templates/apps/web/lib/site-brand.tsx +4 -8
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +187 -0
- package/templates/apps/web/middleware.ts +1 -1
- package/templates/apps/web/package.json +2 -1
- package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
- package/templates/apps/web/src/lib/commerce-sections.ts +93 -0
- package/templates/content/_site.mdx +18 -3
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
- package/templates/database/CHANGELOG.md +149 -0
- package/templates/database/README.md +10 -3
- package/templates/database/__tests__/appointment.db.test.ts +106 -0
- package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
- package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
- package/templates/database/__tests__/tenancy.test.ts +141 -0
- package/templates/database/config/schema.prisma +79 -2
- package/templates/database/content/schema.prisma +87 -11
- package/templates/database/inbox/schema.prisma +452 -9
- package/templates/database/jest.config.cjs +5 -3
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
- package/templates/database/package.json +3 -2
- package/templates/database/tenancy.ts +86 -1
- package/templates/database/tsconfig.json +1 -0
- package/versions.json +34 -34
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
- package/templates/apps/api/src/routes/content/create.ts +0 -115
- package/templates/apps/api/src/routes/content/delete.ts +0 -82
- package/templates/apps/api/src/routes/content/update.ts +0 -112
- package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
- package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
- package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
- package/templates/apps/mcp/src/tools/create_page.ts +0 -96
- package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
- package/templates/apps/mcp/src/tools/update_page.ts +0 -92
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
- package/templates/apps/web/public/logo-dark.svg +0 -21
- package/templates/apps/web/public/logo-light.svg +0 -21
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* account/actions.ts — the update-your-card banner's Server Action (#6038,
|
|
5
|
+
* D5): mints a Stripe billing-portal session on SUBMIT, never on render, and
|
|
6
|
+
* hands the browser straight to it via `redirect()`.
|
|
7
|
+
*
|
|
8
|
+
* The portal URL is never embedded in page data — it is created here, on
|
|
9
|
+
* click, and never rendered into HTML or a client bundle (D5's precise
|
|
10
|
+
* claim: `redirect()` DOES surface the target in the address bar/history/
|
|
11
|
+
* navigation telemetry for a Server Action, which is inherent to any hosted-
|
|
12
|
+
* portal handoff, not something this design avoids).
|
|
13
|
+
*
|
|
14
|
+
* CSRF is already handled: `forwardedCookieInit()` forwards the incoming
|
|
15
|
+
* request's real `Origin` alongside the cookie, and `apps/api`'s
|
|
16
|
+
* `authPreHandler` requires a matching `Origin` on this cookie-authenticated
|
|
17
|
+
* mutating request (D3.1/D5).
|
|
18
|
+
*
|
|
19
|
+
* `redirect()` throws `NEXT_REDIRECT` by design — called OUTSIDE the
|
|
20
|
+
* try/catch (mirrors `checkout.actions.ts`'s idiom) so the catch never
|
|
21
|
+
* swallows the navigation this action exists to perform.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { subscription } from "@working-theory/api-client";
|
|
25
|
+
import { redirect } from "next/navigation";
|
|
26
|
+
|
|
27
|
+
import { forwardedCookieInit } from "../../../../lib/server-api";
|
|
28
|
+
|
|
29
|
+
/** PII-safe error identity for logs (the `checkout.actions.ts` idiom: `err.name` only). */
|
|
30
|
+
function errorName(err: unknown): string {
|
|
31
|
+
return err instanceof Error ? err.name : "unknown";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Opens the caller's own Stripe billing-portal session and redirects there.
|
|
36
|
+
* Never returns on success. On a non-2xx or thrown error, logs a PII-safe
|
|
37
|
+
* status/name and redirects back to `/account` unchanged (the banner's
|
|
38
|
+
* action-unavailable posture — there is no error UI slot on this button).
|
|
39
|
+
*/
|
|
40
|
+
export async function openBillingPortal(): Promise<void> {
|
|
41
|
+
const init = await forwardedCookieInit();
|
|
42
|
+
|
|
43
|
+
let url: string;
|
|
44
|
+
try {
|
|
45
|
+
const res = await subscription.portalSession(init);
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
console.error(`[openBillingPortal] status=${res.status}`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
url = res.body.url;
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error(`[openBillingPortal] errName=${errorName(err)}`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Outside the try/catch on purpose — see the file header.
|
|
57
|
+
redirect(url);
|
|
58
|
+
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* /[locale]/account — the signed-in respondent's own profile, results,
|
|
3
|
-
* purchases (#4984).
|
|
2
|
+
* /[locale]/account — the signed-in respondent's own profile, results,
|
|
3
|
+
* purchases, and subscription status (#4984, #6038).
|
|
4
4
|
*
|
|
5
5
|
* RSC. The `(authed)` layout is the gate — it already resolved a session or
|
|
6
6
|
* rendered `SignInGate`; this page reads the session again ONLY for the
|
|
7
7
|
* profile fields (name / email / verified). Every data read goes through
|
|
8
8
|
* `@working-theory/api-client` with the caller's cookie forwarded
|
|
9
9
|
* (`../../../../lib/server-api`, D9): `waitlistSignups.me(key)` once per key
|
|
10
|
-
* in `lib/account-config.ts` (Results)
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* in `lib/account-config.ts` (Results), `flowCheckouts.me()` (Purchases),
|
|
11
|
+
* and `subscription.me()` (the update-your-card banner, #6038). A 404 (no
|
|
12
|
+
* signup / no user row for that key / commerce module off) or a 401/403
|
|
13
|
+
* (session missing, email unverified — the profile row already says so) is
|
|
14
|
+
* skipped, never thrown; every other error rethrows to the route error
|
|
15
|
+
* boundary.
|
|
14
16
|
*
|
|
15
17
|
* Chrome: this page composes `AppShell` + `SidebarNav` directly, with this
|
|
16
18
|
* page's own three in-page anchors. The page renders inside the locale
|
|
@@ -20,16 +22,19 @@
|
|
|
20
22
|
|
|
21
23
|
import {
|
|
22
24
|
flowCheckouts,
|
|
25
|
+
subscription,
|
|
23
26
|
waitlistSignups,
|
|
24
27
|
type FlowCheckoutMeItem,
|
|
28
|
+
type SubscriptionMeResponse,
|
|
25
29
|
type WaitlistSignupMeResponse,
|
|
26
30
|
} from "@working-theory/api-client";
|
|
27
31
|
import { AppShell } from "@working-theory/templates/shells";
|
|
28
32
|
import { localeHref } from "@working-theory/ui";
|
|
29
|
-
import { DescriptionList } from "@working-theory/ui/blocks";
|
|
33
|
+
import { DescriptionList, SubscriptionStatusBanner } from "@working-theory/ui/blocks";
|
|
30
34
|
import { SidebarNav, type SidebarNavItem } from "@working-theory/ui/chrome";
|
|
31
35
|
import {
|
|
32
36
|
Badge,
|
|
37
|
+
Button,
|
|
33
38
|
EmptyState,
|
|
34
39
|
Heading,
|
|
35
40
|
MoneyAmount,
|
|
@@ -53,6 +58,7 @@ import { forwardedCookieInit, unwrapApiResult } from "../../../../lib/server-api
|
|
|
53
58
|
import { ForbiddenState } from "../waitlist/components/ForbiddenState";
|
|
54
59
|
import { formatCount, formatDateTime } from "../waitlist/format";
|
|
55
60
|
|
|
61
|
+
import { openBillingPortal } from "./actions";
|
|
56
62
|
import { SignOutButton } from "./sign-out-button";
|
|
57
63
|
|
|
58
64
|
interface Props {
|
|
@@ -93,6 +99,19 @@ async function readCheckouts(init: CookieInit): Promise<FlowCheckoutMeItem[]> {
|
|
|
93
99
|
return unwrapApiResult(Promise.resolve(res));
|
|
94
100
|
}
|
|
95
101
|
|
|
102
|
+
/**
|
|
103
|
+
* The update-your-card banner's read (#6038). `null` covers both "nothing
|
|
104
|
+
* to show" (401/403/404 — the commerce-disabled-build 404 included, D8) and
|
|
105
|
+
* "no row" (a healthy subscriber, `actionRequired: false`) — the page's
|
|
106
|
+
* render condition below is `sub?.actionRequired`, so both collapse to no
|
|
107
|
+
* banner without a separate branch.
|
|
108
|
+
*/
|
|
109
|
+
async function readSubscription(init: CookieInit): Promise<SubscriptionMeResponse | null> {
|
|
110
|
+
const res = await subscription.me(init);
|
|
111
|
+
if (!res.ok && SKIPPED_STATUSES.has(res.status)) return null; // AC-4, the existing constant
|
|
112
|
+
return unwrapApiResult(Promise.resolve(res));
|
|
113
|
+
}
|
|
114
|
+
|
|
96
115
|
export default async function AccountPage({ params }: Props): Promise<ReactElement> {
|
|
97
116
|
const { locale } = await params;
|
|
98
117
|
const t = await getTranslations({ locale, namespace: "account" });
|
|
@@ -112,7 +131,11 @@ export default async function AccountPage({ params }: Props): Promise<ReactEleme
|
|
|
112
131
|
}
|
|
113
132
|
|
|
114
133
|
const init = await forwardedCookieInit();
|
|
115
|
-
const [signups, checkouts] = await Promise.all([
|
|
134
|
+
const [signups, checkouts, sub] = await Promise.all([
|
|
135
|
+
readSignups(init),
|
|
136
|
+
readCheckouts(init),
|
|
137
|
+
readSubscription(init),
|
|
138
|
+
]);
|
|
116
139
|
|
|
117
140
|
const { name, email, emailVerified } = session.user;
|
|
118
141
|
const displayName = name || email;
|
|
@@ -145,6 +168,38 @@ export default async function AccountPage({ params }: Props): Promise<ReactEleme
|
|
|
145
168
|
</Text>
|
|
146
169
|
</div>
|
|
147
170
|
|
|
171
|
+
{sub?.actionRequired ? (
|
|
172
|
+
<SubscriptionStatusBanner
|
|
173
|
+
headline={t("subscription.headline")}
|
|
174
|
+
description={
|
|
175
|
+
sub.status === "canceled" && sub.accessRevokesAt
|
|
176
|
+
? t("subscription.descriptionCanceled", {
|
|
177
|
+
date: formatDateTime(sub.accessRevokesAt, locale),
|
|
178
|
+
})
|
|
179
|
+
: t("subscription.descriptionPastDue")
|
|
180
|
+
}
|
|
181
|
+
tone={sub.status === "canceled" ? "error" : "warning"}
|
|
182
|
+
action={
|
|
183
|
+
sub.portalAvailable ? (
|
|
184
|
+
<form action={openBillingPortal}>
|
|
185
|
+
{/* variant="unstyled" + text-current/border-current, not variant="secondary" —
|
|
186
|
+
a per-mode-fixed color fails contrast against the banner's warning/error
|
|
187
|
+
fill in dark mode (design-review F4); size="big" for the 44px touch-target
|
|
188
|
+
floor this is a mobile CTA under (design-review F5, Rule 14). */}
|
|
189
|
+
<Button
|
|
190
|
+
type="submit"
|
|
191
|
+
variant="unstyled"
|
|
192
|
+
size="big"
|
|
193
|
+
className="rounded-control border-thin border-current bg-transparent px-control-x text-current hover:opacity-90 active:opacity-80"
|
|
194
|
+
>
|
|
195
|
+
{t("subscription.action")}
|
|
196
|
+
</Button>
|
|
197
|
+
</form>
|
|
198
|
+
) : undefined
|
|
199
|
+
}
|
|
200
|
+
/>
|
|
201
|
+
) : null}
|
|
202
|
+
|
|
148
203
|
<section id="profile" className="flex flex-col gap-4" data-testid="account-profile">
|
|
149
204
|
<Heading level={2}>{t("profile.heading")}</Heading>
|
|
150
205
|
<DescriptionList
|
|
@@ -50,5 +50,22 @@ export default async function AuthedLayout({
|
|
|
50
50
|
);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
// data-profile="app" puts the Pro-host editor surfaces in this group on
|
|
54
|
+
// @working-theory/theme's app profile (#5953); tokens.css's own
|
|
55
|
+
// [data-profile="app"] header documents what it overrides. The div carries
|
|
56
|
+
// no className and no layout of its own — it exists only because this
|
|
57
|
+
// layout returns a fragment and the attribute needs an element.
|
|
58
|
+
//
|
|
59
|
+
// Two things about the placement are deliberate. Only the SIGNED-IN branch
|
|
60
|
+
// carries it: SignInGate above is the marketing site's sign-in front door,
|
|
61
|
+
// not an app surface. And the attribute goes on this nested div rather than
|
|
62
|
+
// the app's <html> because apps/web's marketing routes share that root and
|
|
63
|
+
// must stay on the marketing profile — which also makes this half immune to
|
|
64
|
+
// the `:root` shadowing the Admin host hit (different element, no
|
|
65
|
+
// specificity tie, so the app values apply and inherit down).
|
|
66
|
+
return (
|
|
67
|
+
<div data-profile="app" data-testid="authed-profile-root">
|
|
68
|
+
{children}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
54
71
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// CancellationPolicyPage route (#5947). Reads the CancellationPolicyPage MDX frontmatter, renders sections via
|
|
2
|
+
// SectionRenderer, and emits a JSON-LD <script>.
|
|
3
|
+
import type {
|
|
4
|
+
getMdxSource as defaultGetMdxSource,
|
|
5
|
+
getPageSections as defaultGetPageSections} from "@working-theory/page-renderer";
|
|
6
|
+
import {
|
|
7
|
+
renderPage,
|
|
8
|
+
renderPageMetadata
|
|
9
|
+
} from "@working-theory/page-renderer";
|
|
10
|
+
import type { Metadata } from "next";
|
|
11
|
+
import type { ReactElement } from "react";
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
params: Promise<{ locale: string }>;
|
|
15
|
+
getMdxSource?: typeof defaultGetMdxSource;
|
|
16
|
+
getPageSections?: typeof defaultGetPageSections;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function generateMetadata({
|
|
20
|
+
params,
|
|
21
|
+
}: Pick<Props, "params">): Promise<Metadata> {
|
|
22
|
+
const { locale } = await params;
|
|
23
|
+
return renderPageMetadata({ type: "CancellationPolicyPage", slug: "cancellation", locale });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default async function Page({
|
|
27
|
+
params,
|
|
28
|
+
getMdxSource,
|
|
29
|
+
getPageSections,
|
|
30
|
+
}: Props): Promise<ReactElement> {
|
|
31
|
+
const { locale } = await params;
|
|
32
|
+
return renderPage({
|
|
33
|
+
type: "CancellationPolicyPage",
|
|
34
|
+
slug: "cancellation",
|
|
35
|
+
locale,
|
|
36
|
+
// Without a namespace `renderPage` 404s on a missing seed — and a scaffold
|
|
37
|
+
// IS a missing-seed environment, since `build-templates.mjs` snapshots
|
|
38
|
+
// `apps/` but deliberately not `content/`. The namespace makes this route
|
|
39
|
+
// render the #4154 authoring empty state at 200, exactly as /terms does.
|
|
40
|
+
translationsNamespace: "cancellation",
|
|
41
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
42
|
+
...(getPageSections !== undefined && { getPageSections }),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// Chrome extract — Nav + Footer from @working-theory/ui (#1451)
|
|
5
5
|
// Shell/Chrome composition — Nav + Footer wrap via @working-theory/templates MarketingShell (#2820)
|
|
6
6
|
// Session-aware Nav actions — Sign in / Account + Sign out from the server-read session (#4984)
|
|
7
|
+
// Auth route group — the marketing chrome is dropped on the five (auth) paths (#5963)
|
|
7
8
|
// Site-declared theme — `_site.mdx` `theme:` block → token overlay → CSS vars
|
|
8
9
|
//
|
|
9
10
|
// Sits under app/[locale]/ and sets the <html lang> attribute to the
|
|
@@ -48,6 +49,7 @@ import type { ReactElement, ReactNode } from "react";
|
|
|
48
49
|
import { auth } from "../../auth";
|
|
49
50
|
import { GoogleAnalytics } from "../../components/google-analytics";
|
|
50
51
|
import { SignOutTrigger } from "../../components/sign-out-trigger";
|
|
52
|
+
import { isAuthPath } from "../../lib/auth-routes";
|
|
51
53
|
import { navItems } from "../../lib/nav-config";
|
|
52
54
|
import { getOgImageUrl } from "../../lib/og";
|
|
53
55
|
import { SITE_NAME, siteLogo } from "../../lib/site-brand";
|
|
@@ -320,6 +322,21 @@ export default async function LocaleLayout({
|
|
|
320
322
|
const pathname = (await headers()).get("x-pathname") ?? homePath;
|
|
321
323
|
const isDeckHome = siteChrome.home === "deck" && pathname === homePath;
|
|
322
324
|
|
|
325
|
+
// The (auth) route group's chrome branch (#5963).
|
|
326
|
+
//
|
|
327
|
+
// A route group's layout nests INSIDE its parent segment's layout, so
|
|
328
|
+
// `(auth)/layout.tsx` renders BETWEEN the Nav and the Footer below — a group
|
|
329
|
+
// changes which siblings share a layout, it does not let a child escape an
|
|
330
|
+
// ancestor. The only place the marketing frame can be dropped for those five
|
|
331
|
+
// paths is here, where it is composed.
|
|
332
|
+
//
|
|
333
|
+
// Fail-safe direction: `isAuthPath()` answers false for an absent
|
|
334
|
+
// `x-pathname`, so a missing header can never strip a marketing page's
|
|
335
|
+
// chrome; the worst case is an auth page rendered WITH chrome, which the
|
|
336
|
+
// group's own suite catches. The marketing tree is NOT restructured into a
|
|
337
|
+
// `(marketing)` group — a ~35-directory move for a five-page delta (spec D1).
|
|
338
|
+
const isAuthRoute = isAuthPath(pathname, enabledLocales);
|
|
339
|
+
|
|
323
340
|
// Placeholder social handles + copyright (#5469 D5): `{{COMPANY_HANDLE}}` /
|
|
324
341
|
// `{{COMPANY_NAME}}` are create-ailk markers substituted at scaffold time
|
|
325
342
|
// (packages/create-ailk/src/programmatic.ts), so a customer's site never
|
|
@@ -420,94 +437,123 @@ export default async function LocaleLayout({
|
|
|
420
437
|
* emitted unconditionally in OSS forks; paying customers install
|
|
421
438
|
* @working-theory/branding and gate it in their own layout.
|
|
422
439
|
*/}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
440
|
+
{isAuthRoute ? (
|
|
441
|
+
/*
|
|
442
|
+
* The (auth) branch (#5963): the five auth paths render their own
|
|
443
|
+
* `(auth)/layout.tsx` — AuthShell — with no Nav and no marketing
|
|
444
|
+
* Footer above or below it. Everything outside this ternary (the
|
|
445
|
+
* document, the flash-prevention script, the theme vars, the
|
|
446
|
+
* favicons, NextIntlClientProvider, the consent Banner and
|
|
447
|
+
* Telemetry) is shared by both branches and written once.
|
|
448
|
+
*/
|
|
449
|
+
children
|
|
450
|
+
) : (
|
|
451
|
+
<MarketingShell
|
|
452
|
+
contentOwnsMain
|
|
453
|
+
gutters="plain"
|
|
454
|
+
bannerPlacement="above-nav"
|
|
455
|
+
{...(siteChrome.announcementBanner
|
|
456
|
+
? {
|
|
457
|
+
banner: (
|
|
458
|
+
<Banner
|
|
459
|
+
variant="contrast"
|
|
460
|
+
placement="above-nav"
|
|
461
|
+
message="Medium length banner heading with link goes here"
|
|
462
|
+
action={{
|
|
463
|
+
label: "Link",
|
|
464
|
+
href: "/pricing",
|
|
465
|
+
kind: "link",
|
|
466
|
+
}}
|
|
467
|
+
routes={{ include: [homePath] }}
|
|
468
|
+
dismissible={false}
|
|
469
|
+
locale={locale}
|
|
470
|
+
/>
|
|
471
|
+
),
|
|
472
|
+
}
|
|
473
|
+
: {})}
|
|
474
|
+
{...(isDeckHome
|
|
475
|
+
? { snap: "mandatory" as const, deckHero: true }
|
|
476
|
+
: {})}
|
|
477
|
+
nav={
|
|
478
|
+
<Nav
|
|
479
|
+
locale={locale}
|
|
480
|
+
labels={navLabels}
|
|
481
|
+
navItems={siteChrome.nav === "mark-only" ? [] : navItems}
|
|
482
|
+
// Deck home (#5697): the two forwards that join #5426 D4 to a
|
|
483
|
+
// real deck. `sticky` pins the bar — MarketingShell renders
|
|
484
|
+
// {nav} as a plain child of the scroll container, so under
|
|
485
|
+
// `scroll-snap-type: y mandatory` the browser snaps slide 1 to
|
|
486
|
+
// the top on load and the unpinned bar is gone before the
|
|
487
|
+
// reader touches anything. `deckMorph` is what puts BrandLogo's
|
|
488
|
+
// `.ailk-deck-brand-mark` / `.ailk-deck-brand-wordmark` hooks in
|
|
489
|
+
// the document for the D4 CSS to key off; it requires both
|
|
490
|
+
// `logo` and `wordmark` (Nav.tsx:344), which the layout already
|
|
491
|
+
// passes above. Scoped to the SAME `isDeckHome` gate
|
|
492
|
+
// MarketingShell's snap/deckHero use, so a non-deck route's
|
|
493
|
+
// render is byte-identical to before this branch existed.
|
|
494
|
+
{...(isDeckHome ? { sticky: true, deckMorph: true } : {})}
|
|
495
|
+
siteName={SITE_NAME}
|
|
496
|
+
showThemeToggle={false}
|
|
497
|
+
logo={brandLogo}
|
|
498
|
+
wordmark={brandWordmark}
|
|
499
|
+
overflowLabel={navOverflowLabel}
|
|
500
|
+
actions={navActions}
|
|
501
|
+
{...(hasFlowAction
|
|
502
|
+
? {
|
|
503
|
+
renderFlowTrigger: (action, trigger) =>
|
|
504
|
+
action.labelKey === "signOut" ? (
|
|
505
|
+
<SignOutTrigger>{trigger}</SignOutTrigger>
|
|
506
|
+
) : (
|
|
507
|
+
<span
|
|
508
|
+
className="text-body-sm text-(--color-text-muted)"
|
|
509
|
+
data-testid="nav-account-name"
|
|
510
|
+
>
|
|
511
|
+
{navLabels.items.account}
|
|
512
|
+
</span>
|
|
513
|
+
),
|
|
514
|
+
}
|
|
515
|
+
: {})}
|
|
516
|
+
/>
|
|
517
|
+
}
|
|
518
|
+
footer={
|
|
519
|
+
<Footer
|
|
520
|
+
locale={locale}
|
|
521
|
+
itemLabels={navLabels.items}
|
|
522
|
+
navItems={navItems}
|
|
523
|
+
siteName={SITE_NAME}
|
|
524
|
+
logo={brandLogo}
|
|
525
|
+
social={[...socialLinks]}
|
|
526
|
+
{...(siteChrome.footer === "centered"
|
|
527
|
+
? { layout: "centered" as const }
|
|
528
|
+
: {})}
|
|
529
|
+
showThemeToggle
|
|
530
|
+
copyrightText={copyrightText}
|
|
531
|
+
legalLinks={legalLinks}
|
|
532
|
+
languageSwitcher={
|
|
533
|
+
<LocaleSwitcherNav
|
|
534
|
+
current={locale}
|
|
535
|
+
locales={enabledLocales}
|
|
536
|
+
pathname={pathname}
|
|
438
537
|
/>
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
renderFlowTrigger: (action, trigger) =>
|
|
459
|
-
action.labelKey === "signOut" ? (
|
|
460
|
-
<SignOutTrigger>{trigger}</SignOutTrigger>
|
|
461
|
-
) : (
|
|
462
|
-
<span
|
|
463
|
-
className="text-body-sm text-(--color-text-muted)"
|
|
464
|
-
data-testid="nav-account-name"
|
|
465
|
-
>
|
|
466
|
-
{navLabels.items.account}
|
|
467
|
-
</span>
|
|
468
|
-
),
|
|
469
|
-
}
|
|
470
|
-
: {})}
|
|
471
|
-
/>
|
|
472
|
-
}
|
|
473
|
-
footer={
|
|
474
|
-
<Footer
|
|
475
|
-
locale={locale}
|
|
476
|
-
itemLabels={navLabels.items}
|
|
477
|
-
navItems={navItems}
|
|
478
|
-
siteName={SITE_NAME}
|
|
479
|
-
logo={brandLogo}
|
|
480
|
-
social={[...socialLinks]}
|
|
481
|
-
{...(siteChrome.footer === "centered"
|
|
482
|
-
? { layout: "centered" as const }
|
|
483
|
-
: {})}
|
|
484
|
-
showThemeToggle
|
|
485
|
-
copyrightText={copyrightText}
|
|
486
|
-
legalLinks={legalLinks}
|
|
487
|
-
languageSwitcher={
|
|
488
|
-
<LocaleSwitcherNav
|
|
489
|
-
current={locale}
|
|
490
|
-
locales={enabledLocales}
|
|
491
|
-
pathname={pathname}
|
|
492
|
-
/>
|
|
493
|
-
}
|
|
494
|
-
// #4157 — deliberately left as a literal "Built by Working
|
|
495
|
-
// Theory Labs" / https://ailaunchkit.ai, not SITE_NAME: this credits
|
|
496
|
-
// the OSS TEMPLATE this site is built with (a "powered by"
|
|
497
|
-
// link, unconditional in OSS forks per the comment above),
|
|
498
|
-
// never the customer's own identity, so it isn't the vendor-
|
|
499
|
-
// brand-as-default defect #4157 fixes. See
|
|
500
|
-
// docs/specs/4157-scaffold-brand-defaults-spec.md § Design
|
|
501
|
-
// Decisions for the fuller reasoning.
|
|
502
|
-
attribution={{
|
|
503
|
-
label: "Built by Working Theory Labs",
|
|
504
|
-
href: "https://ailaunchkit.ai",
|
|
505
|
-
}}
|
|
506
|
-
/>
|
|
507
|
-
}
|
|
508
|
-
>
|
|
509
|
-
{children}
|
|
510
|
-
</MarketingShell>
|
|
538
|
+
}
|
|
539
|
+
// #4157 — deliberately left as a literal "Built by Working
|
|
540
|
+
// Theory Labs" / https://ailaunchkit.ai, not SITE_NAME: this credits
|
|
541
|
+
// the OSS TEMPLATE this site is built with (a "powered by"
|
|
542
|
+
// link, unconditional in OSS forks per the comment above),
|
|
543
|
+
// never the customer's own identity, so it isn't the vendor-
|
|
544
|
+
// brand-as-default defect #4157 fixes. See
|
|
545
|
+
// docs/specs/4157-scaffold-brand-defaults-spec.md § Design
|
|
546
|
+
// Decisions for the fuller reasoning.
|
|
547
|
+
attribution={{
|
|
548
|
+
label: "Built by Working Theory Labs",
|
|
549
|
+
href: "https://ailaunchkit.ai",
|
|
550
|
+
}}
|
|
551
|
+
/>
|
|
552
|
+
}
|
|
553
|
+
>
|
|
554
|
+
{children}
|
|
555
|
+
</MarketingShell>
|
|
556
|
+
)}
|
|
511
557
|
{/*
|
|
512
558
|
* Cookie consent (#5557). Bottom-pinned `info` Banner in consent
|
|
513
559
|
* mode: `secondaryAction` makes Banner compose BannerConsentIsland,
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for /[locale]/purchase/cancel (#6084).
|
|
3
|
+
*
|
|
4
|
+
* D4 — reads nothing. AC-2 — states nothing was charged, links to
|
|
5
|
+
* `/services`, noindex, every string from the `purchase` namespace.
|
|
6
|
+
*/
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { render } from "@testing-library/react";
|
|
9
|
+
|
|
10
|
+
jest.mock("next/navigation", () => ({
|
|
11
|
+
__esModule: true,
|
|
12
|
+
notFound: jest.fn(() => {
|
|
13
|
+
throw new Error("NEXT_NOT_FOUND");
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
const requestedKeys: string[] = [];
|
|
18
|
+
jest.mock("next-intl/server", () => ({
|
|
19
|
+
__esModule: true,
|
|
20
|
+
getTranslations: jest.fn().mockResolvedValue((key: string) => {
|
|
21
|
+
requestedKeys.push(key);
|
|
22
|
+
return `copy:${key}`;
|
|
23
|
+
}),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
const capturedSections: unknown[] = [];
|
|
27
|
+
jest.mock("@working-theory/ui/sections", () => ({
|
|
28
|
+
__esModule: true,
|
|
29
|
+
SectionRenderer: ({ sections }: { sections: unknown[] }) => {
|
|
30
|
+
capturedSections.splice(0, capturedSections.length, ...sections);
|
|
31
|
+
return React.createElement("div", {
|
|
32
|
+
"data-testid": "section-renderer",
|
|
33
|
+
"data-section-count": String(sections.length),
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
const mockGetSession = jest.fn();
|
|
39
|
+
jest.mock("@working-theory/api-client", () => ({
|
|
40
|
+
__esModule: true,
|
|
41
|
+
checkout: { getSession: (...args: unknown[]) => mockGetSession(...args) },
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
// Mocks the REAL boundary `resolveCommerceClaim()` itself calls
|
|
45
|
+
// (`checkLicense` from `@working-theory/license`), not the internal wrapper
|
|
46
|
+
// module — matching the eslint "no internal-module mocks" rule (#507).
|
|
47
|
+
const mockCheckLicense = jest.fn();
|
|
48
|
+
jest.mock("@working-theory/license", () => ({
|
|
49
|
+
__esModule: true,
|
|
50
|
+
checkLicense: (...args: unknown[]) => mockCheckLicense(...args),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
// `../../../../src/lib/commerce-sections` wraps a lazy, webpack-ignored
|
|
54
|
+
// `import()` of the paid `@working-theory/ui-commerce` package — mocked
|
|
55
|
+
// directly, mirroring `../__tests__/success.test.tsx`'s identical mock.
|
|
56
|
+
const mockEnsureCommerceSectionsRegistered = jest.fn();
|
|
57
|
+
// eslint-disable-next-line no-restricted-syntax -- see the identical exception in success.test.tsx
|
|
58
|
+
jest.mock("../../../../src/lib/commerce-sections", () => ({
|
|
59
|
+
__esModule: true,
|
|
60
|
+
ensureCommerceSectionsRegistered: () =>
|
|
61
|
+
mockEnsureCommerceSectionsRegistered(),
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
import { notFound } from "next/navigation";
|
|
65
|
+
|
|
66
|
+
import Page, { generateMetadata } from "../cancel/page";
|
|
67
|
+
|
|
68
|
+
const mockNotFound = notFound as jest.MockedFunction<typeof notFound>;
|
|
69
|
+
|
|
70
|
+
function sectionTypes(): string[] {
|
|
71
|
+
return capturedSections.map((s) => (s as { type: string }).type);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function renderPage() {
|
|
75
|
+
const element = await Page({ params: Promise.resolve({ locale: "en" }) });
|
|
76
|
+
return render(element);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe("/purchase/cancel", () => {
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
capturedSections.length = 0;
|
|
82
|
+
requestedKeys.length = 0;
|
|
83
|
+
mockGetSession.mockReset();
|
|
84
|
+
mockCheckLicense.mockReset();
|
|
85
|
+
mockCheckLicense.mockResolvedValue({ revoked: false, features: ["commerce"] });
|
|
86
|
+
mockNotFound.mockClear();
|
|
87
|
+
mockEnsureCommerceSectionsRegistered.mockReset();
|
|
88
|
+
mockEnsureCommerceSectionsRegistered.mockResolvedValue(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("renders payment-receipt(outcome:cancel) → cta, with a primary link to /services (AC-2, D5)", async () => {
|
|
92
|
+
await renderPage();
|
|
93
|
+
|
|
94
|
+
expect(sectionTypes()).toEqual(["payment-receipt", "cta"]);
|
|
95
|
+
const receipt = capturedSections[0] as {
|
|
96
|
+
data: Record<string, unknown>;
|
|
97
|
+
};
|
|
98
|
+
expect(receipt.data.outcome).toBe("cancel");
|
|
99
|
+
const cta = capturedSections[1] as { data: Record<string, unknown> };
|
|
100
|
+
expect((cta.data.primaryCta as { href: string }).href).toBe("/services");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("falls back to content-block instead of payment-receipt when @working-theory/ui-commerce is not installed", async () => {
|
|
104
|
+
mockEnsureCommerceSectionsRegistered.mockResolvedValue(false);
|
|
105
|
+
|
|
106
|
+
await renderPage();
|
|
107
|
+
|
|
108
|
+
expect(sectionTypes()).toEqual(["content-block", "cta"]);
|
|
109
|
+
const fallback = capturedSections[0] as {
|
|
110
|
+
data: { headline: string; body: string };
|
|
111
|
+
};
|
|
112
|
+
expect(fallback.data.headline).toBe("copy:cancel.title");
|
|
113
|
+
expect(fallback.data.body).toContain("copy:cancel.message");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("reads NOTHING — no API call at all (D4)", async () => {
|
|
117
|
+
await renderPage();
|
|
118
|
+
expect(mockGetSession).not.toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("takes every string from the purchase namespace (D4/AC-2)", async () => {
|
|
122
|
+
await renderPage();
|
|
123
|
+
expect(requestedKeys.length).toBeGreaterThan(0);
|
|
124
|
+
expect(requestedKeys.every((k) => k.startsWith("cancel."))).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("is not indexable", async () => {
|
|
128
|
+
const metadata = await generateMetadata({
|
|
129
|
+
params: Promise.resolve({ locale: "en" }),
|
|
130
|
+
});
|
|
131
|
+
expect(metadata.robots).toEqual({ index: false, follow: true });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("carries no data-outcome=success and no PaymentComplete string — a cancel is not a payment claim", async () => {
|
|
135
|
+
await renderPage();
|
|
136
|
+
const serialized = JSON.stringify(capturedSections);
|
|
137
|
+
expect(serialized).not.toContain('"outcome":"success"');
|
|
138
|
+
expect(serialized).not.toContain("PaymentComplete");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ── T-16 / D12 — the request-time commerce-claim gate ───────────────────
|
|
142
|
+
|
|
143
|
+
it("answers notFound() when the commerce claim is NOT held (D12)", async () => {
|
|
144
|
+
mockCheckLicense.mockResolvedValue({ revoked: false, features: [] });
|
|
145
|
+
|
|
146
|
+
await expect(renderPage()).rejects.toThrow("NEXT_NOT_FOUND");
|
|
147
|
+
expect(mockNotFound).toHaveBeenCalledTimes(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("fail-closed: answers notFound() when the licence read THROWS", async () => {
|
|
151
|
+
mockCheckLicense.mockRejectedValue(new Error("licence read blew up"));
|
|
152
|
+
|
|
153
|
+
await expect(renderPage()).rejects.toThrow();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("renders normally when the commerce claim IS held", async () => {
|
|
157
|
+
mockCheckLicense.mockResolvedValue({ revoked: false, features: ["commerce"] });
|
|
158
|
+
await renderPage();
|
|
159
|
+
expect(mockNotFound).not.toHaveBeenCalled();
|
|
160
|
+
expect(sectionTypes()).toEqual(["payment-receipt", "cta"]);
|
|
161
|
+
});
|
|
162
|
+
});
|