Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,21 @@ export const usePickupShipment = (basket) => {
* @returns {Promise<Object>} The updated shipment response
*/
const updatePickupShipment = async (basketId, storeInfo, options = {}) => {
const defaultPickupShippingMethodId = '005'
const {pickupShippingMethodId = defaultPickupShippingMethodId} = options
let {pickupShippingMethodId} = options

if (!storeInfo?.inventoryId) {
return
}

if (!pickupShippingMethodId) {
const {data: fetchedShippingMethods} = await refetchShippingMethods()
pickupShippingMethodId = getPickupShippingMethodId(fetchedShippingMethods)
}

if (!pickupShippingMethodId) {
throw new Error('No pickup shipping method available for this site')
}

// Update shipment to ensure pickup configuration
return await updateShipmentForBasketMutation.mutateAsync({
parameters: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-cur
import CheckoutSkeleton from '@salesforce/retail-react-app/app/pages/checkout-one-click/partials/one-click-checkout-skeleton'
import UnavailableProductConfirmationModal from '@salesforce/retail-react-app/app/components/unavailable-product-confirmation-modal'
import LoadingSpinner from '@salesforce/retail-react-app/app/components/loading-spinner'
import {isPickupShipment} from '@salesforce/retail-react-app/app/utils/shipment-utils'
import OrderSummary from '@salesforce/retail-react-app/app/components/order-summary'
import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
import {
API_ERROR_MESSAGE,
TOAST_MESSAGE_REMOVED_ITEM_FROM_CART,
STORE_LOCATOR_IS_ENABLED
TOAST_MESSAGE_REMOVED_ITEM_FROM_CART
} from '@salesforce/retail-react-app/app/constants'
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
import {
Expand Down Expand Up @@ -78,10 +78,12 @@ const CheckoutOneClick = () => {
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(null)
const [isEditingPayment, setIsEditingPayment] = useState(false)

// Only enable BOPIS functionality if the feature toggle is on
const isPickupOrder = STORE_LOCATOR_IS_ENABLED
? basket?.shipments[0]?.shippingMethod?.c_storePickupEnabled === true
: false
// Compute shipment types
const pickupShipments = basket?.shipments?.filter((s) => isPickupShipment(s)) || []
const deliveryShipments = basket?.shipments?.filter((s) => !isPickupShipment(s)) || []
const hasPickupShipments = pickupShipments.length > 0
const hasDeliveryShipments = deliveryShipments.length > 0
const isPickupOnly = hasPickupShipments && !hasDeliveryShipments

const selectedShippingAddress = basket?.shipments && basket?.shipments[0]?.shippingAddress
const selectedBillingAddress = basket?.billingAddress
Expand Down Expand Up @@ -165,7 +167,7 @@ const CheckoutOneClick = () => {
}

// For one-click checkout, billing same as shipping by default
const billingSameAsShipping = !isPickupOrder
const billingSameAsShipping = !isPickupOnly
const billingAddress = billingSameAsShipping
? selectedShippingAddress
: billingAddressForm.getValues()
Expand Down Expand Up @@ -279,10 +281,6 @@ const CheckoutOneClick = () => {
if (customerId && shipping) {
// Whitelist fields and strip non-customer fields (e.g., id, _type)
const {
addressId: _ignoreAddressId,
creationDate: _ignoreCreation,
lastModified: _ignoreModified,
preferred: _ignorePreferred,
address1,
address2,
city,
Expand Down Expand Up @@ -412,8 +410,9 @@ const CheckoutOneClick = () => {
idps={idps}
onRegisteredUserChoseGuest={setRegisteredUserChoseGuest}
/>
{isPickupOrder ? <PickupAddress /> : <ShippingAddress />}
{!isPickupOrder && <ShippingOptions />}
{hasPickupShipments && <PickupAddress />}
{hasDeliveryShipments && <ShippingAddress />}
{hasDeliveryShipments && <ShippingOptions />}
<Payment
enableUserRegistration={enableUserRegistration}
setEnableUserRegistration={setEnableUserRegistration}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,80 @@ describe('Checkout One Click', () => {
getConfig.mockImplementation(() => mockConfig)
})

test('renders pickup and shipping sections for mixed baskets', async () => {
const mixedBasket = JSON.parse(JSON.stringify(scapiBasketWithItem))
if (!mixedBasket.productItems || mixedBasket.productItems.length === 0) {
mixedBasket.productItems = [
{
itemId: 'item-delivery-1',
productId: '701643070725M',
quantity: 1,
price: 19.18,
shipmentId: 'me'
}
]
}
mixedBasket.productItems.push({
itemId: 'item-pickup-1',
productId: '701643070725M',
quantity: 1,
price: 19.18,
shipmentId: 'pickup1',
inventoryId: 'inventory_m_store_store1'
})
mixedBasket.shipments = [
{
shipmentId: 'me',
shippingAddress: null,
shippingMethod: null
},
{
shipmentId: 'pickup1',
c_fromStoreId: 'store1',
shippingMethod: {id: 'PICKUP', c_storePickupEnabled: true},
shippingAddress: {
firstName: 'Store 1',
lastName: 'Pickup',
address1: '1 Market St',
city: 'San Francisco',
postalCode: '94105',
stateCode: 'CA',
countryCode: 'US'
}
}
]

global.server.use(
rest.get('*/baskets', (req, res, ctx) => {
return res(
ctx.json({
baskets: [mixedBasket],
total: 1
})
)
})
)

window.history.pushState({}, 'Checkout', createPathWithDefaults('/checkout'))
renderWithProviders(<WrappedCheckout history={history} />, {
wrapperProps: {
isGuest: true,
siteAlias: 'uk',
appConfig: mockConfig.app
}
})

await waitFor(() => {
expect(screen.getByText(/pickup address & information/i)).toBeInTheDocument()
})
await waitFor(() => {
expect(screen.getByText(/shipping address/i)).toBeInTheDocument()
})
await waitFor(() => {
expect(screen.getByText(/shipping & gift options/i)).toBeInTheDocument()
})
})

afterEach(() => {
jest.resetModules()
jest.clearAllMocks()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
/* eslint-disable react/prop-types */
import React from 'react'
import {render, screen, waitFor} from '@testing-library/react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
import CCRadioGroup from '@salesforce/retail-react-app/app/pages/checkout-one-click/partials/one-click-cc-radio-group'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import React from 'react'
import {screen, waitFor, fireEvent, cleanup} from '@testing-library/react'
import {screen, waitFor, fireEvent} from '@testing-library/react'
import ContactInfo from '@salesforce/retail-react-app/app/pages/checkout-one-click/partials/one-click-contact-info'
import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
import {rest} from 'msw'
import {AuthHelpers} from '@salesforce/commerce-sdk-react'

jest.setTimeout(60000)
const validEmail = 'test@salesforce.com'
const invalidEmail = 'invalidEmail'
const mockAuthHelperFunctions = {
[AuthHelpers.LoginRegisteredUserB2C]: {mutateAsync: jest.fn()},
[AuthHelpers.Logout]: {mutateAsync: jest.fn()},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import React, {useState, useMemo, useEffect, useRef, useCallback} from 'react'
import React, {useState, useEffect, useRef, useCallback} from 'react'
import PropTypes from 'prop-types'
import {defineMessage, FormattedMessage, useIntl} from 'react-intl'
import {
Expand Down Expand Up @@ -46,7 +46,6 @@ const Payment = ({
enableUserRegistration,
setEnableUserRegistration,
registeredUserChoseGuest = false,
onPaymentMethodSaved,
onSavePreferenceChange,
onPaymentSubmitted,
selectedPaymentMethod,
Expand Down Expand Up @@ -104,36 +103,6 @@ const Payment = ({
}
}

// Detect new payment instruments that aren't in the customer's saved list
const newPaymentInstruments = useMemo(() => {
// Use currentFormPayment if available, otherwise fall back to appliedPayment
const paymentToCheck = currentFormPayment || appliedPayment

if (!isGuest && paymentToCheck) {
// If customer has no saved payment instruments, any new payment is considered new
if (!customer?.paymentInstruments || customer.paymentInstruments.length === 0) {
return [paymentToCheck]
}

// Check if current payment instrument is not in saved list
const isNewPayment = !customer.paymentInstruments.some((saved) => {
// Compare the entire payment instrument structure
return (
saved.paymentCard?.cardType === paymentToCheck.paymentCard?.cardType &&
saved.paymentCard?.numberLastDigits ===
paymentToCheck.paymentCard?.numberLastDigits &&
saved.paymentCard?.holder === paymentToCheck.paymentCard?.holder &&
saved.paymentCard?.expirationMonth ===
paymentToCheck.paymentCard?.expirationMonth &&
saved.paymentCard?.expirationYear === paymentToCheck.paymentCard?.expirationYear
)
})

return isNewPayment ? [paymentToCheck] : []
}
return []
}, [isGuest, customer, appliedPayment, currentFormPayment])

// Watch form values in real-time to detect new payment instruments
useEffect(() => {
if (paymentMethodForm && !isGuest) {
Expand Down Expand Up @@ -275,8 +244,27 @@ const Payment = ({
customerPaymentInstrumentId: preferred.paymentInstrumentId
}
})
// After auto-apply, if we already have a shipping address, submit billing so we can advance
if (selectedShippingAddress) {
if (isPickupOrder) {
try {
const saved = customer?.paymentInstruments?.find(
(pi) => pi.paymentInstrumentId === preferred.paymentInstrumentId
)
const addr = saved?.billingAddress
if (addr) {
const cleaned = {...addr}
delete cleaned.addressId
delete cleaned.creationDate
delete cleaned.lastModified
delete cleaned.preferred
await updateBillingAddressForBasket({
body: cleaned,
parameters: {basketId: activeBasketIdRef.current || basket.basketId}
})
}
} catch {
// ignore; user can enter billing manually
}
} else if (selectedShippingAddress) {
await onBillingSubmit()
// Ensure basket is refreshed with payment & billing
await currentBasketQuery.refetch()
Expand Down Expand Up @@ -324,6 +312,27 @@ const Payment = ({
}
})
await currentBasketQuery.refetch()
if (isPickupOrder) {
try {
const saved = customer?.paymentInstruments?.find(
(pi) => pi.paymentInstrumentId === paymentInstrumentId
)
const addr = saved?.billingAddress
if (addr) {
const cleaned = {...addr}
delete cleaned.addressId
delete cleaned.creationDate
delete cleaned.lastModified
delete cleaned.preferred
await updateBillingAddressForBasket({
body: cleaned,
parameters: {basketId: activeBasketIdRef.current || basket.basketId}
})
await currentBasketQuery.refetch()
}
} catch {
}
}
setIsApplyingSavedPayment(false)
onSelectedPaymentMethodChange?.(paymentInstrumentId)
}
Expand Down Expand Up @@ -583,8 +592,6 @@ Payment.propTypes = {
setEnableUserRegistration: PropTypes.func,
/** Whether a registered user has chosen guest checkout */
registeredUserChoseGuest: PropTypes.bool,
/** Callback when payment method is successfully saved */
onPaymentMethodSaved: PropTypes.func,
/** Callback when save preference changes */
onSavePreferenceChange: PropTypes.func,
/** Callback when payment is submitted with full card details */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {Checkbox, Text} from '@salesforce/retail-react-app/app/components/shared
import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
import {FormattedMessage} from 'react-intl'

export default function SavePaymentMethod({paymentInstrument, onSaved, checked}) {
export default function SavePaymentMethod({onSaved, checked}) {
const [shouldSave, setShouldSave] = useState(false)
const {data: customer} = useCurrentCustomer()

Expand Down Expand Up @@ -46,8 +46,6 @@ export default function SavePaymentMethod({paymentInstrument, onSaved, checked})
}

SavePaymentMethod.propTypes = {
/** The payment instrument to potentially save */
paymentInstrument: PropTypes.object,
/** Callback when checkbox state changes - receives boolean value */
onSaved: PropTypes.func,
/** Controlled checked prop to preselect visually */
Expand Down
Loading
Loading