Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ea3b3df
fix pwless login in checkout contact info
alexvuong Jul 8, 2025
dc12c4a
fix changelog
alexvuong Jul 8, 2025
fc5e79e
minor fix
alexvuong Jul 8, 2025
d48e1af
lint
alexvuong Jul 8, 2025
c1c3804
lint
alexvuong Jul 8, 2025
0e89a9d
small fix
alexvuong Jul 8, 2025
80b0bfd
clean up
alexvuong Jul 8, 2025
86d6229
build translations
alexvuong Jul 8, 2025
eea3677
provide context
alexvuong Jul 8, 2025
5d705a3
apply the same for use auth modal
alexvuong Jul 9, 2025
6af0bca
fix the logic in use auth modal
alexvuong Jul 10, 2025
91c722e
fix the logic in login page
alexvuong Jul 10, 2025
9f6a29c
fix the logic in login page
alexvuong Jul 10, 2025
58bbcb3
translations
alexvuong Jul 10, 2025
8dec3de
Merge branch 'develop' into fix-pwless-login-contact-checkout
alexvuong Jul 10, 2025
b0a004f
lint
alexvuong Jul 10, 2025
774ab91
Merge branch 'develop' into fix-pwless-login-contact-checkout
alexvuong Jul 10, 2025
952ef3e
lint
alexvuong Jul 10, 2025
1233881
lint
alexvuong Jul 10, 2025
874797a
Merge branch 'develop' into fix-pwless-login-contact-checkout
alexvuong Jul 10, 2025
db741e2
allow enter to be aware of passwordless
alexvuong Jul 11, 2025
9180224
lint
alexvuong Jul 11, 2025
847f979
edit changlog
alexvuong Jul 11, 2025
bddbfe4
remove constant
alexvuong Jul 11, 2025
443a51a
simplify the logic
alexvuong Jul 14, 2025
07d4e75
simplify the logic
alexvuong Jul 14, 2025
eaa944a
simplify the logic
alexvuong Jul 14, 2025
c9626da
simplify the logic
alexvuong Jul 14, 2025
168c9f2
revert test
alexvuong Jul 14, 2025
c5c5756
lint
alexvuong Jul 14, 2025
f329e64
Merge branch 'develop' into fix-pwless-login-contact-checkout
alexvuong Jul 14, 2025
2ec021a
revert constants.js
alexvuong Jul 14, 2025
c60168e
Merge remote-tracking branch 'origin/fix-pwless-login-contact-checkou…
alexvuong Jul 14, 2025
1fd92cd
PR feedback
alexvuong Jul 14, 2025
251b983
Merge branch 'develop' into fix-pwless-login-contact-checkout
alexvuong Jul 14, 2025
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
4 changes: 2 additions & 2 deletions packages/template-retail-react-app/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
- Provide support for partial hydration [#2696](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2696)
- Show Automatic Bonus Products on Cart Page [#2704](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2704)
- Support Standard Products [2697](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2697)

- Fix passwordless race conditions in form submission [#2758](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2758)

## v6.1.0 (May 22, 2025)

Expand Down Expand Up @@ -453,4 +453,4 @@ The versions published below were not published on npm, and the versioning match

### v1.0.0 (Sep 08, 2021)

- PWA Kit General Availability and open source. 🎉
- PWA Kit General Availability and open source. 🎉
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,19 @@ import {Button, Divider, Stack, Text} from '@salesforce/retail-react-app/app/com
import LoginFields from '@salesforce/retail-react-app/app/components/forms/login-fields'
import StandardLogin from '@salesforce/retail-react-app/app/components/standard-login'
import SocialLogin from '@salesforce/retail-react-app/app/components/social-login'
import {LOGIN_TYPES} from '@salesforce/retail-react-app/app/constants'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we delete this constant from constants.js if we are not using it anymore?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep it since removing it will be a breaking change


const noop = () => {}
const PasswordlessLogin = ({
form,
handleForgotPasswordClick,
handlePasswordlessLoginClick,
isSocialEnabled = false,
idps = [],
setLoginType
setLoginType = noop
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need this prop in the logic but we can't remove it now since it will be a breaking change.

}) => {
const [showPasswordView, setShowPasswordView] = useState(false)

const handlePasswordButton = async (e) => {
setLoginType(LOGIN_TYPES.PASSWORD)
const isValid = await form.trigger()
// Manually trigger the browser native form validations
const domForm = e.target.closest('form')
Expand All @@ -48,8 +47,8 @@ const PasswordlessLogin = ({
/>
<Button
type="submit"
onClick={() => {
handlePasswordlessLoginClick()
onClick={(e) => {
handlePasswordlessLoginClick(e)
form.clearErrors('global')
}}
isLoading={form.formState.isSubmitting}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ describe('PasswordlessLogin component', () => {
})

test('renders password input after "Password" button is clicked', async () => {
const mockSetLoginType = jest.fn()
const {user} = renderWithProviders(<WrapperComponent setLoginType={mockSetLoginType} />)
const {user} = renderWithProviders(<WrapperComponent />)

await user.type(screen.getByLabelText('Email'), 'myemail@test.com')
await user.click(screen.getByRole('button', {name: 'Password'}))
Expand All @@ -42,8 +41,7 @@ describe('PasswordlessLogin component', () => {
})

test('stays on page when email field has form validation errors after the "Password" button is clicked', async () => {
const mockSetLoginType = jest.fn()
const {user} = renderWithProviders(<WrapperComponent setLoginType={mockSetLoginType} />)
const {user} = renderWithProviders(<WrapperComponent />)

await user.type(screen.getByLabelText('Email'), 'badEmail')
await user.click(screen.getByRole('button', {name: 'Password'}))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ const SocialLogin = ({form, idps = []}) => {
return (
config && (
<Button
key={name}
onClick={() => {
onSocialLoginClick(name)
}}
Expand Down
1 change: 0 additions & 1 deletion packages/template-retail-react-app/app/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,6 @@ export const SHOPPER_CONTEXT_SEARCH_PARAMS = {
}
}

// Constants for Login
export const LOGIN_TYPES = {
PASSWORD: 'password',
PASSWORDLESS: 'passwordless',
Expand Down
135 changes: 68 additions & 67 deletions packages/template-retail-react-app/app/hooks/use-auth-modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import {
API_ERROR_MESSAGE,
CREATE_ACCOUNT_FIRST_ERROR_MESSAGE,
FEATURE_UNAVAILABLE_ERROR_MESSAGE,
LOGIN_TYPES,
PASSWORDLESS_ERROR_MESSAGES,
USER_NOT_FOUND_ERROR
} from '@salesforce/retail-react-app/app/constants'
Expand Down Expand Up @@ -88,8 +87,6 @@ export const AuthModal = ({
const register = useAuthHelper(AuthHelpers.Register)
const appOrigin = useAppOrigin()

const [loginType, setLoginType] = useState(LOGIN_TYPES.PASSWORD)
const [passwordlessLoginEmail, setPasswordlessLoginEmail] = useState(initialEmail)
const {getPasswordResetToken} = usePasswordReset()
const authorizePasswordlessLogin = useAuthHelper(AuthHelpers.AuthorizePasswordless)
const passwordlessConfigCallback = getConfig().app.login?.passwordless?.callbackURI
Expand All @@ -103,66 +100,67 @@ export const AuthModal = ({
)
const mergeBasket = useShopperBasketsMutation('mergeBasket')

const submitForm = async (data) => {
const handlePasswordlessLogin = async (email) => {
try {
const redirectPath = window.location.pathname + (window.location.search || '')
await authorizePasswordlessLogin.mutateAsync({
userid: email,
callbackURI: `${callbackURL}?redirectUrl=${redirectPath}`
})
setCurrentView(EMAIL_VIEW)
} catch (error) {
const message = USER_NOT_FOUND_ERROR.test(error.message)
? formatMessage(CREATE_ACCOUNT_FIRST_ERROR_MESSAGE)
: PASSWORDLESS_ERROR_MESSAGES.some((msg) => msg.test(error.message))
? formatMessage(FEATURE_UNAVAILABLE_ERROR_MESSAGE)
: formatMessage(API_ERROR_MESSAGE)
form.setError('global', {type: 'manual', message})
}
}

const submitForm = async (data, isPasswordless = false) => {
form.clearErrors()

const onLoginSuccess = () => {
navigate('/account')
}

const handlePasswordlessLogin = async (email) => {
try {
const redirectPath = window.location.pathname + window.location.search
await authorizePasswordlessLogin.mutateAsync({
userid: email,
callbackURI: `${callbackURL}?redirectUrl=${redirectPath}`
})
setCurrentView(EMAIL_VIEW)
} catch (error) {
const message = USER_NOT_FOUND_ERROR.test(error.message)
? formatMessage(CREATE_ACCOUNT_FIRST_ERROR_MESSAGE)
: PASSWORDLESS_ERROR_MESSAGES.some((msg) => msg.test(error.message))
? formatMessage(FEATURE_UNAVAILABLE_ERROR_MESSAGE)
: formatMessage(API_ERROR_MESSAGE)
form.setError('global', {type: 'manual', message})
}
}

return {
login: async (data) => {
if (loginType === LOGIN_TYPES.PASSWORD) {
try {
await login.mutateAsync({
username: data.email,
password: data.password
if (isPasswordless) {
const email = data.email
await handlePasswordlessLogin(email)
return
}

try {
await login.mutateAsync({
username: data.email,
password: data.password
})
const hasBasketItem = baskets?.baskets?.[0]?.productItems?.length > 0
// we only want to merge basket when the user is logged in as a recurring user
// only recurring users trigger the login mutation, new user triggers register mutation
// this logic needs to stay in this block because this is the only place that tells if a user is a recurring user
// if you change logic here, also change it in login page
const shouldMergeBasket = hasBasketItem && prevAuthType === 'guest'
if (shouldMergeBasket) {
mergeBasket.mutate({
headers: {
// This is not required since the request has no body
// but CommerceAPI throws a '419 - Unsupported Media Type' error if this header is removed.
'Content-Type': 'application/json'
},
parameters: {
createDestinationBasket: true
}
})
const hasBasketItem = baskets?.baskets?.[0]?.productItems?.length > 0
// we only want to merge basket when the user is logged in as a recurring user
// only recurring users trigger the login mutation, new user triggers register mutation
// this logic needs to stay in this block because this is the only place that tells if a user is a recurring user
// if you change logic here, also change it in login page
const shouldMergeBasket = hasBasketItem && prevAuthType === 'guest'
if (shouldMergeBasket) {
mergeBasket.mutate({
headers: {
// This is not required since the request has no body
// but CommerceAPI throws a '419 - Unsupported Media Type' error if this header is removed.
'Content-Type': 'application/json'
},
parameters: {
createDestinationBasket: true
}
})
}
} catch (error) {
const message = /Unauthorized/i.test(error.message)
? formatMessage(LOGIN_ERROR)
: formatMessage(API_ERROR_MESSAGE)
form.setError('global', {type: 'manual', message})
}
} else if (loginType === LOGIN_TYPES.PASSWORDLESS) {
setPasswordlessLoginEmail(data.email)
await handlePasswordlessLogin(data.email)
} catch (error) {
const message = /Unauthorized/i.test(error.message)
? formatMessage(LOGIN_ERROR)
: formatMessage(API_ERROR_MESSAGE)
form.setError('global', {type: 'manual', message})
}
},
register: async (data) => {
Expand Down Expand Up @@ -198,15 +196,15 @@ export const AuthModal = ({
}
},
email: async () => {
await handlePasswordlessLogin(passwordlessLoginEmail)
const email = form.getValues().email || initialEmail
await handlePasswordlessLogin(email)
}
}[currentView](data)
}

// Reset form and local state when opening the modal
useEffect(() => {
if (isOpen) {
setLoginType(LOGIN_TYPES.PASSWORD)
setCurrentView(initialView)
form.reset()
}
Expand All @@ -223,15 +221,14 @@ export const AuthModal = ({
fieldsRef?.[initialField]?.ref.focus()
}, [form.control?.fieldsRef?.current])

// Clear form state when changing views
useEffect(() => {
form.reset()
// we don't want to reset the form on email view
// because we want to pass the email to PasswordlessEmailConfirmation
if (currentView !== EMAIL_VIEW) {
form.reset()
}
}, [currentView])

useEffect(() => {
setPasswordlessLoginEmail(initialEmail)
}, [initialEmail])

useEffect(() => {
// Lets determine if the user has either logged in, or registed.
const loggingIn = currentView === LOGIN_VIEW
Expand Down Expand Up @@ -302,16 +299,20 @@ export const AuthModal = ({
{!form.formState.isSubmitSuccessful && currentView === LOGIN_VIEW && (
<LoginForm
form={form}
submitForm={submitForm}
submitForm={(data) => {
const shouldUsePasswordless =
isPasswordlessEnabled && !data.password
return submitForm(data, shouldUsePasswordless)
}}
clickCreateAccount={() => setCurrentView(REGISTER_VIEW)}
handlePasswordlessLoginClick={() =>
setLoginType(LOGIN_TYPES.PASSWORDLESS)
}
//TODO: potentially remove this prop in the next major release since
// we don't need to use this props anymore
handlePasswordlessLoginClick={noop}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need this prop in the logic but we can't remove it now since it will be a breaking change.

handleForgotPasswordClick={() => setCurrentView(PASSWORD_VIEW)}
isPasswordlessEnabled={isPasswordlessEnabled}
isSocialEnabled={isSocialEnabled}
idps={idps}
setLoginType={setLoginType}
setLoginType={noop}
/>
)}
{!form.formState.isSubmitSuccessful && currentView === REGISTER_VIEW && (
Expand All @@ -332,7 +333,7 @@ export const AuthModal = ({
<PasswordlessEmailConfirmation
form={form}
submitForm={submitForm}
email={passwordlessLoginEmail}
email={form.getValues().email || initialEmail}
/>
)}
</ModalBody>
Expand Down
Loading
Loading