From e8549d577098d0d21d27d88f5f0c636a95d2217c Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Wed, 15 Jul 2026 11:51:24 +0300 Subject: [PATCH] feat(giveback): gate the page for geo-unavailable regions When Giveback isn't enabled for the visitor's region, replace the whole experience with a single explicit "not available in your country yet" gate that still explains, at a high level, what the campaign is. The FAQ already promised this notice; this delivers it. Wired to the campaign-wide `status.enabled` field, which resolves for anonymous visitors too (unlike per-user `eligible`, which is null until sign-in). Gated only after status resolves so the fallback never flashes before the campaign body while the overview query is in flight. Co-Authored-By: Claude Opus 4.8 --- .../components/GeoGateFallback.spec.tsx | 17 +++++ .../giveback/components/GeoGateFallback.tsx | 75 +++++++++++++++++++ .../giveback/components/GivebackPage.tsx | 13 +++- packages/shared/src/lib/image.ts | 6 ++ .../giveback/GeoGateFallback.stories.tsx | 28 +++++++ 5 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 packages/shared/src/features/giveback/components/GeoGateFallback.spec.tsx create mode 100644 packages/shared/src/features/giveback/components/GeoGateFallback.tsx create mode 100644 packages/storybook/stories/features/giveback/GeoGateFallback.stories.tsx diff --git a/packages/shared/src/features/giveback/components/GeoGateFallback.spec.tsx b/packages/shared/src/features/giveback/components/GeoGateFallback.spec.tsx new file mode 100644 index 0000000000..cb59cd566f --- /dev/null +++ b/packages/shared/src/features/giveback/components/GeoGateFallback.spec.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { GeoGateFallback } from './GeoGateFallback'; + +it('explains that giveback is not available in the visitor region', () => { + render(); + + expect( + screen.getByRole('heading', { + name: 'Giveback is not available in your country yet', + }), + ).toBeInTheDocument(); + expect(screen.getByText('Giveback by daily.dev')).toBeInTheDocument(); + expect( + screen.getByText(/rolling out to more countries soon/i), + ).toBeInTheDocument(); +}); diff --git a/packages/shared/src/features/giveback/components/GeoGateFallback.tsx b/packages/shared/src/features/giveback/components/GeoGateFallback.tsx new file mode 100644 index 0000000000..15270a9344 --- /dev/null +++ b/packages/shared/src/features/giveback/components/GeoGateFallback.tsx @@ -0,0 +1,75 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import { + Typography, + TypographyColor, + TypographyTag, + TypographyType, +} from '../../../components/typography/Typography'; +import { FlexCol, FlexRow } from '../../../components/utilities'; +import { DailyIcon } from '../../../components/icons'; +import { cloudinaryGivebackPunchyStaring } from '../../../lib/image'; + +// Full-page gate. When Giveback isn't enabled for the visitor's region we block +// the whole experience and only explain, at a high level, what the campaign is. +// The gate decision (status.enabled) lives in GivebackPage - this is purely +// presentational so it can be rendered without the campaign queries. +export const GeoGateFallback = (): ReactElement => { + return ( +
+
+ + + Punchy, the daily.dev mascot, looking on hopefully + + + + + Giveback by daily.dev + + + + + Giveback is not available in your country yet + + + + Giveback turns part of our growth budget into donations for causes the + community picks, funded by daily.dev, at no cost to you. + + + + It's in beta and rolling out to more countries soon. + + +
+ ); +}; diff --git a/packages/shared/src/features/giveback/components/GivebackPage.tsx b/packages/shared/src/features/giveback/components/GivebackPage.tsx index 94516877e3..97e2254a5a 100644 --- a/packages/shared/src/features/giveback/components/GivebackPage.tsx +++ b/packages/shared/src/features/giveback/components/GivebackPage.tsx @@ -14,6 +14,7 @@ import { GivebackLeaderboard } from './GivebackLeaderboard'; import { GivebackCausesPanel } from './GivebackCausesPanel'; import { GivebackCausesBreakdown } from './GivebackCausesBreakdown'; import { GivebackFaq } from './GivebackFaq'; +import { GeoGateFallback } from './GeoGateFallback'; import type { GivebackTabId } from './GivebackTabNav'; import { useLogContext } from '../../../contexts/LogContext'; import { LogEvent } from '../../../lib/log'; @@ -42,6 +43,12 @@ export const GivebackPage = (): ReactElement => { const isEligible = status?.eligible === true; const selection = useGivebackCauseSelection(isEligible); + // Geo gate: `enabled` is a campaign-wide field that resolves for everyone + // (including anonymous visitors), so it's the signal for "not live in this + // region". Only gate once status has resolved, otherwise the fallback would + // flash before the campaign body while the overview query is in flight. + const geoBlocked = !!status && !status.enabled; + // Causes are confirmed inside the warm-up funnel; once they save (or the // visitor arrives already onboarded) the tabbed experience takes over. const [completedOnboarding, setCompletedOnboarding] = useState(false); @@ -131,11 +138,13 @@ export const GivebackPage = (): ReactElement => {
+ {geoBlocked && } + {/* Hold the body until we know whether to force the funnel. The funnel is a full-screen overlay on the same background, so revealing the hero/tabs first would flash them on screen before it covers them. While resolving, only the shared background shows, so there's no flash and no shift. */} - {onboardingResolved && !forcedFunnel && ( + {!geoBlocked && onboardingResolved && !forcedFunnel && (
@@ -198,7 +207,7 @@ export const GivebackPage = (): ReactElement => { )} - {showFunnel && ( + {!geoBlocked && showFunnel && ( = { + title: 'Features/Giveback/Geo gate', + component: GeoGateFallback, + parameters: { + layout: 'fullscreen', + }, + globals: { + theme: 'dark', + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {};