Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
d03c2a3
E2E tests and defaultDnt
jeremy-jung1 Oct 17, 2024
de48df8
linting
jeremy-jung1 Oct 17, 2024
93bce37
Update CHANGELOG.md
jeremy-jung1 Oct 17, 2024
5052c9f
Update tests after feedback
jeremy-jung1 Oct 17, 2024
eb99945
Merge branch 'develop' into W-16649600-browser-e2e-2
jeremy-jung1 Oct 17, 2024
bdba893
Adding back UI changes
jeremy-jung1 Oct 17, 2024
391beb0
Merge branch 'dnt-ui-branch' into W-16649600-browser-e2e-2
jeremy-jung1 Oct 17, 2024
a739823
linting after restoring UI changes
jeremy-jung1 Oct 17, 2024
f0991a5
Merge branch 'dnt-ui-branch' into W-16649600-browser-e2e-2
jeremy-jung1 Oct 17, 2024
4070774
increase bundlesize
jeremy-jung1 Oct 17, 2024
2f1f284
Merge pull request #2083 from SalesforceCommerceCloud/W-16649600-brow…
jeremy-jung1 Oct 21, 2024
c99d40e
Pass in DNT to use-einstein
jeremy-jung1 Oct 31, 2024
5d8d9ff
Add handling for undefined
jeremy-jung1 Oct 31, 2024
b24d407
Fix tests
jeremy-jung1 Oct 31, 2024
3541494
Removing commas that weren't there before
jeremy-jung1 Oct 31, 2024
ac04ed3
Fix tests in SDK
jeremy-jung1 Oct 31, 2024
1d19c06
Update CHANGELOGs
jeremy-jung1 Oct 31, 2024
d4ae45e
Make the very default DNT behavior to be false
jeremy-jung1 Oct 31, 2024
f773a87
More updates
jeremy-jung1 Nov 1, 2024
2a8a151
linting
jeremy-jung1 Nov 1, 2024
ed034cf
Add dntPreference to existing tests
jeremy-jung1 Nov 1, 2024
3083f75
Add comment
jeremy-jung1 Nov 1, 2024
4353b24
Update test
jeremy-jung1 Nov 1, 2024
94dd8bf
Combine getDntPreference into getDnt
jeremy-jung1 Nov 5, 2024
b9d015d
Edit functions
jeremy-jung1 Nov 5, 2024
e876155
Update parameter
jeremy-jung1 Nov 5, 2024
327711e
Linting
jeremy-jung1 Nov 5, 2024
0405161
Fix tests
jeremy-jung1 Nov 5, 2024
34f6416
Update einstein parameter
jeremy-jung1 Nov 5, 2024
3b92826
Fix template tests
jeremy-jung1 Nov 5, 2024
1d5d77a
Update parameters
jeremy-jung1 Nov 5, 2024
14bed3c
More comment
jeremy-jung1 Nov 5, 2024
aedce43
Avoid the word "use" in parameter
jeremy-jung1 Nov 12, 2024
c4c1576
change wording
jeremy-jung1 Nov 13, 2024
02f9600
Simplify JWT usage
jeremy-jung1 Nov 13, 2024
d53b25b
Update return value names
jeremy-jung1 Nov 14, 2024
0a9c7ce
jsdoc
jeremy-jung1 Nov 15, 2024
734f443
Merge pull request #2109 from SalesforceCommerceCloud/W-16571700-alte…
jeremy-jung1 Nov 15, 2024
60eb461
Update wording
jeremy-jung1 Jan 4, 2025
6a2a51d
Update changelog
jeremy-jung1 Jan 4, 2025
3fde987
Merge branch 'develop' into unify-interface-wording
jeremy-jung1 Jan 14, 2025
4409b7f
Merge branch 'develop' into dnt-ui-branch
jeremy-jung1 Jan 14, 2025
4949802
Merge branch 'dnt-ui-branch' into unify-interface-wording
jeremy-jung1 Jan 14, 2025
d5132b2
Apply feedback
jeremy-jung1 Jan 15, 2025
384b0fb
linting
jeremy-jung1 Jan 15, 2025
a2723ae
Merge pull request #2182 from SalesforceCommerceCloud/unify-interface…
jeremy-jung1 Jan 16, 2025
b46e2bf
Merge branch 'develop' into dnt-ui-branch
jeremy-jung1 Jan 21, 2025
806619b
changelogs
jeremy-jung1 Jan 21, 2025
fe1a9a8
Merge branch 'dnt-ui-branch' of github.com:SalesforceCommerceCloud/pw…
jeremy-jung1 Jan 21, 2025
4e5855b
major version bump
jeremy-jung1 Jan 21, 2025
d11cf25
change text
jeremy-jung1 Jan 22, 2025
6b251bf
Merge branch 'develop' into dnt-ui-branch
jeremy-jung1 Jan 22, 2025
24874ac
increase bundlesize
jeremy-jung1 Jan 22, 2025
f75e35e
Merge branch 'dnt-ui-branch' of github.com:SalesforceCommerceCloud/pw…
jeremy-jung1 Jan 22, 2025
4c32314
Fix failing test
jeremy-jung1 Jan 22, 2025
2377443
Revert "change text"
jeremy-jung1 Jan 22, 2025
7cec56d
Merge pull request #2217 from SalesforceCommerceCloud/change-banner-text
jeremy-jung1 Jan 22, 2025
7dc8a0e
Merge branch 'develop' into dnt-ui-branch
jeremy-jung1 Jan 23, 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
101 changes: 101 additions & 0 deletions e2e/tests/dnt.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright (c) 2023, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

const { test, expect } = require("@playwright/test");
const config = require("../config");
const {
generateUserCredentials
} = require("../scripts/utils.js");

const REGISTERED_USER_CREDENTIALS = generateUserCredentials();

const registerUser = async (page) => {
await page.goto(config.RETAIL_APP_HOME + "/registration");

const registrationFormHeading = page.getByText(/Let's get started!/i);
await registrationFormHeading.waitFor();

await page
.locator("input#firstName")
.fill(REGISTERED_USER_CREDENTIALS.firstName);
await page
.locator("input#lastName")
.fill(REGISTERED_USER_CREDENTIALS.lastName);
await page.locator("input#email").fill(REGISTERED_USER_CREDENTIALS.email);
await page
.locator("input#password")
.fill(REGISTERED_USER_CREDENTIALS.password);

await page.getByRole("button", { name: /Create Account/i }).click();

await expect(
page.getByRole("heading", { name: /Account Details/i })
).toBeVisible();

await expect(
page.getByRole("heading", { name: /My Account/i })
).toBeVisible();
}

const checkDntCookie = async (page, expectedValue) => {
var cookies = await page.context().cookies();
var cookieName = 'dw_dnt';
var cookie = cookies.find(cookie => cookie.name === cookieName);
expect(cookie).toBeTruthy();
expect(cookie.value).toBe(expectedValue);
}


test("Shopper can use the consent tracking form", async ({ page }) => {
await page.context().clearCookies();
await page.goto(config.RETAIL_APP_HOME);

const modalSelector = '[aria-label="Close consent tracking form"]'
page.locator(modalSelector).waitFor()
await expect(page.getByText(/Tracking Consent/i)).toBeVisible({timeout: 10000});

// Decline Tracking
const declineButton = page.locator('button:visible', { hasText: 'Decline' });
await expect(declineButton).toBeVisible();
await declineButton.click();

// Intercept einstein request
let apiCallsMade = false;
await page.route('https://api.cquotient.com/v3/activities/aaij-MobileFirst/viewCategory', (route) => {
apiCallsMade = true;
route.continue();
});

// The value of 1 comes from defaultDnt prop in _app-config/index.jsx
checkDntCookie(page, '1')

// Trigger einstein events
await page.click('text=Womens');
// Reloading the page after setting DNT makes the form not appear again
await page.reload()
await expect(page.getByText(/Tracking Consent/i)).toBeHidden();

// Registering after setting DNT persists the preference
await registerUser(page)
checkDntCookie(page, '1')

// Logging out clears the preference
const buttons = await page.getByText(/Log Out/i).elementHandles();
for (const button of buttons) {
if (await button.isVisible()) {
await button.click();
break;
}
}
var cookies = await page.context().cookies();
if (cookies.some(item => item.name === "dw_dnt")) {
throw new Error('dw_dnt still exists in the cookies');
}
await page.reload();
await expect(page.getByText(/Tracking Consent/i)).toBeVisible({timeout: 10000});
expect(apiCallsMade).toBe(false);
});
1 change: 1 addition & 0 deletions packages/commerce-sdk-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

- 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)
- DNT interface improvement [#2203](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2203)

## v3.1.0 (Oct 28, 2024)

Expand Down
32 changes: 22 additions & 10 deletions packages/commerce-sdk-react/src/auth/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,22 @@ const configSLASPrivate = {
...config,
enablePWAKitPrivateClient: true
}
const JWTNotExpired = jwt.sign(
{
exp: Math.floor(Date.now() / 1000) + 1000,
sub: `cc-slas::zzrf_001::scid:xxxxxx::usid:usid`,
isb: `uido:ecom::upn:[email protected]::uidn:firstname lastname::gcid:guestuserid::rcid:rcid::chid:siteId`
},
'secret'
)
const JWTExpired = jwt.sign(
{
exp: Math.floor(Date.now() / 1000) - 1000,
sub: `cc-slas::zzrf_001::scid:xxxxxx::usid:usid`,
isb: `uido:ecom::upn:[email protected]::uidn:firstname lastname::gcid:guestuserid::rcid:rcid::chid:siteId`
},
'secret'
)

const FAKE_SLAS_EXPIRY = DEFAULT_SLAS_REFRESH_TOKEN_REGISTERED_TTL - 1

Expand Down Expand Up @@ -160,8 +176,6 @@ describe('Auth', () => {
})
test('isTokenExpired', () => {
const auth = new Auth(config)
const JWTNotExpired = jwt.sign({exp: Math.floor(Date.now() / 1000) + 1000}, 'secret')
const JWTExpired = jwt.sign({exp: Math.floor(Date.now() / 1000) - 1000}, 'secret')
// @ts-expect-error private method
expect(auth.isTokenExpired(JWTNotExpired)).toBe(false)
// @ts-expect-error private method
Expand Down Expand Up @@ -256,7 +270,6 @@ describe('Auth', () => {
})
test('ready - re-use valid access token', async () => {
const auth = new Auth(config)
const JWTNotExpired = jwt.sign({exp: Math.floor(Date.now() / 1000) + 1000}, 'secret')

const data: StoredAuthData = {
refresh_token_guest: 'refresh_token_guest',
Expand Down Expand Up @@ -338,8 +351,6 @@ describe('Auth', () => {
})
test('ready - use refresh token when access token is expired', async () => {
const auth = new Auth(config)
const JWTNotExpired = jwt.sign({exp: Math.floor(Date.now() / 1000) + 1000}, 'secret')
const JWTExpired = jwt.sign({exp: Math.floor(Date.now() / 1000) - 1000}, 'secret')

// To simulate real-world scenario, let's first test with a good valid token
const data: StoredAuthData = {
Expand Down Expand Up @@ -374,8 +385,6 @@ describe('Auth', () => {

test('ready - use refresh token when access token is expired with slas private client', async () => {
const auth = new Auth(configSLASPrivate)
const JWTNotExpired = jwt.sign({exp: Math.floor(Date.now() / 1000) + 1000}, 'secret')
const JWTExpired = jwt.sign({exp: Math.floor(Date.now() / 1000) - 1000}, 'secret')

// To simulate real-world scenario, let's first test with a good valid token
const data: StoredAuthData = {
Expand Down Expand Up @@ -429,8 +438,6 @@ describe('Auth', () => {
}
})

const JWTExpired = jwt.sign({exp: Math.floor(Date.now() / 1000) - 1000}, 'secret')

// To simulate real-world scenario, let's start with an expired access token
const data: StoredAuthData = {
refresh_token_guest: 'refresh_token_guest',
Expand Down Expand Up @@ -471,7 +478,7 @@ describe('Auth', () => {
// When user has not selected DNT pref
[true, undefined, {dnt: true}],
[false, undefined, {dnt: false}],
[undefined, undefined, {}],
[undefined, undefined, {dnt: false}],
// When user has selected DNT, the dw_dnt cookie sets dnt
[true, '0', {dnt: false}],
[false, '1', {dnt: true}],
Expand All @@ -489,6 +496,11 @@ describe('Auth', () => {
expect.anything(),
expect.objectContaining(expected)
)
const expectedDnt = 'dnt' in expected ? expected.dnt : false
const dntPref = auth.getDnt({
includeDefaults: true
})
expect(dntPref).toBe(expectedDnt)
}
)

Expand Down
66 changes: 41 additions & 25 deletions packages/commerce-sdk-react/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ type AuthDataMap = Record<
callback?: (storage: BaseStorage) => void
}
>
type DntOptions = {
includeDefaults: boolean
}

const isParentTrusted = isOriginTrusted(getParentOrigin())

Expand Down Expand Up @@ -312,9 +315,21 @@ class Auth {
storage.delete(key)
}

getDnt() {
/**
* Return the value of the DNT cookie or undefined if it is not set.
* The DNT cookie being undefined means that there is a necessity to
* get the user's input for consent tracking, but not that there is no
* DNT value to apply to analytics layers. DNT value will default to
* a certain value and this is reflected by effectiveDnt.
*
* If the cookie value is invalid, then it will be deleted in this function.
*
* If includeDefaults is true, then even if the cookie is not defined,
* defaultDnt will be returned, if it exists. If defaultDnt is not defined, then
* the SDK Default will return (false)
*/
getDnt(options?: DntOptions) {
const dntCookieVal = this.get(DNT_COOKIE_NAME)
// Only '1' or '0' are valid, and invalid values, lack of cookie, or value conflict with token must be an undefined DNT
let dntCookieStatus = undefined
const accessToken = this.getAccessToken()
let isInSync = true
Expand All @@ -327,6 +342,23 @@ class Auth {
} else {
dntCookieStatus = Boolean(Number(dntCookieVal))
}

if (options?.includeDefaults) {
const defaultDnt = this.defaultDnt

let effectiveDnt
const dntCookie = dntCookieVal === '1' ? true : dntCookieVal === '0' ? false : undefined
if (dntCookie !== undefined) {
effectiveDnt = dntCookie
} else {
// If the cookie is not set, read the defaultDnt preference.
// If defaultDnt doesn't exist, default to false, following SLAS default for dnt
effectiveDnt = defaultDnt !== undefined ? defaultDnt : false
}

return effectiveDnt
}

return dntCookieStatus
}

Expand Down Expand Up @@ -399,22 +431,6 @@ class Auth {
return validTimeSeconds <= tokenAgeSeconds
}

/**
* Gets the Do-Not-Track (DNT) preference from the `dw_dnt` cookie.
* If user has set their DNT preference, read the cookie, if not, use the default DNT pref. If the default DNT pref has not been set, default to false.
*/
private getDntPreference(dw_dnt: string | undefined, defaultDnt: boolean | undefined) {
let dntPref
// Read `dw_dnt` cookie
const dntCookie = dw_dnt === '1' ? true : dw_dnt === '0' ? false : undefined
dntPref = dntCookie

// If the cookie is not set, read the default DNT preference.
if (dntCookie === undefined) dntPref = defaultDnt !== undefined ? defaultDnt : undefined

return dntPref
}

/**
* Returns the SLAS access token or an empty string if the access token
* is not found in local store or if SFRA wants PWA to trigger refresh token login.
Expand Down Expand Up @@ -561,7 +577,7 @@ class Auth {
}

async refreshAccessToken() {
const dntPref = this.getDntPreference(this.get(DNT_COOKIE_NAME), this.defaultDnt)
const dntPref = this.getDnt({includeDefaults: true})
const refreshTokenRegistered = this.get('refresh_token_registered')
const refreshTokenGuest = this.get('refresh_token_guest')
const refreshToken = refreshTokenRegistered || refreshTokenGuest
Expand All @@ -573,7 +589,7 @@ class Auth {
this.client,
{
refreshToken,
...(dntPref !== undefined && {dnt: dntPref})
dnt: dntPref
},
{
clientSecret: this.clientSecret
Expand Down Expand Up @@ -742,12 +758,12 @@ class Auth {
this.logWarning(SLAS_SECRET_WARNING_MSG)
}
const usid = this.get('usid')
const dntPref = this.getDntPreference(this.get(DNT_COOKIE_NAME), this.defaultDnt)
const dntPref = this.getDnt({includeDefaults: true})
const isGuest = true
const guestPrivateArgs = [
this.client,
{
...(dntPref !== undefined && {dnt: dntPref}),
dnt: dntPref,
...(usid && {usid})
},
{clientSecret: this.clientSecret}
Expand All @@ -756,7 +772,7 @@ class Auth {
this.client,
{
redirectURI: this.redirectURI,
...(dntPref !== undefined && {dnt: dntPref}),
dnt: dntPref,
...(usid && {usid})
}
] as const
Expand Down Expand Up @@ -817,7 +833,7 @@ class Auth {
}
const redirectURI = this.redirectURI
const usid = this.get('usid')
const dntPref = this.getDntPreference(this.get(DNT_COOKIE_NAME), this.defaultDnt)
const dntPref = this.getDnt({includeDefaults: true})
const isGuest = false
const token = await helpers.loginRegisteredUserB2C(
this.client,
Expand All @@ -827,7 +843,7 @@ class Auth {
},
{
redirectURI,
...(dntPref !== undefined && {dnt: dntPref}),
dnt: dntPref,
...(usid && {usid})
}
)
Expand Down
24 changes: 12 additions & 12 deletions packages/commerce-sdk-react/src/hooks/useDNT.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,26 @@ describe('useDNT tests', () => {
expect(mockSetDnt).toHaveBeenCalledWith(true)
})

it('dntStatus should be false if dw_dnt cookie is "1"', () => {
const {dntStatus} = useDNT()
expect(dntStatus).toBe(true)
it('selectedDnt should be false if dw_dnt cookie is "1"', () => {
const {selectedDnt} = useDNT()
expect(selectedDnt).toBe(true)
})

it('dntStatus should be false if dw_dnt cookie is "0"', () => {
it('selectedDnt should be false if dw_dnt cookie is "0"', () => {
mockGetDnt.mockReturnValue(false)
const {dntStatus} = useDNT()
expect(dntStatus).toBe(false)
const {selectedDnt} = useDNT()
expect(selectedDnt).toBe(false)
})

it('dntStatus should be undefined if dw_dnt cookie is not defined', () => {
it('selectedDnt should be undefined if dw_dnt cookie is not defined', () => {
mockGetDnt.mockReturnValueOnce(undefined)
const {dntStatus} = useDNT()
expect(dntStatus).toBeUndefined()
const {selectedDnt} = useDNT()
expect(selectedDnt).toBeUndefined()
})

it('dntStatus should be undefined if dw_dnt cookie is invalid', () => {
it('selectedDnt should be undefined if dw_dnt cookie is invalid', () => {
mockGetDnt.mockReturnValueOnce(undefined)
const {dntStatus} = useDNT()
expect(dntStatus).toBeUndefined()
const {selectedDnt} = useDNT()
expect(selectedDnt).toBeUndefined()
})
})
Loading
Loading