Skip to content

Commit 1efced8

Browse files
authored
feat: api-first Plus landing experiment (featurePlusApiLanding) (#5888)
1 parent 601b9da commit 1efced8

File tree

17 files changed

+335
-63
lines changed

17 files changed

+335
-63
lines changed

packages/shared/src/components/CustomFeedEmptyScreen.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,16 @@ import { LogEvent, TargetId } from '../lib/log';
2121
import { Button } from './buttons/Button';
2222
import { useConditionalFeature, usePlusSubscription } from '../hooks';
2323
import { IconSize } from './Icon';
24-
import { featurePlusCtaCopy } from '../lib/featureManagement';
24+
import { featurePlusApiLanding } from '../lib/featureManagement';
2525
import Link from './utilities/Link';
2626

2727
export const CustomFeedEmptyScreen = (): ReactElement => {
2828
const { logSubscriptionEvent, isPlus } = usePlusSubscription();
29-
const {
30-
value: { full: plusCta },
31-
} = useConditionalFeature({
32-
feature: featurePlusCtaCopy,
29+
const { value: isApiLanding } = useConditionalFeature({
30+
feature: featurePlusApiLanding,
3331
shouldEvaluate: !isPlus,
3432
});
33+
const plusCta = isApiLanding ? 'Get API Access' : 'Level Up with Plus';
3534
const [selectedAlgo, setSelectedAlgo] = usePersistentContext(
3635
DEFAULT_ALGORITHM_KEY,
3736
DEFAULT_ALGORITHM_INDEX,

packages/shared/src/components/PlusUserBadge.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { DateFormat } from './utilities';
1515
import { TimeFormatType } from '../lib/dateFormat';
1616
import { usePlusSubscription } from '../hooks/usePlusSubscription';
1717
import { LogEvent, TargetId } from '../lib/log';
18-
import { featurePlusCtaCopy } from '../lib/featureManagement';
18+
import { featurePlusApiLanding } from '../lib/featureManagement';
1919
import { useConditionalFeature } from '../hooks';
2020
import { IconSize } from './Icon';
2121

@@ -31,12 +31,11 @@ export const PlusUserBadge = ({
3131
size = IconSize.Size16,
3232
}: Props): ReactElement | null => {
3333
const { isPlus, logSubscriptionEvent } = usePlusSubscription();
34-
const {
35-
value: { full: plusCta },
36-
} = useConditionalFeature({
37-
feature: featurePlusCtaCopy,
34+
const { value: isApiLanding } = useConditionalFeature({
35+
feature: featurePlusApiLanding,
3836
shouldEvaluate: !isPlus,
3937
});
38+
const plusCta = isApiLanding ? 'Get API Access' : 'Level Up with Plus';
4039

4140
if (!user.isPlus) {
4241
return null;

packages/shared/src/components/UpgradeToPlus.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { LogEvent } from '../lib/log';
1313
import { useAuthContext } from '../contexts/AuthContext';
1414
import { AuthTriggers } from '../lib/auth';
1515
import type { WithClassNameProps } from './utilities';
16-
import { featurePlusCtaCopy } from '../lib/featureManagement';
16+
import { featurePlusApiLanding } from '../lib/featureManagement';
1717

1818
type Props = {
1919
iconOnly?: boolean;
@@ -37,11 +37,15 @@ export const UpgradeToPlus = ({
3737
const isLaptopXL = useViewSize(ViewSize.LaptopXL);
3838
const isFullCTAText = !isLaptop || isLaptopXL;
3939
const { isPlus, logSubscriptionEvent } = usePlusSubscription();
40-
const { value: ctaCopy } = useConditionalFeature({
41-
feature: featurePlusCtaCopy,
40+
const { value: isApiLanding } = useConditionalFeature({
41+
feature: featurePlusApiLanding,
4242
shouldEvaluate: !isPlus,
4343
});
44+
const ctaCopy = isApiLanding
45+
? { full: 'Get API Access', short: 'API access' }
46+
: { full: 'Level Up with Plus', short: 'Upgrade' };
4447
const content = isFullCTAText ? ctaCopy.full : ctaCopy.short;
48+
const defaultColor = isApiLanding ? ButtonColor.Bacon : ButtonColor.Avocado;
4549

4650
const onClick = useCallback(
4751
(e: React.MouseEvent) => {
@@ -70,7 +74,7 @@ export const UpgradeToPlus = ({
7074
className={classNames(!iconOnly && 'flex-1', className)}
7175
icon={<DevPlusIcon />}
7276
size={size}
73-
color={ButtonColor.Avocado}
77+
color={defaultColor}
7478
variant={ButtonVariant.Primary}
7579
onClick={onClick}
7680
{...(variant && { variant, color })}

packages/shared/src/components/banners/PlusMobileEntryBanner.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import type { TargetType } from '../../lib/log';
1414
import { LogEvent } from '../../lib/log';
1515
import { useLogContext } from '../../contexts/LogContext';
1616
import { useBoot } from '../../hooks';
17+
import { useFeature } from '../GrowthBookProvider';
18+
import { featurePlusApiLanding } from '../../lib/featureManagement';
1719

1820
type PlusBannerProps = Omit<MarketingCta, 'flags'> & {
1921
targetType: TargetType;
@@ -31,10 +33,12 @@ const PlusMobileEntryBanner = ({
3133
}: PlusBannerProps): ReactElement | null => {
3234
const { logEvent } = useLogContext();
3335
const { clearMarketingCta } = useBoot();
36+
const isApiLanding = useFeature(featurePlusApiLanding);
3437
if (!flags) {
3538
return null;
3639
}
3740
const { leadIn, description, ctaText, ctaUrl } = flags;
41+
const ctaColor = isApiLanding ? ButtonColor.Bacon : ButtonColor.Avocado;
3842

3943
const handleClose = () => {
4044
logEvent({
@@ -85,7 +89,7 @@ const PlusMobileEntryBanner = ({
8589
tag="a"
8690
href={ctaUrl || '/plus'}
8791
variant={ButtonVariant.Primary}
88-
color={ButtonColor.Avocado}
92+
color={ctaColor}
8993
onClick={handleClick}
9094
>
9195
<Typography

packages/shared/src/components/cards/plus/PlusGrid.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { useBoot } from '../../../hooks';
1313
import { LogEvent, TargetType } from '../../../lib/log';
1414
import { useLogContext } from '../../../contexts/LogContext';
1515
import { PlusItemStatus, PlusListItem } from '../../plus/PlusListItem';
16+
import { useFeature } from '../../GrowthBookProvider';
17+
import { featurePlusApiLanding } from '../../../lib/featureManagement';
1618

1719
const bulletPointsControl = [
1820
{
@@ -40,11 +42,13 @@ const bulletPointsControl = [
4042
const PlusGrid = ({ flags, campaignId }: MarketingCta) => {
4143
const { logEvent } = useLogContext();
4244
const { clearMarketingCta } = useBoot();
45+
const isApiLanding = useFeature(featurePlusApiLanding);
4346

4447
if (!flags) {
4548
return null;
4649
}
4750
const { title, description, ctaText, ctaUrl } = flags;
51+
const ctaColor = isApiLanding ? ButtonColor.Bacon : ButtonColor.Avocado;
4852

4953
const handleClose = () => {
5054
logEvent({
@@ -109,7 +113,7 @@ const PlusGrid = ({ flags, campaignId }: MarketingCta) => {
109113
tag="a"
110114
href={ctaUrl || '/plus'}
111115
variant={ButtonVariant.Primary}
112-
color={ButtonColor.Avocado}
116+
color={ctaColor}
113117
onClick={handleClick}
114118
>
115119
{ctaText}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import type { ComponentType, ReactElement } from 'react';
2+
import React, { useId } from 'react';
3+
import {
4+
Typography,
5+
TypographyColor,
6+
TypographyTag,
7+
TypographyType,
8+
} from '../typography/Typography';
9+
import { Button, ButtonSize, ButtonVariant } from '../buttons/Button';
10+
import { anchorDefaultRel } from '../../lib/strings';
11+
import { plusPublicApiDocs } from '../../lib/constants';
12+
import type { IconProps } from '../Icon';
13+
import { IconSize } from '../Icon';
14+
import { AiIcon, MagicIcon, TerminalIcon } from '../icons';
15+
16+
type ShowcaseCard = {
17+
title: string;
18+
body: string;
19+
icon: ComponentType<IconProps>;
20+
iconClasses: string;
21+
};
22+
23+
const showcaseCards: Array<ShowcaseCard> = [
24+
{
25+
title: 'Keep your coding agent current',
26+
body: `LLMs stop learning the day they ship. Wire daily.dev in so your agent can reference current libraries, recent CVEs, and what senior devs are actually reading. Pre-built integrations for Claude Code, Cursor, Codex, and OpenClaw.`,
27+
icon: AiIcon,
28+
iconClasses: 'bg-overlay-float-water text-accent-water-default',
29+
},
30+
{
31+
title: `Ship the internal tool you've been putting off`,
32+
body: `The Slack bot for #engineering. The weekly team digest. The tech radar dashboard. Whatever's been sitting in your Notes doc is a few endpoints away.`,
33+
icon: TerminalIcon,
34+
iconClasses: 'bg-overlay-float-bun text-accent-bun-default',
35+
},
36+
{
37+
title: 'Automate your own reading exactly how you want it',
38+
body: `Mirror your personalized feed to Notion, Obsidian, or email. Get alerts when the topics you follow start trending. Your workflow, no copy-paste.`,
39+
icon: MagicIcon,
40+
iconClasses: 'bg-overlay-float-cabbage text-accent-cabbage-default',
41+
},
42+
];
43+
44+
const ShowcaseCardView = ({ card }: { card: ShowcaseCard }): ReactElement => {
45+
const { icon: Icon, iconClasses } = card;
46+
47+
return (
48+
<div className="flex flex-1 flex-col rounded-16 border border-border-subtlest-tertiary bg-surface-float p-6">
49+
<div
50+
className={`mb-4 flex size-12 items-center justify-center rounded-12 ${iconClasses}`}
51+
>
52+
<Icon secondary size={IconSize.Medium} />
53+
</div>
54+
<Typography
55+
bold
56+
className="mb-2"
57+
color={TypographyColor.Primary}
58+
tag={TypographyTag.H3}
59+
type={TypographyType.Title3}
60+
>
61+
{card.title}
62+
</Typography>
63+
<Typography
64+
color={TypographyColor.Tertiary}
65+
type={TypographyType.Callout}
66+
>
67+
{card.body}
68+
</Typography>
69+
</div>
70+
);
71+
};
72+
73+
export const PlusApiShowcase = (): ReactElement => {
74+
const id = useId();
75+
const titleId = `${id}-title`;
76+
77+
return (
78+
<section aria-labelledby={titleId} className="my-10">
79+
<Typography
80+
bold
81+
className="mb-10 text-center"
82+
id={titleId}
83+
tag={TypographyTag.H2}
84+
type={TypographyType.Title3}
85+
>
86+
What you can build with the API
87+
</Typography>
88+
<div className="mx-auto flex max-w-5xl flex-col gap-4 laptop:flex-row">
89+
{showcaseCards.map((card) => (
90+
<ShowcaseCardView key={card.title} card={card} />
91+
))}
92+
</div>
93+
<div className="mt-8 flex justify-center">
94+
<Button
95+
tag="a"
96+
href={plusPublicApiDocs}
97+
target="_blank"
98+
rel={anchorDefaultRel}
99+
size={ButtonSize.Medium}
100+
variant={ButtonVariant.Secondary}
101+
>
102+
Read the API docs
103+
</Button>
104+
</div>
105+
</section>
106+
);
107+
};

packages/shared/src/components/plus/PlusDesktop.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@ import { usePlusSubscription } from '../../hooks';
1313

1414
import { PurchaseType } from '../../graphql/paddle';
1515
import { PlusProductToggle } from './PlusProductToggle';
16+
import { useFeature } from '../GrowthBookProvider';
17+
import { featurePlusApiLanding } from '../../lib/featureManagement';
1618

1719
const PlusFAQs = dynamic(() => import('./PlusFAQ').then((mod) => mod.PlusFAQ));
20+
const PlusApiShowcase = dynamic(() =>
21+
import('./PlusApiShowcase').then((mod) => mod.PlusApiShowcase),
22+
);
1823

1924
export const PlusDesktop = ({
2025
shouldShowPlusHeader,
@@ -32,14 +37,15 @@ export const PlusDesktop = ({
3237
query: { selectedPlan },
3338
} = useRouter();
3439
const { isPlus } = usePlusSubscription();
40+
const isApiLanding = useFeature(featurePlusApiLanding);
3541
const initialPaymentOption = selectedPlan ? `${selectedPlan}` : null;
3642
const [selectedOption, setSelectedOption] = useState<string | null>(null);
37-
const ref = useRef();
43+
const ref = useRef<HTMLDivElement>(null);
3844

3945
const onChangeCheckoutOption: OpenCheckoutFn = useCallback(
4046
({ priceId, giftToUserId, quantity }) => {
4147
setSelectedOption(priceId);
42-
openCheckout({ priceId, giftToUserId, quantity });
48+
openCheckout?.({ priceId, giftToUserId, quantity });
4349
},
4450
[openCheckout],
4551
);
@@ -56,7 +62,7 @@ export const PlusDesktop = ({
5662

5763
const { priceId } = giftOneYear;
5864
setSelectedOption(priceId);
59-
openCheckout({ priceId, giftToUserId: giftToUser.id });
65+
openCheckout?.({ priceId, giftToUserId: giftToUser.id });
6066

6167
return;
6268
}
@@ -66,7 +72,7 @@ export const PlusDesktop = ({
6672
// Auto-select if user is not plus or it is organization checkout
6773
if (option && (!isPlus || isOrganization)) {
6874
setSelectedOption(option);
69-
openCheckout({ priceId: option });
75+
openCheckout?.({ priceId: option });
7076
}
7177
}, [
7278
giftOneYear,
@@ -98,7 +104,7 @@ export const PlusDesktop = ({
98104
/>
99105
)}
100106
<PlusInfo
101-
productOptions={productOptions}
107+
productOptions={productOptions ?? []}
102108
selectedOption={selectedOption}
103109
onChange={onChangeCheckoutOption}
104110
shouldShowPlusHeader={shouldShowPlusHeader}
@@ -120,6 +126,7 @@ export const PlusDesktop = ({
120126
)}
121127
</div>
122128
</div>
129+
{isApiLanding && !isOrganization && !giftToUser && <PlusApiShowcase />}
123130
<PlusFAQs />
124131
</>
125132
);

packages/shared/src/components/plus/PlusFAQ.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ import {
99
import { Accordion } from '../accordion';
1010
import { anchorDefaultRel } from '../../lib/strings';
1111
import { feedback } from '../../lib/constants';
12-
import { plusFAQItems } from './common';
12+
import { plusFAQItemsApi, plusFAQItemsControl } from './common';
1313
import { useLogContext } from '../../contexts/LogContext';
1414
import { LogEvent } from '../../lib/log';
15+
import { useConditionalFeature } from '../../hooks';
16+
import { featurePlusApiLanding } from '../../lib/featureManagement';
17+
import { usePlusSubscription } from '../../hooks/usePlusSubscription';
1518

1619
interface FAQ {
1720
question: string;
@@ -50,6 +53,12 @@ const FAQItem = ({ item }: { item: FAQ }): ReactElement => {
5053
export const PlusFAQ = (): ReactElement => {
5154
const id = useId();
5255
const titleId = `${id}-title`;
56+
const { isPlus } = usePlusSubscription();
57+
const { value: isApiLanding } = useConditionalFeature({
58+
feature: featurePlusApiLanding,
59+
shouldEvaluate: !isPlus,
60+
});
61+
const items = isApiLanding ? plusFAQItemsApi : plusFAQItemsControl;
5362
return (
5463
<section aria-labelledby={titleId} className="my-10">
5564
<Typography
@@ -62,7 +71,7 @@ export const PlusFAQ = (): ReactElement => {
6271
Frequently asked questions
6372
</Typography>
6473
<div className="mx-auto flex max-w-3xl flex-col gap-4">
65-
{plusFAQItems.map((item) => (
74+
{items.map((item) => (
6675
<FAQItem key={item.question} item={item} />
6776
))}
6877
</div>

0 commit comments

Comments
 (0)