diff --git a/client/src/templates/Challenges/components/mobile-app-modal.test.tsx b/client/src/templates/Challenges/components/mobile-app-modal.test.tsx index e0e4fda1f46..6b5680d956f 100644 --- a/client/src/templates/Challenges/components/mobile-app-modal.test.tsx +++ b/client/src/templates/Challenges/components/mobile-app-modal.test.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { renderToString } from 'react-dom/server'; import { render, screen, fireEvent } from '@testing-library/react'; import { describe, @@ -75,6 +76,14 @@ describe('MobileAppModal', () => { expect(screen.getByRole('dialog')).toBeInTheDocument(); }); + it('does not render before mount', () => { + // useEffect does not run during server-side rendering, so the 'mounted' + // flag stays false and the component should produce no output. + expect( + renderToString() + ).toBe(''); + }); + it('does not render on a desktop OS', () => { Object.defineProperty(navigator, 'userAgent', { value: DESKTOP_UA, diff --git a/client/src/templates/Challenges/components/mobile-app-modal.tsx b/client/src/templates/Challenges/components/mobile-app-modal.tsx index 0fcedae3052..7e155dd9861 100644 --- a/client/src/templates/Challenges/components/mobile-app-modal.tsx +++ b/client/src/templates/Challenges/components/mobile-app-modal.tsx @@ -58,6 +58,20 @@ function MobileAppModal({ const os = detectOS(); const [dismissed, setDismissed] = useState(isDismissedFor30Days); + // Defer rendering until after the first browser paint. On a direct page + // load the component hydrates before the browser has computed layout, so + // document.documentElement.clientWidth is 0. The Modal's scroll-lock + // utility calculates the scrollbar compensation as + // window.innerWidth - clientWidth, which produces window.innerWidth when + // clientWidth is 0, and stamps that value as padding-right on , + // breaking the layout. Waiting for useEffect guarantees that layout has + // been computed before the modal (and its scroll-lock) opens. + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + useEffect(() => { if (isProjectPreviewOpen) setDismissed(true); }, [isProjectPreviewOpen]); @@ -73,7 +87,13 @@ function MobileAppModal({ const storeName = os === 'ios' ? t('mobile-app-modal.ios') : t('mobile-app-modal.android'); - if (os === 'other' || !isAvailable || isProjectPreviewOpen || dismissed) + if ( + !mounted || + os === 'other' || + !isAvailable || + isProjectPreviewOpen || + dismissed + ) return null; return (