Skip to content

Commit

Permalink
@W-17550783 - [Passwordless login] Redirect customer to page prior to…
Browse files Browse the repository at this point in the history
… login (#2221)
  • Loading branch information
yunakim714 authored Feb 3, 2025
1 parent a17d1b1 commit 960d851
Show file tree
Hide file tree
Showing 11 changed files with 291 additions and 28 deletions.
2 changes: 1 addition & 1 deletion packages/commerce-sdk-react/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1128,7 +1128,7 @@ class Auth {
*/
async authorizePasswordless(parameters: AuthorizePasswordlessParams) {
const userid = parameters.userid
const callbackURI = this.passwordlessLoginCallbackURI
const callbackURI = parameters.callbackURI || this.passwordlessLoginCallbackURI
const usid = this.get('usid')
const mode = callbackURI ? 'callback' : 'sms'

Expand Down
13 changes: 12 additions & 1 deletion packages/template-retail-react-app/app/hooks/use-auth-modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import {usePrevious} from '@salesforce/retail-react-app/app/hooks/use-previous'
import {usePasswordReset} from '@salesforce/retail-react-app/app/hooks/use-password-reset'
import {isServer} from '@salesforce/retail-react-app/app/utils/utils'
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
import {isAbsoluteURL} from '@salesforce/retail-react-app/app/page-designer/utils'
import {useAppOrigin} from '@salesforce/retail-react-app/app/hooks/use-app-origin'

export const LOGIN_VIEW = 'login'
export const REGISTER_VIEW = 'register'
Expand Down Expand Up @@ -84,11 +86,16 @@ export const AuthModal = ({
const toast = useToast()
const login = useAuthHelper(AuthHelpers.LoginRegisteredUserB2C)
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
const callbackURL = isAbsoluteURL(passwordlessConfigCallback)
? passwordlessConfigCallback
: `${appOrigin}${passwordlessConfigCallback}`

const {data: baskets} = useCustomerBaskets(
{parameters: {customerId}},
Expand All @@ -105,7 +112,11 @@ export const AuthModal = ({

const handlePasswordlessLogin = async (email) => {
try {
await authorizePasswordlessLogin.mutateAsync({userid: email})
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import Account from '@salesforce/retail-react-app/app/pages/account'
import {rest} from 'msw'
import {mockedRegisteredCustomer} from '@salesforce/retail-react-app/app/mocks/mock-data'
import * as ReactHookForm from 'react-hook-form'
import {AuthHelpers} from '@salesforce/commerce-sdk-react'

jest.setTimeout(60000)

Expand Down Expand Up @@ -47,6 +48,21 @@ const mockRegisteredCustomer = {
login: 'customer@test.com'
}

const mockAuthHelperFunctions = {
[AuthHelpers.AuthorizePasswordless]: {mutateAsync: jest.fn()},
[AuthHelpers.Register]: {mutateAsync: jest.fn()}
}

jest.mock('@salesforce/commerce-sdk-react', () => {
const originalModule = jest.requireActual('@salesforce/commerce-sdk-react')
return {
...originalModule,
useAuthHelper: jest
.fn()
.mockImplementation((helperType) => mockAuthHelperFunctions[helperType])
}
})

let authModal = undefined
const MockedComponent = (props) => {
const {initialView, isPasswordlessEnabled = false} = props
Expand Down Expand Up @@ -155,17 +171,63 @@ test('Renders check email modal on email mode', async () => {
mockUseForm.mockRestore()
})

test('Renders passwordless login when enabled', async () => {
const user = userEvent.setup()
describe('Passwordless enabled', () => {
test('Renders passwordless login when enabled', async () => {
const user = userEvent.setup()

renderWithProviders(<MockedComponent isPasswordlessEnabled={true} />)
renderWithProviders(<MockedComponent isPasswordlessEnabled={true} />)

// open the modal
const trigger = screen.getByText(/open modal/i)
await user.click(trigger)
// open the modal
const trigger = screen.getByText(/open modal/i)
await user.click(trigger)

await waitFor(() => {
expect(screen.getByText(/continue securely/i)).toBeInTheDocument()
await waitFor(() => {
expect(screen.getByText(/continue securely/i)).toBeInTheDocument()
})
})

test('Allows passwordless login', async () => {
const {user} = renderWithProviders(<MockedComponent isPasswordlessEnabled={true} />)
const validEmail = 'test@salesforce.com'

// open the modal
const trigger = screen.getByText(/open modal/i)
await user.click(trigger)

await waitFor(() => {
expect(screen.getByText(/continue securely/i)).toBeInTheDocument()
})

// enter a valid email address
await user.type(screen.getByLabelText('Email'), validEmail)

// initiate passwordless login
const passwordlessLoginButton = screen.getByText(/continue securely/i)
// Click the button twice as the isPasswordlessLoginClicked state doesn't change after the first click
await user.click(passwordlessLoginButton)
await user.click(passwordlessLoginButton)
expect(
mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync
).toHaveBeenCalledWith({
userid: validEmail,
callbackURI: 'https://webhook.site/27761b71-50c1-4097-a600-21a3b89a546c?redirectUrl=/'
})

// check that check email modal is open
await waitFor(() => {
const withinForm = within(screen.getByTestId('sf-form-resend-passwordless-email'))
expect(withinForm.getByText(/Check Your Email/i)).toBeInTheDocument()
expect(withinForm.getByText(validEmail)).toBeInTheDocument()
})

// resend the email
user.click(screen.getByText(/Resend Link/i))
expect(
mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync
).toHaveBeenCalledWith({
userid: validEmail,
callbackURI: 'https://webhook.site/27761b71-50c1-4097-a600-21a3b89a546c?redirectUrl=/'
})
})
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ import {
import useNavigation from '@salesforce/retail-react-app/app/hooks/use-navigation'
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 {isAbsoluteURL} from '@salesforce/retail-react-app/app/page-designer/utils'
import {useAppOrigin} from '@salesforce/retail-react-app/app/hooks/use-app-origin'
import {AuthHelpers, useAuthHelper, useShopperBasketsMutation} from '@salesforce/commerce-sdk-react'
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
import {
API_ERROR_MESSAGE,
FEATURE_UNAVAILABLE_ERROR_MESSAGE,
Expand All @@ -55,6 +58,7 @@ const ContactInfo = ({isSocialEnabled = false, isPasswordlessEnabled = false, id
const navigate = useNavigation()
const {data: customer} = useCurrentCustomer()
const {data: basket} = useCurrentBasket()
const appOrigin = useAppOrigin()
const login = useAuthHelper(AuthHelpers.LoginRegisteredUserB2C)
const logout = useAuthHelper(AuthHelpers.Logout)
const authorizePasswordlessLogin = useAuthHelper(AuthHelpers.AuthorizePasswordless)
Expand All @@ -77,10 +81,18 @@ const ContactInfo = ({isSocialEnabled = false, isPasswordlessEnabled = false, id
const [authModalView, setAuthModalView] = useState(PASSWORD_VIEW)
const authModal = useAuthModal(authModalView)
const [isPasswordlessLoginClicked, setIsPasswordlessLoginClicked] = useState(false)
const passwordlessConfigCallback = getConfig().app.login?.passwordless?.callbackURI
const callbackURL = isAbsoluteURL(passwordlessConfigCallback)
? passwordlessConfigCallback
: `${appOrigin}${passwordlessConfigCallback}`

const handlePasswordlessLogin = async (email) => {
try {
await authorizePasswordlessLogin.mutateAsync({userid: email})
const redirectPath = window.location.pathname + window.location.search
await authorizePasswordlessLogin.mutateAsync({
userid: email,
callbackURI: `${callbackURL}?redirectUrl=${redirectPath}`
})
setAuthModalView(EMAIL_VIEW)
authModal.onOpen()
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ describe('passwordless and social disabled', () => {

describe('passwordless enabled', () => {
let currentBasket = JSON.parse(JSON.stringify(scapiBasketWithItem))

beforeEach(() => {
global.server.use(
rest.put('*/baskets/:basketId/customer', (req, res, ctx) => {
Expand Down Expand Up @@ -147,6 +148,9 @@ describe('passwordless enabled', () => {
})

test('allows passwordless login', async () => {
jest.spyOn(window, 'location', 'get').mockReturnValue({
pathname: '/checkout'
})
const {user} = renderWithProviders(<ContactInfo isPasswordlessEnabled={true} />)

// enter a valid email address
Expand All @@ -159,7 +163,11 @@ describe('passwordless enabled', () => {
await user.click(passwordlessLoginButton)
expect(
mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync
).toHaveBeenCalledWith({userid: validEmail})
).toHaveBeenCalledWith({
userid: validEmail,
callbackURI:
'https://webhook.site/27761b71-50c1-4097-a600-21a3b89a546c?redirectUrl=/checkout'
})

// check that check email modal is open
await waitFor(() => {
Expand All @@ -172,7 +180,11 @@ describe('passwordless enabled', () => {
user.click(screen.getByText(/Resend Link/i))
expect(
mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync
).toHaveBeenCalledWith({userid: validEmail})
).toHaveBeenCalledWith({
userid: validEmail,
callbackURI:
'https://webhook.site/27761b71-50c1-4097-a600-21a3b89a546c?redirectUrl=/checkout'
})
})

test('allows login using password', async () => {
Expand Down
17 changes: 10 additions & 7 deletions packages/template-retail-react-app/app/pages/login/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const Login = ({initialView = LOGIN_VIEW}) => {
const [currentView, setCurrentView] = useState(initialView)
const [passwordlessLoginEmail, setPasswordlessLoginEmail] = useState('')
const [loginType, setLoginType] = useState(LOGIN_TYPES.PASSWORD)
const [redirectPath, setRedirectPath] = useState('')

const handleMergeBasket = () => {
const hasBasketItem = baskets?.baskets?.[0]?.productItems?.length > 0
Expand Down Expand Up @@ -149,7 +150,12 @@ const Login = ({initialView = LOGIN_VIEW}) => {
// customer baskets to be loaded to guarantee proper basket merging.
useEffect(() => {
if (path === PASSWORDLESS_LOGIN_LANDING_PATH && isSuccessCustomerBaskets) {
const token = queryParams.get('token')
const token = decodeURIComponent(queryParams.get('token'))
if (queryParams.get('redirect_url')) {
setRedirectPath(decodeURIComponent(queryParams.get('redirect_url')))
} else {
setRedirectPath('')
}

const passwordlessLogin = async () => {
try {
Expand All @@ -170,13 +176,10 @@ const Login = ({initialView = LOGIN_VIEW}) => {
useEffect(() => {
if (isRegistered) {
handleMergeBasket()
if (location?.state?.directedFrom) {
navigate(location.state.directedFrom)
} else {
navigate('/account')
}
const redirectTo = redirectPath ? redirectPath : '/account'
navigate(redirectTo)
}
}, [isRegistered])
}, [isRegistered, redirectPath])

/**************** Einstein ****************/
useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import Registration from '@salesforce/retail-react-app/app/pages/registration'
import ResetPassword from '@salesforce/retail-react-app/app/pages/reset-password'
import mockConfig from '@salesforce/retail-react-app/config/mocks/default'
import {mockedRegisteredCustomer} from '@salesforce/retail-react-app/app/mocks/mock-data'

const mockMergedBasket = {
basketId: 'a10ff320829cb0eef93ca5310a',
currency: 'USD',
Expand Down Expand Up @@ -97,6 +98,7 @@ describe('Logging in tests', function () {
})
)
})

test('Allows customer to sign in to their account', async () => {
const {user} = renderWithProviders(<MockedComponent />, {
wrapperProps: {
Expand Down
Loading

0 comments on commit 960d851

Please sign in to comment.