Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@W-17550783 - [Passwordless login] Redirect customer to page prior to login #2221

Open
wants to merge 19 commits into
base: feature-passwordless-social-login
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 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
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 @@ -1112,7 +1112,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=/'
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: is there a way to add a unit test that verfies when passwordlessConfigCallback is a relative path it creates the URL correclty?

})
})
})

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
14 changes: 8 additions & 6 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,10 @@ 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')))
}

const passwordlessLogin = async () => {
try {
Expand All @@ -170,11 +174,9 @@ 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)
setRedirectPath('')
Copy link
Collaborator

@alexvuong alexvuong Jan 31, 2025

Choose a reason for hiding this comment

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

I feel like this line here can potentially cause react warning about unsettable state. We are navigating away before the state can be set.
I tried to check out code locally to test out, but I keep running into error with typescript from commerce-sdk-react despite some effects with workarounds.

}
}, [isRegistered])

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
Loading