diff --git a/_inc/client/lib/plans/constants.js b/_inc/client/lib/plans/constants.js
index 5fcae039daae7..57e0f1c3f9e95 100644
--- a/_inc/client/lib/plans/constants.js
+++ b/_inc/client/lib/plans/constants.js
@@ -41,6 +41,13 @@ export const JETPACK_MONTHLY_PLANS = [
PLAN_JETPACK_PERSONAL_MONTHLY,
];
+export const JETPACK_BACKUP_PRODUCTS = [
+ PLAN_JETPACK_BACKUP_DAILY,
+ PLAN_JETPACK_BACKUP_DAILY_MONTHLY,
+ PLAN_JETPACK_BACKUP_REALTIME,
+ PLAN_JETPACK_BACKUP_REALTIME_MONTHLY,
+];
+
export const PLAN_MONTHLY_PERIOD = 31;
export const PLAN_ANNUAL_PERIOD = 365;
@@ -119,6 +126,10 @@ export function isNew( plan ) {
return includes( NEW_PLANS, plan );
}
+export function isJetpackBackup( product ) {
+ return includes( JETPACK_BACKUP_PRODUCTS, product );
+}
+
export function getPlanClass( plan ) {
switch ( plan ) {
case PLAN_JETPACK_FREE:
diff --git a/_inc/client/my-plan/index.jsx b/_inc/client/my-plan/index.jsx
index f47b223fad7ff..6a5d0e9bee5cc 100644
--- a/_inc/client/my-plan/index.jsx
+++ b/_inc/client/my-plan/index.jsx
@@ -27,9 +27,7 @@ export class MyPlan extends React.Component {
return (
-
-
-
+
`
+
+```jsx
+import React from 'react';
+import MyPlanCard from 'components/my-plan-card';
+import Button from 'components/button';
+
+export default class extends React.Component {
+ render() {
+ return (
+
Manage Payment }
+ details="Expires on October 27, 2020"
+ icon="images/plans/plan-personal.svg"
+ tagLine="Your data is being securely backed up and you have access to priority support."
+ title="Jetpack Personal"
+ />
+ );
+ }
+}
+```
+
+### ` ` props
+
+The following props can be passed to the My Plan Card component:
+
+* `action`: ( element | node ) Action button element or node.
+* `isError`: ( bool ) With this flag being set the details string is in an error state (red copy).
+* `isPlaceholder`: ( bool ) Flag indicating that the component in is a loading state
+* `details`: ( string ) Details about a plan or product, e.g. expiration or auto-renew date like `Expires on October 27, 2020`
+* `icon`: ( string ) Plan or product icon path
+* `tagLine`: ( string | element | node ) Plan or product tag line. It can be a string, a node or a React element (e.g. ``)
+* `title`: ( string | element | node ) Plan or product title. It can be a string, a node or a React element (e.g. ``)
diff --git a/_inc/client/my-plan/my-plan-card/index.jsx b/_inc/client/my-plan/my-plan-card/index.jsx
new file mode 100644
index 0000000000000..755a4df5b01be
--- /dev/null
+++ b/_inc/client/my-plan/my-plan-card/index.jsx
@@ -0,0 +1,49 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+/**
+ * Style dependencies
+ */
+import './style.scss';
+
+const MyPlanCard = ( { action, isError, isPlaceholder, details, icon, tagLine, title } ) => {
+ const cardClassNames = classNames( 'my-plan-card', {
+ 'is-placeholder': isPlaceholder,
+ 'has-action-only': action && ! details && ! isPlaceholder,
+ } );
+ const detailsClassNames = classNames( 'my-plan-card__details', { 'is-error': isError } );
+
+ return (
+
+
+
{ icon &&
}
+
+ { title &&
{ title } }
+ { tagLine &&
{ tagLine }
}
+
+
+ { ( details || action || isPlaceholder ) && (
+
+
{ isPlaceholder ? null : details }
+
{ isPlaceholder ? null : action }
+
+ ) }
+
+ );
+};
+
+MyPlanCard.propTypes = {
+ action: PropTypes.oneOfType( [ PropTypes.node, PropTypes.element ] ),
+ isError: PropTypes.bool,
+ isPlaceholder: PropTypes.bool,
+ details: PropTypes.string,
+ icon: PropTypes.string,
+ tagLine: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node, PropTypes.element ] ),
+ title: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node, PropTypes.element ] ),
+};
+
+export default MyPlanCard;
diff --git a/_inc/client/my-plan/my-plan-card/style.scss b/_inc/client/my-plan/my-plan-card/style.scss
new file mode 100644
index 0000000000000..9e3cad73d85d1
--- /dev/null
+++ b/_inc/client/my-plan/my-plan-card/style.scss
@@ -0,0 +1,122 @@
+@import '../../scss/mixin_breakpoint';
+@import '../../scss/calypso-colors';
+
+// My Plan Card
+.my-plan-card {
+ @include breakpoint( '>960px' ) {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: space-between;
+ }
+}
+
+.my-plan-card__primary {
+ display: flex;
+ flex-flow: row nowrap;
+ flex-grow: 1;
+}
+
+.my-plan-card__header {
+ flex: 1;
+}
+
+.my-plan-card__title {
+ font-size: 20px;
+ font-weight: 600;
+ line-height: 29px;
+ margin: 6px 0;
+ color: $gray-dark;
+}
+
+.my-plan-card__tag-line {
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 17px;
+ margin: 0 0 24px;
+
+ @include breakpoint( '>960px' ) {
+ margin-bottom: 8px;
+ }
+}
+
+.my-plan-card__icon {
+ flex: 0 0 auto;
+ width: 64px;
+ height: 64px;
+ margin: 8px 20px 16px 0;
+
+ @include breakpoint( '<660px' ) {
+ display: none;
+ }
+
+ img {
+ width: 100%;
+ height: 100%;
+ }
+}
+
+.my-plan-card__secondary {
+ position: relative;
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 0 0;
+
+ @include breakpoint( '>960px' ) {
+ flex-flow: column nowrap;
+ justify-content: center;
+ align-items: flex-end;
+ padding: 0 0 0 24px;
+ }
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -16px;
+ right: -16px;
+ border-top: 1px solid $light-gray-700;
+
+ @include breakpoint( '>480px' ) {
+ left: -24px;
+ right: -24px;
+ }
+
+ @include breakpoint( '>960px' ) {
+ content: none;
+ }
+ }
+
+ .has-action-only & {
+ padding-top: 0;
+ justify-content: center;
+
+ &::before {
+ content: none;
+ }
+ }
+}
+
+.my-plan-card__details {
+ padding-top: 8px;
+ white-space: nowrap;
+ color: $gray-darken-10;
+
+ @include breakpoint( '>960px' ) {
+ padding-top: 0;
+ }
+
+ &.is-error {
+ color: $alert-red;
+ }
+}
+
+.my-plan-card__action {
+ padding-top: 8px;
+ white-space: nowrap;
+
+ .has-action-only & {
+ padding-top: 0;
+ }
+}
diff --git a/_inc/client/my-plan/my-plan-header/index.js b/_inc/client/my-plan/my-plan-header/index.js
index 7f2fe8fd8bd63..a17072d382e44 100644
--- a/_inc/client/my-plan/my-plan-header/index.js
+++ b/_inc/client/my-plan/my-plan-header/index.js
@@ -2,240 +2,190 @@
* External dependencies
*/
import React from 'react';
-import { translate as __ } from 'i18n-calypso';
+import { moment, translate as __ } from 'i18n-calypso';
import { connect } from 'react-redux';
+import { find, filter, isEmpty } from 'lodash';
/**
* Internal dependencies
*/
-import analytics from 'lib/analytics';
+import Card from 'components/card';
import ChecklistCta from './checklist-cta';
import ChecklistProgress from './checklist-progress-card';
-import { getPlanClass } from 'lib/plans/constants';
+import MyPlanCard from '../my-plan-card';
+import UpgradeLink from 'components/upgrade-link';
+import { getPlanClass, isJetpackBackup } from 'lib/plans/constants';
import { getUpgradeUrl, getSiteRawUrl, showBackups } from 'state/initial-state';
+import { getSitePurchases } from 'state/site';
import { imagePath } from 'constants/urls';
-import UpgradeLink from 'components/upgrade-link';
+import PropTypes from 'prop-types';
class MyPlanHeader extends React.Component {
- trackChecklistCtaClick = () =>
- void analytics.tracks.recordEvent(
- 'jetpack_myplan_headerchecklistcta_click',
- this.props.plan
- ? {
- plan: this.props.plan,
- }
- : undefined
- );
+ getProductProps( productSlug ) {
+ const { displayBackups, purchases } = this.props;
- render() {
- const { plan, siteSlug } = this.props;
+ if ( ! productSlug ) {
+ return {
+ isPlaceholder: true,
+ };
+ }
- const PlanHeaderCard = props => {
- const { title, text, imgSrc, imgAlt } = props;
-
- return (
-
-
-
-
-
-
{ title }
-
{ text }
-
-
-
- );
- };
-
- let planCard = '';
- switch ( getPlanClass( plan ) ) {
+ const purchase = find( purchases, purchaseObj => purchaseObj.product_slug === productSlug );
+ const expiration =
+ purchase && purchase.expiry_date
+ ? __( 'Expires on %s.', { args: moment( purchase.expiry_date ).format( 'LL' ) } )
+ : null;
+
+ switch ( getPlanClass( productSlug ) ) {
case 'is-free-plan':
- planCard = (
-
-
-
-
-
-
- { __( 'Your plan: Jetpack Free' ) }
-
-
- { __(
- 'Worried about security? Get backups, automated security fixes and more: {{a}}Upgrade now{{/a}}',
- {
- components: {
- a: (
-
- ),
- },
- }
- ) }
-
-
-
-
- );
- break;
+ return {
+ icon: imagePath + '/plans/plan-free.svg',
+ tagLine: __(
+ 'Worried about security? Get backups, automated security fixes and more: {{a}}Upgrade now{{/a}}',
+ {
+ components: {
+ a: (
+
+ ),
+ },
+ }
+ ),
+ title: __( 'Jetpack Free' ),
+ };
case 'is-personal-plan':
- planCard = (
-
-
-
-
-
-
- { __( 'Your plan: Jetpack Personal' ) }
-
- { this.props.showBackups ? (
-
- { __( 'Daily backups, spam filtering, and priority support.' ) }
-
- ) : (
-
- { __( 'Spam filtering and priority support.' ) }
-
- ) }
-
-
-
- );
- break;
+ return {
+ details: expiration,
+ icon: imagePath + '/plans/plan-personal.svg',
+ tagLine: displayBackups
+ ? __( 'Daily backups, spam filtering, and priority support.' )
+ : __( 'Spam filtering and priority support.' ),
+ title: __( 'Jetpack Personal' ),
+ };
case 'is-premium-plan':
- planCard = (
-
-
-
-
-
-
- { __( 'Your plan: Jetpack Premium' ) }
-
-
- { __(
- 'Full security suite, marketing and revenue automation tools, unlimited video hosting, and priority support.'
- ) }
-
-
-
-
- );
- break;
+ return {
+ details: expiration,
+ icon: imagePath + '/plans/plan-premium.svg',
+ tagLine: __(
+ 'Full security suite, marketing and revenue automation tools, unlimited video hosting, and priority support.'
+ ),
+ title: __( 'Jetpack Premium' ),
+ };
case 'is-business-plan':
- planCard = (
-
-
-
-
-
-
- { __( 'Your plan: Jetpack Professional' ) }
-
-
- { __(
- 'Full security suite, marketing and revenue automation tools, unlimited video hosting, unlimited themes, enhanced search, and priority support.'
- ) }
-
-
-
-
- );
- break;
+ return {
+ details: expiration,
+ icon: imagePath + '/plans/plan-business.svg',
+ tagLine: __(
+ 'Full security suite, marketing and revenue automation tools, unlimited video hosting, unlimited themes, enhanced search, and priority support.'
+ ),
+ title: __( 'Jetpack Professional' ),
+ };
case 'is-daily-backup-plan':
- planCard = (
-
- ),
- },
- }
- ) }
- imgSrc={ imagePath + '/plans/plan-free.svg' }
- imgAlt={ __( 'Jetpack Daily Backup Plan' ) }
- />
- );
- break;
+ return {
+ details: expiration,
+ icon: imagePath + '/products/product-jetpack-backup.svg',
+ tagLine: __( 'Your data is being securely backed up every day with a 30-day archive.' ),
+ title: __( 'Jetpack Backup {{em}}Daily{{/em}}', {
+ components: {
+ em: ,
+ },
+ } ),
+ };
case 'is-realtime-backup-plan':
- planCard = (
-
- ),
- },
- }
- ) }
- imgSrc={ imagePath + '/plans/plan-free.svg' }
- imgAlt={ __( 'Jetpack Real-time Backup Plan' ) }
- />
- );
- break;
+ return {
+ details: expiration,
+ icon: imagePath + '/products/product-jetpack-backup.svg',
+ tagLine: __( 'Your data is being securely backed up as you edit.' ),
+ title: __( 'Jetpack Backup {{em}}Real-Time{{/em}}', {
+ components: {
+ em: ,
+ },
+ } ),
+ };
default:
- planCard = (
-
- );
- break;
+ return {
+ isPlaceholder: true,
+ };
+ }
+ }
+
+ renderPlan() {
+ const { plan } = this.props;
+ const planProps = this.getProductProps( plan );
+
+ return (
+
+ { this.renderHeader( __( 'My Plan' ) ) }
+
+
+ );
+ }
+
+ renderProducts() {
+ const { purchases } = this.props;
+ const products = filter( purchases, purchase => isJetpackBackup( purchase.product_slug ) );
+
+ if ( isEmpty( products ) ) {
+ return null;
}
+
return (
- <>
- { planCard }
+
+ { this.renderHeader( __( 'My Products' ) ) }
+ { products.map( ( { ID, product_slug } ) => {
+ const productProps = this.getProductProps( product_slug );
+
+ return ;
+ } ) }
+
+ );
+ }
+
+ renderHeader( title ) {
+ return { title } ;
+ }
+
+ render() {
+ const { plan, siteSlug } = this.props;
+
+ return (
+
+ { this.renderPlan() }
+ { this.renderProducts() }
+
+
+
- >
+
);
}
}
+
+MyPlanHeader.propTypes = {
+ plan: PropTypes.string,
+ siteRawUrl: PropTypes.string,
+
+ // From connect HoC
+ siteSlug: PropTypes.string,
+ displayBackups: PropTypes.bool,
+ plansMainTopUpgradeUrl: PropTypes.string,
+ purchases: PropTypes.array,
+};
+
export default connect( state => {
return {
siteSlug: getSiteRawUrl( state ),
- showBackups: showBackups( state ),
+ displayBackups: showBackups( state ),
plansMainTopUpgradeUrl: getUpgradeUrl( state, 'plans-main-top' ),
+ purchases: getSitePurchases( state ),
};
} )( MyPlanHeader );
diff --git a/_inc/client/my-plan/style.scss b/_inc/client/my-plan/style.scss
index 6507d21d970eb..f9c347206c7e9 100644
--- a/_inc/client/my-plan/style.scss
+++ b/_inc/client/my-plan/style.scss
@@ -1,49 +1,24 @@
@import '../scss/calypso-colors';
-.jp-landing__plans.dops-card {
- padding: 0;
-}
-
.jp-landing__plans {
+ margin-bottom: 32px;
.dops-button {
margin-right: 10px;
}
}
-.jp-landing__plan-card-current {
- // Force a break by including full-width psuedo-element at correct position.
- &::before {
- content: '';
- order: 20;
- width: 100%;
- }
-
- h3 {
- font-size: 1.5em;
- width: 100%; // Make sure placeholder appears
- order: 10;
- }
-
- .jp-landing__plan-features-text {
- font-size: rem( 14px );
- order: 30;
- }
-
- @include breakpoint( ">660px" ) {
- display: flex;
- flex-wrap: wrap;
-
- .jp-landing__plan-features-text {
- margin-right: 3em;
- }
- }
-
+.jp-landing__card-header {
+ margin-top: 0;
+ font-size: 15px;
+ color: $gray-darken-20;
}
.jp-landing__plan-features-header-checklist-cta-container {
order: 30;
align-self: center;
+ display: flex;
+ justify-content: flex-end;
.dops-button {
margin-right: 0;
@@ -136,67 +111,6 @@
}
}
-.jp-landing__plan-card {
- padding: rem( 32px );
-
- @include breakpoint( "<480px" ) {
- text-align: center;
- }
-
- @include breakpoint( ">480px" ) {
- display: flex;
- align-items: center;
- flex-wrap: nowrap;
- }
-
- .jp-landing__plan-features-title,
- .jp-landing__plan-features-text {
- padding: 0;
- }
-
- .jp-landing__plan-features-title {
- margin-bottom: rem( 16px );
- }
-
- .jp-landing__plan-features-text:last-child {
- margin-bottom: 0;
- }
-}
-
-.jp-landing__plan-card-img {
- float: left;
- margin-right: rem( 42px );
-
- @include breakpoint( "<960px" ) {
- margin-right: rem( 16px );
- }
-
- @include breakpoint( "<480px" ) {
- width: 100%;
- max-width: 100%;
- text-align: center;
- }
-}
-
-.jp-landing__plan-icon {
- width: rem( 82px );
- position: relative;
- left: rem( -3px );
-
- @include breakpoint( ">960px" ) {
- width: rem( 96px );
- left: rem( 2px );
- }
-}
-
-.jp-landing__plan-card-img.is-placeholder {
- width: rem( 120px );
- height: rem( 85px );
-
- & + .jp-landing__plan-card-current {
- width: 80%;
- }
-}
.jp-landing__plan-features-title.is-placeholder {
height: rem( 24px );
diff --git a/images/products/product-jetpack-backup.svg b/images/products/product-jetpack-backup.svg
new file mode 100644
index 0000000000000..fdebe39c06a73
--- /dev/null
+++ b/images/products/product-jetpack-backup.svg
@@ -0,0 +1 @@
+