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: Add red bubble and warning Notice when paid plans are expired or expiring soon. #40115

Merged
merged 38 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
cbe4591
Add expired statuses to products, WIP.
elliottprogrammer Oct 18, 2024
8784bb5
changelog
elliottprogrammer Oct 18, 2024
ecbd97d
Update & refine logic to determine if products is expired/expiring.
elliottprogrammer Oct 22, 2024
0f615af
Add logic in remaining product classes for expired/expiring functiona…
elliottprogrammer Oct 22, 2024
86af562
Add manage purchase/subscription url for My Jetpack products.
elliottprogrammer Oct 23, 2024
cb99238
Fix $status possibly null when checking expiry status.
elliottprogrammer Oct 23, 2024
ab7411c
Add error/warning border color to expired/expiring product cards.
elliottprogrammer Oct 23, 2024
4ed025c
Trigger red bubble and Notice for expired or expiring paid products.
elliottprogrammer Nov 10, 2024
d974978
changelog
elliottprogrammer Nov 10, 2024
b6be77a
Add changelog.
elliottprogrammer Nov 10, 2024
4535a36
Merge branch 'trunk' into add/mj-expired-products-cards
elliottprogrammer Nov 11, 2024
abde3ee
Merge branch 'add/mj-expired-products-cards' into add/redbubble_for_e…
elliottprogrammer Nov 11, 2024
ae4099d
Fix how paid products are determined, regarding bundles.
elliottprogrammer Nov 11, 2024
6311a5e
Merge branch 'trunk' into add/mj-expired-products-cards
elliottprogrammer Nov 11, 2024
690afda
Merge branch 'add/mj-expired-products-cards' into add/redbubble_for_e…
elliottprogrammer Nov 11, 2024
f417485
Fix get_site_current_plan() doctype return value.
elliottprogrammer Nov 11, 2024
f2002c3
Merge branch 'add/mj-expired-products-cards' into add/redbubble_for_e…
elliottprogrammer Nov 11, 2024
cc7fdc5
Merge branch 'trunk' into add/mj-expired-products-cards
elliottprogrammer Nov 19, 2024
f674558
Merge branch 'trunk' into add/mj-expired-products-cards
elliottprogrammer Nov 22, 2024
8ab9836
Refactor/improve how we determine 'has_paid_plan_for_product()'.
elliottprogrammer Nov 22, 2024
dc04662
Change product card error & warning border to 1px, per feedback.
elliottprogrammer Nov 22, 2024
435de91
Merge branch 'add/mj-expired-products-cards' into add/redbubble_for_e…
elliottprogrammer Nov 22, 2024
2f65fe8
Address feedback & additional improvements.
elliottprogrammer Nov 24, 2024
02fb3ee
Fix translation error resulting from build process over-optimization.
elliottprogrammer Nov 24, 2024
124348f
Combine 'useGetExpired(& Expiring)NoticeContent' hooks into one, per …
elliottprogrammer Nov 25, 2024
0cba8d1
Fix notice CTA go to renewal and not open new tab.
elliottprogrammer Nov 25, 2024
0e37ba3
Fix red border on non-connected product cards.
elliottprogrammer Nov 25, 2024
bfd68ce
Merge branch 'add/mj-expired-products-cards' into add/redbubble_for_e…
elliottprogrammer Nov 25, 2024
210caad
Add option to hide expiration status & and get_renew_url function.
elliottprogrammer Nov 26, 2024
184802c
Make expired CTA goto purchase manage, and expiring CTA goto straight…
elliottprogrammer Nov 26, 2024
d52ab18
Merge branch 'add/mj-expired-products-cards' into add/redbubble_for_e…
elliottprogrammer Nov 26, 2024
9b386e9
Clean up, refactor, & optimize code in use-get-expiring-notice-conten…
elliottprogrammer Nov 26, 2024
5983d01
Merge branch 'trunk' into add/redbubble_for_expired_products
elliottprogrammer Nov 27, 2024
65f3114
My Jetpack: Your plans section: Highlight expired plans & add renew l…
elliottprogrammer Dec 2, 2024
569583a
Merge branch 'trunk' into add/redbubble_for_expired_products
elliottprogrammer Dec 2, 2024
dcc76a1
Merge branch 'trunk' into add/redbubble_for_expired_products
elliottprogrammer Dec 2, 2024
4479d6f
Fix text-domain in js-packages/components/action-button to jetpack-co…
elliottprogrammer Dec 2, 2024
7d60f42
Fix translator comment missing.
elliottprogrammer Dec 2, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Significance: patch
Type: changed
Comment: Just adding a couple additional props to the NoticeActionButton.


Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const ActionButton = props => {
isDisabled,
displayError = false,
errorMessage = __( 'An error occurred. Please try again.', 'jetpack-components' ),
variant = 'primary',
isExternalLink = false,
customClass,
} = props;

Expand All @@ -43,7 +45,8 @@ const ActionButton = props => {
className={ clsx( styles.button, 'jp-action-button--button', customClass ) }
label={ label }
onClick={ onClick }
variant="primary"
variant={ isExternalLink ? 'link' : variant }
isExternalLink={ isExternalLink }
disabled={ isLoading || isDisabled }
>
{ isLoading ? loadingContent : label }
Expand All @@ -70,6 +73,10 @@ ActionButton.propTypes = {
displayError: PropTypes.bool,
/** The error message string */
errorMessage: PropTypes.oneOfType( [ PropTypes.string, PropTypes.element ] ),
/** The type/variant of button */
variant: PropTypes.arrayOf( PropTypes.oneOf( [ 'primary', 'secondary', 'link' ] ) ),
/** Will display the button as a link with an external icon. */
isExternalLink: PropTypes.bool,
};

export default ActionButton;
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
position: relative;
margin: 0;
font-size: 16px;
line-height: 22px;
line-height: 24px;
background-color: var( --jp-white );

// notice content
Expand All @@ -28,6 +28,7 @@
color: var( --jp-black );
font-size: 16px;
font-weight: 600;
padding: var(--spacing-base) calc(var(--spacing-base)* 3) !important;
}

// X close button
Expand Down Expand Up @@ -85,6 +86,7 @@
}
}


.message {
margin-right: var( --spacing-base ); // 8px
flex-grow: 1;
Expand All @@ -106,6 +108,17 @@
fill: none;
}
}

& :global(.products-list) {
margin: 10px 0;
& :global(.product-badge) {
display: inline-block;
padding: 2px 8px;
margin: 4px 8px 4px 0;
border-radius: 6px;
background-color: #f1f1f1
}
}
}

.separator {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Text, H3, Title, Button } from '@automattic/jetpack-components';
import { __, _n } from '@wordpress/i18n';
import { dateI18n } from '@wordpress/date';
import { __, _n, sprintf } from '@wordpress/i18n';
import clsx from 'clsx';
import { useCallback } from 'react';
import { MyJetpackRoutes } from '../../constants';
import { MyJetpackRoutes, PRODUCT_STATUSES } from '../../constants';
import { QUERY_PURCHASES_KEY, REST_API_SITE_PURCHASES_ENDPOINT } from '../../data/constants';
import useSimpleQuery from '../../data/use-simple-query';
import { getMyJetpackWindowInitialState } from '../../data/utils/get-my-jetpack-window-state';
Expand All @@ -24,10 +26,10 @@ import styles from './style.module.scss';
function PlanSection( { purchase = {} } ) {
const { product_name } = purchase;
return (
<>
<div className={ styles[ 'plan-container' ] }>
<Title>{ product_name }</Title>
<PlanExpiry { ...purchase } />
</>
</div>
);
}

Expand All @@ -42,7 +44,67 @@ function PlanSection( { purchase = {} } ) {
* @return {object} - A plan expiry component.
*/
function PlanExpiry( purchase ) {
const { expiry_message, product_name, subscribed_date } = purchase;
const { ID, expiry_date, expiry_status, product_name, product_slug, subscribed_date, domain } =
purchase;

const managePurchaseUrl = `https://wordpress.com/me/purchases/${ domain }/${ ID }`;
const renewUrl = `https://wordpress.com/checkout/${ product_slug }/renew/${ ID }/${ domain }`;

const isExpired = PRODUCT_STATUSES.EXPIRED === expiry_status;
const isExpiringSoon = PRODUCT_STATUSES.EXPIRING_SOON === expiry_status;
const isExpiringPurchase = isExpired || isExpiringSoon;

const expiryMessageClassName = clsx( {
[ styles[ 'is-expired' ] ]: isExpired,
[ styles[ 'is-expiring-soon' ] ]: isExpiringSoon,
} );

const expiryMessage = useCallback( () => {
const displayDate = dateI18n( 'F jS, Y', expiry_date );
if ( isExpiringPurchase ) {
// Expiring soon
if ( isExpiringSoon ) {
return sprintf(
// translators: %1$s is the formatted date to display, i.e.- November 24th, 2024
__( 'Expiring soon on %1$s', 'jetpack-my-jetpack' ),
displayDate
);
}

// Expired
return sprintf(
// translators: %1$s is the formatted date to display, i.e.- November 24th, 2024
__( 'Expired on %1$s', 'jetpack-my-jetpack' ),
displayDate
);
}

return sprintf(
// translators: %1$s is the formatted date to display, i.e.- November 24th, 2024
__( 'Expires on %1$s', 'jetpack-my-jetpack' ),
displayDate
);
}, [ expiry_date, isExpiringPurchase, isExpiringSoon ] );

const expiryAction = useCallback( () => {
if ( ! isExpiringPurchase ) {
return null;
}

if ( isExpiringSoon ) {
return (
<Button href={ renewUrl } isExternalLink={ true } variant="link" weight="regular">
{ __( 'Renew subscription', 'jetpack-my-jetpack' ) }
</Button>
);
}

return (
<Button href={ managePurchaseUrl } isExternalLink={ true } variant="link" weight="regular">
{ __( 'Resume subscription', 'jetpack-my-jetpack' ) }
</Button>
);
}, [ isExpiringPurchase, isExpiringSoon, managePurchaseUrl, renewUrl ] );

if ( isLifetimePurchase( purchase ) ) {
return (
Expand All @@ -56,9 +118,12 @@ function PlanExpiry( purchase ) {
}

return (
<Text variant="body" className={ styles[ 'expire-date' ] }>
{ expiry_message }
</Text>
<>
<Text variant="body" className={ clsx( styles[ 'expire-date' ], expiryMessageClassName ) }>
{ expiryMessage() }
</Text>
{ isExpiringPurchase && <Text>{ expiryAction() }</Text> }
</>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,26 @@
}
}

.expire-date {
.plan-container {
margin-bottom: calc( var(--spacing-base ) * 3 );
}

.expire-date {
display: flex;

&--with-icon {
margin-right: 8px;
}
}

.is-expired {
color: var(--jp-red-60);
}

.is-expiring-soon {
color: var(--jp-yellow-40);
}

.actions-list-item {
line-height: calc( var( --spacing-base ) * 3 );
margin: 0 0 var( --spacing-base ) 0;
Expand Down
1 change: 1 addition & 0 deletions projects/packages/my-jetpack/_inc/context/notices/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type NoticeButtonAction = NoticeAction & {
isLoading?: boolean;
loadingText?: string;
isDisabled?: boolean;
isExternalLink?: boolean;
};

export type Notice = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getMyJetpackWindowInitialState } from '../../data/utils/get-my-jetpack-
import useBadInstallNotice from './use-bad-install-notice';
import useConnectionErrorsNotice from './use-connection-errors-notice';
import useDeprecateFeatureNotice from './use-deprecate-feature-notice';
import useExpiringPlansNotice from './use-expiring-plans-notice';
import useSiteConnectionNotice from './use-site-connection-notice';

const useNotificationWatcher = () => {
Expand All @@ -11,6 +12,7 @@ const useNotificationWatcher = () => {
useSiteConnectionNotice( redBubbleAlerts );
useConnectionErrorsNotice();
useDeprecateFeatureNotice( redBubbleAlerts );
useExpiringPlansNotice( redBubbleAlerts );
};

export default useNotificationWatcher;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const ProductsList = ( { products }: { products: string[] } ) => {
if ( ! products ) {
return null;
}
return (
<ul className="products-list">
{ products.map( ( product, index ) => (
<li key={ index } className="product-badge">
{ product }
</li>
) ) }
</ul>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { __ } from '@wordpress/i18n';
import { useContext, useEffect, useCallback } from 'react';
import { NOTICE_PRIORITY_MEDIUM } from '../../context/constants';
import { NoticeContext } from '../../context/notices/noticeContext';
import useAnalytics from '../use-analytics';
import { useGetExpiringNoticeContent } from './use-get-expiring-notice-content';
import type { NoticeOptions } from '../../context/notices/types';

type RedBubbleAlerts = Window[ 'myJetpackInitialState' ][ 'redBubbleAlerts' ];

const useExpiringPlansNotice = ( redBubbleAlerts: RedBubbleAlerts ) => {
const { setNotice } = useContext( NoticeContext );
const { recordEvent } = useAnalytics();

const planExpiredAlerts = Object.keys( redBubbleAlerts ).filter(
key => key.endsWith( '--plan_expiring_soon' ) || key.endsWith( '--plan_expired' )
) as Array< `${ string }--plan_expiring_soon` | `${ string }--plan_expired` >;

const expiredAlerts =
planExpiredAlerts.length &&
planExpiredAlerts.filter( alert => alert.endsWith( '--plan_expired' ) );
const expiringSoonAlerts =
planExpiredAlerts.length &&
planExpiredAlerts.filter( alert => alert.endsWith( '--plan_expiring_soon' ) );

// Already expired alerts take precidence over expiring alerts.
// i.e.- Display 'expired' alert if there is one, otherwise display 'expiring soon' alert.
const alertToDisplay = expiredAlerts.length ? expiredAlerts[ 0 ] : expiringSoonAlerts[ 0 ];
const isExpiredAlert = alertToDisplay && alertToDisplay.endsWith( '--plan_expired' );
const expiredAlertType = isExpiredAlert ? 'expired' : 'expiring-soon';

const {
product_slug: productSlug,
product_name: productName,
expiry_date: expiryDate,
manage_url: manageUrl,
products_effected: productsEffected,
} = redBubbleAlerts[ alertToDisplay ] || {};

const { noticeTitle, noticeMessage, learnMoreUrl } =
useGetExpiringNoticeContent( {
productSlug,
expiredAlertType,
productName,
expiryDate,
productsEffected,
} ) || {};

const onPrimaryCtaClick = useCallback( () => {
window.location.href = manageUrl;
recordEvent(
isExpiredAlert
? 'jetpack_my_jetpack_plan_expired_notice_primary_cta_click'
: 'jetpack_my_jetpack_plan_expiring_soon_notice_primary_cta_click',
{
product_slug: productSlug,
}
);
}, [ isExpiredAlert, manageUrl, productSlug, recordEvent ] );

const onSecondaryCtaClick = useCallback( () => {
window.open( learnMoreUrl );
recordEvent(
isExpiredAlert
? 'jetpack_my_jetpack_plan_expired_notice_secondary_cta_click'
: 'jetpack_my_jetpack_plan_expiring_soon_notice_secondary_cta_click',
{
product_slug: productSlug,
}
);
}, [ learnMoreUrl, isExpiredAlert, productSlug, recordEvent ] );

useEffect( () => {
if ( ! alertToDisplay ) {
return;
}

const resumePlanText = __( 'Resume my plan', 'jetpack-my-jetpack' );
const renewPlanText = __( 'Renew my plan', 'jetpack-my-jetpack' );

const noticeOptions: NoticeOptions = {
id: isExpiredAlert ? 'plan-expired-notice' : 'plan-expiring-soon-notice',
level: isExpiredAlert ? 'error' : 'warning',
actions: [
{
label: isExpiredAlert ? resumePlanText : renewPlanText,
onClick: onPrimaryCtaClick,
noDefaultClasses: true,
},
{
label: __( 'Learn more', 'jetpack-my-jetpack' ),
onClick: onSecondaryCtaClick,
isExternalLink: true,
},
],
priority: NOTICE_PRIORITY_MEDIUM,
};

setNotice( {
title: noticeTitle,
message: noticeMessage,
options: noticeOptions,
} );
}, [
redBubbleAlerts,
setNotice,
recordEvent,
alertToDisplay,
onPrimaryCtaClick,
onSecondaryCtaClick,
noticeTitle,
noticeMessage,
isExpiredAlert,
] );
};

export default useExpiringPlansNotice;
Loading
Loading