@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.
- package/apps/storefront/astro.config.mjs +59 -0
- package/apps/storefront/env.d.ts +24 -8
- package/apps/storefront/package.json +1 -1
- package/apps/storefront/src/components/admin/{AdminFoxyCartSettingsPanel.astro → AdminCheckoutSettingsPanel.astro} +50 -51
- package/apps/storefront/src/components/admin/AdminSettingsTab.astro +2 -2
- package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +613 -0
- package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +3 -3
- package/apps/storefront/src/components/chrome/SiteHeader.astro +3 -4
- package/apps/storefront/src/components/commerce/{FoxySidecartStyles.astro → CartDrawerStyles.astro} +13 -14
- package/apps/storefront/src/components/commerce/CheckoutLoader.astro +21 -0
- package/apps/storefront/src/components/islands/VariantSelector.tsx +101 -20
- package/apps/storefront/src/config/adminTargetTenant.ts +2 -2
- package/apps/storefront/src/config/devTenantSeed.ts +65 -16
- package/apps/storefront/src/config/resolver.ts +48 -1
- package/apps/storefront/src/layouts/Layout.astro +14 -12
- package/apps/storefront/src/lib/admin/README.md +2 -2
- package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +5 -5
- package/apps/storefront/src/lib/admin/orderClient.ts +20 -20
- package/apps/storefront/src/lib/admin/services/mock/{foxyCartSettings.ts → checkoutSettings.ts} +21 -22
- package/apps/storefront/src/lib/admin/services/mock/index.ts +3 -2
- package/apps/storefront/src/lib/admin/services/mock/settings.ts +1 -1
- package/apps/storefront/src/lib/admin/services/mock/subscriptions.ts +398 -0
- package/apps/storefront/src/lib/admin/services/orders.ts +21 -21
- package/apps/storefront/src/lib/admin/viewmodels/subscriptions.ts +106 -0
- package/apps/storefront/src/lib/auth/loginGate.ts +14 -3
- package/apps/storefront/src/lib/{foxyCommerce.ts → checkoutCommerce.ts} +20 -18
- package/apps/storefront/src/lib/commerce/{foxyCartSettings.ts → checkoutSettings.ts} +65 -66
- package/apps/storefront/src/lib/dev/activityIngestToken.ts +9 -0
- package/apps/storefront/src/lib/dev/activityStore.ts +12 -0
- package/apps/storefront/src/lib/dev/cliSignInCode.ts +23 -4
- package/apps/storefront/src/lib/dev/runtimeStore.ts +193 -22
- package/apps/storefront/src/lib/edgeCache.ts +7 -6
- package/apps/storefront/src/lib/health.ts +53 -0
- package/apps/storefront/src/lib/privacy/emailHint.ts +33 -0
- package/apps/storefront/src/lib/rawMarketingHtml.ts +1 -0
- package/apps/storefront/src/lib/sections.ts +19 -23
- package/apps/storefront/src/lib/securityHeaders.ts +1 -1
- package/apps/storefront/src/lib/storyblok/content-model.ts +3 -2
- package/apps/storefront/src/middleware/index.ts +29 -12
- package/apps/storefront/src/pages/[...slug].astro +5 -6
- package/apps/storefront/src/pages/admin.astro +10 -3
- package/apps/storefront/src/pages/api/admin/orders/[id].ts +4 -4
- package/apps/storefront/src/pages/api/admin/orders.ts +5 -5
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +14 -7
- package/apps/storefront/src/pages/api/checkout-style.css.ts +5 -5
- package/apps/storefront/src/pages/auth/login.astro +4 -1
- package/apps/storefront/src/pages/auth/magic.astro +263 -43
- package/apps/storefront/src/pages/capabilities.astro +1 -1
- package/apps/storefront/src/pages/cockpit.astro +2365 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +19 -0
- package/apps/storefront/src/pages/index.astro +23 -29
- package/apps/storefront/src/pages/products/[handle].astro +50 -20
- package/package.json +6 -2
- package/packages/public-runtime/src/capabilities.ts +97 -9
- package/packages/public-runtime/src/{foxy-orders.ts → checkout-orders.ts} +28 -27
- package/packages/public-runtime/src/checkout-style-publish.ts +25 -25
- package/packages/public-runtime/src/checkout-style.ts +13 -13
- package/packages/public-runtime/src/{foxy.ts → checkout.ts} +111 -34
- package/packages/public-runtime/src/csp.ts +11 -11
- package/packages/public-runtime/src/customization-scripts.ts +28 -0
- package/packages/public-runtime/src/extension-contract.ts +73 -0
- package/packages/public-runtime/src/index.ts +4 -2
- package/packages/public-runtime/src/product.ts +57 -0
- package/packages/public-runtime/src/raw-html-policy.ts +35 -0
- package/packages/public-runtime/src/tenant.ts +14 -11
- package/pnpm-workspace.yaml +1 -0
- package/scripts/dev/checkout-watch.mjs +9 -0
- package/scripts/dev/file-browser.mjs +5 -1
- package/scripts/dev/transient-files.mjs +34 -0
- package/scripts/tot-dev.mjs +16 -3
- package/apps/storefront/src/components/commerce/FoxyLoader.astro +0 -23
- package/apps/storefront/src/pages/api/dev/activity.ts +0 -124
- package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +0 -73
- package/apps/storefront/src/pages/api/dev/diff.ts +0 -68
- package/apps/storefront/src/pages/api/dev/file.ts +0 -57
- package/apps/storefront/src/pages/api/dev/locate.ts +0 -125
- package/apps/storefront/src/pages/api/dev/publish.ts +0 -128
- package/apps/storefront/src/pages/api/dev/tree.ts +0 -70
- package/apps/storefront/src/pages/api/dev/write.ts +0 -116
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* REAL orders view model (card 8585 / U3) — replaces the deterministic mock as
|
|
3
|
-
* the Orders tab's data source. Fetches the tenant's orders
|
|
4
|
-
*
|
|
3
|
+
* the Orders tab's data source. Fetches the tenant's checkout orders via U2's
|
|
4
|
+
* order client and maps the frozen DTO (`@tot/public-runtime` checkout-orders)
|
|
5
5
|
* into the EXISTING `OrdersViewModel` shape, so `OrdersWorkspace.astro` renders
|
|
6
6
|
* unchanged. Read-only (scope-locks) — no write paths.
|
|
7
7
|
*
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
* `verificationDetail` slots — no shape change needed.
|
|
20
20
|
*/
|
|
21
21
|
import type {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
CheckoutOrderDetail,
|
|
23
|
+
CheckoutOrderListFilters,
|
|
24
|
+
CheckoutOrderPage,
|
|
25
|
+
CheckoutOrderSummary,
|
|
26
|
+
CheckoutOrderVerification,
|
|
27
27
|
} from "@tot/public-runtime";
|
|
28
28
|
import type {
|
|
29
29
|
OrderDetail,
|
|
@@ -34,7 +34,7 @@ import type {
|
|
|
34
34
|
OrderShipState,
|
|
35
35
|
OrdersViewModel,
|
|
36
36
|
} from "../viewmodels/orders";
|
|
37
|
-
import type {
|
|
37
|
+
import type { CheckoutOrderClient } from "../orderClient";
|
|
38
38
|
|
|
39
39
|
/** Next-step label, mirroring the mock's queue semantics. */
|
|
40
40
|
function deriveNextStep(fulfillment: OrderShipState, blocker: string | null): string {
|
|
@@ -47,7 +47,7 @@ function deriveNextStep(fulfillment: OrderShipState, blocker: string | null): st
|
|
|
47
47
|
|
|
48
48
|
/** Provisional risk until U4 fills `verification`. Prefer a real riskLevel when present. */
|
|
49
49
|
function deriveRisk(
|
|
50
|
-
verification:
|
|
50
|
+
verification: CheckoutOrderVerification | null,
|
|
51
51
|
blocker: string | null,
|
|
52
52
|
): OrderRow["riskLevel"] {
|
|
53
53
|
const r = verification?.riskLevel;
|
|
@@ -57,7 +57,7 @@ function deriveRisk(
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/** Map a DTO summary to the view model's OrderRow. */
|
|
60
|
-
export function mapSummaryToRow(s:
|
|
60
|
+
export function mapSummaryToRow(s: CheckoutOrderSummary): OrderRow {
|
|
61
61
|
return {
|
|
62
62
|
id: s.id,
|
|
63
63
|
placedAt: s.placedAt,
|
|
@@ -76,7 +76,7 @@ export function mapSummaryToRow(s: FoxyOrderSummary): OrderRow {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/** Commerce-derived readiness checks (U4 enriches the identity dimension). */
|
|
79
|
-
function deriveReadinessChecks(d:
|
|
79
|
+
function deriveReadinessChecks(d: CheckoutOrderDetail): OrderReadinessCheck[] {
|
|
80
80
|
const ageHold = d.blocker === "Age verification hold";
|
|
81
81
|
return [
|
|
82
82
|
{
|
|
@@ -119,7 +119,7 @@ function deriveReadinessChecks(d: FoxyOrderDetail): OrderReadinessCheck[] {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
/** Base commerce evidence events (U4 replaces with the real verification timeline). */
|
|
122
|
-
function deriveEvidenceTimeline(d:
|
|
122
|
+
function deriveEvidenceTimeline(d: CheckoutOrderDetail): OrderEvidenceEvent[] {
|
|
123
123
|
const events: OrderEvidenceEvent[] = [
|
|
124
124
|
{ title: "Order imported", detail: "Storefront order became available in the admin queue.", at: d.placedAt },
|
|
125
125
|
{ title: "Payment authorized", detail: "The customer completed checkout and payment was authorized.", at: d.placedAt },
|
|
@@ -131,7 +131,7 @@ function deriveEvidenceTimeline(d: FoxyOrderDetail): OrderEvidenceEvent[] {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
/** Map a DTO detail (its summary already mapped to `row`) to the view model's OrderDetail. */
|
|
134
|
-
export function mapDetailToDetail(d:
|
|
134
|
+
export function mapDetailToDetail(d: CheckoutOrderDetail, row: OrderRow): OrderDetail {
|
|
135
135
|
return {
|
|
136
136
|
...row,
|
|
137
137
|
lineItems: d.lineItems.map((li) => ({
|
|
@@ -168,8 +168,8 @@ function buildQueues(rows: OrderRow[]): OrderQueueSummary[] {
|
|
|
168
168
|
*/
|
|
169
169
|
export function mapOrdersToViewModel(
|
|
170
170
|
targetTenant: string,
|
|
171
|
-
summaries:
|
|
172
|
-
details: Record<string,
|
|
171
|
+
summaries: CheckoutOrderSummary[],
|
|
172
|
+
details: Record<string, CheckoutOrderDetail>,
|
|
173
173
|
): OrdersViewModel {
|
|
174
174
|
const rows = summaries.map(mapSummaryToRow);
|
|
175
175
|
|
|
@@ -214,9 +214,9 @@ export function emptyOrdersViewModel(targetTenant: string): OrdersViewModel {
|
|
|
214
214
|
|
|
215
215
|
export interface GetOrdersViewModelOptions {
|
|
216
216
|
targetTenant: string;
|
|
217
|
-
client:
|
|
217
|
+
client: CheckoutOrderClient;
|
|
218
218
|
/** List filters (queue/status/pagination); passed through to the order client. */
|
|
219
|
-
filters?: Parameters<
|
|
219
|
+
filters?: Parameters<CheckoutOrderClient["listOrders"]>[0];
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
/**
|
|
@@ -236,7 +236,7 @@ export type OrdersLoadStatus = "loaded" | "empty" | "error" | "not_configured";
|
|
|
236
236
|
export interface OrdersLoadResult {
|
|
237
237
|
status: OrdersLoadStatus;
|
|
238
238
|
vm: OrdersViewModel;
|
|
239
|
-
page:
|
|
239
|
+
page: CheckoutOrderPage | null;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
/**
|
|
@@ -245,8 +245,8 @@ export interface OrdersLoadResult {
|
|
|
245
245
|
* `payment`/search are NOT here — they are client-side refinements (the DTO list
|
|
246
246
|
* API has no payment filter, per contract §9).
|
|
247
247
|
*/
|
|
248
|
-
export function parseOrderFilters(params: URLSearchParams):
|
|
249
|
-
const f:
|
|
248
|
+
export function parseOrderFilters(params: URLSearchParams): CheckoutOrderListFilters {
|
|
249
|
+
const f: CheckoutOrderListFilters = {};
|
|
250
250
|
const queue = params.get("queue");
|
|
251
251
|
if (queue && queue !== "all") f.queue = queue;
|
|
252
252
|
const status = params.get("status");
|
|
@@ -272,7 +272,7 @@ export async function getOrdersViewModelResult(
|
|
|
272
272
|
): Promise<OrdersLoadResult> {
|
|
273
273
|
try {
|
|
274
274
|
const { orders, page } = await opts.client.listOrders(opts.filters);
|
|
275
|
-
const details: Record<string,
|
|
275
|
+
const details: Record<string, CheckoutOrderDetail> = {};
|
|
276
276
|
await Promise.all(
|
|
277
277
|
orders.map(async (o) => {
|
|
278
278
|
try {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscriptions view model (read-only v1). The source of truth is catalog
|
|
3
|
+
* purchase options; active lifecycle actions require a later subscription API
|
|
4
|
+
* and audited mutation gates.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type SubscriptionProgramReadiness = "ready" | "needs_setup" | "blocked";
|
|
8
|
+
|
|
9
|
+
export type SubscriptionAttentionKind =
|
|
10
|
+
| "setup_gap"
|
|
11
|
+
| "verification_hold"
|
|
12
|
+
| "payment_attention"
|
|
13
|
+
| "product_blocker";
|
|
14
|
+
|
|
15
|
+
export type SubscriptionSubscriberStatus =
|
|
16
|
+
| "active"
|
|
17
|
+
| "paused"
|
|
18
|
+
| "payment_attention"
|
|
19
|
+
| "verification_hold";
|
|
20
|
+
|
|
21
|
+
export type SubscriptionRenewalState = "scheduled" | "attention" | "held";
|
|
22
|
+
|
|
23
|
+
export interface SubscriptionSummary {
|
|
24
|
+
subscriptionProducts: number;
|
|
25
|
+
activeSubscribers: number;
|
|
26
|
+
upcomingRenewalValue: number;
|
|
27
|
+
needsAttention: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SubscriptionCheckoutField {
|
|
31
|
+
name: string;
|
|
32
|
+
value: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SubscriptionSubscriberRow {
|
|
36
|
+
id: string;
|
|
37
|
+
programId: string;
|
|
38
|
+
customerId: string;
|
|
39
|
+
customerName: string;
|
|
40
|
+
email: string;
|
|
41
|
+
status: SubscriptionSubscriberStatus;
|
|
42
|
+
verificationState: "verified" | "pending" | "blocked" | "not_required";
|
|
43
|
+
paymentState: "current" | "attention";
|
|
44
|
+
nextRenewalAt: string;
|
|
45
|
+
renewalValue: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SubscriptionAttentionRow {
|
|
49
|
+
id: string;
|
|
50
|
+
kind: SubscriptionAttentionKind;
|
|
51
|
+
programId: string;
|
|
52
|
+
productId: string;
|
|
53
|
+
customerId: string | null;
|
|
54
|
+
label: string;
|
|
55
|
+
detail: string;
|
|
56
|
+
action: string;
|
|
57
|
+
tone: "amber" | "red" | "blue";
|
|
58
|
+
blocking: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SubscriptionRenewalEvent {
|
|
62
|
+
id: string;
|
|
63
|
+
customerId: string;
|
|
64
|
+
customerName: string;
|
|
65
|
+
at: string;
|
|
66
|
+
amount: number;
|
|
67
|
+
state: SubscriptionRenewalState;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface SubscriptionEvidenceEvent {
|
|
71
|
+
title: string;
|
|
72
|
+
detail: string;
|
|
73
|
+
at: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SubscriptionProgramRow {
|
|
77
|
+
id: string;
|
|
78
|
+
productId: string;
|
|
79
|
+
productName: string;
|
|
80
|
+
optionId: string;
|
|
81
|
+
optionLabel: string;
|
|
82
|
+
frequency: string;
|
|
83
|
+
savingsLabel: string | null;
|
|
84
|
+
priceAdjustmentLabel: string | null;
|
|
85
|
+
readiness: SubscriptionProgramReadiness;
|
|
86
|
+
readinessDetail: string;
|
|
87
|
+
subscriberCount: number;
|
|
88
|
+
upcomingRenewalValue: number;
|
|
89
|
+
needsAttentionCount: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface SubscriptionProgramDetail extends SubscriptionProgramRow {
|
|
93
|
+
checkoutFields: SubscriptionCheckoutField[];
|
|
94
|
+
subscribers: SubscriptionSubscriberRow[];
|
|
95
|
+
upcomingRenewals: SubscriptionRenewalEvent[];
|
|
96
|
+
evidenceTimeline: SubscriptionEvidenceEvent[];
|
|
97
|
+
attention: SubscriptionAttentionRow[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface SubscriptionsViewModel {
|
|
101
|
+
targetTenant: string;
|
|
102
|
+
summary: SubscriptionSummary;
|
|
103
|
+
programs: SubscriptionProgramRow[];
|
|
104
|
+
detailsById: Record<string, SubscriptionProgramDetail>;
|
|
105
|
+
attention: SubscriptionAttentionRow[];
|
|
106
|
+
}
|
|
@@ -64,11 +64,22 @@ export function parseGateMode(raw: string | null | undefined): GateMode {
|
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Paths that MUST stay reachable without a session, or the visitor could never
|
|
67
|
-
* authenticate: the login page
|
|
68
|
-
*
|
|
67
|
+
* authenticate: the login page, the GET-safe magic-link confirm landing, the
|
|
68
|
+
* send/verify endpoints, and the exact unauthenticated magic-link resend endpoint.
|
|
69
|
+
* Everything else on a login-gated host requires a session.
|
|
70
|
+
*
|
|
71
|
+
* `/auth/magic` is a PRE-auth entry point — an invitee arrives here with no session
|
|
72
|
+
* to confirm sign-in. Gating it hands the invitee a "Coming soon" holding page and
|
|
73
|
+
* dead-ends the onboarding flow (the exact bug when a magic link lands on a gated
|
|
74
|
+
* host). `/api/auth/magic-exchange` is already covered by the `/api/auth/` prefix.
|
|
69
75
|
*/
|
|
70
76
|
export function isAuthPath(pathname: string): boolean {
|
|
71
|
-
return
|
|
77
|
+
return (
|
|
78
|
+
pathname === "/auth/login" ||
|
|
79
|
+
pathname === "/auth/magic" ||
|
|
80
|
+
pathname.startsWith("/api/auth/") ||
|
|
81
|
+
pathname === "/api/grants/magic-resend"
|
|
82
|
+
);
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
/**
|
|
@@ -1,39 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Request-scoped
|
|
2
|
+
* Request-scoped checkout commerce resolution (Track A).
|
|
3
3
|
*
|
|
4
|
-
* Bridges the tenant's static `commerce.
|
|
4
|
+
* Bridges the tenant's static `commerce.checkout` config with runtime env so the
|
|
5
5
|
* demo store can be swapped without a code change and the signing secret stays
|
|
6
|
-
* out of source
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* out of source. The env binding names intentionally remain `FOXY_*` because
|
|
7
|
+
* existing internal developer secrets and provider setup use those operational
|
|
8
|
+
* names:
|
|
9
|
+
* - `FOXY_STORE_SUBDOMAIN` overrides the tenant's default subdomain (one
|
|
10
|
+
* env var points every commerce tenant at our shared demo store).
|
|
9
11
|
* - `FOXY_CART_SECRET` is the cart-validation secret used only in `signed`
|
|
10
12
|
* mode; the `unsigned-demo` mode never reads it (validation is OFF), which
|
|
11
13
|
* is what lets a developer run the whole cart locally with zero secrets.
|
|
12
14
|
*/
|
|
13
|
-
import type { TenantConfig,
|
|
14
|
-
import {
|
|
15
|
+
import type { TenantConfig, ResolvedManagedCheckout } from "@tot/public-runtime";
|
|
16
|
+
import { resolveManagedCheckout } from "@tot/public-runtime";
|
|
15
17
|
import { readEnv } from "@/lib/env";
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
|
-
* Resolve the effective
|
|
19
|
-
* no store configured. Applies the
|
|
20
|
+
* Resolve the effective checkout facts for a tenant, or null when the tenant has
|
|
21
|
+
* no store configured. Applies the checkout env overrides.
|
|
20
22
|
*/
|
|
21
23
|
export async function resolveTenantCommerce(
|
|
22
24
|
tenant: TenantConfig,
|
|
23
|
-
): Promise<
|
|
24
|
-
const
|
|
25
|
-
if (!
|
|
25
|
+
): Promise<ResolvedManagedCheckout | null> {
|
|
26
|
+
const checkout = tenant.commerce?.checkout;
|
|
27
|
+
if (!checkout) return null;
|
|
26
28
|
// Per-environment overrides so the SAME tenant can point at different stores /
|
|
27
|
-
// checkout hosts by env: FOXY_STORE_SUBDOMAIN picks the
|
|
28
|
-
//
|
|
29
|
+
// checkout hosts by env: FOXY_STORE_SUBDOMAIN picks the checkout store;
|
|
30
|
+
// FOXY_STORE_DOMAIN sets a branded custom checkout domain (e.g. checkout.preview.
|
|
29
31
|
// tokenoftrust.com on the shared test env, secure.<merchant>.com on live).
|
|
30
32
|
const subOverride = await readEnv("FOXY_STORE_SUBDOMAIN");
|
|
31
33
|
const domainOverride = await readEnv("FOXY_STORE_DOMAIN");
|
|
32
34
|
const storeSubDomain =
|
|
33
|
-
subOverride && subOverride.length ? subOverride :
|
|
35
|
+
subOverride && subOverride.length ? subOverride : checkout.storeSubDomain;
|
|
34
36
|
const storeDomain =
|
|
35
|
-
domainOverride && domainOverride.length ? domainOverride :
|
|
36
|
-
return
|
|
37
|
+
domainOverride && domainOverride.length ? domainOverride : checkout.storeDomain;
|
|
38
|
+
return resolveManagedCheckout({ ...checkout, storeSubDomain, storeDomain });
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
/**
|
|
@@ -42,7 +44,7 @@ export async function resolveTenantCommerce(
|
|
|
42
44
|
* no secret is a misconfiguration the caller must handle (fall back to no cart).
|
|
43
45
|
*/
|
|
44
46
|
export async function readCartSecret(
|
|
45
|
-
commerce:
|
|
47
|
+
commerce: ResolvedManagedCheckout,
|
|
46
48
|
): Promise<string | undefined> {
|
|
47
49
|
if (commerce.mode !== "signed") return undefined;
|
|
48
50
|
return readEnv("FOXY_CART_SECRET");
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Managed checkout settings model — the admin "Cart & Checkout appearance" section.
|
|
3
3
|
*
|
|
4
|
-
* Mirrors the settings surface that the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Mirrors the settings surface that the commerce service actually manages, and
|
|
5
|
+
* defines how each field is PREPOPULATED from the tenant's theme (the
|
|
6
|
+
* style-guide), OVERRIDDEN, and RESET.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* 1. `colors` —
|
|
10
|
-
*
|
|
11
|
-
* Foxy model; documented in tot-foxycart, applied via the theme.
|
|
8
|
+
* The remote settings are three groups:
|
|
9
|
+
* 1. `colors` — provider-native brand colors (primary/secondary/tertiary +
|
|
10
|
+
* usage), applied via the hosted checkout theme.
|
|
12
11
|
* 2. `templates`— per-template-type `contentUrl` pointers (checkout / cart /
|
|
13
|
-
* cart_include[
|
|
14
|
-
*
|
|
12
|
+
* cart_include[drawer] / receipt / email), set and refreshed
|
|
13
|
+
* through the commerce service.
|
|
15
14
|
* The colors/fonts/logo live INSIDE the hosted template file.
|
|
16
15
|
* 3. `store` — storeSubDomain + loader feature toggles (excise tax, age/ID
|
|
17
16
|
* verification). Non-visual; no theme default.
|
|
@@ -20,21 +19,21 @@
|
|
|
20
19
|
* the template pointers (defaulted to a ToT-hosted, theme-derived template URL).
|
|
21
20
|
* The feature toggles are mirrored as plain config (no default → not resettable).
|
|
22
21
|
*
|
|
23
|
-
* The ACTUAL persistence — PATCHing
|
|
24
|
-
* URLs — is the
|
|
25
|
-
*
|
|
22
|
+
* The ACTUAL persistence — PATCHing provider colors + PUT/refresh of template
|
|
23
|
+
* URLs — is the commerce-service seam, intentionally NOT wired here; this module
|
|
24
|
+
* is the pure model the admin renders and edits.
|
|
26
25
|
*/
|
|
27
|
-
import type {
|
|
26
|
+
import type { ResolvedManagedCheckout, ThemeTokens } from "@tot/public-runtime";
|
|
28
27
|
|
|
29
|
-
/**
|
|
30
|
-
export type
|
|
28
|
+
/** Checkout template types exposed by the commerce service. */
|
|
29
|
+
export type CheckoutTemplateType =
|
|
31
30
|
| "checkout"
|
|
32
31
|
| "cart"
|
|
33
32
|
| "cart_include"
|
|
34
33
|
| "receipt"
|
|
35
34
|
| "email";
|
|
36
35
|
|
|
37
|
-
export const
|
|
36
|
+
export const CHECKOUT_TEMPLATE_TYPES: readonly CheckoutTemplateType[] = [
|
|
38
37
|
"checkout",
|
|
39
38
|
"cart",
|
|
40
39
|
"cart_include",
|
|
@@ -42,60 +41,60 @@ export const FOXY_TEMPLATE_TYPES: readonly FoxyTemplateType[] = [
|
|
|
42
41
|
"email",
|
|
43
42
|
] as const;
|
|
44
43
|
|
|
45
|
-
export const
|
|
44
|
+
export const CHECKOUT_TEMPLATE_LABELS: Record<CheckoutTemplateType, string> = {
|
|
46
45
|
checkout: "Checkout page",
|
|
47
46
|
cart: "Full cart page",
|
|
48
|
-
cart_include: "
|
|
47
|
+
cart_include: "Cart drawer (slide-out)",
|
|
49
48
|
receipt: "Receipt page",
|
|
50
49
|
email: "Order email",
|
|
51
50
|
};
|
|
52
51
|
|
|
53
|
-
/**
|
|
54
|
-
export interface
|
|
52
|
+
/** Hosted checkout brand-color fields. */
|
|
53
|
+
export interface CheckoutBrandColors {
|
|
55
54
|
primary: string;
|
|
56
55
|
secondary: string;
|
|
57
56
|
tertiary: string;
|
|
58
|
-
/**
|
|
57
|
+
/** How broadly the colors apply. "none" keeps the provider default look. */
|
|
59
58
|
usage: "none" | "all";
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
/** A per-template-type pointer
|
|
63
|
-
export interface
|
|
61
|
+
/** A per-template-type pointer managed by the commerce service. */
|
|
62
|
+
export interface CheckoutTemplatePointer {
|
|
64
63
|
contentUrl: string;
|
|
65
64
|
templateSet: string;
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
/** Store + loader feature config
|
|
69
|
-
export interface
|
|
67
|
+
/** Store + loader feature config. Non-visual. */
|
|
68
|
+
export interface CheckoutStoreConfig {
|
|
70
69
|
storeSubDomain: string;
|
|
71
70
|
exciseTaxCollectionEnabled: boolean;
|
|
72
71
|
verificationEnabled: boolean;
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
/** The full
|
|
76
|
-
export interface
|
|
77
|
-
colors:
|
|
78
|
-
templates: Record<
|
|
79
|
-
store:
|
|
74
|
+
/** The full checkout settings mirror. */
|
|
75
|
+
export interface CheckoutSettings {
|
|
76
|
+
colors: CheckoutBrandColors;
|
|
77
|
+
templates: Record<CheckoutTemplateType, CheckoutTemplatePointer>;
|
|
78
|
+
store: CheckoutStoreConfig;
|
|
80
79
|
}
|
|
81
80
|
|
|
82
81
|
/** Overrides are a deep-partial of the mirror; anything unset falls back to the
|
|
83
82
|
* theme-derived default. Reset === delete the key here. */
|
|
84
|
-
export type
|
|
85
|
-
colors?: Partial<
|
|
86
|
-
templates?: Partial<Record<
|
|
87
|
-
store?: Partial<
|
|
83
|
+
export type CheckoutSettingsOverrides = {
|
|
84
|
+
colors?: Partial<CheckoutBrandColors>;
|
|
85
|
+
templates?: Partial<Record<CheckoutTemplateType, Partial<CheckoutTemplatePointer>>>;
|
|
86
|
+
store?: Partial<CheckoutStoreConfig>;
|
|
88
87
|
};
|
|
89
88
|
|
|
90
89
|
// ---------------------------------------------------------------------------
|
|
91
|
-
// Prepopulation: theme (style-guide) →
|
|
90
|
+
// Prepopulation: theme (style-guide) → checkout settings defaults.
|
|
92
91
|
// ---------------------------------------------------------------------------
|
|
93
92
|
|
|
94
93
|
export interface DeriveOptions {
|
|
95
94
|
/**
|
|
96
95
|
* Absolute origin the ToT-hosted, theme-derived templates are served from —
|
|
97
96
|
* the tenant's own storefront origin (e.g. `https://sample-store.example`). Templates
|
|
98
|
-
* live at `${originBase}/
|
|
97
|
+
* live at `${originBase}/checkout-templates/<type>.html`.
|
|
99
98
|
*/
|
|
100
99
|
originBase: string;
|
|
101
100
|
}
|
|
@@ -105,25 +104,25 @@ export interface DeriveOptions {
|
|
|
105
104
|
* step: brand colors come straight from the theme tokens; each template pointer
|
|
106
105
|
* defaults to a stable ToT-hosted URL for that tenant's theme-derived template.
|
|
107
106
|
*/
|
|
108
|
-
export function
|
|
107
|
+
export function deriveCheckoutSettings(
|
|
109
108
|
theme: ThemeTokens,
|
|
110
|
-
commerce:
|
|
109
|
+
commerce: ResolvedManagedCheckout | null,
|
|
111
110
|
opts: DeriveOptions,
|
|
112
|
-
):
|
|
111
|
+
): CheckoutSettings {
|
|
113
112
|
const base = opts.originBase.replace(/\/$/, "");
|
|
114
|
-
const templateUrl = (type:
|
|
115
|
-
`${base}/
|
|
113
|
+
const templateUrl = (type: CheckoutTemplateType) =>
|
|
114
|
+
`${base}/checkout-templates/${type}.html`;
|
|
116
115
|
|
|
117
116
|
const templates = Object.fromEntries(
|
|
118
|
-
|
|
117
|
+
CHECKOUT_TEMPLATE_TYPES.map((type) => [
|
|
119
118
|
type,
|
|
120
119
|
{ contentUrl: templateUrl(type), templateSet: "DEFAULT" },
|
|
121
120
|
]),
|
|
122
|
-
) as Record<
|
|
121
|
+
) as Record<CheckoutTemplateType, CheckoutTemplatePointer>;
|
|
123
122
|
|
|
124
123
|
return {
|
|
125
124
|
colors: {
|
|
126
|
-
//
|
|
125
|
+
// Checkout primary/secondary/tertiary ← the tenant's brand tokens.
|
|
127
126
|
primary: theme.color.primary,
|
|
128
127
|
secondary: theme.color.accent,
|
|
129
128
|
tertiary: theme.color.sale,
|
|
@@ -143,16 +142,16 @@ export function deriveFoxyCartSettings(
|
|
|
143
142
|
// Resolve with provenance: defaults + overrides → renderable field descriptors.
|
|
144
143
|
// ---------------------------------------------------------------------------
|
|
145
144
|
|
|
146
|
-
export type
|
|
147
|
-
export type
|
|
145
|
+
export type CheckoutFieldKind = "color" | "url" | "text" | "boolean" | "select";
|
|
146
|
+
export type CheckoutFieldGroup = "colors" | "templates" | "store";
|
|
148
147
|
|
|
149
148
|
/** One renderable settings row: resolved value + provenance + reset affordance. */
|
|
150
|
-
export interface
|
|
151
|
-
group:
|
|
152
|
-
/** Dot path into
|
|
149
|
+
export interface CheckoutSettingsField {
|
|
150
|
+
group: CheckoutFieldGroup;
|
|
151
|
+
/** Dot path into CheckoutSettings, e.g. "colors.primary" / "templates.checkout.contentUrl". */
|
|
153
152
|
key: string;
|
|
154
153
|
label: string;
|
|
155
|
-
kind:
|
|
154
|
+
kind: CheckoutFieldKind;
|
|
156
155
|
value: string | boolean;
|
|
157
156
|
/** The theme-derived default; undefined when the field has no theme default. */
|
|
158
157
|
default?: string | boolean;
|
|
@@ -178,17 +177,17 @@ function readPath(obj: unknown, path: string): unknown {
|
|
|
178
177
|
* path, else "theme". `resettable` marks fields with a real theme default (the
|
|
179
178
|
* visual ones) so the UI can show/hide the "Reset to theme" control.
|
|
180
179
|
*/
|
|
181
|
-
export function
|
|
182
|
-
defaults:
|
|
183
|
-
overrides:
|
|
184
|
-
):
|
|
180
|
+
export function buildCheckoutSettingsFields(
|
|
181
|
+
defaults: CheckoutSettings,
|
|
182
|
+
overrides: CheckoutSettingsOverrides,
|
|
183
|
+
): CheckoutSettingsField[] {
|
|
185
184
|
const field = (
|
|
186
|
-
group:
|
|
185
|
+
group: CheckoutFieldGroup,
|
|
187
186
|
key: string,
|
|
188
187
|
label: string,
|
|
189
|
-
kind:
|
|
188
|
+
kind: CheckoutFieldKind,
|
|
190
189
|
opts: { resettable?: boolean; options?: string[]; help?: string } = {},
|
|
191
|
-
):
|
|
190
|
+
): CheckoutSettingsField => {
|
|
192
191
|
const def = readPath(defaults, key);
|
|
193
192
|
const ov = readPath(overrides, key);
|
|
194
193
|
const overridden = ov !== undefined;
|
|
@@ -207,7 +206,7 @@ export function buildFoxySettingsFields(
|
|
|
207
206
|
};
|
|
208
207
|
};
|
|
209
208
|
|
|
210
|
-
const fields:
|
|
209
|
+
const fields: CheckoutSettingsField[] = [
|
|
211
210
|
// Group 1 — brand colors (theme-derived; the "make it look bigd" surface).
|
|
212
211
|
field("colors", "colors.primary", "Primary color", "color", {
|
|
213
212
|
help: "Checkout/cart primary buttons and accents. From --color-primary.",
|
|
@@ -220,28 +219,28 @@ export function buildFoxySettingsFields(
|
|
|
220
219
|
}),
|
|
221
220
|
field("colors", "colors.usage", "Apply brand colors", "select", {
|
|
222
221
|
options: ["all", "none"],
|
|
223
|
-
help: '"all" applies the brand palette to
|
|
222
|
+
help: '"all" applies the brand palette to hosted checkout; "none" keeps checkout defaults.',
|
|
224
223
|
}),
|
|
225
224
|
];
|
|
226
225
|
|
|
227
226
|
// Group 2 — template pointers (default = ToT-hosted theme-derived template).
|
|
228
|
-
for (const type of
|
|
227
|
+
for (const type of CHECKOUT_TEMPLATE_TYPES) {
|
|
229
228
|
fields.push(
|
|
230
|
-
field("templates", `templates.${type}.contentUrl`,
|
|
231
|
-
help: `Hosted template URL for the ${
|
|
229
|
+
field("templates", `templates.${type}.contentUrl`, CHECKOUT_TEMPLATE_LABELS[type], "url", {
|
|
230
|
+
help: `Hosted template URL for the ${CHECKOUT_TEMPLATE_LABELS[type].toLowerCase()}. Defaults to the ToT theme-derived template; override with a custom URL.`,
|
|
232
231
|
}),
|
|
233
232
|
);
|
|
234
233
|
}
|
|
235
234
|
|
|
236
235
|
// Group 3 — store/feature config (no theme default → not resettable).
|
|
237
236
|
fields.push(
|
|
238
|
-
field("store", "store.storeSubDomain", "
|
|
237
|
+
field("store", "store.storeSubDomain", "Checkout store subdomain", "text", {
|
|
239
238
|
resettable: false,
|
|
240
|
-
help: "
|
|
239
|
+
help: "Internal checkout store identifier for this tenant.",
|
|
241
240
|
}),
|
|
242
241
|
field("store", "store.exciseTaxCollectionEnabled", "Collect excise tax", "boolean", {
|
|
243
242
|
resettable: false,
|
|
244
|
-
help: "
|
|
243
|
+
help: "Managed pre-cart excise tax collection.",
|
|
245
244
|
}),
|
|
246
245
|
field("store", "store.verificationEnabled", "Age / identity verification", "boolean", {
|
|
247
246
|
resettable: false,
|
|
@@ -25,6 +25,15 @@ export interface ActivityIngestClaims {
|
|
|
25
25
|
email: string;
|
|
26
26
|
tenant: string;
|
|
27
27
|
capability: string;
|
|
28
|
+
/**
|
|
29
|
+
* The invite→terminal→problem TRACE id, minted once when this developer's
|
|
30
|
+
* sign-in code was issued (see api/dev/cli-signin-code.ts). It rides in the
|
|
31
|
+
* claims so every runtime/activity write the local loop makes with this token
|
|
32
|
+
* is stampable with the same id — support looks a live session up by it.
|
|
33
|
+
* Optional so tokens minted before this field existed still verify (they just
|
|
34
|
+
* carry no trace); every freshly-minted token sets it.
|
|
35
|
+
*/
|
|
36
|
+
traceId?: string;
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
function newToken(): string {
|
|
@@ -14,6 +14,12 @@ export interface ActivityEvent {
|
|
|
14
14
|
event: ActivityEventKind;
|
|
15
15
|
/** Epoch ms, as reported by the local loop. */
|
|
16
16
|
at: number;
|
|
17
|
+
/**
|
|
18
|
+
* The invite→terminal→problem TRACE id, stamped from the ingest-token claims
|
|
19
|
+
* (never the body) so a file save is attributable to the same span as the
|
|
20
|
+
* runtime record. Absent on events written by tokens minted before tracing.
|
|
21
|
+
*/
|
|
22
|
+
traceId?: string;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
const KEY_PREFIX = "dev-activity:";
|
|
@@ -48,6 +54,12 @@ export async function appendActivity(
|
|
|
48
54
|
await kv.put(key(appDomain), JSON.stringify(next), { expirationTtl: TTL_SECONDS });
|
|
49
55
|
}
|
|
50
56
|
|
|
57
|
+
/** Delete the vendor's activity ring buffer — part of a "true reset" so stale saves
|
|
58
|
+
* don't re-complete Step 1 after the developer stops their local loop. */
|
|
59
|
+
export async function clearActivity(kv: KVNamespace, appDomain: string): Promise<void> {
|
|
60
|
+
await kv.delete(key(appDomain));
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
/**
|
|
52
64
|
* Business-readable translation of a reported file path + event, for the
|
|
53
65
|
* activity log. File-level only — no per-line diff enrichment (Darrin,
|