Skip to content

Commit bb3b29d

Browse files
authored
Merge pull request #23104 from Yoast/jpv/promote-aibi-free-trial-in-plugin
Add AIBI free trial promotion modal and Plans page changes
2 parents 70d1762 + c0c53c0 commit bb3b29d

File tree

7 files changed

+487
-13
lines changed

7 files changed

+487
-13
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { useSelect } from "@wordpress/data";
2+
import { createInterpolateElement, useMemo } from "@wordpress/element";
3+
import { __ } from "@wordpress/i18n";
4+
import { ExternalLinkIcon } from "@heroicons/react/outline";
5+
import { Button, Modal as UiModal, useSvgAria, useModalContext } from "@yoast/ui-library";
6+
import { STORE_NAME_INTRODUCTIONS } from "../../constants";
7+
import { Modal } from "../modal";
8+
9+
/**
10+
* @param {Object} thumbnail The thumbnail: img props.
11+
* @param {string} buttonLink The button link.
12+
* @returns {JSX.Element} The element.
13+
*/
14+
const AiBrandInsightsFreeTrialContent = ( {
15+
thumbnail,
16+
buttonLink,
17+
} ) => {
18+
const { onClose, initialFocus } = useModalContext();
19+
const svgAriaProps = useSvgAria();
20+
21+
return (
22+
<>
23+
<div className="yst-px-10 yst-pt-10 yst-introduction-gradient yst-text-center">
24+
<img
25+
className="yst-w-full yst-h-auto yst-rounded-md yst-drop-shadow-md"
26+
alt={ __( "Web chart showing aspects of brand visibility in AI responses", "wordpress-seo" ) }
27+
loading="lazy"
28+
decoding="async"
29+
{ ...thumbnail }
30+
/>
31+
<div className="yst-mt-6 yst-text-xs yst-font-medium yst-flex yst-flex-col yst-items-center">
32+
<span className="yst-introduction-modal-uppercase yst-flex yst-gap-2 yst-items-center">
33+
<span className="yst-ai-insights-icon" { ...svgAriaProps } />
34+
{ "Yoast AI Brand Insights" }
35+
</span>
36+
</div>
37+
</div>
38+
<div className="yst-px-10 yst-pb-4 yst-flex yst-flex-col yst-items-center">
39+
<div className="yst-mt-4 yst-mx-1.5 yst-text-center">
40+
<UiModal.Title as="h3" className="yst-text-slate-900 yst-text-lg yst-font-medium">
41+
{
42+
__( "Your first brand analysis is free!", "wordpress-seo" )
43+
}
44+
</UiModal.Title>
45+
<div className="yst-mt-2 yst-text-slate-600 yst-text-sm">
46+
<p>
47+
{ createInterpolateElement(
48+
__(
49+
"As a Yoast customer, you can run your first brand analysis for <strong>free</strong>. See how your brand shows up in AI responses.",
50+
"wordpress-seo"
51+
),
52+
{
53+
strong: <strong className="yst-font-semibold" />,
54+
}
55+
) }
56+
</p>
57+
</div>
58+
</div>
59+
<div className="yst-w-full yst-flex yst-mt-6">
60+
<Button
61+
as="a"
62+
className="yst-grow"
63+
size="extra-large"
64+
variant="ai-primary"
65+
href={ buttonLink }
66+
target="_blank"
67+
rel="noopener noreferrer"
68+
ref={ initialFocus }
69+
>
70+
{ __( "Start your free trial", "wordpress-seo" ) }
71+
<ExternalLinkIcon className="yst--me-1 yst-ms-1 yst-h-5 yst-w-5" { ...svgAriaProps } />
72+
<span className="yst-sr-only">
73+
{
74+
/* translators: Hidden accessibility text. */
75+
__( "(Opens in a new browser tab)", "wordpress-seo" )
76+
}
77+
</span>
78+
</Button>
79+
</div>
80+
<Button
81+
className="yst-mt-2"
82+
variant="tertiary"
83+
onClick={ onClose }
84+
>
85+
{ __( "Close", "wordpress-seo" ) }
86+
</Button>
87+
</div>
88+
</>
89+
);
90+
};
91+
92+
/**
93+
* @returns {JSX.Element} The element.
94+
*/
95+
export const AiBrandInsightsFreeTrial = () => {
96+
const imageLink = useSelect( select => select( STORE_NAME_INTRODUCTIONS ).selectImageLink( "ai-brand-insights-pre-launch.png" ), [] );
97+
const buttonLink = useSelect( select => select( STORE_NAME_INTRODUCTIONS )
98+
.selectLink( "https://yoa.st/aibi-introduction-free-trial" ), [] );
99+
const thumbnail = useMemo( () => ( {
100+
src: imageLink,
101+
width: "432",
102+
height: "243",
103+
} ), [ imageLink ] );
104+
105+
return (
106+
<Modal>
107+
<AiBrandInsightsFreeTrialContent
108+
buttonLink={ buttonLink }
109+
thumbnail={ thumbnail }
110+
/>
111+
</Modal>
112+
);
113+
};

packages/js/src/introductions/initialize.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Root } from "@yoast/ui-library";
77
import { get, isEmpty, find } from "lodash";
88
import { LINK_PARAMS_NAME, PLUGIN_URL_NAME, WISTIA_EMBED_PERMISSION_NAME } from "../shared-admin/store";
99
import { Introduction, IntroductionProvider } from "./components";
10+
import { AiBrandInsightsFreeTrial } from "./components/modals/ai-brand-insights-free-trial";
1011
import { AiBrandInsightsPostLaunch } from "./components/modals/ai-brand-insights-post-launch";
1112
import { BlackFridayAnnouncement } from "./components/modals/black-friday-announcement";
1213
import { DelayedPremiumUpsell } from "./components/modals/delayed-premium-upsell";
@@ -23,6 +24,7 @@ domReady( () => {
2324
}
2425

2526
const initialComponents = {
27+
"ai-brand-insights-free-trial": AiBrandInsightsFreeTrial,
2628
"ai-brand-insights-post-launch": AiBrandInsightsPostLaunch,
2729
"black-friday-announcement": BlackFridayAnnouncement,
2830
"delayed-premium-upsell": DelayedPremiumUpsell,

packages/js/src/plans/components/cards/ai-plus-card.js

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,74 @@
11
import { CheckCircleIcon } from "@heroicons/react/solid";
2-
import { Card, Title, useSvgAria } from "@yoast/ui-library";
2+
import { Badge, Card, Title, useSvgAria } from "@yoast/ui-library";
33
import { useSelect } from "@wordpress/data";
44
import { __, sprintf } from "@wordpress/i18n";
55
import { safeCreateInterpolateElement } from "../../../helpers/i18n";
6-
import { STORE_NAME } from "../../constants";
6+
import { ADD_ONS, STORE_NAME } from "../../constants";
77
import { ButtonLinkWithArrow } from "../actions/button-link-with-arrow";
8+
import { CardLink } from "../actions/card-link";
89
import { ReactComponent as AiPlusSvg } from "../../../../images/ai-plus-plans.svg";
910

11+
/**
12+
* Renders a badge at the top of the card when text is provided.
13+
*
14+
* @param {?string} text The badge text. If null, nothing is rendered.
15+
*
16+
* @returns {?JSX.Element} The badge element or null.
17+
*/
18+
const AiPlusCardBadge = ( { text } ) => {
19+
if ( ! text ) {
20+
return null;
21+
}
22+
23+
return (
24+
<div className="yst-absolute yst-top-0 yst--translate-y-1/2 yst-w-full yst-text-center">
25+
<Badge size="small" className="yst-border" variant="info">
26+
{ text }
27+
</Badge>
28+
</div>
29+
);
30+
};
31+
32+
/**
33+
* The footer actions for the AI+ card.
34+
*
35+
* @param {string} learnMoreLink The URL to learn more about the product.
36+
* @param {?string} buyLink An optional URL for the primary CTA button.
37+
* @param {?string} buyLabel An optional label for the primary CTA button.
38+
*
39+
* @returns {JSX.Element} The element.
40+
*/
41+
const AiPlusCardActions = ( { learnMoreLink, buyLink, buyLabel } ) => {
42+
if ( buyLink ) {
43+
return (
44+
<div className="yst-flex yst-flex-col yst-gap-y-1">
45+
<CardLink label={ buyLabel } href={ buyLink } />
46+
<ButtonLinkWithArrow
47+
variant="tertiary"
48+
className="yst-py-0 yst-mt-2"
49+
iconClassName="yst-ml-1.5"
50+
label={ __( "Learn more", "wordpress-seo" ) }
51+
href={ learnMoreLink }
52+
/>
53+
</div>
54+
);
55+
}
56+
57+
return <ButtonLinkWithArrow variant="primary" label={ __( "Learn more", "wordpress-seo" ) } href={ learnMoreLink } />;
58+
};
59+
1060
/**
1161
* A base card component for the Yoast AI+ plan.
1262
*
13-
* @param {React.ReactNode} header The header content of the card. An SVG is expected.
1463
* @param {string} title The title of the card, typically the product name.
1564
* @param {string} description The description of the product.
1665
* @param {string} [listDescription] An optional description for the list of features.
1766
* @param {string[]} list A list of features of the product.
1867
* @param {React.ReactNode} [includes] An optional section to specify the "Now includes" plugins.
1968
* @param {string} learnMoreLink The URL to learn more about the product.
69+
* @param {?string} badge An optional badge text to display at the top of the card.
70+
* @param {?string} buyLink An optional URL for the primary CTA button.
71+
* @param {?string} buyLabel An optional label for the primary CTA button.
2072
*
2173
* @returns {JSX.Element} The element.
2274
*/
@@ -27,6 +79,9 @@ const BaseAiPlusCard = ( {
2779
list,
2880
includes,
2981
learnMoreLink,
82+
badge = null,
83+
buyLink = null,
84+
buyLabel = null,
3085
} ) => {
3186
const svgAriaProps = useSvgAria();
3287

@@ -56,9 +111,10 @@ const BaseAiPlusCard = ( {
56111
<hr className="yst-mt-4 yst-mb-6 yst-border-t yst-border-slate-200" />
57112
</>
58113
) }
59-
<ButtonLinkWithArrow variant="primary" label={ __( "Learn more", "wordpress-seo" ) } href={ learnMoreLink } />
114+
<AiPlusCardActions learnMoreLink={ learnMoreLink } buyLink={ buyLink } buyLabel={ buyLabel } />
60115
</Card.Footer>
61116
</Card>
117+
<AiPlusCardBadge text={ badge } />
62118
</div>
63119
);
64120
};
@@ -68,11 +124,17 @@ const BaseAiPlusCard = ( {
68124
* @returns {JSX.Element} The element.
69125
*/
70126
export const AiPlusCard = () => {
71-
const learnMoreLink = useSelect( ( select ) => select( STORE_NAME ).selectLink( "https://yoa.st/plans-ai-plus-learn-more" ), [] );
127+
const { isPremiumActive, learnMoreLink, freeTrialLink } = useSelect( ( select ) => {
128+
const plansSelect = select( STORE_NAME );
129+
return {
130+
isPremiumActive: plansSelect.selectAddOnIsActive( ADD_ONS.premium ),
131+
learnMoreLink: plansSelect.selectLink( "https://yoa.st/plans-ai-plus-learn-more" ),
132+
freeTrialLink: plansSelect.selectLink( "https://yoa.st/aibi-plans-free-trial" ),
133+
};
134+
}, [] );
72135

73136
return (
74137
<BaseAiPlusCard
75-
header={ <AiPlusSvg /> }
76138
title="Yoast SEO AI+"
77139
description={
78140
__( "For marketers and content publishers looking to boost brand awareness and visibility, this package combines powerful on page SEO tools with AI brand monitoring and insights.", "wordpress-seo" )
@@ -93,6 +155,9 @@ export const AiPlusCard = () => {
93155
] }
94156
learnMoreLink={ learnMoreLink }
95157
includes="Local SEO + Video SEO + News SEO"
158+
badge={ isPremiumActive ? __( "Free trial available", "wordpress-seo" ) : null }
159+
buyLink={ isPremiumActive ? freeTrialLink : null }
160+
buyLabel={ isPremiumActive ? __( "Start your free trial", "wordpress-seo" ) : null }
96161
/>
97162
);
98163
};
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
namespace Yoast\WP\SEO\Introductions\Application;
4+
5+
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
6+
use Yoast\WP\SEO\Helpers\Product_Helper;
7+
use Yoast\WP\SEO\Introductions\Domain\Introduction_Interface;
8+
9+
/**
10+
* Represents the introduction for the AI Brand Insights free trial.
11+
*/
12+
class AI_Brand_Insights_Free_Trial implements Introduction_Interface {
13+
14+
use User_Allowed_Trait;
15+
16+
public const ID = 'ai-brand-insights-free-trial';
17+
18+
/**
19+
* Holds the current page helper.
20+
*
21+
* @var Current_Page_Helper
22+
*/
23+
private $current_page_helper;
24+
25+
/**
26+
* Holds the product helper.
27+
*
28+
* @var Product_Helper
29+
*/
30+
private $product_helper;
31+
32+
/**
33+
* Constructs the introduction.
34+
*
35+
* @param Current_Page_Helper $current_page_helper The current page helper.
36+
* @param Product_Helper $product_helper The product helper.
37+
*/
38+
public function __construct(
39+
Current_Page_Helper $current_page_helper,
40+
Product_Helper $product_helper
41+
) {
42+
$this->current_page_helper = $current_page_helper;
43+
$this->product_helper = $product_helper;
44+
}
45+
46+
/**
47+
* Returns the ID.
48+
*
49+
* @return string The ID.
50+
*/
51+
public function get_id() {
52+
return self::ID;
53+
}
54+
55+
/**
56+
* Returns the requested pagination priority. Lower means earlier.
57+
*
58+
* @return int The priority.
59+
*/
60+
public function get_priority() {
61+
return 20;
62+
}
63+
64+
/**
65+
* Returns whether this introduction should show.
66+
*
67+
* @return bool Whether this introduction should show.
68+
*/
69+
public function should_show() {
70+
return $this->current_page_helper->is_yoast_seo_page() && $this->product_helper->is_premium();
71+
}
72+
}

src/introductions/application/ai-brand-insights-post-launch.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Yoast\WP\SEO\Introductions\Application;
44

55
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
6+
use Yoast\WP\SEO\Helpers\Product_Helper;
67
use Yoast\WP\SEO\Introductions\Domain\Introduction_Interface;
78

89
/**
@@ -21,13 +22,25 @@ class AI_Brand_Insights_Post_Launch implements Introduction_Interface {
2122
*/
2223
private $current_page_helper;
2324

25+
/**
26+
* Holds the product helper.
27+
*
28+
* @var Product_Helper
29+
*/
30+
private $product_helper;
31+
2432
/**
2533
* Constructs the introduction.
2634
*
2735
* @param Current_Page_Helper $current_page_helper The current page helper.
36+
* @param Product_Helper $product_helper The product helper.
2837
*/
29-
public function __construct( Current_Page_Helper $current_page_helper ) {
38+
public function __construct(
39+
Current_Page_Helper $current_page_helper,
40+
Product_Helper $product_helper
41+
) {
3042
$this->current_page_helper = $current_page_helper;
43+
$this->product_helper = $product_helper;
3144
}
3245

3346
/**
@@ -54,6 +67,6 @@ public function get_priority() {
5467
* @return bool Whether this introduction should show.
5568
*/
5669
public function should_show() {
57-
return $this->current_page_helper->is_yoast_seo_page();
70+
return $this->current_page_helper->is_yoast_seo_page() && ! $this->product_helper->is_premium();
5871
}
5972
}

0 commit comments

Comments
 (0)