@mohasinac/appkit 4.33.1 → 4.34.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/dist/_internal/server/features/checkout/actions.js +9 -1
- package/dist/_internal/server/features/checkout/expand-order-items.js +9 -0
- package/dist/_internal/server/features/media/contextGuards.d.ts +3 -3
- package/dist/_internal/server/features/media/contextGuards.js +19 -5
- package/dist/_internal/server/features/orders/actions.d.ts +9 -0
- package/dist/_internal/server/features/orders/actions.js +47 -4
- package/dist/_internal/server/features/orders/adapters.js +10 -0
- package/dist/_internal/server/features/refunds/actions.d.ts +9 -1
- package/dist/_internal/server/features/refunds/actions.js +36 -3
- package/dist/_internal/server/features/site-settings/adapters.js +1 -1
- package/dist/_internal/shared/features/orders/refund-copy.d.ts +12 -0
- package/dist/_internal/shared/features/orders/refund-copy.js +12 -0
- package/dist/_internal/shared/features/orders/return-reasons.d.ts +59 -0
- package/dist/_internal/shared/features/orders/return-reasons.js +96 -0
- package/dist/_internal/shared/features/orders/schema.d.ts +30 -0
- package/dist/_internal/shared/features/orders/schema.js +20 -0
- package/dist/_internal/shared/features/products/schema.d.ts +12 -0
- package/dist/_internal/shared/features/products/schema.js +4 -1
- package/dist/_internal/shared/media/limits.d.ts +14 -0
- package/dist/_internal/shared/media/limits.js +29 -0
- package/dist/client.d.ts +9 -1
- package/dist/client.js +19 -1
- package/dist/features/admin/components/AdminProductEditorView.js +6 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +2 -6
- package/dist/features/admin/components/index.d.ts +0 -2
- package/dist/features/admin/components/index.js +0 -1
- package/dist/features/admin/schemas/admin-editor-forms.js +1 -1
- package/dist/features/admin/schemas/firestore.d.ts +0 -2
- package/dist/features/auctions/components/AuctionDetailPageView.js +4 -3
- package/dist/features/contact/components/ContactForm.js +46 -31
- package/dist/features/media/upload/MediaUploadList.d.ts +16 -1
- package/dist/features/media/upload/MediaUploadList.js +58 -8
- package/dist/features/orders/components/RefundRequestView.d.ts +26 -10
- package/dist/features/orders/components/RefundRequestView.js +108 -24
- package/dist/features/orders/schemas/firestore.d.ts +42 -0
- package/dist/features/orders/types/index.d.ts +24 -0
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +5 -3
- package/dist/features/products/components/ProductDetailPageView.js +14 -2
- package/dist/features/products/components/ProductFeatureBadges.d.ts +9 -1
- package/dist/features/products/components/ProductFeatureBadges.js +17 -1
- package/dist/features/products/components/ProductForm.js +46 -10
- package/dist/features/products/components/ProductGrid.js +10 -1
- package/dist/features/products/constants/final-sale.d.ts +40 -0
- package/dist/features/products/constants/final-sale.js +40 -0
- package/dist/features/products/index.d.ts +1 -0
- package/dist/features/products/index.js +1 -0
- package/dist/features/products/schemas/firestore.d.ts +19 -2
- package/dist/features/products/schemas/firestore.js +4 -0
- package/dist/features/products/types/index.d.ts +2 -0
- package/dist/features/seller/components/SellerProductShell.d.ts +7 -0
- package/dist/features/seller/components/SellerProductShell.js +82 -11
- package/dist/features/seller/index.d.ts +1 -0
- package/dist/features/seller/index.js +1 -0
- package/dist/features/seller/schemas/product-form.d.ts +304 -0
- package/dist/features/seller/schemas/product-form.js +132 -0
- package/dist/features/seller/utils/product-draft-mapping.d.ts +117 -0
- package/dist/features/seller/utils/product-draft-mapping.js +147 -0
- package/dist/features/shell/SectionForm.js +62 -7
- package/dist/features/shell/form-sections.js +4 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +182 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +12 -3
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +1 -0
- package/dist/schemas/registry.d.ts +271 -0
- package/dist/schemas/registry.js +9 -0
- package/dist/seed/_helpers/final-sale-fixtures.d.ts +35 -0
- package/dist/seed/_helpers/final-sale-fixtures.js +61 -0
- package/dist/seed/products-art-seed-data.js +9 -1
- package/dist/seed/products-auctions-seed-data.js +2 -1
- package/dist/seed/products-classifieds-seed-data.js +2 -1
- package/dist/seed/products-digital-codes-seed-data.js +2 -1
- package/dist/seed/products-live-items-seed-data.js +2 -1
- package/dist/seed/products-preorders-seed-data.d.ts +2 -138
- package/dist/seed/products-preorders-seed-data.js +2 -1
- package/dist/seed/products-prize-draws-seed-data.js +2 -1
- package/dist/seed/products-standard-seed-data.js +2 -1
- package/dist/seed/products-stickers-seed-data.js +9 -1
- package/dist/server.d.ts +6 -2
- package/dist/server.js +15 -2
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/motion.d.ts +12 -0
- package/dist/tokens/motion.js +12 -0
- package/dist/ui/components/CollapsibleSection.d.ts +18 -1
- package/dist/ui/components/CollapsibleSection.js +20 -3
- package/dist/ui/components/Motion.js +17 -1
- package/package.json +1 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
function pick(v) {
|
|
2
|
+
return v === undefined || v === null || v === "" ? undefined : v;
|
|
3
|
+
}
|
|
4
|
+
/** Drop keys whose value is undefined so they never reach Firestore. */
|
|
5
|
+
function compact(obj) {
|
|
6
|
+
const out = {};
|
|
7
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
8
|
+
if (v !== undefined)
|
|
9
|
+
out[k] = v;
|
|
10
|
+
}
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The flat draft keys this mapper OWNS.
|
|
15
|
+
*
|
|
16
|
+
* Stripped from the payload after being folded into their nested homes — the
|
|
17
|
+
* write schema does not name them, so leaving them in would be harmless today
|
|
18
|
+
* and confusing forever ("is `classifiedCity` stored or not?").
|
|
19
|
+
*/
|
|
20
|
+
const OWNED_FLAT_KEYS = [
|
|
21
|
+
"classifiedCity", "classifiedLocality", "classifiedPincode",
|
|
22
|
+
"classifiedContactMethod", "classifiedAcceptsShipping", "classifiedNegotiable",
|
|
23
|
+
"digitalCodeDelivery", "digitalCodePoolSize",
|
|
24
|
+
"digitalCodeRedemptionInstructions", "digitalCodeExpiresAt",
|
|
25
|
+
"liveSpecies", "liveAgeMonths", "liveSex", "liveCareInfo",
|
|
26
|
+
"liveTransportMethod", "liveHandlingFee", "liveJurisdictions", "liveCites",
|
|
27
|
+
"printSize", "printMaterial", "printFinish", "printEditionSize",
|
|
28
|
+
];
|
|
29
|
+
/**
|
|
30
|
+
* Fold the draft's flat per-type keys into the nested blocks the write schema
|
|
31
|
+
* expects. Every other key passes through untouched.
|
|
32
|
+
*
|
|
33
|
+
* A block is emitted only when the draft actually carries something for it, so
|
|
34
|
+
* saving a standard listing does not write an empty `classified: {}`.
|
|
35
|
+
*/
|
|
36
|
+
export function draftToProductInput(draft) {
|
|
37
|
+
const out = { ...draft };
|
|
38
|
+
for (const k of OWNED_FLAT_KEYS)
|
|
39
|
+
delete out[k];
|
|
40
|
+
// ── classified ────────────────────────────────────────────────────────────
|
|
41
|
+
if (pick(draft.classifiedCity) !== undefined) {
|
|
42
|
+
out.classified = compact({
|
|
43
|
+
meetupArea: compact({
|
|
44
|
+
city: draft.classifiedCity,
|
|
45
|
+
locality: pick(draft.classifiedLocality),
|
|
46
|
+
pincode: pick(draft.classifiedPincode),
|
|
47
|
+
}),
|
|
48
|
+
acceptsShipping: draft.classifiedAcceptsShipping,
|
|
49
|
+
negotiable: draft.classifiedNegotiable,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// ── digital code ──────────────────────────────────────────────────────────
|
|
53
|
+
if (pick(draft.digitalCodeDelivery) !== undefined) {
|
|
54
|
+
out.digitalCode = compact({
|
|
55
|
+
codeDeliveryMethod: draft.digitalCodeDelivery,
|
|
56
|
+
codePoolSize: pick(draft.digitalCodePoolSize),
|
|
57
|
+
redemptionInstructions: pick(draft.digitalCodeRedemptionInstructions),
|
|
58
|
+
expiresAt: pick(draft.digitalCodeExpiresAt),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// ── live item ─────────────────────────────────────────────────────────────
|
|
62
|
+
if (pick(draft.liveSpecies) !== undefined) {
|
|
63
|
+
out.liveItem = compact({
|
|
64
|
+
species: draft.liveSpecies,
|
|
65
|
+
ageMonths: pick(draft.liveAgeMonths),
|
|
66
|
+
sex: pick(draft.liveSex),
|
|
67
|
+
careInfo: pick(draft.liveCareInfo),
|
|
68
|
+
/*
|
|
69
|
+
* `transport` is a REQUIRED object on the schema, and `method` inside it
|
|
70
|
+
* is required too — so a live listing whose form left the transport
|
|
71
|
+
* method blank must still produce the object, or the whole `liveItem`
|
|
72
|
+
* block fails to parse and takes the species with it. "courier" is the
|
|
73
|
+
* form's own default.
|
|
74
|
+
*/
|
|
75
|
+
transport: compact({
|
|
76
|
+
method: draft.liveTransportMethod ?? "courier",
|
|
77
|
+
handlingFee: pick(draft.liveHandlingFee),
|
|
78
|
+
}),
|
|
79
|
+
// Required, min 1. Left empty the schema rejects the listing with a
|
|
80
|
+
// real message — which is correct: shipping a live animal into a
|
|
81
|
+
// jurisdiction that forbids it is what the field exists to prevent.
|
|
82
|
+
jurisdictionAllowed: draft.liveJurisdictions ?? [],
|
|
83
|
+
// `cites` is a STRING permit number on the document, while the form
|
|
84
|
+
// carries a boolean "CITES applies" checkbox. Only the affirmative case
|
|
85
|
+
// means anything, and it means "documented" until a number is entered.
|
|
86
|
+
cites: draft.liveCites ? "documented" : undefined,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
// ── print meta (art + stickers share one block) ───────────────────────────
|
|
90
|
+
const printEntries = compact({
|
|
91
|
+
size: pick(draft.printSize),
|
|
92
|
+
material: pick(draft.printMaterial),
|
|
93
|
+
finish: pick(draft.printFinish),
|
|
94
|
+
editionSize: pick(draft.printEditionSize),
|
|
95
|
+
});
|
|
96
|
+
if (Object.keys(printEntries).length > 0)
|
|
97
|
+
out.printMeta = printEntries;
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The inverse — seed the form from a stored document.
|
|
102
|
+
*
|
|
103
|
+
* Tolerant of documents written before the nested blocks existed: a missing
|
|
104
|
+
* block yields undefined flat keys, which render as empty inputs rather than
|
|
105
|
+
* throwing.
|
|
106
|
+
*/
|
|
107
|
+
export function productToDraft(product) {
|
|
108
|
+
const { classified, digitalCode, liveItem, printMeta } = product;
|
|
109
|
+
return compact({
|
|
110
|
+
/*
|
|
111
|
+
* Flat fields the edit pages' hand-written `initialValues` blocks omit.
|
|
112
|
+
*
|
|
113
|
+
* Not a nesting mismatch like the rest of this function — just fields
|
|
114
|
+
* nobody added to ten near-identical literals. `finalSale` matters most:
|
|
115
|
+
* unseeded it renders as `undefined`, which `isFinalSale` reads as TRUE,
|
|
116
|
+
* so a listing the seller had opted into returns would open its own edit
|
|
117
|
+
* form showing "final sale". The stored value survives an untouched save
|
|
118
|
+
* (an absent key is stripped, and the update is partial), but the form was
|
|
119
|
+
* telling the seller something untrue about their own listing.
|
|
120
|
+
*/
|
|
121
|
+
finalSale: product.finalSale,
|
|
122
|
+
returnPolicy: product.returnPolicy,
|
|
123
|
+
classifiedCity: classified?.meetupArea?.city,
|
|
124
|
+
classifiedLocality: classified?.meetupArea?.locality,
|
|
125
|
+
classifiedPincode: classified?.meetupArea?.pincode,
|
|
126
|
+
classifiedAcceptsShipping: classified?.acceptsShipping,
|
|
127
|
+
classifiedNegotiable: classified?.negotiable,
|
|
128
|
+
digitalCodeDelivery: digitalCode?.codeDeliveryMethod,
|
|
129
|
+
digitalCodePoolSize: digitalCode?.codePoolSize,
|
|
130
|
+
digitalCodeRedemptionInstructions: digitalCode?.redemptionInstructions,
|
|
131
|
+
digitalCodeExpiresAt: digitalCode?.expiresAt,
|
|
132
|
+
liveSpecies: liveItem?.species,
|
|
133
|
+
liveAgeMonths: liveItem?.ageMonths,
|
|
134
|
+
liveSex: liveItem?.sex,
|
|
135
|
+
liveCareInfo: liveItem?.careInfo,
|
|
136
|
+
liveTransportMethod: liveItem?.transport?.method,
|
|
137
|
+
liveHandlingFee: liveItem?.transport?.handlingFee,
|
|
138
|
+
liveJurisdictions: liveItem?.jurisdictionAllowed,
|
|
139
|
+
// String -> boolean, mirroring the outbound direction: any recorded permit
|
|
140
|
+
// means the checkbox is ticked.
|
|
141
|
+
liveCites: liveItem?.cites ? true : undefined,
|
|
142
|
+
printSize: printMeta?.size,
|
|
143
|
+
printMaterial: printMeta?.material,
|
|
144
|
+
printFinish: printMeta?.finish,
|
|
145
|
+
printEditionSize: printMeta?.editionSize,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -9,6 +9,7 @@ import { FormShellContext } from "../../ui/forms/FormShell";
|
|
|
9
9
|
import { useFormBottomActions } from "../layout/hooks/useFormBottomActions";
|
|
10
10
|
import { useSectionState } from "../account/hooks/useCollapsedSections";
|
|
11
11
|
import { FormSchemaContext } from "./FormShell";
|
|
12
|
+
import { EASE_OUT_MS } from "../../tokens/motion";
|
|
12
13
|
/** Sections in render order: required first, original order preserved within each group. */
|
|
13
14
|
export function orderSections(sections) {
|
|
14
15
|
return [...sections.filter((s) => s.required), ...sections.filter((s) => !s.required)];
|
|
@@ -78,8 +79,34 @@ export function scrollToSection(id) {
|
|
|
78
79
|
return;
|
|
79
80
|
requestAnimationFrame(() => {
|
|
80
81
|
const el = document.getElementById(sectionAnchorId(id));
|
|
81
|
-
el
|
|
82
|
-
|
|
82
|
+
if (!el)
|
|
83
|
+
return;
|
|
84
|
+
/*
|
|
85
|
+
* The header is already at its final position, so scrolling can happen on
|
|
86
|
+
* the next frame.
|
|
87
|
+
*/
|
|
88
|
+
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
89
|
+
/*
|
|
90
|
+
* Focusing the offending field cannot.
|
|
91
|
+
*
|
|
92
|
+
* `<Collapse>` animates height 0 -> auto over EASE_OUT_MS, and Framer
|
|
93
|
+
* drives that with rAF rather than a CSS transition — so there is no
|
|
94
|
+
* `transitionend` to wait for, and one `requestAnimationFrame` lands while
|
|
95
|
+
* the panel is still ~0px tall inside `overflow: hidden`. Focusing there
|
|
96
|
+
* makes the browser scroll-anchor against a box that is about to grow,
|
|
97
|
+
* which is why "jump to error" could leave the field off-screen.
|
|
98
|
+
*
|
|
99
|
+
* A required section renders with no <Collapse> at all, so it is already
|
|
100
|
+
* at full height and this simply fires a tick later than it needs to.
|
|
101
|
+
*/
|
|
102
|
+
const reduced = typeof window !== "undefined" &&
|
|
103
|
+
typeof window.matchMedia === "function" &&
|
|
104
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
105
|
+
const focusInvalid = () => el.querySelector("[aria-invalid='true']")?.focus();
|
|
106
|
+
if (reduced)
|
|
107
|
+
focusInvalid();
|
|
108
|
+
else
|
|
109
|
+
window.setTimeout(focusInvalid, EASE_OUT_MS);
|
|
83
110
|
});
|
|
84
111
|
}
|
|
85
112
|
export function SectionForm({ sections, values, onChange, onSubmit, submitLabel = "Save", isLoading = false, schema, openIds: controlledOpenIds, onOpenChange, expandMode = "multi", hideActions = false, destructiveAction, onValidationChange, onCancel, cancelLabel, bottomBar = true, }) {
|
|
@@ -112,7 +139,20 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
|
|
|
112
139
|
setUncontrolledOpenIds(ids);
|
|
113
140
|
onOpenChange?.(ids);
|
|
114
141
|
}, [controlledOpenIds, onOpenChange]);
|
|
115
|
-
|
|
142
|
+
/*
|
|
143
|
+
* `ordered` depends on `values`, so it is a NEW array on every keystroke.
|
|
144
|
+
* Anything derived from it must key on the visible section SET instead, or
|
|
145
|
+
* the effect below re-fires per character — running a second full-tree
|
|
146
|
+
* `safeParse` on top of the one `handleFieldChange` already performs, and
|
|
147
|
+
* rebuilding every downstream callback with it.
|
|
148
|
+
*
|
|
149
|
+
* `ordered` itself must keep recomputing: a `when()` predicate can flip
|
|
150
|
+
* mid-typing and a section can appear or disappear. It is only the DERIVED
|
|
151
|
+
* values that are stable across that.
|
|
152
|
+
*/
|
|
153
|
+
const visibleSectionKey = ordered.map((s) => s.id).join("|");
|
|
154
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
155
|
+
const fieldToSectionIndex = useMemo(() => buildFieldToSectionIndex(ordered), [visibleSectionKey]);
|
|
116
156
|
// Keep the latest callback without making runValidation depend on it — the
|
|
117
157
|
// caller usually passes an inline arrow, which would otherwise rebuild the
|
|
118
158
|
// validator on every render and re-fire the effect below in a loop.
|
|
@@ -227,14 +267,29 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
|
|
|
227
267
|
}, [fieldErrors]);
|
|
228
268
|
return (_jsxs(Stack, { gap: "md", children: [ordered.map((section) => {
|
|
229
269
|
const isRequired = section.required === true;
|
|
230
|
-
|
|
231
|
-
|
|
270
|
+
/*
|
|
271
|
+
* A required section is always open — it holds the fields without
|
|
272
|
+
* which the record cannot be saved, so hiding it is never useful.
|
|
273
|
+
*
|
|
274
|
+
* `collapsible={!isRequired}` below is what makes that VISIBLE: the
|
|
275
|
+
* header renders as a plain heading with no control. Previously this
|
|
276
|
+
* flag only forced `isCollapsed` false while the header still drew a
|
|
277
|
+
* chevron button, so the section was permanently open AND appeared
|
|
278
|
+
* broken. `onToggle` no longer needs its `if (!isRequired)` guard,
|
|
279
|
+
* because a static header has nothing to click.
|
|
280
|
+
*/
|
|
232
281
|
const isCollapsed = isRequired
|
|
233
282
|
? false
|
|
234
283
|
: !(openIds.includes(section.id) || forceOpenIds.includes(section.id));
|
|
235
284
|
const errorCount = errorCountFor(section);
|
|
236
|
-
return (_jsx(CollapsibleSection, { id: sectionAnchorId(section.id), title:
|
|
237
|
-
|
|
285
|
+
return (_jsx(CollapsibleSection, { id: sectionAnchorId(section.id), title:
|
|
286
|
+
/*
|
|
287
|
+
* `as="span"` matters: a collapsible section's header renders
|
|
288
|
+
* this INSIDE a <button>, and <Row>'s default <div> made that
|
|
289
|
+
* <button><div><span><div> — invalid, since a button may only
|
|
290
|
+
* contain phrasing content. Same variant props, legal element.
|
|
291
|
+
*/
|
|
292
|
+
_jsxs(Row, { as: "span", align: "center", gap: "xs", className: "min-w-0", children: [_jsx(Span, { weight: "semibold", size: "sm", children: section.heading ?? section.label }), isRequired && _jsx(Span, { size: "xs", color: "muted", children: "Required" })] }), isCollapsed: isCollapsed, collapsible: !isRequired, onToggle: () => toggle(section.id), keepMounted: section.keepMounted, renderHeaderExtra: errorCount > 0
|
|
238
293
|
? () => (_jsxs(Badge, { variant: "danger", children: [errorCount, " ", errorCount === 1 ? "issue" : "issues"] }))
|
|
239
294
|
: undefined, children: section.render({ values, onChange: handleFieldChange, errors: fieldErrors }) }, section.id));
|
|
240
295
|
}), !hideActions && (
|
|
@@ -92,6 +92,10 @@ export const SECTION_GROUP_DEFAULTS = {
|
|
|
92
92
|
// logistics
|
|
93
93
|
address: "logistics", shipping: "logistics", delivery: "logistics",
|
|
94
94
|
routing: "logistics",
|
|
95
|
+
// Returns sit with logistics rather than with pricing: the seller is
|
|
96
|
+
// describing what happens to the goods after delivery, and the section
|
|
97
|
+
// reads directly after Shipping on the listing form.
|
|
98
|
+
returns: "logistics",
|
|
95
99
|
// relationships
|
|
96
100
|
owner: "relationships", members: "relationships", link: "relationships",
|
|
97
101
|
google: "relationships", sync: "relationships", subscribe: "relationships",
|
|
@@ -1927,6 +1927,48 @@ const rawTesterChecklistItems = [
|
|
|
1927
1927
|
},
|
|
1928
1928
|
],
|
|
1929
1929
|
},
|
|
1930
|
+
{
|
|
1931
|
+
pageKey: "return-request",
|
|
1932
|
+
pageLabel: "Requesting a return \u2014 and what final sale actually blocks",
|
|
1933
|
+
href: "/user/orders",
|
|
1934
|
+
cases: [
|
|
1935
|
+
{
|
|
1936
|
+
key: "return-cta-only-when-returnable",
|
|
1937
|
+
label: "The \"Request Return\" button appears only on a DELIVERED order inside the 7-day window",
|
|
1938
|
+
description: "Added 2026-09-04. BEFORE there was no buyer-facing return flow at all: the action existed, the CTA was registered in ACTIONS.USER, and nothing anywhere rendered it. AFTER: open a delivered order's detail page and the button is there beside Track Shipment. Open a pending or shipped one and it is not. Open a delivered order older than 7 days and the page explains the window has closed rather than offering a form that would be refused.",
|
|
1939
|
+
},
|
|
1940
|
+
{
|
|
1941
|
+
key: "return-not-received-accepted-on-final-sale",
|
|
1942
|
+
label: "\"It never arrived\" is accepted on a FINAL SALE order",
|
|
1943
|
+
description: "This is the whole rule. Final sale refuses a change of mind; it can never refuse a claim that the buyer did not get what they paid for. On a final-sale order, choose \"It never arrived\", tick the three acknowledgements, submit \u2014 it must go through and the order must move to Return Requested.",
|
|
1944
|
+
},
|
|
1945
|
+
{
|
|
1946
|
+
key: "return-change-of-mind-refused-by-name",
|
|
1947
|
+
label: "On a final-sale order the change-of-mind reasons are NOT OFFERED, and the panel says why",
|
|
1948
|
+
description: "Open the return form for a final-sale order. A warning panel names the situation, and the reason picker lists only the five fault reasons \u2014 never-arrived, not-as-described, damaged, wrong item, counterfeit. \"I changed my mind\" must not be selectable. On a returnable order all ten reasons appear. A generic \"request refused\" with no explanation is the failure this replaced.",
|
|
1949
|
+
},
|
|
1950
|
+
{
|
|
1951
|
+
key: "return-reason-is-persisted",
|
|
1952
|
+
label: "The reason the buyer picked is actually STORED and visible to staff",
|
|
1953
|
+
description: "BEFORE: `requestReturnAction` parsed a free-text reason and then threw it away before writing \u2014 an order reached Return Requested with nothing recorded about why, and the seller had nothing to act on. AFTER: submit a return, then open the same order in /admin/return-requests and confirm the coded reason and any note are there.",
|
|
1954
|
+
},
|
|
1955
|
+
{
|
|
1956
|
+
key: "return-partial-gates-selected-lines-only",
|
|
1957
|
+
label: "On a mixed order, a final-sale line does not block a return of the returnable line",
|
|
1958
|
+
description: "Final sale is snapshotted PER LINE at purchase, not per order. On an order containing one final-sale item and one returnable item, a change-of-mind return scoped to the returnable line must be accepted; the same reason scoped to the final-sale line must not.",
|
|
1959
|
+
},
|
|
1960
|
+
{
|
|
1961
|
+
key: "return-prize-draw-still-refused-entirely",
|
|
1962
|
+
label: "A prize-draw order is still refused for EVERY reason \u2014 a different, earlier gate",
|
|
1963
|
+
description: "`isNonRefundable` is a property of the listing TYPE and admits no exception, including a seller-fault one. Open a prize-draw order's refund surface: it must refuse outright and say so, without offering a reason picker at all. Confusing this with final sale in either direction is the failure.",
|
|
1964
|
+
},
|
|
1965
|
+
{
|
|
1966
|
+
key: "return-terms-snapshotted-at-purchase",
|
|
1967
|
+
label: "Changing a listing's return terms does NOT change an order already placed",
|
|
1968
|
+
description: "Buy a returnable listing. Then, as the seller, switch that listing to final sale. Go back to the order and request a change-of-mind return \u2014 it must still be ACCEPTED, because the buyer agreed to the terms shown at checkout. The order carries its own copy; a seller must not be able to withdraw a right after the sale.",
|
|
1969
|
+
},
|
|
1970
|
+
],
|
|
1971
|
+
},
|
|
1930
1972
|
]),
|
|
1931
1973
|
...group("selling", "Selling", [
|
|
1932
1974
|
{
|
|
@@ -2162,6 +2204,30 @@ const rawTesterChecklistItems = [
|
|
|
2162
2204
|
description: "BEFORE: an invalid field produced a banner listing the problem with no way to reach it, and on the site-settings page the summary was mounted outside every form context and so displayed NOTHING at all. AFTER: submit a form with a bad value (a negative refund on an order, a non-https third-party ad URL, an AdSense id that is not ca-pub-…) and the summary must list it, tag it with its section, collapse open that section when clicked, and show a count badge on the section header.",
|
|
2163
2205
|
href: "/admin/ads",
|
|
2164
2206
|
},
|
|
2207
|
+
{
|
|
2208
|
+
key: "required-section-has-no-dead-chevron",
|
|
2209
|
+
label: "A REQUIRED section renders as a plain heading \u2014 no chevron, nothing to click",
|
|
2210
|
+
description: "Fixed 2026-09-04. BEFORE: SectionDef documented a required section as rendering \"without a collapse control so it can't be hidden\", but CollapsibleSection had no way to say that, so SectionForm passed a no-op handler and the header still drew a full button with a rotating chevron. Clicking it did nothing, on ~73 required sections across the app \u2014 which reads as a broken form. AFTER: on any sectionised form (admin product, admin user, store product), the first section's header has no chevron and no hover state, while every optional section below it still expands and collapses normally.",
|
|
2211
|
+
href: "/admin/products/new",
|
|
2212
|
+
},
|
|
2213
|
+
{
|
|
2214
|
+
key: "open-section-does-not-clip-dropdowns",
|
|
2215
|
+
label: "A dropdown opened inside an expanded section is not clipped at the panel edge",
|
|
2216
|
+
description: "BEFORE: the collapse animation kept `overflow: hidden` in its SETTLED open state, not just during the transition, so anything escaping the panel box was cut off. AFTER: expand a section containing a select or picker near its bottom edge, open it, and the list must render in full rather than being sliced by the section border.",
|
|
2217
|
+
href: "/admin/products/new",
|
|
2218
|
+
},
|
|
2219
|
+
{
|
|
2220
|
+
key: "error-jump-lands-on-the-field",
|
|
2221
|
+
label: "Jumping to an error scrolls to the section AND lands focus on the offending field",
|
|
2222
|
+
description: "BEFORE: the scroll fired one animation frame after expanding, while the panel was still ~0px tall inside overflow:hidden \u2014 so focus landed against a box that was about to grow and the field could end up off-screen. AFTER: submit a form with an error inside a COLLAPSED section. It expands, scrolls, and the invalid input is both visible and focused once the animation finishes.",
|
|
2223
|
+
href: "/admin/products/new",
|
|
2224
|
+
},
|
|
2225
|
+
{
|
|
2226
|
+
key: "long-form-typing-is-smooth",
|
|
2227
|
+
label: "Typing in a long form does not stutter",
|
|
2228
|
+
description: "BEFORE: the section list was rebuilt on every keystroke, which re-fired the validation effect on top of the validation the keystroke already triggered \u2014 two full-tree schema parses per character on a 40-field listing form. AFTER: type a long description into the seller product form. It should keep up. This is a feel test, not a pass/fail assertion; report it if it lags.",
|
|
2229
|
+
href: "/store/products/new",
|
|
2230
|
+
},
|
|
2165
2231
|
{
|
|
2166
2232
|
key: "form-mobile-action-bar",
|
|
2167
2233
|
label: "Save and Cancel appear in the pinned mobile bar, once",
|
|
@@ -2410,6 +2476,122 @@ const rawTesterChecklistItems = [
|
|
|
2410
2476
|
},
|
|
2411
2477
|
],
|
|
2412
2478
|
},
|
|
2479
|
+
{
|
|
2480
|
+
pageKey: "final-sale-authoring",
|
|
2481
|
+
pageLabel: "Final sale \u2014 the seller switch",
|
|
2482
|
+
href: "/store/products/new",
|
|
2483
|
+
cases: [
|
|
2484
|
+
{
|
|
2485
|
+
key: "final-sale-defaults-on",
|
|
2486
|
+
label: "A brand-new listing is FINAL SALE by default \u2014 the Returns section shows \"Accept change-of-mind returns\" switched OFF",
|
|
2487
|
+
description: "Added 2026-09-04. Open the Returns section on a new listing of any type. The toggle is off and the panel explains that not-received / damaged / wrong-item / not-as-described / counterfeit claims are still accepted. Absent means final sale, so a listing saved without ever opening this section is also final sale \u2014 which is the point.",
|
|
2488
|
+
href: "/store/products/new",
|
|
2489
|
+
},
|
|
2490
|
+
{
|
|
2491
|
+
key: "final-sale-opt-out-survives-reload",
|
|
2492
|
+
label: "Turning returns ON, saving, and RELOADING shows it still on",
|
|
2493
|
+
description: "This is the case that matters, and a reload is the only way to see it. `productBaseSchema` is a plain z.object() with no .passthrough(), so it strips every key it does not name \u2014 a field missing from it saves with a success toast and is gone. BEFORE such a field existed at all. AFTER: toggle Accept returns ON, Save, reload the edit page, and the toggle must still be ON. If it reverts to off, `finalSale` is being stripped on write.",
|
|
2494
|
+
href: "/store/products",
|
|
2495
|
+
},
|
|
2496
|
+
{
|
|
2497
|
+
key: "return-policy-authorable-by-seller",
|
|
2498
|
+
label: "A seller can write their own Return policy text \u2014 for the first time",
|
|
2499
|
+
description: "BEFORE: `returnPolicy` existed on the document and the ONLY editor for it was the admin product form, so a seller could never state their own terms. AFTER: the Returns section on the seller form has a Return policy textarea. Type into it, save, reload, and confirm it persists and appears on the public listing under Delivery & Returns.",
|
|
2500
|
+
href: "/store/products",
|
|
2501
|
+
},
|
|
2502
|
+
{
|
|
2503
|
+
key: "final-sale-admin-override",
|
|
2504
|
+
label: "An admin can flip the same switch on any listing, and it is the SAME field",
|
|
2505
|
+
description: "Open a listing in the admin product editor. The \"Accept change-of-mind returns\" toggle sits directly above the Return policy box and reflects whatever the seller chose. Change it, save, then reopen the SELLER edit form for that listing \u2014 it must show the admin's value. Two independent flags would be the failure.",
|
|
2506
|
+
href: "/admin/products",
|
|
2507
|
+
},
|
|
2508
|
+
{
|
|
2509
|
+
key: "final-sale-badges-render",
|
|
2510
|
+
label: "A final-sale listing shows the pill on its card AND the line on its detail page",
|
|
2511
|
+
description: "Seed data opts one listing per type OUT of final sale, so both states are visible. On /products, a final-sale listing carries a dark \"Final Sale\" pill; the opted-out one does not. On the detail page, the Delivery & Returns block always renders a returns line \u2014 even when the seller wrote no shipping or policy text at all, which previously hid the block entirely and with it the most consequential term of the sale.",
|
|
2512
|
+
href: "/products",
|
|
2513
|
+
},
|
|
2514
|
+
],
|
|
2515
|
+
},
|
|
2516
|
+
{
|
|
2517
|
+
pageKey: "media-limits",
|
|
2518
|
+
pageLabel: "Media \u2014 ten images and one video, on every listing type",
|
|
2519
|
+
href: "/store/products/new",
|
|
2520
|
+
cases: [
|
|
2521
|
+
{
|
|
2522
|
+
key: "media-gallery-accepts-images-and-video",
|
|
2523
|
+
label: "The gallery takes BOTH images and a video, and shows a live per-type count",
|
|
2524
|
+
description: "Changed 2026-09-04. BEFORE the single-media control on the listing form was video-only (`kind=\"video\"`), so the one \"add media\" affordance a seller saw refused photos. AFTER: the gallery accepts image files and a video file in the same control, and a counter under it reads \"N/10 images \u00b7 N/1 video\" as you add them.",
|
|
2525
|
+
href: "/store/products/new",
|
|
2526
|
+
},
|
|
2527
|
+
{
|
|
2528
|
+
key: "media-tenth-image-uploads",
|
|
2529
|
+
label: "The 10th gallery image uploads \u2014 the 5th used to fail with a 400",
|
|
2530
|
+
description: "BEFORE: the gallery was capped at 5 and sent its Nth image as index N+1, while the server's signed-URL guard rejected any index above 5 \u2014 so the LAST slot of every product form returned a 400 that read as a random upload failure. AFTER: add ten images one at a time. All ten must succeed, including the tenth.",
|
|
2531
|
+
href: "/store/products/new",
|
|
2532
|
+
},
|
|
2533
|
+
{
|
|
2534
|
+
key: "media-eleventh-image-refused-client-side",
|
|
2535
|
+
label: "The 11th image is refused by the FORM with a clear message, not by the server",
|
|
2536
|
+
description: "Try to add an 11th image. The control must refuse it before uploading, naming the limit (\"You can upload up to 10 images.\"). A server-side 400 arriving after the bytes have gone up is the wrong behaviour \u2014 the user has already waited.",
|
|
2537
|
+
href: "/store/products/new",
|
|
2538
|
+
},
|
|
2539
|
+
{
|
|
2540
|
+
key: "media-second-video-refused-first-kept",
|
|
2541
|
+
label: "A second video is refused and the FIRST one is kept",
|
|
2542
|
+
description: "With one video already in the gallery, try to add another. The message must say only one video is allowed and tell you to remove the current one \u2014 and crucially the existing video must still be there afterwards. Silently replacing it would lose work.",
|
|
2543
|
+
href: "/store/products/new",
|
|
2544
|
+
},
|
|
2545
|
+
{
|
|
2546
|
+
key: "media-caps-flat-across-types",
|
|
2547
|
+
label: "The same 10 + 1 applies to an auction and a pre-order \u2014 there is no per-type table",
|
|
2548
|
+
description: "Repeat the ten-image check on /store/auctions/new and /store/pre-orders/new. Both must allow ten. BEFORE the server carried separate AUCTION_IMAGE_MAX and PREORDER_IMAGE_MAX constants, both 5, disagreeing with everything else.",
|
|
2549
|
+
href: "/store/auctions/new",
|
|
2550
|
+
},
|
|
2551
|
+
{
|
|
2552
|
+
key: "media-live-listing-still-requires-video",
|
|
2553
|
+
label: "A live listing STILL refuses to publish without a video",
|
|
2554
|
+
description: "Unchanged behaviour, re-verified because the surrounding control changed. Create a live listing with photos but no video and press Publish. It must be refused, and the Video field must be marked required. That is a requiredness rule, not a count rule, and the media-limit work must not have removed it.",
|
|
2555
|
+
href: "/store/live/new",
|
|
2556
|
+
},
|
|
2557
|
+
],
|
|
2558
|
+
},
|
|
2559
|
+
{
|
|
2560
|
+
pageKey: "listing-type-fields-roundtrip",
|
|
2561
|
+
pageLabel: "Per-type fields survive a save \u2014 all nine listing types",
|
|
2562
|
+
href: "/store/products",
|
|
2563
|
+
cases: [
|
|
2564
|
+
{
|
|
2565
|
+
key: "roundtrip-classified-meetup",
|
|
2566
|
+
label: "A classified's meetup city, locality and pincode come back after a save",
|
|
2567
|
+
description: "Fixed 2026-09-04, and this was live data loss. BEFORE: the form held `classifiedCity` while the write schema expected `classified.meetupArea.city`, nothing translated between them, and the schema strips unnamed keys \u2014 so the seller pressed Publish, got a success, and the listing saved with none of the fields that made it a classified. AFTER: fill in the meetup details, save, reopen the edit form, and every value must be there.",
|
|
2568
|
+
href: "/store/classified/new",
|
|
2569
|
+
},
|
|
2570
|
+
{
|
|
2571
|
+
key: "roundtrip-live-species-jurisdictions",
|
|
2572
|
+
label: "A live listing's species, transport method and permitted jurisdictions survive",
|
|
2573
|
+
description: "Same bug, and the most serious instance of it: `jurisdictionAllowed` is the field that prevents shipping a live animal somewhere that forbids it, and it was being dropped. Create a live listing with species, age, sex, care info, transport method, handling fee and at least one jurisdiction. Save, reopen, confirm all of it returned.",
|
|
2574
|
+
href: "/store/live/new",
|
|
2575
|
+
},
|
|
2576
|
+
{
|
|
2577
|
+
key: "roundtrip-digital-code-delivery",
|
|
2578
|
+
label: "A digital code's delivery method, pool size and redemption instructions survive",
|
|
2579
|
+
href: "/store/digital-codes/new",
|
|
2580
|
+
},
|
|
2581
|
+
{
|
|
2582
|
+
key: "roundtrip-print-meta",
|
|
2583
|
+
label: "An art print's size, material, finish and edition size survive \u2014 and so do a sticker sheet's",
|
|
2584
|
+
description: "Art and stickers share one `printMeta` block, so check both: create one of each with all four fields filled, save, reopen.",
|
|
2585
|
+
href: "/store/art/new",
|
|
2586
|
+
},
|
|
2587
|
+
{
|
|
2588
|
+
key: "roundtrip-edit-form-opens-populated",
|
|
2589
|
+
label: "Opening the edit form for an EXISTING listing shows its per-type fields already filled",
|
|
2590
|
+
description: "The inverse half of the same bug. BEFORE: the edit pages hand-built their initial values and listed none of the per-type keys, so the form opened blank for them even when the document held real data \u2014 which read as \"my data was deleted\". AFTER: open the edit page for any seeded classified, live, digital-code, art or sticker listing and its per-type fields are populated on arrival.",
|
|
2591
|
+
href: "/store/classified",
|
|
2592
|
+
},
|
|
2593
|
+
],
|
|
2594
|
+
},
|
|
2413
2595
|
]),
|
|
2414
2596
|
...group("content-discovery", "Content & Discovery", [
|
|
2415
2597
|
{
|
package/dist/index.d.ts
CHANGED
|
@@ -1095,7 +1095,6 @@ export { AdminReviewsView } from "./features/admin/index";
|
|
|
1095
1095
|
export { AdminSectionsView } from "./features/admin/index";
|
|
1096
1096
|
export { AdminSessionsManager } from "./features/admin/index";
|
|
1097
1097
|
export { AdminSidebar } from "./features/admin/index";
|
|
1098
|
-
export { AdminSiteView } from "./features/admin/index";
|
|
1099
1098
|
export { AdminSublistingCategoriesView } from "./features/admin/index";
|
|
1100
1099
|
export { AdminSublistingCategoryEditorView } from "./features/admin/index";
|
|
1101
1100
|
export type { AdminSublistingCategoryEditorViewProps } from "./features/admin/index";
|
|
@@ -1198,7 +1197,6 @@ export type { AdminReviewsViewProps } from "./features/admin/index";
|
|
|
1198
1197
|
export type { AdminSectionsViewProps } from "./features/admin/index";
|
|
1199
1198
|
export type { AdminSessionsManagerProps } from "./features/admin/index";
|
|
1200
1199
|
export type { AdminSidebarProps } from "./features/admin/index";
|
|
1201
|
-
export type { AdminSiteViewProps } from "./features/admin/index";
|
|
1202
1200
|
export type { AdminSiteSettingsViewProps } from "./features/admin/index";
|
|
1203
1201
|
export type { AdminStatCardProps } from "./features/admin/index";
|
|
1204
1202
|
export type { AdminStoresViewProps } from "./features/admin/index";
|
|
@@ -2175,6 +2173,7 @@ export { RefundHistoryTable } from "./features/orders/index";
|
|
|
2175
2173
|
export type { RefundHistoryTableProps } from "./features/orders/index";
|
|
2176
2174
|
export { RefundRequestView } from "./features/orders/index";
|
|
2177
2175
|
export type { RefundRequestViewProps } from "./features/orders/index";
|
|
2176
|
+
export type { RefundRequestSubmission } from "./features/orders/components/RefundRequestView";
|
|
2178
2177
|
export { PaymentGatewayValues } from "./features/orders/index";
|
|
2179
2178
|
export { PaymentMethodValues } from "./features/orders/index";
|
|
2180
2179
|
export { OutOfStockPolicyValues } from "./features/orders/index";
|
|
@@ -2386,6 +2385,7 @@ export { getProductFilterKeys } from "./features/products/index";
|
|
|
2386
2385
|
export { getProductSortOptions } from "./features/products/index";
|
|
2387
2386
|
export { getProductTableColumns } from "./features/products/index";
|
|
2388
2387
|
export { normalizeListingType, isAuctionListing, isPreOrderListing, isStandardListing, isPrizeDrawListing, isClassifiedListing, isDigitalCodeListing, isLiveListing, isArtListing, isStickersListing, } from "./features/products/index";
|
|
2388
|
+
export { isFinalSale, acceptsChangeOfMindReturns, } from "./features/products/constants/final-sale";
|
|
2389
2389
|
export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, } from "./_internal/shared/listing-types/feature-flags";
|
|
2390
2390
|
export { actionTracker, setActionTrackerSink, resetActionTrackerSink, type ActionEvent, type ActionTrackerSink, } from "./_internal/shared/listing-types/action-tracker";
|
|
2391
2391
|
export { cartRequiresShipping, cartIsDigitalOnly, cartIsChatOnly, } from "./_internal/shared/listing-types/cart-shipping";
|
|
@@ -3072,6 +3072,8 @@ export type { BundleQueryRule } from "./features/categories/schemas/firestore";
|
|
|
3072
3072
|
export { BUNDLE_MIN_ITEMS, BUNDLE_MAX_ITEMS, BUNDLE_MAX_PER_USER_DEFAULT, BUNDLES_PAGE_SIZE, BUNDLES_FEATURED_LIMIT, } from "./_internal/shared/features/categories/bundle-config";
|
|
3073
3073
|
export { BUNDLE_COPY, BUNDLE_STOCK_VARIANT, } from "./_internal/shared/features/categories/bundle-copy";
|
|
3074
3074
|
export { REFUND_COPY } from "./_internal/shared/features/orders/refund-copy";
|
|
3075
|
+
export { RETURN_REASON, FINAL_SALE_EXEMPT_REASONS, RETURN_REASON_LABEL, RETURN_REASON_GROUP_LABEL, isFinalSaleExempt, isReturnReason, selectableReturnReasons, } from "./_internal/shared/features/orders/return-reasons";
|
|
3076
|
+
export type { ReturnReason } from "./_internal/shared/features/orders/return-reasons";
|
|
3075
3077
|
export { bundleCreateSchema, bundleUpdateSchema, bundleQueryRuleSchema, bundleDisplaySchema, type BundleCreateBody, type BundleUpdateBody, } from "./_internal/shared/features/categories/bundle-schemas";
|
|
3076
3078
|
export type { GroupedListingDocument, GroupTheme, GroupVisibility } from "./features/grouped/schemas/firestore";
|
|
3077
3079
|
export { navItemFormSchema, navItemCreateSchema, navItemUpdateSchema, type NavItemFormValues, } from "./features/admin/schemas/nav-item-form";
|
|
@@ -3144,7 +3146,7 @@ export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_
|
|
|
3144
3146
|
export type { ListingTypePlugin } from "./_internal/shared/listing-types/_registry";
|
|
3145
3147
|
export { getListingRule, getCategoryRule, pickOrderType, getSplitKey, runSyncPreflight, CHECKOUT_RULES, CATEGORY_CHECKOUT_RULES, CHECKOUT_MAX_ORDERS_PER_TX, PRIZE_DRAW_MAX_REVEALS_PER_ORDER, BUNDLE_MAX_QTY_PER_TX, STANDARD_MAX_QTY_PER_LINE, } from "./_internal/shared/checkout/rules";
|
|
3146
3148
|
export type { ListingCheckoutRule, CategoryCheckoutRule, CartItemProductPair, OrderItemInput, RefundPolicy, } from "./_internal/shared/checkout/rules";
|
|
3147
|
-
export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, } from "./_internal/shared/media/limits";
|
|
3149
|
+
export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
|
|
3148
3150
|
export type { MediaKind, AllowedImageMime, AllowedVideoMime, AllowedDocMime, AllowedMime, } from "./_internal/shared/media/limits";
|
|
3149
3151
|
export { PRODUCT_FIELDS, PRODUCT_STATUS_TRANSITIONS, ORDER_FIELDS, REVIEW_FIELDS, BID_FIELDS, AD_FIELDS, AD_TRACKED_FIELDS, BLOG_FIELDS, BRAND_FIELDS, WISHLIST_FIELDS, HISTORY_FIELDS, COUPON_USAGE_FIELDS, CAROUSEL_FIELDS, FAQ_FIELDS, HOMEPAGE_SECTION_FIELDS, SITE_SETTINGS_FIELDS, COMMON_FIELDS, OAUTH_STATE_VALUES, SCHEMA_DEFAULTS, } from "./constants/field-names";
|
|
3150
3152
|
export { SORT_DIR, sortBy } from "./constants/sort";
|
|
@@ -3226,3 +3228,4 @@ export { ROLES_TABS, isRolesTabId, ADMIN_SETTINGS_TABS, isAdminSettingsTabId, }
|
|
|
3226
3228
|
export type { AdminPageTab, RolesTabId, AdminSettingsTabId, } from "./features/admin/constants/admin-page-tabs";
|
|
3227
3229
|
export { validateActionIndexEntry, normaliseRoute, } from "./features/search/action-index/validate-entry";
|
|
3228
3230
|
export type { ActionIndexValidationContext, ActionIndexValidationIssue, } from "./features/search/action-index/validate-entry";
|
|
3231
|
+
export { draftToProductInput, productToDraft, } from "./features/seller/utils/product-draft-mapping";
|
package/dist/index.js
CHANGED
|
@@ -2126,8 +2126,6 @@ export { AdminSessionsManager } from "./features/admin/index";
|
|
|
2126
2126
|
// AdminSidebar - Shared export for admin sidebar.
|
|
2127
2127
|
export { AdminSidebar } from "./features/admin/index";
|
|
2128
2128
|
// [CLIENT-SSR]-Runs in both SSR and browser â€" React component or hook that does not depend on browser-only APIs.
|
|
2129
|
-
// AdminSiteView - Component for admin site view.
|
|
2130
|
-
export { AdminSiteView } from "./features/admin/index";
|
|
2131
2129
|
// AdminSublistingCategoriesView - Admin list view for sublisting categories (SC1).
|
|
2132
2130
|
export { AdminSublistingCategoriesView } from "./features/admin/index";
|
|
2133
2131
|
// AdminSublistingCategoryEditorView - Admin create/edit form for sublisting categories (SC1).
|
|
@@ -4367,6 +4365,9 @@ export { getProductTableColumns } from "./features/products/index";
|
|
|
4367
4365
|
// normalizeListingType + predicate helpers â€" canonical SB1-G accessors.
|
|
4368
4366
|
// SB-UNI-F 2026-05-13 â€" Phase 2 predicates added (classified/digital-code/live).
|
|
4369
4367
|
export { normalizeListingType, isAuctionListing, isPreOrderListing, isStandardListing, isPrizeDrawListing, isClassifiedListing, isDigitalCodeListing, isLiveListing, isArtListing, isStickersListing, } from "./features/products/index";
|
|
4368
|
+
// Final sale — ABSENT MEANS TRUE, so these are the only legal reads of the
|
|
4369
|
+
// `finalSale` field. See the module docstring for why `!p.finalSale` is wrong.
|
|
4370
|
+
export { isFinalSale, acceptsChangeOfMindReturns, } from "./features/products/constants/final-sale";
|
|
4370
4371
|
// SB-UNI-X4 2026-05-13 â€" per-type feature-flag helpers.
|
|
4371
4372
|
export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, } from "./_internal/shared/listing-types/feature-flags";
|
|
4372
4373
|
// SB-UNI-X5 2026-05-13 â€" action telemetry sink.
|
|
@@ -5454,6 +5455,10 @@ export { BUNDLE_MIN_ITEMS, BUNDLE_MAX_ITEMS, BUNDLE_MAX_PER_USER_DEFAULT, BUNDLE
|
|
|
5454
5455
|
export { BUNDLE_COPY, BUNDLE_STOCK_VARIANT, } from "./_internal/shared/features/categories/bundle-copy";
|
|
5455
5456
|
// S-SBUNI-RULES 2026-05-13 â€" shared copy for refund / shipping / sibling-payment UI.
|
|
5456
5457
|
export { REFUND_COPY } from "./_internal/shared/features/orders/refund-copy";
|
|
5458
|
+
// Return / refund reasons — the closed enum the final-sale gate branches on.
|
|
5459
|
+
// See the module docstring for why the exempt set is "seller or carrier fault"
|
|
5460
|
+
// rather than "serious reasons".
|
|
5461
|
+
export { RETURN_REASON, FINAL_SALE_EXEMPT_REASONS, RETURN_REASON_LABEL, RETURN_REASON_GROUP_LABEL, isFinalSaleExempt, isReturnReason, selectableReturnReasons, } from "./_internal/shared/features/orders/return-reasons";
|
|
5457
5462
|
// S-SBUNI-4 follow-up â€" shared admin bundle zod schemas.
|
|
5458
5463
|
export { bundleCreateSchema, bundleUpdateSchema, bundleQueryRuleSchema, bundleDisplaySchema, } from "./_internal/shared/features/categories/bundle-schemas";
|
|
5459
5464
|
export { navItemFormSchema, navItemCreateSchema, navItemUpdateSchema, } from "./features/admin/schemas/nav-item-form";
|
|
@@ -5520,7 +5525,7 @@ export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_
|
|
|
5520
5525
|
// Checkout rule registry â€" pure TS, safe in both client and server bundles.
|
|
5521
5526
|
export { getListingRule, getCategoryRule, pickOrderType, getSplitKey, runSyncPreflight, CHECKOUT_RULES, CATEGORY_CHECKOUT_RULES, CHECKOUT_MAX_ORDERS_PER_TX, PRIZE_DRAW_MAX_REVEALS_PER_ORDER, BUNDLE_MAX_QTY_PER_TX, STANDARD_MAX_QTY_PER_LINE, } from "./_internal/shared/checkout/rules";
|
|
5522
5527
|
// Media upload limits â€" pure constants, safe in both client and server bundles.
|
|
5523
|
-
export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, } from "./_internal/shared/media/limits";
|
|
5528
|
+
export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
|
|
5524
5529
|
// ── Field name constants — canonical source of truth for all Firestore field names ──
|
|
5525
5530
|
// NOTE: STORE_FIELDS, CATEGORY_FIELDS, EVENT_FIELDS, EVENT_ENTRY_FIELDS, PAYOUT_FIELDS,
|
|
5526
5531
|
// COUPON_FIELDS, CART_FIELDS, USER_FIELDS, ADDRESS_FIELDS, NOTIFICATION_FIELDS,
|
|
@@ -5629,3 +5634,7 @@ export { SITE_SETTINGS_TABS, SITE_SETTINGS_TAB_IDS, isSiteSettingsTabId, } from
|
|
|
5629
5634
|
export { PAYOUTS_TABS, isPayoutsTabId, SHIPPING_TABS, isShippingTabId, FULFILLMENT_TABS, isFulfillmentTabId, STOREFRONT_TABS, isStorefrontTabId, } from "./features/seller/constants/store-tabs";
|
|
5630
5635
|
export { ROLES_TABS, isRolesTabId, ADMIN_SETTINGS_TABS, isAdminSettingsTabId, } from "./features/admin/constants/admin-page-tabs";
|
|
5631
5636
|
export { validateActionIndexEntry, normaliseRoute, } from "./features/search/action-index/validate-entry";
|
|
5637
|
+
// Seller listing form draft <-> product write payload. The flat form keys and
|
|
5638
|
+
// the nested document blocks are different shapes, and nothing translated
|
|
5639
|
+
// between them — see the module docstring.
|
|
5640
|
+
export { draftToProductInput, productToDraft, } from "./features/seller/utils/product-draft-mapping";
|
|
@@ -146,6 +146,7 @@ export declare const DEFAULT_ROUTE_MAP: {
|
|
|
146
146
|
readonly ORDER_DETAIL: (id: string) => string;
|
|
147
147
|
readonly ORDER_CANCEL: (id: string) => string;
|
|
148
148
|
readonly ORDER_TRACK: (id: string) => string;
|
|
149
|
+
readonly ORDER_RETURN: (id: string) => string;
|
|
149
150
|
readonly ORDER_INVOICE: (id: string) => string;
|
|
150
151
|
readonly ORDER_PAYMENT: (id: string) => string;
|
|
151
152
|
readonly NOTIFICATIONS: "/user/notifications";
|
|
@@ -549,6 +550,7 @@ export declare const ROUTES: {
|
|
|
549
550
|
readonly ORDER_DETAIL: (id: string) => string;
|
|
550
551
|
readonly ORDER_CANCEL: (id: string) => string;
|
|
551
552
|
readonly ORDER_TRACK: (id: string) => string;
|
|
553
|
+
readonly ORDER_RETURN: (id: string) => string;
|
|
552
554
|
readonly ORDER_INVOICE: (id: string) => string;
|
|
553
555
|
readonly ORDER_PAYMENT: (id: string) => string;
|
|
554
556
|
readonly NOTIFICATIONS: "/user/notifications";
|
|
@@ -150,6 +150,7 @@ export const DEFAULT_ROUTE_MAP = {
|
|
|
150
150
|
ORDER_DETAIL: (id) => `/user/orders/view/${id}`,
|
|
151
151
|
ORDER_CANCEL: (id) => `/user/orders/${id}/cancel`,
|
|
152
152
|
ORDER_TRACK: (id) => `/user/orders/${id}/track`,
|
|
153
|
+
ORDER_RETURN: (id) => `/user/orders/${id}/return`,
|
|
153
154
|
ORDER_INVOICE: (id) => `/user/orders/${id}/invoice`,
|
|
154
155
|
ORDER_PAYMENT: (id) => `/user/orders/${id}/payment`,
|
|
155
156
|
NOTIFICATIONS: "/user/notifications",
|