@@ -29,19 +29,22 @@ import {
2929import LoginForm from '@salesforce/retail-react-app/app/components/login'
3030import ResetPasswordForm from '@salesforce/retail-react-app/app/components/reset-password'
3131import RegisterForm from '@salesforce/retail-react-app/app/components/register'
32- import PasswordlessEmailConfirmation from '@salesforce/retail-react-app/app/components/email-confirmation/index '
32+ import OtpAuth from '@salesforce/retail-react-app/app/components/otp-auth '
3333import { noop } from '@salesforce/retail-react-app/app/utils/utils'
3434import {
3535 API_ERROR_MESSAGE ,
3636 FEATURE_UNAVAILABLE_ERROR_MESSAGE ,
37- PASSWORDLESS_ERROR_MESSAGES
37+ PASSWORDLESS_ERROR_MESSAGES ,
38+ INVALID_TOKEN_ERROR ,
39+ INVALID_TOKEN_ERROR_MESSAGE
3840} from '@salesforce/retail-react-app/app/constants'
3941import useNavigation from '@salesforce/retail-react-app/app/hooks/use-navigation'
4042import { usePrevious } from '@salesforce/retail-react-app/app/hooks/use-previous'
4143import { usePasswordReset } from '@salesforce/retail-react-app/app/hooks/use-password-reset'
4244import { isServer } from '@salesforce/retail-react-app/app/utils/utils'
4345import { getConfig } from '@salesforce/pwa-kit-runtime/utils/ssr-config'
44- import { buildAbsoluteUrl } from '@salesforce/retail-react-app/app/utils/url'
46+ import { getEnvBasePath } from '@salesforce/pwa-kit-runtime/utils/ssr-namespace-paths'
47+ import { isAbsoluteURL } from '@salesforce/retail-react-app/app/page-designer/utils'
4548import { useAppOrigin } from '@salesforce/retail-react-app/app/hooks/use-app-origin'
4649
4750export const LOGIN_VIEW = 'login'
@@ -79,6 +82,7 @@ export const AuthModal = ({
7982
8083 const navigate = useNavigation ( )
8184 const [ currentView , setCurrentView ] = useState ( initialView )
85+ const [ isOtpAuthOpen , setIsOtpAuthOpen ] = useState ( false )
8286 const form = useForm ( )
8387 const toast = useToast ( )
8488 const login = useAuthHelper ( AuthHelpers . LoginRegisteredUserB2C )
@@ -87,10 +91,11 @@ export const AuthModal = ({
8791
8892 const { getPasswordResetToken} = usePasswordReset ( )
8993 const authorizePasswordlessLogin = useAuthHelper ( AuthHelpers . AuthorizePasswordless )
90- const passwordlessConfig = getConfig ( ) . app . login ?. passwordless
91- const passwordlessConfigCallback = passwordlessConfig ?. callbackURI
92- const passwordlessMode = passwordlessConfig ?. mode
93- const callbackURL = buildAbsoluteUrl ( appOrigin , passwordlessConfigCallback )
94+ const loginPasswordless = useAuthHelper ( AuthHelpers . LoginPasswordlessUser )
95+ const passwordlessConfigCallback = getConfig ( ) . app . login ?. passwordless ?. callbackURI
96+ const callbackURL = isAbsoluteURL ( passwordlessConfigCallback )
97+ ? passwordlessConfigCallback
98+ : `${ appOrigin } ${ getEnvBasePath ( ) } ${ passwordlessConfigCallback } `
9499
95100 const { data : baskets } = useCustomerBaskets (
96101 { parameters : { customerId} } ,
@@ -100,13 +105,19 @@ export const AuthModal = ({
100105
101106 const handlePasswordlessLogin = async ( email ) => {
102107 try {
108+ // TODO: use proper parameters from the config
103109 const redirectPath = window . location . pathname + ( window . location . search || '' )
104110 await authorizePasswordlessLogin . mutateAsync ( {
105111 userid : email ,
106- mode : passwordlessMode ,
107- ...( callbackURL && { callbackURI : `${ callbackURL } ?redirectUrl=${ redirectPath } ` } )
112+ mode : 'email' ,
113+ locale : 'en-GB' ,
114+ callbackURI : `${ callbackURL } ?redirectUrl=${ redirectPath } `
108115 } )
109- setCurrentView ( EMAIL_VIEW )
116+ // Close AuthModal first, then open OtpAuth modal after a brief delay
117+ onClose ( )
118+ setTimeout ( ( ) => {
119+ setIsOtpAuthOpen ( true )
120+ } , 150 ) // Small delay to allow AuthModal to close first
110121 } catch ( error ) {
111122 const message = PASSWORDLESS_ERROR_MESSAGES . some ( ( msg ) => msg . test ( error . message ) )
112123 ? formatMessage ( FEATURE_UNAVAILABLE_ERROR_MESSAGE )
@@ -115,6 +126,34 @@ export const AuthModal = ({
115126 }
116127 }
117128
129+ const handleMergeBasket = ( ) => {
130+ const hasBasketItem = baskets ?. baskets ?. [ 0 ] ?. productItems ?. length > 0
131+ // we only want to merge basket when the user is logged in as a recurring user
132+ // only recurring users trigger the login mutation, new user triggers register mutation
133+ // this logic needs to stay in this block because this is the only place that tells if a user is a recurring user
134+ // if you change logic here, also change it in login page
135+ const shouldMergeBasket = hasBasketItem && prevAuthType === 'guest'
136+ if ( shouldMergeBasket ) {
137+ try {
138+ mergeBasket . mutate ( {
139+ headers : {
140+ // This is not required since the request has no body
141+ // but CommerceAPI throws a '419 - Unsupported Media Type' error if this header is removed.
142+ 'Content-Type' : 'application/json'
143+ } ,
144+ parameters : {
145+ createDestinationBasket : true
146+ }
147+ } )
148+ } catch ( error ) {
149+ form . setError ( 'global' , {
150+ type : 'manual' ,
151+ message : formatMessage ( API_ERROR_MESSAGE )
152+ } )
153+ }
154+ }
155+ }
156+
118157 const submitForm = async ( data , isPasswordless = false ) => {
119158 form . clearErrors ( )
120159
@@ -135,30 +174,13 @@ export const AuthModal = ({
135174 username : data . email ,
136175 password : data . password
137176 } )
138- const hasBasketItem = baskets ?. baskets ?. [ 0 ] ?. productItems ?. length > 0
139- // we only want to merge basket when the user is logged in as a recurring user
140- // only recurring users trigger the login mutation, new user triggers register mutation
141- // this logic needs to stay in this block because this is the only place that tells if a user is a recurring user
142- // if you change logic here, also change it in login page
143- const shouldMergeBasket = hasBasketItem && prevAuthType === 'guest'
144- if ( shouldMergeBasket ) {
145- mergeBasket . mutate ( {
146- headers : {
147- // This is not required since the request has no body
148- // but CommerceAPI throws a '419 - Unsupported Media Type' error if this header is removed.
149- 'Content-Type' : 'application/json'
150- } ,
151- parameters : {
152- createDestinationBasket : true
153- }
154- } )
155- }
156177 } catch ( error ) {
157178 const message = / U n a u t h o r i z e d / i. test ( error . message )
158179 ? formatMessage ( LOGIN_ERROR )
159180 : formatMessage ( API_ERROR_MESSAGE )
160181 form . setError ( 'global' , { type : 'manual' , message} )
161182 }
183+ handleMergeBasket ( )
162184 } ,
163185 register : async ( data ) => {
164186 try {
@@ -191,14 +213,23 @@ export const AuthModal = ({
191213 : formatMessage ( API_ERROR_MESSAGE )
192214 form . setError ( 'global' , { type : 'manual' , message} )
193215 }
194- } ,
195- email : async ( ) => {
196- const email = form . getValues ( ) . email || initialEmail
197- await handlePasswordlessLogin ( email )
198216 }
199217 } [ currentView ] ( data )
200218 }
201219
220+ const handleOtpVerification = async ( pwdlessLoginToken ) => {
221+ try {
222+ await loginPasswordless . mutateAsync ( { pwdlessLoginToken} )
223+ } catch ( e ) {
224+ const errorData = await e . response ?. json ( )
225+ const message = INVALID_TOKEN_ERROR . test ( errorData . message )
226+ ? formatMessage ( INVALID_TOKEN_ERROR_MESSAGE )
227+ : formatMessage ( API_ERROR_MESSAGE )
228+ form . setError ( 'global' , { type : 'manual' , message} )
229+ }
230+ handleMergeBasket ( )
231+ }
232+
202233 // Reset form and local state when opening the modal
203234 useEffect ( ( ) => {
204235 if ( isOpen ) {
@@ -219,25 +250,23 @@ export const AuthModal = ({
219250 } , [ form . control ?. fieldsRef ?. current ] )
220251
221252 useEffect ( ( ) => {
222- // we don't want to reset the form on email view
223- // because we want to pass the email to PasswordlessEmailConfirmation
224- if ( currentView !== EMAIL_VIEW ) {
225- form . reset ( )
226- }
253+ form . reset ( )
227254 } , [ currentView ] )
228255
229256 useEffect ( ( ) => {
230257 // Lets determine if the user has either logged in, or registed.
231258 const loggingIn = currentView === LOGIN_VIEW
232259 const registering = currentView === REGISTER_VIEW
233- const isNowRegistered = isOpen && isRegistered && ( loggingIn || registering )
260+ const isNowRegistered =
261+ ( isOpen || isOtpAuthOpen ) && isRegistered && ( loggingIn || registering )
234262 // If the customer changed, but it's not because they logged in or registered. Do nothing.
235263 if ( ! isNowRegistered ) {
236264 return
237265 }
238266
239- // We are done with the modal.
267+ // We are done with the modal. Close any modals that are open.
240268 onClose ( )
269+ setIsOtpAuthOpen ( false )
241270
242271 // Show a toast only for those registed users returning to the site.
243272 if ( loggingIn ) {
@@ -275,67 +304,70 @@ export const AuthModal = ({
275304 initialView === PASSWORD_VIEW ? onClose ( ) : setCurrentView ( LOGIN_VIEW )
276305
277306 return (
278- < Modal
279- size = "sm"
280- closeOnOverlayClick = { false }
281- data-testid = "sf-auth-modal"
282- isOpen = { isOpen }
283- onOpen = { onOpen }
284- onClose = { onClose }
285- { ...props }
286- >
287- < ModalOverlay />
288- < ModalContent >
289- < ModalCloseButton
290- aria-label = { formatMessage ( {
291- id : 'auth_modal.button.close.assistive_msg' ,
292- defaultMessage : 'Close login form'
293- } ) }
294- />
295- < ModalBody pb = { 8 } bg = "white" paddingBottom = { 14 } marginTop = { 14 } >
296- { ! form . formState . isSubmitSuccessful && currentView === LOGIN_VIEW && (
297- < LoginForm
298- form = { form }
299- submitForm = { ( data ) => {
300- const shouldUsePasswordless =
301- isPasswordlessEnabled && ! data . password
302- return submitForm ( data , shouldUsePasswordless )
303- } }
304- clickCreateAccount = { ( ) => setCurrentView ( REGISTER_VIEW ) }
305- //TODO: potentially remove this prop in the next major release since
306- // we don't need to use this props anymore
307- handlePasswordlessLoginClick = { noop }
308- handleForgotPasswordClick = { ( ) => setCurrentView ( PASSWORD_VIEW ) }
309- isPasswordlessEnabled = { isPasswordlessEnabled }
310- isSocialEnabled = { isSocialEnabled }
311- idps = { idps }
312- setLoginType = { noop }
313- />
314- ) }
315- { ! form . formState . isSubmitSuccessful && currentView === REGISTER_VIEW && (
316- < RegisterForm
317- form = { form }
318- submitForm = { submitForm }
319- clickSignIn = { onBackToSignInClick }
320- />
321- ) }
322- { currentView === PASSWORD_VIEW && (
323- < ResetPasswordForm
324- form = { form }
325- submitForm = { submitForm }
326- clickSignIn = { onBackToSignInClick }
327- />
328- ) }
329- { currentView === EMAIL_VIEW && (
330- < PasswordlessEmailConfirmation
331- form = { form }
332- submitForm = { submitForm }
333- email = { form . getValues ( ) . email || initialEmail }
334- />
335- ) }
336- </ ModalBody >
337- </ ModalContent >
338- </ Modal >
307+ < >
308+ < Modal
309+ size = "sm"
310+ closeOnOverlayClick = { false }
311+ data-testid = "sf-auth-modal"
312+ isOpen = { isOpen }
313+ onOpen = { onOpen }
314+ onClose = { onClose }
315+ { ...props }
316+ >
317+ < ModalOverlay />
318+ < ModalContent >
319+ < ModalCloseButton
320+ aria-label = { formatMessage ( {
321+ id : 'auth_modal.button.close.assistive_msg' ,
322+ defaultMessage : 'Close login form'
323+ } ) }
324+ />
325+ < ModalBody pb = { 8 } bg = "white" paddingBottom = { 14 } marginTop = { 14 } >
326+ { ! form . formState . isSubmitSuccessful && currentView === LOGIN_VIEW && (
327+ < LoginForm
328+ form = { form }
329+ submitForm = { ( data ) => {
330+ const shouldUsePasswordless =
331+ isPasswordlessEnabled && ! data . password
332+ return submitForm ( data , shouldUsePasswordless )
333+ } }
334+ clickCreateAccount = { ( ) => setCurrentView ( REGISTER_VIEW ) }
335+ //TODO: potentially remove this prop in the next major release since
336+ // we don't need to use this props anymore
337+ handlePasswordlessLoginClick = { noop }
338+ handleForgotPasswordClick = { ( ) => setCurrentView ( PASSWORD_VIEW ) }
339+ isPasswordlessEnabled = { isPasswordlessEnabled }
340+ isSocialEnabled = { isSocialEnabled }
341+ idps = { idps }
342+ setLoginType = { noop }
343+ />
344+ ) }
345+ { ! form . formState . isSubmitSuccessful && currentView === REGISTER_VIEW && (
346+ < RegisterForm
347+ form = { form }
348+ submitForm = { submitForm }
349+ clickSignIn = { onBackToSignInClick }
350+ />
351+ ) }
352+ { currentView === PASSWORD_VIEW && (
353+ < ResetPasswordForm
354+ form = { form }
355+ submitForm = { submitForm }
356+ clickSignIn = { onBackToSignInClick }
357+ />
358+ ) }
359+ </ ModalBody >
360+ </ ModalContent >
361+ </ Modal >
362+ < OtpAuth
363+ isOpen = { isOtpAuthOpen }
364+ onClose = { ( ) => setIsOtpAuthOpen ( false ) }
365+ form = { form }
366+ handleSendEmailOtp = { handlePasswordlessLogin }
367+ handleOtpVerification = { handleOtpVerification }
368+ hideCheckoutAsGuestButton = { true }
369+ />
370+ </ >
339371 )
340372}
341373
0 commit comments