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 @@ -233,11 +233,13 @@ const Payment = ({
})
}

const [showRegistrationNotice, setShowRegistrationNotice] = useState(false)
const handleRegistrationSuccess = useCallback(
async (newBasketId) => {
if (newBasketId) {
activeBasketIdRef.current = newBasketId
}
setShowRegistrationNotice(true)
setShouldSavePaymentMethod(true)
try {
const values = paymentMethodForm?.getValues?.()
Expand Down Expand Up @@ -563,7 +565,7 @@ const Payment = ({
isBillingAddress
/>
)}
{isGuest && (
{(isGuest || showRegistrationNotice) && (
<UserRegistration
enableUserRegistration={enableUserRegistration}
setEnableUserRegistration={onUserRegistrationToggle}
Expand All @@ -578,6 +580,7 @@ const Payment = ({
}
onSavePreferenceChange={onSavePreferenceChange}
onRegistered={handleRegistrationSuccess}
showNotice={showRegistrationNotice}
/>
)}
</Stack>
Expand Down Expand Up @@ -610,26 +613,30 @@ const Payment = ({

<Divider borderColor="gray.100" />

{selectedBillingAddress && (
{(selectedBillingAddress ||
(effectiveBillingSameAsShipping && selectedShippingAddress)) && (
<Stack spacing={2}>
<Heading as="h3" fontSize="md">
<FormattedMessage
defaultMessage="Billing Address"
id="checkout_payment.heading.billing_address"
/>
</Heading>
<AddressDisplay address={selectedBillingAddress} />
<AddressDisplay
address={selectedBillingAddress || selectedShippingAddress}
/>
</Stack>
)}

{isGuest && (
{(isGuest || showRegistrationNotice) && (
<UserRegistration
enableUserRegistration={enableUserRegistration}
setEnableUserRegistration={setEnableUserRegistration}
isGuestCheckout={registeredUserChoseGuest}
isDisabled={!appliedPayment && !paymentMethodForm.formState.isValid}
onSavePreferenceChange={onSavePreferenceChange}
onRegistered={handleRegistrationSuccess}
showNotice={showRegistrationNotice}
/>
)}
</Stack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,9 @@ import {
import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast'
import {Text, Button, Box} from '@salesforce/retail-react-app/app/components/shared/ui'
import {Text} from '@salesforce/retail-react-app/app/components/shared/ui'
import {isPickupShipment} from '@salesforce/retail-react-app/app/utils/shipment-utils'
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
import {useSelectedStore} from '@salesforce/retail-react-app/app/hooks/use-selected-store'
import useNavigation from '@salesforce/retail-react-app/app/hooks/use-navigation'
import usePickupShipment from '@salesforce/retail-react-app/app/hooks/use-pickup-shipment'
import {STORE_LOCATOR_IS_ENABLED} from '@salesforce/retail-react-app/app/constants'

const submitButtonMessage = defineMessage({
defaultMessage: 'Continue to Shipping Method',
Expand Down Expand Up @@ -68,11 +64,6 @@ export default function ShippingAddress() {

const hasMultipleDeliveryShipments = deliveryShipments.length > 1

const storeLocatorEnabled = getConfig()?.app?.storeLocatorEnabled ?? STORE_LOCATOR_IS_ENABLED
const {selectedStore} = useSelectedStore()
const {navigate} = useNavigation()
const {updatePickupShipment} = usePickupShipment(basket)

// Prepare a shipping methods query we can manually refetch after address updates
const shippingMethodsQuery = useShippingMethodsForShipment(
{
Expand All @@ -86,29 +77,6 @@ export default function ShippingAddress() {
}
)

const switchToPickup = async () => {
try {
if (!selectedStore?.inventoryId) {
navigate('/store-locator')
return
}
const refreshed = await currentBasketQuery.refetch()
const latestBasketId = refreshed?.data?.basketId || basket.basketId
await updatePickupShipment(latestBasketId, selectedStore)
await currentBasketQuery.refetch()
goToStep(STEPS.PICKUP_ADDRESS)
} catch (_e) {
toast({
title: formatMessage({
defaultMessage:
'We could not switch to Store Pickup. Please try again or choose a different store.',
id: 'shipping_address.error.switch_to_pickup_failed'
}),
status: 'error'
})
}
}

const submitAndContinue = async (address) => {
setIsLoading(true)
try {
Expand Down Expand Up @@ -314,24 +282,12 @@ export default function ShippingAddress() {
onBackToSingle={() => setIsMultiShipping(false)}
/>
) : (
<>
{storeLocatorEnabled && (
<Box mb={3}>
<Button variant="link" onClick={switchToPickup}>
{formatMessage({
defaultMessage: 'Pick up in store',
id: 'shipping_address.action.pickup_in_store'
})}
</Button>
</Box>
)}
<ShippingAddressSelection
selectedAddress={selectedShippingAddress}
submitButtonLabel={submitButtonMessage}
onSubmit={submitAndContinue}
formTitleAriaLabel={shippingAddressAriaLabel}
/>
</>
<ShippingAddressSelection
selectedAddress={selectedShippingAddress}
submitButtonLabel={submitButtonMessage}
onSubmit={submitAndContinue}
formTitleAriaLabel={shippingAddressAriaLabel}
/>
)}
</ToggleCardEdit>
{(hasMultipleDeliveryShipments || isAddressFilled) && (
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, {useRef} from 'react'
import React, {useRef, useState} from 'react'
import {FormattedMessage} from 'react-intl'
import PropTypes from 'prop-types'
import {
Expand All @@ -13,6 +13,8 @@ import {
Stack,
Text,
Heading,
Badge,
HStack,
useDisclosure
} from '@salesforce/retail-react-app/app/components/shared/ui'
import OtpAuth from '@salesforce/retail-react-app/app/components/otp-auth'
Expand All @@ -28,7 +30,8 @@ export default function UserRegistration({
isGuestCheckout = false,
isDisabled = false,
onSavePreferenceChange,
onRegistered
onRegistered,
showNotice = false
}) {
const {data: basket} = useCurrentBasket()
const {isGuest} = useCustomerType()
Expand All @@ -41,6 +44,7 @@ export default function UserRegistration({
: `${appOrigin}${passwordlessConfigCallback}`
const {isOpen: isOtpOpen, onOpen: onOtpOpen, onClose: onOtpClose} = useDisclosure()
const otpSentRef = useRef(false)
const [registrationSucceeded, setRegistrationSucceeded] = useState(false)

const handleOtpClose = () => {
otpSentRef.current = false
Expand Down Expand Up @@ -85,6 +89,7 @@ export default function UserRegistration({
await onRegistered(basket?.basketId)
}
handleOtpClose()
setRegistrationSucceeded(true)
} catch (_e) {
// Let OtpAuth surface errors via its own UI/toast
}
Expand All @@ -96,6 +101,49 @@ export default function UserRegistration({
return null
}

// After successful registration (local) or when parent instructs to show, render notice
if (registrationSucceeded || showNotice) {
return (
<Box
border="1px solid"
borderColor="gray.200"
rounded="md"
p={4}
data-testid="sf-account-creation-notification"
>
<HStack justify="space-between" align="start" mb={2}>
<Heading fontSize="lg" lineHeight="30px">
<FormattedMessage
defaultMessage="Account Created"
id="account_creation_notification.title"
/>
</Heading>
<Badge
colorScheme="green"
fontSize="0.9em"
px={3}
py={1}
rounded="md"
aria-label="Verified"
>
<FormattedMessage
defaultMessage="Verified"
id="account_creation_notification.verified"
/>
</Badge>
</HStack>
<Stack spacing={2}>
<Text color="gray.700">
<FormattedMessage
defaultMessage="We’ve created and verified your account using the information from your order. Next time you check out, just enter the code we send to log in — no password needed."
id="account_creation_notification.body"
/>
</Text>
</Stack>
</Box>
)
}

return (
<>
<Box
Expand Down Expand Up @@ -175,5 +223,7 @@ UserRegistration.propTypes = {
isDisabled: PropTypes.bool,
/** Callback to set save-for-future preference */
onSavePreferenceChange: PropTypes.func,
onRegistered: PropTypes.func
onRegistered: PropTypes.func,
/** When true, forces the success notice to show (e.g., after component would normally unmount) */
showNotice: PropTypes.bool
}
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,42 @@ describe('UserRegistration', () => {
expect(authorizePasswordlessLogin.mutateAsync).toHaveBeenCalledTimes(1)
})

test('shows account creation notification after successful OTP verification', async () => {
const user = userEvent.setup()
setup()
// Enable registration to trigger OTP
await user.click(screen.getByRole('checkbox', {name: /Create an account/i}))
// Verify OTP (mocked)
const otpButton = await screen.findByTestId('otp-verify')
await user.click(otpButton)
// Notification should appear after registration succeeds
await waitFor(() => {
expect(screen.getByTestId('sf-account-creation-notification')).toBeInTheDocument()
})
// Optional: assert key content
expect(screen.getByText(/Account Created/i)).toBeInTheDocument()
// Use aria-label to avoid ambiguity with body text containing 'verified'
expect(screen.getByLabelText(/Verified/i)).toBeInTheDocument()
})

test('renders account creation notification when showNotice prop is true', async () => {
render(
<IntlProvider locale="en-GB">
<UserRegistration
enableUserRegistration={false}
setEnableUserRegistration={jest.fn()}
isGuestCheckout={false}
isDisabled={false}
onSavePreferenceChange={jest.fn()}
onRegistered={jest.fn()}
showNotice
/>
</IntlProvider>
)
expect(screen.getByTestId('sf-account-creation-notification')).toBeInTheDocument()
expect(screen.getByText(/Account Created/i)).toBeInTheDocument()
})

test('calls loginPasswordless with OTP code and register flag', async () => {
const user = userEvent.setup()
const {loginPasswordless} = setup()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,24 @@
"value": "Addresses"
}
],
"account_creation_notification.body": [
{
"type": 0,
"value": "We’ve created and verified your account using the information from your order. Next time you check out, just enter the code we send to log in — no password needed."
}
],
"account_creation_notification.title": [
{
"type": 0,
"value": "Account Created"
}
],
"account_creation_notification.verified": [
{
"type": 0,
"value": "Verified"
}
],
"account_detail.title.account_details": [
{
"type": 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,24 @@
"value": "Addresses"
}
],
"account_creation_notification.body": [
{
"type": 0,
"value": "We’ve created and verified your account using the information from your order. Next time you check out, just enter the code we send to log in — no password needed."
}
],
"account_creation_notification.title": [
{
"type": 0,
"value": "Account Created"
}
],
"account_creation_notification.verified": [
{
"type": 0,
"value": "Verified"
}
],
"account_detail.title.account_details": [
{
"type": 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,48 @@
"value": "]"
}
],
"account_creation_notification.body": [
{
"type": 0,
"value": "["
},
{
"type": 0,
"value": "Ẇḗḗ’ṽḗḗ ƈřḗḗȧȧŧḗḗḓ ȧȧƞḓ ṽḗḗřīƒīḗḗḓ ẏǿǿŭŭř ȧȧƈƈǿǿŭŭƞŧ ŭŭşīƞɠ ŧħḗḗ īƞƒǿǿřḿȧȧŧīǿǿƞ ƒřǿǿḿ ẏǿǿŭŭř ǿǿřḓḗḗř. Ƞḗḗẋŧ ŧīḿḗḗ ẏǿǿŭŭ ƈħḗḗƈķ ǿǿŭŭŧ, ĵŭŭşŧ ḗḗƞŧḗḗř ŧħḗḗ ƈǿǿḓḗḗ ẇḗḗ şḗḗƞḓ ŧǿǿ ŀǿǿɠ īƞ — ƞǿǿ ƥȧȧşşẇǿǿřḓ ƞḗḗḗḗḓḗḗḓ."
},
{
"type": 0,
"value": "]"
}
],
"account_creation_notification.title": [
{
"type": 0,
"value": "["
},
{
"type": 0,
"value": "Ȧƈƈǿǿŭŭƞŧ Ƈřḗḗȧȧŧḗḗḓ"
},
{
"type": 0,
"value": "]"
}
],
"account_creation_notification.verified": [
{
"type": 0,
"value": "["
},
{
"type": 0,
"value": "Ṽḗḗřīƒīḗḗḓ"
},
{
"type": 0,
"value": "]"
}
],
"account_detail.title.account_details": [
{
"type": 0,
Expand Down
Loading
Loading