Skip to content

Commit 0706784

Browse files
committed
merge from parent branch
2 parents 2b2b485 + 1db750a commit 0706784

File tree

20 files changed

+1169
-618
lines changed

20 files changed

+1169
-618
lines changed

e2e/tests/a11y/desktop/a11y-snapshot-test-guest.spec.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ test.describe('Accessibility Tests with Snapshots for guest user', () => {
111111
await runAccessibilityTest(page, ['guest', 'cart-a11y-violations.json'])
112112
})
113113

114-
test('Checkout should not have new accessibility issues', async ({page}) => {
114+
// TODO: Remove skip and regenerate snapshots.
115+
test.skip('Checkout should not have new accessibility issues', async ({page}) => {
115116
await addProductToCart({page})
116117

117118
// cart

packages/pwa-kit-runtime/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## [Unreleased]
22
- Add HttpOnly session cookies for SLAS private client proxy [#3680](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3680)
3+
- Handle logout when HttpOnly session cookies is enabled [#3699](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3699)
34

45
## v3.17.0-dev
56
- Add Node 24 support. Migrate deprecated Node.js `url.parse()` and `url.format()` to the WHATWG `URL` [#3652](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3652)

packages/pwa-kit-runtime/src/ssr/server/build-remote-server.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,7 @@ export const RemoteServerFactory = {
967967
// purpose so we don't want to overwrite the header for those calls.
968968
proxyRequest.setHeader('Authorization', `Basic ${encodedSlasCredentials}`)
969969
} else if (
970+
process.env.MRT_DISABLE_HTTPONLY_SESSION_COOKIES === 'false' &&
970971
incomingRequest.path?.match(options.slasEndpointsRequiringAccessToken)
971972
) {
972973
// Inject tokens from HttpOnly cookies for endpoints like /oauth2/logout
@@ -984,13 +985,19 @@ export const RemoteServerFactory = {
984985
}
985986

986987
// Inject refresh_token into query string from HttpOnly cookie
987-
// Try registered user cookie first, then guest
988-
const refreshToken =
989-
cookies[`cc-nx_${site}`] || cookies[`cc-nx-g_${site}`]
988+
// refresh_token ishouls required for /oauth2/logout
989+
const refreshToken = cookies[`cc-nx_${site}`]
990990
if (refreshToken) {
991991
const url = new URL(proxyRequest.path, 'http://localhost')
992992
url.searchParams.set('refresh_token', refreshToken)
993993
proxyRequest.path = url.pathname + url.search
994+
} else {
995+
logger.warn(
996+
`Registered refresh token cookie (cc-nx_${site}) not found for ${incomingRequest.path}. The logout request may fail.`,
997+
{
998+
namespace: '_setupSlasPrivateClientProxy'
999+
}
1000+
)
9941001
}
9951002
}
9961003
}

packages/pwa-kit-runtime/src/ssr/server/process-token-response.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,23 @@ export function applyHttpOnlySessionCookies(responseBuffer, proxyRes, req, res,
197197
expires: refreshExpires
198198
})
199199
)
200+
201+
// Delete the opposite refresh token cookie to mirror client-side behavior:
202+
// Login (guest → registered): delete guest cookie cc-nx-g
203+
// Logout (registered → guest): delete registered cookie cc-nx
204+
const staleCookieName = isGuest ? `cc-nx_${site}` : `cc-nx-g_${site}`
205+
res.append(
206+
SET_COOKIE,
207+
cookieAsString({
208+
name: staleCookieName,
209+
value: '',
210+
path: '/',
211+
secure: true,
212+
sameSite: 'lax',
213+
httpOnly: true,
214+
expires: new Date(0)
215+
})
216+
)
200217
}
201218

202219
// Strip token fields from body so they are not exposed to the client

packages/pwa-kit-runtime/src/ssr/server/process-token-response.test.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,12 @@ describe('applyHttpOnlySessionCookies', () => {
210210
expect(uidoCookie.value).toBe('ecom')
211211
expect(uidoCookie.httpOnly).toBeUndefined()
212212

213-
// Should NOT have registered refresh cookie
214-
expect(res.cookies.find((c) => c.startsWith('cc-nx_testsite='))).toBeUndefined()
213+
// Registered refresh cookie should be expired (deleted)
214+
const staleRegisteredCookie = parseCookie(
215+
res.cookies.find((c) => c.startsWith('cc-nx_testsite='))
216+
)
217+
expect(staleRegisteredCookie.value).toBe('')
218+
expect(staleRegisteredCookie.expires).toEqual(new Date(0))
215219

216220
// Tokens stripped from body, other fields preserved
217221
const body = JSON.parse(result.toString('utf8'))
@@ -255,8 +259,12 @@ describe('applyHttpOnlySessionCookies', () => {
255259
const uidoCookie = parseCookie(res.cookies.find((c) => c.includes('uido_testsite=')))
256260
expect(uidoCookie.value).toBe('ecom')
257261

258-
// Should NOT have guest refresh cookie
259-
expect(res.cookies.find((c) => c.includes('cc-nx-g_testsite='))).toBeUndefined()
262+
// Guest refresh cookie should be expired (deleted)
263+
const staleGuestCookie = parseCookie(
264+
res.cookies.find((c) => c.startsWith('cc-nx-g_testsite='))
265+
)
266+
expect(staleGuestCookie.value).toBe('')
267+
expect(staleGuestCookie.expires).toEqual(new Date(0))
260268

261269
// No dnt cookie when dnt absent from JWT
262270
expect(res.cookies.find((c) => c.includes('cc-at-dnt_testsite'))).toBeUndefined()

packages/template-retail-react-app/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
## v9.1.0-dev
55
- Update jest-fetch-mock and Jest 29 dependencies [#3663](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3663)
66
- Add Node 24 support. Drop Node 16 support [#3652](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3652)
7+
- [Bugfix] Fix error toast for no applicable shipping methods in one-click checkout [#3673](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3673)
78
- [Feature] Subscribe to marketing communications. Email capture component updated in footer section to use Shopper Consents API. [#3674](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3674)
89
- [Bugfix] Fix for custom billing address as returning shoppers in 1CC [#3693](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3693)
910

packages/template-retail-react-app/app/components/display-price/list-price.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const ListPrice = ({labelForA11y, price, isRange = false, as = 's', currency, ..
3636
aria-label={intl.formatMessage(msg.ariaLabelListPriceWithRange, {
3737
listPrice: listPriceText || ''
3838
})}
39-
color="gray.600"
39+
color="gray.700"
4040
>
4141
{listPriceText}
4242
</Text>
@@ -47,7 +47,7 @@ const ListPrice = ({labelForA11y, price, isRange = false, as = 's', currency, ..
4747
aria-label={intl.formatMessage(msg.ariaLabelListPrice, {
4848
listPrice: listPriceText || ''
4949
})}
50-
color="gray.600"
50+
color="gray.700"
5151
>
5252
{listPriceText}
5353
</Text>

packages/template-retail-react-app/app/pages/cart/index.jsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ import {
8181
import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
8282
import UnavailableProductConfirmationModal from '@salesforce/retail-react-app/app/components/unavailable-product-confirmation-modal'
8383
import {getUpdateBundleChildArray} from '@salesforce/retail-react-app/app/utils/product-utils'
84-
import {isPickupShipment} from '@salesforce/retail-react-app/app/utils/shipment-utils'
84+
import {
85+
isPickupShipment,
86+
groupShipmentsByDeliveryOption
87+
} from '@salesforce/retail-react-app/app/utils/shipment-utils'
8588
import {useSelectedStore} from '@salesforce/retail-react-app/app/hooks/use-selected-store'
8689
import {useMultiship} from '@salesforce/retail-react-app/app/hooks/use-multiship'
8790

@@ -377,6 +380,15 @@ const Cart = () => {
377380
return
378381
}
379382

383+
// Don't assign methods until at least one delivery shipment has an address
384+
const {deliveryShipments} = groupShipmentsByDeliveryOption(basket)
385+
const hasDeliveryWithAddress = deliveryShipments.some(
386+
(s) => s.shippingAddress?.address1
387+
)
388+
if (deliveryShipments.length > 0 && !hasDeliveryWithAddress) {
389+
return
390+
}
391+
380392
setIsProcessingShippingMethods(true)
381393
try {
382394
await updateShipmentsWithoutMethods()

packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-shipping-address.jsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,24 +48,25 @@ export default function ShippingAddress(props) {
4848
const {enableUserRegistration = false, isShipmentCleanupComplete = true} = props
4949
const {formatMessage} = useIntl()
5050
const [isManualSubmitLoading, setIsManualSubmitLoading] = useState(false)
51-
const [isMultiShipping, setIsMultiShipping] = useState(false)
52-
const [openedByUser, setOpenedByUser] = useState(false)
5351
const {data: customer} = useCurrentCustomer()
5452
const currentBasketQuery = useCurrentBasket()
5553
const {data: basket} = currentBasketQuery
5654
const deliveryShipments =
5755
basket?.shipments?.filter((shipment) => !isPickupShipment(shipment)) || []
56+
const hasMultipleDeliveryShipments = deliveryShipments.length > 1
57+
const [isMultiShipping, setIsMultiShipping] = useState(hasMultipleDeliveryShipments)
58+
const [openedByUser, setOpenedByUser] = useState(false)
5859
const selectedShippingAddress = deliveryShipments[0]?.shippingAddress
5960
const targetDeliveryShipmentId = deliveryShipments[0]?.shipmentId || 'me'
6061
const isAddressFilled = selectedShippingAddress?.address1 && selectedShippingAddress?.city
61-
const {step, STEPS, goToStep, goToNextStep, contactPhone} = useCheckout()
62+
const {step, STEPS, goToStep, goToNextStep, contactPhone, setConsolidationLock} = useCheckout()
6263
const createCustomerAddress = useShopperCustomersMutation('createCustomerAddress')
6364
const updateCustomerAddress = useShopperCustomersMutation('updateCustomerAddress')
6465
const updateShippingAddressForShipment = useShopperBasketsMutation(
6566
'updateShippingAddressForShipment'
6667
)
6768
const multishipEnabled = getConfig()?.app?.multishipEnabled ?? true
68-
const hasMultipleDeliveryShipments = deliveryShipments.length > 1
69+
6970
const {removeEmptyShipments} = useMultiship(basket)
7071
const {updateItemsToDeliveryShipment} = useItemShipmentManagement(basket?.basketId)
7172

@@ -114,6 +115,14 @@ export default function ShippingAddress(props) {
114115
const targetShipmentId = targetShipment?.shipmentId || DEFAULT_SHIPMENT_ID
115116
let basketAfterItemMoves = null
116117

118+
// Do not advance the step while basket mutations are in flight
119+
const willConsolidate = deliveryItems.some(
120+
(item) => item.shipmentId !== targetShipmentId
121+
)
122+
if (willConsolidate) {
123+
setConsolidationLock(true)
124+
}
125+
117126
await updateShippingAddressForShipment.mutateAsync({
118127
parameters: {
119128
basketId: basket.basketId,
@@ -172,6 +181,7 @@ export default function ShippingAddress(props) {
172181
}
173182
// Remove any empty shipments. Use updated basket if available
174183
await removeEmptyShipments(basketAfterItemMoves || basket)
184+
setConsolidationLock(false)
175185

176186
// For registered shoppers: if an existing shipping method is still valid for the new address,
177187
// skip the Shipping Options step and go straight to Payment.
@@ -198,6 +208,7 @@ export default function ShippingAddress(props) {
198208
console.error('Error submitting shipping address:', error)
199209
}
200210
} finally {
211+
setConsolidationLock(false)
201212
setIsManualSubmitLoading(false)
202213
}
203214
}

0 commit comments

Comments
 (0)