Skip to content

Commit 7db5f84

Browse files
committed
Reassure users about what was just verified, given the active stock availability mode
1 parent 97fd94c commit 7db5f84

7 files changed

Lines changed: 286 additions & 38 deletions

File tree

.changeset/curly-doves-dance.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"saleor-dashboard": patch
3+
---
4+
5+
ProductDoctor: Add mode-aware reassurance under public-API verification badge and tighten ambiguous diagnostic copy

locale/defaultMessages.json

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2841,6 +2841,10 @@
28412841
"context": "Warning when warehouse with stock not in shipping zone",
28422842
"string": "Stock unreachable in \"{channelName}\""
28432843
},
2844+
"EURyYW": {
2845+
"context": "Reassurance under verification badge when product is purchasable in direct mode",
2846+
"string": "Storefront API confirms the product is purchasable. Stock availability is taken directly from the warehouse-channel link, regardless of shipping zones."
2847+
},
28442848
"EXqb2l": {
28452849
"string": "Go to Saleor Cloud"
28462850
},
@@ -5141,10 +5145,6 @@
51415145
"context": "years after label",
51425146
"string": "years after issue"
51435147
},
5144-
"QfK+5Q": {
5145-
"context": "Description for warehouse not in zone warning",
5146-
"string": "Warehouses with stock are not assigned to any shipping zone in this channel."
5147-
},
51485148
"Qhb89u": {
51495149
"context": "Warning when no countries covered",
51505150
"string": "No countries covered"
@@ -5545,10 +5545,6 @@
55455545
"context": "header",
55465546
"string": "Create Variant"
55475547
},
5548-
"T7r26L": {
5549-
"context": "Description for no warehouses warning",
5550-
"string": "Products in this channel cannot be fulfilled without warehouses."
5551-
},
55525548
"T83iU7": {
55535549
"string": "Search categories..."
55545550
},
@@ -5952,6 +5948,10 @@
59525948
"VNX4fn": {
59535949
"string": "Proceed"
59545950
},
5951+
"VNXfqA": {
5952+
"context": "Reassurance under verification badge when product is purchasable in legacy mode",
5953+
"string": "Storefront API confirms the product is purchasable for customers covered by the channel's shipping zones."
5954+
},
59555955
"VOiUXQ": {
59565956
"string": "Used to calculate rates for shipping for products of this product type, when specific weight is not given"
59575957
},
@@ -6733,6 +6733,10 @@
67336733
"ZdCdo5": {
67346734
"string": "Last updated: {date}"
67356735
},
6736+
"ZemrGW": {
6737+
"context": "Reassurance under verification badge when product is not visible to public API",
6738+
"string": "The storefront API cannot find this product. Confirm it is published and listed in this channel."
6739+
},
67366740
"ZepEWY": {
67376741
"context": "Dry run item",
67386742
"string": "Item"
@@ -8264,6 +8268,10 @@
82648268
"context": "tooltip",
82658269
"string": "Stock allocations occur when:"
82668270
},
8271+
"icyljC": {
8272+
"context": "Description for no warehouses warning",
8273+
"string": "Without an assigned warehouse, this product will appear unavailable to customers in this channel and orders cannot be fulfilled."
8274+
},
82678275
"icz/jb": {
82688276
"context": "customer",
82698277
"string": "Join Date"
@@ -8708,6 +8716,10 @@
87088716
"krer6Z": {
87098717
"string": "Features preview"
87108718
},
8719+
"ksIgHn": {
8720+
"context": "Description for warehouse-not-in-zone warning (only fires in legacy stock availability mode)",
8721+
"string": "Warehouses with stock are not covered by any shipping zone in this channel. In legacy stock availability mode this hides the product from customers; assign the warehouse to a shipping zone or switch to direct stock availability."
8722+
},
87118723
"kuo4fW": {
87128724
"context": "dialog title",
87138725
"string": "Capture manual transaction"
@@ -10847,6 +10859,10 @@
1084710859
"context": "relative time",
1084810860
"string": "just now"
1084910861
},
10862+
"wPODVm": {
10863+
"context": "Reassurance under verification badge when product is not purchasable",
10864+
"string": "The storefront API does not return this product as purchasable. Review the issues listed above to resolve."
10865+
},
1085010866
"wQdR8M": {
1085110867
"string": "Add search engine title and description to make this category easier to find"
1085210868
},

src/products/components/ProductDoctor/AvailabilityCard.test.tsx

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import Wrapper from "@test/wrapper";
22
import { fireEvent, render, screen } from "@testing-library/react";
33

4-
import { AvailabilityCard } from "./AvailabilityCard";
4+
import { AvailabilityCard, PublicApiVerificationBadge } from "./AvailabilityCard";
5+
import { type ChannelVerificationResult } from "./hooks/usePublicApiVerification";
56
import { type AvailabilityIssue, type DiagnosticsResult } from "./utils/types";
67

78
// usePublicApiVerification hits the live API; stub it for isolated UI tests.
@@ -206,3 +207,116 @@ describe("AvailabilityCard channel header severity gating", () => {
206207
expect(badge).toHaveAttribute("data-test-count", "2");
207208
});
208209
});
210+
211+
const makeVerification = (
212+
overrides: Partial<ChannelVerificationResult["result"]> & {
213+
status?: ChannelVerificationResult["status"];
214+
} = {},
215+
): ChannelVerificationResult => {
216+
const { status = "success", ...resultOverrides } = overrides;
217+
218+
return {
219+
channelId: "channel-1",
220+
channelSlug: "default-channel",
221+
status,
222+
error: null,
223+
result:
224+
status === "success"
225+
? {
226+
productFound: true,
227+
isAvailable: true,
228+
isAvailableForPurchase: true,
229+
availableForPurchaseAt: "2024-01-01T00:00:00Z",
230+
variantsWithStock: 2,
231+
totalVariants: 2,
232+
variants: [],
233+
...resultOverrides,
234+
}
235+
: null,
236+
};
237+
};
238+
239+
describe("PublicApiVerificationBadge reassurance", () => {
240+
it("shows the legacy-mode reassurance when product is purchasable in legacy mode", () => {
241+
// Arrange
242+
const result = makeVerification({ isAvailable: true, variantsWithStock: 2 });
243+
244+
// Act
245+
render(
246+
<PublicApiVerificationBadge result={result} useLegacyShippingZoneStockAvailability={true} />,
247+
{ wrapper: Wrapper },
248+
);
249+
250+
// Assert
251+
const reassurance = screen.getByTestId("verification-reassurance");
252+
253+
expect(reassurance).toHaveAttribute("data-test-reassurance", "purchasable-legacy");
254+
// Legacy reassurance must reference shipping zones explicitly so the user
255+
// understands what was verified.
256+
expect(reassurance).toHaveTextContent(/shipping zones/i);
257+
expect(reassurance).toHaveTextContent(/purchasable/i);
258+
});
259+
260+
it("shows the direct-mode reassurance when product is purchasable in direct mode", () => {
261+
// Arrange
262+
const result = makeVerification({ isAvailable: true, variantsWithStock: 2 });
263+
264+
// Act
265+
render(
266+
<PublicApiVerificationBadge result={result} useLegacyShippingZoneStockAvailability={false} />,
267+
{ wrapper: Wrapper },
268+
);
269+
270+
// Assert
271+
const reassurance = screen.getByTestId("verification-reassurance");
272+
273+
expect(reassurance).toHaveAttribute("data-test-reassurance", "purchasable-direct");
274+
// Direct-mode reassurance must call out the direct warehouse-channel link
275+
// so the user knows shipping zones don't gate availability in this mode.
276+
expect(reassurance).toHaveTextContent(/warehouse-channel link/i);
277+
expect(reassurance).toHaveTextContent(/regardless of shipping zones/i);
278+
});
279+
280+
it("points to the issue list when verification reports not purchasable", () => {
281+
// Arrange
282+
const result = makeVerification({ isAvailable: false, variantsWithStock: 0 });
283+
284+
// Act
285+
render(<PublicApiVerificationBadge result={result} />, { wrapper: Wrapper });
286+
287+
// Assert
288+
const reassurance = screen.getByTestId("verification-reassurance");
289+
290+
expect(reassurance).toHaveAttribute("data-test-reassurance", "not-purchasable");
291+
expect(reassurance).toHaveTextContent(/review the issues listed above/i);
292+
});
293+
294+
it("points to publish/listing config when product is not visible to the API", () => {
295+
// Arrange
296+
const result = makeVerification({ productFound: false });
297+
298+
// Act
299+
render(<PublicApiVerificationBadge result={result} />, { wrapper: Wrapper });
300+
301+
// Assert
302+
const reassurance = screen.getByTestId("verification-reassurance");
303+
304+
expect(reassurance).toHaveAttribute("data-test-reassurance", "not-visible");
305+
expect(reassurance).toHaveTextContent(/published and listed/i);
306+
});
307+
308+
it("does not render reassurance during loading or after errors", () => {
309+
// Arrange / Act / Assert - loading state
310+
const { rerender } = render(
311+
<PublicApiVerificationBadge result={makeVerification({ status: "loading" })} />,
312+
{ wrapper: Wrapper },
313+
);
314+
315+
expect(screen.queryByTestId("verification-reassurance")).toBeNull();
316+
317+
// Error state
318+
rerender(<PublicApiVerificationBadge result={makeVerification({ status: "error" })} />);
319+
320+
expect(screen.queryByTestId("verification-reassurance")).toBeNull();
321+
});
322+
});

src/products/components/ProductDoctor/AvailabilityCard.tsx

Lines changed: 84 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,9 @@ export const AvailabilityCard = ({
267267
? () => verification.verifyChannel(summary.id, summary.slug)
268268
: undefined
269269
}
270+
useLegacyShippingZoneStockAvailability={
271+
useLegacyShippingZoneStockAvailability
272+
}
270273
/>
271274
);
272275
})}
@@ -474,9 +477,15 @@ const StockAvailabilityModeIndicator = ({
474477

475478
interface PublicApiVerificationBadgeProps {
476479
result: ChannelVerificationResult;
480+
/** Active stock-availability mode. Drives the reassurance line so users know
481+
* what was just verified given the shop's mode. Defaults to legacy. */
482+
useLegacyShippingZoneStockAvailability?: boolean;
477483
}
478484

479-
export const PublicApiVerificationBadge = ({ result }: PublicApiVerificationBadgeProps) => {
485+
export const PublicApiVerificationBadge = ({
486+
result,
487+
useLegacyShippingZoneStockAvailability = true,
488+
}: PublicApiVerificationBadgeProps) => {
480489
const intl = useIntl();
481490

482491
if (result.status === "loading") {
@@ -511,43 +520,95 @@ export const PublicApiVerificationBadge = ({ result }: PublicApiVerificationBadg
511520

512521
if (!productFound) {
513522
return (
514-
<Box display="flex" alignItems="center" gap={1}>
515-
<XCircle size={14} color="var(--mu-colors-text-default2)" />
516-
<Text size={1} color="default2">
517-
{intl.formatMessage(messages.publicApiNotVisible)}
518-
</Text>
519-
</Box>
523+
<PublicApiVerificationBadgeShell
524+
icon={<XCircle size={14} color="var(--mu-colors-text-default2)" />}
525+
statusColor="default2"
526+
status={intl.formatMessage(messages.publicApiNotVisible)}
527+
reassurance={intl.formatMessage(messages.verificationReassurance_notVisible)}
528+
reassuranceTestId="not-visible"
529+
/>
520530
);
521531
}
522532

523533
if (isAvailable && variantsWithStock > 0) {
524534
return (
525-
<Box display="flex" alignItems="center" gap={1}>
526-
<CheckCircle size={14} color="var(--mu-colors-text-success1)" />
527-
<Text size={1} color="success1">
528-
{intl.formatMessage(messages.publicApiPurchasable)}
529-
</Text>
530-
<Text size={1} color="default2">
531-
· {intl.formatMessage(messages.publicApiVariantsInStock, { count: variantsWithStock })}
532-
</Text>
533-
</Box>
535+
<PublicApiVerificationBadgeShell
536+
icon={<CheckCircle size={14} color="var(--mu-colors-text-success1)" />}
537+
statusColor="success1"
538+
status={intl.formatMessage(messages.publicApiPurchasable)}
539+
statusSuffix={intl.formatMessage(messages.publicApiVariantsInStock, {
540+
count: variantsWithStock,
541+
})}
542+
reassurance={intl.formatMessage(
543+
useLegacyShippingZoneStockAvailability
544+
? messages.verificationReassurance_purchasableLegacy
545+
: messages.verificationReassurance_purchasableDirect,
546+
)}
547+
reassuranceTestId={
548+
useLegacyShippingZoneStockAvailability ? "purchasable-legacy" : "purchasable-direct"
549+
}
550+
/>
534551
);
535552
}
536553

537554
return (
555+
<PublicApiVerificationBadgeShell
556+
icon={<XCircle size={14} color="var(--mu-colors-text-warning1)" />}
557+
statusColor="warning1"
558+
status={intl.formatMessage(messages.publicApiNotPurchasable)}
559+
statusSuffix={
560+
variantsWithStock === 0
561+
? intl.formatMessage(messages.publicApiNoVariantsInStock)
562+
: undefined
563+
}
564+
reassurance={intl.formatMessage(messages.verificationReassurance_notPurchasable)}
565+
reassuranceTestId="not-purchasable"
566+
/>
567+
);
568+
};
569+
570+
interface PublicApiVerificationBadgeShellProps {
571+
icon: React.ReactNode;
572+
statusColor: "success1" | "warning1" | "default2";
573+
status: string;
574+
/** Optional secondary status fragment (e.g. variants-in-stock count). */
575+
statusSuffix?: string;
576+
/** Mode-aware explanatory line that reassures the user about what was just
577+
* verified given the active stock-availability mode. */
578+
reassurance: string;
579+
reassuranceTestId: string;
580+
}
581+
582+
const PublicApiVerificationBadgeShell = ({
583+
icon,
584+
statusColor,
585+
status,
586+
statusSuffix,
587+
reassurance,
588+
reassuranceTestId,
589+
}: PublicApiVerificationBadgeShellProps) => (
590+
<Box display="flex" flexDirection="column" gap={0.5}>
538591
<Box display="flex" alignItems="center" gap={1}>
539-
<XCircle size={14} color="var(--mu-colors-text-warning1)" />
540-
<Text size={1} color="warning1">
541-
{intl.formatMessage(messages.publicApiNotPurchasable)}
592+
{icon}
593+
<Text size={1} color={statusColor}>
594+
{status}
542595
</Text>
543-
{variantsWithStock === 0 && (
596+
{statusSuffix && (
544597
<Text size={1} color="default2">
545-
· {intl.formatMessage(messages.publicApiNoVariantsInStock)}
598+
· {statusSuffix}
546599
</Text>
547600
)}
548601
</Box>
549-
);
550-
};
602+
<Text
603+
size={1}
604+
color="default2"
605+
data-test-id="verification-reassurance"
606+
data-test-reassurance={reassuranceTestId}
607+
>
608+
{reassurance}
609+
</Text>
610+
</Box>
611+
);
551612

552613
interface ChannelSearchInputProps {
553614
value: string;

0 commit comments

Comments
 (0)