Skip to content

Commit 50bfe72

Browse files
committed
Merge remote-tracking branch 'origin/develop' into W-20726574-update-refapp-to-use-basepaths
2 parents 9fa94f7 + 0e107a7 commit 50bfe72

File tree

11 files changed

+363
-190
lines changed

11 files changed

+363
-190
lines changed

packages/template-retail-react-app/app/components/otp-auth/index.jsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Button,
1313
Input,
1414
SimpleGrid,
15+
Spinner,
1516
Stack,
1617
Text,
1718
HStack,
@@ -271,6 +272,17 @@ const OtpAuth = ({
271272
))}
272273
</SimpleGrid>
273274

275+
{/* Loading spinner during verification */}
276+
{isVerifying && (
277+
<Spinner
278+
size="sm"
279+
color="blue.500"
280+
role="status"
281+
aria-live="polite"
282+
data-testid="otp-verifying-spinner"
283+
/>
284+
)}
285+
274286
{/* Error message */}
275287
{error && (
276288
<Text fontSize="sm" color="red.500" textAlign="center">

packages/template-retail-react-app/app/components/otp-auth/index.test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,41 @@ describe('OtpAuth', () => {
246246
await user.type(otpInputs[7], '8')
247247
expect(otpInputs[7]).toHaveFocus()
248248
})
249+
250+
test('shows spinner while OTP is being verified', async () => {
251+
const deferred = {}
252+
const verifyingPromise = new Promise((resolve) => {
253+
deferred.resolve = resolve
254+
})
255+
const mockVerify = jest.fn().mockReturnValue(verifyingPromise)
256+
257+
const user = userEvent.setup()
258+
renderWithProviders(
259+
<OtpAuth
260+
isOpen={true}
261+
onClose={mockOnClose}
262+
form={mockForm}
263+
handleOtpVerification={mockVerify}
264+
handleSendEmailOtp={mockHandleSendEmailOtp}
265+
/>
266+
)
267+
268+
expect(screen.queryByTestId('otp-verifying-spinner')).not.toBeInTheDocument()
269+
270+
const otpInputs = screen.getAllByRole('textbox')
271+
fireEvent.paste(otpInputs[0], {
272+
clipboardData: {getData: () => '12345678'}
273+
})
274+
275+
await waitFor(() => {
276+
expect(screen.getByTestId('otp-verifying-spinner')).toBeInTheDocument()
277+
})
278+
279+
deferred.resolve({success: true})
280+
await waitFor(() => {
281+
expect(screen.queryByTestId('otp-verifying-spinner')).not.toBeInTheDocument()
282+
})
283+
})
249284
})
250285

251286
describe('Keyboard Navigation', () => {

packages/template-retail-react-app/app/pages/checkout-one-click/index.jsx

Lines changed: 8 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast'
3131
import useNavigation from '@salesforce/retail-react-app/app/hooks/use-navigation'
3232
import {
3333
useCheckout,
34-
CheckoutProvider
34+
CheckoutProvider,
35+
getCheckoutGuestChoiceFromStorage,
36+
setCheckoutGuestChoiceInStorage
3537
} from '@salesforce/retail-react-app/app/pages/checkout-one-click/util/checkout-context'
3638
import ContactInfo from '@salesforce/retail-react-app/app/pages/checkout-one-click/partials/one-click-contact-info'
3739
import PickupAddress from '@salesforce/retail-react-app/app/pages/checkout-one-click/partials/one-click-pickup-address'
@@ -55,16 +57,17 @@ import {
5557
getPaymentInstrumentCardType,
5658
getMaskCreditCardNumber
5759
} from '@salesforce/retail-react-app/app/utils/cc-utils'
58-
import {nanoid} from 'nanoid'
5960

6061
const CheckoutOneClick = () => {
6162
const {formatMessage} = useIntl()
6263
const navigate = useNavigation()
63-
const {step, STEPS, contactPhone} = useCheckout()
64+
const {step, STEPS} = useCheckout()
6465
const showToast = useToast()
6566
const [isLoading, setIsLoading] = useState(false)
6667
const [enableUserRegistration, setEnableUserRegistration] = useState(false)
67-
const [registeredUserChoseGuest, setRegisteredUserChoseGuest] = useState(false)
68+
const [registeredUserChoseGuest, setRegisteredUserChoseGuest] = useState(
69+
getCheckoutGuestChoiceFromStorage
70+
)
6871
const [shouldSavePaymentMethod, setShouldSavePaymentMethod] = useState(false)
6972
const [isOtpLoading, setIsOtpLoading] = useState(false)
7073
const [isPlacingOrder, setIsPlacingOrder] = useState(false)
@@ -119,8 +122,6 @@ const CheckoutOneClick = () => {
119122
ShopperBasketsMutations.UpdateBillingAddressForBasket
120123
)
121124
const {mutateAsync: createOrder} = useShopperOrdersMutation(ShopperOrdersMutations.CreateOrder)
122-
const createCustomerAddress = useShopperCustomersMutation('createCustomerAddress')
123-
const updateCustomer = useShopperCustomersMutation('updateCustomer')
124125

125126
const handleSavePreferenceChange = (shouldSave) => {
126127
setShouldSavePaymentMethod(shouldSave)
@@ -382,87 +383,9 @@ const CheckoutOneClick = () => {
382383
fullCardDetails
383384
)
384385
}
385-
386-
// For newly registered guests only, persist shipping address when billing same as shipping
387-
// Skip saving pickup/store addresses - only save delivery addresses
388-
// For multi-shipment orders, save all delivery addresses with the first one as default
389-
if (
390-
enableUserRegistration &&
391-
currentCustomer?.isRegistered &&
392-
!registeredUserChoseGuest
393-
) {
394-
try {
395-
const customerId = order.customerInfo?.customerId
396-
if (!customerId) return
397-
398-
// Get all delivery shipments (not pickup) from the order
399-
// This handles both single delivery and multi-shipment orders
400-
// For BOPIS orders, pickup shipments are filtered out
401-
const deliveryShipments =
402-
order?.shipments?.filter(
403-
(shipment) =>
404-
!isPickupShipment(shipment) && shipment.shippingAddress
405-
) || []
406-
407-
if (deliveryShipments.length > 0) {
408-
// Save all delivery addresses, with the first one as preferred
409-
for (let i = 0; i < deliveryShipments.length; i++) {
410-
const shipment = deliveryShipments[i]
411-
const shipping = shipment.shippingAddress
412-
if (!shipping) continue
413-
414-
// Whitelist fields and strip non-customer fields (e.g., id, _type)
415-
const {
416-
address1,
417-
address2,
418-
city,
419-
countryCode,
420-
firstName,
421-
lastName,
422-
phone,
423-
postalCode,
424-
stateCode
425-
} = shipping || {}
426-
427-
await createCustomerAddress.mutateAsync({
428-
parameters: {customerId},
429-
body: {
430-
addressId: nanoid(),
431-
preferred: i === 0, // First address is preferred
432-
address1,
433-
address2,
434-
city,
435-
countryCode,
436-
firstName,
437-
lastName,
438-
phone,
439-
postalCode,
440-
stateCode
441-
}
442-
})
443-
}
444-
}
445-
446-
// Persist phone number as phoneHome for newly registered guest shoppers
447-
const phoneHome = basket?.billingAddress?.phone || contactPhone
448-
if (phoneHome) {
449-
await updateCustomer.mutateAsync({
450-
parameters: {customerId},
451-
body: {phoneHome}
452-
})
453-
}
454-
} catch (_e) {
455-
// Only surface error if shopper opted to register/save details; otherwise fail silently
456-
showError(
457-
formatMessage({
458-
id: 'checkout.error.cannot_save_address',
459-
defaultMessage: 'Could not save shipping address.'
460-
})
461-
)
462-
}
463-
}
464386
}
465387

388+
setCheckoutGuestChoiceInStorage(false)
466389
navigate(`/checkout/confirmation/${order.orderNo}`)
467390
} catch (error) {
468391
const message = formatMessage({

packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-contact-info.jsx

Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ import {
2626
} from '@salesforce/retail-react-app/app/components/shared/ui'
2727
import {useForm} from 'react-hook-form'
2828
import {FormattedMessage, useIntl} from 'react-intl'
29-
import {useCheckout} from '@salesforce/retail-react-app/app/pages/checkout-one-click/util/checkout-context'
29+
import {
30+
useCheckout,
31+
setCheckoutGuestChoiceInStorage
32+
} from '@salesforce/retail-react-app/app/pages/checkout-one-click/util/checkout-context'
3033
import useLoginFields from '@salesforce/retail-react-app/app/components/forms/useLoginFields'
3134
import {
3235
ToggleCard,
@@ -110,7 +113,7 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG
110113
const [isCheckingEmail, setIsCheckingEmail] = useState(false)
111114
const [isSubmitting, setIsSubmitting] = useState(false)
112115
const [isBlurChecking, setIsBlurChecking] = useState(false)
113-
const [, setRegisteredUserChoseGuest] = useState(false)
116+
const [registeredUserChoseGuest, setRegisteredUserChoseGuest] = useState(false)
114117
const [emailError, setEmailError] = useState('')
115118

116119
// Auto-focus the email field when the component mounts
@@ -270,37 +273,11 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG
270273
}
271274

272275
// Handle checkout as guest from OTP modal
273-
const handleCheckoutAsGuest = async () => {
274-
try {
275-
const email = form.getValues('email')
276-
const phone = form.getValues('phone')
277-
// Update basket with guest email
278-
await updateCustomerForBasket.mutateAsync({
279-
parameters: {basketId: basket.basketId},
280-
body: {email: email}
281-
})
282-
283-
// Save phone number to basket billing address for guest shoppers
284-
if (phone) {
285-
await updateBillingAddressForBasket.mutateAsync({
286-
parameters: {basketId: basket.basketId},
287-
body: {
288-
...basket?.billingAddress,
289-
phone: phone
290-
}
291-
})
292-
}
293-
294-
// Set the flag that "Checkout as Guest" was clicked
295-
setRegisteredUserChoseGuest(true)
296-
if (onRegisteredUserChoseGuest) {
297-
onRegisteredUserChoseGuest(true)
298-
}
299-
300-
// Proceed to next step (shipping address)
301-
goToNextStep()
302-
} catch (error) {
303-
setError(error.message)
276+
const handleCheckoutAsGuest = () => {
277+
setRegisteredUserChoseGuest(true)
278+
setCheckoutGuestChoiceInStorage(true)
279+
if (onRegisteredUserChoseGuest) {
280+
onRegisteredUserChoseGuest(true)
304281
}
305282
}
306283

@@ -359,6 +336,7 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG
359336

360337
// Reset guest checkout flag since user is now logged in
361338
setRegisteredUserChoseGuest(false)
339+
setCheckoutGuestChoiceInStorage(false)
362340
if (onRegisteredUserChoseGuest) {
363341
onRegisteredUserChoseGuest(false)
364342
}
@@ -470,7 +448,7 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG
470448
return
471449
}
472450

473-
if (!result.isRegistered) {
451+
if (!result.isRegistered || registeredUserChoseGuest) {
474452
// Guest shoppers must provide phone number before proceeding
475453
const phone = (formData.phone || '').trim()
476454
if (!phone) {

0 commit comments

Comments
 (0)