Skip to content
Open
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
5 changes: 2 additions & 3 deletions packages/shared/src/components/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ import {
briefCardFeedFeature,
briefFeedEntrypointPage,
featureFeedAdTemplate,
featureNewD1Experience,
} from '../lib/featureManagement';
import { useNewD1ExperienceFeature } from '../hooks/useNewD1ExperienceFeature';
import type { AwardProps } from '../graphql/njord';
import { getProductsQueryOptions } from '../graphql/njord';
import { useUpdateQuery } from '../hooks/useUpdateQuery';
Expand Down Expand Up @@ -263,8 +263,7 @@ export default function Feed<T>({
feature: briefCardFeedFeature,
shouldEvaluate: shouldEvaluateBriefCard,
});
const { value: isNewD1Experience } = useConditionalFeature({
feature: featureNewD1Experience,
const { value: isNewD1Experience } = useNewD1ExperienceFeature({
shouldEvaluate: shouldEvaluateBriefCard,
});
const showBriefCard =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ import { useAuthContext } from '../../contexts/AuthContext';
import { webappUrl } from '../../lib/constants';
import { FeedSettingsMenu } from '../feeds/FeedSettings/types';
import { Tooltip } from '../tooltip/Tooltip';
import { useConditionalFeature } from '../../hooks/useConditionalFeature';
import { featureNewD1Experience } from '../../lib/featureManagement';
import { useNewD1ExperienceFeature } from '../../hooks/useNewD1ExperienceFeature';

export const ToggleClickbaitShield = ({
origin,
Expand All @@ -41,8 +40,7 @@ export const ToggleClickbaitShield = ({
const { user } = useAuthContext();
const { maxTries, hasUsedFreeTrial, triesLeft } = useClickbaitTries();
const isClickbaitShieldEnabled = flags?.clickbaitShieldEnabled ?? false;
const { value: isNewD1Experience } = useConditionalFeature({
feature: featureNewD1Experience,
const { value: isNewD1Experience } = useNewD1ExperienceFeature({
shouldEvaluate: !isPlus,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ import { FeedSettingsMenu } from '../../feeds/FeedSettings/types';
import { useAuthContext } from '../../../contexts/AuthContext';
import { webappUrl } from '../../../lib/constants';
import { Tooltip } from '../../tooltip/Tooltip';
import { useConditionalFeature } from '../../../hooks/useConditionalFeature';
import { featureNewD1Experience } from '../../../lib/featureManagement';
import { useNewD1ExperienceFeature } from '../../../hooks/useNewD1ExperienceFeature';

export const ClickbaitShield = ({
post,
Expand All @@ -33,8 +32,7 @@ export const ClickbaitShield = ({
const router = useRouter();
const { user } = useAuthContext();
const { hasUsedFreeTrial, triesLeft } = useClickbaitTries();
const { value: isNewD1Experience } = useConditionalFeature({
feature: featureNewD1Experience,
const { value: isNewD1Experience } = useNewD1ExperienceFeature({
shouldEvaluate: !isPlus,
});

Expand Down
6 changes: 2 additions & 4 deletions packages/shared/src/components/feeds/FeedContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ import {
} from '../../lib/image';
import { useUploadCv } from '../../features/profile/hooks/useUploadCv';
import { TargetId } from '../../lib/log';
import { useConditionalFeature } from '../../hooks/useConditionalFeature';
import { featureNewD1Experience } from '../../lib/featureManagement';
import { useNewD1ExperienceFeature } from '../../hooks/useNewD1ExperienceFeature';

export interface FeedContainerProps {
children: ReactNode;
Expand Down Expand Up @@ -197,8 +196,7 @@ export const FeedContainer = ({
});
const shouldEvaluateBanner =
!!marketingCta && shouldShow && activeFeedName === SharedFeedPage.MyFeed;
const { value: isNewD1Experience } = useConditionalFeature({
feature: featureNewD1Experience,
const { value: isNewD1Experience } = useNewD1ExperienceFeature({
shouldEvaluate: shouldEvaluateBanner,
});
const shouldShowBanner = shouldEvaluateBanner && !isNewD1Experience;
Expand Down
12 changes: 6 additions & 6 deletions packages/shared/src/components/filters/IntroQuestButton.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useSettingsContext } from '../../contexts/SettingsContext';
import { useLazyModal } from '../../hooks/useLazyModal';
import { ActionType } from '../../graphql/actions';
import { useActions, useViewSize, ViewSize } from '../../hooks';
import { useConditionalFeature } from '../../hooks/useConditionalFeature';
import { useNewD1ExperienceFeature } from '../../hooks/useNewD1ExperienceFeature';
import { useQuestDashboard } from '../../hooks/useQuestDashboard';
import { IntroQuestButton } from './IntroQuestButton';
import { LazyModal } from '../modals/common/types';
Expand Down Expand Up @@ -37,8 +37,8 @@ jest.mock('../../hooks', () => ({
useViewSize: jest.fn(),
}));

jest.mock('../../hooks/useConditionalFeature', () => ({
useConditionalFeature: jest.fn(),
jest.mock('../../hooks/useNewD1ExperienceFeature', () => ({
useNewD1ExperienceFeature: jest.fn(),
}));

jest.mock('../../hooks/useQuestDashboard', () => ({
Expand All @@ -60,7 +60,7 @@ const mockUseSettingsContext = useSettingsContext as jest.Mock;
const mockUseLazyModal = useLazyModal as jest.Mock;
const mockUseActions = useActions as jest.Mock;
const mockUseViewSize = useViewSize as jest.Mock;
const mockUseConditionalFeature = useConditionalFeature as jest.Mock;
const mockUseNewD1ExperienceFeature = useNewD1ExperienceFeature as jest.Mock;
const mockUseQuestDashboard = useQuestDashboard as jest.Mock;
const openModal = jest.fn();

Expand Down Expand Up @@ -102,7 +102,7 @@ describe('IntroQuestButton', () => {
checkHasCompleted: jest.fn(() => false),
});
mockUseViewSize.mockImplementation((size) => size === ViewSize.Laptop);
mockUseConditionalFeature.mockReturnValue({ value: true });
mockUseNewD1ExperienceFeature.mockReturnValue({ value: true });
mockUseQuestDashboard.mockReturnValue({
data: {
intro: [
Expand Down Expand Up @@ -218,7 +218,7 @@ describe('IntroQuestButton', () => {
});

it('does not render when new D1 experience flag is off', () => {
mockUseConditionalFeature.mockReturnValue({ value: false });
mockUseNewD1ExperienceFeature.mockReturnValue({ value: false });

render(<IntroQuestButton />);

Expand Down
6 changes: 2 additions & 4 deletions packages/shared/src/components/filters/IntroQuestButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import { ActionType } from '../../graphql/actions';
import { useActions, useViewSize, ViewSize } from '../../hooks';
import { useLazyModal } from '../../hooks/useLazyModal';
import { LazyModal } from '../modals/common/types';
import { useConditionalFeature } from '../../hooks/useConditionalFeature';
import { featureNewD1Experience } from '../../lib/featureManagement';
import { useNewD1ExperienceFeature } from '../../hooks/useNewD1ExperienceFeature';
import { useQuestDashboard } from '../../hooks/useQuestDashboard';
import { QuestStatus } from '../../graphql/quests';

Expand All @@ -25,8 +24,7 @@ export function IntroQuestButton(): ReactElement | null {
const { openModal } = useLazyModal();
const isLaptop = useViewSize(ViewSize.Laptop);
const { checkHasCompleted } = useActions();
const { value: isNewD1Experience } = useConditionalFeature({
feature: featureNewD1Experience,
const { value: isNewD1Experience } = useNewD1ExperienceFeature({
shouldEvaluate: isAuthReady && isLoggedIn && loadedSettings,
});
const { data } = useQuestDashboard();
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/components/layout/common.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ jest.mock('../tooltip/Tooltip', () => ({
},
}));

jest.mock('../../hooks/useNewD1ExperienceFeature', () => ({
useNewD1ExperienceFeature: jest.fn().mockReturnValue({ value: false }),
}));

const mockUseAuthContext = useAuthContext as jest.Mock;
const mockUseLogContext = useLogContext as jest.Mock;
const mockUseActions = useActions as jest.Mock;
Expand Down
6 changes: 2 additions & 4 deletions packages/shared/src/components/layout/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ import {
import { downloadBrowserExtension } from '../../lib/constants';
import { anchorDefaultRel } from '../../lib/strings';
import ConditionalWrapper from '../ConditionalWrapper';
import { useConditionalFeature } from '../../hooks/useConditionalFeature';
import { featureNewD1Experience } from '../../lib/featureManagement';
import { useNewD1ExperienceFeature } from '../../hooks/useNewD1ExperienceFeature';

type State<T> = [T, Dispatch<SetStateAction<T>>];

Expand Down Expand Up @@ -114,8 +113,7 @@ export const SearchControlHeader = ({
isActionsFetched &&
canInstallExtension &&
!hasDismissedInstallExtension;
const { value: isNewD1Experience } = useConditionalFeature({
feature: featureNewD1Experience,
const { value: isNewD1Experience } = useNewD1ExperienceFeature({
shouldEvaluate: shouldEvaluateInstallExtensionPrompt,
});

Expand Down
150 changes: 150 additions & 0 deletions packages/shared/src/hooks/__tests__/useNewD1ExperienceFeature.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { renderHook } from '@testing-library/react';
import { useNewD1ExperienceFeature } from '../useNewD1ExperienceFeature';
import { useConditionalFeature } from '../useConditionalFeature';
import { useQuestDashboard } from '../useQuestDashboard';
import {
QuestRewardType,
QuestStatus,
QuestType,
type UserQuest,
} from '../../graphql/quests';
import { featureNewD1Experience } from '../../lib/featureManagement';

jest.mock('../useConditionalFeature', () => ({
useConditionalFeature: jest.fn(),
}));

jest.mock('../useQuestDashboard', () => ({
useQuestDashboard: jest.fn(),
}));

const mockUseConditionalFeature = useConditionalFeature as jest.Mock;
const mockUseQuestDashboard = useQuestDashboard as jest.Mock;

const buildIntroQuest = (overrides: Partial<UserQuest> = {}): UserQuest => ({
userQuestId: 'uq-1',
rotationId: 'rot-1',
progress: 0,
status: QuestStatus.InProgress,
completedAt: null,
claimedAt: null,
locked: false,
claimable: false,
quest: {
id: 'quest-1',
name: 'Install the browser extension',
description: 'Pin daily.dev.',
type: QuestType.Intro,
eventType: 'extension_install',
targetCount: 1,
},
rewards: [{ type: QuestRewardType.Xp, amount: 10 }],
...overrides,
});

describe('useNewD1ExperienceFeature', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseConditionalFeature.mockReturnValue({
value: true,
isLoading: false,
});
});

it('returns false when intro quests are empty even if GrowthBook resolves true', () => {
mockUseQuestDashboard.mockReturnValue({
data: { intro: [] },
isLoading: false,
});

const { result } = renderHook(() =>
useNewD1ExperienceFeature({ shouldEvaluate: true }),
);

expect(result.current.value).toBe(false);
expect(mockUseConditionalFeature).toHaveBeenCalledWith({
feature: featureNewD1Experience,
shouldEvaluate: false,
});
});

it('returns false while quest dashboard data is unavailable', () => {
mockUseQuestDashboard.mockReturnValue({
data: undefined,
isLoading: true,
});

const { result } = renderHook(() =>
useNewD1ExperienceFeature({ shouldEvaluate: true }),
);

expect(result.current.value).toBe(false);
expect(result.current.isLoading).toBe(true);
});

it('delegates to GrowthBook value when intro quests exist', () => {
mockUseQuestDashboard.mockReturnValue({
data: { intro: [buildIntroQuest()] },
isLoading: false,
});

const { result } = renderHook(() =>
useNewD1ExperienceFeature({ shouldEvaluate: true }),
);

expect(result.current.value).toBe(true);
expect(mockUseConditionalFeature).toHaveBeenCalledWith({
feature: featureNewD1Experience,
shouldEvaluate: true,
});
});

it('returns false when GrowthBook value is false even with intro quests', () => {
mockUseConditionalFeature.mockReturnValue({
value: false,
isLoading: false,
});
mockUseQuestDashboard.mockReturnValue({
data: { intro: [buildIntroQuest()] },
isLoading: false,
});

const { result } = renderHook(() =>
useNewD1ExperienceFeature({ shouldEvaluate: true }),
);

expect(result.current.value).toBe(false);
});

it('returns false without evaluating when shouldEvaluate is false', () => {
mockUseQuestDashboard.mockReturnValue({
data: { intro: [buildIntroQuest()] },
isLoading: false,
});

const { result } = renderHook(() =>
useNewD1ExperienceFeature({ shouldEvaluate: false }),
);

expect(result.current.value).toBe(false);
expect(mockUseConditionalFeature).toHaveBeenCalledWith({
feature: featureNewD1Experience,
shouldEvaluate: false,
});
});

it('treats missing shouldEvaluate as true', () => {
mockUseQuestDashboard.mockReturnValue({
data: { intro: [buildIntroQuest()] },
isLoading: false,
});

const { result } = renderHook(() => useNewD1ExperienceFeature());

expect(result.current.value).toBe(true);
expect(mockUseConditionalFeature).toHaveBeenCalledWith({
feature: featureNewD1Experience,
shouldEvaluate: true,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { useCallback } from 'react';
import { useAuthContext } from '../../contexts/AuthContext';
import { useActions } from '../useActions';
import { ActionType } from '../../graphql/actions';
import { useConditionalFeature } from '../useConditionalFeature';
import { featureNewD1Experience } from '../../lib/featureManagement';
import { useNewD1ExperienceFeature } from '../useNewD1ExperienceFeature';

interface UseProfileCompletionIndicator {
showIndicator: boolean;
Expand All @@ -29,8 +28,7 @@ export const useProfileCompletionIndicator =
const shouldEvaluate =
isActionsFetched && !isDismissed && profileCompletionPercentage < 100;

const { value: isNewD1Experience } = useConditionalFeature({
feature: featureNewD1Experience,
const { value: isNewD1Experience } = useNewD1ExperienceFeature({
shouldEvaluate,
});

Expand Down
29 changes: 29 additions & 0 deletions packages/shared/src/hooks/useNewD1ExperienceFeature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { featureNewD1Experience } from '../lib/featureManagement';
import { useConditionalFeature } from './useConditionalFeature';
import { useQuestDashboard } from './useQuestDashboard';

interface UseNewD1ExperienceFeature {
value: boolean;
isLoading: boolean;
}

export const useNewD1ExperienceFeature = ({
shouldEvaluate = true,
}: {
shouldEvaluate?: boolean;
} = {}): UseNewD1ExperienceFeature => {
const { data, isLoading: isQuestsLoading } = useQuestDashboard({
enabled: shouldEvaluate,
});
const hasIntroQuests = (data?.intro?.length ?? 0) > 0;
const enabled = shouldEvaluate && hasIntroQuests;
const { value, isLoading } = useConditionalFeature({
feature: featureNewD1Experience,
shouldEvaluate: enabled,
});

return {
value: enabled && value,
isLoading: isLoading || isQuestsLoading,
};
};
6 changes: 4 additions & 2 deletions packages/shared/src/hooks/useQuestDashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { QUEST_DASHBOARD_QUERY } from '../graphql/quests';
import { RequestKey, StaleTime, generateQueryKey } from '../lib/query';
import { useRequestProtocol } from './useRequestProtocol';

export const useQuestDashboard = () => {
export const useQuestDashboard = ({
enabled = true,
}: { enabled?: boolean } = {}) => {
const { isLoggedIn, user } = useAuthContext();
const { requestMethod } = useRequestProtocol();

Expand All @@ -18,7 +20,7 @@ export const useQuestDashboard = () => {

return result.questDashboard;
},
enabled: isLoggedIn,
enabled: isLoggedIn && enabled,
staleTime: StaleTime.OneMinute,
retry: false,
});
Expand Down
Loading