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 (
+
+
+
+
+
+
+
+
+
+ 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 = {};