@tokenoftrust/storefront-runner 1.3.2 → 1.3.4-rc.2

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.
Files changed (79) hide show
  1. package/apps/storefront/astro.config.mjs +59 -0
  2. package/apps/storefront/env.d.ts +24 -8
  3. package/apps/storefront/package.json +1 -1
  4. package/apps/storefront/src/components/admin/{AdminFoxyCartSettingsPanel.astro → AdminCheckoutSettingsPanel.astro} +50 -51
  5. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +2 -2
  6. package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +613 -0
  7. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +3 -3
  8. package/apps/storefront/src/components/chrome/SiteHeader.astro +3 -4
  9. package/apps/storefront/src/components/commerce/{FoxySidecartStyles.astro → CartDrawerStyles.astro} +13 -14
  10. package/apps/storefront/src/components/commerce/CheckoutLoader.astro +21 -0
  11. package/apps/storefront/src/components/islands/VariantSelector.tsx +101 -20
  12. package/apps/storefront/src/config/adminTargetTenant.ts +2 -2
  13. package/apps/storefront/src/config/devTenantSeed.ts +65 -16
  14. package/apps/storefront/src/config/resolver.ts +48 -1
  15. package/apps/storefront/src/layouts/Layout.astro +14 -12
  16. package/apps/storefront/src/lib/admin/README.md +2 -2
  17. package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +5 -5
  18. package/apps/storefront/src/lib/admin/orderClient.ts +20 -20
  19. package/apps/storefront/src/lib/admin/services/mock/{foxyCartSettings.ts → checkoutSettings.ts} +21 -22
  20. package/apps/storefront/src/lib/admin/services/mock/index.ts +3 -2
  21. package/apps/storefront/src/lib/admin/services/mock/settings.ts +1 -1
  22. package/apps/storefront/src/lib/admin/services/mock/subscriptions.ts +398 -0
  23. package/apps/storefront/src/lib/admin/services/orders.ts +21 -21
  24. package/apps/storefront/src/lib/admin/viewmodels/subscriptions.ts +106 -0
  25. package/apps/storefront/src/lib/auth/loginGate.ts +14 -3
  26. package/apps/storefront/src/lib/{foxyCommerce.ts → checkoutCommerce.ts} +20 -18
  27. package/apps/storefront/src/lib/commerce/{foxyCartSettings.ts → checkoutSettings.ts} +65 -66
  28. package/apps/storefront/src/lib/dev/activityIngestToken.ts +9 -0
  29. package/apps/storefront/src/lib/dev/activityStore.ts +12 -0
  30. package/apps/storefront/src/lib/dev/cliSignInCode.ts +23 -4
  31. package/apps/storefront/src/lib/dev/runtimeStore.ts +193 -22
  32. package/apps/storefront/src/lib/edgeCache.ts +7 -6
  33. package/apps/storefront/src/lib/health.ts +53 -0
  34. package/apps/storefront/src/lib/privacy/emailHint.ts +33 -0
  35. package/apps/storefront/src/lib/rawMarketingHtml.ts +1 -0
  36. package/apps/storefront/src/lib/sections.ts +19 -23
  37. package/apps/storefront/src/lib/securityHeaders.ts +1 -1
  38. package/apps/storefront/src/lib/storyblok/content-model.ts +3 -2
  39. package/apps/storefront/src/middleware/index.ts +29 -12
  40. package/apps/storefront/src/pages/[...slug].astro +5 -6
  41. package/apps/storefront/src/pages/admin.astro +10 -3
  42. package/apps/storefront/src/pages/api/admin/orders/[id].ts +4 -4
  43. package/apps/storefront/src/pages/api/admin/orders.ts +5 -5
  44. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +14 -7
  45. package/apps/storefront/src/pages/api/checkout-style.css.ts +5 -5
  46. package/apps/storefront/src/pages/auth/login.astro +4 -1
  47. package/apps/storefront/src/pages/auth/magic.astro +263 -43
  48. package/apps/storefront/src/pages/capabilities.astro +1 -1
  49. package/apps/storefront/src/pages/cockpit.astro +2365 -0
  50. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +19 -0
  51. package/apps/storefront/src/pages/index.astro +23 -29
  52. package/apps/storefront/src/pages/products/[handle].astro +50 -20
  53. package/package.json +6 -2
  54. package/packages/public-runtime/src/capabilities.ts +97 -9
  55. package/packages/public-runtime/src/{foxy-orders.ts → checkout-orders.ts} +28 -27
  56. package/packages/public-runtime/src/checkout-style-publish.ts +25 -25
  57. package/packages/public-runtime/src/checkout-style.ts +13 -13
  58. package/packages/public-runtime/src/{foxy.ts → checkout.ts} +111 -34
  59. package/packages/public-runtime/src/csp.ts +11 -11
  60. package/packages/public-runtime/src/customization-scripts.ts +28 -0
  61. package/packages/public-runtime/src/extension-contract.ts +73 -0
  62. package/packages/public-runtime/src/index.ts +4 -2
  63. package/packages/public-runtime/src/product.ts +57 -0
  64. package/packages/public-runtime/src/raw-html-policy.ts +35 -0
  65. package/packages/public-runtime/src/tenant.ts +14 -11
  66. package/pnpm-workspace.yaml +1 -0
  67. package/scripts/dev/checkout-watch.mjs +9 -0
  68. package/scripts/dev/file-browser.mjs +5 -1
  69. package/scripts/dev/transient-files.mjs +34 -0
  70. package/scripts/tot-dev.mjs +16 -3
  71. package/apps/storefront/src/components/commerce/FoxyLoader.astro +0 -23
  72. package/apps/storefront/src/pages/api/dev/activity.ts +0 -124
  73. package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +0 -73
  74. package/apps/storefront/src/pages/api/dev/diff.ts +0 -68
  75. package/apps/storefront/src/pages/api/dev/file.ts +0 -57
  76. package/apps/storefront/src/pages/api/dev/locate.ts +0 -125
  77. package/apps/storefront/src/pages/api/dev/publish.ts +0 -128
  78. package/apps/storefront/src/pages/api/dev/tree.ts +0 -70
  79. package/apps/storefront/src/pages/api/dev/write.ts +0 -116
@@ -9,33 +9,66 @@
9
9
  * the token before the user ever clicked. So: NEVER exchange on GET. The token rides
10
10
  * in a hidden form field and leaves only via the POST.
11
11
  */
12
+ import { normalizeEmailHint, redactEmailForHint } from "@/lib/privacy/emailHint";
13
+
12
14
  export const prerender = false;
13
15
 
14
16
  const cspNonce = Astro.locals.cspNonce;
15
17
  const url = new URL(Astro.request.url);
16
18
  const token = url.searchParams.get("token") ?? "";
17
- const email = (url.searchParams.get("email") ?? "").trim(); // display-only (never trusted)
19
+ const legacyEmail = (url.searchParams.get("email") ?? "").trim(); // legacy display hint only
20
+ const emailHint =
21
+ normalizeEmailHint(url.searchParams.get("email_hint")) ??
22
+ redactEmailForHint(legacyEmail) ??
23
+ "";
24
+ if (url.searchParams.has("email")) {
25
+ const canonical = new URLSearchParams(url.searchParams);
26
+ canonical.delete("email");
27
+ if (emailHint) canonical.set("email_hint", emailHint);
28
+ return Astro.redirect(
29
+ `/auth/magic${canonical.toString() ? `?${canonical.toString()}` : ""}`,
30
+ 302,
31
+ );
32
+ }
33
+ const tenant = (url.searchParams.get("tenant") ?? "").trim().toLowerCase();
34
+ // Destination context for the confirm card. appDomain always rides in `tenant`; the
35
+ // friendly title is optional. Display-only and bounded — Astro escapes on render, so
36
+ // this is XSS-safe; the cap just keeps a hostile/overlong value from breaking layout.
37
+ const appDomain = tenant;
38
+ const appTitle = (url.searchParams.get("app_title") ?? "").trim().slice(0, 80);
39
+ const appLabel = appTitle || appDomain; // primary destination name to show, if any
18
40
  const errorCode = url.searchParams.get("error");
41
+ const resendStatus = url.searchParams.get("resend");
19
42
  const next = (() => {
20
43
  const n = url.searchParams.get("next") ?? "/dashboard";
21
44
  return n.startsWith("/") && !n.startsWith("//") ? n : "/dashboard";
22
45
  })();
23
46
 
24
47
  const ERROR_MESSAGES: Record<string, string> = {
25
- invalid: "This sign-in link isn't valid or has already been used. Ask for a fresh invite.",
26
- expired: "This sign-in link has expired. Ask for a fresh invite.",
27
- used: "This sign-in link was already used. Ask for a fresh invite.",
48
+ invalid: "This sign-in link isn't valid or has already been used.",
49
+ expired: "This sign-in link has expired.",
50
+ used: "This sign-in link was already used.",
28
51
  missing: "This sign-in link is missing its token.",
29
52
  unconfigured: "Sign-in isn't available right now. Please try again shortly.",
30
- verify_failed: "We couldn't complete sign-in. Ask for a fresh invite and try again.",
53
+ verify_failed: "We couldn't complete sign-in.",
54
+ };
55
+ const RESEND_MESSAGES: Record<string, string> = {
56
+ invalid: "Enter the email address from your invite and try again.",
57
+ unavailable: "We couldn't resend a link right now. Please try again shortly.",
31
58
  };
32
59
  // On GET with a token → confirm card. On GET with ?error → error card. No token, no
33
60
  // error → treat as a broken link.
34
- const errorMessage = errorCode
35
- ? (ERROR_MESSAGES[errorCode] ?? "Something went wrong — ask for a fresh invite.")
36
- : token
37
- ? null
38
- : ERROR_MESSAGES.missing;
61
+ const resendMessage = resendStatus ? (RESEND_MESSAGES[resendStatus] ?? null) : null;
62
+ const errorMessage = resendStatus === "sent"
63
+ ? null
64
+ : resendMessage
65
+ ? resendMessage
66
+ : errorCode
67
+ ? (ERROR_MESSAGES[errorCode] ?? "Something went wrong.")
68
+ : token
69
+ ? null
70
+ : ERROR_MESSAGES.missing;
71
+ const showResendForm = !!errorMessage && !!tenant && resendStatus !== "sent";
39
72
  ---
40
73
 
41
74
  <html lang="en">
@@ -44,60 +77,247 @@ const errorMessage = errorCode
44
77
  <meta name="viewport" content="width=device-width, initial-scale=1" />
45
78
  <meta name="robots" content="noindex, nofollow" />
46
79
  <title>Confirm sign in — Regulated Storefront by Token of Trust©</title>
80
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
81
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
82
+ <link
83
+ rel="stylesheet"
84
+ href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@600;700;800&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@500;600&display=swap"
85
+ />
47
86
  <style nonce={cspNonce}>
48
- :root { color-scheme: light dark; }
87
+ /* Signed-in developer cockpit design system — the sign-in door matches the
88
+ room it opens into (apps/storefront/src/pages/cockpit.astro). Theme via the
89
+ token triple: :root default light, @media dark, and [data-theme] overrides. */
90
+ :root {
91
+ --bg: #f7f9fc; --surface: #ffffff;
92
+ --ink: #0a2540; --ink-2: #33415b; --muted: #55637a; --faint: #8593a8;
93
+ --accent: #0e9f8e; --accent-ink: #0b7a6d; --accent-wash: #e7f6f4; --accent-line: #b8e6df;
94
+ --border: #e3e8ef;
95
+ --warn: #c23b3b; --warn-wash: #fbecec; --warn-line: #f0cccc;
96
+ --sh: 0 10px 34px -14px rgba(10, 37, 64, .22);
97
+ --display: "Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
98
+ --body: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
99
+ --mono: "JetBrains Mono", ui-monospace, "SF Mono", "Cascadia Code", Menlo, monospace;
100
+ color-scheme: light;
101
+ }
102
+ @media (prefers-color-scheme: dark) {
103
+ :root {
104
+ --bg: #071320; --surface: #0e2134;
105
+ --ink: #eaf1f8; --ink-2: #bdccdc; --muted: #90a3b7; --faint: #6c8098;
106
+ --accent: #2fd4bf; --accent-ink: #7fe7da; --accent-wash: #0f2e30; --accent-line: #1e514b;
107
+ --border: #1b3348;
108
+ --warn: #f2857f; --warn-wash: #2a1414; --warn-line: #5a2626;
109
+ --sh: 0 18px 44px -22px rgba(0, 0, 0, .7);
110
+ color-scheme: dark;
111
+ }
112
+ }
113
+ :root[data-theme="dark"] {
114
+ --bg: #071320; --surface: #0e2134;
115
+ --ink: #eaf1f8; --ink-2: #bdccdc; --muted: #90a3b7; --faint: #6c8098;
116
+ --accent: #2fd4bf; --accent-ink: #7fe7da; --accent-wash: #0f2e30; --accent-line: #1e514b;
117
+ --border: #1b3348;
118
+ --warn: #f2857f; --warn-wash: #2a1414; --warn-line: #5a2626;
119
+ --sh: 0 18px 44px -22px rgba(0, 0, 0, .7);
120
+ color-scheme: dark;
121
+ }
122
+ :root[data-theme="light"] {
123
+ --bg: #f7f9fc; --surface: #ffffff;
124
+ --ink: #0a2540; --ink-2: #33415b; --muted: #55637a; --faint: #8593a8;
125
+ --accent: #0e9f8e; --accent-ink: #0b7a6d; --accent-wash: #e7f6f4; --accent-line: #b8e6df;
126
+ --border: #e3e8ef;
127
+ --warn: #c23b3b; --warn-wash: #fbecec; --warn-line: #f0cccc;
128
+ --sh: 0 10px 34px -14px rgba(10, 37, 64, .22);
129
+ color-scheme: light;
130
+ }
131
+
49
132
  * { box-sizing: border-box; }
50
133
  body {
51
- font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
52
134
  margin: 0; min-height: 100vh; display: grid; place-items: center;
53
- padding: 2rem 1.25rem; background: #f5f4f0; color: #1c1c1c;
135
+ padding: 2rem 1.25rem;
136
+ background:
137
+ radial-gradient(34rem 18rem at 92% -8%, var(--accent-wash) 0%, transparent 60%),
138
+ var(--bg);
139
+ background-repeat: no-repeat;
140
+ color: var(--ink-2); font-family: var(--body); font-size: 15px; line-height: 1.6;
141
+ letter-spacing: -.011em; -webkit-font-smoothing: antialiased;
54
142
  }
55
- @media (prefers-color-scheme: dark) { body { background: #14141a; color: #ececf1; } }
56
143
  .card {
57
- width: 100%; max-width: 26rem; background: #fff; border: 1px solid #e6e4de;
58
- border-radius: 14px; padding: 2rem 1.75rem; box-shadow: 0 6px 24px rgba(0,0,0,.06);
59
- }
60
- @media (prefers-color-scheme: dark) { .card { background: #1e1e28; border-color: #33333f; box-shadow: none; } }
61
- .eyebrow { font: 600 .68rem/1 ui-monospace, monospace; letter-spacing: .12em;
62
- text-transform: uppercase; color: #8a7f66; margin: 0 0 .75rem; }
63
- h1 { font-size: 1.4rem; letter-spacing: -.01em; margin: 0 0 .4rem; }
64
- .sub { color: #6a6a6a; margin: 0 0 1.5rem; font-size: .95rem; }
65
- @media (prefers-color-scheme: dark) { .sub { color: #a2a2ad; } }
66
- .who { font-weight: 600; }
67
- button {
68
- width: 100%; margin-top: .5rem; padding: .8rem 1rem; border: 0; border-radius: 9px;
69
- background: #1a1a1a; color: #fff; font-weight: 600; font-size: 1rem; cursor: pointer;
70
- }
71
- @media (prefers-color-scheme: dark) { button { background: #ececf1; color: #14141a; } }
72
- button:hover { opacity: .92; }
73
- .err { background: #fdecec; border: 1px solid #f5c2c2; color: #9a1c1c;
74
- border-radius: 9px; padding: .7rem .85rem; font-size: .9rem; margin: 0 0 1rem; }
75
- @media (prefers-color-scheme: dark) { .err { background: #3a1f22; border-color: #6b2f34; color: #f6b9b9; } }
76
- .note { font-size: .78rem; color: #9a9a9a; margin: 1.1rem 0 0; text-align: center; }
77
- a { color: inherit; }
144
+ width: 100%; max-width: 26rem; background: var(--surface);
145
+ border: 1px solid var(--accent-line); border-radius: 20px;
146
+ padding: 2.1rem 1.9rem 1.9rem; position: relative; overflow: hidden;
147
+ box-shadow: var(--sh); animation: rise .5s cubic-bezier(.2, .9, .3, 1) both;
148
+ }
149
+ .card::before {
150
+ content: ""; position: absolute; inset: 0 0 auto 0; height: 3px;
151
+ background: linear-gradient(90deg, var(--accent), var(--accent-line));
152
+ }
153
+ @keyframes rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
154
+
155
+ .mark {
156
+ width: 46px; height: 46px; border-radius: 13px; display: grid; place-items: center;
157
+ color: #fff; background: linear-gradient(150deg, var(--accent), var(--accent-ink));
158
+ box-shadow: 0 6px 18px -6px rgba(14, 159, 142, .75); position: relative;
159
+ }
160
+ .mark::after {
161
+ content: ""; position: absolute; inset: -5px; border-radius: 17px;
162
+ border: 1.5px solid var(--accent); opacity: 0; animation: halo 2.8s ease-in-out infinite;
163
+ }
164
+ @keyframes halo { 0%, 100% { opacity: 0; transform: scale(.96); } 50% { opacity: .5; transform: scale(1.04); } }
165
+ .mark svg { width: 24px; height: 24px; }
166
+
167
+ .eyebrow {
168
+ font-family: var(--mono); font-size: .66rem; font-weight: 600; text-transform: uppercase;
169
+ letter-spacing: .13em; color: var(--accent-ink); margin: 1.15rem 0 .55rem;
170
+ }
171
+ h1 {
172
+ font-family: var(--display); font-weight: 800; letter-spacing: -.025em; color: var(--ink);
173
+ font-size: 1.55rem; margin: 0 0 .5rem; text-wrap: balance;
174
+ }
175
+ .sub { color: var(--muted); font-size: .95rem; line-height: 1.55; margin: 0 0 1.1rem; }
176
+
177
+ /* identity chip — reassures which inbox this link was sent to. The hint is
178
+ display-only / never trusted; the token governs the sign-in. */
179
+ .ident {
180
+ display: flex; align-items: center; gap: .65rem; margin: 0 0 1.5rem;
181
+ padding: .55rem .7rem; border: 1px solid var(--border); border-radius: 12px; background: var(--bg);
182
+ }
183
+ .ident .ava {
184
+ width: 30px; height: 30px; border-radius: 50%; flex: none; display: grid; place-items: center;
185
+ font-family: var(--display); font-weight: 700; font-size: .85rem; color: #fff; text-transform: uppercase;
186
+ background: linear-gradient(150deg, var(--accent), var(--accent-ink));
187
+ }
188
+ .ident .ava svg { width: 15px; height: 15px; }
189
+ .ident-txt { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
190
+ .ident-lbl { font-family: var(--mono); font-size: .6rem; text-transform: uppercase; letter-spacing: .12em; color: var(--faint); }
191
+ .ident-val { font-size: .9rem; font-weight: 600; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
192
+ .ident-dom { font-family: var(--mono); font-size: .72rem; color: var(--faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
193
+
194
+ form { margin: 0; }
195
+ .cta {
196
+ width: 100%; display: inline-flex; align-items: center; justify-content: center; gap: .55rem;
197
+ font-family: var(--display); font-weight: 700; font-size: 1rem; color: #fff; background: var(--accent);
198
+ border: 0; border-radius: 999px; padding: .9rem 1.3rem; cursor: pointer;
199
+ box-shadow: 0 10px 26px -10px rgba(14, 159, 142, .85);
200
+ transition: transform .12s, background .12s, box-shadow .12s;
201
+ }
202
+ .cta:hover { transform: translateY(-1px); background: var(--accent-ink); box-shadow: 0 14px 30px -10px rgba(14, 159, 142, .95); }
203
+ .cta:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
204
+ .cta svg { width: 17px; height: 17px; }
205
+
206
+ .note {
207
+ display: flex; align-items: center; justify-content: center; gap: .4rem;
208
+ font-family: var(--mono); font-size: .74rem; color: var(--faint);
209
+ margin: 1.15rem 0 0; text-align: center;
210
+ }
211
+ .note svg { width: 12px; height: 12px; flex: none; opacity: .8; }
212
+
213
+ .err {
214
+ display: flex; gap: .55rem; align-items: flex-start; background: var(--warn-wash);
215
+ border: 1px solid var(--warn-line); color: var(--warn); border-radius: 11px;
216
+ padding: .75rem .9rem; font-size: .9rem; line-height: 1.5; margin: 0 0 .2rem;
217
+ }
218
+ .err svg { width: 16px; height: 16px; flex: none; margin-top: .1rem; }
219
+ .resend {
220
+ display: grid; gap: .75rem; margin: 1rem 0 0;
221
+ }
222
+ .field {
223
+ display: grid; gap: .35rem;
224
+ }
225
+ .hint { margin: -.2rem 0 0; color: var(--muted); font-size: .84rem; }
226
+ label {
227
+ font-family: var(--mono); font-size: .66rem; font-weight: 600; text-transform: uppercase;
228
+ letter-spacing: .12em; color: var(--faint);
229
+ }
230
+ input[type="email"] {
231
+ width: 100%; border: 1px solid var(--border); border-radius: 11px;
232
+ padding: .8rem .85rem; color: var(--ink); background: var(--bg);
233
+ font: 600 .95rem/1.35 var(--body);
234
+ }
235
+ input[type="email"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
236
+ .altrow { margin: 1rem 0 0; }
237
+ .altlink {
238
+ font-family: var(--display); font-weight: 600; font-size: .9rem;
239
+ color: var(--accent-ink); text-decoration: none;
240
+ }
241
+ .altlink:hover { text-decoration: underline; }
242
+ .altlink:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
243
+
244
+ @media (prefers-reduced-motion: reduce) {
245
+ .card, .mark::after { animation: none !important; }
246
+ .cta { transition: none; }
247
+ }
78
248
  </style>
79
249
  </head>
80
250
  <body>
81
251
  <main class="card">
252
+ <span class="mark" aria-hidden="true">
253
+ <svg viewBox="0 0 24 24" fill="none"><path d="M12 2.5 20 6v6c0 4.6-3.2 7.9-8 9.5-4.8-1.6-8-4.9-8-9.5V6l8-3.5Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="m8.5 12 2.4 2.4L15.8 9.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
254
+ </span>
82
255
  <p class="eyebrow">Token of Trust© · Regulated Storefront</p>
83
- {errorMessage ? (
256
+ {resendStatus === "sent" ? (
257
+ <>
258
+ <h1>Check your email</h1>
259
+ <p class="sub">If that address is still on this store's invite list, a fresh sign-in link is on the way.</p>
260
+ <p class="altrow"><a class="altlink" href="/auth/login?next=%2Fcockpit">Sign in with a code instead →</a></p>
261
+ </>
262
+ ) : errorMessage ? (
84
263
  <>
85
264
  <h1>Sign-in link problem</h1>
86
- <p class="err">{errorMessage}</p>
87
- <p class="note"><a href="/auth/login">Sign in with a code instead →</a></p>
265
+ <p class="err">
266
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/><path d="M12 7.5v5M12 16h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
267
+ <span>{errorMessage}</span>
268
+ </p>
269
+ {showResendForm ? (
270
+ <form class="resend" method="post" action="/api/grants/magic-resend">
271
+ <input type="hidden" name="tenant" value={tenant} />
272
+ <input type="hidden" name="next" value={next} />
273
+ <div class="field">
274
+ <label for="resend-email">Invite email</label>
275
+ <input id="resend-email" type="email" name="email" autocomplete="email" required />
276
+ </div>
277
+ {emailHint ? <p class="hint">Hint: {emailHint}</p> : null}
278
+ <button class="cta" type="submit">
279
+ Send a fresh link
280
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M4 6.5h16v11H4z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="m4.5 7 7.5 5.5L19.5 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
281
+ </button>
282
+ </form>
283
+ ) : (
284
+ <p class="altrow"><a class="altlink" href="/auth/login?next=%2Fcockpit">Sign in with a code instead →</a></p>
285
+ )}
88
286
  </>
89
287
  ) : (
90
288
  <>
91
289
  <h1>Confirm sign in</h1>
92
- <p class="sub">{email
93
- ? <>Continue as <span class="who">{email}</span> to the Regulated Storefront dashboard.</>
94
- : <>Continue to the Regulated Storefront dashboard.</>}</p>
290
+ <p class="sub">Confirm it's you to finish signing in.</p>
291
+ {appLabel ? (
292
+ <div class="ident">
293
+ <span class="ava" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="M4 9.5 5.2 5h13.6L20 9.5M4 9.5V19h16V9.5M4 9.5h16M9 19v-5h6v5" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg></span>
294
+ <span class="ident-txt">
295
+ <span class="ident-lbl">Signing in to</span>
296
+ <span class="ident-val" title={appLabel}>{appLabel}</span>
297
+ {appTitle && appDomain && appTitle !== appDomain ? <span class="ident-dom" title={appDomain}>{appDomain}</span> : null}
298
+ </span>
299
+ </div>
300
+ ) : null}
301
+ {emailHint ? (
302
+ <div class="ident">
303
+ <span class="ava" aria-hidden="true">{emailHint.slice(0, 1)}</span>
304
+ <span class="ident-txt" aria-label={`Invite sent to ${emailHint}`}><span class="ident-lbl">Invite sent to </span><span class="ident-val" title={emailHint}>{emailHint}</span></span>
305
+ </div>
306
+ ) : null}
95
307
  <form method="post" action="/api/auth/magic-exchange">
96
308
  <input type="hidden" name="token" value={token} />
309
+ <input type="hidden" name="tenant" value={tenant} />
310
+ <input type="hidden" name="email_hint" value={emailHint} />
97
311
  <input type="hidden" name="next" value={next} />
98
- <button type="submit">Confirm sign in</button>
312
+ <button class="cta" type="submit">
313
+ Confirm sign in
314
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
315
+ </button>
99
316
  </form>
100
- <p class="note">This is a single-use link — confirming completes your sign-in.</p>
317
+ <p class="note">
318
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="5" y="11" width="14" height="9" rx="2" stroke="currentColor" stroke-width="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3" stroke="currentColor" stroke-width="2"/></svg>
319
+ This is a single-use link — confirming completes your sign-in.
320
+ </p>
101
321
  </>
102
322
  )}
103
323
  </main>
@@ -63,7 +63,7 @@ interface CapMeta {
63
63
  track: string;
64
64
  }
65
65
  const CAPS: CapMeta[] = [
66
- { key: "cartCheckout", name: "Cart & Checkout", sub: "FoxyCart · Track A", track: "commerce" },
66
+ { key: "cartCheckout", name: "Cart & Checkout", sub: "Managed checkout · Track A", track: "commerce" },
67
67
  { key: "ageVerification", name: "Age & ID Verification", sub: "Token of Trust · 21+", track: "compliance" },
68
68
  { key: "exciseTax", name: "Excise Tax Collection", sub: "at checkout · Track B", track: "compliance" },
69
69
  ];