@salesforce/retail-react-app 9.0.0-preview.0 → 9.0.0-preview.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,8 @@
1
+ ## v9.0.0-preview.1 (Feb 09, 2026)
2
+ - [Bugfix] 1CC Bug Fixes [#3638](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3638)
3
+
1
4
  ## v9.0.0-preview.0 (Feb 06, 2026)
2
- - [Feature] One Click Checkout [#3552](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3552)
5
+ - [Feature] One Click Checkout (in Developer Preview) [#3552](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3552)
3
6
  - [Feature] Add `fuzzyPathMatching` to reduce computational overhead of route generation at time of application load [#3530](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3530)
4
7
  - [Bugfix] Fix Passwordless Login landingPath, Reset Password landingPath, and Social Login redirectUri value in config not being used [#3560](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3560)
5
8
  - [Feature] PWA Integration with OMS
@@ -254,7 +254,6 @@ describe('OtpAuth', () => {
254
254
  })
255
255
  const mockVerify = jest.fn().mockReturnValue(verifyingPromise)
256
256
 
257
- const user = userEvent.setup()
258
257
  renderWithProviders(
259
258
  <OtpAuth
260
259
  isOpen={true}
@@ -20,7 +20,7 @@ const PasswordlessLogin = ({
20
20
  handlePasswordlessLoginClick,
21
21
  isSocialEnabled = false,
22
22
  idps = [],
23
- setLoginType = noop
23
+ setLoginType: _setLoginType = noop
24
24
  }) => {
25
25
  const [showPasswordView, setShowPasswordView] = useState(false)
26
26
 
@@ -31,7 +31,9 @@ import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast'
31
31
  import useNavigation from '@salesforce/retail-react-app/app/hooks/use-navigation'
32
32
  import {
33
33
  useCheckout,
34
- CheckoutProvider
34
+ CheckoutProvider,
35
+ getCheckoutGuestChoiceFromStorage,
36
+ setCheckoutGuestChoiceInStorage
35
37
  } from '@salesforce/retail-react-app/app/pages/checkout-one-click/util/checkout-context'
36
38
  import ContactInfo from '@salesforce/retail-react-app/app/pages/checkout-one-click/partials/one-click-contact-info'
37
39
  import PickupAddress from '@salesforce/retail-react-app/app/pages/checkout-one-click/partials/one-click-pickup-address'
@@ -55,16 +57,17 @@ import {
55
57
  getPaymentInstrumentCardType,
56
58
  getMaskCreditCardNumber
57
59
  } from '@salesforce/retail-react-app/app/utils/cc-utils'
58
- import {nanoid} from 'nanoid'
59
60
 
60
61
  const CheckoutOneClick = () => {
61
62
  const {formatMessage} = useIntl()
62
63
  const navigate = useNavigation()
63
- const {step, STEPS, contactPhone} = useCheckout()
64
+ const {step, STEPS} = useCheckout()
64
65
  const showToast = useToast()
65
66
  const [isLoading, setIsLoading] = useState(false)
66
67
  const [enableUserRegistration, setEnableUserRegistration] = useState(false)
67
- const [registeredUserChoseGuest, setRegisteredUserChoseGuest] = useState(false)
68
+ const [registeredUserChoseGuest, setRegisteredUserChoseGuest] = useState(
69
+ getCheckoutGuestChoiceFromStorage
70
+ )
68
71
  const [shouldSavePaymentMethod, setShouldSavePaymentMethod] = useState(false)
69
72
  const [isOtpLoading, setIsOtpLoading] = useState(false)
70
73
  const [isPlacingOrder, setIsPlacingOrder] = useState(false)
@@ -119,8 +122,6 @@ const CheckoutOneClick = () => {
119
122
  ShopperBasketsMutations.UpdateBillingAddressForBasket
120
123
  )
121
124
  const {mutateAsync: createOrder} = useShopperOrdersMutation(ShopperOrdersMutations.CreateOrder)
122
- const createCustomerAddress = useShopperCustomersMutation('createCustomerAddress')
123
- const updateCustomer = useShopperCustomersMutation('updateCustomer')
124
125
 
125
126
  const handleSavePreferenceChange = (shouldSave) => {
126
127
  setShouldSavePaymentMethod(shouldSave)
@@ -382,87 +383,9 @@ const CheckoutOneClick = () => {
382
383
  fullCardDetails
383
384
  )
384
385
  }
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
- }
464
386
  }
465
387
 
388
+ setCheckoutGuestChoiceInStorage(false)
466
389
  navigate(`/checkout/confirmation/${order.orderNo}`)
467
390
  } catch (error) {
468
391
  const message = formatMessage({
@@ -751,11 +751,6 @@ describe('Checkout One Click', () => {
751
751
  })
752
752
  ).toBeInTheDocument()
753
753
 
754
- // Billing address should default to the shipping address
755
-
756
- // Should display billing address that matches shipping address
757
- expect(step3Content.getByText('123 Main St')).toBeInTheDocument()
758
-
759
754
  // Edit billing address
760
755
  // Toggle to edit billing address (not via same-as-shipping label in this flow)
761
756
  // Click the checkbox by role if present; otherwise skip
@@ -1208,10 +1203,6 @@ describe('Checkout One Click', () => {
1208
1203
  })
1209
1204
  ).toBeInTheDocument()
1210
1205
 
1211
- // Verify billing address is displayed (it shows John Smith from the mock)
1212
- expect(step3Content.getByText('John Smith')).toBeInTheDocument()
1213
- expect(step3Content.getByText('123 Main St')).toBeInTheDocument()
1214
-
1215
1206
  // Verify UserRegistration component is hidden for registered customers
1216
1207
  expect(screen.queryByTestId('sf-user-registration-content')).not.toBeInTheDocument()
1217
1208
 
@@ -2650,7 +2641,7 @@ describe('Checkout One Click', () => {
2650
2641
 
2651
2642
  // Click "Edit Payment Info" button
2652
2643
  const editPaymentButton = screen.getByRole('button', {
2653
- name: /toggle_card.action.editPaymentInfo|Edit Payment Info/i
2644
+ name: /toggle_card.action.changePaymentInfo|Change/i
2654
2645
  })
2655
2646
  await user.click(editPaymentButton)
2656
2647
 
@@ -26,7 +26,10 @@ import {
26
26
  } from '@salesforce/retail-react-app/app/components/shared/ui'
27
27
  import {useForm} from 'react-hook-form'
28
28
  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'
30
33
  import useLoginFields from '@salesforce/retail-react-app/app/components/forms/useLoginFields'
31
34
  import {
32
35
  ToggleCard,
@@ -110,7 +113,7 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG
110
113
  const [isCheckingEmail, setIsCheckingEmail] = useState(false)
111
114
  const [isSubmitting, setIsSubmitting] = useState(false)
112
115
  const [isBlurChecking, setIsBlurChecking] = useState(false)
113
- const [, setRegisteredUserChoseGuest] = useState(false)
116
+ const [registeredUserChoseGuest, setRegisteredUserChoseGuest] = useState(false)
114
117
  const [emailError, setEmailError] = useState('')
115
118
 
116
119
  // Auto-focus the email field when the component mounts
@@ -242,11 +245,19 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG
242
245
  lastEmailSentRef.current = normalizedEmail
243
246
  return {isRegistered: true}
244
247
  } catch (error) {
245
- const message = formatMessage(getAuthorizePasswordlessErrorMessage(error.message))
246
- setError(message)
247
- // Keep continue button visible if email is valid (for unregistered users)
248
- if (isValidEmail(email)) {
248
+ // 404 = email not registered (guest); treat as guest and continue
249
+ const isGuestNotFound = String(error?.message || '').includes('404')
250
+ if (isGuestNotFound && isValidEmail(email)) {
251
+ setError('')
249
252
  setShowContinueButton(true)
253
+ } else {
254
+ const message = formatMessage(
255
+ getAuthorizePasswordlessErrorMessage(error.message)
256
+ )
257
+ setError(message)
258
+ if (isValidEmail(email)) {
259
+ setShowContinueButton(true)
260
+ }
250
261
  }
251
262
  // Update the last email sent ref even on error to prevent retrying immediately
252
263
  lastEmailSentRef.current = normalizedEmail
@@ -270,37 +281,11 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG
270
281
  }
271
282
 
272
283
  // 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)
284
+ const handleCheckoutAsGuest = () => {
285
+ setRegisteredUserChoseGuest(true)
286
+ setCheckoutGuestChoiceInStorage(true)
287
+ if (onRegisteredUserChoseGuest) {
288
+ onRegisteredUserChoseGuest(true)
304
289
  }
305
290
  }
306
291
 
@@ -359,6 +344,7 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG
359
344
 
360
345
  // Reset guest checkout flag since user is now logged in
361
346
  setRegisteredUserChoseGuest(false)
347
+ setCheckoutGuestChoiceInStorage(false)
362
348
  if (onRegisteredUserChoseGuest) {
363
349
  onRegisteredUserChoseGuest(false)
364
350
  }
@@ -470,7 +456,7 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG
470
456
  return
471
457
  }
472
458
 
473
- if (!result.isRegistered) {
459
+ if (!result.isRegistered || registeredUserChoseGuest) {
474
460
  // Guest shoppers must provide phone number before proceeding
475
461
  const phone = (formData.phone || '').trim()
476
462
  if (!phone) {
@@ -534,6 +520,9 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG
534
520
  }
535
521
  }
536
522
 
523
+ const customerEmail = customer?.email || form.getValues('email')
524
+ const customerPhone = customer?.phoneHome || form.getValues('phone')
525
+
537
526
  return (
538
527
  <>
539
528
  <ToggleCard
@@ -557,8 +546,8 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG
557
546
  id: 'checkout_contact_info.action.sign_out'
558
547
  })
559
548
  : formatMessage({
560
- defaultMessage: 'Edit',
561
- id: 'checkout_contact_info.action.edit'
549
+ defaultMessage: 'Change',
550
+ id: 'checkout_contact_info.action.change'
562
551
  })
563
552
  }
564
553
  >
@@ -688,18 +677,14 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG
688
677
  </Container>
689
678
  </ToggleCardEdit>
690
679
 
691
- {(customer?.email || form.getValues('email')) && (
680
+ {customerEmail ? (
692
681
  <ToggleCardSummary>
693
682
  <Stack spacing={1}>
694
- <Text>{customer?.email || form.getValues('email')}</Text>
695
- {(customer?.phoneHome || form.getValues('phone')) && (
696
- <Text fontSize="sm" color="gray.600">
697
- {customer?.phoneHome || form.getValues('phone')}
698
- </Text>
699
- )}
683
+ <Text>{customerEmail}</Text>
684
+ {customerPhone && <Text>{customerPhone}</Text>}
700
685
  </Stack>
701
686
  </ToggleCardSummary>
702
- )}
687
+ ) : null}
703
688
  </ToggleCard>
704
689
 
705
690
  {/* Sign Out Confirmation Dialog */}
@@ -7,6 +7,7 @@
7
7
  import React from 'react'
8
8
  import {screen, waitFor, fireEvent, act} from '@testing-library/react'
9
9
  import ContactInfo from '@salesforce/retail-react-app/app/pages/checkout-one-click/partials/one-click-contact-info'
10
+ import {setCheckoutGuestChoiceInStorage} from '@salesforce/retail-react-app/app/pages/checkout-one-click/util/checkout-context'
10
11
  import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
11
12
  import {rest} from 'msw'
12
13
  import {AuthHelpers, useCustomerType} from '@salesforce/commerce-sdk-react'
@@ -79,6 +80,7 @@ jest.mock('@salesforce/retail-react-app/app/hooks/use-current-customer', () => (
79
80
  const mockSetContactPhone = jest.fn()
80
81
  const mockGoToNextStep = jest.fn()
81
82
  jest.mock('@salesforce/retail-react-app/app/pages/checkout-one-click/util/checkout-context', () => {
83
+ const setCheckoutGuestChoiceInStorage = jest.fn()
82
84
  return {
83
85
  useCheckout: jest.fn().mockReturnValue({
84
86
  customer: null,
@@ -91,7 +93,8 @@ jest.mock('@salesforce/retail-react-app/app/pages/checkout-one-click/util/checko
91
93
  goToStep: null,
92
94
  goToNextStep: mockGoToNextStep,
93
95
  setContactPhone: mockSetContactPhone
94
- })
96
+ }),
97
+ setCheckoutGuestChoiceInStorage
95
98
  }
96
99
  })
97
100
 
@@ -110,13 +113,17 @@ jest.mock('@salesforce/retail-react-app/app/hooks/use-multi-site', () => ({
110
113
  // Mock OtpAuth to expose a verify trigger
111
114
  jest.mock('@salesforce/retail-react-app/app/components/otp-auth', () => {
112
115
  // eslint-disable-next-line react/prop-types
113
- return function MockOtpAuth({isOpen, handleOtpVerification, onCheckoutAsGuest}) {
116
+ return function MockOtpAuth({isOpen, handleOtpVerification, onCheckoutAsGuest, onClose}) {
117
+ const handleGuestClick = () => {
118
+ onCheckoutAsGuest?.()
119
+ onClose?.()
120
+ }
114
121
  return isOpen ? (
115
122
  <div>
116
123
  <div>Confirm it&apos;s you</div>
117
124
  <p>To log in to your account, enter the code sent to your email.</p>
118
125
  <div>
119
- <button type="button" onClick={onCheckoutAsGuest}>
126
+ <button type="button" onClick={handleGuestClick}>
120
127
  Checkout as a guest
121
128
  </button>
122
129
  <button type="button">Resend Code</button>
@@ -289,7 +296,8 @@ describe('ContactInfo Component', () => {
289
296
  goToStep: jest.fn(),
290
297
  goToNextStep: jest.fn(),
291
298
  setContactPhone: jest.fn()
292
- })
299
+ }),
300
+ setCheckoutGuestChoiceInStorage: jest.fn()
293
301
  }
294
302
  }
295
303
  )
@@ -606,11 +614,10 @@ describe('ContactInfo Component', () => {
606
614
  expect(screen.getByText(/Resend Code/i)).toBeInTheDocument()
607
615
  })
608
616
 
609
- test('shows error message when updateCustomerForBasket fails', async () => {
610
- // Mock OTP authorization to succeed so modal opens
617
+ test('clicking "Checkout as a guest" does not update basket or advance step', async () => {
618
+ // "Checkout as Guest" only closes the modal and sets registeredUserChoseGuest state;
619
+ // basket is updated when the user later submits the form with phone and clicks Continue.
611
620
  mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync.mockResolvedValue({})
612
- // Mock update to fail when choosing guest
613
- mockUpdateCustomerForBasket.mutateAsync.mockRejectedValue(new Error('API Error'))
614
621
 
615
622
  const {user} = renderWithProviders(<ContactInfo />)
616
623
  const emailInput = screen.getByLabelText('Email')
@@ -625,22 +632,17 @@ describe('ContactInfo Component', () => {
625
632
  await user.click(submitButton)
626
633
  await screen.findByTestId('otp-verify')
627
634
 
628
- // Click "Checkout as a guest" which triggers updateCustomerForBasket and should set error
635
+ // Click "Checkout as a guest" should not call basket mutations or goToNextStep
629
636
  await user.click(screen.getByText(/Checkout as a guest/i))
630
637
 
631
638
  await waitFor(() => {
632
- expect(mockUpdateCustomerForBasket.mutateAsync).toHaveBeenCalled()
633
- })
634
- // Error alert should be rendered; component maps errors via getPasswordlessErrorMessage to generic message
635
- await waitFor(() => {
636
- const alerts = screen.queryAllByRole('alert')
637
- const hasError = alerts.some(
638
- (n) =>
639
- n.textContent?.includes('Something went wrong') ||
640
- n.textContent?.includes('API Error')
641
- )
642
- expect(hasError).toBe(true)
639
+ expect(mockUpdateCustomerForBasket.mutateAsync).not.toHaveBeenCalled()
640
+ expect(mockGoToNextStep).not.toHaveBeenCalled()
643
641
  })
642
+ // Modal closes; user stays on Contact Info (Continue button visible again)
643
+ expect(
644
+ screen.getByRole('button', {name: /continue to shipping address/i})
645
+ ).toBeInTheDocument()
644
646
  })
645
647
 
646
648
  test('does not proceed to next step when OTP modal is already open on form submission', async () => {
@@ -747,37 +749,41 @@ describe('ContactInfo Component', () => {
747
749
  expect(phoneInput.value).toBe('(555) 123-4567')
748
750
  })
749
751
 
750
- test('saves phone number to billing address when guest checks out via "Checkout as Guest" button', async () => {
751
- // Mock successful OTP authorization to open modal
752
+ test('notifies parent when guest chooses "Checkout as Guest" and stays on Contact Info', async () => {
753
+ // Open OTP modal (registered email), click "Checkout as a guest" — modal closes,
754
+ // parent is notified via onRegisteredUserChoseGuest(true), user stays on Contact Info.
752
755
  mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync.mockResolvedValue({})
753
- mockUpdateCustomerForBasket.mutateAsync.mockResolvedValue({})
754
- mockUpdateBillingAddressForBasket.mutateAsync.mockResolvedValue({})
755
756
 
756
- const {user} = renderWithProviders(<ContactInfo />)
757
+ const onRegisteredUserChoseGuestSpy = jest.fn()
758
+ const {user} = renderWithProviders(
759
+ <ContactInfo onRegisteredUserChoseGuest={onRegisteredUserChoseGuestSpy} />
760
+ )
757
761
 
758
762
  const emailInput = screen.getByLabelText('Email')
759
- const phoneInput = screen.getByLabelText('Phone')
760
-
761
- // Enter phone first - use fireEvent to ensure value is set
762
- fireEvent.change(phoneInput, {target: {value: '(727) 555-1234'}})
763
763
 
764
- // Enter email and wait for OTP modal to open
764
+ // Enter email and open OTP modal (blur triggers registered-user check)
765
765
  await user.type(emailInput, validEmail)
766
766
  fireEvent.change(emailInput, {target: {value: validEmail}})
767
767
  fireEvent.blur(emailInput)
768
768
 
769
- // Wait for OTP modal to open
770
769
  await screen.findByTestId('otp-verify')
771
770
 
772
- // Click "Checkout as a guest" button
771
+ // Click "Checkout as a guest" — modal closes; parent is notified; no basket update
773
772
  await user.click(screen.getByText(/Checkout as a guest/i))
774
773
 
774
+ expect(onRegisteredUserChoseGuestSpy).toHaveBeenCalledWith(true)
775
+ expect(setCheckoutGuestChoiceInStorage).toHaveBeenCalledWith(true)
776
+ expect(mockUpdateCustomerForBasket.mutateAsync).not.toHaveBeenCalled()
777
+ expect(mockGoToNextStep).not.toHaveBeenCalled()
778
+
779
+ // Modal closes; user stays on Contact Info (Continue button visible for entering phone)
775
780
  await waitFor(() => {
776
- expect(mockUpdateBillingAddressForBasket.mutateAsync).toHaveBeenCalled()
777
- const callArgs = mockUpdateBillingAddressForBasket.mutateAsync.mock.calls[0]?.[0]
778
- expect(callArgs?.parameters).toMatchObject({basketId: 'test-basket-id'})
779
- expect(callArgs?.body?.phone).toMatch(/727/)
781
+ expect(screen.queryByText("Confirm it's you")).not.toBeInTheDocument()
780
782
  })
783
+ expect(
784
+ screen.getByRole('button', {name: /continue to shipping address/i})
785
+ ).toBeInTheDocument()
786
+ expect(screen.getByLabelText('Phone')).toBeInTheDocument()
781
787
  })
782
788
 
783
789
  test('uses phone from billing address when persisting to customer profile after OTP verification', async () => {
@@ -836,5 +842,8 @@ describe('ContactInfo Component', () => {
836
842
  body: {phoneHome: billingPhone}
837
843
  })
838
844
  })
845
+
846
+ // Guest choice storage should be cleared when user signs in via OTP
847
+ expect(setCheckoutGuestChoiceInStorage).toHaveBeenCalledWith(false)
839
848
  })
840
849
  })
@@ -441,8 +441,8 @@ const Payment = ({
441
441
  disabled={appliedPayment == null}
442
442
  onEdit={handleEditPayment}
443
443
  editLabel={formatMessage({
444
- defaultMessage: 'Edit Payment Info',
445
- id: 'toggle_card.action.editPaymentInfo'
444
+ defaultMessage: 'Change',
445
+ id: 'toggle_card.action.changePaymentInfo'
446
446
  })}
447
447
  >
448
448
  <ToggleCardEdit>
@@ -528,27 +528,28 @@ const Payment = ({
528
528
  isBillingAddress
529
529
  />
530
530
  )}
531
- {(isGuest || showRegistrationNotice) && (
532
- <UserRegistration
533
- enableUserRegistration={enableUserRegistration}
534
- setEnableUserRegistration={onUserRegistrationToggle}
535
- onLoadingChange={onOtpLoadingChange}
536
- isGuestCheckout={registeredUserChoseGuest}
537
- isDisabled={
538
- !(
539
- appliedPayment ||
540
- paymentMethodForm.formState.isValid ||
541
- (isPickupOnly &&
542
- billingAddressForm.formState.isValid)
543
- ) ||
544
- (!effectiveBillingSameAsShipping &&
545
- !billingAddressForm.formState.isValid)
546
- }
547
- onSavePreferenceChange={onSavePreferenceChange}
548
- onRegistered={handleRegistrationSuccess}
549
- showNotice={showRegistrationNotice}
550
- />
551
- )}
531
+ {(isGuest || showRegistrationNotice) &&
532
+ !registeredUserChoseGuest && (
533
+ <UserRegistration
534
+ enableUserRegistration={enableUserRegistration}
535
+ setEnableUserRegistration={onUserRegistrationToggle}
536
+ onLoadingChange={onOtpLoadingChange}
537
+ isGuestCheckout={registeredUserChoseGuest}
538
+ isDisabled={
539
+ !(
540
+ appliedPayment ||
541
+ paymentMethodForm.formState.isValid ||
542
+ (isPickupOnly &&
543
+ billingAddressForm.formState.isValid)
544
+ ) ||
545
+ (!effectiveBillingSameAsShipping &&
546
+ !billingAddressForm.formState.isValid)
547
+ }
548
+ onSavePreferenceChange={onSavePreferenceChange}
549
+ onRegistered={handleRegistrationSuccess}
550
+ showNotice={showRegistrationNotice}
551
+ />
552
+ )}
552
553
  </Stack>
553
554
  </>
554
555
  ) : null}
@@ -579,8 +580,7 @@ const Payment = ({
579
580
 
580
581
  <Divider borderColor="gray.100" />
581
582
 
582
- {(selectedBillingAddress ||
583
- (effectiveBillingSameAsShipping && selectedShippingAddress)) && (
583
+ {selectedBillingAddress && !effectiveBillingSameAsShipping && (
584
584
  <Stack spacing={2}>
585
585
  <Heading as="h3" fontSize="md">
586
586
  <FormattedMessage
@@ -588,13 +588,11 @@ const Payment = ({
588
588
  id="checkout_payment.heading.billing_address"
589
589
  />
590
590
  </Heading>
591
- <AddressDisplay
592
- address={selectedBillingAddress || selectedShippingAddress}
593
- />
591
+ <AddressDisplay address={selectedBillingAddress} />
594
592
  </Stack>
595
593
  )}
596
594
 
597
- {(isGuest || showRegistrationNotice) && (
595
+ {(isGuest || showRegistrationNotice) && !registeredUserChoseGuest && (
598
596
  <UserRegistration
599
597
  enableUserRegistration={enableUserRegistration}
600
598
  setEnableUserRegistration={setEnableUserRegistration}
@@ -1310,7 +1310,7 @@ describe('Payment Component', () => {
1310
1310
  // Click Edit Payment Info to enter edit mode
1311
1311
  const summary = screen.getAllByTestId('toggle-card-summary').pop()
1312
1312
  const editButton = within(summary).getByRole('button', {
1313
- name: /toggle_card.action.editPaymentInfo|Edit Payment Info/i
1313
+ name: /toggle_card.action.changePaymentInfo|Change/i
1314
1314
  })
1315
1315
  await user.click(editButton)
1316
1316
 
@@ -109,13 +109,12 @@ export default function ShippingOptions() {
109
109
  const defaultMethodId = shippingMethods?.defaultShippingMethodId
110
110
  return methods.find((m) => m.id === defaultMethodId) || methods[0]
111
111
  },
112
+ // Skip auto-apply when a valid method is already selected.
113
+ // When the shopper clicked "Change" to edit shippingoptions, stay on the edit view.
112
114
  shouldSkip: () => {
113
115
  if (selectedShippingMethod?.id && !isPickupMethod(selectedShippingMethod)) {
114
116
  const stillValid = deliveryMethods.some((m) => m.id === selectedShippingMethod.id)
115
- if (stillValid) {
116
- goToNextStep()
117
- return true
118
- }
117
+ if (stillValid) return true
119
118
  }
120
119
  return false
121
120
  },
@@ -160,8 +159,11 @@ export default function ShippingOptions() {
160
159
  )
161
160
  }, [step, customer, selectedShippingMethod, shippingMethods, STEPS.SHIPPING_OPTIONS])
162
161
 
163
- // Use calculated loading state or auto-select loading state
164
- const effectiveIsLoading = Boolean(isAutoSelectLoading) || Boolean(shouldShowInitialLoading)
162
+ // Use calculated loading state or auto-select loading state only for single-shipment.
163
+ // For multi-shipment, each ShipmentMethods fetches its own methods
164
+ const effectiveIsLoading = hasMultipleDeliveryShipments
165
+ ? false
166
+ : Boolean(isAutoSelectLoading) || Boolean(shouldShowInitialLoading)
165
167
 
166
168
  const form = useForm({
167
169
  shouldUnregister: false,
@@ -264,8 +266,8 @@ export default function ShippingOptions() {
264
266
  }
265
267
  onEdit={() => goToStep(STEPS.SHIPPING_OPTIONS)}
266
268
  editLabel={formatMessage({
267
- defaultMessage: 'Edit Shipping Options',
268
- id: 'toggle_card.action.editShippingOptions'
269
+ defaultMessage: 'Change',
270
+ id: 'toggle_card.action.changeShippingOptions'
269
271
  })}
270
272
  >
271
273
  <ToggleCardEdit>
@@ -5,7 +5,7 @@
5
5
  * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
6
  */
7
7
  import React from 'react'
8
- import {screen, waitFor} from '@testing-library/react'
8
+ import {screen, waitFor, within} from '@testing-library/react'
9
9
  import ShippingOptions from '@salesforce/retail-react-app/app/pages/checkout-one-click/partials/one-click-shipping-options'
10
10
  import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
11
11
 
@@ -341,7 +341,7 @@ describe('ShippingOptions Component', () => {
341
341
  })
342
342
 
343
343
  describe('for registered users with auto-selection', () => {
344
- test('skips shipping method update when existing method is still valid', async () => {
344
+ test('skips shipping method update when existing method is still valid and stays on edit view', async () => {
345
345
  jest.resetModules()
346
346
 
347
347
  jest.doMock('@salesforce/retail-react-app/app/hooks/use-current-customer', () => ({
@@ -376,10 +376,14 @@ describe('ShippingOptions Component', () => {
376
376
  localRenderWithProviders(<module.default />)
377
377
 
378
378
  await waitFor(() => {
379
- expect(mockGoToNextStep).toHaveBeenCalled()
379
+ expect(mockUpdateShippingMethod.mutateAsync).not.toHaveBeenCalled()
380
380
  })
381
381
 
382
- expect(mockUpdateShippingMethod.mutateAsync).not.toHaveBeenCalled()
382
+ // Does not auto-advance so user can change option or click Continue (fixes "Change" flicker)
383
+ expect(mockGoToNextStep).not.toHaveBeenCalled()
384
+ expect(
385
+ screen.getAllByRole('button', {name: /continue to payment/i}).length
386
+ ).toBeGreaterThan(0)
383
387
  })
384
388
 
385
389
  test('auto-selects default method when existing method is no longer valid', async () => {
@@ -798,6 +802,93 @@ describe('ShippingOptions Component', () => {
798
802
  expect(screen.getByText('Continue to Payment')).toBeInTheDocument()
799
803
  })
800
804
 
805
+ test('multi-shipment edit view shows shipping options for each shipment', async () => {
806
+ jest.resetModules()
807
+
808
+ jest.doMock(
809
+ '@salesforce/retail-react-app/app/pages/checkout-one-click/util/checkout-context',
810
+ () => ({
811
+ useCheckout: jest.fn().mockReturnValue({
812
+ step: 3,
813
+ STEPS: {
814
+ CONTACT_INFO: 0,
815
+ PICKUP_ADDRESS: 1,
816
+ SHIPPING_ADDRESS: 2,
817
+ SHIPPING_OPTIONS: 3,
818
+ PAYMENT: 4
819
+ },
820
+ goToStep: mockGoToStep,
821
+ goToNextStep: mockGoToNextStep
822
+ })
823
+ })
824
+ )
825
+ jest.doMock('@salesforce/retail-react-app/app/hooks/use-current-customer', () => ({
826
+ useCurrentCustomer: () => ({
827
+ data: {customerId: 'test-customer-id', isRegistered: true}
828
+ })
829
+ }))
830
+ jest.doMock('@salesforce/retail-react-app/app/hooks/use-current-basket', () => ({
831
+ useCurrentBasket: () => ({
832
+ data: {
833
+ basketId: 'test-basket-id',
834
+ shipments: [
835
+ {
836
+ shipmentId: 'ship1',
837
+ shippingAddress: {
838
+ firstName: 'Oscar',
839
+ lastName: 'Robertson',
840
+ address1: '333 South St',
841
+ city: 'West Lafayette',
842
+ stateCode: 'IN',
843
+ postalCode: '98103'
844
+ },
845
+ shippingMethod: {id: 'std', name: 'Standard'}
846
+ },
847
+ {
848
+ shipmentId: 'ship2',
849
+ shippingAddress: {
850
+ firstName: 'Lee',
851
+ lastName: 'Robertson',
852
+ address1: '158 South St',
853
+ city: 'West Lafayette',
854
+ stateCode: 'IN',
855
+ postalCode: '98103'
856
+ },
857
+ shippingMethod: {id: 'std2', name: 'Standard 2'}
858
+ }
859
+ ],
860
+ shippingItems: [
861
+ {shipmentId: 'ship1', price: 0},
862
+ {shipmentId: 'ship2', price: 0}
863
+ ]
864
+ },
865
+ derivedData: {hasBasket: true, totalItems: 2, totalShippingCost: 0}
866
+ })
867
+ }))
868
+
869
+ const sdk = await import('@salesforce/commerce-sdk-react')
870
+ sdk.useShippingMethodsForShipment.mockImplementation(({parameters}) => {
871
+ if (parameters.shipmentId === 'ship1') return {data: multiShipMethods1}
872
+ if (parameters.shipmentId === 'ship2') return {data: multiShipMethods2}
873
+ return {data: multiShipMethods1}
874
+ })
875
+
876
+ const {renderWithProviders: localRenderWithProviders} = await import(
877
+ '@salesforce/retail-react-app/app/utils/test-utils'
878
+ )
879
+ const module = await import(
880
+ '@salesforce/retail-react-app/app/pages/checkout-one-click/partials/one-click-shipping-options'
881
+ )
882
+
883
+ localRenderWithProviders(<module.default />)
884
+
885
+ const cards = screen.getAllByTestId('sf-toggle-card-step-2')
886
+ expect(cards.length).toBeGreaterThan(0)
887
+ expect(within(cards[0]).queryByTestId('loading')).toBeNull()
888
+ expect(screen.getAllByText('Shipment 1:').length).toBeGreaterThan(0)
889
+ expect(screen.getAllByText('Shipment 2:').length).toBeGreaterThan(0)
890
+ })
891
+
801
892
  test('auto-selects default method when no method is set on shipment', async () => {
802
893
  jest.resetModules()
803
894
 
@@ -5,7 +5,7 @@
5
5
  * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
6
  */
7
7
  import React, {useRef, useState, useEffect} from 'react'
8
- import {FormattedMessage} from 'react-intl'
8
+ import {FormattedMessage, useIntl} from 'react-intl'
9
9
  import PropTypes from 'prop-types'
10
10
  import {
11
11
  Box,
@@ -23,7 +23,12 @@ import {
23
23
  import OtpAuth from '@salesforce/retail-react-app/app/components/otp-auth'
24
24
  import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
25
25
  import {useCustomerType, useAuthHelper, AuthHelpers} from '@salesforce/commerce-sdk-react'
26
+ import {useShopperCustomersMutation} from '@salesforce/commerce-sdk-react'
26
27
  import useMultiSite from '@salesforce/retail-react-app/app/hooks/use-multi-site'
28
+ import {useCheckout} from '@salesforce/retail-react-app/app/pages/checkout-one-click/util/checkout-context'
29
+ import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast'
30
+ import {isPickupShipment} from '@salesforce/retail-react-app/app/utils/shipment-utils'
31
+ import {nanoid} from 'nanoid'
27
32
 
28
33
  export default function UserRegistration({
29
34
  enableUserRegistration,
@@ -36,15 +41,28 @@ export default function UserRegistration({
36
41
  onLoadingChange
37
42
  }) {
38
43
  const {data: basket} = useCurrentBasket()
44
+ const {contactPhone} = useCheckout()
39
45
  const {isGuest} = useCustomerType()
40
46
  const authorizePasswordlessLogin = useAuthHelper(AuthHelpers.AuthorizePasswordless)
41
47
  const loginPasswordless = useAuthHelper(AuthHelpers.LoginPasswordlessUser)
42
48
  const {locale} = useMultiSite()
49
+ const {formatMessage} = useIntl()
50
+ const showToast = useToast()
51
+ const createCustomerAddress = useShopperCustomersMutation('createCustomerAddress')
52
+ const updateCustomer = useShopperCustomersMutation('updateCustomer')
53
+
43
54
  const {isOpen: isOtpOpen, onOpen: onOtpOpen, onClose: onOtpClose} = useDisclosure()
44
55
  const otpSentRef = useRef(false)
45
56
  const [registrationSucceeded, setRegistrationSucceeded] = useState(false)
46
57
  const [isLoadingOtp, setIsLoadingOtp] = useState(false)
47
58
 
59
+ const showError = (message) => {
60
+ showToast({
61
+ title: message,
62
+ status: 'error'
63
+ })
64
+ }
65
+
48
66
  const handleOtpClose = () => {
49
67
  otpSentRef.current = false
50
68
  onOtpClose()
@@ -92,13 +110,79 @@ export default function UserRegistration({
92
110
  }
93
111
  }, [isOtpOpen, isLoadingOtp, onLoadingChange])
94
112
 
113
+ const saveAddressesAndPhoneToProfile = async (customerId) => {
114
+ if (!basket || !customerId) return
115
+ const deliveryShipments =
116
+ basket.shipments?.filter(
117
+ (shipment) => !isPickupShipment(shipment) && shipment.shippingAddress
118
+ ) || []
119
+ try {
120
+ if (deliveryShipments.length > 0) {
121
+ for (let i = 0; i < deliveryShipments.length; i++) {
122
+ const shipment = deliveryShipments[i]
123
+ const shipping = shipment.shippingAddress
124
+ if (!shipping) continue
125
+
126
+ const {
127
+ address1,
128
+ address2,
129
+ city,
130
+ countryCode,
131
+ firstName,
132
+ lastName,
133
+ phone,
134
+ postalCode,
135
+ stateCode
136
+ } = shipping || {}
137
+
138
+ await createCustomerAddress.mutateAsync({
139
+ parameters: {customerId},
140
+ body: {
141
+ addressId: nanoid(),
142
+ preferred: i === 0,
143
+ address1,
144
+ address2,
145
+ city,
146
+ countryCode,
147
+ firstName,
148
+ lastName,
149
+ phone,
150
+ postalCode,
151
+ stateCode
152
+ }
153
+ })
154
+ }
155
+ }
156
+
157
+ const phoneHome = basket.billingAddress?.phone || contactPhone
158
+ if (phoneHome) {
159
+ await updateCustomer.mutateAsync({
160
+ parameters: {customerId},
161
+ body: {phoneHome}
162
+ })
163
+ }
164
+ } catch (_e) {
165
+ showError(
166
+ formatMessage({
167
+ id: 'checkout.error.cannot_save_address',
168
+ defaultMessage: 'Could not save shipping address.'
169
+ })
170
+ )
171
+ }
172
+ }
173
+
95
174
  const handleOtpVerification = async (otpCode) => {
96
175
  try {
97
- await loginPasswordless.mutateAsync({
176
+ const token = await loginPasswordless.mutateAsync({
98
177
  pwdlessLoginToken: otpCode,
99
178
  register_customer: true
100
179
  })
101
180
 
181
+ const customerId = token?.customer_id || token?.customerId
182
+ if (customerId && basket) {
183
+ await saveAddressesAndPhoneToProfile(customerId)
184
+ }
185
+
102
186
  if (onRegistered) {
103
187
  await onRegistered(basket?.basketId)
104
188
  }
@@ -15,6 +15,13 @@ import useAuthContext from '@salesforce/commerce-sdk-react/hooks/useAuthContext'
15
15
 
16
16
  jest.mock('@salesforce/retail-react-app/app/hooks/use-current-basket')
17
17
 
18
+ jest.mock(
19
+ '@salesforce/retail-react-app/app/pages/checkout-one-click/util/checkout-context',
20
+ () => ({
21
+ useCheckout: () => ({contactPhone: ''})
22
+ })
23
+ )
24
+
18
25
  const {AuthHelpers} = jest.requireActual('@salesforce/commerce-sdk-react')
19
26
 
20
27
  const TEST_MESSAGES = {
@@ -29,12 +36,23 @@ const mockAuthHelperFunctions = {
29
36
  [AuthHelpers.LoginPasswordlessUser]: {mutateAsync: jest.fn()}
30
37
  }
31
38
 
39
+ const mockCreateCustomerAddress = {mutateAsync: jest.fn().mockResolvedValue({})}
40
+ const mockUpdateCustomer = {mutateAsync: jest.fn().mockResolvedValue({})}
41
+ const mockCreateCustomerPaymentInstrument = {mutateAsync: jest.fn().mockResolvedValue({})}
42
+
32
43
  jest.mock('@salesforce/commerce-sdk-react', () => {
33
44
  const original = jest.requireActual('@salesforce/commerce-sdk-react')
34
45
  return {
35
46
  ...original,
36
47
  useCustomerType: jest.fn(),
37
- useAuthHelper: jest.fn((helper) => mockAuthHelperFunctions[helper])
48
+ useAuthHelper: jest.fn((helper) => mockAuthHelperFunctions[helper]),
49
+ useShopperCustomersMutation: jest.fn((mutationType) => {
50
+ if (mutationType === 'createCustomerAddress') return mockCreateCustomerAddress
51
+ if (mutationType === 'updateCustomer') return mockUpdateCustomer
52
+ if (mutationType === 'createCustomerPaymentInstrument')
53
+ return mockCreateCustomerPaymentInstrument
54
+ return {mutateAsync: jest.fn()}
55
+ })
38
56
  }
39
57
  })
40
58
  jest.mock('@salesforce/commerce-sdk-react/hooks/useAuthContext', () =>
@@ -10,6 +10,26 @@ import useEinstein from '@salesforce/retail-react-app/app/hooks/use-einstein'
10
10
  import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
11
11
  import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
12
12
  import {isPickupShipment} from '@salesforce/retail-react-app/app/utils/shipment-utils'
13
+ import {
14
+ getSessionJSONItem,
15
+ setSessionJSONItem,
16
+ clearSessionJSONItem
17
+ } from '@salesforce/retail-react-app/app/utils/utils'
18
+
19
+ /** SessionStorage key for "checkout as guest" choice so it persists when shopper navigates away and returns */
20
+ export const CHECKOUT_GUEST_CHOICE_STORAGE_KEY = 'sf_checkout_one_click_guest_choice'
21
+
22
+ export const getCheckoutGuestChoiceFromStorage = () => {
23
+ return getSessionJSONItem(CHECKOUT_GUEST_CHOICE_STORAGE_KEY) === true
24
+ }
25
+
26
+ export const setCheckoutGuestChoiceInStorage = (value) => {
27
+ if (value) {
28
+ setSessionJSONItem(CHECKOUT_GUEST_CHOICE_STORAGE_KEY, true)
29
+ } else {
30
+ clearSessionJSONItem(CHECKOUT_GUEST_CHOICE_STORAGE_KEY)
31
+ }
32
+ }
13
33
 
14
34
  const CheckoutContext = React.createContext()
15
35
 
@@ -1249,10 +1249,10 @@
1249
1249
  "value": " with your confirmation number and receipt shortly."
1250
1250
  }
1251
1251
  ],
1252
- "checkout_contact_info.action.edit": [
1252
+ "checkout_contact_info.action.change": [
1253
1253
  {
1254
1254
  "type": 0,
1255
- "value": "Edit"
1255
+ "value": "Change"
1256
1256
  }
1257
1257
  ],
1258
1258
  "checkout_contact_info.action.sign_out": [
@@ -4681,6 +4681,18 @@
4681
4681
  "value": "Change"
4682
4682
  }
4683
4683
  ],
4684
+ "toggle_card.action.changePaymentInfo": [
4685
+ {
4686
+ "type": 0,
4687
+ "value": "Change"
4688
+ }
4689
+ ],
4690
+ "toggle_card.action.changeShippingOptions": [
4691
+ {
4692
+ "type": 0,
4693
+ "value": "Change"
4694
+ }
4695
+ ],
4684
4696
  "toggle_card.action.edit": [
4685
4697
  {
4686
4698
  "type": 0,
@@ -1249,10 +1249,10 @@
1249
1249
  "value": " with your confirmation number and receipt shortly."
1250
1250
  }
1251
1251
  ],
1252
- "checkout_contact_info.action.edit": [
1252
+ "checkout_contact_info.action.change": [
1253
1253
  {
1254
1254
  "type": 0,
1255
- "value": "Edit"
1255
+ "value": "Change"
1256
1256
  }
1257
1257
  ],
1258
1258
  "checkout_contact_info.action.sign_out": [
@@ -4681,6 +4681,18 @@
4681
4681
  "value": "Change"
4682
4682
  }
4683
4683
  ],
4684
+ "toggle_card.action.changePaymentInfo": [
4685
+ {
4686
+ "type": 0,
4687
+ "value": "Change"
4688
+ }
4689
+ ],
4690
+ "toggle_card.action.changeShippingOptions": [
4691
+ {
4692
+ "type": 0,
4693
+ "value": "Change"
4694
+ }
4695
+ ],
4684
4696
  "toggle_card.action.edit": [
4685
4697
  {
4686
4698
  "type": 0,
@@ -2473,14 +2473,14 @@
2473
2473
  "value": "]"
2474
2474
  }
2475
2475
  ],
2476
- "checkout_contact_info.action.edit": [
2476
+ "checkout_contact_info.action.change": [
2477
2477
  {
2478
2478
  "type": 0,
2479
2479
  "value": "["
2480
2480
  },
2481
2481
  {
2482
2482
  "type": 0,
2483
- "value": "Ḗḓīŧ"
2483
+ "value": "Ƈħȧȧƞɠḗḗ"
2484
2484
  },
2485
2485
  {
2486
2486
  "type": 0,
@@ -9841,6 +9841,34 @@
9841
9841
  "value": "]"
9842
9842
  }
9843
9843
  ],
9844
+ "toggle_card.action.changePaymentInfo": [
9845
+ {
9846
+ "type": 0,
9847
+ "value": "["
9848
+ },
9849
+ {
9850
+ "type": 0,
9851
+ "value": "Ƈħȧȧƞɠḗḗ"
9852
+ },
9853
+ {
9854
+ "type": 0,
9855
+ "value": "]"
9856
+ }
9857
+ ],
9858
+ "toggle_card.action.changeShippingOptions": [
9859
+ {
9860
+ "type": 0,
9861
+ "value": "["
9862
+ },
9863
+ {
9864
+ "type": 0,
9865
+ "value": "Ƈħȧȧƞɠḗḗ"
9866
+ },
9867
+ {
9868
+ "type": 0,
9869
+ "value": "]"
9870
+ }
9871
+ ],
9844
9872
  "toggle_card.action.edit": [
9845
9873
  {
9846
9874
  "type": 0,
package/config/default.js CHANGED
@@ -74,6 +74,9 @@ module.exports = {
74
74
  appSourceId: '7ae070a6-f4ec-4def-a383-d9cacc3f20a1',
75
75
  tenantId: 'g82wgnrvm-ywk9dggrrw8mtggy.pc-rnd'
76
76
  },
77
+ // Note: this feature is in Developer Preview at this time. To use One Click Checkout,
78
+ // enable the oneClickCheckout flag and configure private SLAS client. For more details, please
79
+ // check https://github.com/SalesforceCommerceCloud/pwa-kit/releases/tag/v3.16.0
77
80
  oneClickCheckout: {
78
81
  enabled: false
79
82
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/retail-react-app",
3
- "version": "9.0.0-preview.0",
3
+ "version": "9.0.0-preview.2",
4
4
  "license": "See license in LICENSE",
5
5
  "author": "cc-pwa-kit@salesforce.com",
6
6
  "ccExtensibility": {
@@ -46,10 +46,10 @@
46
46
  "@loadable/component": "^5.15.3",
47
47
  "@peculiar/webcrypto": "^1.4.2",
48
48
  "@salesforce/cc-datacloud-typescript": "1.1.2",
49
- "@salesforce/commerce-sdk-react": "5.0.0-preview.0",
50
- "@salesforce/pwa-kit-dev": "3.16.0-preview.0",
51
- "@salesforce/pwa-kit-react-sdk": "3.16.0-preview.0",
52
- "@salesforce/pwa-kit-runtime": "3.16.0-preview.0",
49
+ "@salesforce/commerce-sdk-react": "5.0.0-preview.2",
50
+ "@salesforce/pwa-kit-dev": "3.16.0-preview.2",
51
+ "@salesforce/pwa-kit-react-sdk": "3.16.0-preview.2",
52
+ "@salesforce/pwa-kit-runtime": "3.16.0-preview.2",
53
53
  "@tanstack/react-query": "^4.28.0",
54
54
  "@tanstack/react-query-devtools": "^4.29.1",
55
55
  "@testing-library/dom": "^9.0.1",
@@ -108,5 +108,5 @@
108
108
  "maxSize": "366 kB"
109
109
  }
110
110
  ],
111
- "gitHead": "f14c1e991a7657031ac73de7c8a4445e6a66a33d"
111
+ "gitHead": "70ef0ffc94a2538cbf735883eb6061d410980438"
112
112
  }
@@ -460,8 +460,8 @@
460
460
  "checkout_confirmation.message.will_email_shortly": {
461
461
  "defaultMessage": "We will send an email to <b>{email}</b> with your confirmation number and receipt shortly."
462
462
  },
463
- "checkout_contact_info.action.edit": {
464
- "defaultMessage": "Edit"
463
+ "checkout_contact_info.action.change": {
464
+ "defaultMessage": "Change"
465
465
  },
466
466
  "checkout_contact_info.action.sign_out": {
467
467
  "defaultMessage": "Sign Out"
@@ -1949,6 +1949,12 @@
1949
1949
  "toggle_card.action.change": {
1950
1950
  "defaultMessage": "Change"
1951
1951
  },
1952
+ "toggle_card.action.changePaymentInfo": {
1953
+ "defaultMessage": "Change"
1954
+ },
1955
+ "toggle_card.action.changeShippingOptions": {
1956
+ "defaultMessage": "Change"
1957
+ },
1952
1958
  "toggle_card.action.edit": {
1953
1959
  "defaultMessage": "Edit"
1954
1960
  },
@@ -460,8 +460,8 @@
460
460
  "checkout_confirmation.message.will_email_shortly": {
461
461
  "defaultMessage": "We will send an email to <b>{email}</b> with your confirmation number and receipt shortly."
462
462
  },
463
- "checkout_contact_info.action.edit": {
464
- "defaultMessage": "Edit"
463
+ "checkout_contact_info.action.change": {
464
+ "defaultMessage": "Change"
465
465
  },
466
466
  "checkout_contact_info.action.sign_out": {
467
467
  "defaultMessage": "Sign Out"
@@ -1949,6 +1949,12 @@
1949
1949
  "toggle_card.action.change": {
1950
1950
  "defaultMessage": "Change"
1951
1951
  },
1952
+ "toggle_card.action.changePaymentInfo": {
1953
+ "defaultMessage": "Change"
1954
+ },
1955
+ "toggle_card.action.changeShippingOptions": {
1956
+ "defaultMessage": "Change"
1957
+ },
1952
1958
  "toggle_card.action.edit": {
1953
1959
  "defaultMessage": "Edit"
1954
1960
  },