Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

My Jetpack: Custom trigger and docs for Interstitial modal #41621

Merged
merged 10 commits into from
Feb 26, 2025
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { default, default as ProductInterstitialModal } from './product-interstitial-modal';
export { default as ProductInterstitialPlugin } from './product-interstitial-plugin';
export { default as ProductInterstitialMyJetpack } from './product-interstitial-my-jetpack';
export { default as ProductInterstitialFeatureList } from './product-interstifial-feature-list';
export { default as ProductInterstitialModalCta } from './product-interstitial-modal-cta';
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,20 @@ import styles from './style.module.scss';

interface ProductInterstitialModalCtaProps {
slug: string;
buttonLabel?: string;
disabled?: boolean;
isExternalLink?: boolean;
href?: string;
}

// Component to handle the CTA for the product upgrades
const ProductInterstitialModalCta: FC< ProductInterstitialModalCtaProps > = ( { slug } ) => {
const ProductInterstitialModalCta: FC< ProductInterstitialModalCtaProps > = ( {
slug,
buttonLabel,
disabled,
isExternalLink,
href,
} ) => {
const quantity = null;

const {
Expand Down Expand Up @@ -68,8 +78,11 @@ const ProductInterstitialModalCta: FC< ProductInterstitialModalCtaProps > = ( {
className={ styles[ 'action-button' ] }
isLoading={ hasMainCheckoutStarted }
onClick={ mainCheckoutRedirect }
isExternalLink={ isExternalLink }
href={ href }
disabled={ disabled }
>
{ __( 'Upgrade', 'jetpack-my-jetpack' ) }
{ buttonLabel || __( 'Upgrade', 'jetpack-my-jetpack' ) }
</Button>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { Text, Button, ThemeProvider, Col, Container } from '@automattic/jetpack
import { Modal } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import clsx from 'clsx';
import { useCallback, useState, type FC } from 'react';
import React from 'react';
import { useCallback, useState, cloneElement, type FC } from 'react';
import styles from './style.module.scss';

interface BaseProductInterstitialModalProps {
Expand All @@ -15,14 +14,18 @@ interface BaseProductInterstitialModalProps {
* Description of the modal
*/
description?: string;
/**
* Custom trigger component to replace default button. It also handles the onOpen callback like the regular button.
*/
customModalTrigger?: React.ReactElement;
/**
* Trigger button of the modal
*/
triggerButton?: React.ReactNode;
modalTriggerButtonLabel?: string;
/**
* Variant of the trigger button
*/
triggerButtonVariant?: 'primary' | 'secondary';
modalTriggerButtonVariant?: 'primary' | 'secondary';
/**
* Class name of the modal
*/
Expand Down Expand Up @@ -52,9 +55,9 @@ interface BaseProductInterstitialModalProps {
*/
onClose?: () => void;
/**
* On click callback of the modal
* On click callback of the main modal button
*/
onClick?: () => void;
onModalMainButtonClick?: () => void;
/**
* Is CTA button disabled
*/
Expand All @@ -77,7 +80,7 @@ type WithMainCTAButton = BaseProductInterstitialModalProps & {
/**
* Main button of the modal
*/
modalMainButton: React.ReactNode;
modalMainButton: React.ReactElement;
/**
* Href of the CTA button in the modal
*/
Expand Down Expand Up @@ -119,11 +122,12 @@ const ProductInterstitialModal: FC< ProductInterstitialModalProps > = props => {
description,
className,
children,
triggerButton,
triggerButtonVariant = 'primary',
customModalTrigger,
modalTriggerButtonLabel,
modalTriggerButtonVariant = 'primary',
onOpen,
onClose,
onClick,
onModalMainButtonClick,
modalMainButton,
isButtonDisabled,
buttonHasExternalLink = false,
Expand All @@ -147,16 +151,35 @@ const ProductInterstitialModal: FC< ProductInterstitialModalProps > = props => {
setOpen( false );
}, [ onClose ] );

if ( ! title || ! children || ! triggerButton ) {
if ( ! title || ( ! modalTriggerButtonLabel && ! customModalTrigger ) ) {
return null;
}

// Render trigger element
const triggerElement = customModalTrigger ? (
// Clone custom trigger and inject onClick handler
cloneElement( customModalTrigger, { onClick: openModal } )
) : (
// Default button behavior
<Button variant={ modalTriggerButtonVariant } onClick={ openModal }>
{ modalTriggerButtonLabel }
</Button>
);

const PrimaryButton =
modalMainButton &&
cloneElement( modalMainButton, {
onClick: onModalMainButtonClick,
buttonLabel,
disabled: isButtonDisabled,
isExternalLink: buttonHasExternalLink,
href: buttonHref,
} );

return (
<>
<ThemeProvider>
<Button variant={ triggerButtonVariant } onClick={ openModal }>
{ triggerButton }
</Button>
{ triggerElement }
{ isOpen && (
<Modal
onRequestClose={ closeModal }
Expand Down Expand Up @@ -190,18 +213,7 @@ const ProductInterstitialModal: FC< ProductInterstitialModalProps > = props => {
) }
</div>
<div className={ styles[ 'primary-footer' ] }>
{ modalMainButton ?? (
<Button
variant="primary"
className={ styles[ 'action-button' ] }
disabled={ isButtonDisabled }
onClick={ onClick }
isExternalLink={ buttonHasExternalLink }
href={ buttonHref }
>
{ buttonLabel }
</Button>
) }
{ PrimaryButton }
<Button
variant="link"
isExternalLink={ secondaryButtonHasExternalLink }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
ProductInterstitialModal,
ProductInterstitialFeatureList,
ProductInterstitialModalCta,
} from './';
} from '.';

interface ProductInterstitialPluginProps {
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { Meta, Story, Canvas } from '@storybook/blocks';
import ProductInterstitialModal from '../index';
import * as ProductInterstitialModalStories from './index.stories';

<Meta of={ProductInterstitialModalStories} />

# Product Interstitial Modal
This is a component to create modals with a consistent layout across Jetpack products. It supports a customizable trigger element, multiple columns, and various content sections.

## Component variants and wrappers

### In My Jetpack

When including the component in My Jetpack `<ProductInterstitialMyJetpack />` component, there is a wrapper component provided that uses a product's slug to generate proper modal content and redirects users to the corresponding product page.
It also renders the feature list the same way as the old product interstitial page.

### In Jetpack plugins

[Work in progress]

## Props

Below are the current available props for `ProductInterstitialModal`.

### Modal Content

#### title (required)
The title displayed at the top of the modal.

#### description
Optional description text shown below the title.

#### children
Content placed in the left column between the description and price component.

#### priceComponent
Optional pricing information component displayed at the bottom of the left column.

## Modal Triggers

The modal can be triggered in two ways:

#### Default Button Trigger
```jsx
<ProductInterstitialModal
modalTriggerButtonLabel="Open Modal"
modalTriggerButtonVariant="primary"
// ... other props
/>
```

<Canvas of={ProductInterstitialModalStories.Default} />

#### Custom Trigger
You can provide any React element as a custom trigger. The component will inject the onClick handler.

```jsx
<ProductInterstitialModal
customModalTrigger={<JetpackLogo style={{ cursor: 'pointer' }} />}
// ... other props
/>
```

<Canvas of={ProductInterstitialModalStories.WithCustomTrigger} />

### Layout Options

#### secondaryColumn
Content for the right column (or middle column when additionalColumn is used).

This column is designed to show a video or an image.
When using the video, the `isWithVideo` prop should be set to `true` to apply aspect ratio formatting.

```jsx
<ProductInterstitialModal
customModalTrigger={<JetpackLogo style={{ cursor: 'pointer' }} />}
// ... other props
/>
```

<Canvas of={ProductInterstitialModalStories.Default} />

#### additionalColumn
Optional third column that creates a three-column layout.

[Styling is a work in progress]

<Canvas of={ProductInterstitialModalStories.WithAdditionalColumn} />

#### isWithVideo
When true, applies aspect ratio formatting for video content in the secondary column.

[Example in progress]

### Callbacks

#### onOpen
Called when the modal is opened.

#### onClose
Called when the modal is closed.

#### onModalMainButtonClick
Called when the main CTA button is clicked.

### Main CTA Options

The modal supports two types of main CTA configurations:

1. Using `modalMainButton` for a custom button component
2. Using `buttonLabel`, `buttonHref`, and `buttonHasExternalLink` for a standard button

### Secondary Button

#### secondaryButtonHref
URL for the "Learn more" link.

#### secondaryButtonHasExternalLink
When true, shows external link indicator on the secondary button.

## Usage Examples

### Basic Modal
```jsx
<ProductInterstitialModal
title="Jetpack Boost"
description="Speed up your WordPress site"
modalTriggerButtonLabel="Learn More"
buttonLabel="Upgrade"
onOpen={() => console.log('Modal opened')}
/>
```

### With Custom Trigger and Video
```jsx
<ProductInterstitialModal
title="Jetpack AI"
customModalTrigger={<JetpackLogo style={{ cursor: 'pointer' }} />}
isWithVideo={true}
secondaryColumn={<VideoComponent />}
/>
```
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ProductPrice } from '@automattic/jetpack-components';
import { ProductPrice, JetpackLogo } from '@automattic/jetpack-components';
import { __ } from '@wordpress/i18n';
import React from 'react';
import { HashRouter, Routes, Route } from 'react-router-dom';
import ProductInterstitialFeatureList from '../product-interstifial-feature-list';
import ProductInterstitialModal from '../product-interstitial-modal';
Expand Down Expand Up @@ -42,8 +41,9 @@ const DefaultArgs = {
legend="/month, paid yearly"
/>
),
triggerButton: 'Open Modal',
modalTriggerButtonLabel: 'Open Modal',
buttonLabel: 'Upgrade now',
isWithVideo: false,
secondaryColumn: <img src={ boostImage } alt="Boost" />,
secondaryButtonHref: 'https://jetpack.com',
secondaryButtonHasExternalLink: true,
Expand Down Expand Up @@ -85,5 +85,12 @@ WithVideo.args = {
),
};

export const WithCustomTrigger = Template.bind( {} );
WithCustomTrigger.args = {
...DefaultArgs,
modalTriggerButtonLabel: undefined,
customModalTrigger: <JetpackLogo style={ { cursor: 'pointer' } } />,
};

Default.parameters = {};
Default.args = DefaultArgs;
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { useGoBack } from '../../../hooks/use-go-back';
import useMyJetpackConnection from '../../../hooks/use-my-jetpack-connection';
import useMyJetpackNavigate from '../../../hooks/use-my-jetpack-navigate';
import GoBackLink from '../../go-back-link';
import { ProductInterstitialPlugin } from '../../product-interstitial-modal';
import { ProductInterstitialMyJetpack } from '../../product-interstitial-modal';
import styles from './style.module.scss';

const debug = debugFactory( 'my-jetpack:product-interstitial:jetpack-ai-product-page' );
Expand Down Expand Up @@ -239,10 +239,10 @@ export default function () {
</div>
{ ! shouldContactUs && ! hasUnlimited && (
<>
<ProductInterstitialPlugin
<ProductInterstitialMyJetpack
slug="jetpack-ai"
onOpen={ upgradeClickHandlerModal }
triggerButton={ __( 'Get more requests', 'jetpack-my-jetpack' ) }
modalTriggerButtonLabel={ __( 'Get more requests', 'jetpack-my-jetpack' ) }
buttonLabel={ __( 'Upgrade', 'jetpack-my-jetpack' ) }
isWithVideo
secondaryColumn={
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: added

Updated interstitial modal to accept custom trigger.
Loading