diff --git a/projects/packages/connection/changelog/update-type-for-tracking-string b/projects/packages/connection/changelog/update-type-for-tracking-string new file mode 100644 index 0000000000000..07d6b6a845094 --- /dev/null +++ b/projects/packages/connection/changelog/update-type-for-tracking-string @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Fix type for tracking product string diff --git a/projects/packages/connection/src/class-package-version.php b/projects/packages/connection/src/class-package-version.php index 09ff7311fdea3..94a31c1860ab6 100644 --- a/projects/packages/connection/src/class-package-version.php +++ b/projects/packages/connection/src/class-package-version.php @@ -12,7 +12,7 @@ */ class Package_Version { - const PACKAGE_VERSION = '2.11.3'; + const PACKAGE_VERSION = '2.11.4-alpha'; const PACKAGE_SLUG = 'connection'; diff --git a/projects/packages/connection/src/class-tracking.php b/projects/packages/connection/src/class-tracking.php index 5af18537c89ed..edf23af785fd0 100644 --- a/projects/packages/connection/src/class-tracking.php +++ b/projects/packages/connection/src/class-tracking.php @@ -38,7 +38,7 @@ class Tracking { /** * Creates the Tracking object. * - * @param String $product_name the slug of the product that we are tracking. + * @param string $product_name the slug of the product that we are tracking. * @param \Automattic\Jetpack\Connection\Manager $connection the connection manager object. */ public function __construct( $product_name = 'jetpack', $connection = null ) { diff --git a/projects/packages/my-jetpack/.phan/baseline.php b/projects/packages/my-jetpack/.phan/baseline.php index 7f99fefac08c1..9158438791ca1 100644 --- a/projects/packages/my-jetpack/.phan/baseline.php +++ b/projects/packages/my-jetpack/.phan/baseline.php @@ -11,28 +11,26 @@ // # Issue statistics: // PhanTypeMismatchArgumentNullable : 60+ occurrences // PhanTypeMismatchPropertyDefault : 15+ occurrences - // PhanParamTooMany : 10+ occurrences // PhanTypeMismatchReturnProbablyReal : 10+ occurrences + // PhanParamTooMany : 9 occurrences // PhanAbstractStaticMethodCallInStatic : 8 occurrences - // PhanNoopNew : 6 occurrences - // PhanTypeMismatchReturn : 6 occurrences + // PhanNoopNew : 7 occurrences // PhanPluginDuplicateConditionalNullCoalescing : 5 occurrences + // PhanTypeMismatchReturn : 5 occurrences // PhanTypeMismatchReturnNullable : 3 occurrences // PhanImpossibleCondition : 2 occurrences // PhanNonClassMethodCall : 2 occurrences + // PhanRedundantCondition : 2 occurrences // PhanTypeMismatchArgumentProbablyReal : 2 occurrences // PhanPluginMixedKeyNoKey : 1 occurrence - // PhanRedundantCondition : 1 occurrence - // PhanTypeMismatchArgumentInternal : 1 occurrence // PhanTypeMismatchArgumentNullableInternal : 1 occurrence // Currently, file_suppressions and directory_suppressions are the only supported suppressions 'file_suppressions' => [ 'src/class-activitylog.php' => ['PhanTypeMismatchArgumentProbablyReal'], - 'src/class-initializer.php' => ['PhanImpossibleCondition', 'PhanNoopNew', 'PhanParamTooMany', 'PhanRedundantCondition', 'PhanTypeMismatchArgumentInternal', 'PhanTypeMismatchReturn', 'PhanTypeMismatchReturnNullable', 'PhanTypeMismatchReturnProbablyReal'], + 'src/class-initializer.php' => ['PhanImpossibleCondition', 'PhanNoopNew', 'PhanRedundantCondition', 'PhanTypeMismatchReturn', 'PhanTypeMismatchReturnNullable'], 'src/class-jetpack-manage.php' => ['PhanTypeMismatchArgumentProbablyReal'], 'src/class-products.php' => ['PhanNonClassMethodCall'], - 'src/class-rest-product-data.php' => ['PhanParamTooMany', 'PhanTypeMismatchReturn'], 'src/class-rest-products.php' => ['PhanParamTooMany', 'PhanPluginMixedKeyNoKey', 'PhanTypeMismatchReturn', 'PhanTypeMismatchReturnProbablyReal'], 'src/class-rest-purchases.php' => ['PhanParamTooMany', 'PhanTypeMismatchReturn', 'PhanTypeMismatchReturnProbablyReal'], 'src/class-rest-zendesk-chat.php' => ['PhanParamTooMany'], diff --git a/projects/packages/my-jetpack/_inc/components/connected-product-card/index.tsx b/projects/packages/my-jetpack/_inc/components/connected-product-card/index.tsx index f146ebebf4f55..a46ab3b185b51 100644 --- a/projects/packages/my-jetpack/_inc/components/connected-product-card/index.tsx +++ b/projects/packages/my-jetpack/_inc/components/connected-product-card/index.tsx @@ -15,21 +15,23 @@ import useProduct from '../../data/products/use-product'; import useAnalytics from '../../hooks/use-analytics'; import useMyJetpackConnection from '../../hooks/use-my-jetpack-connection'; import useMyJetpackNavigate from '../../hooks/use-my-jetpack-navigate'; +import preventWidows from '../../utils/prevent-widows'; import ProductCard from '../product-card'; import type { AdditionalAction, SecondaryAction } from '../product-card/types'; import type { FC, ReactNode } from 'react'; interface ConnectedProductCardProps { admin: boolean; - recommendation: boolean; + recommendation?: boolean; + showMenu?: boolean; slug: JetpackModule; - children: ReactNode; + children?: ReactNode; isDataLoading?: boolean; Description?: FC; additionalActions?: AdditionalAction[]; secondaryAction?: SecondaryAction; upgradeInInterstitial?: boolean; - primaryActionOverride?: AdditionalAction; + primaryActionOverride?: Record< string, AdditionalAction >; onMouseEnter?: () => void; onMouseLeave?: () => void; } @@ -85,7 +87,7 @@ const ConnectedProductCard: FC< ConnectedProductCardProps > = ( { const DefaultDescription = () => { // Replace the last space with a non-breaking space to prevent widows - const cardDescription = defaultDescription.replace( /\s(?=[^\s]*$)/, '\u00A0' ); + const cardDescription = preventWidows( defaultDescription ); return ( diff --git a/projects/packages/my-jetpack/_inc/components/product-card/index.tsx b/projects/packages/my-jetpack/_inc/components/product-card/index.tsx index 1f1cd6dfe0c2f..48921438620e1 100644 --- a/projects/packages/my-jetpack/_inc/components/product-card/index.tsx +++ b/projects/packages/my-jetpack/_inc/components/product-card/index.tsx @@ -29,7 +29,7 @@ export type ProductCardProps = { slug: JetpackModule; additionalActions?: AdditionalAction[]; upgradeInInterstitial?: boolean; - primaryActionOverride?: AdditionalAction; + primaryActionOverride?: Record< string, AdditionalAction >; secondaryAction?: SecondaryAction; onInstallStandalone?: InstallCallback; onActivateStandalone?: () => void; diff --git a/projects/packages/my-jetpack/_inc/components/product-card/types.ts b/projects/packages/my-jetpack/_inc/components/product-card/types.ts index 4bf56cea1a18c..ab2c2f3891dcf 100644 --- a/projects/packages/my-jetpack/_inc/components/product-card/types.ts +++ b/projects/packages/my-jetpack/_inc/components/product-card/types.ts @@ -7,9 +7,10 @@ type ProductButtonProps = Pick< >; export type AdditionalAction = ProductButtonProps & { - href: string; label: string; - onClick: () => void; + href?: string; + onClick?: () => void; + isExternalLink?: boolean; }; export type SecondaryAction = ProductButtonProps & { diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/ai-card.jsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/ai-card.jsx index 0fdcbca387d0c..6e027c4f8c674 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/ai-card.jsx +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/ai-card.jsx @@ -1,7 +1,8 @@ +import { PRODUCT_SLUGS } from '../../data/constants'; import ProductCard from '../connected-product-card'; const AiCard = props => { - return ; + return ; }; export default AiCard; diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/anti-spam-card.jsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/anti-spam-card.jsx index 629a1ebb7edd9..676d8f7c8f404 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/anti-spam-card.jsx +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/anti-spam-card.jsx @@ -1,9 +1,9 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import { PRODUCT_SLUGS } from '../../data/constants'; import ProductCard from '../connected-product-card'; const AntiSpamCard = props => { - return ; + return ; }; AntiSpamCard.propTypes = { diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/backup-card/index.jsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/backup-card/index.jsx index 9400d74d3ed3d..e295888013479 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/backup-card/index.jsx +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/backup-card/index.jsx @@ -11,6 +11,7 @@ import { REST_API_COUNT_BACKUP_ITEMS_ENDPOINT, QUERY_BACKUP_HISTORY_KEY, QUERY_BACKUP_STATS_KEY, + PRODUCT_SLUGS, } from '../../../data/constants'; import useProduct from '../../../data/products/use-product'; import useSimpleQuery from '../../../data/use-simple-query'; @@ -19,6 +20,8 @@ import useAnalytics from '../../../hooks/use-analytics'; import ProductCard from '../../connected-product-card'; import styles from './style.module.scss'; +const productSlug = PRODUCT_SLUGS.BACKUP; + const getIcon = slug => { switch ( slug ) { case 'post': @@ -122,15 +125,14 @@ const getTimeSinceLastRenewableEvent = lastRewindableEventTime => { }; const BackupCard = props => { - const slug = 'backup'; - const { detail } = useProduct( slug ); + const { detail } = useProduct( productSlug ); const { status } = detail; const hasBackups = status === PRODUCT_STATUSES.ACTIVE || status === PRODUCT_STATUSES.CAN_UPGRADE; return hasBackups ? ( - + ) : ( - + ); }; diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/boost-card/index.tsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/boost-card/index.tsx index 92dca7565bc9e..9b68548b9f656 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/boost-card/index.tsx +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/boost-card/index.tsx @@ -1,11 +1,12 @@ import { __ } from '@wordpress/i18n'; import { useState, useCallback } from 'react'; import { PRODUCT_STATUSES } from '../../../constants'; +import { PRODUCT_SLUGS } from '../../../data/constants'; import ProductCard from '../../connected-product-card'; import BoostSpeedScore from './boost-speed-score'; -import type { FC } from 'react'; +import type { ProductCardComponent } from '../types'; -const BoostCard: FC< { admin: boolean } > = props => { +const BoostCard: ProductCardComponent = props => { const [ shouldShowTooltip, setShouldShowTooltip ] = useState( false ); // Override the primary action button to read "Boost your site" instead // of the default text, "Lern more". @@ -25,7 +26,7 @@ const BoostCard: FC< { admin: boolean } > = props => { return ( { - return ; + return ; }; CreatorCard.propTypes = { diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/crm-card.jsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/crm-card.jsx index 417de8a8932a6..148cf25bb09b2 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/crm-card.jsx +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/crm-card.jsx @@ -1,9 +1,9 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import { PRODUCT_SLUGS } from '../../data/constants'; import ProductCard from '../connected-product-card'; const CrmCard = props => { - return ; + return ; }; CrmCard.propTypes = { diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/extras-card.jsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/extras-card.jsx index ba99049e3395b..07b0ea805ecc8 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/extras-card.jsx +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/extras-card.jsx @@ -1,9 +1,9 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import { PRODUCT_SLUGS } from '../../data/constants'; import ProductCard from '../connected-product-card'; const ExtrasCard = props => { - return ; + return ; }; ExtrasCard.propTypes = { diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/index.tsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/index.tsx index f88ef76980941..be198ae05a8da 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/index.tsx +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/index.tsx @@ -1,6 +1,7 @@ import { Container, Col, Text, AdminSectionHero } from '@automattic/jetpack-components'; import { __ } from '@wordpress/i18n'; import { useMemo } from 'react'; +import { PRODUCT_SLUGS } from '../../data/constants'; import { getMyJetpackWindowInitialState } from '../../data/utils/get-my-jetpack-window-state'; import StatsSection from '../stats-section'; import AiCard from './ai-card'; @@ -23,7 +24,9 @@ type DisplayItemsProps = { type DisplayItemType = Record< // We don't have a card for Security or Extras, and scan is displayed as protect. - Exclude< JetpackModule, 'extras' | 'scan' | 'security' >, + // 'jetpack-ai' is the official slug for the AI module, so we also exclude 'ai'. + // The backend still supports the 'ai' slug, so it is part of the JetpackModule type. + Exclude< JetpackModule, 'extras' | 'scan' | 'security' | 'ai' >, FC< { admin: boolean } > >; @@ -46,7 +49,7 @@ const DisplayItems: FC< DisplayItemsProps > = ( { slugs } ) => { }; const filteredSlugs = slugs.filter( slug => { - if ( slug === 'stats' && showFullJetpackStatsCard ) { + if ( slug === PRODUCT_SLUGS.STATS && showFullJetpackStatsCard ) { return false; } diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/index.tsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/index.tsx index da5c17fbe9593..da344805b4b11 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/index.tsx +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/index.tsx @@ -1,15 +1,12 @@ -import { type FC } from 'react'; +import { PRODUCT_SLUGS } from '../../../data/constants'; import ProductCard from '../../connected-product-card'; import ProtectValueSection from './protect-value-section'; +import type { ProductCardComponent } from '../types'; -const ProtectCard: FC< { admin: boolean; recommendation?: boolean } > = props => { - const slug = 'protect'; - - return ( - - - - ); -}; +const ProtectCard: ProductCardComponent = props => ( + + + +); export default ProtectCard; diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/search-card.jsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/search-card.jsx index e554eb28675de..5c1ac800c5e01 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/search-card.jsx +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/search-card.jsx @@ -1,9 +1,9 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import { PRODUCT_SLUGS } from '../../data/constants'; import ProductCard from '../connected-product-card'; const SearchCard = props => { - return ; + return ; }; SearchCard.propTypes = { diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/social-card.jsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/social-card.jsx index 22f9973cdcc71..692665af57357 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/social-card.jsx +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/social-card.jsx @@ -1,9 +1,9 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import { PRODUCT_SLUGS } from '../../data/constants'; import ProductCard from '../connected-product-card'; const SocialCard = props => { - return ; + return ; }; SocialCard.propTypes = { diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/stats-card.jsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/stats-card.jsx index a66d97fe5f0d6..7372ba4fee094 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/stats-card.jsx +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/stats-card.jsx @@ -1,9 +1,9 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import { PRODUCT_SLUGS } from '../../data/constants'; import ProductCard from '../connected-product-card'; const StatsCard = props => { - return ; + return ; }; StatsCard.propTypes = { diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/types.ts b/projects/packages/my-jetpack/_inc/components/product-cards-section/types.ts new file mode 100644 index 0000000000000..8949030227a60 --- /dev/null +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/types.ts @@ -0,0 +1,6 @@ +import type { FC } from 'react'; + +export type ProductCardComponent = FC< { + admin: boolean; + recommendation?: boolean; +} >; diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/videopress-card.jsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/videopress-card.jsx deleted file mode 100644 index 131ee03f89cab..0000000000000 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/videopress-card.jsx +++ /dev/null @@ -1,85 +0,0 @@ -/** - * External dependencies - */ -import { numberFormat } from '@automattic/jetpack-components'; -import { __ } from '@wordpress/i18n'; -import PropTypes from 'prop-types'; -import { - REST_API_VIDEOPRESS_FEATURED_STATS, - QUERY_VIDEOPRESS_STATS_KEY, -} from '../../data/constants'; -import useSimpleQuery from '../../data/use-simple-query'; -import { getMyJetpackWindowInitialState } from '../../data/utils/get-my-jetpack-window-state'; -/** - * Internal dependencies - */ -import ProductCard from '../connected-product-card'; -import { SingleContextualInfo, ChangePercentageContext } from './contextual-card-info'; - -const useVideoPressStats = () => { - const { - data: stats, - isLoading, - isError, - } = useSimpleQuery( { - name: QUERY_VIDEOPRESS_STATS_KEY, - query: { path: REST_API_VIDEOPRESS_FEATURED_STATS }, - } ); - - const views = stats?.data?.views ?? {}; - const { previous = null, current = null } = views; - const currentFormatted = - current !== null - ? numberFormat( current, { notation: 'compact', compactDisplay: 'short' } ) - : null; - const change = current !== null && previous !== null ? current - previous : null; - let changePercentage = null; - - if ( change !== null ) { - if ( change === 0 ) { - changePercentage = 0; - } else if ( previous === 0 ) { - changePercentage = 100; - } else { - changePercentage = Math.round( ( change / previous ) * 100 ); - } - } - - return { - isLoading, - isError, - currentFormatted, - change, - changePercentage, - }; -}; - -const VideopressCard = props => { - const { videoPressStats = false } = getMyJetpackWindowInitialState( 'myJetpackFlags' ); - const { loading, hasError, change, currentFormatted, changePercentage } = useVideoPressStats(); - - if ( ! videoPressStats || hasError ) { - return ; - } - - const description = __( 'Views, last 7 days', 'jetpack-my-jetpack' ); - - return ( - - - } - /> - - ); -}; - -VideopressCard.propTypes = { - admin: PropTypes.bool.isRequired, -}; - -export default VideopressCard; diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/videopress-card/index.tsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/videopress-card/index.tsx new file mode 100644 index 0000000000000..03e3d0a5072ea --- /dev/null +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/videopress-card/index.tsx @@ -0,0 +1,48 @@ +/** + * External dependencies + */ +import { Text } from '@automattic/jetpack-components'; +import { useCallback } from 'react'; +import { PRODUCT_SLUGS } from '../../../data/constants'; +import useProduct from '../../../data/products/use-product'; +import { getMyJetpackWindowInitialState } from '../../../data/utils/get-my-jetpack-window-state'; +import ProductCard from '../../connected-product-card'; +import useVideoPressCardDescription from './use-videopress-description'; +import VideoPressValueSection from './videopress-value-section'; +import type { ProductCardComponent } from '../types'; + +import './style.scss'; + +const slug = PRODUCT_SLUGS.VIDEOPRESS; + +const VideopressCard: ProductCardComponent = ( { admin } ) => { + const { detail } = useProduct( slug ); + const { isPluginActive = false } = detail || {}; + const { videopress: data } = getMyJetpackWindowInitialState(); + + const descriptionText = useVideoPressCardDescription( { + isPluginActive, + videoCount: data.videoCount, + } ); + + const Description = useCallback( () => { + return ( + + { descriptionText } + + ); + }, [ descriptionText ] ); + + return ( + + + + ); +}; + +export default VideopressCard; diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/videopress-card/style.scss b/projects/packages/my-jetpack/_inc/components/product-cards-section/videopress-card/style.scss new file mode 100644 index 0000000000000..76eaec7aa103b --- /dev/null +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/videopress-card/style.scss @@ -0,0 +1,13 @@ +.videopress-card__video-count { + font-size: calc( var( --font-headline-small ) - 4px ); + color: var( --jp-gray-90 ); + line-height: 1.125; + + margin-top: calc( var( --spacing-base ) / 2 ); +} + +p.description { + color: var( --jp-gray-70 ); + font-size: var( --font-body-small ); + margin: 0 0 1rem; +} \ No newline at end of file diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/videopress-card/use-videopress-description.ts b/projects/packages/my-jetpack/_inc/components/product-cards-section/videopress-card/use-videopress-description.ts new file mode 100644 index 0000000000000..db68010f1f8d9 --- /dev/null +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/videopress-card/use-videopress-description.ts @@ -0,0 +1,31 @@ +import { __ } from '@wordpress/i18n'; +import preventWidows from '../../../utils/prevent-widows'; + +interface useVideoPressCardDescriptionProps { + isPluginActive: boolean; + videoCount: number; +} + +const useVideoPressCardDescription = ( { + isPluginActive, + videoCount, +}: useVideoPressCardDescriptionProps ) => { + if ( ! isPluginActive && videoCount ) { + return preventWidows( + __( 'Existing videos you could load faster without ads:', 'jetpack-my-jetpack' ) + ); + } + + if ( isPluginActive && ! videoCount ) { + return preventWidows( + __( + 'Stunning-quality, ad-free video in the WordPress Editor. Begin by uploading your first video.', + 'jetpack-my-jetpack' + ) + ); + } + + return ''; +}; + +export default useVideoPressCardDescription; diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/videopress-card/videopress-value-section.tsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/videopress-card/videopress-value-section.tsx new file mode 100644 index 0000000000000..aaf6954848620 --- /dev/null +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/videopress-card/videopress-value-section.tsx @@ -0,0 +1,18 @@ +import type { FC } from 'react'; + +import './style.scss'; + +interface VideoPressValueSectionProps { + isPluginActive: boolean; + data: Window[ 'myJetpackInitialState' ][ 'videopress' ]; +} + +const VideoPressValueSection: FC< VideoPressValueSectionProps > = ( { isPluginActive, data } ) => { + if ( ! isPluginActive && data.videoCount ) { + return { data.videoCount }; + } + + return null; +}; + +export default VideoPressValueSection; diff --git a/projects/packages/my-jetpack/_inc/data/constants.ts b/projects/packages/my-jetpack/_inc/data/constants.ts index 4f5830e6edbd9..2f09c4c96707f 100644 --- a/projects/packages/my-jetpack/_inc/data/constants.ts +++ b/projects/packages/my-jetpack/_inc/data/constants.ts @@ -32,3 +32,21 @@ export const QUERY_PURCHASES_KEY = 'purchases'; export const QUERY_EVALUATE_KEY = 'evaluate site recommendations'; export const QUERY_SAVE_EVALUATION_KEY = 'save site evaluation result'; export const QUERY_REMOVE_EVALUATION_KEY = 'remove site evaluation result'; + +// Product Slugs +export const PRODUCT_SLUGS = { + ANTI_SPAM: 'anti-spam', + BACKUP: 'backup', + BOOST: 'boost', + CRM: 'crm', + CREATOR: 'creator', + EXTRAS: 'extras', + JETPACK_AI: 'jetpack-ai', + SCAN: 'scan', + SEARCH: 'search', + SOCIAL: 'social', + SECURITY: 'security', + PROTECT: 'protect', + VIDEOPRESS: 'videopress', + STATS: 'stats', +} satisfies Record< string, JetpackModule >; diff --git a/projects/packages/my-jetpack/_inc/utils/prevent-widows.ts b/projects/packages/my-jetpack/_inc/utils/prevent-widows.ts new file mode 100644 index 0000000000000..81ab5152bbc06 --- /dev/null +++ b/projects/packages/my-jetpack/_inc/utils/prevent-widows.ts @@ -0,0 +1,44 @@ +type PreventWidowsFunction = ( text: string, wordsToKeep?: number ) => string; +type PreventWidowsInPartFunction = ( + text: string, + spacesToSubstitute: number +) => { part: string; substituted: number }; + +const reverseSpaceRegex = /\s+(\S*)$/; + +// This implementation was copied and slighly modified from the Calypso codebase +// Src: https://github.com/Automattic/wp-calypso/blob/trunk/client/lib/formatting/prevent-widows.js +// The instances where the part is an array or a component have been removed as we don't use the +// same translate function that Calypso does. +const preventWidowsInPart: PreventWidowsInPartFunction = ( part, spacesToSubstitute ) => { + let substituted = 0; + + if ( part && part.length > 0 ) { + let text = part; + let retVal = ''; + + // If the part is a string, work from the right looking for spaces + // TODO Work out if we can tell that this is a RTL language, and if it's appropriate to join words in this way + while ( substituted < spacesToSubstitute && reverseSpaceRegex.test( text ) ) { + const match = reverseSpaceRegex.exec( text ); + retVal = '\xA0' + match[ 1 ] + retVal; + text = text.replace( reverseSpaceRegex, '' ); + substituted++; + } + retVal = text + retVal; + // Return the modified string and the number of spaces substituted + return { part: retVal, substituted }; + } + + // For anything else e.g. an element without children, there's nothing to do. + return { part, substituted }; +}; + +const preventWidows: PreventWidowsFunction = ( text, wordsToKeep = 2 ) => { + return preventWidowsInPart( + 'string' === typeof text ? text.trim() : text, + Math.max( 1, wordsToKeep - 1 ) + ).part; +}; + +export default preventWidows; diff --git a/projects/packages/my-jetpack/changelog/add-value-to-videopress-card-inactive-state b/projects/packages/my-jetpack/changelog/add-value-to-videopress-card-inactive-state new file mode 100644 index 0000000000000..36112dd257a1f --- /dev/null +++ b/projects/packages/my-jetpack/changelog/add-value-to-videopress-card-inactive-state @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Add value to the inactive state on the VideoPress card in My Jetpack diff --git a/projects/packages/my-jetpack/global.d.ts b/projects/packages/my-jetpack/global.d.ts index 9a60ed9c37af4..f10c8dd9ab5fa 100644 --- a/projects/packages/my-jetpack/global.d.ts +++ b/projects/packages/my-jetpack/global.d.ts @@ -31,6 +31,7 @@ type JetpackModule = | 'crm' | 'creator' | 'extras' + | 'ai' | 'jetpack-ai' | 'scan' | 'search' @@ -241,6 +242,26 @@ interface Window { standalone_mode: boolean; }; }; + videopress: { + featuredStats: { + label: string; + data: { + views: { + current: number; + previous: number; + }; + impressions: { + current: number; + previous: number; + }; + watch_time: { + current: number; + previous: number; + }; + }; + }; + videoCount: number; + }; purchases: { items: Array< { ID: string; diff --git a/projects/packages/my-jetpack/src/class-initializer.php b/projects/packages/my-jetpack/src/class-initializer.php index 909700a440354..2b96ea802e97d 100644 --- a/projects/packages/my-jetpack/src/class-initializer.php +++ b/projects/packages/my-jetpack/src/class-initializer.php @@ -27,6 +27,7 @@ use Automattic\Jetpack\Sync\Functions as Sync_Functions; use Automattic\Jetpack\Terms_Of_Service; use Automattic\Jetpack\Tracking; +use Automattic\Jetpack\VideoPress\Stats as VideoPress_Stats; use Automattic\Jetpack\Waf\Waf_Runner; use Jetpack; use WP_Error; @@ -277,6 +278,7 @@ public static function enqueue_scripts() { array( 'blocked_logins' => (int) get_site_option( 'jetpack_protect_blocked_attempts', 0 ) ) ), ), + 'videopress' => self::get_videopress_stats(), ) ); @@ -298,6 +300,28 @@ public static function enqueue_scripts() { } } + /** + * Get stats for VideoPress + * + * @return array|WP_Error + */ + public static function get_videopress_stats() { + $video_count = array_sum( (array) wp_count_attachments( 'video' ) ); + + if ( ! class_exists( 'Automattic\Jetpack\VideoPress\Stats' ) ) { + return array( + 'videoCount' => $video_count, + ); + } + + $videopress_stats = new VideoPress_Stats(); + + return array( + 'featuredStats' => $videopress_stats->get_featured_stats(), + 'videoCount' => $video_count, + ); + } + /** * Get product slugs of the active purchases * @@ -313,7 +337,7 @@ public static function get_purchases() { function ( $purchase ) { return $purchase->product_slug; }, - $purchases + (array) $purchases ); } @@ -606,7 +630,7 @@ public static function get_site() { return new WP_Error( 'site_data_fetch_failed', 'Site data fetch failed', array( 'status' => $response_code ) ); } - return rest_ensure_response( $body, 200 ); + return rest_ensure_response( $body ); } /** @@ -640,7 +664,7 @@ public static function get_site_info() { /** * Returns whether a site has been determined "commercial" or not. * - * @return bool + * @return bool|null */ public static function is_commercial_site() { if ( is_wp_error( self::$site_info ) ) { @@ -666,7 +690,7 @@ public static function is_registered() { */ public static function dismiss_welcome_banner() { \Jetpack_Options::update_option( 'dismissed_welcome_banner', true ); - return rest_ensure_response( array( 'success' => true ), 200 ); + return rest_ensure_response( array( 'success' => true ) ); } /** diff --git a/projects/packages/my-jetpack/src/class-rest-product-data.php b/projects/packages/my-jetpack/src/class-rest-product-data.php index d49585ca11026..15530adb28174 100644 --- a/projects/packages/my-jetpack/src/class-rest-product-data.php +++ b/projects/packages/my-jetpack/src/class-rest-product-data.php @@ -107,14 +107,14 @@ public static function get_site_backup_undo_event() { } } - return rest_ensure_response( $undo_event, 200 ); + return rest_ensure_response( $undo_event ); } /** * This will collect a count of all the items that could be backed up * This is used to show what backup could be doing if it is not enabled * - * @return array + * @return WP_Error|\WP_REST_Response */ public static function count_things_that_can_be_backed_up() { $image_mime_type = 'image'; @@ -142,6 +142,6 @@ public static function count_things_that_can_be_backed_up() { // Add all audio attachments together to get the total audio count $data['total_audio_count'] = array_sum( (array) wp_count_attachments( $audio_mime_type ) ); - return rest_ensure_response( $data, 200 ); + return rest_ensure_response( $data ); } } diff --git a/projects/packages/my-jetpack/src/products/class-videopress.php b/projects/packages/my-jetpack/src/products/class-videopress.php index 307228664fd57..1ee6b122516cb 100644 --- a/projects/packages/my-jetpack/src/products/class-videopress.php +++ b/projects/packages/my-jetpack/src/products/class-videopress.php @@ -92,7 +92,7 @@ public static function get_title() { * @return string */ public static function get_description() { - return __( 'High-quality, ad-free video built specifically for WordPress', 'jetpack-my-jetpack' ); + return __( 'Stunning-quality, ad-free video in the WordPress Editor', 'jetpack-my-jetpack' ); } /** @@ -101,7 +101,7 @@ public static function get_description() { * @return string */ public static function get_long_description() { - return __( 'High-quality, ad-free video built specifically for WordPress.', 'jetpack-my-jetpack' ); + return __( 'Stunning-quality, ad-free video in the WordPress Editor', 'jetpack-my-jetpack' ); } /**