Skip to content

Commit 2e28088

Browse files
committed
add extension button
1 parent ffa2c67 commit 2e28088

6 files changed

Lines changed: 192 additions & 9 deletions

File tree

packages/shared/src/components/modals/IntroQuestModal.spec.tsx

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { act, render, screen } from '@testing-library/react';
33
import ReactModal from 'react-modal';
44
import { IntroQuestModal } from './IntroQuestModal';
55
import { QUEST_CLAIMED_STAMP_REVEAL_DELAY_MS } from '../quest/QuestCard';
6+
import { useLogContext } from '../../contexts/LogContext';
67
import { ActionType } from '../../graphql/actions';
78
import { useActions, useViewSize } from '../../hooks';
89
import { useQuestDashboard } from '../../hooks/useQuestDashboard';
@@ -13,12 +14,17 @@ import {
1314
QuestType,
1415
type UserQuest,
1516
} from '../../graphql/quests';
17+
import { downloadBrowserExtension } from '../../lib/constants';
18+
import { BrowserName, getCurrentBrowserName } from '../../lib/func';
19+
import { LogEvent, TargetType } from '../../lib/log';
1620

1721
ReactModal.setAppElement('body');
1822

23+
const mockPush = jest.fn();
24+
1925
jest.mock('next/router', () => ({
2026
useRouter: () => ({
21-
push: jest.fn(),
27+
push: mockPush,
2228
}),
2329
}));
2430

@@ -30,6 +36,15 @@ jest.mock('../../hooks/useClaimQuestReward', () => ({
3036
useClaimQuestReward: jest.fn(),
3137
}));
3238

39+
jest.mock('../../contexts/LogContext', () => ({
40+
useLogContext: jest.fn(),
41+
}));
42+
43+
jest.mock('../../lib/func', () => ({
44+
...jest.requireActual('../../lib/func'),
45+
getCurrentBrowserName: jest.fn(),
46+
}));
47+
3348
jest.mock('../tooltip/Tooltip', () => ({
3449
Tooltip: ({ children }: { children: React.ReactNode }) => children,
3550
}));
@@ -46,7 +61,10 @@ const mockUseActions = useActions as jest.Mock;
4661
const mockUseViewSize = useViewSize as jest.Mock;
4762
const mockUseQuestDashboard = useQuestDashboard as jest.Mock;
4863
const mockUseClaimQuestReward = useClaimQuestReward as jest.Mock;
64+
const mockUseLogContext = useLogContext as jest.Mock;
65+
const mockGetCurrentBrowserName = getCurrentBrowserName as jest.Mock;
4966
const completeAction = jest.fn();
67+
const logEvent = jest.fn();
5068

5169
const buildIntroQuest = (overrides: Partial<UserQuest> = {}): UserQuest => ({
5270
userQuestId: 'uq-1',
@@ -75,9 +93,15 @@ const buildIntroQuest = (overrides: Partial<UserQuest> = {}): UserQuest => ({
7593
describe('IntroQuestModal', () => {
7694
beforeEach(() => {
7795
completeAction.mockReset();
96+
logEvent.mockReset();
97+
mockPush.mockReset();
7898
mockUseActions.mockReturnValue({
7999
completeAction,
80100
});
101+
mockUseLogContext.mockReturnValue({
102+
logEvent,
103+
});
104+
mockGetCurrentBrowserName.mockReturnValue(BrowserName.Chrome);
81105
mockUseViewSize.mockReturnValue(false);
82106
mockUseClaimQuestReward.mockReturnValue({
83107
mutate: jest.fn(),
@@ -118,6 +142,10 @@ describe('IntroQuestModal', () => {
118142

119143
render(<IntroQuestModal isOpen onRequestClose={jest.fn()} />);
120144

145+
expect(logEvent).toHaveBeenCalledWith({
146+
event_name: LogEvent.Impression,
147+
target_type: TargetType.IntroQuestModal,
148+
});
121149
expect(completeAction).toHaveBeenCalledWith(ActionType.ViewedIntroQuests);
122150
expect(
123151
screen.getByRole('heading', { name: 'Intro quests', hidden: true }),
@@ -136,6 +164,12 @@ describe('IntroQuestModal', () => {
136164
hidden: true,
137165
}),
138166
).toBeInTheDocument();
167+
expect(
168+
screen.getByRole('button', {
169+
name: 'Go to Chrome Web Store',
170+
hidden: true,
171+
}),
172+
).toBeInTheDocument();
139173
});
140174

141175
it('does not render destination button for completed quests', () => {
@@ -170,6 +204,69 @@ describe('IntroQuestModal', () => {
170204
).not.toBeInTheDocument();
171205
});
172206

207+
it('opens the extension intro quest in the matching browser store', () => {
208+
const onRequestClose = jest.fn();
209+
const openSpy = jest.spyOn(window, 'open').mockImplementation(() => null);
210+
211+
mockGetCurrentBrowserName.mockReturnValue(BrowserName.Edge);
212+
mockUseQuestDashboard.mockReturnValue({
213+
data: {
214+
intro: [
215+
buildIntroQuest({
216+
status: QuestStatus.Completed,
217+
progress: 1,
218+
}),
219+
],
220+
},
221+
isPending: false,
222+
isError: false,
223+
});
224+
225+
render(<IntroQuestModal isOpen onRequestClose={onRequestClose} />);
226+
227+
screen
228+
.getByRole('button', {
229+
name: 'Go to Edge Add-ons',
230+
hidden: true,
231+
})
232+
.click();
233+
234+
expect(openSpy).toHaveBeenCalledWith(
235+
downloadBrowserExtension,
236+
'_blank',
237+
'noopener,noreferrer',
238+
);
239+
expect(onRequestClose).toHaveBeenCalled();
240+
expect(mockPush).not.toHaveBeenCalled();
241+
242+
openSpy.mockRestore();
243+
});
244+
245+
it('does not render the extension destination button for unsupported browsers', () => {
246+
mockGetCurrentBrowserName.mockReturnValue(BrowserName.Safari);
247+
mockUseQuestDashboard.mockReturnValue({
248+
data: {
249+
intro: [
250+
buildIntroQuest({
251+
status: QuestStatus.Completed,
252+
progress: 1,
253+
}),
254+
],
255+
},
256+
isPending: false,
257+
isError: false,
258+
});
259+
260+
render(<IntroQuestModal isOpen onRequestClose={jest.fn()} />);
261+
262+
expect(
263+
screen.queryByRole('button', {
264+
name: /Go to /,
265+
hidden: true,
266+
}),
267+
).not.toBeInTheDocument();
268+
});
269+
173270
it('shows the claim button and claimed stamp for claimable intro quests', async () => {
174271
jest.useFakeTimers();
175272
const mutate = jest.fn((_variables, callbacks) => callbacks?.onSuccess?.());

packages/shared/src/components/modals/IntroQuestModal.tsx

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ import {
2525
} from '../quest/QuestRewardAnimations';
2626
import { ActionType } from '../../graphql/actions';
2727
import type { QuestType } from '../../graphql/quests';
28+
import { useLogContext } from '../../contexts/LogContext';
2829
import { useActions } from '../../hooks';
2930
import { useClaimQuestReward } from '../../hooks/useClaimQuestReward';
3031
import { useQuestDashboard } from '../../hooks/useQuestDashboard';
31-
import { webappUrl } from '../../lib/constants';
32+
import { downloadBrowserExtension, webappUrl } from '../../lib/constants';
33+
import { BrowserName, getCurrentBrowserName } from '../../lib/func';
34+
import { LogEvent, TargetType } from '../../lib/log';
3235

3336
type IntroQuestFlightLayerState = {
3437
claimRotationId: string;
@@ -50,6 +53,28 @@ const introDestinationByEventType: Record<string, QuestDestination> = {
5053
},
5154
};
5255

56+
const getExtensionIntroDestination = (
57+
browserName: BrowserName,
58+
): QuestDestination | null => {
59+
switch (browserName) {
60+
case BrowserName.Edge:
61+
return {
62+
label: 'Edge Add-ons',
63+
href: downloadBrowserExtension,
64+
openInNewTab: true,
65+
};
66+
case BrowserName.Chrome:
67+
case BrowserName.Brave:
68+
return {
69+
label: 'Chrome Web Store',
70+
href: downloadBrowserExtension,
71+
openInNewTab: true,
72+
};
73+
default:
74+
return null;
75+
}
76+
};
77+
5378
const padStep = (index: number): string =>
5479
`Step ${(index + 1).toString().padStart(2, '0')}`;
5580

@@ -58,6 +83,8 @@ export const IntroQuestModal = ({
5883
...props
5984
}: ModalProps): ReactElement => {
6085
const router = useRouter();
86+
const browserName = getCurrentBrowserName();
87+
const { logEvent } = useLogContext();
6188
const { completeAction } = useActions();
6289
const { data, isPending, isError } = useQuestDashboard();
6390
const {
@@ -82,6 +109,7 @@ export const IntroQuestModal = ({
82109
] = useState<string[]>([]);
83110
const [deferredClaimedStampRotationIds, setDeferredClaimedStampRotationIds] =
84111
useState<string[]>([]);
112+
const loggedImpressionRef = useRef(false);
85113
const claimedStampTimersRef = useRef<number[]>([]);
86114
const claimedStampRotationIdSet = useMemo(
87115
() => new Set(claimedStampRotationIds),
@@ -99,14 +127,37 @@ export const IntroQuestModal = ({
99127
() => new Set(deferredClaimedStampRotationIds),
100128
[deferredClaimedStampRotationIds],
101129
);
130+
const introDestinations = useMemo<Record<string, QuestDestination | null>>(
131+
() => ({
132+
...introDestinationByEventType,
133+
extension_install: getExtensionIntroDestination(browserName),
134+
}),
135+
[browserName],
136+
);
102137

103138
useEffect(() => {
104139
if (!props.isOpen) {
105140
return;
106141
}
107142

143+
if (!loggedImpressionRef.current) {
144+
logEvent({
145+
event_name: LogEvent.Impression,
146+
target_type: TargetType.IntroQuestModal,
147+
});
148+
loggedImpressionRef.current = true;
149+
}
150+
108151
completeAction(ActionType.ViewedIntroQuests);
109-
}, [completeAction, props.isOpen]);
152+
}, [completeAction, logEvent, props.isOpen]);
153+
154+
useEffect(() => {
155+
if (props.isOpen) {
156+
return;
157+
}
158+
159+
loggedImpressionRef.current = false;
160+
}, [props.isOpen]);
110161

111162
const clearClaimedStampTimers = useCallback(() => {
112163
claimedStampTimersRef.current.forEach((timerId) => {
@@ -237,7 +288,18 @@ export const IntroQuestModal = ({
237288

238289
const handleDestinationClick = useCallback(
239290
async (destination: QuestDestination) => {
240-
onRequestClose?.();
291+
onRequestClose?.(undefined as never);
292+
293+
if ('href' in destination) {
294+
if (destination.openInNewTab) {
295+
window.open(destination.href, '_blank', 'noopener,noreferrer');
296+
return;
297+
}
298+
299+
window.location.assign(destination.href);
300+
return;
301+
}
302+
241303
await router.push(`${webappUrl}${destination.path.replace(/^\//, '')}`);
242304
},
243305
[onRequestClose, router],
@@ -294,9 +356,7 @@ export const IntroQuestModal = ({
294356
key={userQuest.rotationId}
295357
quest={userQuest}
296358
onClaim={handleClaim}
297-
destination={
298-
introDestinationByEventType[userQuest.quest.eventType]
299-
}
359+
destination={introDestinations[userQuest.quest.eventType]}
300360
onDestinationClick={handleDestinationClick}
301361
showLevelSystem
302362
isClaiming={claimingQuestId === userQuest.userQuestId}

packages/shared/src/components/quest/QuestButton.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1184,6 +1184,17 @@ export const QuestButton = ({
11841184
const handleDestinationClick = useCallback(
11851185
async (destination: QuestDestination) => {
11861186
setIsOpen(false);
1187+
1188+
if ('href' in destination) {
1189+
if (destination.openInNewTab) {
1190+
window.open(destination.href, '_blank', 'noopener,noreferrer');
1191+
return;
1192+
}
1193+
1194+
window.location.assign(destination.href);
1195+
return;
1196+
}
1197+
11871198
await router.push(`${webappUrl}${destination.path.replace(/^\//, '')}`);
11881199
},
11891200
[router],

packages/shared/src/components/quest/QuestCard.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,22 @@ type ClaimedStampMaskHole = {
1616
ry: number;
1717
};
1818

19-
export type QuestDestination = {
19+
type QuestPathDestination = {
2020
label: string;
2121
path: string;
22+
href?: never;
23+
openInNewTab?: never;
2224
};
2325

26+
type QuestHrefDestination = {
27+
label: string;
28+
href: string;
29+
openInNewTab?: boolean;
30+
path?: never;
31+
};
32+
33+
export type QuestDestination = QuestPathDestination | QuestHrefDestination;
34+
2435
export const QUEST_CLAIMED_STAMP_REVEAL_DELAY_MS = 220;
2536
export const QUEST_CLAIMED_STAMP_ANIMATION_MS = 340;
2637

packages/shared/src/lib/featureManagement.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,10 @@ export const featureProfileCompletionIndicator = new Feature(
145145
0,
146146
);
147147

148-
export const featureNewD1Experience = new Feature('new_d1_experience', true);
148+
export const featureNewD1Experience = new Feature(
149+
'new_d1_experience',
150+
isDevelopment,
151+
);
149152

150153
export const questsFeature = new Feature('quests', true);
151154

packages/shared/src/lib/log.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,7 @@ export enum TargetType {
465465
Source = 'source',
466466
Tag = 'tag',
467467
Quest = 'quest',
468+
IntroQuestModal = 'intro quest modal',
468469
// Settings
469470
Layout = 'layout',
470471
Theme = 'theme',

0 commit comments

Comments
 (0)