create-ailk 0.18.0 → 0.19.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/LICENSE +6 -5
- package/NOTICE +2 -0
- package/dist/add-module.js +4 -1
- 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/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 +9 -0
- package/dist/lib/scaffold-render-sweep.js +24 -3
- package/dist/license-set.d.ts +1 -1
- package/dist/license-set.js +13 -8
- package/dist/module-architecture.js +119 -0
- package/dist/programmatic.js +3 -0
- 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.js +140 -1
- package/dist/template-manifest.d.ts +4 -3
- package/dist/topology.d.ts +7 -2
- package/dist/topology.js +60 -3
- package/package.json +5 -4
- package/templates/.env.example +3 -3
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/apps/api/CLAUDE.md +25 -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/lib/__mocks__/prisma.ts +9 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
- package/templates/apps/api/src/lib/stripe.ts +81 -0
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +266 -39
- package/templates/apps/api/src/middleware/auth.ts +206 -46
- package/templates/apps/api/src/routes/leads/index.ts +4 -0
- package/templates/apps/api/src/server.ts +107 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +58 -0
- package/templates/apps/api/src/services/lead-routing.ts +30 -2
- 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__/billing.test.tsx +288 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +568 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +272 -42
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +115 -16
- 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 +126 -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 +139 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +20 -390
- 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 +114 -50
- 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 +177 -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/deletion-request.tsx +132 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +131 -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 +4 -1
- 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 +80 -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 +108 -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 +5 -1
- package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
- package/templates/apps/web/lib/account-config.ts +188 -52
- 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 +24 -0
- package/templates/apps/web/messages/en.json +156 -35
- package/templates/database/CHANGELOG.md +21 -0
- package/templates/database/LICENSE +6 -5
- package/templates/database/NOTICE +2 -0
- package/templates/database/README.md +1 -1
- 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__/seed.test.ts +54 -2
- package/templates/database/inbox/schema.prisma +90 -15
- 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/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
|
@@ -10,12 +10,31 @@
|
|
|
10
10
|
* out ends the session, then navigates home and refreshes so every RSC under
|
|
11
11
|
* the `(authed)` gate re-reads the (now absent) session.
|
|
12
12
|
*
|
|
13
|
+
* #8231 D1, as amended by #8334 — the rail reads Inbox, a Library group
|
|
14
|
+
* holding Artifacts, and the footer's ?. Settings is not a rail row: it lives
|
|
15
|
+
* in the avatar menu, as on the Admin (the operator's 2026-10-11 Phase 6
|
|
16
|
+
* read), so no row is selected on a Settings page. A row is selected on its
|
|
17
|
+
* own path and the paths under it; the Inbox also on an item's page
|
|
18
|
+
* (/account/inbox/…).
|
|
19
|
+
*
|
|
13
20
|
* #7469 (DESIGN.md §2b as amended 2026-10-01, matching the Admin's §5.15) —
|
|
14
21
|
* below `md` the bar reads avatar · Inbox · For you · Help, with no count
|
|
15
22
|
* on any tab. For you is a page of the Inbox route (`?tab=for-you`): the
|
|
16
23
|
* frame reads the URL and hands the tab to the page (`PhoneTabProvider`),
|
|
17
24
|
* which shows one panel per tab. At `md`+ nothing changes — For you stays
|
|
18
25
|
* the aside.
|
|
26
|
+
*
|
|
27
|
+
* #8233 D4 — on the Inbox route itself the phone top bar's trailing cell is
|
|
28
|
+
* an empty node handed to the page (`NewMenuSlotProvider`), which portals
|
|
29
|
+
* its compact New into it; every other path has no trailing cell.
|
|
30
|
+
*
|
|
31
|
+
* #8337 D4 — the `data-ailk-shell="account"` marker on a `display: contents`
|
|
32
|
+
* wrapper around `SignedInShell`: the layout's `shell-vars` reset
|
|
33
|
+
* (`:root:has([data-ailk-shell])`) matches only while this frame is in the
|
|
34
|
+
* document, so the Admin's colour tokens reach the shell and everything it
|
|
35
|
+
* portals to `document.body` (the avatar/help menus, For-you, New, the phone
|
|
36
|
+
* sheets) without the wrapper adding a box — `contents` keeps the shell's own
|
|
37
|
+
* layout (`min-h-screen`) unchanged.
|
|
19
38
|
*/
|
|
20
39
|
|
|
21
40
|
import {
|
|
@@ -33,11 +52,12 @@ import {
|
|
|
33
52
|
import { localeHref } from "@working-theory/ui";
|
|
34
53
|
import { Link } from "@working-theory/ui/primitives";
|
|
35
54
|
import { createAuthClient } from "better-auth/react";
|
|
36
|
-
import { Inbox, Settings, Sparkles } from "lucide-react";
|
|
55
|
+
import { Inbox, Layers, Settings, Sparkles } from "lucide-react";
|
|
37
56
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
38
|
-
import type
|
|
57
|
+
import { useState, type ReactElement, type ReactNode } from "react";
|
|
39
58
|
|
|
40
59
|
import { ACCOUNT_PERSIST_KEY } from "./for-you-labels";
|
|
60
|
+
import { NewMenuSlotProvider } from "./new-menu";
|
|
41
61
|
|
|
42
62
|
const authClient = createAuthClient();
|
|
43
63
|
|
|
@@ -51,6 +71,9 @@ export interface AccountFrameLabels extends SignedInShellLabels {
|
|
|
51
71
|
/** The phone's For you tab label (#7469). */
|
|
52
72
|
forYou: string;
|
|
53
73
|
settings: string;
|
|
74
|
+
/** The rail's Library group heading (#8231 D1). */
|
|
75
|
+
library: string;
|
|
76
|
+
artifacts: string;
|
|
54
77
|
}
|
|
55
78
|
|
|
56
79
|
export interface AccountFrameProps {
|
|
@@ -77,14 +100,21 @@ export function AccountFrame({
|
|
|
77
100
|
const pathname = usePathname();
|
|
78
101
|
const searchParams = useSearchParams();
|
|
79
102
|
const [dismissed] = useForYouDismissed(ACCOUNT_PERSIST_KEY);
|
|
103
|
+
const [topBarActions, setTopBarActions] = useState<HTMLElement | null>(null);
|
|
80
104
|
|
|
81
105
|
const homeHref = localeHref(locale, "/");
|
|
82
106
|
const inboxHref = localeHref(locale, "/account");
|
|
107
|
+
const artifactsHref = localeHref(locale, "/account/artifacts");
|
|
108
|
+
const settingsHref = localeHref(locale, "/account/general");
|
|
109
|
+
const within = (href: string): boolean => pathname === href || pathname.startsWith(`${href}/`);
|
|
83
110
|
// The Inbox row stays selected on an item's own page (/account/inbox/…).
|
|
84
111
|
const onInbox = pathname === inboxHref || pathname.startsWith(`${inboxHref}/inbox/`);
|
|
112
|
+
const activePath = onInbox ? inboxHref : within(artifactsHref) ? artifactsHref : undefined;
|
|
85
113
|
// #7469 — the phone tab: For you only on the Inbox route itself.
|
|
86
114
|
const phoneTab =
|
|
87
115
|
pathname === inboxHref && searchParams?.get("tab") === FOR_YOU_TAB ? FOR_YOU_TAB : "inbox";
|
|
116
|
+
// #8233 D4 — the Inbox route's New lives in the top bar; nowhere else.
|
|
117
|
+
const onInboxRoute = pathname === inboxHref;
|
|
88
118
|
|
|
89
119
|
const onSignOut = (): void => {
|
|
90
120
|
void (async () => {
|
|
@@ -99,7 +129,15 @@ export function AccountFrame({
|
|
|
99
129
|
})();
|
|
100
130
|
};
|
|
101
131
|
|
|
102
|
-
const {
|
|
132
|
+
const {
|
|
133
|
+
home,
|
|
134
|
+
inbox,
|
|
135
|
+
forYou: forYouLabel,
|
|
136
|
+
settings,
|
|
137
|
+
library,
|
|
138
|
+
artifacts,
|
|
139
|
+
...shellLabels
|
|
140
|
+
} = labels;
|
|
103
141
|
|
|
104
142
|
// D13 — the logo only: no box, no name, no popover; it links home. The
|
|
105
143
|
// same link is the phone top bar's leading cell.
|
|
@@ -118,52 +156,78 @@ export function AccountFrame({
|
|
|
118
156
|
const forYouLeft = forYou.items.filter((item) => !dismissed.has(item.id));
|
|
119
157
|
|
|
120
158
|
return (
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
159
|
+
<div className="contents" data-ailk-shell="account" data-testid="account-shell">
|
|
160
|
+
<SignedInShell
|
|
161
|
+
persistKey={ACCOUNT_PERSIST_KEY}
|
|
162
|
+
locale={locale}
|
|
163
|
+
navItems={[{ labelKey: "inbox", label: inbox, path: "/account", icon: Inbox }]}
|
|
164
|
+
railGroups={[
|
|
165
|
+
{
|
|
166
|
+
key: "library",
|
|
167
|
+
heading: library,
|
|
168
|
+
items: [
|
|
169
|
+
{ labelKey: "artifacts", label: artifacts, path: "/account/artifacts", icon: Layers },
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
]}
|
|
173
|
+
tabItems={[
|
|
174
|
+
{ key: "inbox", label: inbox, href: inboxHref, icon: Inbox, isActive: onInbox && phoneTab === "inbox" },
|
|
175
|
+
{
|
|
176
|
+
key: FOR_YOU_TAB,
|
|
177
|
+
label: forYouLabel,
|
|
178
|
+
href: `${inboxHref}?tab=${FOR_YOU_TAB}`,
|
|
179
|
+
icon: Sparkles,
|
|
180
|
+
isActive: phoneTab === FOR_YOU_TAB,
|
|
181
|
+
},
|
|
182
|
+
]}
|
|
183
|
+
{...(activePath ? { activePath } : {})}
|
|
184
|
+
user={user}
|
|
185
|
+
userMenuItems={[
|
|
186
|
+
{
|
|
187
|
+
id: "settings",
|
|
188
|
+
label: settings,
|
|
189
|
+
icon: Settings,
|
|
190
|
+
href: settingsHref,
|
|
191
|
+
},
|
|
192
|
+
]}
|
|
193
|
+
onSignOut={onSignOut}
|
|
194
|
+
userDisplay="avatar"
|
|
195
|
+
accountMenuLayout="card"
|
|
196
|
+
helpItems={helpItems}
|
|
197
|
+
railHeader={logoLink}
|
|
198
|
+
topBarStart={logoLink}
|
|
199
|
+
{...(onInboxRoute
|
|
200
|
+
? {
|
|
201
|
+
topBarEnd: (
|
|
202
|
+
<span
|
|
203
|
+
ref={setTopBarActions}
|
|
204
|
+
className="flex items-center"
|
|
205
|
+
data-testid="account-top-bar-actions"
|
|
206
|
+
/>
|
|
207
|
+
),
|
|
208
|
+
}
|
|
209
|
+
: {})}
|
|
210
|
+
{...(forYouLeft.length > 0
|
|
211
|
+
? {
|
|
212
|
+
aside: (
|
|
213
|
+
<ForYouPanel
|
|
214
|
+
items={forYouLeft}
|
|
215
|
+
persistKey={ACCOUNT_PERSIST_KEY}
|
|
216
|
+
labels={forYou.labels}
|
|
217
|
+
locale={locale}
|
|
218
|
+
now={forYou.now}
|
|
219
|
+
/>
|
|
220
|
+
),
|
|
221
|
+
}
|
|
222
|
+
: {})}
|
|
223
|
+
labels={shellLabels}
|
|
224
|
+
>
|
|
225
|
+
<PhoneTabProvider tab={phoneTab}>
|
|
226
|
+
<NewMenuSlotProvider node={onInboxRoute ? topBarActions : null}>
|
|
227
|
+
{children}
|
|
228
|
+
</NewMenuSlotProvider>
|
|
229
|
+
</PhoneTabProvider>
|
|
230
|
+
</SignedInShell>
|
|
231
|
+
</div>
|
|
168
232
|
);
|
|
169
233
|
}
|
|
@@ -13,62 +13,41 @@
|
|
|
13
13
|
* navigation telemetry for a Server Action, which is inherent to any hosted-
|
|
14
14
|
* portal handoff, not something this design avoids).
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Where the portal comes from is `billing-source.ts`'s `billingPortalUrl()`
|
|
17
|
+
* (#8217, spec docs/specs/8217-spoke-billing-spoke-checkout-reads-spec.md
|
|
18
|
+
* D10, D11), the account tree's billing seam — this file tests no topology:
|
|
18
19
|
*
|
|
19
|
-
* -
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* (
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* (`lib/hub-session/session.ts`) sends the sealed session's credential as
|
|
29
|
-
* `Authorization: Bearer`, and `authPreHandler` skips its origin check for a
|
|
30
|
-
* bearer. This action does not complete on a spoke: the hub admits a hub
|
|
31
|
-
* credential on four read routes only (`HUB_SESSION_ROUTES`), so
|
|
32
|
-
* `POST /v1/subscription/portal-session` is a 404 there, and Settings hides
|
|
33
|
-
* the Billing button because `subscription.me()` is a 404 too.
|
|
20
|
+
* - A single site: the hub's `POST /v1/subscription/portal-session`, with
|
|
21
|
+
* the caller's cookie and real `Origin` forwarded, which `apps/api`'s
|
|
22
|
+
* `authPreHandler` checks against its trusted origins on this
|
|
23
|
+
* cookie-authenticated mutating request; the route accepts only a cookie
|
|
24
|
+
* session, never a bearer (D3.1/D5).
|
|
25
|
+
* - A spoke that signs in through its hub and carries commerce: the spoke's
|
|
26
|
+
* own Stripe key, for the customer its hub vouches for — read server-side
|
|
27
|
+
* by the rendered billing action, never from this action's input.
|
|
28
|
+
* - A spoke without that action: no portal, so nothing to redirect to.
|
|
34
29
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
30
|
+
* It takes no argument, so nothing a browser sends can name a customer.
|
|
31
|
+
* Next's Server Action origin check applies as before.
|
|
32
|
+
*
|
|
33
|
+
* `redirect()` throws `NEXT_REDIRECT` by design — called OUTSIDE any
|
|
34
|
+
* try/catch (mirrors `checkout.actions.ts`'s idiom) so a catch never
|
|
37
35
|
* swallows the navigation this action exists to perform.
|
|
38
36
|
*/
|
|
39
37
|
|
|
40
|
-
import { subscription } from "@working-theory/api-client";
|
|
41
38
|
import { redirect } from "next/navigation";
|
|
42
39
|
|
|
43
|
-
import {
|
|
44
|
-
|
|
45
|
-
/** PII-safe error identity for logs (the `checkout.actions.ts` idiom: `err.name` only). */
|
|
46
|
-
function errorName(err: unknown): string {
|
|
47
|
-
return err instanceof Error ? err.name : "unknown";
|
|
48
|
-
}
|
|
40
|
+
import { billingPortalUrl } from "./billing-source";
|
|
49
41
|
|
|
50
42
|
/**
|
|
51
43
|
* Opens the caller's own Stripe billing-portal session and redirects there.
|
|
52
|
-
* Never returns on success.
|
|
53
|
-
*
|
|
54
|
-
*
|
|
44
|
+
* Never returns on success. With no portal (a failed mint, logged PII-safe by
|
|
45
|
+
* `billingPortalUrl`, or none to open), it returns and the page stays as it
|
|
46
|
+
* is — there is no error UI slot on this button.
|
|
55
47
|
*/
|
|
56
48
|
export async function openBillingPortal(): Promise<void> {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const res = await subscription.portalSession(init);
|
|
62
|
-
if (!res.ok) {
|
|
63
|
-
console.error(`[openBillingPortal] status=${res.status}`);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
url = res.body.url;
|
|
67
|
-
} catch (err) {
|
|
68
|
-
console.error(`[openBillingPortal] errName=${errorName(err)}`);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Outside the try/catch on purpose — see the file header.
|
|
49
|
+
const url = await billingPortalUrl();
|
|
50
|
+
if (url === null) return;
|
|
51
|
+
// Outside any try/catch on purpose — see the file header.
|
|
73
52
|
redirect(url);
|
|
74
53
|
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /[locale]/account/artifacts/[pieceId] — one artifact (#8234, spec
|
|
3
|
+
* docs/specs/8234-member-artifacts-account-artifacts-and-spec.md D4).
|
|
4
|
+
*
|
|
5
|
+
* RSC, inside the account layout's SignedInShell, through the shared
|
|
6
|
+
* `InboxItemFrame`: back to Artifacts, the title, the review state, and —
|
|
7
|
+
* once approved — "Approved <date>". The body lists every served version in
|
|
8
|
+
* the read's order, newest first: its number, its delivery date, its note,
|
|
9
|
+
* the member's own review when there is one, and one action — Download for a
|
|
10
|
+
* file (re-served by `versions/[version]/file/route.ts`, never shown inline
|
|
11
|
+
* on this origin) or View for an https link, opened in a new tab. A link
|
|
12
|
+
* that is not https renders no action. The read serves no deliverer (8169
|
|
13
|
+
* D6), so none is shown.
|
|
14
|
+
*
|
|
15
|
+
* Inside the latest version only: the review form while it is reviewable
|
|
16
|
+
* (not approved, no review yet — `isReviewable`), else the waiting line once
|
|
17
|
+
* the member asked for changes; nothing once approved. Nothing limits how
|
|
18
|
+
* often: every new latest version is reviewable again (#8140 disposition 3).
|
|
19
|
+
*
|
|
20
|
+
* The artifact is found the way the Inbox item page finds its item: the
|
|
21
|
+
* artifacts read runs (deduped per request) and `[pieceId]` is matched raw or
|
|
22
|
+
* decoded. No session, no match or a 404 read is a not-found; any other
|
|
23
|
+
* failed read renders the error row with Retry (spec 7423 D12). Titles,
|
|
24
|
+
* notes and comments render as text only.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { InboxItemFrame } from "@working-theory/templates/account";
|
|
28
|
+
import { localeHref } from "@working-theory/ui";
|
|
29
|
+
import { Button, Heading, Link, Text } from "@working-theory/ui/primitives";
|
|
30
|
+
import {
|
|
31
|
+
isHttpsLink,
|
|
32
|
+
type MemberArtifact,
|
|
33
|
+
type MemberPieceVersion,
|
|
34
|
+
} from "@working-theory/validation";
|
|
35
|
+
import type { Metadata } from "next";
|
|
36
|
+
import { notFound } from "next/navigation";
|
|
37
|
+
import { getTranslations } from "next-intl/server";
|
|
38
|
+
import type { ReactElement } from "react";
|
|
39
|
+
|
|
40
|
+
import { accountSession, logReadFailure } from "../../account-data";
|
|
41
|
+
import { ErrorRow } from "../../error-row";
|
|
42
|
+
import {
|
|
43
|
+
artifactState,
|
|
44
|
+
isReviewable,
|
|
45
|
+
loadArtifacts,
|
|
46
|
+
STATE_KEYS,
|
|
47
|
+
versionFileHref,
|
|
48
|
+
} from "../artifact-data";
|
|
49
|
+
|
|
50
|
+
import { ReviewForm, type ReviewFormLabels } from "./review-form";
|
|
51
|
+
|
|
52
|
+
interface Props {
|
|
53
|
+
params: Promise<{ locale: string; pieceId: string }>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A segment, percent-decoded when it decodes — matched in both forms, so an
|
|
58
|
+
* id is found whether or not the framework already decoded it.
|
|
59
|
+
*/
|
|
60
|
+
function decodeSegment(segment: string): string {
|
|
61
|
+
try {
|
|
62
|
+
return decodeURIComponent(segment);
|
|
63
|
+
} catch {
|
|
64
|
+
return segment;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
69
|
+
const { locale } = await params;
|
|
70
|
+
const t = await getTranslations({ locale, namespace: "account" });
|
|
71
|
+
return { title: t("artifacts.title") };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default async function AccountArtifactPage({
|
|
75
|
+
params,
|
|
76
|
+
}: Props): Promise<ReactElement> {
|
|
77
|
+
const { locale, pieceId: segment } = await params;
|
|
78
|
+
const t = await getTranslations({ locale, namespace: "account" });
|
|
79
|
+
const session = await accountSession(locale);
|
|
80
|
+
if (!session) notFound();
|
|
81
|
+
|
|
82
|
+
let artifacts: MemberArtifact[];
|
|
83
|
+
try {
|
|
84
|
+
artifacts = await loadArtifacts(session.ctx);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
logReadFailure("artifacts", err);
|
|
87
|
+
return (
|
|
88
|
+
<div className="max-w-3xl py-8" data-testid="artifact">
|
|
89
|
+
<ErrorRow
|
|
90
|
+
message={t("artifacts.itemError")}
|
|
91
|
+
retryLabel={t("artifacts.retry")}
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const artifact = artifacts.find(
|
|
98
|
+
(candidate) =>
|
|
99
|
+
candidate.id === segment || candidate.id === decodeSegment(segment),
|
|
100
|
+
);
|
|
101
|
+
if (!artifact) notFound();
|
|
102
|
+
|
|
103
|
+
const date = (iso: string): string =>
|
|
104
|
+
new Intl.DateTimeFormat(locale, {
|
|
105
|
+
dateStyle: "medium",
|
|
106
|
+
timeZone: "UTC",
|
|
107
|
+
}).format(new Date(iso));
|
|
108
|
+
const state = artifactState(artifact);
|
|
109
|
+
const reviewable = isReviewable(artifact);
|
|
110
|
+
const latest = artifact.versions[0]!.version;
|
|
111
|
+
const labels: ReviewFormLabels = {
|
|
112
|
+
heading: t("artifacts.review.heading", { version: latest }),
|
|
113
|
+
comment: t("artifacts.review.comment"),
|
|
114
|
+
commentHint: t("artifacts.review.commentHint"),
|
|
115
|
+
approve: t("artifacts.review.approve"),
|
|
116
|
+
requestChanges: t("artifacts.review.requestChanges"),
|
|
117
|
+
submitting: t("artifacts.review.submitting"),
|
|
118
|
+
sent: t("artifacts.review.sent"),
|
|
119
|
+
errors: {
|
|
120
|
+
comment_required: t("artifacts.review.errors.commentRequired"),
|
|
121
|
+
closed: t("artifacts.review.errors.closed"),
|
|
122
|
+
not_latest: t("artifacts.review.errors.notLatest"),
|
|
123
|
+
rate_limited: t("artifacts.review.errors.rateLimited"),
|
|
124
|
+
failed: t("artifacts.review.errors.failed"),
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// The one action of a version: Download a file, View an https link, else none.
|
|
129
|
+
const versionAction = (v: MemberPieceVersion): ReactElement | null => {
|
|
130
|
+
if (v.file !== null) {
|
|
131
|
+
return (
|
|
132
|
+
<Button asChild variant="outline" className="max-md:min-h-11">
|
|
133
|
+
<Link
|
|
134
|
+
href={versionFileHref(locale, artifact.id, v.version)}
|
|
135
|
+
variant="bare"
|
|
136
|
+
data-testid={`artifact-version-${v.version}-download`}
|
|
137
|
+
>
|
|
138
|
+
{t("artifacts.download", { filename: v.file.filename })}
|
|
139
|
+
</Link>
|
|
140
|
+
</Button>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (isHttpsLink(v.link)) {
|
|
144
|
+
return (
|
|
145
|
+
<Button asChild variant="outline" className="max-md:min-h-11">
|
|
146
|
+
<Link
|
|
147
|
+
href={v.link}
|
|
148
|
+
external
|
|
149
|
+
variant="bare"
|
|
150
|
+
data-testid={`artifact-version-${v.version}-view`}
|
|
151
|
+
>
|
|
152
|
+
{t("artifacts.view")}
|
|
153
|
+
</Link>
|
|
154
|
+
</Button>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<InboxItemFrame
|
|
162
|
+
back={{
|
|
163
|
+
href: localeHref(locale, "/account/artifacts"),
|
|
164
|
+
label: t("artifacts.back"),
|
|
165
|
+
}}
|
|
166
|
+
caption={t("artifacts.caption")}
|
|
167
|
+
title={artifact.title}
|
|
168
|
+
status={t(STATE_KEYS[state])}
|
|
169
|
+
lead={
|
|
170
|
+
artifact.approvedAt !== null
|
|
171
|
+
? t("artifacts.approvedOn", { date: date(artifact.approvedAt) })
|
|
172
|
+
: null
|
|
173
|
+
}
|
|
174
|
+
testIdPrefix="artifact"
|
|
175
|
+
>
|
|
176
|
+
<section className="mt-block-loose">
|
|
177
|
+
<Heading level={2} visual={4} className="!text-body font-semibold">
|
|
178
|
+
{t("artifacts.versions")}
|
|
179
|
+
</Heading>
|
|
180
|
+
<div className="mt-block-tight flex flex-col gap-4">
|
|
181
|
+
{artifact.versions.map((v) => {
|
|
182
|
+
const action = versionAction(v);
|
|
183
|
+
return (
|
|
184
|
+
<div
|
|
185
|
+
key={v.version}
|
|
186
|
+
className="rounded-md border-hairline border-(--color-border) p-card"
|
|
187
|
+
data-testid={`artifact-version-${v.version}`}
|
|
188
|
+
>
|
|
189
|
+
<Text as="p" variant="body" className="font-semibold">
|
|
190
|
+
{t("artifacts.version", { version: v.version })}
|
|
191
|
+
</Text>
|
|
192
|
+
<Text as="p" variant="body-sm" tone="muted">
|
|
193
|
+
{t("artifacts.deliveredOn", { date: date(v.deliveredAt) })}
|
|
194
|
+
</Text>
|
|
195
|
+
{v.note !== null ? (
|
|
196
|
+
<Text
|
|
197
|
+
as="p"
|
|
198
|
+
variant="body-sm"
|
|
199
|
+
className="mt-2 whitespace-pre-line"
|
|
200
|
+
>
|
|
201
|
+
{v.note}
|
|
202
|
+
</Text>
|
|
203
|
+
) : null}
|
|
204
|
+
{v.review !== null && v.reviewedAt !== null ? (
|
|
205
|
+
<div className="mt-2">
|
|
206
|
+
<Text as="p" variant="body-sm" tone="muted">
|
|
207
|
+
{v.review === "approved"
|
|
208
|
+
? t("artifacts.reviewApproved", {
|
|
209
|
+
date: date(v.reviewedAt),
|
|
210
|
+
})
|
|
211
|
+
: t("artifacts.reviewChangesRequested", {
|
|
212
|
+
date: date(v.reviewedAt),
|
|
213
|
+
})}
|
|
214
|
+
</Text>
|
|
215
|
+
{v.reviewComment !== null ? (
|
|
216
|
+
<Text
|
|
217
|
+
as="p"
|
|
218
|
+
variant="body-sm"
|
|
219
|
+
className="whitespace-pre-line"
|
|
220
|
+
>
|
|
221
|
+
{v.reviewComment}
|
|
222
|
+
</Text>
|
|
223
|
+
) : null}
|
|
224
|
+
</div>
|
|
225
|
+
) : null}
|
|
226
|
+
{action !== null ? (
|
|
227
|
+
<div className="mt-block-tight">{action}</div>
|
|
228
|
+
) : null}
|
|
229
|
+
{v.version === latest && reviewable ? (
|
|
230
|
+
<ReviewForm
|
|
231
|
+
pieceId={artifact.id}
|
|
232
|
+
version={v.version}
|
|
233
|
+
labels={labels}
|
|
234
|
+
/>
|
|
235
|
+
) : null}
|
|
236
|
+
{v.version === latest && state === "changes-requested" ? (
|
|
237
|
+
<div
|
|
238
|
+
className="mt-block-tight"
|
|
239
|
+
data-testid="artifact-review-waiting"
|
|
240
|
+
>
|
|
241
|
+
<Text as="p" variant="body-sm">
|
|
242
|
+
{t("artifacts.review.waiting")}
|
|
243
|
+
</Text>
|
|
244
|
+
</div>
|
|
245
|
+
) : null}
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
})}
|
|
249
|
+
</div>
|
|
250
|
+
</section>
|
|
251
|
+
</InboxItemFrame>
|
|
252
|
+
);
|
|
253
|
+
}
|