Skip to content

Feature passwordless social login #2079

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

Merged
merged 74 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
fee6914
Add social login
bredmond-sf Sep 25, 2024
0a93e1b
Merge branch 'develop' of github.com:SalesforceCommerceCloud/pwa-kit …
bredmond-sf Sep 25, 2024
2e4bede
Remove new default.js key
bredmond-sf Sep 25, 2024
a383f52
Tweak test
bredmond-sf Sep 26, 2024
114363e
Merge branch 'feature-passwordless-social-login' of github.com:Salesf…
bredmond-sf Sep 26, 2024
76b9cd0
Fix bordercolor
bredmond-sf Sep 26, 2024
da6ac02
Tweak icon
bredmond-sf Sep 26, 2024
f44e927
Merge pull request #2027 from SalesforceCommerceCloud/W-16544327-soci…
bredmond-sf Sep 26, 2024
cf49a18
Merge branch 'feature-passwordless-social-login' of github.com:Salesf…
bredmond-sf Sep 27, 2024
89f9a30
Merge branch 'develop' of github.com:SalesforceCommerceCloud/pwa-kit …
bredmond-sf Oct 1, 2024
82be946
Merge branch 'develop' of github.com:SalesforceCommerceCloud/pwa-kit …
bredmond-sf Oct 2, 2024
7a2dedd
Merge branch 'develop' of github.com:SalesforceCommerceCloud/pwa-kit …
bredmond-sf Oct 3, 2024
7f01ff7
Add wrappers for social login helpers in `commerce-sdk-react` (#2049)
yunakim714 Oct 9, 2024
59a1ddc
Merge branch 'develop' into feature-passwordless-social-login
hajinsuha1 Oct 15, 2024
34c450c
Merge branch 'develop' into feature-passwordless-social-login
hajinsuha1 Oct 15, 2024
27ec267
Merge branch 'develop' into feature-passwordless-social-login
hajinsuha1 Oct 22, 2024
f116aca
Add wrappers for passwordless login helpers in `commerce-sdk-react` (…
yunakim714 Oct 24, 2024
8a3798e
Social Login Redirect Page (#2068)
yunakim714 Oct 24, 2024
415b673
Merge branch 'develop' into feature-passwordless-social-login
hajinsuha1 Oct 29, 2024
7d80a01
Merge branch 'develop' into feature-passwordless-social-login
hajinsuha1 Oct 31, 2024
ef0fbe2
@W-16617186 Passwordless Login UI Buttons (#2032)
hajinsuha1 Oct 31, 2024
372abcf
@W-16795956 - Implement "Check Email" page (#2110)
yunakim714 Nov 11, 2024
8bc9f69
@W-16909794 - Add passwordless/social login UI buttons to Checkout pa…
yunakim714 Nov 13, 2024
c02cf88
Merge branch 'develop' into feature-passwordless-social-login
hajinsuha1 Nov 21, 2024
888776d
Merge branch 'develop' into feature-passwordless-social-login
yunakim714 Dec 4, 2024
25b3661
@W-16889880 - Complete Social Login Integration: Connect Backend API …
yunakim714 Dec 4, 2024
efe9c14
@W-17263304 Passwordless login with marketing cloud (#2131)
bredmond-sf Dec 30, 2024
1c687fd
@W-17458080 - Use env vars to configure redirect and callback uris fo…
yunakim714 Jan 14, 2025
4a60961
add back button
jeremy-jung1 Jan 16, 2025
306107e
Merge branch 'develop' into feature-passwordless-social-login
jeremy-jung1 Jan 16, 2025
fd37b12
changelog
jeremy-jung1 Jan 16, 2025
3ec8af6
linting
jeremy-jung1 Jan 16, 2025
868bfb3
Add test
jeremy-jung1 Jan 17, 2025
0d5e884
translations
jeremy-jung1 Jan 17, 2025
9c8006a
Merge branch 'develop' into feature-passwordless-social-login
jeremy-jung1 Jan 17, 2025
76664e1
Change text
jeremy-jung1 Jan 17, 2025
a2133ec
Update index.test.js
jeremy-jung1 Jan 17, 2025
9aecbd6
Add condition
jeremy-jung1 Jan 17, 2025
5e2b100
linting
jeremy-jung1 Jan 17, 2025
751e58c
Update spacing
jeremy-jung1 Jan 17, 2025
0c08ccf
Remove changelog for later and spacing
jeremy-jung1 Jan 21, 2025
37e78d1
Account for spacing above and below stack
jeremy-jung1 Jan 21, 2025
c83473d
Merge pull request #2208 from SalesforceCommerceCloud/W-17526051-back…
jeremy-jung1 Jan 21, 2025
fb61a7b
@W-17458039 - Handle error states for social/passwordless login and r…
yunakim714 Jan 22, 2025
1b9aeb5
@W-17271709 Passwordless Login in Checkout (#2178)
hajinsuha1 Jan 22, 2025
b70a97c
@W-17386338 - Secure SSR Endpoints by Verifying SLAS Callback Request…
yunakim714 Jan 23, 2025
86964df
Merge branch 'develop' into feature-passwordless-social-login
yunakim714 Jan 28, 2025
a17d1b1
Merge branch 'develop' into feature-passwordless-social-login
yunakim714 Jan 30, 2025
960d851
@W-17550783 - [Passwordless login] Redirect customer to page prior to…
yunakim714 Feb 3, 2025
d84a358
Merge branch 'develop' into feature-passwordless-social-login
yunakim714 Feb 5, 2025
5a47508
cleanup
yunakim714 Feb 5, 2025
18cd6eb
Merge branch 'feature-passwordless-social-login' of github.com:Salesf…
yunakim714 Feb 5, 2025
1cf11d6
change jose registry to npmjs
yunakim714 Feb 5, 2025
67b6fb5
update changelog
yunakim714 Feb 5, 2025
f025826
update pwa-kit-create-app changelog'
yunakim714 Feb 5, 2025
48da858
fix unit tests
yunakim714 Feb 6, 2025
9b2c5bb
resolve absolute paths issue with babel
yunakim714 Feb 6, 2025
65572b7
fix retail app unit tests
yunakim714 Feb 6, 2025
2d2f5f4
resolve flaky checkout test
yunakim714 Feb 6, 2025
bf0cb44
add 1s timeout
yunakim714 Feb 6, 2025
c41787e
apply babel config to all files in template retail app
yunakim714 Feb 6, 2025
908c20f
cleanup
yunakim714 Feb 6, 2025
445d4ef
lint:
yunakim714 Feb 6, 2025
458f911
fix social login not using private client and refactor applySLASPriva…
hajinsuha1 Feb 6, 2025
ccc8a1f
[Social Login] Remove initial fetch to improve performance (#2243)
yunakim714 Feb 10, 2025
09ef3df
bump commerce sdk isomorphic version
yunakim714 Feb 10, 2025
bf22e96
Merge branch 'develop' into feature-passwordless-social-login
vmarta Feb 10, 2025
72a160e
Fix linting error
vmarta Feb 10, 2025
33b7c88
Fix CI test for Checkout Confirmation
vmarta Feb 11, 2025
e173869
Bump up the bundle size limit
vmarta Feb 11, 2025
f5db134
Update Social Login E2E test (#2248)
yunakim714 Feb 11, 2025
83295ac
[Social Login] Update default value of `applySLASPrivateClientToEndpo…
vmarta Feb 12, 2025
32f9f69
Update CHANGELOG.md
vmarta Feb 12, 2025
4e15295
Merge branch 'develop' into feature-passwordless-social-login
vmarta Feb 12, 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
3 changes: 3 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,9 @@ jobs:

- name: Set Retail App Private Client Home
run: export RETAIL_APP_HOME=https://scaffold-pwa-e2e-pwa-kit-private.mobify-storefront.com/

- name: Set PWA Kit E2E Test User
run: export PWA_E2E_USER_EMAIL=e2e.pwa.kit@gmail.com PWA_E2E_USER_PASSWORD=hpv_pek-JZK_xkz0wzf

- name: Install Playwright Browsers
run: npx playwright install --with-deps
Expand Down
3 changes: 3 additions & 0 deletions e2e/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,7 @@ module.exports = {
"worker",
],
},
PWA_E2E_USER_EMAIL: process.env.PWA_E2E_USER_EMAIL,
PWA_E2E_USER_PASSWORD: process.env.PWA_E2E_USER_PASSWORD,
SOCIAL_LOGIN_RETAIL_APP_HOME: "https://wasatch-mrt-feature-public.mrt-storefront-staging.com"
};
66 changes: 66 additions & 0 deletions e2e/scripts/pageHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,35 @@ export const navigateToPDPDesktop = async ({page}) => {
await productTile.click()
}

/**
* Navigates to the `Cotton Turtleneck Sweater` PDP (Product Detail Page) on Desktop
* with the black variant selected.
*
* @param {Object} options.page - Object that represents a tab/window in the browser provided by playwright
*/
export const navigateToPDPDesktopSocial = async ({page, productName, productColor, productPrice}) => {
await page.goto(config.SOCIAL_LOGIN_RETAIL_APP_HOME)
await answerConsentTrackingForm(page)

await page.getByRole("link", { name: "Womens" }).hover()
const topsNav = await page.getByRole("link", { name: "Tops", exact: true })
await expect(topsNav).toBeVisible()

await topsNav.click()

// PLP
const productTile = page.getByRole("link", {
name: RegExp(productName, 'i'),
})
// selecting swatch
const productTileImg = productTile.locator("img")
await productTileImg.waitFor({state: 'visible'})
await expect(productTile.getByText(RegExp(`From \\${productPrice}`, 'i'))).toBeVisible()

await productTile.getByLabel(RegExp(productColor, 'i'), { exact: true }).hover()
await productTile.click()
}

/**
* Adds the `Cotton Turtleneck Sweater` product to the cart with the variant:
* Color: Black
Expand Down Expand Up @@ -273,6 +302,43 @@ export const loginShopper = async ({page, userCredentials}) => {
}
}

/**
* Attempts to log in a shopper with provided user credentials.
*
* @param {Object} options.page - Object that represents a tab/window in the browser provided by playwright
* @return {Boolean} - denotes whether or not login was successful
*/
export const socialLoginShopper = async ({page}) => {
try {
await page.goto(config.SOCIAL_LOGIN_RETAIL_APP_HOME + "/login")

await page.getByText(/Google/i).click()
await expect(page.getByText(/Sign in with Google/i)).toBeVisible({ timeout: 10000 })
await page.waitForSelector('input[type="email"]')

// Fill in the email input
await page.fill('input[type="email"]', config.PWA_E2E_USER_EMAIL)
await page.click('#identifierNext')

await page.waitForSelector('input[type="password"]')

// Fill in the password input
await page.fill('input[type="password"]', config.PWA_E2E_USER_PASSWORD)
await page.click('#passwordNext')
await page.waitForLoadState()

await expect(page.getByRole("heading", { name: /Account Details/i })).toBeVisible({timeout: 20000})
await expect(page.getByText(/e2e.pwa.kit@gmail.com/i)).toBeVisible()

// Password card should be hidden for social login user
await expect(page.getByRole("heading", { name: /Password/i })).toBeHidden()

return true
} catch {
return false
}
}

/**
* Search for products by query string that takes you to the PLP
*
Expand Down
60 changes: 52 additions & 8 deletions e2e/tests/desktop/registered-shopper.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@
const {test, expect} = require('@playwright/test')
const config = require('../../config')
const {
addProductToCart,
registerShopper,
validateOrderHistory,
validateWishlist,
loginShopper,
navigateToPDPDesktop
} = require('../../scripts/pageHelpers')
addProductToCart,
registerShopper,
validateOrderHistory,
validateWishlist,
loginShopper,
navigateToPDPDesktop,
navigateToPDPDesktopSocial,
socialLoginShopper,
} = require("../../scripts/pageHelpers")
const {generateUserCredentials, getCreditCardExpiry} = require('../../scripts/utils.js')

let registeredUserCredentials = {}

test.beforeAll(async () => {
Expand Down Expand Up @@ -156,3 +157,46 @@ test('Registered shopper can add item to wishlist', async ({page}) => {
// wishlist
await validateWishlist({page})
})

/**
* Test that social login persists a user's shopping cart
* TODO: Fix flaky test
* Skipping this test for now because Google login requires 2FA, which Playwright cannot get past.
*/
test.skip("Registered shopper logged in through social retains persisted cart", async ({ page }) => {
navigateToPDPDesktopSocial({page, productName: "Floral Ruffle Top", productColor: "Cardinal Red Multi", productPrice: "£35.19"})

// Add to Cart
await expect(
page.getByRole("heading", { name: /Floral Ruffle Top/i })
).toBeVisible({timeout: 15000})
await page.getByRole("radio", { name: "L", exact: true }).click()

await page.locator("button[data-testid='quantity-increment']").click()

// Selected Size and Color texts are broken into multiple elements on the page.
// So we need to look at the page URL to verify selected variants
const updatedPageURL = await page.url()
const params = updatedPageURL.split("?")[1]
expect(params).toMatch(/size=9LG/i)
expect(params).toMatch(/color=JJ9DFXX/i)
await page.getByRole("button", { name: /Add to Cart/i }).click()

const addedToCartModal = page.getByText(/2 items added to cart/i)

await addedToCartModal.waitFor()

await page.getByLabel("Close", { exact: true }).click()

// Social Login
await socialLoginShopper({
page
})

// Check Items in Cart
await page.getByLabel(/My cart/i).click()
await page.waitForLoadState()
await expect(
page.getByRole("link", { name: /Floral Ruffle Top/i })
).toBeVisible()
})
1 change: 1 addition & 0 deletions packages/commerce-sdk-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Clear auth state if session has been invalidated by a password change [#2092](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2092)
- DNT interface improvement [#2203](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2203)
- Support Node 22 [#2218](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2218)
- Add `authorizeIDP`, `loginIDPUser`, `authorizePasswordless`, `getPasswordLessAccessToken`, `getPasswordResetToken`, and `resetPassword` wrapper functions to support Social Login, Passwordless Login, and Password Reset [#2079] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2079)

## v3.1.0 (Oct 28, 2024)

Expand Down
24 changes: 12 additions & 12 deletions packages/commerce-sdk-react/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/commerce-sdk-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"version": "node ./scripts/version.js"
},
"dependencies": {
"commerce-sdk-isomorphic": "^3.1.1",
"commerce-sdk-isomorphic": "^3.2.0",
"js-cookie": "^3.0.1",
"jwt-decode": "^4.0.0"
},
Expand Down
92 changes: 88 additions & 4 deletions packages/commerce-sdk-react/src/auth/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ jest.mock('commerce-sdk-isomorphic', () => {
loginGuestUserPrivate: jest.fn().mockResolvedValue(''),
loginRegisteredUserB2C: jest.fn().mockResolvedValue(''),
logout: jest.fn().mockResolvedValue(''),
handleTokenResponse: jest.fn().mockResolvedValue('')
handleTokenResponse: jest.fn().mockResolvedValue(''),
loginIDPUser: jest.fn().mockResolvedValue(''),
authorizeIDP: jest.fn().mockResolvedValue(''),
authorizePasswordless: jest.fn().mockResolvedValue(''),
getPasswordLessAccessToken: jest.fn().mockResolvedValue('')
},
ShopperCustomers: jest.fn().mockImplementation(() => {
return {
Expand All @@ -59,7 +63,8 @@ jest.mock('../utils', () => ({
onClient: () => true,
getParentOrigin: jest.fn().mockResolvedValue(''),
isOriginTrusted: () => false,
getDefaultCookieAttributes: () => {}
getDefaultCookieAttributes: () => {},
isAbsoluteUrl: () => true
}))

/** The auth data we store has a slightly different shape than what we use. */
Expand All @@ -72,7 +77,8 @@ const config = {
siteId: 'siteId',
proxy: 'proxy',
redirectURI: 'redirectURI',
logger: console
logger: console,
passwordlessLoginCallbackURI: 'passwordlessLoginCallbackURI'
}

const configSLASPrivate = {
Expand All @@ -96,10 +102,21 @@ const JWTExpired = jwt.sign(
'secret'
)

const configPasswordlessSms = {
clientId: 'clientId',
organizationId: 'organizationId',
shortCode: 'shortCode',
siteId: 'siteId',
proxy: 'proxy',
redirectURI: 'redirectURI',
logger: console
}

const FAKE_SLAS_EXPIRY = DEFAULT_SLAS_REFRESH_TOKEN_REGISTERED_TTL - 1

const TOKEN_RESPONSE: ShopperLoginTypes.TokenResponse = {
access_token: 'access_token_xyz',
access_token:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjYy1zbGFzOjp6enJmXzAwMTo6c2NpZDpjOWM0NWJmZC0wZWQzLTRhYTIteHh4eC00MGY4ODk2MmI4MzY6OnVzaWQ6YjQ4NjUyMzMtZGU5Mi00MDM5LXh4eHgtYWEyZGZjOGMxZWE1IiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJpc2IiOiJ1aWRvOmVjb206OnVwbjpHdWVzdHx8am9obi5kb2VAZXhhbXBsZS5jb206OnVpZG46Sm9obiBEb2U6OmdjaWQ6Z3Vlc3QtMTIzNDU6OnJjaWQ6cmVnaXN0ZXJlZC02Nzg5MCIsImRudCI6InRlc3QifQ.9yKtUb22ExO-Q4VNQRAyIgTm63l3x5z45Uu1FIQa5dQ',
customer_id: 'customer_id_xyz',
enc_user_id: 'enc_user_id_xyz',
expires_in: 1800,
Expand Down Expand Up @@ -596,6 +613,73 @@ describe('Auth', () => {
clientSecret: SLAS_SECRET_PLACEHOLDER
})
})

test('loginIDPUser calls isomorphic loginIDPUser', async () => {
const auth = new Auth(config)
await auth.loginIDPUser({redirectURI: 'redirectURI', code: 'test'})
expect(helpers.loginIDPUser).toHaveBeenCalled()
const functionArg = (helpers.loginIDPUser as jest.Mock).mock.calls[0][2]
expect(functionArg).toMatchObject({redirectURI: 'redirectURI', code: 'test'})
})

test('loginIDPUser adds clientSecret to parameters when using private client', async () => {
const auth = new Auth(configSLASPrivate)
await auth.loginIDPUser({redirectURI: 'test', code: 'test'})
expect(helpers.loginIDPUser).toHaveBeenCalled()
const functionArg = (helpers.loginIDPUser as jest.Mock).mock.calls[0][1]
expect(functionArg).toMatchObject({
clientSecret: SLAS_SECRET_PLACEHOLDER
})
})

test('authorizeIDP calls isomorphic authorizeIDP', async () => {
const auth = new Auth(config)
await auth.authorizeIDP({redirectURI: 'redirectURI', hint: 'test'})
expect(helpers.authorizeIDP).toHaveBeenCalled()
const functionArg = (helpers.authorizeIDP as jest.Mock).mock.calls[0][1]
expect(functionArg).toMatchObject({redirectURI: 'redirectURI', hint: 'test'})
})

test('authorizeIDP adds clientSecret to parameters when using private client', async () => {
const auth = new Auth(configSLASPrivate)
await auth.authorizeIDP({redirectURI: 'test', hint: 'test'})
expect(helpers.authorizeIDP).toHaveBeenCalled()
const privateClient = (helpers.authorizeIDP as jest.Mock).mock.calls[0][2]
expect(privateClient).toBe(true)
})

test('authorizePasswordless calls isomorphic authorizePasswordless', async () => {
const auth = new Auth(config)
await auth.authorizePasswordless({
callbackURI: 'callbackURI',
userid: 'userid',
mode: 'callback'
})
expect(helpers.authorizePasswordless).toHaveBeenCalled()
const functionArg = (helpers.authorizePasswordless as jest.Mock).mock.calls[0][2]
expect(functionArg).toMatchObject({
callbackURI: 'callbackURI',
userid: 'userid',
mode: 'callback'
})
})

test('authorizePasswordless sets mode to sms as configured', async () => {
const auth = new Auth(configPasswordlessSms)
await auth.authorizePasswordless({userid: 'userid', mode: 'sms'})
expect(helpers.authorizePasswordless).toHaveBeenCalled()
const functionArg = (helpers.authorizePasswordless as jest.Mock).mock.calls[0][2]
expect(functionArg).toMatchObject({userid: 'userid', mode: 'sms'})
})

test('getPasswordLessAccessToken calls isomorphic getPasswordLessAccessToken', async () => {
const auth = new Auth(config)
await auth.getPasswordLessAccessToken({pwdlessLoginToken: '12345678'})
expect(helpers.getPasswordLessAccessToken).toHaveBeenCalled()
const functionArg = (helpers.getPasswordLessAccessToken as jest.Mock).mock.calls[0][2]
expect(functionArg).toMatchObject({pwdlessLoginToken: '12345678'})
})

test('logout as registered user calls isomorphic logout', async () => {
const auth = new Auth(config)

Expand Down
Loading
Loading