Skip to content

Commit 8ed8c86

Browse files
authored
W-19001098 Port over pwless bug fix in v4 (#2810)
* Fix pwless race conditions for all login places
1 parent 0387cb2 commit 8ed8c86

File tree

11 files changed

+161
-163
lines changed

11 files changed

+161
-163
lines changed

jest.config.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,6 @@ module.exports = {
6363
'<rootDir>/src/pages/account/wishlist/partials/wishlist-primary-action.test.js',
6464
'<rootDir>/src/pages/checkout/confirmation.test.js',
6565
'<rootDir>/src/pages/checkout/partials/contact-info.test.js',
66-
// TODO: renable when pwless bug is fixed
67-
// '<rootDir>/src/pages/checkout/partials/contact-info.test.js',
6866
'<rootDir>/src/pages/checkout/partials/login-state.test.js',
6967
'<rootDir>/src/pages/account/orders.test.js',
7068
'<rootDir>/src/pages/account/wishlist/index.test.js',

src/components/login/index.jsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,11 @@ import {noop} from '../../utils/utils'
1717
const LoginForm = ({
1818
submitForm,
1919
handleForgotPasswordClick,
20-
handlePasswordlessLoginClick,
2120
clickCreateAccount = noop,
2221
form,
2322
isPasswordlessEnabled = false,
2423
isSocialEnabled = false,
25-
idps = [],
26-
setLoginType
24+
idps = []
2725
}) => {
2826
return (
2927
<Fragment>
@@ -62,10 +60,8 @@ const LoginForm = ({
6260
<PasswordlessLogin
6361
form={form}
6462
handleForgotPasswordClick={handleForgotPasswordClick}
65-
handlePasswordlessLoginClick={handlePasswordlessLoginClick}
6663
isSocialEnabled={isSocialEnabled}
6764
idps={idps}
68-
setLoginType={setLoginType}
6965
/>
7066
) : (
7167
<StandardLogin
@@ -105,12 +101,10 @@ LoginForm.propTypes = {
105101
submitForm: PropTypes.func,
106102
handleForgotPasswordClick: PropTypes.func,
107103
clickCreateAccount: PropTypes.func,
108-
handlePasswordlessLoginClick: PropTypes.func,
109104
form: PropTypes.object,
110105
isPasswordlessEnabled: PropTypes.bool,
111106
isSocialEnabled: PropTypes.bool,
112-
idps: PropTypes.arrayOf(PropTypes.string),
113-
setLoginType: PropTypes.func
107+
idps: PropTypes.arrayOf(PropTypes.string)
114108
}
115109

116110
export default LoginForm

src/components/passwordless-login/index.jsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,16 @@ import {Button, Separator, Stack, Text} from '@chakra-ui/react'
1212
import LoginFields from '../forms/login-fields'
1313
import StandardLogin from '../standard-login'
1414
import SocialLogin from '../social-login'
15-
import {LOGIN_TYPES} from '../../constants'
1615

1716
const PasswordlessLogin = ({
1817
form,
1918
handleForgotPasswordClick,
20-
handlePasswordlessLoginClick,
2119
isSocialEnabled = false,
22-
idps = [],
23-
setLoginType
20+
idps = []
2421
}) => {
2522
const [showPasswordView, setShowPasswordView] = useState(false)
2623

2724
const handlePasswordButton = async (e) => {
28-
setLoginType(LOGIN_TYPES.PASSWORD)
2925
const isValid = await form.trigger()
3026
// Manually trigger the browser native form validations
3127
const domForm = e.target.closest('form')
@@ -49,7 +45,6 @@ const PasswordlessLogin = ({
4945
<Button
5046
type="submit"
5147
onClick={() => {
52-
handlePasswordlessLoginClick()
5348
form.clearErrors('global')
5449
}}
5550
isLoading={form.formState.isSubmitting}
@@ -99,11 +94,9 @@ const PasswordlessLogin = ({
9994
PasswordlessLogin.propTypes = {
10095
form: PropTypes.object,
10196
handleForgotPasswordClick: PropTypes.func,
102-
handlePasswordlessLoginClick: PropTypes.func,
10397
isSocialEnabled: PropTypes.bool,
10498
idps: PropTypes.arrayOf(PropTypes.string),
105-
hideEmail: PropTypes.bool,
106-
setLoginType: PropTypes.func
99+
hideEmail: PropTypes.bool
107100
}
108101

109102
export default PasswordlessLogin

src/components/standard-login/index.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ const StandardLogin = ({
5555
)}
5656
{hideEmail && (
5757
<Button
58-
onClick={() => setShowPasswordView(false)}
58+
onClick={() => {
59+
form.resetField('password')
60+
setShowPasswordView(false)
61+
}}
5962
borderColor="gray.500"
6063
color="blue.600"
6164
variant="outline"

src/constants.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,6 @@ export const SHOPPER_CONTEXT_SEARCH_PARAMS = {
180180
}
181181
}
182182

183-
// Constants for Login
184-
export const LOGIN_TYPES = {
185-
PASSWORD: 'password',
186-
PASSWORDLESS: 'passwordless',
187-
SOCIAL: 'social'
188-
}
189-
190183
export const PASSWORDLESS_ERROR_MESSAGES = [
191184
/callback_uri doesn't match/i,
192185
/passwordless permissions error/i,

src/hooks/use-auth-modal.js

Lines changed: 64 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import {
2828
API_ERROR_MESSAGE,
2929
CREATE_ACCOUNT_FIRST_ERROR_MESSAGE,
3030
FEATURE_UNAVAILABLE_ERROR_MESSAGE,
31-
LOGIN_TYPES,
3231
PASSWORDLESS_ERROR_MESSAGES,
3332
USER_NOT_FOUND_ERROR
3433
} from '../constants'
@@ -83,8 +82,6 @@ export const AuthModal = ({
8382
const appOrigin = useAppOrigin()
8483
const config = useExtensionConfig()
8584

86-
const [loginType, setLoginType] = useState(LOGIN_TYPES.PASSWORD)
87-
const [passwordlessLoginEmail, setPasswordlessLoginEmail] = useState(initialEmail)
8885
const {getPasswordResetToken} = usePasswordReset()
8986
const authorizePasswordlessLogin = useAuthHelper(AuthHelpers.AuthorizePasswordless)
9087
const passwordlessConfigCallback = config.login?.passwordless?.callbackURI
@@ -101,66 +98,67 @@ export const AuthModal = ({
10198
)
10299
const mergeBasket = useShopperBasketsMutation('mergeBasket')
103100

104-
const submitForm = async (data) => {
101+
const handlePasswordlessLogin = async (email) => {
102+
try {
103+
const redirectPath = window.location.pathname + (window.location.search || '')
104+
await authorizePasswordlessLogin.mutateAsync({
105+
userid: email,
106+
callbackURI: `${callbackURL}?redirectUrl=${redirectPath}`
107+
})
108+
setCurrentView(EMAIL_VIEW)
109+
} catch (error) {
110+
const message = USER_NOT_FOUND_ERROR.test(error.message)
111+
? formatMessage(CREATE_ACCOUNT_FIRST_ERROR_MESSAGE)
112+
: PASSWORDLESS_ERROR_MESSAGES.some((msg) => msg.test(error.message))
113+
? formatMessage(FEATURE_UNAVAILABLE_ERROR_MESSAGE)
114+
: formatMessage(API_ERROR_MESSAGE)
115+
form.setError('global', {type: 'manual', message})
116+
}
117+
}
118+
119+
const submitForm = async (data, isPasswordless = false) => {
105120
form.clearErrors()
106121

107122
const onLoginSuccess = () => {
108123
navigate('/account')
109124
}
110125

111-
const handlePasswordlessLogin = async (email) => {
112-
try {
113-
const redirectPath = window.location.pathname + window.location.search
114-
await authorizePasswordlessLogin.mutateAsync({
115-
userid: email,
116-
callbackURI: `${callbackURL}?redirectUrl=${redirectPath}`
117-
})
118-
setCurrentView(EMAIL_VIEW)
119-
} catch (error) {
120-
const message = USER_NOT_FOUND_ERROR.test(error.message)
121-
? formatMessage(CREATE_ACCOUNT_FIRST_ERROR_MESSAGE)
122-
: PASSWORDLESS_ERROR_MESSAGES.some((msg) => msg.test(error.message))
123-
? formatMessage(FEATURE_UNAVAILABLE_ERROR_MESSAGE)
124-
: formatMessage(API_ERROR_MESSAGE)
125-
form.setError('global', {type: 'manual', message})
126-
}
127-
}
128-
129126
return {
130127
login: async (data) => {
131-
if (loginType === LOGIN_TYPES.PASSWORD) {
132-
try {
133-
await login.mutateAsync({
134-
username: data.email,
135-
password: data.password
128+
if (isPasswordless) {
129+
const email = data.email
130+
await handlePasswordlessLogin(email)
131+
return
132+
}
133+
134+
try {
135+
await login.mutateAsync({
136+
username: data.email,
137+
password: data.password
138+
})
139+
const hasBasketItem = baskets?.baskets?.[0]?.productItems?.length > 0
140+
// we only want to merge basket when the user is logged in as a recurring user
141+
// only recurring users trigger the login mutation, new user triggers register mutation
142+
// this logic needs to stay in this block because this is the only place that tells if a user is a recurring user
143+
// if you change logic here, also change it in login page
144+
const shouldMergeBasket = hasBasketItem && prevAuthType === 'guest'
145+
if (shouldMergeBasket) {
146+
mergeBasket.mutate({
147+
headers: {
148+
// This is not required since the request has no body
149+
// but CommerceAPI throws a '419 - Unsupported Media Type' error if this header is removed.
150+
'Content-Type': 'application/json'
151+
},
152+
parameters: {
153+
createDestinationBasket: true
154+
}
136155
})
137-
const hasBasketItem = baskets?.baskets?.[0]?.productItems?.length > 0
138-
// we only want to merge basket when the user is logged in as a recurring user
139-
// only recurring users trigger the login mutation, new user triggers register mutation
140-
// this logic needs to stay in this block because this is the only place that tells if a user is a recurring user
141-
// if you change logic here, also change it in login page
142-
const shouldMergeBasket = hasBasketItem && prevAuthType === 'guest'
143-
if (shouldMergeBasket) {
144-
mergeBasket.mutate({
145-
headers: {
146-
// This is not required since the request has no body
147-
// but CommerceAPI throws a '419 - Unsupported Media Type' error if this header is removed.
148-
'Content-Type': 'application/json'
149-
},
150-
parameters: {
151-
createDestinationBasket: true
152-
}
153-
})
154-
}
155-
} catch (error) {
156-
const message = /Unauthorized/i.test(error.message)
157-
? formatMessage(LOGIN_ERROR)
158-
: formatMessage(API_ERROR_MESSAGE)
159-
form.setError('global', {type: 'manual', message})
160156
}
161-
} else if (loginType === LOGIN_TYPES.PASSWORDLESS) {
162-
setPasswordlessLoginEmail(data.email)
163-
await handlePasswordlessLogin(data.email)
157+
} catch (error) {
158+
const message = /Unauthorized/i.test(error.message)
159+
? formatMessage(LOGIN_ERROR)
160+
: formatMessage(API_ERROR_MESSAGE)
161+
form.setError('global', {type: 'manual', message})
164162
}
165163
},
166164
register: async (data) => {
@@ -196,15 +194,15 @@ export const AuthModal = ({
196194
}
197195
},
198196
email: async () => {
199-
await handlePasswordlessLogin(passwordlessLoginEmail)
197+
const email = form.getValues().email || initialEmail
198+
await handlePasswordlessLogin(email)
200199
}
201200
}[currentView](data)
202201
}
203202

204203
// Reset form and local state when opening the modal
205204
useEffect(() => {
206205
if (isOpen) {
207-
setLoginType(LOGIN_TYPES.PASSWORD)
208206
setCurrentView(initialView)
209207
form.reset()
210208
}
@@ -223,13 +221,13 @@ export const AuthModal = ({
223221

224222
// Clear form state when changing views
225223
useEffect(() => {
226-
form.reset()
224+
// we don't want to reset the form on email view
225+
// because we want to pass the email to PasswordlessEmailConfirmation
226+
if (currentView !== EMAIL_VIEW) {
227+
form.reset()
228+
}
227229
}, [currentView])
228230

229-
useEffect(() => {
230-
setPasswordlessLoginEmail(initialEmail)
231-
}, [initialEmail])
232-
233231
useEffect(() => {
234232
// Lets determine if the user has either logged in, or registed.
235233
const loggingIn = currentView === LOGIN_VIEW
@@ -302,16 +300,16 @@ export const AuthModal = ({
302300
{!form.formState.isSubmitSuccessful && currentView === LOGIN_VIEW && (
303301
<LoginForm
304302
form={form}
305-
submitForm={submitForm}
303+
submitForm={(data) => {
304+
const shouldUsePasswordless =
305+
isPasswordlessEnabled && !data.password
306+
return submitForm(data, shouldUsePasswordless)
307+
}}
306308
clickCreateAccount={() => setCurrentView(REGISTER_VIEW)}
307-
handlePasswordlessLoginClick={() =>
308-
setLoginType(LOGIN_TYPES.PASSWORDLESS)
309-
}
310309
handleForgotPasswordClick={() => setCurrentView(PASSWORD_VIEW)}
311310
isPasswordlessEnabled={isPasswordlessEnabled}
312311
isSocialEnabled={isSocialEnabled}
313312
idps={idps}
314-
setLoginType={setLoginType}
315313
/>
316314
)}
317315

@@ -336,7 +334,7 @@ export const AuthModal = ({
336334
<PasswordlessEmailConfirmation
337335
form={form}
338336
submitForm={submitForm}
339-
email={passwordlessLoginEmail}
337+
email={form.getValues().email || initialEmail}
340338
/>
341339
)}
342340
</Dialog.Body>

src/hooks/use-auth-modal.test.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,7 @@ test('Renders check email modal on email mode', async () => {
194194
mockUseForm.mockRestore()
195195
})
196196

197-
//TODO: there is a bug in this feature that is being addressed in PR 2758
198-
describe.skip('Passwordless enabled', () => {
197+
describe('Passwordless enabled', () => {
199198
test('Renders passwordless login when enabled', async () => {
200199
const {user} = renderWithProviders(<MockedComponent isPasswordlessEnabled={true} />)
201200

src/pages/checkout/partials/contact-info.jsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ const ContactInfo = ({isSocialEnabled = false, isPasswordlessEnabled = false, id
6060

6161
const [authModalView, setAuthModalView] = useState(PASSWORD_VIEW)
6262
const authModal = useAuthModal(authModalView)
63-
const [isPasswordlessLoginClicked, setIsPasswordlessLoginClicked] = useState(false)
6463
const passwordlessConfigCallback = config.login?.passwordless?.callbackURI
6564
const callbackURL = isAbsoluteURL(passwordlessConfigCallback)
6665
? passwordlessConfigCallback
@@ -87,11 +86,6 @@ const ContactInfo = ({isSocialEnabled = false, isPasswordlessEnabled = false, id
8786

8887
const submitForm = async (data) => {
8988
setError(null)
90-
if (isPasswordlessLoginClicked) {
91-
handlePasswordlessLogin(data.email)
92-
setIsPasswordlessLoginClicked(false)
93-
return
94-
}
9589
try {
9690
if (!data.password) {
9791
await updateCustomerForBasket.mutateAsync({
@@ -146,8 +140,15 @@ const ContactInfo = ({isSocialEnabled = false, isPasswordlessEnabled = false, id
146140
}
147141
}, [showPasswordField])
148142

149-
const onPasswordlessLoginClick = async () => {
150-
setIsPasswordlessLoginClicked(true)
143+
const onPasswordlessLoginClick = async (e) => {
144+
const isValid = await form.trigger('email')
145+
const domForm = e.target.closest('form')
146+
if (isValid && domForm.checkValidity()) {
147+
const email = form.getValues().email
148+
await handlePasswordlessLogin(email)
149+
} else {
150+
domForm.reportValidity()
151+
}
151152
}
152153

153154
return (

0 commit comments

Comments
 (0)