Skip to content

ShopifyPrivacyBanner.showBanner()/showPreferences() types declare zero args, but headless stores require storefrontAccessToken (and domains) at runtime #4069

Description

@hendrik244

What is the location of your example repository?

No response

Which package or tool is having this issue?

Hydrogen

What version of that package or tool are you using?

shopify/hydrogen@2026.10.0-preview.3

What version of React Router 7 are you using?

Framework: Next.js 16 App Router, via ShopifyScripts/privacyBanner from @shopify/hydrogen/react (not Remix/Hydrogen's Oxygen runtime)

Steps to Reproduce

  1. Set up a headless Next.js storefront with ShopifyScripts (analytics={{channel: 'headless'}}, consent={{mode: 'default-banner'}}), plus a same-origin SFAPI proxy (handleShopifyRoutes in middleware) per the docs.
  2. Call window.privacyBanner.showBanner() with no arguments, as the published types require.
  3. Observe the banner's bannerQuery request to /api/{version}/graphql.json fail with "Online Store channel is locked" — no x-shopify-storefront-access-token header is ever attached.
  4. Pass {storefrontAccessToken, checkoutRootDomain, storefrontRootDomain, locale, country} into showBanner(...) (requires bypassing the current types with a cast) — the same request now succeeds.

Expected Behavior

  • (a) The types should reflect the actual required options for headless stores, and the docs should call out that headless setups must pass storefrontAccessToken (etc.) into showBanner()/showPreferences(), or
  • (b) showBanner()/showPreferences() should resolve a token automatically for headless setups the same way other ShopifyScripts-driven consent calls do (e.g. via the #shopify-features liquid-token fallback, or by reusing whatever token/config was already given to ShopifyScripts itself), so headless callers don't have to manually re-supply a token the app already gave to ShopifyScripts.

Actual Behavior

dist/globals.d.mts types window.privacyBanner as:

type ShopifyPrivacyBanner = {
  showPreferences: () => Promise<void>;
  showBanner: () => Promise<void>;
};

implying no configuration is needed once ShopifyScripts has bootstrapped the page. But in the compiled storefront-banner.js (https://cdn.shopify.com/shopifycloud/privacy-banner/storefront-banner.js), both methods are thin wrappers around the same internal loader the old loadBanner(options) API used:

n.showBanner = function () {
  return t(this, arguments, void 0, function (n) {
    // ...
    return [4, rr(e(e({}, n), { showPreferences: !1, forceShow: !0 }))];
  });
};

For a headless storefront (window.Shopify.customerPrivacy.config.isHeadless === true, set when ShopifyScripts is given analytics={{channel: 'headless'}}), the internal getServerData(...) call used to fetch the banner's own content explicitly skips the #shopify-features liquid-token fallback that non-headless/non-banner consent calls use:

this.accessToken = null != e ? e : a ? void 0 : this.liquidAccessToken();
// a === isHeadless -> skips liquidAccessToken() entirely, accessToken stays undefined

So unless a storefrontAccessToken (plus checkoutRootDomain/storefrontRootDomain/locale/country) is explicitly passed into showBanner(options)/showPreferences(options), the banner's own bannerQuery request (proxied through the app's same-origin SFAPI proxy, per the documented headless request-handler pattern) goes out with no Storefront Access Token attached at all, and the Storefront API responds with:

{"errors":[{"message":"Online Store channel is locked.","extensions":{"code":"BAD_REQUEST"}}]}

This is confusing for three reasons:

  1. The TypeScript types don't allow passing these options at all — following the types leads directly into this failure.
  2. The error message ("Online Store channel is locked") gives no hint that a missing access token is the actual cause.
  3. Current headless docs (e.g. shopify.dev/docs/storefronts/headless/hydrogen/analytics/consent, and the hydrogen-analytics skill bundled with the package) document showBanner()/showPreferences() as needing no arguments, with no mention of a headless-specific requirement.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions