From bbb31b61f76a03c1aed76686084f575f323050ff Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 11 May 2026 15:17:17 +0300 Subject: [PATCH 1/9] feat: add contextual referral and review prompts Co-authored-by: Cursor --- packages/shared/src/components/MainLayout.tsx | 2 + .../modals/badges/TopReaderBadgeModal.tsx | 34 +++- .../modals/streaks/NewStreakModal.tsx | 26 ++- .../post/brief/BriefPostHeaderActions.tsx | 5 +- .../post/common/PostContentShare.tsx | 4 +- .../referral/ContextualReferralLink.tsx | 121 +++++++++++++ .../referral/ExtensionStoreReviewPrompt.tsx | 160 ++++++++++++++++++ .../src/components/squads/SquadHeaderBar.tsx | 12 +- packages/shared/src/graphql/actions.ts | 1 + .../shared/src/hooks/useSquadInvitation.ts | 7 +- packages/shared/src/lib/constants.ts | 2 + packages/shared/src/lib/featureManagement.ts | 10 ++ packages/shared/src/lib/log.ts | 9 + packages/shared/src/lib/referralGrowth.ts | 50 ++++++ .../Customization/DevCard/DevCardStep2.tsx | 78 ++++++--- 15 files changed, 485 insertions(+), 36 deletions(-) create mode 100644 packages/shared/src/components/referral/ContextualReferralLink.tsx create mode 100644 packages/shared/src/components/referral/ExtensionStoreReviewPrompt.tsx create mode 100644 packages/shared/src/lib/referralGrowth.ts diff --git a/packages/shared/src/components/MainLayout.tsx b/packages/shared/src/components/MainLayout.tsx index ef63d4e726a..ee5d208cb1c 100644 --- a/packages/shared/src/components/MainLayout.tsx +++ b/packages/shared/src/components/MainLayout.tsx @@ -36,6 +36,7 @@ import { SpotlightProvider } from './spotlight/SpotlightContext'; import { SpotlightHost } from './spotlight/SpotlightHost'; import { FeedbackWidget } from './feedback'; import { isExtension } from '../lib/func'; +import { ExtensionStoreReviewPrompt } from './referral/ExtensionStoreReviewPrompt'; const GoBackHeaderMobile = dynamic( () => @@ -186,6 +187,7 @@ function MainLayoutComponent({ + diff --git a/packages/shared/src/components/modals/badges/TopReaderBadgeModal.tsx b/packages/shared/src/components/modals/badges/TopReaderBadgeModal.tsx index b58280e5945..a893874b8db 100644 --- a/packages/shared/src/components/modals/badges/TopReaderBadgeModal.tsx +++ b/packages/shared/src/components/modals/badges/TopReaderBadgeModal.tsx @@ -17,6 +17,12 @@ import { LogEvent, TargetId, TargetType } from '../../../lib/log'; import { formatDate, TimeFormatType } from '../../../lib/dateFormat'; import { useTopReader } from '../../../hooks/useTopReader'; import { ModalClose } from '../common/ModalClose'; +import { ContextualReferralLink } from '../../referral/ContextualReferralLink'; +import { ReferralCampaignKey } from '../../../lib/referral'; +import { ReferralGrowthSurface } from '../../../lib/referralGrowth'; +import { webappUrl } from '../../../lib/constants'; +import { useConditionalFeature } from '../../../hooks/useConditionalFeature'; +import { featureReferralGrowthLoops } from '../../../lib/featureManagement'; type TopReaderBadgeModalProps = { badgeId?: string; @@ -25,15 +31,24 @@ type TopReaderBadgeModalProps = { const TopReaderBadgeModal = ( props: ModalProps & TopReaderBadgeModalProps, -): ReactElement => { +): ReactElement | null => { const { onRequestClose, onAfterOpen, onAfterClose, badgeId, origin } = props; const { user } = useAuthContext(); const { logEvent } = useLogContext(); const isMobile = useViewSize(ViewSize.MobileL); - const { data: topReaders } = useTopReader({ user, limit: 1, badgeId }); + const { data: topReaders } = useTopReader({ + user: user!, + limit: 1, + badgeId, + }); const topReader = topReaders?.[0]; + const { value: showReferralGrowthLoop } = useConditionalFeature({ + feature: featureReferralGrowthLoops, + shouldEvaluate: !!user?.id, + }); + const profileUrl = user?.username ? `${webappUrl}${user.username}` : ''; const { mutateAsync: onDownloadUrl, isPending: downloading } = useMutation({ mutationFn: downloadUrl, @@ -55,7 +70,7 @@ const TopReaderBadgeModal = ( ); const onClickDownload = useCallback(async () => { - if (!topReader) { + if (!topReader?.image) { return; } @@ -101,7 +116,7 @@ const TopReaderBadgeModal = ( You've earned the top reader badge! @@ -116,6 +131,17 @@ const TopReaderBadgeModal = ( > Download badge + {showReferralGrowthLoop && ( + + )} ); diff --git a/packages/shared/src/components/modals/streaks/NewStreakModal.tsx b/packages/shared/src/components/modals/streaks/NewStreakModal.tsx index 1f79b0784a1..c4e3de628db 100644 --- a/packages/shared/src/components/modals/streaks/NewStreakModal.tsx +++ b/packages/shared/src/components/modals/streaks/NewStreakModal.tsx @@ -12,12 +12,18 @@ import { } from '../../../lib/image'; import type { StreakModalProps } from './common'; import { useLogContext } from '../../../contexts/LogContext'; -import { LogEvent, TargetType } from '../../../lib/log'; +import { LogEvent, Origin, TargetType } from '../../../lib/log'; import { generateQueryKey, RequestKey } from '../../../lib/query'; import { useAuthContext } from '../../../contexts/AuthContext'; import { useActions } from '../../../hooks'; import { ActionType } from '../../../graphql/actions'; import StreakReminderSwitch from '../../streak/StreakReminderSwitch'; +import { ContextualReferralLink } from '../../referral/ContextualReferralLink'; +import { ReferralCampaignKey } from '../../../lib/referral'; +import { ReferralGrowthSurface } from '../../../lib/referralGrowth'; +import { webappUrl } from '../../../lib/constants'; +import { useConditionalFeature } from '../../../hooks/useConditionalFeature'; +import { featureReferralGrowthLoops } from '../../../lib/featureManagement'; const Paragraph = classed('p', 'text-center text-text-tertiary'); @@ -37,6 +43,11 @@ export default function NewStreakModal({ const shouldShowSplash = currentStreak >= maxStreak; const daysPlural = currentStreak === 1 ? 'day' : 'days'; const loggedImpression = useRef(false); + const { value: showReferralGrowthLoop } = useConditionalFeature({ + feature: featureReferralGrowthLoops, + shouldEvaluate: !!user?.id, + }); + const profileUrl = user?.username ? `${webappUrl}${user.username}` : ''; useEffect(() => { if (loggedImpression.current) { @@ -120,9 +131,20 @@ export default function NewStreakModal({ ? 'Epic win! You are in a league of your own' : `New milestone reached! You are unstoppable.`} + {showReferralGrowthLoop && ( + + )} diff --git a/packages/shared/src/components/post/brief/BriefPostHeaderActions.tsx b/packages/shared/src/components/post/brief/BriefPostHeaderActions.tsx index d1cb33406be..dc066198c4b 100644 --- a/packages/shared/src/components/post/brief/BriefPostHeaderActions.tsx +++ b/packages/shared/src/components/post/brief/BriefPostHeaderActions.tsx @@ -35,8 +35,11 @@ export const BriefPostHeaderActions = ({ )} + + ); +} diff --git a/packages/shared/src/components/referral/ExtensionStoreReviewPrompt.tsx b/packages/shared/src/components/referral/ExtensionStoreReviewPrompt.tsx new file mode 100644 index 00000000000..bcd76edeb44 --- /dev/null +++ b/packages/shared/src/components/referral/ExtensionStoreReviewPrompt.tsx @@ -0,0 +1,160 @@ +import type { ReactElement } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; +import { Button, ButtonSize, ButtonVariant } from '../buttons/Button'; +import { + Typography, + TypographyColor, + TypographyType, +} from '../typography/Typography'; +import { useAuthContext } from '../../contexts/AuthContext'; +import { useActions } from '../../hooks/useActions'; +import { ActionType } from '../../graphql/actions'; +import { isExtension } from '../../lib/func'; +import { useConditionalFeature } from '../../hooks/useConditionalFeature'; +import { featureExtensionStoreReviewPrompt } from '../../lib/featureManagement'; +import { useLogContext } from '../../contexts/LogContext'; +import { LogEvent, TargetType } from '../../lib/log'; +import { chromeWebStoreReviewUrl } from '../../lib/constants'; +import { useLazyModal } from '../../hooks/useLazyModal'; +import { LazyModal } from '../modals/common/types'; + +export function ExtensionStoreReviewPrompt(): ReactElement | null { + const { user } = useAuthContext(); + const { logEvent } = useLogContext(); + const { openModal } = useLazyModal(); + const { checkHasCompleted, completeAction, isActionsFetched } = useActions(); + const [isPositive, setIsPositive] = useState(false); + const loggedImpression = useRef(false); + const { value: isFeatureEnabled } = useConditionalFeature({ + feature: featureExtensionStoreReviewPrompt, + shouldEvaluate: isExtension && !!user?.id, + }); + + const hasPositiveSignal = + checkHasCompleted(ActionType.StreakMilestone) || + checkHasCompleted(ActionType.VotePost) || + checkHasCompleted(ActionType.BookmarkPost); + const shouldShow = + isExtension && + isFeatureEnabled && + !!user?.id && + isActionsFetched && + hasPositiveSignal && + !checkHasCompleted(ActionType.ChromeStoreReviewPrompt); + + useEffect(() => { + if (!shouldShow || loggedImpression.current) { + return; + } + + logEvent({ + event_name: LogEvent.StoreReviewPromptImpression, + target_type: TargetType.StoreReviewPrompt, + }); + loggedImpression.current = true; + }, [logEvent, shouldShow]); + + if (!shouldShow) { + return null; + } + + const completePrompt = () => + completeAction(ActionType.ChromeStoreReviewPrompt); + + const onPositive = () => { + setIsPositive(true); + logEvent({ + event_name: LogEvent.StoreReviewPromptPositive, + target_type: TargetType.StoreReviewPrompt, + }); + }; + + const onNegative = () => { + logEvent({ + event_name: LogEvent.StoreReviewPromptNegative, + target_type: TargetType.StoreReviewPrompt, + }); + completePrompt(); + openModal({ type: LazyModal.Feedback }); + }; + + const onDismiss = () => { + logEvent({ + event_name: LogEvent.StoreReviewPromptDismiss, + target_type: TargetType.StoreReviewPrompt, + }); + completePrompt(); + }; + + const onReview = () => { + logEvent({ + event_name: LogEvent.StoreReviewPromptClick, + target_type: TargetType.StoreReviewPrompt, + }); + completePrompt(); + }; + + return ( +
+
+
+ + {isPositive + ? 'Would you mind leaving a quick review?' + : 'Are you enjoying daily.dev on your new tab?'} + + + {isPositive + ? 'Honest Chrome Web Store reviews help other developers discover daily.dev.' + : 'Your feedback helps us understand whether this is the right moment to ask.'} + +
+
+ {isPositive ? ( + + ) : ( + <> + + + + )} + +
+
+
+ ); +} diff --git a/packages/shared/src/components/squads/SquadHeaderBar.tsx b/packages/shared/src/components/squads/SquadHeaderBar.tsx index 7dba4c90f9a..3ef19f9ee2d 100644 --- a/packages/shared/src/components/squads/SquadHeaderBar.tsx +++ b/packages/shared/src/components/squads/SquadHeaderBar.tsx @@ -42,7 +42,7 @@ const SquadSlackButton = ({ }: SquadBarButtonProps) => { const { user } = useAuthContext(); const { data: sourceIntegration, isPending } = useSourceIntegrationQuery({ - sourceId: squad.id, + sourceId: squad.id!, userIntegrationType: UserIntegrationType.Slack, }); @@ -59,7 +59,7 @@ const SquadSlackButton = ({ return 'Connect to Slack'; } - if (sourceIntegration?.userIntegration.userId === user.id) { + if (sourceIntegration?.userIntegration.userId === user?.id) { return 'Manage'; } @@ -87,7 +87,7 @@ const SquadAwardButton = ({ }: Pick) => { const { user } = useAuthContext(); const eligibleAdmin = useGetSquadAwardAdmin({ - sendingUser: user, + sendingUser: user!, squad, }); const canAwardSquad = !!eligibleAdmin; @@ -99,11 +99,11 @@ const SquadAwardButton = ({ ({ icon={} {...props} > - Invitation link + Invite colleagues ); }; diff --git a/packages/shared/src/graphql/actions.ts b/packages/shared/src/graphql/actions.ts index 47b47d13edc..6f6d8b81963 100644 --- a/packages/shared/src/graphql/actions.ts +++ b/packages/shared/src/graphql/actions.ts @@ -62,6 +62,7 @@ export enum ActionType { DisableAchievementCompletion = 'disable_achievement_completion', DismissInstallExtension = 'dismiss_install_extension', DismissShortcutsExtensionPromo = 'dismiss_shortcuts_extension_promo', + ChromeStoreReviewPrompt = 'chrome_store_review_prompt', DismissBriefCard = 'dismiss_brief_card', DigestUpsell = 'digest_upsell', AskUpsellSearch = 'ask_upsell_search', diff --git a/packages/shared/src/hooks/useSquadInvitation.ts b/packages/shared/src/hooks/useSquadInvitation.ts index 7dc522c19c4..e40e6f3f75a 100644 --- a/packages/shared/src/hooks/useSquadInvitation.ts +++ b/packages/shared/src/hooks/useSquadInvitation.ts @@ -5,6 +5,7 @@ import { LogEvent } from '../lib/log'; import type { Squad } from '../graphql/sources'; import { useActions } from './useActions'; import { ActionType } from '../graphql/actions'; +import { ReferralGrowthSurface } from '../lib/referralGrowth'; export interface UseSquadInvitationProps { squad: Squad; @@ -30,7 +31,11 @@ export const useSquadInvitation = ({ const logAndCopyLink = () => { logEvent({ event_name: LogEvent.ShareSquadInvitation, - extra: JSON.stringify({ origin, squad: squad.id }), + extra: JSON.stringify({ + origin, + squad: squad.id, + surface: ReferralGrowthSurface.Squad, + }), }); completeAction(ActionType.SquadInvite); diff --git a/packages/shared/src/lib/constants.ts b/packages/shared/src/lib/constants.ts index c7907deefda..c8b7239e430 100644 --- a/packages/shared/src/lib/constants.ts +++ b/packages/shared/src/lib/constants.ts @@ -29,6 +29,8 @@ export const firstNotificationLink = 'https://r.daily.dev/notifications'; export const reportSquadMember = 'https://r.daily.dev/report-squad-member'; export const squadFeedback = 'https://r.daily.dev/squad-feedback'; export const downloadBrowserExtension = 'https://r.daily.dev/extension'; +export const chromeWebStoreReviewUrl = + 'https://chromewebstore.google.com/detail/dailydev-where-developers/jlmpjdjjbgclbocgajdjefcidcncaied/reviews'; export const twitter = 'https://r.daily.dev/twitter'; export const slackIntegration = 'https://r.daily.dev/slack'; export const statusPage = 'https://r.daily.dev/status'; diff --git a/packages/shared/src/lib/featureManagement.ts b/packages/shared/src/lib/featureManagement.ts index 59322567e89..abb4a8c6f0a 100644 --- a/packages/shared/src/lib/featureManagement.ts +++ b/packages/shared/src/lib/featureManagement.ts @@ -195,6 +195,16 @@ export const featureShortcutsExtensionPromoCopy = new Feature( export const featureShortcutsHub = new Feature('shortcuts_hub', false); +export const featureReferralGrowthLoops = new Feature( + 'referral_growth_loops', + false, +); + +export const featureExtensionStoreReviewPrompt = new Feature( + 'extension_store_review_prompt', + false, +); + export const featureNewTabCustomizer = new Feature( 'extension_newtab_customizer', false, diff --git a/packages/shared/src/lib/log.ts b/packages/shared/src/lib/log.ts index 06227beaf07..cdcfbaf8e27 100644 --- a/packages/shared/src/lib/log.ts +++ b/packages/shared/src/lib/log.ts @@ -204,6 +204,13 @@ export enum LogEvent { // Referral campaign CopyReferralLink = 'copy referral link', InviteReferral = 'invite referral', + ReferralPromptImpression = 'impression referral prompt', + ReferralPromptClick = 'click referral prompt', + StoreReviewPromptImpression = 'impression store review prompt', + StoreReviewPromptPositive = 'positive store review prompt', + StoreReviewPromptNegative = 'negative store review prompt', + StoreReviewPromptClick = 'click store review prompt', + StoreReviewPromptDismiss = 'dismiss store review prompt', // Shortcuts RevokeShortcutAccess = 'revoke shortcut access', SaveShortcutAccess = 'save shortcut access', @@ -491,6 +498,8 @@ export enum TargetType { InviteFriendsPage = 'invite friends page', ProfilePage = 'profile page', GenericReferralPopup = 'generic referral popup', + ReferralPrompt = 'referral prompt', + StoreReviewPrompt = 'store review prompt', Shortcuts = 'shortcuts', VerifyEmail = 'verify email', ResendVerificationCode = 'resend verification code', diff --git a/packages/shared/src/lib/referralGrowth.ts b/packages/shared/src/lib/referralGrowth.ts new file mode 100644 index 00000000000..9ff48feb86e --- /dev/null +++ b/packages/shared/src/lib/referralGrowth.ts @@ -0,0 +1,50 @@ +import { ReferralCampaignKey } from './referral'; + +export enum ReferralGrowthSurface { + PostUpvote = 'post_upvote', + StreakMilestone = 'streak_milestone', + TopReaderBadge = 'top_reader_badge', + DevCard = 'devcard', + Squad = 'squad', + Brief = 'brief', +} + +export const referralGrowthTriggerMatrix: Record< + ReferralGrowthSurface, + { + campaignKey: ReferralCampaignKey; + trigger: string; + shareUnit: string; + } +> = { + [ReferralGrowthSurface.PostUpvote]: { + campaignKey: ReferralCampaignKey.SharePost, + trigger: 'post_upvote', + shareUnit: 'post_link', + }, + [ReferralGrowthSurface.StreakMilestone]: { + campaignKey: ReferralCampaignKey.ShareProfile, + trigger: 'streak_milestone', + shareUnit: 'profile_link', + }, + [ReferralGrowthSurface.TopReaderBadge]: { + campaignKey: ReferralCampaignKey.ShareProfile, + trigger: 'top_reader_badge', + shareUnit: 'profile_link', + }, + [ReferralGrowthSurface.DevCard]: { + campaignKey: ReferralCampaignKey.ShareProfile, + trigger: 'devcard_generated', + shareUnit: 'devcard_profile_link', + }, + [ReferralGrowthSurface.Squad]: { + campaignKey: ReferralCampaignKey.Generic, + trigger: 'squad_invite', + shareUnit: 'squad_invitation_link', + }, + [ReferralGrowthSurface.Brief]: { + campaignKey: ReferralCampaignKey.SharePost, + trigger: 'brief_share', + shareUnit: 'brief_post_link', + }, +}; diff --git a/packages/webapp/components/layouts/SettingsLayout/Customization/DevCard/DevCardStep2.tsx b/packages/webapp/components/layouts/SettingsLayout/Customization/DevCard/DevCardStep2.tsx index 3b1f4b1572c..2b2d8082d07 100644 --- a/packages/webapp/components/layouts/SettingsLayout/Customization/DevCard/DevCardStep2.tsx +++ b/packages/webapp/components/layouts/SettingsLayout/Customization/DevCard/DevCardStep2.tsx @@ -22,7 +22,7 @@ import { } from '@dailydotdev/shared/src/lib/query'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { gqlClient } from '@dailydotdev/shared/src/graphql/common'; -import { LogEvent } from '@dailydotdev/shared/src/lib/log'; +import { LogEvent, Origin } from '@dailydotdev/shared/src/lib/log'; import { Button } from '@dailydotdev/shared/src/components/buttons/Button'; import { ClickableText } from '@dailydotdev/shared/src/components/buttons/ClickableText'; import { @@ -37,7 +37,7 @@ import { TwitterIcon, } from '@dailydotdev/shared/src/components/icons'; import { DevCardFetchWrapper } from '@dailydotdev/shared/src/components/profile/devcard/DevCardFetchWrapper'; -import { devCard } from '@dailydotdev/shared/src/lib/constants'; +import { devCard, webappUrl } from '@dailydotdev/shared/src/lib/constants'; import { checkLowercaseEquality } from '@dailydotdev/shared/src/lib/strings'; import classNames from 'classnames'; import { isNullOrUndefined } from '@dailydotdev/shared/src/lib/func'; @@ -49,6 +49,12 @@ import { TypographyType, } from '@dailydotdev/shared/src/components/typography/Typography'; import { Tooltip } from '@dailydotdev/shared/src/components/tooltip/Tooltip'; +import { ContextualReferralLink } from '@dailydotdev/shared/src/components/referral/ContextualReferralLink'; +import { ReferralCampaignKey } from '@dailydotdev/shared/src/lib/referral'; +import { ReferralGrowthSurface } from '@dailydotdev/shared/src/lib/referralGrowth'; +import { addLogQueryParams } from '@dailydotdev/shared/src/lib/share'; +import { useConditionalFeature } from '@dailydotdev/shared/src/hooks/useConditionalFeature'; +import { featureReferralGrowthLoops } from '@dailydotdev/shared/src/lib/featureManagement'; import { GENERATE_DEVCARD_MUTATION } from '../../../../../graphql/devcard'; import type { GenerateDevCardParams } from '../../../../../graphql/devcard'; @@ -61,7 +67,11 @@ export const DevCardStep2 = ({ }: Step2Props): ReactElement => { const [type, setType] = useState(DevCardType.Vertical); const { user } = useAuthContext(); - const { devcard } = useDevCard(user?.id); + const userId = user?.id ?? ''; + const username = user?.username ?? ''; + const userName = user?.name ?? username; + const reputation = user?.reputation ?? 0; + const { devcard } = useDevCard(userId); const { theme, showBorder, isProfileCover } = devcard ?? { theme: DevCardTheme.Default, showBorder: true, @@ -72,23 +82,32 @@ export const DevCardStep2 = ({ const [devCardSrc, setDevCardSrc] = useState( initialDevCardSrc ?? - `${process.env.NEXT_PUBLIC_API_URL}/devcards/v2/${user.id}.png?type=default&r=${randomStr}`, + `${process.env.NEXT_PUBLIC_API_URL}/devcards/v2/${userId}.png?type=default&r=${randomStr}`, ); const client = useQueryClient(); const { logEvent } = useLogContext(); const key = useMemo( - () => generateQueryKey(RequestKey.DevCard, { id: user?.id }), - [user], + () => generateQueryKey(RequestKey.DevCard, { id: userId }), + [userId], ); + const profileUrl = username ? `${webappUrl}${username}` : webappUrl; + const trackedProfileUrl = + addLogQueryParams({ + link: profileUrl, + userId, + cid: ReferralCampaignKey.ShareProfile, + }) ?? profileUrl; + const { value: showReferralGrowthLoop } = useConditionalFeature({ + feature: featureReferralGrowthLoops, + shouldEvaluate: !!userId, + }); const embedCode = useMemo( () => - `${user?.name}'s Dev Card`, - [user?.name, user?.username, devCardSrc, type], + }" alt="${userName}'s Dev Card"/>`, + [userName, devCardSrc, trackedProfileUrl, type], ); const [copyingEmbed, copyEmbed] = useCopyLink(() => embedCode); const [selectedTab, setSelectedTab] = useState(0); @@ -98,7 +117,7 @@ export const DevCardStep2 = ({ const downloadImage = async (url?: string): Promise => { const finalUrl = url ?? devCardSrc; - await onDownloadUrl({ url: finalUrl, filename: `${user.username}.png` }); + await onDownloadUrl({ url: finalUrl, filename: `${username}.png` }); }; const { mutateAsync: onGenerate, isPending: isLoading } = useMutation({ @@ -111,7 +130,7 @@ export const DevCardStep2 = ({ }, onSuccess: (data, vars) => { - if (!data?.devCard?.imageUrl || vars.type === DevCardType.Twitter) { + if (!data?.devCard?.imageUrl || vars?.type === DevCardType.Twitter) { return; } @@ -158,7 +177,10 @@ export const DevCardStep2 = ({ logEvent({ event_name: LogEvent.DownloadDevcard, extra: JSON.stringify({ - format: devcardTypeToEventFormat[type], + format: + devcardTypeToEventFormat[ + type as keyof typeof devcardTypeToEventFormat + ], }), }); } @@ -213,7 +235,7 @@ export const DevCardStep2 = ({ style={{ transformStyle: 'preserve-3d' }} > @@ -275,6 +297,17 @@ export const DevCardStep2 = ({ Add your DevCard to GitHub, your website, or use it as your X header image. + {showReferralGrowthLoop && ( + + )}
{Object.keys(themeToLinearGradient).map((value) => { - const isLocked = user?.reputation < requiredPoints[value]; + const themeValue = value as DevCardTheme; + const isLocked = reputation < requiredPoints[themeValue]; return ( {value} ) @@ -396,11 +430,13 @@ export const DevCardStep2 = ({ className={classNames( 'h-10 w-10 rounded-full', isLocked && 'opacity-32', - checkLowercaseEquality(theme, value) && - 'border-4 border-accent-cabbage-default', + checkLowercaseEquality( + theme ?? DevCardTheme.Default, + value, + ) && 'border-4 border-accent-cabbage-default', )} style={{ - background: themeToLinearGradient[value], + background: themeToLinearGradient[themeValue], }} onClick={() => onUpdatePreference({ @@ -441,7 +477,7 @@ export const DevCardStep2 = ({ onUpdatePreference({ isProfileCover: true })} From 158f99bc864c71a198f8c2e9148adcfde6824bc3 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 11 May 2026 15:22:26 +0300 Subject: [PATCH 2/9] fix: keep referral flags out of feature registry Co-authored-by: Cursor --- .../components/modals/badges/TopReaderBadgeModal.tsx | 6 ++++-- .../src/components/modals/streaks/NewStreakModal.tsx | 6 ++++-- .../components/referral/ExtensionStoreReviewPrompt.tsx | 2 +- packages/shared/src/lib/featureManagement.ts | 10 ---------- packages/shared/src/lib/referralGrowth.ts | 10 ++++++++++ .../Customization/DevCard/DevCardStep2.tsx | 6 ++++-- 6 files changed, 23 insertions(+), 17 deletions(-) diff --git a/packages/shared/src/components/modals/badges/TopReaderBadgeModal.tsx b/packages/shared/src/components/modals/badges/TopReaderBadgeModal.tsx index a893874b8db..c9180d6ef36 100644 --- a/packages/shared/src/components/modals/badges/TopReaderBadgeModal.tsx +++ b/packages/shared/src/components/modals/badges/TopReaderBadgeModal.tsx @@ -19,10 +19,12 @@ import { useTopReader } from '../../../hooks/useTopReader'; import { ModalClose } from '../common/ModalClose'; import { ContextualReferralLink } from '../../referral/ContextualReferralLink'; import { ReferralCampaignKey } from '../../../lib/referral'; -import { ReferralGrowthSurface } from '../../../lib/referralGrowth'; +import { + featureReferralGrowthLoops, + ReferralGrowthSurface, +} from '../../../lib/referralGrowth'; import { webappUrl } from '../../../lib/constants'; import { useConditionalFeature } from '../../../hooks/useConditionalFeature'; -import { featureReferralGrowthLoops } from '../../../lib/featureManagement'; type TopReaderBadgeModalProps = { badgeId?: string; diff --git a/packages/shared/src/components/modals/streaks/NewStreakModal.tsx b/packages/shared/src/components/modals/streaks/NewStreakModal.tsx index c4e3de628db..86879cc7a4b 100644 --- a/packages/shared/src/components/modals/streaks/NewStreakModal.tsx +++ b/packages/shared/src/components/modals/streaks/NewStreakModal.tsx @@ -20,10 +20,12 @@ import { ActionType } from '../../../graphql/actions'; import StreakReminderSwitch from '../../streak/StreakReminderSwitch'; import { ContextualReferralLink } from '../../referral/ContextualReferralLink'; import { ReferralCampaignKey } from '../../../lib/referral'; -import { ReferralGrowthSurface } from '../../../lib/referralGrowth'; +import { + featureReferralGrowthLoops, + ReferralGrowthSurface, +} from '../../../lib/referralGrowth'; import { webappUrl } from '../../../lib/constants'; import { useConditionalFeature } from '../../../hooks/useConditionalFeature'; -import { featureReferralGrowthLoops } from '../../../lib/featureManagement'; const Paragraph = classed('p', 'text-center text-text-tertiary'); diff --git a/packages/shared/src/components/referral/ExtensionStoreReviewPrompt.tsx b/packages/shared/src/components/referral/ExtensionStoreReviewPrompt.tsx index bcd76edeb44..82e1d4b6339 100644 --- a/packages/shared/src/components/referral/ExtensionStoreReviewPrompt.tsx +++ b/packages/shared/src/components/referral/ExtensionStoreReviewPrompt.tsx @@ -11,7 +11,7 @@ import { useActions } from '../../hooks/useActions'; import { ActionType } from '../../graphql/actions'; import { isExtension } from '../../lib/func'; import { useConditionalFeature } from '../../hooks/useConditionalFeature'; -import { featureExtensionStoreReviewPrompt } from '../../lib/featureManagement'; +import { featureExtensionStoreReviewPrompt } from '../../lib/referralGrowth'; import { useLogContext } from '../../contexts/LogContext'; import { LogEvent, TargetType } from '../../lib/log'; import { chromeWebStoreReviewUrl } from '../../lib/constants'; diff --git a/packages/shared/src/lib/featureManagement.ts b/packages/shared/src/lib/featureManagement.ts index abb4a8c6f0a..59322567e89 100644 --- a/packages/shared/src/lib/featureManagement.ts +++ b/packages/shared/src/lib/featureManagement.ts @@ -195,16 +195,6 @@ export const featureShortcutsExtensionPromoCopy = new Feature( export const featureShortcutsHub = new Feature('shortcuts_hub', false); -export const featureReferralGrowthLoops = new Feature( - 'referral_growth_loops', - false, -); - -export const featureExtensionStoreReviewPrompt = new Feature( - 'extension_store_review_prompt', - false, -); - export const featureNewTabCustomizer = new Feature( 'extension_newtab_customizer', false, diff --git a/packages/shared/src/lib/referralGrowth.ts b/packages/shared/src/lib/referralGrowth.ts index 9ff48feb86e..3ecfac6f7ba 100644 --- a/packages/shared/src/lib/referralGrowth.ts +++ b/packages/shared/src/lib/referralGrowth.ts @@ -1,5 +1,15 @@ import { ReferralCampaignKey } from './referral'; +export const featureReferralGrowthLoops = { + id: 'referral_growth_loops', + defaultValue: false, +}; + +export const featureExtensionStoreReviewPrompt = { + id: 'extension_store_review_prompt', + defaultValue: false, +}; + export enum ReferralGrowthSurface { PostUpvote = 'post_upvote', StreakMilestone = 'streak_milestone', diff --git a/packages/webapp/components/layouts/SettingsLayout/Customization/DevCard/DevCardStep2.tsx b/packages/webapp/components/layouts/SettingsLayout/Customization/DevCard/DevCardStep2.tsx index 2b2d8082d07..ce0a19654db 100644 --- a/packages/webapp/components/layouts/SettingsLayout/Customization/DevCard/DevCardStep2.tsx +++ b/packages/webapp/components/layouts/SettingsLayout/Customization/DevCard/DevCardStep2.tsx @@ -51,10 +51,12 @@ import { import { Tooltip } from '@dailydotdev/shared/src/components/tooltip/Tooltip'; import { ContextualReferralLink } from '@dailydotdev/shared/src/components/referral/ContextualReferralLink'; import { ReferralCampaignKey } from '@dailydotdev/shared/src/lib/referral'; -import { ReferralGrowthSurface } from '@dailydotdev/shared/src/lib/referralGrowth'; +import { + featureReferralGrowthLoops, + ReferralGrowthSurface, +} from '@dailydotdev/shared/src/lib/referralGrowth'; import { addLogQueryParams } from '@dailydotdev/shared/src/lib/share'; import { useConditionalFeature } from '@dailydotdev/shared/src/hooks/useConditionalFeature'; -import { featureReferralGrowthLoops } from '@dailydotdev/shared/src/lib/featureManagement'; import { GENERATE_DEVCARD_MUTATION } from '../../../../../graphql/devcard'; import type { GenerateDevCardParams } from '../../../../../graphql/devcard'; From da5bdf172a012447a2ece526cc2fdee3a9a33f0a Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 11 May 2026 15:29:50 +0300 Subject: [PATCH 3/9] chore: add temporary referral QA panel Co-authored-by: Cursor --- packages/shared/src/components/MainLayout.tsx | 2 + .../referral/ReferralGrowthTestingPanel.tsx | 223 ++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 packages/shared/src/components/referral/ReferralGrowthTestingPanel.tsx diff --git a/packages/shared/src/components/MainLayout.tsx b/packages/shared/src/components/MainLayout.tsx index ee5d208cb1c..04aff696dab 100644 --- a/packages/shared/src/components/MainLayout.tsx +++ b/packages/shared/src/components/MainLayout.tsx @@ -37,6 +37,7 @@ import { SpotlightHost } from './spotlight/SpotlightHost'; import { FeedbackWidget } from './feedback'; import { isExtension } from '../lib/func'; import { ExtensionStoreReviewPrompt } from './referral/ExtensionStoreReviewPrompt'; +import { ReferralGrowthTestingPanel } from './referral/ReferralGrowthTestingPanel'; const GoBackHeaderMobile = dynamic( () => @@ -187,6 +188,7 @@ function MainLayoutComponent({ + diff --git a/packages/shared/src/components/referral/ReferralGrowthTestingPanel.tsx b/packages/shared/src/components/referral/ReferralGrowthTestingPanel.tsx new file mode 100644 index 00000000000..0a1499f05f8 --- /dev/null +++ b/packages/shared/src/components/referral/ReferralGrowthTestingPanel.tsx @@ -0,0 +1,223 @@ +import type { ReactElement } from 'react'; +import React, { useState } from 'react'; +import { Button, ButtonSize, ButtonVariant } from '../buttons/Button'; +import { + Typography, + TypographyColor, + TypographyType, +} from '../typography/Typography'; +import { useAuthContext } from '../../contexts/AuthContext'; +import { useLazyModal } from '../../hooks/useLazyModal'; +import { LazyModal } from '../modals/common/types'; +import { + chromeWebStoreReviewUrl, + settingsUrl, + webappUrl, +} from '../../lib/constants'; +import { ReferralCampaignKey } from '../../lib/referral'; +import { ReferralGrowthSurface } from '../../lib/referralGrowth'; +import { ContextualReferralLink } from './ContextualReferralLink'; + +const demoSurfaces = [ + { + surface: ReferralGrowthSurface.DevCard, + title: 'DevCard invite', + description: + 'Demo the tracked invite prompt shown after someone prepares their DevCard.', + buttonText: 'Copy DevCard invite', + }, + { + surface: ReferralGrowthSurface.StreakMilestone, + title: 'Streak challenge', + description: + 'Demo the tracked invite prompt attached to a reading streak milestone.', + buttonText: 'Copy streak invite', + }, + { + surface: ReferralGrowthSurface.TopReaderBadge, + title: 'Top reader badge', + description: + 'Demo the tracked invite prompt shown from the top-reader badge moment.', + buttonText: 'Copy badge invite', + }, + { + surface: ReferralGrowthSurface.Brief, + title: 'Brief share', + description: + 'Demo the tracked invite prompt for sharing a useful brief with a colleague.', + buttonText: 'Copy brief invite', + }, +]; + +export function ReferralGrowthTestingPanel(): ReactElement { + const { user } = useAuthContext(); + const { openModal } = useLazyModal(); + const [isCollapsed, setIsCollapsed] = useState(false); + const [selectedSurface, setSelectedSurface] = useState(demoSurfaces[0]); + const [showReviewStep, setShowReviewStep] = useState(false); + const profileUrl = user?.username + ? `${webappUrl}${user.username}` + : webappUrl; + + if (isCollapsed) { + return ( + + ); + } + + return ( + + ); +} From a70bcfbc631230530139c798cb011d40e650c5be Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 11 May 2026 15:46:52 +0300 Subject: [PATCH 4/9] fix: guard referral QA links against invalid URLs Co-authored-by: Cursor --- .../referral/ReferralGrowthTestingPanel.tsx | 19 ++++++++++++++++--- packages/shared/src/lib/share.ts | 9 ++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/components/referral/ReferralGrowthTestingPanel.tsx b/packages/shared/src/components/referral/ReferralGrowthTestingPanel.tsx index 0a1499f05f8..b289f6a1971 100644 --- a/packages/shared/src/components/referral/ReferralGrowthTestingPanel.tsx +++ b/packages/shared/src/components/referral/ReferralGrowthTestingPanel.tsx @@ -49,15 +49,28 @@ const demoSurfaces = [ }, ]; +const getWebappUrl = (): string => { + if (webappUrl) { + return webappUrl; + } + + if (typeof window === 'undefined') { + return ''; + } + + return `${window.location.origin}/`; +}; + export function ReferralGrowthTestingPanel(): ReactElement { const { user } = useAuthContext(); const { openModal } = useLazyModal(); const [isCollapsed, setIsCollapsed] = useState(false); const [selectedSurface, setSelectedSurface] = useState(demoSurfaces[0]); const [showReviewStep, setShowReviewStep] = useState(false); + const qaWebappUrl = getWebappUrl(); const profileUrl = user?.username - ? `${webappUrl}${user.username}` - : webappUrl; + ? `${qaWebappUrl}${user.username}` + : qaWebappUrl; if (isCollapsed) { return ( @@ -119,7 +132,7 @@ export function ReferralGrowthTestingPanel(): ReactElement { - ); - } + const toggleFlags = () => { + if (flagsForced) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (growthbook as any)?.setForcedFeatures?.(new Map()); + setFlagsForced(false); + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (growthbook as any)?.setForcedFeatures?.( + new Map([ + ['referral_growth_loops', true], + ['extension_store_review_prompt', true], + ]), + ); + setFlagsForced(true); + } + }; - return ( - + ); } From 9ff8cacc4e45572f67503fb5d195a9071d67b2a4 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 12 May 2026 08:53:03 +0300 Subject: [PATCH 7/9] feat: polish referral prompt and store review UX/UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ContextualReferralLink: - Gradient card (onion/cabbage) with glow decoration so it feels like a moment, not a form field - UserShareIcon in a coloured icon pill sets context instantly - URL displayed inline in a pill row — users see exactly what they're sharing before clicking - Inline XSmall copy button replaces full-width secondary button - Copy success state: VIcon + "Copied!" in Primary variant with avocado colour ExtensionStoreReviewPrompt: - Five StarIcons (outline → filled gold on step 2) establish the review context without any copy - Warm cheese gradient on the positive step creates a celebratory, differentiated state vs the neutral first question - "Yes, loving it!" / "Not yet" copy is more honest and conversational - Replaced "Not now" text button with a compact MiniCloseIcon dismiss at the far right — same affordance, cleaner layout - 300ms colour transition between the two steps QA panel: - Banner preview JSX kept in sync with the redesigned component so the panel always shows the exact experience users will see Co-authored-by: Cursor --- .../referral/ContextualReferralLink.tsx | 66 ++++++---- .../referral/ExtensionStoreReviewPrompt.tsx | 65 +++++++--- .../referral/ReferralGrowthTestingPanel.tsx | 117 +++++++++++++----- 3 files changed, 181 insertions(+), 67 deletions(-) diff --git a/packages/shared/src/components/referral/ContextualReferralLink.tsx b/packages/shared/src/components/referral/ContextualReferralLink.tsx index f3a2b091100..72d52e6a386 100644 --- a/packages/shared/src/components/referral/ContextualReferralLink.tsx +++ b/packages/shared/src/components/referral/ContextualReferralLink.tsx @@ -16,6 +16,8 @@ import { ShareProvider } from '../../lib/share'; import type { Origin } from '../../lib/log'; import { LogEvent, TargetType } from '../../lib/log'; import type { ReferralGrowthSurface } from '../../lib/referralGrowth'; +import { UserShareIcon, VIcon } from '../icons'; +import { IconSize } from '../Icon'; type ContextualReferralLinkProps = { url: string; @@ -36,7 +38,7 @@ export function ContextualReferralLink({ description, origin, className, - buttonText = 'Copy invite link', + buttonText = 'Copy link', }: ContextualReferralLinkProps): ReactElement | null { const { user } = useAuthContext(); const { logEvent } = useLogContext(); @@ -91,31 +93,51 @@ export function ContextualReferralLink({ return (
-
- - {title} - - - {description} - + {/* Background glow */} +
+ +
+ {/* Icon + copy */} +
+ + + +
+ + {title} + + + {description} + +
+
+ + {/* URL display + inline copy button */} +
+ + {isLoading ? 'Preparing your link…' : link ?? url} + + +
-
); } diff --git a/packages/shared/src/components/referral/ExtensionStoreReviewPrompt.tsx b/packages/shared/src/components/referral/ExtensionStoreReviewPrompt.tsx index 82e1d4b6339..1a63489036b 100644 --- a/packages/shared/src/components/referral/ExtensionStoreReviewPrompt.tsx +++ b/packages/shared/src/components/referral/ExtensionStoreReviewPrompt.tsx @@ -1,5 +1,6 @@ import type { ReactElement } from 'react'; import React, { useEffect, useRef, useState } from 'react'; +import classNames from 'classnames'; import { Button, ButtonSize, ButtonVariant } from '../buttons/Button'; import { Typography, @@ -17,6 +18,10 @@ import { LogEvent, TargetType } from '../../lib/log'; import { chromeWebStoreReviewUrl } from '../../lib/constants'; import { useLazyModal } from '../../hooks/useLazyModal'; import { LazyModal } from '../modals/common/types'; +import { StarIcon, MiniCloseIcon } from '../icons'; +import { IconSize } from '../Icon'; + +const STAR_INDICES = [0, 1, 2, 3, 4] as const; export function ExtensionStoreReviewPrompt(): ReactElement | null { const { user } = useAuthContext(); @@ -95,12 +100,36 @@ export function ExtensionStoreReviewPrompt(): ReactElement | null { }; return ( -
-
-
+
+
+ {/* Star row — shows outline stars initially, filled gold stars in positive step */} +
+ {STAR_INDICES.map((i) => ( + + ))} +
+ + {/* Text */} +
{isPositive - ? 'Would you mind leaving a quick review?' + ? 'Would you leave a quick review?' : 'Are you enjoying daily.dev on your new tab?'} {isPositive - ? 'Honest Chrome Web Store reviews help other developers discover daily.dev.' - : 'Your feedback helps us understand whether this is the right moment to ask.'} + ? 'Honest reviews help other developers discover daily.dev — it takes 30 seconds.' + : 'Let us know if this is a good moment to ask.'}
-
+ + {/* Actions */} +
{isPositive ? ( ) : ( <> @@ -133,26 +164,28 @@ export function ExtensionStoreReviewPrompt(): ReactElement | null { variant={ButtonVariant.Primary} onClick={onPositive} > - Yes + Yes, loving it! )} - + +
diff --git a/packages/shared/src/components/referral/ReferralGrowthTestingPanel.tsx b/packages/shared/src/components/referral/ReferralGrowthTestingPanel.tsx index 8d01cb9eb0c..e401e107306 100644 --- a/packages/shared/src/components/referral/ReferralGrowthTestingPanel.tsx +++ b/packages/shared/src/components/referral/ReferralGrowthTestingPanel.tsx @@ -1,5 +1,6 @@ import type { ReactElement } from 'react'; import React, { useState } from 'react'; +import classNames from 'classnames'; import { Button, ButtonSize, ButtonVariant } from '../buttons/Button'; import { Typography, @@ -15,6 +16,10 @@ import { settingsUrl, } from '../../lib/constants'; import { useGrowthBookContext } from '../GrowthBookProvider'; +import { StarIcon, MiniCloseIcon } from '../icons'; +import { IconSize } from '../Icon'; + +const STAR_INDICES = [0, 1, 2, 3, 4] as const; /* * TEMPORARY QA PANEL @@ -53,7 +58,10 @@ export function ReferralGrowthTestingPanel(): ReactElement | null { }; const openStreakModal = (currentStreak: number, maxStreak: number) => - openModal({ type: LazyModal.NewStreak, props: { currentStreak, maxStreak } }); + openModal({ + type: LazyModal.NewStreak, + props: { currentStreak, maxStreak }, + }); const openFeedbackModal = () => openModal({ type: LazyModal.Feedback }); @@ -65,23 +73,53 @@ export function ReferralGrowthTestingPanel(): ReactElement | null { /* * The review banner renders fixed at the top of the viewport — exactly the * width and position it occupies in the real extension new-tab layout. + * JSX mirrors ExtensionStoreReviewPrompt so what you see here is what users see. */ const reviewBanner = reviewOpen ? ( -
-
-
+
+
+ {/* Stars */} +
+ {STAR_INDICES.map((i) => ( + + ))} +
+ + {/* Text */} +
{reviewPositive - ? 'Would you mind leaving a quick review?' + ? 'Would you leave a quick review?' : 'Are you enjoying daily.dev on your new tab?'} - + {reviewPositive - ? 'Honest Chrome Web Store reviews help other developers discover daily.dev.' - : 'Your feedback helps us understand whether this is the right moment to ask.'} + ? 'Honest reviews help other developers discover daily.dev — it takes 30 seconds.' + : 'Let us know if this is a good moment to ask.'}
-
+ + {/* Actions */} +
{reviewPositive ? ( ) : ( <> @@ -102,29 +140,29 @@ export function ReferralGrowthTestingPanel(): ReactElement | null { variant={ButtonVariant.Primary} onClick={() => setReviewPositive(true)} > - Yes + Yes, loving it! )} - + +
@@ -154,7 +192,10 @@ export function ReferralGrowthTestingPanel(): ReactElement | null { Referral/review QA - + Triggers the real interactions — remove before launch.
@@ -173,15 +214,19 @@ export function ReferralGrowthTestingPanel(): ReactElement | null { 1. Enable flags first - - Forces{' '} - referral_growth_loops on so - the invite sections appear inside the modals below. + + Forces referral_growth_loops{' '} + on so the invite sections appear inside the modals below.
{!user?.id && ( - + ⚠ Log in to test tracked invite links. )} From ed4e480973275826ab8dd003d10ed87db55a4658 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 12 May 2026 09:38:44 +0300 Subject: [PATCH 8/9] feat: dedicated StreakShareCallout for streak milestone invite moment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Picked the streak milestone as the focus surface for deep design — it has the highest invite-conversion potential (peak emotional charge, recurring touchpoints, naturally challenge-able number). New `StreakShareCallout` replaces the generic `ContextualReferralLink` inside `NewStreakModal`: - Bacon/cheese gradient continues the streak fire visual into the share block instead of feeling tacked on - Pre-written, streak-specific message with the user's actual count baked in. Tone adapts to milestone size (friendly at 7, cocky at 30, dominant at 100) - Chat-bubble preview shows users exactly what their friend will receive before they click anything — biggest trust signal in invite flows - Five channel buttons in a horizontal row (WhatsApp, X, Telegram, Email, Copy) — the channels devs actually use for 1:1 invites. Skipped FB, LinkedIn, Reddit (wrong tone for a personal challenge) - Per-channel share URLs include both message text AND link, working around getWhatsappShareLink only encoding the URL - Native Web Share API button surfaces on mobile when supported - Per-channel logging via ShareProvider so we can measure conversion by channel QA panel redesign: - New hero card highlights the streak invite as the focus design - Single primary button forces the flag and opens the modal in one click - Three streak variants (7-day / 30-day / 100-day) demonstrate adaptive copy - Other surfaces moved into a collapsed "Other referral surfaces" details drawer to reduce noise Co-authored-by: Cursor --- .../modals/streaks/NewStreakModal.tsx | 18 +- .../referral/ReferralGrowthTestingPanel.tsx | 309 ++++++++-------- .../referral/StreakShareCallout.tsx | 350 ++++++++++++++++++ 3 files changed, 515 insertions(+), 162 deletions(-) create mode 100644 packages/shared/src/components/referral/StreakShareCallout.tsx diff --git a/packages/shared/src/components/modals/streaks/NewStreakModal.tsx b/packages/shared/src/components/modals/streaks/NewStreakModal.tsx index 86879cc7a4b..80134191cb2 100644 --- a/packages/shared/src/components/modals/streaks/NewStreakModal.tsx +++ b/packages/shared/src/components/modals/streaks/NewStreakModal.tsx @@ -12,18 +12,14 @@ import { } from '../../../lib/image'; import type { StreakModalProps } from './common'; import { useLogContext } from '../../../contexts/LogContext'; -import { LogEvent, Origin, TargetType } from '../../../lib/log'; +import { LogEvent, TargetType } from '../../../lib/log'; import { generateQueryKey, RequestKey } from '../../../lib/query'; import { useAuthContext } from '../../../contexts/AuthContext'; import { useActions } from '../../../hooks'; import { ActionType } from '../../../graphql/actions'; import StreakReminderSwitch from '../../streak/StreakReminderSwitch'; -import { ContextualReferralLink } from '../../referral/ContextualReferralLink'; -import { ReferralCampaignKey } from '../../../lib/referral'; -import { - featureReferralGrowthLoops, - ReferralGrowthSurface, -} from '../../../lib/referralGrowth'; +import { StreakShareCallout } from '../../referral/StreakShareCallout'; +import { featureReferralGrowthLoops } from '../../../lib/referralGrowth'; import { webappUrl } from '../../../lib/constants'; import { useConditionalFeature } from '../../../hooks/useConditionalFeature'; @@ -134,14 +130,10 @@ export default function NewStreakModal({ : `New milestone reached! You are unstoppable.`} {showReferralGrowthLoop && ( - )} { - if (flagsForced) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (growthbook as any)?.setForcedFeatures?.(new Map()); - setFlagsForced(false); - } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (growthbook as any)?.setForcedFeatures?.( + const setFlagsForcedOn = (on: boolean) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const gb = growthbook as any; + if (on) { + gb?.setForcedFeatures?.( new Map([ ['referral_growth_loops', true], ['extension_store_review_prompt', true], ]), ); - setFlagsForced(true); + } else { + gb?.setForcedFeatures?.(new Map()); } + setFlagsForced(on); }; - const openStreakModal = (currentStreak: number, maxStreak: number) => + const launchStreakInvite = (currentStreak: number, maxStreak: number) => { + setFlagsForcedOn(true); openModal({ type: LazyModal.NewStreak, props: { currentStreak, maxStreak }, }); + }; const openFeedbackModal = () => openModal({ type: LazyModal.Feedback }); @@ -70,11 +71,6 @@ export function ReferralGrowthTestingPanel(): ReactElement | null { setReviewPositive(false); }; - /* - * The review banner renders fixed at the top of the viewport — exactly the - * width and position it occupies in the real extension new-tab layout. - * JSX mirrors ExtensionStoreReviewPrompt so what you see here is what users see. - */ const reviewBanner = reviewOpen ? (
- {/* Stars */}
{STAR_INDICES.map((i) => ( ))}
- - {/* Text */}
{reviewPositive @@ -117,8 +110,6 @@ export function ReferralGrowthTestingPanel(): ReactElement | null { : 'Let us know if this is a good moment to ask.'}
- - {/* Actions */}
{reviewPositive ? ( ); @@ -186,7 +178,8 @@ export function ReferralGrowthTestingPanel(): ReactElement | null { return ( <> {reviewBanner} -