Skip to content

Commit 264e133

Browse files
frandioxkdaviduik
andauthored
New cookie system (#3309)
* Add new token headers and forward cookie header * Forward IP header and signature for consent geolocation * Make createRequestHandler an official wrapper for Hydrogen independently of Oxygen * Add CrossRuntimeResponse * Add tracking utils to hydrogen-react * Add storefront.fetchConsent utility * Call fetchConsent automatically in handleRequest wrapper * Limit getting consent to full-page rendering * Stop creating deprecated cookies * Add local privacy banner * Make privacy banner update cookies in both domains * Revert "Make privacy banner update cookies in both domains" This reverts commit 46a3c51. * Make privacy banner update cookies in both domains one after another * Read server-timing from fetch requests using performance API * Revert "Make privacy banner update cookies in both domains one after another" This reverts commit 84d5ecd. * Revert "Add local privacy banner" This reverts commit cbac773. * Exclude headers from cache * Add internal proxy to SFAPI * Fix typecheck * Revert "Stop creating deprecated cookies" This reverts commit 151d3d7. * Use new proxy to get consent in same-origin * Revalidate cart.checkoutUrl after consent changes * Cache tracking values from resource entries * Fallback to deprecated cookies when reading tracking values * Add tests for tracking-utils * Update local cache for tests * Refactor getTrackingValuesFromHeader * Replace usage of getShopifyCookies with getTrackingValues * Update packages/hydrogen/src/customer-privacy/ShopifyCustomerPrivacy.tsx Co-authored-by: Kara Daviduik <105449131+kdaviduik@users.noreply.github.com> * Fix missing comma and minor things * Stricter types and string check * Fix reading tracking values from deprecated cookies * Test refactoring * Fix tests * Minor fixes * Support cross-origin tracking values * Signal that sfapi-proxy is enabled to the browser and use it for consent * Use new proxy from frontend-only cart if enabled * Add a way to query cookies from the browser as a fallback * Collect subrequest headers in server response * Remove server-side fetchTrackingValues * Just collect tracking values in the server but don't request them specifically * Upgrade consent-tracking-api to v0.2 * Avoid using private token in proxy * Cleanup * Do not write mock tracking values to cookies * Avoid passing storefront token from server config * Fix tests * Extract utility * Fix CORS requests to proxy. This makes the SFAPI proxy route behave the same as Liquid Storefronts already do today. * Warn about disabled features and add jsdoc * Do not mark analytics ready when using privacy banner until user accepts consent. This prevents analytics events while the banner is still visible * Workaround double event firing bug in PrivacyBanner. This fixes setting analytics ready too early. Also delays PerfKit loading so that it uses correct tracking values * Add comments and use constants * Workaround consent-tracking-api issue to use previously fetched consent * Refactor and expose utility * Fix tests * Move browser request for cookies to useShopifyCookies hook, and call it internally from useCustomerPrivacy hook * Fix existing bug where old cookies were not removed after user declines consent after showing banner a second time via window.privacyBanner.showPreferences() * Make getShopifyCookies work with new tracking values * Rename field * Make e2e port flexible and setup cookie test shell * Support --customer-account-push in e2e * Wrap in try/catch * Small fixes to e2e tests * Add e2e for privacy-banner cookie checks * Read tracking values from non SFAPI requests to same-origin * Support multiple stores in e2e tests * Rework e2e tests to use fixtures * Assert perfkit requests contain proper tokens * Assert checkoutUrl params * Add e2e test for session migration * Remove old specs * Minor fixes * Do not signal tracking is done from the server when missing _cmp * Mock withPrivacyBanner in e2e * consent-tracking test for declined consent by default * Assert checkoutUrl params * PR feedback * Split analytics requests in e2e tests * consent-tracking-accept e2e test * Spin servers per spec instead of per worker * Test migration with default consent enabled * Change assertion for mock values in params * Remove old server-timing from unit test * Fix tests flakiness when running in parallel * Rename cookie test folder * Support production test stores in e2e * Run consent-tracking-accept spec with and without privacy banner * Add e2e test for old-cookies * Add e2e test for consent-tracking-accept in old cookies * Add e2e test for consent change mid-session * Minor fixes to edge cases * Fix tests flakiness * Changesets * Fix package.json e2e test commands * Do not send empty headers * Add comments, rename functions, feedback * Set playwright workers to 1 to avoid issues * Fix JSDoc comments Co-authored-by: Kara Daviduik <105449131+kdaviduik@users.noreply.github.com> * Typo in changeset Co-authored-by: Kara Daviduik <105449131+kdaviduik@users.noreply.github.com> --------- Co-authored-by: Kara Daviduik <105449131+kdaviduik@users.noreply.github.com> Co-authored-by: Kara Daviduik <kara.daviduik@shopify.com>
1 parent 4ebfadc commit 264e133

File tree

62 files changed

+4509
-421
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+4509
-421
lines changed

.changeset/cold-rules-wave.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@shopify/hydrogen': patch
3+
---
4+
5+
This version adds support for the new cookie system in Shopify (`_shopify_analytics` and `_shopify_marketing` http-only cookies). It is backward compatible and still supports the deprecated `_shopify_y` and `_shopify_s` cookies.
6+
7+
- `createRequestHandler` can now be used for every Hydrogen app, not only the ones deployed to Oxygen. It is now exported from `@shopify/hydrogen`.
8+
- A new Storefront API proxy is now available in Hydrogen's `createRequestHandler`. This will be used to obtain http-only cookies from Storefront API. In general, it should be transparent but it can be disabled with the `proxyStandardRoutes` option.
9+
- `Analytics.Provider` component and `useCustomerPrivacy` hook now make a request internally to the mentioned proxy to obtain cookies in the storefront domain.

.changeset/rotten-bobcats-grab.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@shopify/hydrogen-react': patch
3+
---
4+
5+
New export `getTrackingValues` to obtain information for analytics and marketing. Use this instead of `getShopifyCookies` (which is now deprecated).
6+
7+
`useShopifyCookies` now accepts a `fetchTrackingValues` parameter that can be used to make a Storefront API request and obtain Shopify http-only cookies, `_shopify_analytics` and `_shopify_marketing` (which replace the deprecated `_shopify_y` and `_shopify_s` cookies).

.changeset/two-melons-design.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/hydrogen': patch
3+
---
4+
5+
Fixed a number of issues related to irregular behaviors between Privacy Banner and Hydrogen's analytics events.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
SESSION_SECRET="mock-session"
2+
PUBLIC_CHECKOUT_DOMAIN="checkout.hydrogen.shop"
3+
PUBLIC_STORE_DOMAIN="checkout.hydrogen.shop"
4+
PUBLIC_STOREFRONT_API_TOKEN="b97a750a8afa8fe33f2b4012cb3a9f6f"
5+
PUBLIC_STOREFRONT_ID="1000014875"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
SESSION_SECRET="mock-session"
2+
PUBLIC_CHECKOUT_DOMAIN="www.iwantacheapdomainfortesting12345.club"
3+
PUBLIC_STORE_DOMAIN="www.iwantacheapdomainfortesting12345.club"
4+
PUBLIC_STOREFRONT_API_TOKEN="2aac2e4420f32ba0c7dadf55c7cc387b"
5+
PUBLIC_STOREFRONT_ID="1000070232"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
SESSION_SECRET="mock-session"
2+
PUBLIC_CHECKOUT_DOMAIN="www.kara2345.xyz"
3+
PUBLIC_STORE_DOMAIN="www.kara2345.xyz"
4+
PUBLIC_STOREFRONT_API_TOKEN="8eece95833df895900c1b285987c7f40"
5+
PUBLIC_STOREFRONT_ID="1000070242"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
SESSION_SECRET="mock-session"
2+
PUBLIC_CHECKOUT_DOMAIN="checkout.daviduik.com"
3+
PUBLIC_STORE_DOMAIN="checkout.daviduik.com"
4+
PUBLIC_STOREFRONT_API_TOKEN="a79d329fc13657352c6e4734e5d4ca75"
5+
PUBLIC_STOREFRONT_ID="1000061747"

e2e/envs/.env.mockShop

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
SESSION_SECRET="mock-session"
2+
PUBLIC_CHECKOUT_DOMAIN="mock.shop"
3+
PUBLIC_STORE_DOMAIN="mock.shop"
4+
PUBLIC_STOREFRONT_API_TOKEN=""
5+
PUBLIC_STOREFRONT_ID=""

e2e/fixtures/index.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {test as base} from '@playwright/test';
2+
import {DevServer} from './server';
3+
import path from 'node:path';
4+
import {stat} from 'node:fs/promises';
5+
import {StorefrontPage} from './storefront';
6+
7+
export * from '@playwright/test';
8+
export * from './storefront';
9+
10+
export const test = base.extend<
11+
{storefront: StorefrontPage},
12+
{forEachWorker: void}
13+
>({
14+
storefront: async ({page}, use) => {
15+
const storefront = new StorefrontPage(page);
16+
await use(storefront);
17+
},
18+
});
19+
20+
const TEST_STORE_KEYS = [
21+
'mockShop',
22+
'defaultConsentDisallowed_cookiesEnabled',
23+
'defaultConsentAllowed_cookiesEnabled',
24+
'defaultConsentDisallowed_cookiesDisabled',
25+
'defaultConsentAllowed_cookiesDisabled',
26+
] as const;
27+
28+
type TestStoreKey = (typeof TEST_STORE_KEYS)[number];
29+
30+
export const setTestStore = async (
31+
testStore: TestStoreKey | `https://${string}`,
32+
) => {
33+
const isLocal = !testStore.startsWith('https://');
34+
let server: DevServer | null = null;
35+
36+
test.use({
37+
baseURL: async ({}, use) => {
38+
await use(isLocal ? server?.getUrl() : testStore);
39+
},
40+
});
41+
42+
if (!isLocal) {
43+
console.log(`Using test store: ${testStore}`);
44+
return;
45+
}
46+
47+
test.afterAll(async () => {
48+
await server?.stop();
49+
});
50+
51+
test.beforeAll(async ({}) => {
52+
const filepath = path.resolve(__dirname, `../envs/.env.${testStore}`);
53+
await stat(filepath); // Ensure the file exists
54+
55+
server = new DevServer({
56+
storeKey: testStore,
57+
customerAccountPush: false,
58+
envFile: filepath,
59+
});
60+
61+
await server.start();
62+
});
63+
};

e2e/fixtures/server.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import {spawn} from 'node:child_process';
2+
import path from 'node:path';
3+
4+
type DevServerOptions = {
5+
id?: number;
6+
port?: number;
7+
projectPath?: string;
8+
customerAccountPush?: boolean;
9+
envFile?: string;
10+
storeKey?: string;
11+
};
12+
13+
export class DevServer {
14+
process: ReturnType<typeof spawn> | undefined;
15+
port: number;
16+
projectPath: string;
17+
customerAccountPush: boolean;
18+
capturedUrl?: string;
19+
id?: number;
20+
envFile?: string;
21+
storeKey?: string;
22+
23+
constructor(options: DevServerOptions = {}) {
24+
this.id = options.id;
25+
this.storeKey = options.storeKey;
26+
this.port = options.port ?? 3100;
27+
this.projectPath =
28+
options.projectPath ?? path.join(__dirname, '../../templates/skeleton');
29+
this.customerAccountPush = options.customerAccountPush ?? false;
30+
this.envFile = options.envFile;
31+
}
32+
33+
getUrl() {
34+
return this.capturedUrl || `http://localhost:${this.port}`;
35+
}
36+
37+
start() {
38+
if (this.process) {
39+
throw new Error(`Server ${this.id} is already running`);
40+
}
41+
42+
return new Promise((resolve, reject) => {
43+
const args = ['run', 'dev', '--'];
44+
if (this.customerAccountPush) {
45+
args.push('--customer-account-push');
46+
}
47+
48+
if (this.envFile) {
49+
args.push('--env-file', this.envFile);
50+
}
51+
52+
this.process = spawn('npm', args, {
53+
cwd: this.projectPath,
54+
env: {
55+
...process.env,
56+
NODE_ENV: 'development',
57+
SHOPIFY_HYDROGEN_FLAG_PORT: this.port.toString(),
58+
},
59+
stdio: ['pipe', 'pipe', 'pipe'],
60+
});
61+
62+
let started = false;
63+
const timeout = setTimeout(() => {
64+
if (!started) {
65+
this.stop();
66+
reject(new Error(`Server ${this.id} failed to start within timeout`));
67+
}
68+
}, 60000);
69+
70+
let localUrl: string | undefined;
71+
let tunnelUrl: string | undefined;
72+
73+
const handleOutput = (output: string) => {
74+
if (!localUrl) {
75+
localUrl = output.match(/(http:\/\/localhost:\d+)/)?.[1];
76+
}
77+
if (this.customerAccountPush && !tunnelUrl) {
78+
tunnelUrl = output.match(/(https:\/\/[^\s]+)/)?.[1];
79+
}
80+
81+
if (!started && output.includes('success')) {
82+
started = true;
83+
clearTimeout(timeout);
84+
this.capturedUrl = tunnelUrl || localUrl;
85+
const port = this.capturedUrl?.match(/:(\d+)/)?.[1];
86+
if (port) {
87+
this.port = parseInt(port, 10);
88+
}
89+
if (!this.id) {
90+
this.id =
91+
this.port || parseInt((Math.random() * 1000).toFixed(0), 10);
92+
}
93+
console.log(
94+
`[test-server ${this.id}] Server started on ${this.capturedUrl} [${this.storeKey}]`,
95+
);
96+
// Give the tunnel a bit more time to ensure everything is ready
97+
setTimeout(resolve, tunnelUrl ? 5000 : 0);
98+
}
99+
100+
if (
101+
output.includes('log in to Shopify') ||
102+
output.includes('User verification code:')
103+
) {
104+
clearTimeout(timeout);
105+
this.stop();
106+
reject(
107+
new Error(
108+
'Not logged in to Shopify CLI. Run: cd templates/skeleton && npx shopify auth login',
109+
),
110+
);
111+
} else if (
112+
output.includes('Failed to prompt') ||
113+
output.includes('Select a shop to log in')
114+
) {
115+
clearTimeout(timeout);
116+
this.stop();
117+
reject(
118+
new Error(
119+
'Storefront not linked. Run: cd templates/skeleton && npx shopify hydrogen link',
120+
),
121+
);
122+
}
123+
};
124+
125+
if (this.process.stdout) {
126+
this.process.stdout.on('data', (data) => {
127+
const output = data.toString();
128+
// !started && console.log(output);
129+
handleOutput(output);
130+
});
131+
}
132+
133+
if (this.process.stderr) {
134+
this.process.stderr.on('data', (data) => {
135+
const output = data.toString();
136+
// !started && console.log(output);
137+
handleOutput(output);
138+
});
139+
}
140+
141+
this.process.on('error', (error) => {
142+
clearTimeout(timeout);
143+
reject(error);
144+
});
145+
146+
this.process.on('exit', (code) => {
147+
if (!started) {
148+
clearTimeout(timeout);
149+
reject(new Error(`Server ${this.id} exited with code ${code}`));
150+
}
151+
});
152+
});
153+
}
154+
155+
stop() {
156+
return new Promise((resolve) => {
157+
if (!this.process) return resolve(false);
158+
console.log(`[test-server ${this.id}] Stopping server...`);
159+
160+
this.process.on('exit', () => {
161+
this.process = undefined;
162+
resolve(true);
163+
});
164+
165+
this.process.kill('SIGTERM');
166+
167+
setTimeout(() => {
168+
this.process?.kill('SIGKILL');
169+
}, 5000);
170+
});
171+
}
172+
}

0 commit comments

Comments
 (0)