Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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(<GeoGateFallback />);

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();
});
Original file line number Diff line number Diff line change
@@ -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 (
<div className="relative flex min-h-page w-full items-center justify-center overflow-hidden px-4 py-12 tablet:px-6 tablet:py-16">
<div
aria-hidden
className="bg-accent-cabbage-default/15 pointer-events-none absolute left-1/2 top-1/4 size-[20rem] -translate-x-1/2 rounded-full blur-3xl tablet:size-[32rem]"
/>

<FlexCol className="relative w-full max-w-xl items-center gap-4 text-center tablet:gap-5">
<img
src={cloudinaryGivebackPunchyStaring}
alt="Punchy, the daily.dev mascot, looking on hopefully"
loading="lazy"
className="aspect-square h-32 w-auto select-none object-contain tablet:h-36"
/>

<FlexRow className="items-center gap-2 rounded-10 bg-surface-float px-3 py-1.5">
<DailyIcon />
<Typography
tag={TypographyTag.Span}
type={TypographyType.Caption1}
color={TypographyColor.Secondary}
bold
>
Giveback by daily.dev
</Typography>
</FlexRow>

<Typography
bold
tag={TypographyTag.H1}
type={TypographyType.Title2}
className="[text-wrap:balance] tablet:typo-title1 laptop:typo-large-title"
>
Giveback is not available in your country yet
</Typography>

<Typography
tag={TypographyTag.P}
type={TypographyType.Callout}
color={TypographyColor.Secondary}
className="[text-wrap:pretty] tablet:typo-title3"
>
Giveback turns part of our growth budget into donations for causes the
community picks, funded by daily.dev, at no cost to you.
</Typography>

<Typography
tag={TypographyTag.P}
type={TypographyType.Footnote}
color={TypographyColor.Tertiary}
className="[text-wrap:pretty] tablet:typo-callout"
>
It&apos;s in beta and rolling out to more countries soon.
</Typography>
</FlexCol>
</div>
);
};
13 changes: 11 additions & 2 deletions packages/shared/src/features/giveback/components/GivebackPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -131,11 +138,13 @@ export const GivebackPage = (): ReactElement => {
<div className="relative min-h-page w-full">
<GivebackBackground />

{geoBlocked && <GeoGateFallback />}

{/* 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 && (
<FlexCol className="relative gap-6 py-6 tablet:gap-8 tablet:py-8">
<div className={column}>
<GivebackHero onHowItWorks={handleHowItWorks} />
Expand Down Expand Up @@ -198,7 +207,7 @@ export const GivebackPage = (): ReactElement => {
</FlexCol>
)}

{showFunnel && (
{!geoBlocked && showFunnel && (
<GivebackFunnel
selection={selection}
canClose={replayFunnel}
Expand Down
6 changes: 6 additions & 0 deletions packages/shared/src/lib/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,12 @@ export const cloudinaryCharmNotEnoughTags =
export const cloudinaryCharmGiveback =
'https://media.daily.dev/image/upload/s--d1dldAty--/f_auto,q_auto/v1780848838/public/daily.dev%20Charm%20-%20Giveback%20(1)';

// Punchy giving a hopeful stare, shown on the Giveback geo gate (not available
// in your country). Real alpha, so it floats cleanly on the dark surface - no
// `mix-blend-screen` needed.
export const cloudinaryGivebackPunchyStaring =
'https://media.daily.dev/image/upload/s--CJiuHR4P--/f_auto,q_auto/v1784101387/public/Punchy%20-%20Staring';

// Bespoke Patchy assets for the Giveback "Picture with Patchy" reward reveal.
// The selfie is a transparent video played once — a VP9-alpha .webm for most
// browsers and an HEVC-alpha .mov for Safari (the same source-order trick as
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { GeoGateFallback } from '@dailydotdev/shared/src/features/giveback/components/GeoGateFallback';

// Full-page gate shown when Giveback isn't live in the visitor's region
// (driven by `status.enabled === false` on the real GivebackPage).
const meta: Meta<typeof GeoGateFallback> = {
title: 'Features/Giveback/Geo gate',
component: GeoGateFallback,
parameters: {
layout: 'fullscreen',
},
globals: {
theme: 'dark',
},
decorators: [
(Story) => (
<div className="dark min-h-screen bg-background-default text-text-primary">
<Story />
</div>
),
],
};

export default meta;
type Story = StoryObj<typeof GeoGateFallback>;

export const Default: Story = {};
Loading