create-ailk 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -1
- package/component-catalog.json +220 -8
- package/component-catalog.md +21 -9
- package/dist/add-module.js +9 -5
- package/dist/apply-design.d.ts +9 -0
- package/dist/apply-design.js +78 -31
- package/dist/derive-architecture.d.ts +8 -1
- package/dist/derive-architecture.js +38 -3
- package/dist/design-home.d.ts +54 -11
- package/dist/design-home.js +160 -22
- package/dist/design-package.js +7 -2
- package/dist/lib/design-checks.d.ts +6 -1
- package/dist/lib/design-checks.js +23 -2
- package/dist/lib/env-catalog.js +5 -1
- package/dist/module-architecture.js +4 -2
- package/dist/programmatic.js +63 -25
- package/dist/site-chrome.d.ts +8 -0
- package/dist/site-chrome.js +17 -1
- package/dist/site-roster.d.ts +4 -2
- package/dist/site-roster.js +7 -5
- package/dist/spoke-checkout.d.ts +39 -18
- package/dist/spoke-checkout.js +177 -236
- package/dist/starter-grid.js +16 -0
- package/dist/surfaces.js +30 -14
- package/dist/sync-routes.d.ts +28 -0
- package/dist/sync-routes.js +124 -1
- package/dist/template-record.d.ts +4 -3
- package/dist/template-record.js +6 -3
- package/dist/topology.js +25 -1
- package/dist/update.d.ts +6 -1
- package/dist/update.js +125 -18
- package/package.json +3 -3
- package/site-templates/_base/ailk-template.json +2 -2
- package/site-templates/_base/content/en/WorkingAgreementPage/working-agreement.mdx +78 -0
- package/site-templates/designs/minimalism/DESIGN.md +1 -1
- package/site-templates/designs/minimalism/contact/local-business-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-product.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/person-service.mdx +23 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +11 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +34 -23
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +31 -21
- package/templates/README.md +26 -25
- package/templates/apps/api/CLAUDE.md +20 -7
- package/templates/apps/api/jest.config.cjs +1 -0
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +19 -0
- package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +149 -0
- package/templates/apps/api/src/lib/consent-store.ts +28 -0
- package/templates/apps/api/src/lib/flow-completion-store.ts +136 -0
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +38 -2
- package/templates/apps/api/src/lib/proposal-paid-store.ts +21 -4
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +14 -3
- package/templates/apps/api/src/middleware/auth.ts +13 -6
- package/templates/apps/api/src/openapi/spec.ts +30 -3
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +49 -0
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +41 -0
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +386 -0
- package/templates/apps/api/src/routes/consent/account.ts +266 -0
- package/templates/apps/api/src/routes/consent/index.ts +15 -7
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +36 -0
- package/templates/apps/api/src/routes/content/get-page.ts +4 -0
- package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +515 -0
- package/templates/apps/api/src/routes/leads/index.ts +161 -7
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +26 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +649 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +154 -19
- package/templates/apps/api/src/server.ts +28 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +1038 -118
- package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +389 -0
- package/templates/apps/api/src/services/checkout-buyer.ts +100 -4
- package/templates/apps/api/src/services/consent.ts +95 -0
- package/templates/apps/api/src/services/lead-routing.ts +337 -8
- package/templates/apps/api/src/services/magnet-completion.ts +242 -0
- package/templates/apps/api/src/services/person.ts +40 -8
- package/templates/apps/api/src/services/plan-offerings.ts +59 -0
- package/templates/apps/api/src/services/project-listings.ts +6 -1
- package/templates/apps/api/src/services/public-site-resolver.ts +8 -1
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +69 -61
- package/templates/apps/mcp/jest.config.cjs +1 -0
- package/templates/apps/web/.env.example +14 -0
- package/templates/apps/web/CLAUDE.md +139 -3
- package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +6 -2
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +5 -4
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +3 -14
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +160 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +12 -9
- package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +105 -2
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +36 -3
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +47 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +60 -22
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +16 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +85 -31
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +18 -4
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +3 -1
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +7 -4
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +227 -207
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +17 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +143 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +4 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +385 -89
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +5 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +44 -5
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +68 -34
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +0 -8
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +39 -151
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-gate.ts +61 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +143 -140
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +40 -6
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +68 -54
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +5 -1
- package/templates/apps/web/app/[locale]/about/[slug]/page.tsx +46 -0
- package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +350 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +83 -1
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +293 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +115 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +146 -1
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +310 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +143 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +350 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +29 -3
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/checkout/__tests__/pages.test.tsx +139 -0
- package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +136 -0
- package/templates/apps/web/app/[locale]/checkout/thank-you/page.tsx +114 -0
- package/templates/apps/web/app/[locale]/checkout/unavailable/page.tsx +51 -0
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +24 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +79 -4
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-magnet.test.ts +138 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +17 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +386 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +504 -24
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +12 -3
- package/templates/apps/web/app/[locale]/contact/contact-form-magnet.ts +77 -0
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +2 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +184 -41
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +26 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +120 -16
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +106 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.invite.test.tsx +257 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/account-invite.actions.test.ts +69 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/account-invite.actions.ts +35 -0
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/layout.tsx +23 -2
- package/templates/apps/web/app/[locale]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +89 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +104 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/working-agreement/__tests__/page.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/working-agreement/page.tsx +41 -0
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +114 -0
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +20 -3
- package/templates/apps/web/app/api/checkout/[offering]/route.ts +48 -0
- package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +120 -0
- package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +86 -0
- package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +44 -1
- package/templates/apps/web/app/api/hub-session/sign-in/route.ts +22 -3
- package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +92 -0
- package/templates/apps/web/app/api/webhooks/stripe/route.ts +31 -0
- package/templates/apps/web/auth.ts +28 -2
- package/templates/apps/web/jest.config.cjs +1 -0
- package/templates/apps/web/lib/__tests__/account-invite.test.ts +63 -0
- package/templates/apps/web/lib/__tests__/checkout.test.ts +243 -0
- package/templates/apps/web/lib/__tests__/consent-policies.test.ts +23 -0
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +249 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -19
- package/templates/apps/web/lib/account-invite.ts +37 -0
- package/templates/apps/web/lib/checkout-config.ts +27 -0
- package/templates/apps/web/lib/checkout.ts +93 -0
- package/templates/apps/web/lib/consent-policies.ts +22 -0
- package/templates/apps/web/lib/deck-page.ts +192 -0
- package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +111 -0
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +88 -32
- package/templates/apps/web/lib/prisma-for-auth.ts +8 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +447 -0
- package/templates/apps/web/lib/site-chrome.ts +2 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +40 -12
- package/templates/apps/web/package.json +1 -0
- package/templates/apps/web/src/lib/__tests__/lead-write.test.ts +187 -0
- package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -0
- package/templates/apps/web/src/lib/lead-write.ts +90 -0
- package/templates/apps/web/src/lib/rate-limit.ts +79 -0
- package/templates/database/CHANGELOG.md +47 -0
- package/templates/database/__tests__/consent-source-checkout-migration.test.ts +79 -0
- package/templates/database/__tests__/flow-completion-migration.test.ts +115 -0
- package/templates/database/__tests__/flow-completion.db.test.ts +136 -0
- package/templates/database/__tests__/seed.test.ts +72 -2
- package/templates/database/inbox/schema.prisma +17 -2
- package/templates/database/jest.config.cjs +1 -0
- package/templates/database/migrations/20261018120000_flow_completion/migration.sql +52 -0
- package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +27 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +60 -0
- package/templates/tools/jest/pin-timezone.cjs +34 -0
- package/versions.json +34 -34
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +0 -125
|
@@ -1,181 +1,184 @@
|
|
|
1
|
-
"use client";
|
|
1
|
+
"use client"; // Accept calls a Server Action then refreshes the router; Sign out is the frame's client hook
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* AgreementsInterstitial — the full-page sign-up-agreement ask (#8765 D7,
|
|
5
|
+
* spec docs/specs/8765-sign-up-refuses-without-the-spec.md; replaces the
|
|
6
|
+
* Inbox's `AgreementsPrompt` card, D8).
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* The account layout renders this IN PLACE of `AccountFrame` and its
|
|
9
|
+
* children whenever `GET /v1/consent/account` answers `complete: false`, or
|
|
10
|
+
* fails closed on any read but a 2xx or an older hub's 404 (SEC-8765-C), so no
|
|
11
|
+
* account page, rail, Inbox or For-you panel loads behind it. The file keeps
|
|
12
|
+
* its name because `create-ailk`'s `surfaces.ts` syncs it by path.
|
|
11
13
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* `
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* the
|
|
14
|
+
* - One `h1`: the "updated" title when any agreement shown has an accepted
|
|
15
|
+
* (older) version, else the "new" one — then the lead line.
|
|
16
|
+
* - Each statement through `ConsentStatement`, its `[Label](key)` tokens
|
|
17
|
+
* linked by `consentPoliciesFor(locale)` (D9) — the three destinations
|
|
18
|
+
* `/contact` links (#8761).
|
|
19
|
+
* - Accept posts every id through `acceptAccountAgreementsAction`, then
|
|
20
|
+
* refreshes, so the layout's next read loads the account; a failed write
|
|
21
|
+
* shows the error line and leaves the page as it is.
|
|
22
|
+
* - Sign out is always there (D1's "never blocked"): `useAccountSignOut`,
|
|
23
|
+
* the frame's own Log out.
|
|
20
24
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* `../auth` dynamically): `./agreements-actions` reads a session and posts
|
|
24
|
-
* through `@working-theory/api-client`, the same database/sign-in-adjacent
|
|
25
|
-
* surface the seam exists to keep lazy. The cookie it consumes is minted by
|
|
26
|
-
* `(auth)/sign-up/sign-up.actions.ts` — a sign-in SEED a sign-in-less spoke
|
|
27
|
-
* drops (`SPOKE_REMOVED_ROOTS`/`spokeExclusions`,
|
|
28
|
-
* `packages/create-ailk/src/topology.ts` D4) — but this file never reaches
|
|
29
|
-
* that module directly; the consume-side logic lives here instead,
|
|
30
|
-
* precisely so this directory carries no cross-boundary import into a seed
|
|
31
|
-
* a spoke prunes. A spoke member signs up on the hub (C6), so on a spoke
|
|
32
|
-
* there is simply no carried cookie to find — the "Otherwise" card below
|
|
33
|
-
* shows instead, exactly as it does for any other failed carried check.
|
|
34
|
-
*
|
|
35
|
-
* "Otherwise" card: the statement (plain text, as `AgreementList` reads it
|
|
36
|
-
* — no markdown-link parsing here either; see that component's own note)
|
|
37
|
-
* plus a link to its policy when the wire carries one, and one Accept
|
|
38
|
-
* button that posts every missing id through `setConsentAction` with no
|
|
39
|
-
* `via` (`account_settings`).
|
|
25
|
+
* The wrapper carries the account area's `data-ailk-shell` marker, so the
|
|
26
|
+
* page draws in the same app tokens as the account it stands in for.
|
|
40
27
|
*/
|
|
41
28
|
|
|
42
|
-
import {
|
|
29
|
+
import { ConsentStatement } from "@working-theory/ui/blocks";
|
|
30
|
+
import {
|
|
31
|
+
Button,
|
|
32
|
+
Heading,
|
|
33
|
+
InlineMessage,
|
|
34
|
+
Text,
|
|
35
|
+
} from "@working-theory/ui/primitives";
|
|
43
36
|
import { useRouter } from "next/navigation";
|
|
44
|
-
import {
|
|
37
|
+
import { useState, type ReactElement } from "react";
|
|
38
|
+
|
|
39
|
+
import { consentPoliciesFor } from "../../../../lib/consent-policies";
|
|
45
40
|
|
|
46
|
-
import {
|
|
41
|
+
import { useAccountSignOut } from "./account-frame";
|
|
42
|
+
import { acceptAccountAgreementsAction } from "./agreements-actions";
|
|
47
43
|
|
|
48
|
-
|
|
44
|
+
/** One sign-up agreement the person has not accepted at its current version. */
|
|
45
|
+
export interface AgreementsInterstitialAgreement {
|
|
49
46
|
id: string;
|
|
47
|
+
/** The resolved statement, `[Label](key)` tokens included. */
|
|
50
48
|
statement: string;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
/** True when this purpose has an OWN grant at an older version (D13's "updated" copy). */
|
|
54
|
-
hadPriorGrant: boolean;
|
|
49
|
+
/** The version last accepted, or `null` when never accepted. */
|
|
50
|
+
acceptedVersion: number | null;
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
export interface
|
|
53
|
+
export interface AgreementsInterstitialLabels {
|
|
58
54
|
titleNew: string;
|
|
59
55
|
titleUpdated: string;
|
|
56
|
+
lead: string;
|
|
60
57
|
accept: string;
|
|
58
|
+
signOut: string;
|
|
61
59
|
error: string;
|
|
62
|
-
/**
|
|
63
|
-
|
|
60
|
+
/** Shown when a hub sign-out is refused or fails (#8465). */
|
|
61
|
+
signOutFailed: string;
|
|
64
62
|
}
|
|
65
63
|
|
|
66
|
-
export interface
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
export interface AgreementsInterstitialProps {
|
|
65
|
+
locale: string;
|
|
66
|
+
agreements: AgreementsInterstitialAgreement[];
|
|
67
|
+
labels: AgreementsInterstitialLabels;
|
|
68
|
+
/** The seam's `SIGN_OUT_ENDPOINT`, as the frame takes it (#8465). */
|
|
69
|
+
signOutEndpoint?: string | null;
|
|
70
|
+
/** Where a hub sign-out lands; defaults to the site home. */
|
|
71
|
+
signInHref?: string;
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
export function AgreementsInterstitial({
|
|
75
|
+
locale,
|
|
76
|
+
agreements,
|
|
77
|
+
labels,
|
|
78
|
+
signOutEndpoint,
|
|
79
|
+
signInHref,
|
|
80
|
+
}: AgreementsInterstitialProps): ReactElement {
|
|
74
81
|
const router = useRouter();
|
|
75
|
-
const
|
|
82
|
+
const { onSignOut, signOutFailed } = useAccountSignOut({
|
|
83
|
+
locale,
|
|
84
|
+
signOutEndpoint,
|
|
85
|
+
signInHref,
|
|
86
|
+
});
|
|
76
87
|
const [pending, setPending] = useState(false);
|
|
77
|
-
const [error, setError] = useState
|
|
78
|
-
const
|
|
88
|
+
const [error, setError] = useState(false);
|
|
89
|
+
const policies = consentPoliciesFor(locale);
|
|
79
90
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (attempted.current) return;
|
|
86
|
-
attempted.current = true;
|
|
87
|
-
let active = true;
|
|
88
|
-
void (async () => {
|
|
89
|
-
try {
|
|
90
|
-
const { acceptCarriedAgreementsAction } = await import("./agreements-actions");
|
|
91
|
-
const result = await acceptCarriedAgreementsAction(missing.map((m) => m.id));
|
|
92
|
-
if (!active) return;
|
|
93
|
-
if (result.accepted) {
|
|
94
|
-
setState("hidden");
|
|
95
|
-
router.refresh();
|
|
96
|
-
} else {
|
|
97
|
-
setState("show");
|
|
98
|
-
}
|
|
99
|
-
} catch {
|
|
100
|
-
if (active) setState("show");
|
|
101
|
-
}
|
|
102
|
-
})();
|
|
103
|
-
return () => {
|
|
104
|
-
active = false;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
}, [router]);
|
|
91
|
+
const title = agreements.some(
|
|
92
|
+
(agreement) => agreement.acceptedVersion !== null,
|
|
93
|
+
)
|
|
94
|
+
? labels.titleUpdated
|
|
95
|
+
: labels.titleNew;
|
|
108
96
|
|
|
109
|
-
async function
|
|
97
|
+
async function accept(): Promise<void> {
|
|
110
98
|
setPending(true);
|
|
111
|
-
setError(
|
|
99
|
+
setError(false);
|
|
112
100
|
try {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// effect that would otherwise notice `missing` is now empty never
|
|
118
|
-
// re-runs (its deps are `[router]`, not `missing`), since this is
|
|
119
|
-
// the SAME component instance, not a fresh mount.
|
|
120
|
-
setState("hidden");
|
|
101
|
+
const result = await acceptAccountAgreementsAction(
|
|
102
|
+
agreements.map((agreement) => agreement.id),
|
|
103
|
+
);
|
|
104
|
+
if (result.ok) {
|
|
121
105
|
router.refresh();
|
|
122
|
-
|
|
123
|
-
setError(labels.error);
|
|
124
|
-
setPending(false);
|
|
106
|
+
return;
|
|
125
107
|
}
|
|
126
108
|
} catch {
|
|
127
|
-
|
|
128
|
-
setPending(false);
|
|
109
|
+
// Falls through to the error line below.
|
|
129
110
|
}
|
|
111
|
+
setError(true);
|
|
112
|
+
setPending(false);
|
|
130
113
|
}
|
|
131
114
|
|
|
132
|
-
if (state !== "show") return null;
|
|
133
|
-
|
|
134
|
-
const title = missing.some((m) => m.hadPriorGrant) ? labels.titleUpdated : labels.titleNew;
|
|
135
|
-
|
|
136
115
|
return (
|
|
137
|
-
<div
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
116
|
+
<div className="contents" data-ailk-shell="account">
|
|
117
|
+
<main
|
|
118
|
+
id="main"
|
|
119
|
+
className="flex min-h-screen items-center justify-center bg-bg px-4 py-section text-fg"
|
|
120
|
+
data-testid="account-agreements-interstitial"
|
|
121
|
+
>
|
|
122
|
+
<div className="flex w-full max-w-measure flex-col gap-6">
|
|
123
|
+
<div className="flex flex-col gap-2">
|
|
124
|
+
<Heading level={1} visual={2}>
|
|
125
|
+
{title}
|
|
126
|
+
</Heading>
|
|
127
|
+
<Text as="p" tone="muted">
|
|
128
|
+
{labels.lead}
|
|
149
129
|
</Text>
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div className="flex flex-col gap-3">
|
|
133
|
+
{agreements.map((agreement) => (
|
|
134
|
+
<span
|
|
135
|
+
key={agreement.id}
|
|
136
|
+
data-testid={`account-agreements-statement-${agreement.id}`}
|
|
155
137
|
>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
138
|
+
<ConsentStatement
|
|
139
|
+
statement={agreement.statement}
|
|
140
|
+
policies={policies}
|
|
141
|
+
/>
|
|
142
|
+
</span>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{error ? (
|
|
147
|
+
<InlineMessage
|
|
148
|
+
variant="error"
|
|
149
|
+
data-testid="account-agreements-error"
|
|
150
|
+
>
|
|
151
|
+
{labels.error}
|
|
152
|
+
</InlineMessage>
|
|
153
|
+
) : null}
|
|
154
|
+
{signOutFailed ? (
|
|
155
|
+
<InlineMessage variant="error" data-testid="account-sign-out-error">
|
|
156
|
+
{labels.signOutFailed}
|
|
157
|
+
</InlineMessage>
|
|
158
|
+
) : null}
|
|
159
|
+
|
|
160
|
+
<div className="flex flex-col gap-3 md:flex-row">
|
|
161
|
+
<Button
|
|
162
|
+
type="button"
|
|
163
|
+
size="big"
|
|
164
|
+
disabled={pending}
|
|
165
|
+
onClick={() => void accept()}
|
|
166
|
+
data-testid="account-agreements-accept"
|
|
167
|
+
>
|
|
168
|
+
{labels.accept}
|
|
169
|
+
</Button>
|
|
170
|
+
<Button
|
|
171
|
+
type="button"
|
|
172
|
+
variant="ghost"
|
|
173
|
+
size="big"
|
|
174
|
+
onClick={onSignOut}
|
|
175
|
+
data-testid="account-agreements-sign-out"
|
|
176
|
+
>
|
|
177
|
+
{labels.signOut}
|
|
178
|
+
</Button>
|
|
159
179
|
</div>
|
|
160
|
-
|
|
161
|
-
</
|
|
162
|
-
{error ? (
|
|
163
|
-
<span role="alert" data-testid="account-agreements-prompt-error">
|
|
164
|
-
<Text as="span" variant="caption">
|
|
165
|
-
{error}
|
|
166
|
-
</Text>
|
|
167
|
-
</span>
|
|
168
|
-
) : null}
|
|
169
|
-
<div>
|
|
170
|
-
<Button
|
|
171
|
-
type="button"
|
|
172
|
-
disabled={pending}
|
|
173
|
-
onClick={() => void acceptAll()}
|
|
174
|
-
data-testid="account-agreements-prompt-accept"
|
|
175
|
-
>
|
|
176
|
-
{labels.accept}
|
|
177
|
-
</Button>
|
|
178
|
-
</div>
|
|
180
|
+
</div>
|
|
181
|
+
</main>
|
|
179
182
|
</div>
|
|
180
183
|
);
|
|
181
184
|
}
|
|
@@ -24,6 +24,16 @@
|
|
|
24
24
|
* The `(authed)` layout above owns the signed-out gate, so a missing session
|
|
25
25
|
* here is a race (it expired between the two layouts): the page renders
|
|
26
26
|
* unframed and degrades on its own.
|
|
27
|
+
*
|
|
28
|
+
* #8765 D7 — after the session check the layout reads the caller's
|
|
29
|
+
* account-level sign-up agreements (`GET /v1/consent/account`). While any
|
|
30
|
+
* is not current, it returns `AgreementsInterstitial` IN PLACE of the frame
|
|
31
|
+
* and its children, so no account page loads behind it. It fails closed
|
|
32
|
+
* (SEC-8765-C): any answer but a 2xx or a 404 — a thrown read, a 401, 403,
|
|
33
|
+
* 429 or 5xx — shows the interstitial from the web's own copy of the
|
|
34
|
+
* registry, with the "new" title, and logs `read_failed`. Only a 404, an
|
|
35
|
+
* older hub with no such route, renders the account as before. The gate is
|
|
36
|
+
* on the account area's pages, never the member API (SEC-8765-L).
|
|
27
37
|
*/
|
|
28
38
|
|
|
29
39
|
import { localeHref } from "@working-theory/ui";
|
|
@@ -35,6 +45,8 @@ import { SIGN_IN_PATH, SIGN_OUT_ENDPOINT } from "../../../../lib/topology.genera
|
|
|
35
45
|
|
|
36
46
|
import { accountSession, loadForYou } from "./account-data";
|
|
37
47
|
import { AccountFrame } from "./account-frame";
|
|
48
|
+
import { outstandingAgreements } from "./agreements-gate";
|
|
49
|
+
import { AgreementsInterstitial } from "./agreements-prompt";
|
|
38
50
|
import { forYouLabels } from "./for-you-labels";
|
|
39
51
|
|
|
40
52
|
/** The value when it parses as an https URL, else undefined: operator config is rendered as an href. */
|
|
@@ -61,6 +73,33 @@ export default async function AccountLayout({
|
|
|
61
73
|
if (!session) return <>{children}</>;
|
|
62
74
|
|
|
63
75
|
const t = await getTranslations({ locale, namespace: "account" });
|
|
76
|
+
const signOutProps = SIGN_OUT_ENDPOINT
|
|
77
|
+
? {
|
|
78
|
+
signOutEndpoint: SIGN_OUT_ENDPOINT,
|
|
79
|
+
signInHref: localeHref(locale, SIGN_IN_PATH ?? "/"),
|
|
80
|
+
}
|
|
81
|
+
: {};
|
|
82
|
+
|
|
83
|
+
const outstanding = await outstandingAgreements(session.ctx);
|
|
84
|
+
if (outstanding.length > 0) {
|
|
85
|
+
return (
|
|
86
|
+
<AgreementsInterstitial
|
|
87
|
+
locale={locale}
|
|
88
|
+
agreements={outstanding}
|
|
89
|
+
{...signOutProps}
|
|
90
|
+
labels={{
|
|
91
|
+
titleNew: t("agreements.titleNew"),
|
|
92
|
+
titleUpdated: t("agreements.titleUpdated"),
|
|
93
|
+
lead: t("agreements.lead"),
|
|
94
|
+
accept: t("agreements.accept"),
|
|
95
|
+
signOut: t("agreements.signOut"),
|
|
96
|
+
error: t("agreements.error"),
|
|
97
|
+
signOutFailed: t("frame.signOutFailed"),
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
64
103
|
const forYou = await loadForYou(locale);
|
|
65
104
|
const { name, email } = session.ctx.user;
|
|
66
105
|
const helpCenterUrl = httpsUrl(process.env.AILK_HELP_CENTER_URL);
|
|
@@ -81,12 +120,7 @@ export default async function AccountLayout({
|
|
|
81
120
|
: []),
|
|
82
121
|
]}
|
|
83
122
|
forYou={{ items: forYou.items, now: forYou.now, labels: forYouLabels(session.ctx.t) }}
|
|
84
|
-
{...
|
|
85
|
-
? {
|
|
86
|
-
signOutEndpoint: SIGN_OUT_ENDPOINT,
|
|
87
|
-
signInHref: localeHref(locale, SIGN_IN_PATH ?? "/"),
|
|
88
|
-
}
|
|
89
|
-
: {})}
|
|
123
|
+
{...signOutProps}
|
|
90
124
|
labels={{
|
|
91
125
|
home: t("frame.home"),
|
|
92
126
|
inbox: t("frame.inbox"),
|
|
@@ -39,9 +39,12 @@
|
|
|
39
39
|
* whose `available` read resolved `true`, each opening its popup over the
|
|
40
40
|
* Inbox. None offered (a Free build) → no New at all. A read that rejected
|
|
41
41
|
* drops its row and shows the error row too — a failed read is never silent.
|
|
42
|
+
*
|
|
43
|
+
* #8765 D8 — the Inbox renders nothing about agreements: an outstanding
|
|
44
|
+
* sign-up agreement is asked by the layout's full-page interstitial, before
|
|
45
|
+
* this page loads at all.
|
|
42
46
|
*/
|
|
43
47
|
|
|
44
|
-
import { member } from "@working-theory/api-client";
|
|
45
48
|
import {
|
|
46
49
|
ForYouPanel,
|
|
47
50
|
InboxList,
|
|
@@ -50,9 +53,17 @@ import {
|
|
|
50
53
|
type InboxListRow,
|
|
51
54
|
} from "@working-theory/templates/account";
|
|
52
55
|
import { Heading, Text } from "@working-theory/ui/primitives";
|
|
53
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
CalendarClock,
|
|
58
|
+
ClipboardList,
|
|
59
|
+
Receipt,
|
|
60
|
+
type LucideProps,
|
|
61
|
+
} from "lucide-react";
|
|
54
62
|
import type { Metadata } from "next";
|
|
63
|
+
import { headers } from "next/headers";
|
|
55
64
|
import { getTranslations } from "next-intl/server";
|
|
65
|
+
import { SITE_NAME } from "../../../../lib/site-brand";
|
|
66
|
+
import { readSession } from "../../../../lib/topology.generated";
|
|
56
67
|
import type { ComponentType, ReactElement } from "react";
|
|
57
68
|
|
|
58
69
|
import {
|
|
@@ -63,7 +74,7 @@ import {
|
|
|
63
74
|
logReadFailure,
|
|
64
75
|
type LoadedInboxItem,
|
|
65
76
|
} from "./account-data";
|
|
66
|
-
import {
|
|
77
|
+
import { outstandingAgreements } from "./agreements-gate";
|
|
67
78
|
import { ErrorRow } from "./error-row";
|
|
68
79
|
import { ACCOUNT_PERSIST_KEY, forYouLabels } from "./for-you-labels";
|
|
69
80
|
import { NewMenu, type NewMenuAction } from "./new-menu";
|
|
@@ -76,6 +87,18 @@ interface Props {
|
|
|
76
87
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
77
88
|
const { locale } = await params;
|
|
78
89
|
const t = await getTranslations({ locale, namespace: "account" });
|
|
90
|
+
// Signed out, the `(authed)` layout renders the sign-in gate in this page's
|
|
91
|
+
// place, so the title names THAT state — "Sign in · <site>" — not the Inbox
|
|
92
|
+
// the visitor cannot see yet (#8790 AC-1). The session read is the layout's
|
|
93
|
+
// own (`topology.generated`'s `readSession`); any failure keeps the Inbox
|
|
94
|
+
// title, exactly as before.
|
|
95
|
+
const session = await readSession(await headers()).catch(() => null);
|
|
96
|
+
if (!session) {
|
|
97
|
+
return {
|
|
98
|
+
title: `${t("gate.title")} · ${SITE_NAME}`,
|
|
99
|
+
description: t("description"),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
79
102
|
return { title: t("inbox.title"), description: t("description") };
|
|
80
103
|
}
|
|
81
104
|
|
|
@@ -97,69 +120,57 @@ function soonestFirst(a: LoadedInboxItem, b: LoadedInboxItem): number {
|
|
|
97
120
|
async function loadNewActions(
|
|
98
121
|
ctx: AccountContext,
|
|
99
122
|
): Promise<{ actions: NewMenuAction[]; failed: boolean }> {
|
|
100
|
-
const settled = await Promise.allSettled(
|
|
123
|
+
const settled = await Promise.allSettled(
|
|
124
|
+
newActions.map((action) => action.available(ctx)),
|
|
125
|
+
);
|
|
101
126
|
const actions: NewMenuAction[] = [];
|
|
102
127
|
settled.forEach((result, i) => {
|
|
103
128
|
const action = newActions[i]!;
|
|
104
|
-
if (result.status === "rejected")
|
|
129
|
+
if (result.status === "rejected")
|
|
130
|
+
logReadFailure(`new:${action.id}`, result.reason);
|
|
105
131
|
else if (result.value)
|
|
106
|
-
actions.push({
|
|
132
|
+
actions.push({
|
|
133
|
+
id: action.id,
|
|
134
|
+
label: ctx.t(action.label),
|
|
135
|
+
popup: action.renderPopup(ctx),
|
|
136
|
+
});
|
|
107
137
|
});
|
|
108
|
-
return {
|
|
138
|
+
return {
|
|
139
|
+
actions,
|
|
140
|
+
failed: settled.some((result) => result.status === "rejected"),
|
|
141
|
+
};
|
|
109
142
|
}
|
|
110
143
|
|
|
111
144
|
/** Upcoming items first, soonest first; then the rest, as loaded (newest first). */
|
|
112
145
|
function scheduledOrder(rows: readonly LoadedInboxItem[]): LoadedInboxItem[] {
|
|
113
|
-
const upcoming = rows
|
|
146
|
+
const upcoming = rows
|
|
147
|
+
.filter(({ item }) => item.upcoming === true)
|
|
148
|
+
.sort(soonestFirst);
|
|
114
149
|
return [...upcoming, ...rows.filter(({ item }) => item.upcoming !== true)];
|
|
115
150
|
}
|
|
116
151
|
|
|
117
|
-
|
|
118
|
-
* #8438 D13 — the `signUp` agreements this caller's OWN state does not
|
|
119
|
-
* cover at their CURRENT registry version. A failed or absent read answers
|
|
120
|
-
* none: the prompt is a convenience, never a page-breaking requirement.
|
|
121
|
-
*/
|
|
122
|
-
async function loadMissingAgreements(ctx: AccountContext, locale: string): Promise<MissingAgreement[]> {
|
|
123
|
-
try {
|
|
124
|
-
const res = await member.consents(ctx.init, { locale });
|
|
125
|
-
if (!res.ok) return [];
|
|
126
|
-
return res.body.purposes
|
|
127
|
-
.filter((purpose) => purpose.signUp)
|
|
128
|
-
.filter(
|
|
129
|
-
(purpose) =>
|
|
130
|
-
!purpose.state || !purpose.state.granted || purpose.state.policyVersion !== purpose.policyVersion,
|
|
131
|
-
)
|
|
132
|
-
.map((purpose) => ({
|
|
133
|
-
id: purpose.id,
|
|
134
|
-
statement: purpose.statement,
|
|
135
|
-
policyHref: purpose.policyHref,
|
|
136
|
-
version: purpose.policyVersion,
|
|
137
|
-
hadPriorGrant: purpose.state !== null,
|
|
138
|
-
}));
|
|
139
|
-
} catch {
|
|
140
|
-
return [];
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export default async function AccountInboxPage({ params }: Props): Promise<ReactElement> {
|
|
152
|
+
export default async function AccountInboxPage({ params }: Props): Promise<ReactElement | null> {
|
|
145
153
|
const { locale } = await params;
|
|
146
154
|
const t = await getTranslations({ locale, namespace: "account" });
|
|
147
155
|
const session = await accountSession(locale);
|
|
148
156
|
|
|
157
|
+
// The layout shows the interstitial in place of this page while an
|
|
158
|
+
// agreement is outstanding, but the App Router still renders the page: no
|
|
159
|
+
// Inbox loader may run meanwhile (`member.messages` stamps `readAt`, #8765 D7).
|
|
160
|
+
if (session && (await outstandingAgreements(session.ctx)).length > 0) return null;
|
|
161
|
+
|
|
149
162
|
// The contract's translator, for keys that are data (a kind's `label`).
|
|
150
163
|
const translate = session?.ctx.t ?? ((key: string): string => key);
|
|
151
|
-
const [inbox, forYou, newMenu
|
|
164
|
+
const [inbox, forYou, newMenu] = session
|
|
152
165
|
? await Promise.all([
|
|
153
166
|
loadInboxItems(session.ctx),
|
|
154
167
|
loadForYou(locale),
|
|
155
168
|
loadNewActions(session.ctx),
|
|
156
|
-
loadMissingAgreements(session.ctx, locale),
|
|
157
169
|
])
|
|
158
170
|
: [
|
|
159
171
|
{ items: [], failed: false },
|
|
160
172
|
{ items: [], failed: false, now: "" },
|
|
161
173
|
{ actions: [], failed: false },
|
|
162
|
-
[],
|
|
163
174
|
];
|
|
164
175
|
|
|
165
176
|
const dateFormat = new Intl.DateTimeFormat(locale, {
|
|
@@ -173,14 +184,18 @@ export default async function AccountInboxPage({ params }: Props): Promise<React
|
|
|
173
184
|
return {
|
|
174
185
|
id: `${kind.kind}:${item.id}`,
|
|
175
186
|
...(icon ? { icon } : {}),
|
|
176
|
-
caption: item.at
|
|
187
|
+
caption: item.at
|
|
188
|
+
? `${label} · ${dateFormat.format(new Date(item.at))}`
|
|
189
|
+
: label,
|
|
177
190
|
title: item.title,
|
|
178
191
|
href: inboxItemHref(locale, kind.kind, item.id),
|
|
179
192
|
};
|
|
180
193
|
};
|
|
181
194
|
|
|
182
195
|
const all = inbox.items.map(toRow);
|
|
183
|
-
const purchases = inbox.items
|
|
196
|
+
const purchases = inbox.items
|
|
197
|
+
.filter(({ kind }) => kind.section === "purchases")
|
|
198
|
+
.map(toRow);
|
|
184
199
|
const scheduled = scheduledOrder(
|
|
185
200
|
inbox.items.filter(({ kind }) => kind.section === "upcoming"),
|
|
186
201
|
).map(toRow);
|
|
@@ -189,17 +204,6 @@ export default async function AccountInboxPage({ params }: Props): Promise<React
|
|
|
189
204
|
return (
|
|
190
205
|
// The phone's top bar already clears the canvas — no top inset there.
|
|
191
206
|
<div className="max-w-3xl py-8 max-md:pt-0" data-testid="account-inbox">
|
|
192
|
-
{/* #8438 D13 — the sign-up agreement re-ask, above both phone tabs. */}
|
|
193
|
-
<AgreementsPrompt
|
|
194
|
-
missing={missingAgreements}
|
|
195
|
-
labels={{
|
|
196
|
-
titleNew: t("agreements.titleNew"),
|
|
197
|
-
titleUpdated: t("agreements.titleUpdated"),
|
|
198
|
-
accept: t("agreements.accept"),
|
|
199
|
-
error: t("agreements.error"),
|
|
200
|
-
policyLink: t("settings.privacy.policyLink"),
|
|
201
|
-
}}
|
|
202
|
-
/>
|
|
203
207
|
{/* The page's one h1, on both phone tabs; visually hidden on the phone,
|
|
204
208
|
which has no large title (§5.15). New sits beside it (#8233 D4). */}
|
|
205
209
|
<div className="flex items-center justify-between gap-2">
|
|
@@ -212,7 +216,12 @@ export default async function AccountInboxPage({ params }: Props): Promise<React
|
|
|
212
216
|
/>
|
|
213
217
|
</div>
|
|
214
218
|
<PhoneTabPanel tabs={["inbox"]} desktop data-testid="inbox-panel-inbox">
|
|
215
|
-
<Text
|
|
219
|
+
<Text
|
|
220
|
+
as="p"
|
|
221
|
+
variant="body-sm"
|
|
222
|
+
tone="muted"
|
|
223
|
+
className="mt-block-tight hidden md:block"
|
|
224
|
+
>
|
|
216
225
|
{t("inbox.lead")}
|
|
217
226
|
</Text>
|
|
218
227
|
|
|
@@ -226,7 +235,12 @@ export default async function AccountInboxPage({ params }: Props): Promise<React
|
|
|
226
235
|
|
|
227
236
|
<InboxList
|
|
228
237
|
tabs={[
|
|
229
|
-
{
|
|
238
|
+
{
|
|
239
|
+
id: "all",
|
|
240
|
+
label: t("inbox.tabs.all"),
|
|
241
|
+
count: all.length,
|
|
242
|
+
rows: all,
|
|
243
|
+
},
|
|
230
244
|
{
|
|
231
245
|
id: "purchases",
|
|
232
246
|
label: t("inbox.tabs.purchases"),
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* 3. account-privacy-consents — one `ConsentSwitchList` row per
|
|
15
15
|
* `area: "data"` purpose, or the "no other data-use consents" line.
|
|
16
16
|
* 4. account-privacy-agreements — a read-only `AgreementList` of every
|
|
17
|
-
* agreement (`service_terms`, `account_terms`, …), or "None yet"
|
|
17
|
+
* agreement (`service_terms`, `account_terms`, …), or "None yet"; each
|
|
18
|
+
* statement's links resolve through `consentPoliciesFor(locale)`
|
|
19
|
+
* (#8765 D11).
|
|
18
20
|
* 5. account-deletion — moved unchanged from General (#8438 D16).
|
|
19
21
|
*
|
|
20
22
|
* Sections 3 and 4 read `GET /v1/member/consents`; a failed read shows one
|
|
@@ -35,6 +37,7 @@ import { cookies } from "next/headers";
|
|
|
35
37
|
import { getTranslations } from "next-intl/server";
|
|
36
38
|
import type { ReactElement } from "react";
|
|
37
39
|
|
|
40
|
+
import { consentPoliciesFor } from "../../../../../lib/consent-policies";
|
|
38
41
|
import { accountSession } from "../account-data";
|
|
39
42
|
import { DeletionRequest } from "../general/deletion-request";
|
|
40
43
|
import { loadProfileSource } from "../general/profile-source";
|
|
@@ -188,6 +191,7 @@ export default async function AccountPrivacyPage({ params }: Props): Promise<Rea
|
|
|
188
191
|
notAcceptedLine={t("settings.privacy.agreementsEmpty")}
|
|
189
192
|
emptyLine={t("settings.privacy.agreementsEmpty")}
|
|
190
193
|
testIdPrefix="account-privacy-agreement"
|
|
194
|
+
policies={consentPoliciesFor(locale)}
|
|
191
195
|
/>
|
|
192
196
|
</div>
|
|
193
197
|
</>
|