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

Feature passwordless social login #2079

Draft
wants to merge 48 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
48 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
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 @@ -302,6 +302,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 [email protected] PWA_E2E_USER_PASSWORD=hpv_pek-JZK_xkz0wzf

- name: Install Playwright Browsers
run: npx playwright install --with-deps
Expand Down
2 changes: 2 additions & 0 deletions e2e/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,6 @@ module.exports = {
"worker",
],
},
PWA_E2E_USER_EMAIL: process.env.PWA_E2E_USER_EMAIL,
PWA_E2E_USER_PASSWORD: process.env.PWA_E2E_USER_PASSWORD
};
65 changes: 65 additions & 0 deletions e2e/scripts/pageHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,34 @@ 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.RETAIL_APP_HOME);

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 @@ -254,6 +282,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.RETAIL_APP_HOME + "/login");

await page.getByRole("button", { name: /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(/[email protected]/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
43 changes: 43 additions & 0 deletions e2e/tests/desktop/registered-shopper.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const {
validateWishlist,
loginShopper,
navigateToPDPDesktop,
navigateToPDPDesktopSocial,
socialLoginShopper,
} = require("../../scripts/pageHelpers");
const {
generateUserCredentials,
Expand Down Expand Up @@ -165,3 +167,44 @@ test("Registered shopper can add item to wishlist", async ({ page }) => {
// wishlist
await validateWishlist({page})
});

/**
* Test that social login persists a user's shopping cart
*/
test("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").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();
})
2 changes: 2 additions & 0 deletions packages/commerce-sdk-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

- Update CacheUpdateMatrix for mergeBasket mutation [#2138](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2092)
- Clear auth state if session has been invalidated by a password change [#2092](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2092)
- Add wrappers for social login helpers: `authorizeIDP` and `loginIDPUser` [#2049](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2049)
- Clear auth state if session has been invalidated by a password change [#2092](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2092)

## v3.1.0 (Oct 28, 2024)

Expand Down
89 changes: 86 additions & 3 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,14 +77,25 @@ const config = {
siteId: 'siteId',
proxy: 'proxy',
redirectURI: 'redirectURI',
logger: console
logger: console,
passwordlessLoginCallbackURI: 'passwordlessLoginCallbackURI'
}

const configSLASPrivate = {
...config,
enablePWAKitPrivateClient: true
}

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 = {
Expand Down Expand Up @@ -584,6 +600,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