create-ailk 0.18.0 → 0.19.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/LICENSE +6 -5
- package/NOTICE +2 -0
- package/dist/add-module.js +11 -3
- package/dist/category-profile.d.ts +10 -0
- package/dist/category-profile.js +21 -0
- package/dist/derive-architecture.d.ts +5 -1
- package/dist/derive-architecture.js +8 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/license-key-files.d.ts +6 -3
- package/dist/lib/license-key-files.js +8 -2
- package/dist/lib/scaffold-render-sweep.d.ts +32 -0
- package/dist/lib/scaffold-render-sweep.js +76 -3
- package/dist/license-set.d.ts +1 -1
- package/dist/license-set.js +13 -8
- package/dist/module-architecture.js +128 -3
- package/dist/programmatic.js +11 -1
- package/dist/site-roster.js +21 -4
- package/dist/spoke-checkout.d.ts +11 -3
- package/dist/spoke-checkout.js +62 -12
- package/dist/starter-grid.d.ts +1 -1
- package/dist/surfaces.d.ts +14 -0
- package/dist/surfaces.js +170 -1
- package/dist/template-manifest.d.ts +4 -3
- package/dist/topology.d.ts +7 -2
- package/dist/topology.js +88 -3
- package/package.json +5 -4
- package/templates/.env.example +3 -3
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/apps/api/.env.example +17 -1
- package/templates/apps/api/CLAUDE.md +31 -7
- package/templates/apps/api/LICENSE +6 -5
- package/templates/apps/api/NOTICE +2 -0
- package/templates/apps/api/jest.config.cjs +4 -29
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +19 -0
- package/templates/apps/api/src/__tests__/stripe-boot.test.ts +132 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +17 -0
- package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +237 -0
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +334 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
- package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +217 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +7 -0
- package/templates/apps/api/src/lib/consent-store.ts +335 -0
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
- package/templates/apps/api/src/lib/stripe.ts +176 -3
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +272 -39
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +51 -0
- package/templates/apps/api/src/middleware/auth.ts +214 -46
- package/templates/apps/api/src/middleware/rate-limit.ts +5 -0
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +167 -0
- package/templates/apps/api/src/routes/checkout/session.ts +3 -3
- package/templates/apps/api/src/routes/checkout/sessions.ts +6 -4
- package/templates/apps/api/src/routes/consent/cookie.ts +90 -0
- package/templates/apps/api/src/routes/consent/index.ts +18 -0
- package/templates/apps/api/src/routes/consent/unsubscribe.ts +127 -0
- package/templates/apps/api/src/routes/leads/index.ts +19 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +5 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +17 -4
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +2 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +159 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +158 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +2 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +17 -0
- package/templates/apps/api/src/server.ts +218 -16
- package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +503 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +18 -3
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +173 -4
- package/templates/apps/api/src/services/__tests__/person.test.ts +392 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +87 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +57 -17
- package/templates/apps/api/src/services/consent.ts +488 -3
- package/templates/apps/api/src/services/lead-routing.ts +135 -14
- package/templates/apps/api/src/services/person.ts +501 -1
- package/templates/apps/api/src/services/project-listings.ts +7 -2
- package/templates/apps/api/src/services/request-confirm.ts +52 -0
- package/templates/apps/api/src/services/revalidation.ts +11 -1
- package/templates/apps/api/vercel.json +2 -2
- package/templates/apps/mcp/.env.example +10 -0
- package/templates/apps/mcp/CLAUDE.md +9 -4
- package/templates/apps/mcp/LICENSE +6 -5
- package/templates/apps/mcp/NOTICE +2 -0
- package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
- package/templates/apps/mcp/__tests__/boot.test.ts +78 -1
- package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
- package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
- package/templates/apps/mcp/jest.config.cjs +3 -44
- package/templates/apps/mcp/src/abuse/index.ts +117 -21
- package/templates/apps/mcp/src/boot.ts +5 -23
- package/templates/apps/mcp/src/http-transport.ts +93 -73
- package/templates/apps/mcp/src/index.ts +2 -0
- package/templates/apps/mcp/src/request-context.ts +52 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
- package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
- package/templates/apps/web/LICENSE +6 -5
- package/templates/apps/web/NOTICE +2 -0
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/account-frame-sign-out.test.tsx +175 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +302 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +724 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +346 -41
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +146 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +232 -29
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +140 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +409 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +65 -380
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
- package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +188 -54
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +24 -45
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +174 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/contact-actions.ts +81 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +193 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +30 -4
- package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
- package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +132 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/security-panel.tsx +102 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/security-source.ts +62 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +13 -354
- package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +116 -0
- package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
- package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
- package/templates/apps/web/app/[locale]/layout.tsx +23 -4
- package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
- package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
- package/templates/apps/web/app/robots.txt/route.ts +10 -4
- package/templates/apps/web/auth.ts +25 -2
- package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +84 -0
- package/templates/apps/web/lib/account-config.ts +188 -52
- package/templates/apps/web/lib/auth-email.ts +45 -10
- package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
- package/templates/apps/web/lib/hub-session/index.ts +1 -0
- package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
- package/templates/apps/web/lib/hub-session/seal.ts +13 -9
- package/templates/apps/web/lib/topology.generated.ts +33 -0
- package/templates/apps/web/messages/en.json +194 -33
- package/templates/database/CHANGELOG.md +37 -0
- package/templates/database/LICENSE +6 -5
- package/templates/database/NOTICE +2 -0
- package/templates/database/README.md +1 -1
- package/templates/database/__tests__/consent-ledger.db.test.ts +345 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +6 -2
- package/templates/database/__tests__/fixtures/person-contact-backfill.json +182 -0
- package/templates/database/__tests__/member-area.db.test.ts +15 -3
- package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
- package/templates/database/__tests__/member-contract.db.test.ts +413 -0
- package/templates/database/__tests__/person-contact.db.test.ts +563 -0
- package/templates/database/__tests__/person.db.test.ts +3 -1
- package/templates/database/__tests__/seed.test.ts +54 -2
- package/templates/database/inbox/schema.prisma +166 -25
- package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
- package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
- package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +122 -0
- package/templates/database/migrations/20261014120000_person_contact/migration.sql +231 -0
- package/templates/database/package.json +3 -2
- package/templates/database/seed.ts +34 -2
- package/versions.json +36 -36
- package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
- package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
- package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
- package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
- package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
- package/templates/apps/api/src/__tests__/server.test.ts +0 -591
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
- package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
- package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
- package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
- package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
- package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
- package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
- package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
- package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
- package/templates/database/__tests__/build.test.ts +0 -131
|
@@ -1,367 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* /[locale]/account/settings — User settings (#
|
|
3
|
-
*
|
|
2
|
+
* /[locale]/account/settings — redirects to User settings › General (#8232,
|
|
3
|
+
* spec docs/specs/8232-member-settings-account-general-and-spec.md D8).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Settings is four tabs since #8339 — General · Security · Notifications ·
|
|
6
|
+
* Billing — and this one-page, anchored route (#7430) only forwards. A URL
|
|
7
|
+
* fragment never reaches the server, so `#billing` cannot be redirected
|
|
8
|
+
* here: the browser carries it across this redirect, and General's
|
|
9
|
+
* `BillingAnchor` sends it on to Billing; `#general` lands on General as it
|
|
10
|
+
* is. The hub's authorize step still sends an unverified person here
|
|
11
|
+
* (`VERIFY_EMAIL_PATH`), and this redirect carries them on.
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
* one mode), the email read-only, and the name the member sets and edits
|
|
12
|
-
* (an accepted applicant arrives with none, #7418).
|
|
13
|
-
* - Security — the sign-in methods this site has configured; a Password
|
|
14
|
-
* row only when the password method is on.
|
|
15
|
-
* - Notifications — a note until a preference store exists.
|
|
16
|
-
* - Privacy — the cookie settings (`/privacy#cookie-settings`, since the
|
|
17
|
-
* account area carries no consent Banner, D4) and "Request account
|
|
18
|
-
* deletion", which files a Support request of kind `deletion`.
|
|
19
|
-
* - Billing — only when `subscription.me()` answers a status or an
|
|
20
|
-
* available portal; absent on `status: null` + no portal, and on any
|
|
21
|
-
* 401/403/404 (the commerce-disabled build answers 404).
|
|
22
|
-
* - Then any section a module contributes (`accountSettingsSections`).
|
|
23
|
-
*
|
|
24
|
-
* On a spoke that signs in through its hub (`SIGN_IN_PROVIDER === "hub"`,
|
|
25
|
-
* #7894 D10) the account belongs to the hub: the name is a read-only row,
|
|
26
|
-
* Security says sign-in happens on the hub, and both the name edit and the
|
|
27
|
-
* deletion request are a link to the hub's own account settings — so a
|
|
28
|
-
* request about a hub account never lands in this spoke's inbox
|
|
29
|
-
* (SEC-7894-11). Billing is absent there because its read answers 404.
|
|
30
|
-
*
|
|
31
|
-
* No Credentials, Scope, Organizations or Credits section: those are an
|
|
32
|
-
* agent's or an operator's (§5.12a), never a member's.
|
|
13
|
+
* The directory keeps the one file General reuses unchanged: `theme-modes.ts`.
|
|
33
14
|
*/
|
|
34
15
|
|
|
35
|
-
import { subscription, type SubscriptionMeResponse } from "@working-theory/api-client";
|
|
36
16
|
import { localeHref } from "@working-theory/ui";
|
|
37
|
-
import {
|
|
38
|
-
import { Button, Heading, Link, Text } from "@working-theory/ui/primitives";
|
|
39
|
-
import type { Metadata } from "next";
|
|
40
|
-
import { getTranslations } from "next-intl/server";
|
|
41
|
-
import type { ReactElement, ReactNode } from "react";
|
|
42
|
-
|
|
43
|
-
import { accountSettingsSections } from "../../../../../lib/account-config";
|
|
44
|
-
import { getActiveProviderIds } from "../../../../../lib/auth-env";
|
|
45
|
-
import { hubSessionConfig } from "../../../../../lib/hub-session/config";
|
|
46
|
-
import { SIGN_IN_PROVIDER } from "../../../../../lib/topology.generated";
|
|
47
|
-
import { accountSession, logReadFailure } from "../account-data";
|
|
48
|
-
import { openBillingPortal } from "../actions";
|
|
49
|
-
|
|
50
|
-
import { ColorModeSetting } from "./color-mode";
|
|
51
|
-
import { DeletionRequest } from "./deletion-request";
|
|
52
|
-
import { NameForm } from "./name-form";
|
|
53
|
-
import { siteThemeModeCount } from "./theme-modes";
|
|
17
|
+
import { redirect } from "next/navigation";
|
|
54
18
|
|
|
55
19
|
interface Props {
|
|
56
20
|
params: Promise<{ locale: string }>;
|
|
57
21
|
}
|
|
58
22
|
|
|
59
|
-
export async function
|
|
60
|
-
const { locale } = await params;
|
|
61
|
-
const t = await getTranslations({ locale, namespace: "account" });
|
|
62
|
-
return { title: t("settings.title"), description: t("settings.lead") };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** The section role DESIGN.md §5.12 names: a title, a muted subtitle, then the fields. */
|
|
66
|
-
function Section({
|
|
67
|
-
id,
|
|
68
|
-
title,
|
|
69
|
-
lead,
|
|
70
|
-
children,
|
|
71
|
-
}: {
|
|
72
|
-
id: string;
|
|
73
|
-
title: string;
|
|
74
|
-
lead?: string;
|
|
75
|
-
children: ReactNode;
|
|
76
|
-
}): ReactElement {
|
|
77
|
-
return (
|
|
78
|
-
<section
|
|
79
|
-
id={id}
|
|
80
|
-
aria-labelledby={`${id}-title`}
|
|
81
|
-
className="flex scroll-mt-8 flex-col gap-4 border-t border-(--color-border) pt-8"
|
|
82
|
-
data-testid={`account-settings-${id}`}
|
|
83
|
-
>
|
|
84
|
-
<div>
|
|
85
|
-
<Heading level={2} visual={4} id={`${id}-title`}>
|
|
86
|
-
{title}
|
|
87
|
-
</Heading>
|
|
88
|
-
{lead ? (
|
|
89
|
-
<Text as="p" variant="body-sm" tone="muted" className="mt-1">
|
|
90
|
-
{lead}
|
|
91
|
-
</Text>
|
|
92
|
-
) : null}
|
|
93
|
-
</div>
|
|
94
|
-
{children}
|
|
95
|
-
</section>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** The sign-in methods this deployment has configured, as message keys. */
|
|
100
|
-
const METHOD_KEYS: Record<string, string> = {
|
|
101
|
-
google: "settings.security.google",
|
|
102
|
-
github: "settings.security.github",
|
|
103
|
-
"magic-link": "settings.security.magicLink",
|
|
104
|
-
email: "settings.security.password",
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
/** The hub's own account settings page, on a spoke that signs in through it. */
|
|
108
|
-
function hubSettingsHref(): string | null {
|
|
109
|
-
const config = hubSessionConfig();
|
|
110
|
-
return config === null ? null : new URL("/account/settings", config.hubOrigin).toString();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** A link to the hub's account settings, in place of a control that would act on the spoke. */
|
|
114
|
-
function HubSettingsLink({
|
|
115
|
-
href,
|
|
116
|
-
label,
|
|
117
|
-
testId,
|
|
118
|
-
}: {
|
|
119
|
-
href: string;
|
|
120
|
-
label: string;
|
|
121
|
-
testId: string;
|
|
122
|
-
}): ReactElement {
|
|
123
|
-
return (
|
|
124
|
-
<div>
|
|
125
|
-
<Link href={href} className="inline-flex min-h-6 items-center" data-testid={testId}>
|
|
126
|
-
{label}
|
|
127
|
-
</Link>
|
|
128
|
-
</div>
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** The statuses with a message key; any other renders as the API sent it. */
|
|
133
|
-
const KNOWN_STATUSES: ReadonlySet<string> = new Set(["trialing", "active", "past_due", "canceled"]);
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Billing's read: `null` on any non-2xx — 401/403/404 by D10 — and a failed
|
|
137
|
-
* read leaves the section out rather than failing the whole page. A failure
|
|
138
|
-
* other than those three is logged, so it is not silent to the operator.
|
|
139
|
-
*/
|
|
140
|
-
async function readSubscription(
|
|
141
|
-
init: { headers: Record<string, string> },
|
|
142
|
-
): Promise<SubscriptionMeResponse | null> {
|
|
143
|
-
try {
|
|
144
|
-
const res = await subscription.me(init);
|
|
145
|
-
if (res.ok) return res.body;
|
|
146
|
-
if (![401, 403, 404].includes(res.status)) {
|
|
147
|
-
console.error(`[account] read failed source=billing status=${res.status}`);
|
|
148
|
-
}
|
|
149
|
-
return null;
|
|
150
|
-
} catch (err) {
|
|
151
|
-
logReadFailure("billing", err);
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export default async function AccountSettingsPage({ params }: Props): Promise<ReactElement> {
|
|
23
|
+
export default async function AccountSettingsPage({ params }: Props): Promise<never> {
|
|
157
24
|
const { locale } = await params;
|
|
158
|
-
|
|
159
|
-
const session = await accountSession(locale);
|
|
160
|
-
|
|
161
|
-
// Unreachable behind the (authed) gate; a session that expired between
|
|
162
|
-
// the layout and the page renders the heading alone.
|
|
163
|
-
if (!session) {
|
|
164
|
-
return (
|
|
165
|
-
<div className="max-w-3xl py-8" data-testid="account-settings">
|
|
166
|
-
<Heading level={1} visual={2}>
|
|
167
|
-
{t("settings.title")}
|
|
168
|
-
</Heading>
|
|
169
|
-
</div>
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const { ctx } = session;
|
|
174
|
-
const sub = await readSubscription(ctx.init);
|
|
175
|
-
const showBilling = sub !== null && (sub.status !== null || sub.portalAvailable);
|
|
176
|
-
const methods = getActiveProviderIds().filter((id) => id in METHOD_KEYS);
|
|
177
|
-
const viaHub = SIGN_IN_PROVIDER === "hub";
|
|
178
|
-
const hubHref = viaHub ? hubSettingsHref() : null;
|
|
179
|
-
|
|
180
|
-
return (
|
|
181
|
-
<div className="flex max-w-3xl flex-col gap-8 py-8" data-testid="account-settings">
|
|
182
|
-
<div>
|
|
183
|
-
<Heading level={1} visual={2}>
|
|
184
|
-
{t("settings.title")}
|
|
185
|
-
</Heading>
|
|
186
|
-
<Text as="p" variant="body-sm" tone="muted" className="mt-block-tight">
|
|
187
|
-
{t("settings.lead")}
|
|
188
|
-
</Text>
|
|
189
|
-
</div>
|
|
190
|
-
|
|
191
|
-
<Section id="general" title={t("settings.general.title")} lead={t("settings.general.lead")}>
|
|
192
|
-
{siteThemeModeCount() > 1 ? (
|
|
193
|
-
<div className="flex flex-col items-start gap-2">
|
|
194
|
-
<Text as="p" variant="body-sm" className="font-medium">
|
|
195
|
-
{t("settings.general.colorMode")}
|
|
196
|
-
</Text>
|
|
197
|
-
<ColorModeSetting
|
|
198
|
-
labels={{
|
|
199
|
-
title: t("settings.general.colorMode"),
|
|
200
|
-
light: t("settings.general.light"),
|
|
201
|
-
auto: t("settings.general.auto"),
|
|
202
|
-
dark: t("settings.general.dark"),
|
|
203
|
-
}}
|
|
204
|
-
/>
|
|
205
|
-
</div>
|
|
206
|
-
) : null}
|
|
207
|
-
<DescriptionList
|
|
208
|
-
items={[
|
|
209
|
-
{
|
|
210
|
-
label: t("settings.general.email"),
|
|
211
|
-
value: ctx.user.email,
|
|
212
|
-
"data-testid": "account-settings-email",
|
|
213
|
-
},
|
|
214
|
-
...(viaHub && ctx.user.name
|
|
215
|
-
? [
|
|
216
|
-
{
|
|
217
|
-
label: t("settings.general.name"),
|
|
218
|
-
value: ctx.user.name,
|
|
219
|
-
"data-testid": "account-settings-name",
|
|
220
|
-
},
|
|
221
|
-
]
|
|
222
|
-
: []),
|
|
223
|
-
]}
|
|
224
|
-
/>
|
|
225
|
-
{viaHub ? (
|
|
226
|
-
hubHref !== null ? (
|
|
227
|
-
<HubSettingsLink
|
|
228
|
-
href={hubHref}
|
|
229
|
-
label={t("settings.general.manageOnHub")}
|
|
230
|
-
testId="account-settings-manage-on-hub"
|
|
231
|
-
/>
|
|
232
|
-
) : null
|
|
233
|
-
) : (
|
|
234
|
-
<NameForm
|
|
235
|
-
initialName={ctx.user.name ?? ""}
|
|
236
|
-
labels={{
|
|
237
|
-
name: t("settings.general.name"),
|
|
238
|
-
placeholder: t("settings.general.namePlaceholder"),
|
|
239
|
-
save: t("settings.general.save"),
|
|
240
|
-
saving: t("settings.general.saving"),
|
|
241
|
-
saved: t("settings.general.saved"),
|
|
242
|
-
unsaved: t("settings.general.unsaved"),
|
|
243
|
-
clean: t("settings.general.clean"),
|
|
244
|
-
required: t("settings.general.nameRequired"),
|
|
245
|
-
error: t("settings.general.saveError"),
|
|
246
|
-
}}
|
|
247
|
-
/>
|
|
248
|
-
)}
|
|
249
|
-
</Section>
|
|
250
|
-
|
|
251
|
-
<Section id="security" title={t("settings.security.title")} lead={t("settings.security.lead")}>
|
|
252
|
-
{viaHub ? (
|
|
253
|
-
<Text as="p" variant="body-sm" tone="muted">
|
|
254
|
-
{t("settings.security.viaHub")}
|
|
255
|
-
</Text>
|
|
256
|
-
) : methods.length > 0 ? (
|
|
257
|
-
<div className="flex flex-col gap-2">
|
|
258
|
-
<Text as="p" variant="body-sm" className="font-medium">
|
|
259
|
-
{t("settings.security.methods")}
|
|
260
|
-
</Text>
|
|
261
|
-
<DescriptionList
|
|
262
|
-
items={methods.map((id) => ({
|
|
263
|
-
label: ctx.t(METHOD_KEYS[id]!),
|
|
264
|
-
value: t("settings.security.available"),
|
|
265
|
-
"data-testid": `account-signin-${id}`,
|
|
266
|
-
}))}
|
|
267
|
-
/>
|
|
268
|
-
</div>
|
|
269
|
-
) : (
|
|
270
|
-
<Text as="p" variant="body-sm" tone="muted">
|
|
271
|
-
{t("settings.security.none")}
|
|
272
|
-
</Text>
|
|
273
|
-
)}
|
|
274
|
-
</Section>
|
|
275
|
-
|
|
276
|
-
<Section
|
|
277
|
-
id="notifications"
|
|
278
|
-
title={t("settings.notifications.title")}
|
|
279
|
-
lead={t("settings.notifications.lead")}
|
|
280
|
-
>
|
|
281
|
-
<Text as="p" variant="body-sm" tone="muted">
|
|
282
|
-
{t("settings.notifications.placeholder")}
|
|
283
|
-
</Text>
|
|
284
|
-
</Section>
|
|
285
|
-
|
|
286
|
-
<Section id="privacy" title={t("settings.privacy.title")} lead={t("settings.privacy.lead")}>
|
|
287
|
-
<div>
|
|
288
|
-
<Link
|
|
289
|
-
href={localeHref(locale, "/privacy#cookie-settings")}
|
|
290
|
-
className="inline-flex min-h-6 items-center"
|
|
291
|
-
data-testid="account-cookie-settings"
|
|
292
|
-
>
|
|
293
|
-
{t("settings.privacy.cookies")}
|
|
294
|
-
</Link>
|
|
295
|
-
</div>
|
|
296
|
-
{viaHub ? (
|
|
297
|
-
hubHref !== null ? (
|
|
298
|
-
<HubSettingsLink
|
|
299
|
-
href={hubHref}
|
|
300
|
-
label={t("settings.general.manageOnHub")}
|
|
301
|
-
testId="account-privacy-manage-on-hub"
|
|
302
|
-
/>
|
|
303
|
-
) : null
|
|
304
|
-
) : (
|
|
305
|
-
<div className="flex flex-col gap-2">
|
|
306
|
-
<Text as="p" variant="body-sm" tone="muted">
|
|
307
|
-
{t("settings.privacy.deletionLead")}
|
|
308
|
-
</Text>
|
|
309
|
-
<DeletionRequest
|
|
310
|
-
labels={{
|
|
311
|
-
action: t("settings.privacy.deletion"),
|
|
312
|
-
subject: t("settings.privacy.deletionSubject"),
|
|
313
|
-
message: t("settings.privacy.deletionMessage"),
|
|
314
|
-
sent: t("settings.privacy.deletionSent"),
|
|
315
|
-
error: t("settings.privacy.deletionError"),
|
|
316
|
-
}}
|
|
317
|
-
/>
|
|
318
|
-
</div>
|
|
319
|
-
)}
|
|
320
|
-
</Section>
|
|
321
|
-
|
|
322
|
-
{showBilling ? (
|
|
323
|
-
<Section id="billing" title={t("settings.billing.title")} lead={t("settings.billing.lead")}>
|
|
324
|
-
{sub.status !== null || sub.accessRevokesAt ? (
|
|
325
|
-
<DescriptionList
|
|
326
|
-
items={[
|
|
327
|
-
...(sub.status !== null
|
|
328
|
-
? [
|
|
329
|
-
{
|
|
330
|
-
label: t("settings.billing.status"),
|
|
331
|
-
value: KNOWN_STATUSES.has(sub.status)
|
|
332
|
-
? ctx.t(`settings.billing.statuses.${sub.status}`)
|
|
333
|
-
: sub.status,
|
|
334
|
-
},
|
|
335
|
-
]
|
|
336
|
-
: []),
|
|
337
|
-
...(sub.accessRevokesAt
|
|
338
|
-
? [
|
|
339
|
-
{
|
|
340
|
-
label: t("settings.billing.accessRevokesAt"),
|
|
341
|
-
value: new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(
|
|
342
|
-
new Date(sub.accessRevokesAt),
|
|
343
|
-
),
|
|
344
|
-
},
|
|
345
|
-
]
|
|
346
|
-
: []),
|
|
347
|
-
]}
|
|
348
|
-
/>
|
|
349
|
-
) : null}
|
|
350
|
-
{sub.portalAvailable ? (
|
|
351
|
-
<form action={openBillingPortal}>
|
|
352
|
-
<Button type="submit" variant="secondary" data-testid="account-billing-manage">
|
|
353
|
-
{t("settings.billing.manage")}
|
|
354
|
-
</Button>
|
|
355
|
-
</form>
|
|
356
|
-
) : null}
|
|
357
|
-
</Section>
|
|
358
|
-
) : null}
|
|
359
|
-
|
|
360
|
-
{accountSettingsSections.map((section) => (
|
|
361
|
-
<Section key={section.id} id={section.id} title={ctx.t(section.title)}>
|
|
362
|
-
{section.render(ctx)}
|
|
363
|
-
</Section>
|
|
364
|
-
))}
|
|
365
|
-
</div>
|
|
366
|
-
);
|
|
25
|
+
redirect(localeHref(locale, "/account/general"));
|
|
367
26
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* settings-frame.tsx — User settings' four tabs, General · Security ·
|
|
3
|
+
* Notifications · Billing (#8339 D7; builds on #8232 D1).
|
|
4
|
+
*
|
|
5
|
+
* `memberSettingsFrame` supplies the member's half of the shared
|
|
6
|
+
* `AccountSettingsFrame` (@working-theory/templates/account, spec 8339 D2):
|
|
7
|
+
* `standalone: true` — the Admin's shape, full height and flush, because the
|
|
8
|
+
* account layout renders these routes outside its AppShell (#8430) — Back to
|
|
9
|
+
* the Inbox, and the tabs of `SETTINGS_ROUTES`. Every member settings page
|
|
10
|
+
* calls it and renders the result through `AccountSettingsFrame` (General,
|
|
11
|
+
* via the shared `AccountSettings`) or directly (Security, Notifications,
|
|
12
|
+
* Billing).
|
|
13
|
+
*
|
|
14
|
+
* `SettingsSection` stays as it is: contributed sections
|
|
15
|
+
* (`accountSettingsSections`, `accountBillingSections`) still use it, as do
|
|
16
|
+
* General's and Billing's own trailing content.
|
|
17
|
+
*
|
|
18
|
+
* #8441 — `SETTINGS_ROUTES` is the ONE place the settings routes are named:
|
|
19
|
+
* the tabs here and `isSettingsPath` (which `account-frame.tsx` takes these
|
|
20
|
+
* routes out of its shell with) both read it, so a tab added here is a tab
|
|
21
|
+
* that renders outside the shell — never a second `<main>` nested in the
|
|
22
|
+
* shell's. Adding a tab is one line here (plus its page and message).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { AccountSettingsFrameProps, AccountSettingsTab } from "@working-theory/templates/account";
|
|
26
|
+
import { localeHref } from "@working-theory/ui";
|
|
27
|
+
import { Heading, Icon, Text } from "@working-theory/ui/primitives";
|
|
28
|
+
import { Bell, CreditCard, ShieldCheck, UserRound } from "lucide-react";
|
|
29
|
+
import type { ReactElement, ReactNode } from "react";
|
|
30
|
+
|
|
31
|
+
/** The `next-intl` translator General/Security/Notifications/Billing each
|
|
32
|
+
* hold. Widened from `getTranslations`'s own literal-key-checked type (the
|
|
33
|
+
* `AccountTranslate` idiom, `account-data.ts`) — a settings page's own
|
|
34
|
+
* string keys are static, but the function is shared across four route
|
|
35
|
+
* files with four different generic instantiations. Call sites cast their
|
|
36
|
+
* own `t` with `as unknown as Translate`. */
|
|
37
|
+
export type Translate = (key: string, values?: Record<string, unknown>) => string;
|
|
38
|
+
|
|
39
|
+
/** User settings' tabs, in the reference's order (#8339 D7) — each one a
|
|
40
|
+
* route that renders outside the account shell (#8430, #8441). */
|
|
41
|
+
export const SETTINGS_ROUTES = [
|
|
42
|
+
{ key: "general", path: "/account/general", titleKey: "settings.general.title", icon: UserRound },
|
|
43
|
+
{ key: "security", path: "/account/security", titleKey: "settings.security.title", icon: ShieldCheck },
|
|
44
|
+
{ key: "notifications", path: "/account/notifications", titleKey: "settings.notifications.title", icon: Bell },
|
|
45
|
+
{ key: "billing", path: "/account/billing", titleKey: "settings.billing.title", icon: CreditCard },
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
export type SettingsTabKey = (typeof SETTINGS_ROUTES)[number]["key"];
|
|
49
|
+
|
|
50
|
+
/** Whether `pathname` is one of `SETTINGS_ROUTES`: the request locale's
|
|
51
|
+
* `/<locale>` prefix removed, then the rest compared whole — so a sub-route
|
|
52
|
+
* (`/account/billing/history`) is not a settings route unless it is listed. */
|
|
53
|
+
export function isSettingsPath(pathname: string, locale: string): boolean {
|
|
54
|
+
const prefix = `/${locale}`;
|
|
55
|
+
const bare =
|
|
56
|
+
pathname === prefix || pathname.startsWith(`${prefix}/`) ? pathname.slice(prefix.length) : pathname;
|
|
57
|
+
return SETTINGS_ROUTES.some(({ path }) => path === bare);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The member's half of `AccountSettingsFrameProps` — every settings page's
|
|
61
|
+
* `frame` prop, less `name` (the caller's own session) and `children`. */
|
|
62
|
+
export function memberSettingsFrame(
|
|
63
|
+
locale: string,
|
|
64
|
+
t: Translate,
|
|
65
|
+
active: SettingsTabKey,
|
|
66
|
+
): Omit<AccountSettingsFrameProps, "name" | "children"> {
|
|
67
|
+
const tabs: readonly AccountSettingsTab[] = SETTINGS_ROUTES.map(({ key, path, titleKey, icon }) => ({
|
|
68
|
+
key,
|
|
69
|
+
label: t(titleKey),
|
|
70
|
+
href: localeHref(locale, path),
|
|
71
|
+
icon: <Icon icon={icon} size="sm" decorative className="shrink-0" />,
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
standalone: true,
|
|
76
|
+
back: { label: t("settings.back"), href: localeHref(locale, "/account") },
|
|
77
|
+
railLabel: t("settings.title"),
|
|
78
|
+
accountLabel: t("settings.account"),
|
|
79
|
+
tabs,
|
|
80
|
+
active,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The section role DESIGN.md §5.12 names: a title, a muted subtitle, then the fields. */
|
|
85
|
+
export function SettingsSection({
|
|
86
|
+
id,
|
|
87
|
+
title,
|
|
88
|
+
lead,
|
|
89
|
+
children,
|
|
90
|
+
}: {
|
|
91
|
+
id: string;
|
|
92
|
+
title: string;
|
|
93
|
+
lead?: string;
|
|
94
|
+
children: ReactNode;
|
|
95
|
+
}): ReactElement {
|
|
96
|
+
return (
|
|
97
|
+
<section
|
|
98
|
+
id={id}
|
|
99
|
+
aria-labelledby={`${id}-title`}
|
|
100
|
+
className="flex scroll-mt-8 flex-col gap-4 border-t border-(--color-border) pt-8"
|
|
101
|
+
data-testid={`account-settings-${id}`}
|
|
102
|
+
>
|
|
103
|
+
<div>
|
|
104
|
+
<Heading level={2} visual={4} id={`${id}-title`}>
|
|
105
|
+
{title}
|
|
106
|
+
</Heading>
|
|
107
|
+
{lead ? (
|
|
108
|
+
<Text as="p" variant="body-sm" tone="muted" className="mt-1">
|
|
109
|
+
{lead}
|
|
110
|
+
</Text>
|
|
111
|
+
) : null}
|
|
112
|
+
</div>
|
|
113
|
+
{children}
|
|
114
|
+
</section>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* account/sign-in-method.ts (server code) — how a member signs in, for User settings ›
|
|
3
|
+
* Security's sign-in block (#8232, spec
|
|
4
|
+
* docs/specs/8232-member-settings-account-general-and-spec.md D7).
|
|
5
|
+
*
|
|
6
|
+
* One of the account tree's TWO topology seams (roadmap D1; the other is
|
|
7
|
+
* `billing-source.ts`): no other file under the account tree tests
|
|
8
|
+
* `SIGN_IN_PROVIDER`, `TOPOLOGY` or `SIGN_IN_ENABLED`.
|
|
9
|
+
*
|
|
10
|
+
* - A spoke that signs in through its hub (`SIGN_IN_PROVIDER === "hub"`,
|
|
11
|
+
* #7894) answers `hub`, with the hub's host when it is configured.
|
|
12
|
+
* - Any other site answers `site`, with the sign-in methods this deployment
|
|
13
|
+
* has configured. Better Auth's session does not say which one this
|
|
14
|
+
* member used, so the list is the deployment's (C7).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { getActiveProviderIds } from "../../../../lib/auth-env";
|
|
18
|
+
import { hubSessionConfig } from "../../../../lib/hub-session/config";
|
|
19
|
+
import { SIGN_IN_PROVIDER } from "../../../../lib/topology.generated";
|
|
20
|
+
|
|
21
|
+
/** The sign-in methods General names, in the order it lists them. */
|
|
22
|
+
export const SIGN_IN_METHODS = ["google", "github", "magic-link", "email"] as const;
|
|
23
|
+
|
|
24
|
+
export type SignInMethodId = (typeof SIGN_IN_METHODS)[number];
|
|
25
|
+
|
|
26
|
+
export type SignInMethod =
|
|
27
|
+
| { kind: "hub"; hubHost: string | null }
|
|
28
|
+
| { kind: "site"; methods: SignInMethodId[] };
|
|
29
|
+
|
|
30
|
+
export function signInMethod(): SignInMethod {
|
|
31
|
+
if (SIGN_IN_PROVIDER === "hub") {
|
|
32
|
+
const config = hubSessionConfig();
|
|
33
|
+
return { kind: "hub", hubHost: config === null ? null : new URL(config.hubOrigin).host };
|
|
34
|
+
}
|
|
35
|
+
const active = new Set(getActiveProviderIds());
|
|
36
|
+
return { kind: "site", methods: SIGN_IN_METHODS.filter((id) => active.has(id)) };
|
|
37
|
+
}
|
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
* account/support-actions.ts — the account area's Support request action.
|
|
5
5
|
*
|
|
6
6
|
* `submitSupportRequest` (#7430, spec 7423 D11) — files a Support request
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* as a contact lead: `POST /v1/leads/submit` with `source: "web"`, the
|
|
8
|
+
* session's email and the payload `{ kind: "support", subject, message }`.
|
|
9
|
+
* Every request it files is `support`, whatever is posted (#8232 D12): an
|
|
10
|
+
* account-deletion request goes through `member.requestDeletion`
|
|
11
|
+
* (`profile-actions.ts`), which files it with the deployment that holds the
|
|
12
|
+
* account, so this action needs no topology test (SEC-7894-11). No `siteId` — the
|
|
10
13
|
* route resolves the tenant server-side and ignores a posted one (#6203).
|
|
11
14
|
* The request lands in the admin Inbox as a message. Not a `*Action`-suffixed
|
|
12
15
|
* export in a `*.actions.ts` file: lead capture already has its MCP twin
|
|
@@ -21,7 +24,7 @@
|
|
|
21
24
|
import { leads, visitorForwardHeaders } from "@working-theory/api-client";
|
|
22
25
|
import { headers } from "next/headers";
|
|
23
26
|
|
|
24
|
-
import {
|
|
27
|
+
import { readSession } from "../../../../lib/topology.generated";
|
|
25
28
|
|
|
26
29
|
/** PII-safe error identity for logs (the `checkout.actions.ts` idiom: `err.name` only). */
|
|
27
30
|
function errorName(err: unknown): string {
|
|
@@ -53,7 +56,7 @@ function isNotConfigured(res: { status: number; body: unknown }): boolean {
|
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
/**
|
|
56
|
-
* Files the signed-in member's Support
|
|
59
|
+
* Files the signed-in member's Support request. The email is
|
|
57
60
|
* the session's own — never a form field — so a request always names the
|
|
58
61
|
* person who sent it.
|
|
59
62
|
*/
|
|
@@ -61,10 +64,6 @@ export async function submitSupportRequest(
|
|
|
61
64
|
_prev: SupportRequestState,
|
|
62
65
|
formData: FormData,
|
|
63
66
|
): Promise<SupportRequestState> {
|
|
64
|
-
// A hub sign-in spoke's account belongs to the hub, whose own settings take
|
|
65
|
-
// a deletion request (#7894 D10, SEC-7894-11): here it can only be support.
|
|
66
|
-
const kind =
|
|
67
|
-
field(formData, "kind") === "deletion" && SIGN_IN_PROVIDER !== "hub" ? "deletion" : "support";
|
|
68
67
|
const subject = field(formData, "subject");
|
|
69
68
|
const message = field(formData, "message");
|
|
70
69
|
if (!subject || !message || subject.length > MAX_SUBJECT || message.length > MAX_MESSAGE) {
|
|
@@ -79,7 +78,7 @@ export async function submitSupportRequest(
|
|
|
79
78
|
const body = {
|
|
80
79
|
source: "web",
|
|
81
80
|
email: session.user.email,
|
|
82
|
-
payload: { kind, subject, message },
|
|
81
|
+
payload: { kind: "support", subject, message },
|
|
83
82
|
};
|
|
84
83
|
// #7394 — a spoke counts the request against the visitor's own budget.
|
|
85
84
|
const forwarded = visitorForwardHeaders(
|
|
@@ -27,6 +27,7 @@ import "../globals.css";
|
|
|
27
27
|
|
|
28
28
|
import { Telemetry, VerificationMeta } from "@working-theory/analytics";
|
|
29
29
|
import { MarketingShell } from "@working-theory/templates/shells";
|
|
30
|
+
import { themeToShellCss } from "@working-theory/theme";
|
|
30
31
|
import { Link, isRtl, localeHref } from "@working-theory/ui";
|
|
31
32
|
import { BRAND_PATHS, LocaleSwitcherNav } from "@working-theory/ui/blocks";
|
|
32
33
|
import {
|
|
@@ -58,7 +59,11 @@ import { assertEnabledLocale } from "../../lib/locale-guard";
|
|
|
58
59
|
import { footerColumns, navItems } from "../../lib/nav-config";
|
|
59
60
|
import { getOgImageUrl } from "../../lib/og";
|
|
60
61
|
import { backLinkHref } from "../../lib/profile-back-link";
|
|
61
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
COPYRIGHT_HOLDER,
|
|
64
|
+
SITE_DESCRIPTION,
|
|
65
|
+
SITE_NAME,
|
|
66
|
+
} from "../../lib/site-brand";
|
|
62
67
|
import { siteChrome, type SiteChrome } from "../../lib/site-chrome";
|
|
63
68
|
import { siteLogo } from "../../lib/site-logo";
|
|
64
69
|
// Registers the `_site.mdx` identity with page-renderer at module load, so
|
|
@@ -126,6 +131,11 @@ const NAV_CLIENT_ACTION_FLOW: NavActionFlow["flow"] = {
|
|
|
126
131
|
action: "#",
|
|
127
132
|
};
|
|
128
133
|
|
|
134
|
+
// The member account shell's token reset (#8337 D3) — a package constant
|
|
135
|
+
// (no site, request or user input), computed once at module scope and
|
|
136
|
+
// emitted only on account routes, after `theme-vars`, below.
|
|
137
|
+
const SHELL_CSS = themeToShellCss();
|
|
138
|
+
|
|
129
139
|
// ── Theme helpers ────────────────────────────────────────────────────────────
|
|
130
140
|
|
|
131
141
|
/**
|
|
@@ -214,10 +224,10 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
214
224
|
|
|
215
225
|
return {
|
|
216
226
|
title: SITE_NAME,
|
|
217
|
-
description:
|
|
227
|
+
description: SITE_DESCRIPTION,
|
|
218
228
|
openGraph: {
|
|
219
229
|
title: SITE_NAME,
|
|
220
|
-
description:
|
|
230
|
+
description: SITE_DESCRIPTION,
|
|
221
231
|
type: "website",
|
|
222
232
|
url: baseUrl,
|
|
223
233
|
images: [ogImageUrl],
|
|
@@ -225,7 +235,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
225
235
|
twitter: {
|
|
226
236
|
card: "summary_large_image",
|
|
227
237
|
title: SITE_NAME,
|
|
228
|
-
description:
|
|
238
|
+
description: SITE_DESCRIPTION,
|
|
229
239
|
images: [ogImageUrl],
|
|
230
240
|
},
|
|
231
241
|
...getStealthRobots(),
|
|
@@ -617,6 +627,15 @@ export default async function LocaleLayout({
|
|
|
617
627
|
data-ailk="theme-vars"
|
|
618
628
|
dangerouslySetInnerHTML={{ __html: themeHead.css }}
|
|
619
629
|
/>
|
|
630
|
+
{/* The member account shell's token reset (#8337 D3) — account
|
|
631
|
+
routes only, after theme-vars, so :has([data-ailk-shell])'s
|
|
632
|
+
higher specificity wins whatever a browser's :has() support. */}
|
|
633
|
+
{isAccountRoute ? (
|
|
634
|
+
<style
|
|
635
|
+
data-ailk="shell-vars"
|
|
636
|
+
dangerouslySetInnerHTML={{ __html: SHELL_CSS }}
|
|
637
|
+
/>
|
|
638
|
+
) : null}
|
|
620
639
|
{/* Favicons — single brand mark (theme-independent), PNG fallbacks, ICO legacy */}
|
|
621
640
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
622
641
|
<link
|