{"version":3,"file":"useShopifyCookies.mjs","sources":["../../src/useShopifyCookies.tsx"],"sourcesContent":["import {useEffect, useRef, useState} from 'react';\n// @ts-ignore - worktop/cookie types not properly exported\nimport {stringify} from 'worktop/cookie';\nimport {SHOPIFY_Y, SHOPIFY_S} from './cart-constants.js';\nimport {buildUUID} from './cookies-utils.js';\nimport {\n  getTrackingValues,\n  SHOPIFY_UNIQUE_TOKEN_HEADER,\n  SHOPIFY_VISIT_TOKEN_HEADER,\n} from './tracking-utils.js';\n\nconst longTermLength = 60 * 60 * 24 * 360 * 1; // ~1 year expiry\nconst shortTermLength = 60 * 30; // 30 mins\n\ntype UseShopifyCookiesOptions = CoreShopifyCookiesOptions & {\n  /**\n   * If set to `false`, Shopify cookies will be removed.\n   * If set to `true`, Shopify unique user token cookie will have cookie expiry of 1 year.\n   * Defaults to false.\n   **/\n  hasUserConsent?: boolean;\n  /**\n   * The domain scope of the cookie. Defaults to empty string.\n   **/\n  domain?: string;\n  /**\n   * The checkout domain of the shop. Defaults to empty string. If set, the cookie domain will check if it can be set with the checkout domain.\n   */\n  checkoutDomain?: string;\n  /**\n   * If set to `true`, it skips modifying the deprecated shopify_y and shopify_s cookies.\n   */\n  ignoreDeprecatedCookies?: boolean;\n};\n\n/**\n * Sets the `shopify_y` and `shopify_s` cookies in the browser based on user consent\n * for backward compatibility support.\n *\n * If `fetchTrackingValues` is true, it makes a request to Storefront API\n * to fetch or refresh Shopiy analytics and marketing cookies and tracking values.\n * Generally speaking, this should only be needed if you're not using Hydrogen's\n * built-in analytics components and hooks that already handle this automatically.\n * For example, set it to `true` if you are using `hydrogen-react` only with\n * a different framework and still need to make a same-domain request to\n * Storefront API to set cookies.\n *\n * If `ignoreDeprecatedCookies` is true, it skips setting the deprecated cookies entirely.\n * Useful when you only want to use the newer tracking values and not rely on the deprecated ones.\n *\n * @returns `true` when cookies are set and ready.\n */\nexport function useShopifyCookies(options?: UseShopifyCookiesOptions): boolean {\n  const {\n    hasUserConsent,\n    domain = '',\n    checkoutDomain = '',\n    storefrontAccessToken,\n    fetchTrackingValues,\n    ignoreDeprecatedCookies = false,\n  } = options || {};\n\n  const coreCookiesReady = useCoreShopifyCookies({\n    storefrontAccessToken,\n    fetchTrackingValues,\n    checkoutDomain,\n  });\n\n  useEffect(() => {\n    // Skip setting JS cookies until http-only cookies and server-timing\n    // are ready so that we have values synced in JS and http-only cookies.\n    if (ignoreDeprecatedCookies || !coreCookiesReady) return;\n\n    /**\n     * Setting cookie with domain\n     *\n     * If no domain is provided, the cookie will be set for the current host.\n     * For Shopify, we need to ensure this domain is set with a leading dot.\n     */\n\n    // Use override domain or current host\n    let currentDomain = domain || window.location.host;\n\n    if (checkoutDomain) {\n      const checkoutDomainParts = checkoutDomain.split('.').reverse();\n      const currentDomainParts = currentDomain.split('.').reverse();\n      const sameDomainParts: Array<string> = [];\n      checkoutDomainParts.forEach((part, index) => {\n        if (part === currentDomainParts[index]) {\n          sameDomainParts.push(part);\n        }\n      });\n\n      currentDomain = sameDomainParts.reverse().join('.');\n    }\n\n    // Reset domain if localhost\n    if (/^localhost/.test(currentDomain)) currentDomain = '';\n\n    // Shopify checkout only consumes cookies set with leading dot domain\n    const domainWithLeadingDot = currentDomain\n      ? /^\\./.test(currentDomain)\n        ? currentDomain\n        : `.${currentDomain}`\n      : '';\n\n    /**\n     * Set user and session cookies and refresh the expiry time\n     */\n    if (hasUserConsent) {\n      const trackingValues = getTrackingValues();\n      if (\n        (\n          trackingValues.uniqueToken ||\n          trackingValues.visitToken ||\n          ''\n        ).startsWith('00000000-')\n      ) {\n        // Skip writing cookies when tracking values signal we don't have consent yet\n        return;\n      }\n\n      setCookie(\n        SHOPIFY_Y,\n        trackingValues.uniqueToken || buildUUID(),\n        longTermLength,\n        domainWithLeadingDot,\n      );\n      setCookie(\n        SHOPIFY_S,\n        trackingValues.visitToken || buildUUID(),\n        shortTermLength,\n        domainWithLeadingDot,\n      );\n    } else {\n      setCookie(SHOPIFY_Y, '', 0, domainWithLeadingDot);\n      setCookie(SHOPIFY_S, '', 0, domainWithLeadingDot);\n    }\n  }, [\n    coreCookiesReady,\n    hasUserConsent,\n    domain,\n    checkoutDomain,\n    ignoreDeprecatedCookies,\n  ]);\n\n  return coreCookiesReady;\n}\n\nfunction setCookie(\n  name: string,\n  value: string,\n  maxage: number,\n  domain: string,\n): void {\n  document.cookie = stringify(name, value, {\n    maxage,\n    domain,\n    samesite: 'Lax',\n    path: '/',\n  });\n}\n\nasync function fetchTrackingValuesFromBrowser(\n  storefrontAccessToken?: string,\n  storefrontApiDomain = '',\n): Promise<void> {\n  // These values might come from server-timing or old cookies.\n  // If consent cannot be initially assumed, these tokens\n  // will be dropped in SFAPI and it will return a mock token\n  // starting with '00000000-'.\n  // However, if consent can be assumed initially, these tokens\n  // will be used to create proper cookies and continue our flow.\n  const {uniqueToken, visitToken} = getTrackingValues();\n\n  const response = await fetch(\n    // TODO: update this endpoint when it becomes stable\n    `${storefrontApiDomain.replace(/\\/+$/, '')}/api/unstable/graphql.json`,\n    {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        ...(storefrontAccessToken && {\n          'X-Shopify-Storefront-Access-Token': storefrontAccessToken,\n        }),\n        ...(visitToken || uniqueToken\n          ? {\n              [SHOPIFY_VISIT_TOKEN_HEADER]: visitToken,\n              [SHOPIFY_UNIQUE_TOKEN_HEADER]: uniqueToken,\n            }\n          : undefined),\n      },\n      body: JSON.stringify({\n        query:\n          // This query ensures we get _cmp (consent) server-timing header, which is not available in other queries.\n          // This value can be passed later to consent-tracking-api and privacy-banner scripts to avoid extra requests.\n          'query ensureCookies { consentManagement { cookies(visitorConsent:{}) { cookieDomain } } }',\n      }),\n    },\n  );\n\n  if (!response.ok) {\n    throw new Error(\n      `Failed to fetch consent from browser: ${response.status} ${response.statusText}`,\n    );\n  }\n\n  // Consume the body to complete the request and\n  // ensure server-timing is available in performance API\n  await response.json();\n\n  // Ensure we cache the latest tracking values from resources timing\n  getTrackingValues();\n}\n\ntype CoreShopifyCookiesOptions = {\n  storefrontAccessToken?: string;\n  fetchTrackingValues?: boolean;\n  checkoutDomain?: string;\n};\n\n/**\n * Gets http-only cookies from Storefront API via same-origin fetch request.\n * Falls back to checkout domain if provided to at least obtain the tracking\n * values via server-timing headers.\n */\nfunction useCoreShopifyCookies({\n  checkoutDomain,\n  storefrontAccessToken,\n  fetchTrackingValues = false,\n}: CoreShopifyCookiesOptions) {\n  const [cookiesReady, setCookiesReady] = useState(!fetchTrackingValues);\n  const hasFetchedTrackingValues = useRef(false);\n\n  useEffect(() => {\n    if (!fetchTrackingValues) {\n      // Backend did the work, or proxy is disabled.\n      setCookiesReady(true);\n      return;\n    }\n\n    // React runs effects twice in dev mode, avoid double fetching\n    if (hasFetchedTrackingValues.current) return;\n    hasFetchedTrackingValues.current = true;\n\n    // Fetch consent from browser via proxy\n    fetchTrackingValuesFromBrowser(storefrontAccessToken)\n      .catch((error) =>\n        checkoutDomain\n          ? // Retry with checkout domain if available to at least\n            // get the server-timing values for tracking.\n            fetchTrackingValuesFromBrowser(\n              storefrontAccessToken,\n              checkoutDomain,\n            )\n          : Promise.reject(error),\n      )\n      .catch((error) => {\n        console.warn(\n          '[h2:warn:useShopifyCookies] Failed to fetch tracking values from browser: ' +\n            (error instanceof Error ? error.message : String(error)),\n        );\n      })\n      .finally(() => {\n        // Proceed even on errors, degraded tracking is better than no app\n        setCookiesReady(true);\n      });\n  }, [checkoutDomain, fetchTrackingValues, storefrontAccessToken]);\n\n  return cookiesReady;\n}\n"],"names":[],"mappings":"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"}