Skip to content

fix: resolve browser extension compatibility issue in ShopifyCustomer…#3576

Closed
frankl1nrg wants to merge 2 commits into
Shopify:mainfrom
frankl1nrg:fix/window-shopify-configurable-check
Closed

fix: resolve browser extension compatibility issue in ShopifyCustomer…#3576
frankl1nrg wants to merge 2 commits into
Shopify:mainfrom
frankl1nrg:fix/window-shopify-configurable-check

Conversation

@frankl1nrg

Copy link
Copy Markdown

WHY are these changes introduced?

Fixes #3575

Browser extensions like Urban VPN define window.Shopify as non-configurable before Hydrogen initializes, causing a TypeError: Cannot redefine property: Shopify crash in production. This affects real users and makes Hydrogen apps unusable when common browser extensions are installed.

WHAT is this pull request doing?

  • Adds defensive property descriptor checks in ShopifyCustomerPrivacy.tsx
  • Implements polling fallback when window.Shopify is already defined as non-configurable
  • Maintains all existing functionality while gracefully handling extension conflicts
  • Ensures Hydrogen apps remain stable regardless of browser extensions

Key changes:

  • Check Object.getOwnPropertyDescriptor(window, "Shopify") before attempting redefinition
  • Use setInterval polling to monitor customerPrivacy when property is non-configurable

HOW to test your changes?

Reproduction test:

  1. Install Urban VPN extension in Chrome
  2. Visit a Hydrogen store (e.g., skeleton.hydrogen.shop)
  3. Before fix: App crashes with TypeError: Cannot redefine property: Shopify
  4. After fix: App loads normally with warning in console

Local testing:

  1. Add this to your app to simulate extension:
Object.defineProperty(window, "Shopify", {
  set(value) { this.__Shopify = value; },
  get() { return this.__Shopify; }
  // configurable defaults to false - simulates extension behavior
});

…Privacy component

Added defensive checks to handle cases where window.Shopify is defined as non-configurable by browser extensions, preventing "Cannot redefine property: Shopify" errors. Implemented polling fallback to ensure proper functionality of Hydrogen apps with extensions.
@frankl1nrg frankl1nrg requested a review from a team as a code owner March 12, 2026 22:35
@frankl1nrg

Copy link
Copy Markdown
Author

I have signed the CLA!

@andguy95 andguy95 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@frankl1nrg The entire fallback code path added in this PR is completely untested. Can you please add tests coverage to this scenario please.

window,
'Shopify',
);
if (shopifyDescriptor && !shopifyDescriptor.configurable) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: the fallback block skips two things the normal path does:

  1. setTrackingConsent override - the normal path (~15 lines below) wraps this to inject the headless storefront config. In the fallback, consumers get the raw CDN version, which means consent calls might silently fail for headless stores.
  2. backendConsentEnabled stub - the normal path (~35 lines below) installs {backendConsentEnabled: true} so the CDN reads the right flag before assigning the full API. The fallback skips this.

Also - the PR description says "App loads normally with warning in console" but there's no console.warn here.

I think the polling callback should apply both overrides when it detects customerPrivacy, and we should add a console.warn at the top of this block.

'Shopify',
);
if (shopifyDescriptor && !shopifyDescriptor.configurable) {
customShopify = window.Shopify || undefined;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: customShopify was already assigned to window.Shopify || undefined on line 341. This is a no-op, I think we can remove it.

Suggested change
customShopify = window.Shopify || undefined;
const pollForCustomerPrivacy = setInterval(() => {

setLoaded.customerPrivacy();
clearInterval(pollForCustomerPrivacy);
}
}, 100);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking: the interval runs indefinitely until customerPrivacy appears or the component unmounts - if the CDN script fails to load for any reason, this runs for the page's entire lifetime. I think we should add a max retry count (e.g. 100 tries = 10 seconds at 100ms).

@@ -340,6 +340,21 @@ export function useCustomerPrivacy(props: CustomerPrivacyApiProps) {
let customShopify: {customerPrivacy: CustomerPrivacy} | undefined | object =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question Re: line 325 - wouldn't window.privacyBanner have the same potential issue. If an extension defines it as non-configurable before Hydrogen initializes, the Object.defineProperty on line 325 might throw too?

}

// monitor for when window.Shopify = {} is first set
Object.defineProperty(window, 'Shopify', {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question / possible improvement?: Instead of pre-checking for the descriptor can we try catch the Object.defineProperty(window, 'Shopify', { /* ...code below */ });

Then in the catch intercept like you did above with Object.defineProperty(window.Shopify, 'customerPrivacy', {...}

Something like:


try {
      // Normal path - intercept window.Shopify via property descriptor
      Object.defineProperty(window, 'Shopify', { ....})
} catch {
      console.warn("[h2] browser extension detected, using compatibility mode")

      // Intercept one level deeper Writing to window.Shopify still works via the extension's setter,
      // so we can still install the backendConsentEnabled stub
      window.Shopify.backendConsentEnabled = true

      attempts = 0
      poll every 100ms:
        if window.Shopify.customerPrivacy exists:
          stop polling
          wrap customerPrivacy.setTrackingConsent with headless config pre-applied
          // window.Shopify.customerPrivacy is a plain property on the inner object,
          // so we can still redefine it with our wrapped version
          Object.defineProperty(window.Shopify, 'customerPrivacy', { wrapped version })
          call setLoaded.customerPrivacy()
        else if attempts > 100:
          stop polling
          console.warn("[h2] customerPrivacy never appeared")

      return cleanup: clear interval
}

@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been marked as stale due to inactivity for 60 days. If no further activity occurs, it will be closed in 7 days.

@danielvonmitschke

Copy link
Copy Markdown

this seems to be still an issue.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Browser Extensions Cause "Cannot redefine property: Shopify" Error

3 participants