Skip to content

Commit 18a0a32

Browse files
feat(funnels): CTA micro-liners to fact screen (#4502)
Co-authored-by: Lee Hansel Solevilla <[email protected]> Co-authored-by: Lee Hansel Solevilla <[email protected]>
1 parent b6370b4 commit 18a0a32

File tree

9 files changed

+205
-30
lines changed

9 files changed

+205
-30
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import classNames from 'classnames';
2+
import type { ReactNode } from 'react';
3+
import React from 'react';
4+
import {
5+
Typography,
6+
TypographyTag,
7+
TypographyType,
8+
} from './typography/Typography';
9+
10+
type BadgeProps = {
11+
icon?: ReactNode;
12+
label: string;
13+
variant: 'primary' | 'onion';
14+
};
15+
16+
const variantToClassName: Record<BadgeProps['variant'], string> = {
17+
primary: 'border-border-subtlest-secondary text-surface-primary',
18+
onion: 'border-overlay-secondary-onion text-accent-onion-subtler',
19+
};
20+
21+
export const Badge = ({ label, icon, variant }: BadgeProps) => {
22+
return (
23+
<div
24+
className={classNames(
25+
variantToClassName[variant],
26+
'flex items-center gap-1 rounded-20 border px-3 py-2',
27+
)}
28+
>
29+
{icon}
30+
<Typography type={TypographyType.Body} bold tag={TypographyTag.P}>
31+
{label}
32+
</Typography>
33+
</div>
34+
);
35+
};

packages/shared/src/features/onboarding/shared/FunnelStepCtaWrapper.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ReactElement } from 'react';
2-
import React from 'react';
2+
import React, { useMemo } from 'react';
33
import classNames from 'classnames';
44
import type { ButtonProps } from '../../../components/buttons/Button';
55
import {
@@ -10,10 +10,17 @@ import {
1010
} from '../../../components/buttons/Button';
1111
import { FunnelTargetId } from '../types/funnelEvents';
1212
import { MoveToIcon } from '../../../components/icons';
13+
import {
14+
Typography,
15+
TypographyColor,
16+
TypographyType,
17+
} from '../../../components/typography/Typography';
18+
import { sanitizeMessage } from './utils';
1319

1420
export type FunnelStepCtaWrapperProps = ButtonProps<'button'> & {
1521
cta?: {
1622
label?: string;
23+
note?: string;
1724
};
1825
containerClassName?: string;
1926
skip?: ButtonProps<'button'> & {
@@ -29,10 +36,28 @@ export function FunnelStepCtaWrapper({
2936
containerClassName,
3037
...props
3138
}: FunnelStepCtaWrapperProps): ReactElement {
39+
const note = useMemo(() => {
40+
if (!cta?.note) {
41+
return null;
42+
}
43+
44+
const sanitized = sanitizeMessage(cta.note);
45+
46+
return (
47+
<Typography
48+
className="text-center"
49+
type={TypographyType.Title3}
50+
color={TypographyColor.Primary}
51+
dangerouslySetInnerHTML={{ __html: sanitized }}
52+
/>
53+
);
54+
}, [cta?.note]);
55+
3256
return (
3357
<div className="relative flex flex-1 flex-col gap-4">
3458
<div className={classNames('flex-1', containerClassName)}>{children}</div>
3559
<div className="sticky bottom-2 m-4 flex flex-col gap-4">
60+
{note}
3661
<Button
3762
className={classNames(className, 'w-full')}
3863
data-funnel-track={FunnelTargetId.StepCta}

packages/shared/src/features/onboarding/shared/StepHeadline.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const StepHeadline = ({
3737
return (
3838
<div
3939
data-testid="step-headline-container"
40-
className={classNames('flex flex-col gap-2', className, {
40+
className={classNames('flex flex-col gap-4', className, {
4141
'text-left': align === StepHeadlineAlign.Left,
4242
'text-center': align === StepHeadlineAlign.Center,
4343
'text-right': align === StepHeadlineAlign.Right,

packages/shared/src/features/onboarding/steps/FunnelFact/FunnelFact.spec.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { render, screen, fireEvent } from '@testing-library/react';
33
import { FunnelFact } from '.';
44
import { FunnelStepType } from '../../types/funnel';
55
import type { FunnelStepFact } from '../../types/funnel';
6+
import { StepHeadlineAlign } from '../../shared';
67

78
const mockOnTransition = jest.fn();
89

@@ -13,7 +14,7 @@ const defaultProps: FunnelStepFact = {
1314
parameters: {
1415
headline: 'Test Headline',
1516
explainer: 'Test explanation text',
16-
align: 'center',
17+
align: StepHeadlineAlign.Center,
1718
cta: 'Continue',
1819
},
1920
onTransition: mockOnTransition,
@@ -36,6 +37,21 @@ describe('FunnelFact', () => {
3637
).toBeInTheDocument();
3738
});
3839

40+
it('should render badge when provided', async () => {
41+
renderComponent({
42+
parameters: {
43+
...defaultProps.parameters,
44+
badge: {
45+
cta: 'Badge CTA',
46+
variant: 'primary',
47+
placement: 'top',
48+
},
49+
},
50+
});
51+
52+
expect(await screen.findByText('Badge CTA')).toBeInTheDocument();
53+
});
54+
3955
it('should call onTransition when button is clicked', async () => {
4056
renderComponent();
4157
const button = await screen.findByText('Continue');
@@ -85,7 +101,7 @@ describe('FunnelFact', () => {
85101
renderComponent({
86102
parameters: {
87103
...defaultProps.parameters,
88-
align: 'left',
104+
align: StepHeadlineAlign.Left,
89105
},
90106
});
91107

packages/shared/src/features/onboarding/steps/FunnelFact/FunnelFactCentered.tsx

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,27 @@ import {
1111
ButtonVariant,
1212
ButtonIconPosition,
1313
} from '../../../../components/buttons/common';
14-
import { MoveToIcon } from '../../../../components/icons';
14+
import {
15+
MoveToIcon,
16+
ReputationLightningIcon,
17+
} from '../../../../components/icons';
1518
import { FunnelTargetId } from '../../types/funnelEvents';
19+
import { Badge } from '../../../../components/Badge';
1620

1721
export const FunnelFactCentered = (props: FunnelStepFact): ReactElement => {
1822
const { parameters, transitions, onTransition } = props;
23+
const { badge, headline, explainer, align, visualUrl } = parameters;
1924
const skip = useMemo(
2025
() => transitions.find((t) => t.on === FunnelStepTransitionType.Skip),
2126
[transitions],
2227
);
28+
const badgeComponent = !badge?.cta ? null : (
29+
<Badge
30+
label={badge.cta}
31+
icon={<ReputationLightningIcon className="h-6 w-6" secondary />}
32+
variant={badge.variant}
33+
/>
34+
);
2335

2436
return (
2537
<FunnelFactWrapper {...props}>
@@ -42,27 +54,30 @@ export const FunnelFactCentered = (props: FunnelStepFact): ReactElement => {
4254
{skip?.cta ?? 'Skip'}
4355
</Button>
4456
)}
45-
{parameters?.visualUrl && (
57+
{visualUrl && (
4658
<>
4759
<Head>
48-
<link rel="preload" as="image" href={parameters.visualUrl} />
60+
<link rel="preload" as="image" href={visualUrl} />
4961
</Head>
5062
<LazyImage
5163
aria-hidden
5264
eager
53-
imgSrc={parameters?.visualUrl}
65+
imgSrc={visualUrl}
5466
className="max-h-[25rem] w-full flex-1"
5567
imgAlt="Supportive illustration for the information"
5668
fit="contain"
5769
/>
5870
</>
5971
)}
60-
<StepHeadline
61-
className="!gap-6"
62-
heading={parameters?.headline}
63-
description={parameters?.explainer}
64-
align={parameters?.align}
65-
/>
72+
<div className="flex flex-col items-center gap-4">
73+
{badge?.placement === 'top' && badgeComponent}
74+
<StepHeadline
75+
heading={headline}
76+
description={explainer}
77+
align={align}
78+
/>
79+
{badge?.placement === 'bottom' && badgeComponent}
80+
</div>
6681
</div>
6782
</FunnelFactWrapper>
6883
);

packages/shared/src/features/onboarding/steps/FunnelFact/FunnelFactDefault.tsx

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,24 @@ import { StepHeadline } from '../../shared';
66
import type { FunnelStepFact } from '../../types/funnel';
77
import { FunnelStepTransitionType } from '../../types/funnel';
88
import { LazyImage } from '../../../../components/LazyImage';
9+
import { Badge } from '../../../../components/Badge';
10+
import {
11+
ReputationLightningIcon,
12+
MoveToIcon,
13+
} from '../../../../components/icons';
914
import { FunnelFactWrapper } from './FunnelFactWrapper';
1015
import { Button } from '../../../../components/buttons/Button';
1116
import {
1217
ButtonVariant,
1318
ButtonIconPosition,
1419
} from '../../../../components/buttons/common';
15-
import { MoveToIcon } from '../../../../components/icons';
1620
import { FunnelTargetId } from '../../types/funnelEvents';
1721

1822
export const FunnelFactDefault = (props: FunnelStepFact): ReactElement => {
1923
const { parameters, transitions, onTransition } = props;
20-
const isLayoutReversed =
21-
parameters.layout === 'reversed' || parameters.reverse;
24+
const { badge, headline, explainer, align, reverse, layout, visualUrl } =
25+
parameters;
26+
const isLayoutReversed = layout === 'reversed' || reverse;
2227
const skip = useMemo(
2328
() => transitions.find((t) => t.on === FunnelStepTransitionType.Skip),
2429
[transitions],
@@ -37,6 +42,14 @@ export const FunnelFactDefault = (props: FunnelStepFact): ReactElement => {
3742
</Button>
3843
);
3944

45+
const badgeComponent = !badge?.cta ? null : (
46+
<Badge
47+
label={badge.cta}
48+
icon={<ReputationLightningIcon className="h-6 w-6" secondary />}
49+
variant={badge.variant}
50+
/>
51+
);
52+
4053
return (
4154
<FunnelFactWrapper {...props}>
4255
<div
@@ -48,21 +61,25 @@ export const FunnelFactDefault = (props: FunnelStepFact): ReactElement => {
4861
: 'flex-col justify-between',
4962
)}
5063
>
51-
{skip?.placement === 'top' && !isLayoutReversed && skipButton}
52-
<StepHeadline
53-
heading={parameters?.headline}
54-
description={parameters?.explainer}
55-
align={parameters?.align}
56-
/>
57-
{parameters?.visualUrl && (
64+
<div className="flex flex-col items-center gap-4">
65+
{badge?.placement === 'top' && badgeComponent}
66+
{skip?.placement === 'top' && !isLayoutReversed && skipButton}
67+
<StepHeadline
68+
heading={headline}
69+
description={explainer}
70+
align={align}
71+
/>
72+
{badge?.placement === 'bottom' && badgeComponent}
73+
</div>
74+
{visualUrl && (
5875
<>
5976
<Head>
60-
<link rel="preload" as="image" href={parameters.visualUrl} />
77+
<link rel="preload" as="image" href={visualUrl} />
6178
</Head>
6279
<LazyImage
6380
aria-hidden
6481
eager
65-
imgSrc={parameters?.visualUrl}
82+
imgSrc={visualUrl}
6683
className="h-auto w-full object-cover"
6784
ratio="64%"
6885
imgAlt="Supportive illustration for the information"

packages/shared/src/features/onboarding/steps/FunnelFact/FunnelFactWrapper.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const FunnelFactWrapper = ({
99
...props
1010
}: PropsWithChildren<FunnelStepFact>) => {
1111
const { parameters, onTransition, transitions } = props;
12+
const { cta, ctaNote } = parameters;
1213
const skip = useMemo(
1314
() => transitions.find((t) => t.on === FunnelStepTransitionType.Skip),
1415
[transitions],
@@ -25,7 +26,7 @@ export const FunnelFactWrapper = ({
2526
return (
2627
<FunnelStepCtaWrapper
2728
containerClassName="flex"
28-
cta={{ label: parameters?.cta ?? 'Next' }}
29+
cta={{ label: cta ?? 'Next', note: ctaNote }}
2930
onClick={() =>
3031
onTransition({
3132
type: FunnelStepTransitionType.Complete,

packages/shared/src/features/onboarding/types/funnel.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,13 @@ export interface FunnelStepLoading
110110
export interface FunnelStepFactParameters {
111111
headline: string;
112112
cta?: string;
113+
ctaNote?: string;
113114
reverse?: boolean;
115+
badge?: {
116+
placement?: 'bottom' | 'top';
117+
cta?: string;
118+
variant?: 'primary' | 'onion';
119+
};
114120
explainer: string;
115121
align: StepHeadlineAlign;
116122
visualUrl?: string;

0 commit comments

Comments
 (0)