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
4 changes: 4 additions & 0 deletions packages/shared/src/components/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import { SpotlightProvider } from './spotlight/SpotlightContext';
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(
() =>
Expand Down Expand Up @@ -186,6 +188,8 @@ function MainLayoutComponent({
<InAppNotificationElement />
<PromptElement />
<Toast autoDismissNotifications={autoDismissNotifications} />
<ReferralGrowthTestingPanel />
<ExtensionStoreReviewPrompt />
<BootPopups />
<SpotlightHost />
<StreakMilestonePopup />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ 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 {
featureReferralGrowthLoops,
ReferralGrowthSurface,
} from '../../../lib/referralGrowth';
import { webappUrl } from '../../../lib/constants';
import { useConditionalFeature } from '../../../hooks/useConditionalFeature';

type TopReaderBadgeModalProps = {
badgeId?: string;
Expand All @@ -25,15 +33,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,
Expand All @@ -55,7 +72,7 @@ const TopReaderBadgeModal = (
);

const onClickDownload = useCallback(async () => {
if (!topReader) {
if (!topReader?.image) {
return;
}

Expand Down Expand Up @@ -101,7 +118,7 @@ const TopReaderBadgeModal = (
You&apos;ve earned the top reader badge!
</h1>
<TopReaderBadge
user={user}
user={user!}
keyword={topReader.keyword}
issuedAt={topReader.issuedAt}
/>
Expand All @@ -116,6 +133,17 @@ const TopReaderBadgeModal = (
>
Download badge
</Button>
{showReferralGrowthLoop && (
<ContextualReferralLink
className={classNames('w-full', !isMobile && 'max-w-80')}
url={profileUrl}
campaignKey={ReferralCampaignKey.ShareProfile}
surface={ReferralGrowthSurface.TopReaderBadge}
origin={origin ?? 'top reader badge modal'}
title="Let other devs see your badge"
description="Share your profile so friends can check your reader status and get their own."
/>
)}
</Modal.Body>
</Modal>
);
Expand Down
18 changes: 17 additions & 1 deletion packages/shared/src/components/modals/streaks/NewStreakModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import { useAuthContext } from '../../../contexts/AuthContext';
import { useActions } from '../../../hooks';
import { ActionType } from '../../../graphql/actions';
import StreakReminderSwitch from '../../streak/StreakReminderSwitch';
import { StreakShareCallout } from '../../referral/StreakShareCallout';
import { featureReferralGrowthLoops } from '../../../lib/referralGrowth';
import { webappUrl } from '../../../lib/constants';
import { useConditionalFeature } from '../../../hooks/useConditionalFeature';

const Paragraph = classed('p', 'text-center text-text-tertiary');

Expand All @@ -37,6 +41,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) {
Expand Down Expand Up @@ -120,9 +129,16 @@ export default function NewStreakModal({
? 'Epic win! You are in a league of your own'
: `New milestone reached! You are unstoppable.`}
</Paragraph>
{showReferralGrowthLoop && (
<StreakShareCallout
className="mt-6"
url={profileUrl}
currentStreak={currentStreak}
/>
)}
<Checkbox
name="show_streaks"
className="mt-10"
className={showReferralGrowthLoop ? 'mt-6' : 'mt-10'}
checked={isStreakModalDisabled}
onToggleCallback={handleOptOut}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ export const BriefPostHeaderActions = ({
<Button
icon={<LinkIcon />}
size={ButtonSize.Medium}
aria-label="Share this brief with a colleague"
onClick={() => copyLink({ post })}
/>
>
Share brief
</Button>
)}
<Link passHref href={`${settingsUrl}/notifications`}>
<Button icon={<SettingsIcon />} tag="a" size={ButtonSize.Medium} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ReferralCampaignKey, useGetShortUrl } from '../../../hooks';
import { PostContentWidget } from './PostContentWidget';
import { useActiveFeedContext } from '../../../contexts';
import { postLogEvent } from '../../../lib/feed';
import { ReferralGrowthSurface } from '../../../lib/referralGrowth';

interface PostContentShareProps {
post: Post;
Expand All @@ -34,7 +35,7 @@ export function PostContentShare({
return (
<PostContentWidget
className="mt-6"
title="Should anyone else see this post?"
title="Know a developer who should see this?"
>
<InviteLinkInput
className={{ container: 'w-full flex-1' }}
Expand All @@ -44,6 +45,7 @@ export function PostContentShare({
extra: {
provider: ShareProvider.CopyLink,
origin: Origin.PostContent,
surface: ReferralGrowthSurface.PostUpvote,
},
...(logOpts && logOpts),
})}
Expand Down
143 changes: 143 additions & 0 deletions packages/shared/src/components/referral/ContextualReferralLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import type { ReactElement } from 'react';
import React, { useEffect, useRef } from 'react';
import classNames from 'classnames';
import { Button, ButtonSize, ButtonVariant } from '../buttons/Button';
import {
Typography,
TypographyColor,
TypographyType,
} from '../typography/Typography';
import { useAuthContext } from '../../contexts/AuthContext';
import { useLogContext } from '../../contexts/LogContext';
import { useCopyLink } from '../../hooks/useCopy';
import { useGetShortUrl } from '../../hooks/utils/useGetShortUrl';
import type { ReferralCampaignKey } from '../../lib/referral';
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;
campaignKey: ReferralCampaignKey;
surface: ReferralGrowthSurface;
title: string;
description: string;
origin: Origin | string;
className?: string;
buttonText?: string;
};

export function ContextualReferralLink({
url,
campaignKey,
surface,
title,
description,
origin,
className,
buttonText = 'Copy link',
}: ContextualReferralLinkProps): ReactElement | null {
const { user } = useAuthContext();
const { logEvent } = useLogContext();
const loggedImpression = useRef(false);
const { getTrackedUrl, shareLink, isLoading } = useGetShortUrl({
query: {
url,
cid: campaignKey,
enabled: !!url && !!user?.id,
},
});
const trackedUrl = getTrackedUrl(url, campaignKey);
const link = shareLink || trackedUrl;
const [copied, copyLink] = useCopyLink(() => link);

useEffect(() => {
if (!user?.id || !url || loggedImpression.current) {
return;
}

logEvent({
event_name: LogEvent.ReferralPromptImpression,
target_type: TargetType.ReferralPrompt,
target_id: surface,
extra: JSON.stringify({
campaign: campaignKey,
origin,
}),
});

loggedImpression.current = true;
}, [campaignKey, logEvent, origin, surface, url, user?.id]);

if (!user?.id || !url) {
return null;
}

const onCopy = () => {
copyLink();
logEvent({
event_name: LogEvent.ReferralPromptClick,
target_type: TargetType.ReferralPrompt,
target_id: surface,
extra: JSON.stringify({
campaign: campaignKey,
origin,
provider: ShareProvider.CopyLink,
}),
});
};

return (
<div
className={classNames(
'border-accent-cabbage-default/20 from-accent-onion-default/5 to-accent-cabbage-default/10 relative overflow-hidden rounded-16 border bg-gradient-to-br via-surface-float p-4 text-left',
className,
)}
>
{/* Background glow */}
<div className="bg-accent-cabbage-default/15 pointer-events-none absolute -right-8 -top-8 h-24 w-24 rounded-full blur-2xl" />

<div className="relative flex flex-col gap-3">
{/* Icon + copy */}
<div className="flex items-start gap-3">
<span className="bg-accent-cabbage-default/15 flex h-9 w-9 shrink-0 items-center justify-center rounded-12 text-accent-cabbage-default">
<UserShareIcon size={IconSize.Small} />
</span>
<div className="flex flex-col gap-0.5">
<Typography bold type={TypographyType.Callout}>
{title}
</Typography>
<Typography
type={TypographyType.Footnote}
color={TypographyColor.Tertiary}
>
{description}
</Typography>
</div>
</div>

{/* URL display + inline copy button */}
<div className="flex items-center gap-2 rounded-10 border border-border-subtlest-tertiary bg-surface-primary py-1.5 pl-3 pr-1.5">
<span className="min-w-0 flex-1 truncate text-text-tertiary typo-caption1">
{isLoading ? 'Preparing your link…' : link ?? url}
</span>
<Button
size={ButtonSize.XSmall}
type="button"
variant={copied ? ButtonVariant.Primary : ButtonVariant.Secondary}
icon={
copied ? <VIcon secondary size={IconSize.XSmall} /> : undefined
}
onClick={onCopy}
disabled={isLoading || !link}
>
{copied ? 'Copied!' : buttonText}
</Button>
</div>
</div>
</div>
);
}
Loading