Skip to content

Merge feature/264-order-management into develop @W-23254713 - #3914

Merged
sf-jie-dai merged 34 commits into
developfrom
feature/264-order-management
Jul 1, 2026
Merged

Merge feature/264-order-management into develop @W-23254713#3914
sf-jie-dai merged 34 commits into
developfrom
feature/264-order-management

Conversation

@sf-jie-dai

@sf-jie-dai sf-jie-dai commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Integrates the feature/264-order-management branch into develop. This lands the
full Salesforce Order Management (OMS/SOM) shopper experience on the retail-react-app
order detail / account pages — grouped below into the three shopper actions it delivers:
Order Returns, Order Cancellation, and Shipment Tracking — plus the shared
foundation they build on.

25 commits, built up across the WIs below. All constituent changes already merged to the
feature branch via their own reviewed PRs; this PR is the integration merge.

Order Returns

Order Cancellation

Shipment Tracking

Shared foundation

Supporting work the three flows above depend on:

Integration fixes (post branch-open)

Applied on this branch to make the merge into develop clean and green — not part of the
original feature work:

  • Pin commerce-sdk-isomorphic to stable 5.4.0. The branch carried a temporary
    5.1.0-unstable-* dev build. develop is on 5.2.0, which has no OMS Shopper Orders
    endpoints; 5.4.0 is the first stable release that ships oms-return-order,
    oms-cancel-order, and oms-meta-data, so the OMS hooks require it. Build + tsc clean.
  • List the new 5.4.0 endpoints as unimplemented. The 5.2.0 → 5.4.0 bump adds 8 endpoints
    (across ShopperLogin, ShopperCustomers, ShopperBasketsV2, ShopperExperience, ShopperProducts)
    that don't have React hooks yet, which tripped the SDK's "all endpoints have hooks" guard
    tests. Added each to the test's expected unimplemented list with a TODO per the test's own
    convention; implementing those hooks is out of scope for this OMS integration. Full
    commerce-sdk-react suite is 778/778 green.
  • Add the missing commerce-sdk-react CHANGELOG entry for the OMS order-action hooks
    (cancelOmsOrder/returnOmsOrder mutations + getOmsMetaData), which had landed without
    one — required to pass the per-package changelog check.
  • Bump the vendor.js bundle-size budget 397 kB → 398 kB. The 5.4.0 isomorphic upgrade
    pushes vendor.js to 397.07KB, just over the old budget; main.js is unchanged.
  • Address review feedback (ddiazccrz). Added explicit rel="noopener noreferrer" to the
    external carrier tracking links (Chakra isExternal alone only emits noopener, leaking the
    order-page URL as Referer); hardened canCancel/return ownership checks to require a
    concrete customerId and guard the empty-productItems case; corrected the ensureExternalUrl
    JSDoc; and fixed a stale CHANGELOG line that referenced the removed app.oms.enabled flag.
  • Tighten the tracking-link tests to assert noreferrer (not just noopener) so a
    regression that drops the token is actually caught.
  • CX UI text guidelines pass on the cancel/return flows. Button caps and copy per the review
    sheet: Keep Order, Confirm Cancellation, Review Return, Submit Return,
    Track Shipment {number}, and Cancel the entire order. (source + en-US/en-GB/en-XA
    translations regenerated).

Testing

Each constituent PR carried its own unit/integration coverage and passed CI on merge to the
feature branch. CI on this integration PR is the gate before merge.

sf-madhuri-uppu and others added 25 commits June 11, 2026 18:45
…dk-react (#3864)

* @W-22931003@ Add cancelOmsOrder and getOmsMetaData hooks to commerce-sdk-react

- Add CancelOmsOrder to ShopperOrdersMutations enum (mutation.ts)
- Add useOmsMetaData query hook for fetching cancel/return reason codes (query.ts)
- Add getOmsMetaData query key helper (queryKeyHelpers.ts)
- Add cancelOmsOrder cache invalidation: invalidates order query on success (cache.ts)
- Update query test map with useOmsMetaData
- Add returnOmsOrder to unimplemented endpoints list (available in SDK but no hook yet)

Requires commerce-sdk-isomorphic with shopper-orders-oas v1.14.1
(branch: update-shopper-orders-1.14.1, not yet published to npm)

* Add changelog entry for cancelOmsOrder and getOmsMetaData hooks

* Address PR review feedback

- Invalidate getCustomerOrders cache on cancel (order history shows updated status)
- Add comments for unimplemented endpoints in test file
- Bump commerce-sdk-isomorphic to 5.1.0-unstable-20260611093359 (preview with OAS v1.14.1)

* Update package-lock.json for commerce-sdk-isomorphic unstable version

* Fix prettier: inline single-element array in cancelOmsOrder cache

* Fix tests for unstable SDK: remove endpoints no longer in preview version

- Remove setupCustomerPaymentMethodReference from ShopperCustomers unimplemented list
- Remove resolveQualifiers from ShopperExperience unimplemented list

* Remove changelog entry (will add when merging to develop)

* Add unit tests for cancelOmsOrder mutation and cache invalidation
@W-22918057 Extract standalone OrderTracking component

Pure refactor: move the inline tracking render from renderShippingMethod in
order-detail.jsx into a standalone, testable app/components/order-tracking
component, wired at both call sites (delivery/OMS-fallback and OMS-multi). The
OMS-multi call site no longer needs a wrapping React.Fragment (key moves onto
OrderTracking), and the now-unused ChakraLink import is removed from
order-detail.jsx.

DOM output is identical and i18n message IDs are unchanged (shipping-status
descriptors are kept as inline formatMessage literals so babel-plugin-formatjs
still statically extracts them). The existing orders.test.js suite plus new
isolated component tests pass (96/96); lint clean.

First of the Order Tracking epic work items.
@W-22918455 Render estimated delivery date on order tracking

Render expectedDeliveryDate and actualDeliveryDate on the OrderTracking
component as locale-formatted "Expected delivery: <date>" / "Delivered: <date>"
lines, shown when present and omitted when absent (or when the date is missing
or malformed — formatTrackingDate returns null on an unparseable value so it
never renders "Invalid Date" or throws). Wired at both order-detail call sites;
these fields are OMS-only (no ECOM fallback — they exist only on
omsData.shipments[]).

Adds two i18n keys (extracted + compiled). Unit and page tests cover the
present, absent, and malformed cases across single, multi-shipment, and
ECOM-fallback paths.
* @W-22918457 Guard null delivery dates in OrderTracking

QA on W-22918455 found that new Date(null) returns the epoch (1970-01-01), not Invalid Date, so a null expectedDeliveryDate/actualDeliveryDate slipped past the isNaN guard and rendered '31 Dec 1969' to the shopper. Add a falsy guard (catches null, undefined, '') to formatTrackingDate, with a regression test for the explicit-null case. Defensive regardless of whether OMS serializes null on the wire.

* @W-22918457 Re-trigger CI (flaky e2e SLAS login)
Consume useOrder's isError and render a full-card error (OrderNotFoundCard-style: square corners, title + description + Back to Order History) instead of hanging on the loading skeleton forever (the AC6 bug). A successful order with no omsData is NOT an error — it renders normally via the ECOM fallback. Adds the OrderLoadError component, i18n keys, and unit tests for the error, back-link, and no-omsData paths. Partial (products-only) failure keeps graceful degradation per the team's decision.
… can ingest(#3875)

@W-22952388@ Sync payment-instrument amount before createOrder so OMS can ingest
…3878)

* @W-22821836@ Invalidate customer orders cache after createOrder

After a registered shopper places an order, useCustomerOrders kept
serving the stale (pre-order) result and only refreshed on a fresh
login, because createOrder's cache-update matrix only invalidated
getCustomerBaskets. Add getCustomerOrders to the same invalidate list
so the account order-history page reflects the new order immediately.
Guest checkouts (no customerId) remain a no-op.
* @W-22806925@ Integrate SCAPI Cancel Order API with UI

- Replace dummy cancel handler with real useShopperOrdersMutation(CancelOmsOrder)
- Fetch reason codes from useOmsMetaData API when modal opens
- Update cancel eligibility: omsData must exist + quantityAvailableToCancel === quantityOrdered for all items
- Remove oms.enabled config flag — cancel visibility driven by order.omsData presence
- Remove hardcoded CANCELLATION_REASONS — dropdown populated from API response
- Hide dropdown when no reason codes available (allows cancel without reason)
- Pass isSubmitting from mutation state to disable modal buttons during request
- Show skeleton while reason codes are loading
- Remove extracted translation messages for hardcoded reasons
- Add mock for useShopperOrdersMutation/useOmsMetaData in order detail tests

* Fix: keep modal open during API call, close only after response

* Fix CI: prettier formatting and add changelog entry

* Revert changelog entry — will add when merging to develop

* Fix prettier: correct indentation for map, revert formatMessage to multiline

* Pre-fetch OMS metadata on page load instead of on modal open

* Fix prettier: inline useOmsMetaData and formatMessage calls

* Fix prettier conflict: extract formatMessage to variables (under 100 char width)

* Fix prettier: match showCancelSuccess format with showCancelError

* Address Jie's review: initialize default reason and add no-reason test

- Pre-select the default reason code when modal opens (from API response)
- Add test: passes empty string when no reason codes provided and confirm clicked
- Update existing tests to expect default reason pre-selection

* Fix lint and tighten canCancel check

- Add item.omsData != null guard to prevent undefined === undefined passing
- Inline formatMessage in showCancelSuccess (satisfies generated-project prettier)
- Inline test props (satisfies generated-project prettier)

* Fix prettier: revert showCancelSuccess formatMessage to multiline (matches showCancelError)
feat: surface OMS return eligibility on order detail
…sages (#3884)

* @W-22930993@ Handle cancel order API error scenarios with specific messages

- 404: "Unable to cancel order" / "We could not find this order. Please refresh and try again."
- 409: "Unable to cancel order" / "This order is already being processed and cannot be canceled. Please reach out to the Merchant."
- Everything else (400, 401, 500, network): "Something went wrong" / "We couldn't process your cancellation right now. Please wait a moment and try again."

Error status is read from error.response.status (commerce-sdk-isomorphic ResponseError shape).
Cancel button remains enabled on error to allow retry.

* Fix prettier: inline defaultMessage values

* Address review: disable button on terminal errors, fix copy

- Disable cancel button after 404/409 (terminal — retrying won't help)
- Keep button enabled for transient errors (500/network) to allow retry
- Fix spelling: "canceled" → "cancelled", "Merchant" → "merchant"
- Reword 409 copy: "Please contact the merchant for assistance."

* Address review: revert copy change, fix spelling only, add error tests

- Revert message rewording — keep original copy, only fix "canceled" → "cancelled"
  and "Merchant" → "merchant"
- Add unit tests for cancel error scenarios (404, 409, 500, network failure)
- Tests verify correct title and description for each error code

* Fix lint: split long defaultMessage lines, multiline isDisabled, remove unused act
…s unavailable (#3883)

* @W-22930989@ Hide cancellation reason dropdown when OMS metadata API fails

The dropdown is already hidden when reasonCodes is undefined (metadata
fetch failed). Add retry: 1 to the useOmsMetaData query so React Query
automatically retries once before giving up. If both attempts fail,
the modal renders without the dropdown and the shopper can still
confirm cancellation — the server applies the default reason.

* Add unit tests for metadata API failure scenarios

* Address review: remove retry override, conditional modal text

- Remove retry: 1 — let TanStack default (3) handle resilience
- Show "Confirm cancellation below." when dropdown is hidden (no reason codes)
- Show "Select a reason and confirm cancellation." when dropdown is visible
- Update test for conditional text
feat: return-item-selection modal on order detail
…3895)

feat: add return review step + returnOmsOrder submission
* @W-22806929@ Add comprehensive cancel order integration tests

12 tests covering:
- Eligibility: OMS order shows cancel, non-OMS hides it, partially shipped
  disables, already-cancelled disables, wrong customer disables
- Happy path: modal opens, API called with correct payload, success feedback
- Reason selection: submits selected reason to API
- Error states: button disabled after 409 (terminal), enabled after 500 (retry)
- Modal UX: keep order closes without API call, no-reason-codes shows
  alternative text

* Address review: fix vacuous tests and lint errors

- Fix "cancel submits with selected reason": rewrite as "cancel submits
  empty body when no reason codes available" — tests the actual contract
  (cancelReasonCodes: [] means no dropdown, empty body sent to API)
- Fix "does not show cancel button for different customer": rename to
  "disables cancel button", assert unconditionally (button IS rendered
  but disabled for OMS orders with wrong customerId)
- Rename "quantityAvailableToCancel < quantityOrdered" to "not fully
  cancellable" — clearer wording matching the strict-equality contract
- Fix prettier: multiline mock resolved values
- Remove conditional expects (jest/no-conditional-expect lint rule)
merge develop into feature/264-order-management
…r badges (#3902)

* feat: wire getOrderDisplayStatus into order badges @W-23093717

Use item-level status derivation to show Cancelled badge (red with ×
icon) on both order-detail and order-history pages when all items are
cancelled. Non-cancelled orders retain the existing raw status display.

* Address review: hoist isCancelled out of IIFEs, add negative tests

- Hoist `isCancelled` derivation above the JSX return in order-detail
  and to the top of the .map() callback in order-history, removing the
  inline IIFEs.
- Add negative tests asserting a partially-cancelled order does NOT
  render the "Cancelled" badge (regression guard).

* Address review: memoize isCancelled, add OMS-only scope comment

- Wrap isCancelled in useMemo on order-detail to avoid recomputing on
  every render (matches canCancel/returnableItems pattern).
- Add comment clarifying the derivation is OMS-only; pure ECOM
  cancellations fall through to the raw status string in the badge.
- Fix broken import from merge (return-error-utils missing `import {`).

* Extract shared OrderStatusBadge component

De-duplicate the cancelled badge JSX from order-detail and order-history
into a reusable component with memoized status derivation. Unifies the
message ID (order_status_badge.label.cancelled) so translators only
localize the string once. Prepares a clean extension point for follow-up
status WIs.

---------

Signed-off-by: sf-madhuri-uppu <madhuri.uppu@salesforce.com>
feat: show return status in order status badge
…#3898)

* fix(order-tracking): make carrier tracking links external

A carrier tracking URL can arrive without a scheme (e.g. `www.carrier.com/t`).
Rendered directly in an `href`, the browser treats it as a relative path and
resolves it against the current page, so the tracking-number link navigated
inside the app (`/account/orders/www.carrier.com/t`) instead of going to the
carrier.

Add `ensureExternalUrl` (app/utils/url.js): prepend `https://` to a scheme-less
URL, keep already-absolute http(s) URLs, and reject unsafe or non-web values
(`javascript:`, `data:`, `mailto:`, app-internal paths, …) by returning
`undefined` so the link renders inactive rather than executable or wrong. Apply
it to the OrderTracking tracking-number link.

* chore: trigger CI re-run

* harden ensureExternalUrl per review feedback

Address review findings on the carrier-tracking-link normalizer. The function
trusted the parsed scheme but never validated the resulting host, so several
inputs produced an href that read like the carrier yet navigated elsewhere.

- Reject userinfo: `https://www.ups.com@evil.com` parses to host `evil.com`
  with `www.ups.com` as the username — now rejected on both parse branches.
- Reject backslash forms (`\` is treated as `/` by the URL parser) and run the
  relative-path guard on the raw input so a control char between slashes can't
  collapse `/x/host` into a protocol-relative `//host`.
- Reject non-string input (was throwing on a non-string and crashing the render).
- Reject bare filenames / empty-label hosts (e.g. `data.html`, `a..b`) that
  aren't real external hosts.
- Drop the unreachable http branch and the dead IPv4 check.

Centralize the checks in `isSafeExternalUrl` (http(s) only, no userinfo,
plausible host), applied to both the absolute and prepended candidates. Add
regression tests asserting the resolved host for each case.

* harden ensureExternalUrl against dotted-protocol host confusion

A scheme-less value shaped `label.tld://host` parses to a dotted protocol, so it skipped the dot-less validation and fell through to be re-prepended into `https://label.tld//host` — a link that reads like one host but navigates to another (same class as the userinfo spoof). Reject a dotted-protocol parse that carries an authority; a genuine `host:port` has an empty host on that parse and still resolves. Review feedback.
…#3906)

* feat: add Track Shipment button to order detail @W-23091033

Render a "Track Shipment" order action per OMS shipment that has a
tracking URL — each button opens its own shipment's carrier URL in a new
tab (same destination as the tracking-number link), with no numeric
suffix. When no shipment has a tracking URL, a single disabled button is
shown. The Order Actions row is now full-width stacked on mobile and
inline from the sm breakpoint up, so additional buttons no longer
overflow on narrow screens.

* feat: per-shipment tracking cards + single track action @W-23091033

Align the order-detail tracking UI with storefront-next and the order
tracking epic's "flat list, no address association" scope:

- Render tracking as a flat list of per-shipment cards (one card per
  omsData.shipments[] entry, ECOM order.shipments[] fallback), each with
  carrier, status, tracking-number link, and delivery dates. No address
  on the cards and no positional OMS-to-ECOM index join.
- Consolidate the Track Shipment order action to a single button that
  links to the first shipment with a carrier URL (disabled when none),
  matching storefront-next getTrackShipmentHref.
- Move the shipping address to a single order-level block shown only for
  single-shipment delivery orders; multi-shipment orders no longer
  associate an address per shipment.

Updates the order detail tests and extracts the new Tracking heading
message.

* feat: per-shipment item boxes on order detail @W-23091033

Restructure the Items Ordered area to mirror storefront-next: render one
bordered box per ECOM delivery shipment, each with a "Shipment N" header,
that shipment's status, its items (grouped strictly by shipmentId; all items
when there's a single delivery shipment), and its own native shipping address
and method. Pickup-only orders (no delivery shipment) fall back to a flat item
list.

The top summary card no longer carries a standalone shipping-address block —
the address now lives inside each shipment box. The Tracking cards stay in a
separate flat section (OMS-preferred, ECOM fallback) and carry no address:
there is no reliable join key between an OMS shipment and a specific ECOM
shipment, so a per-shipment box never index-joins OMS data (the deferred TD).

Reconcile the multi-shipment / BOPIS tests to the new per-box behavior: they
previously asserted addresses were hidden and only passed because the products
endpoint was unmocked (which suppressed the box render); they now mock products
and assert each shipment's own address renders in its box, plus strict
item-to-box placement by shipmentId.

* feat: move tracking inside the first shipment box @W-23091033

Match the storefront-next design branch: the flat tracking list now renders
INSIDE the first shipment box, after its items and before its shipping address,
instead of as a standalone section above the items. It is still the whole flat
list (rendered in box 1 only, never split per shipment), so there is no
positional OMS↔ECOM index-join. Pickup-only orders (no delivery box) append the
flat tracking list after the flat item list so tracking is never lost.

Update the regression-lock test: it previously asserted tracking was a separate
section; it now asserts tracking sits inside the first shipment box with the
shipping address as a sibling block (no address inside a tracking card).

* feat: tracking as flat section below shipment boxes @W-23091033

OMS tracking (omsData.shipments[]) has no join key back to a specific ECOM
shipment (deferred TD-0326366), so the layout must not imply which tracking
belongs to which shipment. Move the flat tracking list out of the first
shipment box and render it as a single section BELOW all the boxes — a peer of
the boxes, not nested in one. Each shipment box still shows its own products
(grouped by ECOM shipmentId) and its own native address, both of which ARE
known; only the tracking↔shipment link is unknown, and the flat list is honest
about that. One code path covers single, multi, and pickup-only orders.

Update the regression-lock test: it now asserts tracking renders OUTSIDE (not
nested in) the shipment boxes, with no address/payment text in the section.

* feat: Track Shipment dropdown for multi-shipment orders @W-23091033

UX option A for multi-shipment tracking (one of two branches for review).

When an order has more than one shipment with a carrier tracking URL, the
Track Shipment order action becomes a dropdown (Popover — the shared UI barrel
exposes no Chakra Menu) listing each tracking number as an external link, so
the shopper can pick which carrier link to open. Single-shipment orders keep
the simple external-link button; orders with no tracking URL keep the single
disabled button. This is interim: we still cannot say which tracking maps to
which shipment (deferred TD-0326366), so options are labeled by tracking
number, not by shipment contents.

* chore(order-detail): self-contained rationale in tracking comments

Rewrite the order-detail and test comments to explain the multi-shipment
tracking behavior in project-internal terms (OMS and ECOM shipment arrays share
no join key, so a tracking entry can't be tied to a specific shipment) instead
of referencing tracking artifacts or another product. Customer-facing code
should not carry process/cross-product references; the rationale lives in the
spec and PR instead.

* fix(order-detail): address review findings on tracking layout @W-23091033

- Surface items that match no shipment box (untagged, or a shipmentId naming no
  delivery shipment) in an "Other items" box, so multi-shipment orders never
  silently drop purchased items while the item-count header still counts them.
- Localize the per-shipment box status pill via a shared formatter, so the box
  header and the tracking card render the same localized status instead of one
  showing a raw snake_case token in non-English locales.
- Make the disabled Track Shipment button use aria-disabled + a hidden
  disabled-reason hint (keyboard/SR focusable), matching the Cancel/Return
  buttons in the same row, instead of a native disabled button.

* fix(order-detail): meet AA contrast on shipment status pill @W-23091033

The "Shipment N" status pill rendered gray.700 text on gray.200 (4.04:1),
which fails WCAG AA (4.5:1) and tripped the registered happy-path a11y
snapshot (the color-contrast violation captured on the order detail page).
Darken the text to gray.800 on the same gray.200 background for 5.88:1.
…821845 (#3907)

order-management feature README + strip internal GUS refs
…eturn modal closes (#3910)

return focus to the trigger when the return modal closes
apply CX UI text guidelines to order & account UI
@sf-jie-dai
sf-jie-dai requested a review from a team as a code owner July 1, 2026 01:56
@git2gus

git2gus Bot commented Jul 1, 2026

Copy link
Copy Markdown

Git2Gus App is installed but the .git2gus/config.json doesn't have right values. You should add the required configuration.

@cc-prodsec

cc-prodsec commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

…management

# Conflicts:
#	packages/template-retail-react-app/CHANGELOG.md
@sf-jie-dai sf-jie-dai self-assigned this Jul 1, 2026
sf-jie-dai and others added 4 commits June 30, 2026 22:07
The cancelOmsOrder/returnOmsOrder mutations and getOmsMetaData hook
(#3864, #3869) landed without a commerce-sdk-react CHANGELOG entry, so
the integration PR failed the per-package changelog check. Add it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the temporary 5.1.0-unstable-* dev build with stable 5.4.0, the
first release carrying the OMS Shopper Orders endpoints (oms-return-order,
oms-cancel-order, oms-meta-data). develop's 5.2.0 has no OMS endpoints, so
this is required for the OMS hooks to work rather than a plain bump.

Build + tsc clean; ShopperOrders hooks 33/33 pass against 5.4.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The isomorphic 5.2.0 -> 5.4.0 bump adds endpoints with no React hooks
yet, so the 'all endpoints have hooks' guard tests failed. Add each new
endpoint (requestOtp/verifyOtp, setupCustomerPaymentMethodReference,
promoteTemporaryBasket, getComponent/resolveQualifiers, getProduct
Images/Prices/Promotions) to the expected unimplemented list with a TODO,
matching the test's documented convention. Implementing the hooks is out
of scope for the OMS integration.

Full commerce-sdk-react suite: 778/778 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The commerce-sdk-isomorphic 5.2.0 -> 5.4.0 upgrade pushes vendor.js to
397.07KB, just over the 397 kB budget. Bump to 398 kB (main.js unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ddiazccrz
ddiazccrz previously approved these changes Jul 1, 2026
/>
:{' '}
{trackingHref ? (
<ChakraLink href={trackingHref} isExternal color="blue.600">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isExternal on Chakra's Link only emits rel="noopener", not noopener noreferrer — so reverse-tabnabbing is covered, but the full order-detail URL still leaks as the Referer to the (attacker-influenceable) carrier host. The ensureExternalUrl JSDoc and the comment above claim noreferrer is applied. Should we set rel="noopener noreferrer" explicitly here, or soften the docs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 719a562 — added an explicit rel="noopener noreferrer" here (and on the two Track Shipment links in order-detail). Since Chakra spreads ...rest after its built-in rel, the explicit prop wins over the isExternal-derived noopener, so the Referer no longer leaks to the carrier host.

return statusEligible && shippingEligible
}, [isOmsEnabled, isRegistered, order, customerId])
return (
order.productItems?.every(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[].every(...) is vacuously true, so an OMS order with an empty productItems array would enable Cancel on an order with nothing in it. (Undefined is safe via ?.every ... ?? false — only the empty-array case leaks.) Worth a order.productItems?.length > 0 && guard in front.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 719a562 — added an explicit order.productItems?.length > 0 guard before the .every(...) so an empty items array no longer vacuously enables Cancel.

if (!isOmsEnabled || !isRegistered || !order) return false
if (!isRegistered || !order) return false
if (!order.omsData) return false
const ownsOrder = order.customerInfo?.customerId === customerId

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If both sides are undefined, undefined === undefined grants ownership — today only the isRegistered gate above prevents that, and SCAPI is the authoritative server-side check. Cross-customer access is correctly blocked (ids differ). Still, might be worth hardening to !!customerId && order.customerInfo?.customerId === customerId so it never depends on isRegistered implying a truthy id. Same pattern at L263 for returns.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 719a562 — ownership now requires a concrete id: !!customerId && order.customerInfo?.customerId === customerId, so two undefineds can no longer match. Belt-and-suspenders alongside the isRegistered gate and SCAPI.

.filter(([, row]) => row?.checked)
.reduce((items, [itemId, row]) => {
const quantity = Number(row.quantity)
if (!Number.isFinite(quantity) || quantity <= 0) return items

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this drops non-positive/non-numeric quantities but not fractional or over-max ones — the JSDoc says the UX is integer-valued, yet 2.5 would serialize through. The server 400 ReturnQuantityExceeded is the real backstop, but a Number.isInteger check (and clamping in the picker's change handler) would make the invariant explicit rather than emergent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left as-is for this integration PR — the quantity stepper UI can only emit integers, and the server 400 ReturnQuantityExceeded is the authoritative backstop. Noted as a follow-up nit rather than tightening the validator (and its tests) here.

@@ -1,4 +1,7 @@
## v10.1.0-dev
- Bump `vendor.js` bundle-size budget from 397 kB to 398 kB to accommodate the `commerce-sdk-isomorphic` 5.4.0 upgrade.
- [Feature] Add item-level order returns on the order detail page. Registered shoppers who own an OMS-managed order can return eligible items (OMS-driven via `quantityAvailableToReturn`) through a modal with per-item quantity and reason selection, inline success/error feedback, and a status badge that reflects return progress (including partially-returned multi-unit lines). [#3904](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3904)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not this line — the older #3861 entry just below (around L10) still says the cancel feature is "Gated behind app.oms.enabled config flag." This PR removed that flag (the feature is now data-gated on !!order.omsData), so that sentence is now stale and should be dropped.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 719a562 — updated the #3861 entry to say the cancel feature is shown for OMS-managed orders the shopper owns (gated on order.omsData). Verified zero app.oms.enabled references remain in app/config.

- Add explicit rel="noopener noreferrer" to external carrier tracking
  links (Chakra isExternal only emits noopener, leaking the order page
  URL as Referer to the carrier host)
- Guard canCancel against empty productItems ([].every is vacuously
  true) and require a concrete customerId match for ownership
- Correct ensureExternalUrl JSDoc: isExternal alone yields only noopener
- Fix stale CHANGELOG #3861 entry (cancel is data-gated on order.omsData,
  not the removed app.oms.enabled flag)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sf-jie-dai
sf-jie-dai dismissed stale reviews from syedshehrozhussain and ddiazccrz via 719a562 July 1, 2026 16:16
ddiazccrz
ddiazccrz previously approved these changes Jul 1, 2026
[omsShipmentCount, ecomShipmentCount]
)
const returnableItems = useMemo(() => getReturnableItems(order), [order])
const ownsOrder = order?.customerInfo?.customerId === customerId

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The canCancel fix (L411) hardened this exact pattern with !!customerId &&, but the return path here still uses the bare comparison — so undefined === undefined grants ownership when both ids are missing (feeds showStartReturn). Same defect class as the cancel one; worth mirroring the guard: !!customerId && order?.customerInfo?.customerId === customerId. Not blocking (also gated by isRegistered + server-side SCAPI).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 66bfa7f — mirrored the guard on the return path: !!customerId && order?.customerInfo?.customerId === customerId, matching the canCancel fix. Good catch, this was the twin I missed.

expect(buttons[0]).toHaveTextContent(/^Track Shipment$/)
expect(buttons[0]).toHaveAttribute('href', 'https://carrier.example.com/BBB')
expect(buttons[0]).toHaveAttribute('target', '_blank')
expect(buttons[0]).toHaveAttribute('rel', expect.stringContaining('noopener'))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: stringContaining('noopener') passes for both "noopener" and "noopener noreferrer", so it won't catch a regression that drops the noreferrer token this fix just added. Tightening to stringContaining('noreferrer') (here and L2962) would actually pin the fix.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 66bfa7f — tightened both assertions (L2926, L2962) to stringContaining('noreferrer') so a regression dropping the token now fails the test.

- Mirror the concrete-customerId ownership guard on the return path
  (order-detail L263): !!customerId && ... so undefined === undefined
  can't grant ownership. Twin of the canCancel guard.
- Tighten the Track Shipment rel assertions to stringContaining
  'noreferrer' so a regression dropping the token is actually caught.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…54713

Rows 16 & 17 of the UI text review sheet:
- "Confirm cancellation" button -> "Confirm Cancellation" (button caps)
- "This cancels the entire order." -> "Cancel the entire order."

Updates en-US/en-GB source + compiled translations and test assertions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sf-jie-dai
sf-jie-dai force-pushed the feature/264-order-management branch 3 times, most recently from 6a41cff to 72ae0ac Compare July 1, 2026 18:51
…-23254713

CX UI text guideline caps for buttons:
- "Keep order" -> "Keep Order" (cancel modal)
- "Review return" -> "Review Return" (return modal)
- "Submit return" -> "Submit Return" (return modal)
- "Track shipment {number}" -> "Track Shipment {number}" (tracking dropdown)
Updates en-US/en-GB source + compiled translations.
Also drops the changelog line for the cancel-modal copy tweak.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sf-jie-dai
sf-jie-dai force-pushed the feature/264-order-management branch from 72ae0ac to 4425edc Compare July 1, 2026 19:00
@sf-madhuri-uppu

Copy link
Copy Markdown
Collaborator

Code Review — PR #3914

Comprehensive 4-angle review (line-by-line bugs, removed behavior, cross-file tracing, reuse/efficiency). Findings ranked by severity:

Correctness / Bugs

# Severity File Finding
1 Medium return-items-modal/index.jsx Missing finalFocusRef propReturnItemsModal accepts a finalFocusRef prop and passes it to the Chakra Modal/Drawer to restore focus on close. The caller in order-detail.jsx:1333 never passes it, so when the return modal closes, focus may jump to the top of the page instead of returning to the "Return Items" button. Degrades keyboard/screen-reader navigation.
2 Low order-tracking/index.jsx:52 Epoch date renders as "1 Jan 1970"formatTrackingDate guards null/undefined and unparseable strings, but a truthy epoch string ('1970-01-01T00:00:00Z') passes both guards and renders to the shopper. If SOM ever sends epoch as a "no date" sentinel, this shows a nonsensical date. Low risk given current API behavior.

Cleanup / Maintainability

# Type File Finding
3 Simplification order-detail.jsx:183+ Duplicate feedback state pattern — Cancel and return feedback logic (state, timers, terminal flags, 300ms delays) follow identical patterns. A useFeedbackState() hook would eliminate ~50 lines of duplication and prevent future drift between the two flows.
4 Magic number order-detail.jsx:379,503,507 300ms delay repeated 3 times — The screen-reader announcement delay is a hardcoded magic number. A const ANNOUNCE_DELAY_MS = 300 at module scope would clarify intent and centralize changes.
5 Consistency cancel-order-modal/index.jsx:108,119 Button text casing inconsistency — "Keep order" and "Confirm cancellation" use sentence case while other OMS action buttons ("Return Items", "Track Shipment") use title case. Minor UX inconsistency.

Summary

The only actionable bug is the missing finalFocusRef (accessibility regression). The other findings are cleanup opportunities that could be addressed in follow-up. Overall code quality is solid — no security issues, no crash-path bugs, and error handling is thorough.

@sf-jie-dai

sf-jie-dai commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough pass! Notes on the findings:

Finding 1 (finalFocusRef) — not a bug; this is intentional. The ReturnItemsModal caller in order-detail.jsx deliberately does not pass finalFocusRef. Commit ea40b53 (W-22821845, "return focus to the trigger when the return modal closes") removed the earlier finalFocusRef={headingRef}. With that ref set, Chakra restored focus to the page heading on close; removing it lets Chakra fall back to its default — restoring focus to the element that opened the modal (the Return Items trigger button), which is the WCAG-correct behavior and what that WI was fixing. It also aligns with CancelOrderModal, which accepts no finalFocusRef at all. So focus returns to the trigger, not the top of the page. The modal still accepts the prop for reuse flexibility; this caller intentionally omits it.

Finding 5 (button casing) — already addressed. "Keep order"/"Confirm cancellation" are now "Keep Order"/"Confirm Cancellation" (plus Review Return / Submit Return / Track Shipment {number}) per the CX UI text review — landed in e2971e9 / 4425edc. Looks like the review ran against an earlier commit.

Findings 2 (epoch date), 3 (useFeedbackState refactor), 4 (300ms magic number) — fair observations. Finding 2 is defensive against a sentinel the SOM API doesn't currently send; findings 3 and 4 are cleanup best handled as a follow-up rather than in this integration PR. Noted, thanks.

@sf-jie-dai
sf-jie-dai requested a review from ddiazccrz July 1, 2026 20:13
@sf-jie-dai
sf-jie-dai merged commit 810bdfb into develop Jul 1, 2026
72 of 73 checks passed
@sf-jie-dai
sf-jie-dai deleted the feature/264-order-management branch July 1, 2026 20:25
sf-jie-dai added a commit that referenced this pull request Jul 8, 2026
…fund docs @W-23276443 (#3919)

* chore(oms-ui): order-detail cleanup — feedback hook, announce const, epoch guard, refund docs @W-23276443

Non-blocking follow-ups from the PR #3914 automated review:
- Extract a shared useFeedbackState hook for the cancel/return feedback
  banners (two independent instances so the Cancelled badge stays keyed to
  cancel feedback only). Behavior-preserving.
- Name the 300ms screen-reader announce delay (ANNOUNCE_DELAY_MS), owned by
  the hook; all three call sites route through announce().
- Guard formatTrackingDate against truthy epoch-era sentinel dates so a
  "1 Jan 1970" never renders.
- Document the downstream OMS/SOM refund flow (out of PWA Kit scope).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: drop CHANGELOG entry (cleanup/docs-only, skip changelog) @W-23276443

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(oms-ui): inline ANNOUNCE_DELAY_MS instead of extracting a hook @W-23276443

Drop the useFeedbackState hook (and its test) in favor of a module-level
ANNOUNCE_DELAY_MS const in order-detail.jsx. The hook added a net-larger diff
(new file + test) to centralize a small timer idiom; a named constant covers
the actual value-add (no more magic 300) without the indirection. The inline
useState/useRef feedback pairs and unmount cleanup are restored as-is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(oms-ui): apply review feedback on refunds section @W-23276443

Address knhage's review suggestions: simpler heading, expand PSP acronym on
first use, drop internal source attribution, reduce bold usage, and rephrase
step arrows as sentences.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants