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 "Expired" & "Expires soon" statuses to product cards #39816

Merged
merged 19 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
19 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
4535a36
Merge branch 'trunk' into add/mj-expired-products-cards
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
f417485
Fix get_site_current_plan() doctype return value.
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
0e37ba3
Fix red border on non-connected product cards.
elliottprogrammer Nov 25, 2024
210caad
Add option to hide expiration status & and get_renew_url function.
elliottprogrammer Nov 26, 2024
f0ef134
Merge branch 'trunk' into add/mj-expired-products-cards
elliottprogrammer Nov 27, 2024
6d94d2d
Fix error resulting from merge.
elliottprogrammer Nov 27, 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
Expand Up @@ -47,7 +47,7 @@ const ActionButton: FC< ActionButtonProps > = ( {
const [ isDropdownOpen, setIsDropdownOpen ] = useState( false );
const [ currentAction, setCurrentAction ] = useState< ComponentProps< typeof Button > >( {} );
const { detail } = useProduct( slug );
const { manageUrl, purchaseUrl } = detail;
const { manageUrl, purchaseUrl, managePaidPlanPurchaseUrl, renewPaidPlanPurchaseUrl } = detail;
const isManageDisabled = ! manageUrl;
const dropdownRef = useRef( null );
const chevronRef = useRef( null );
Expand Down Expand Up @@ -188,6 +188,26 @@ const ActionButton: FC< ActionButtonProps > = ( {
PRODUCT_STATUSES.INACTIVE in primaryActionOverride &&
primaryActionOverride[ PRODUCT_STATUSES.INACTIVE ] ),
};
case PRODUCT_STATUSES.EXPIRING_SOON:
return {
...buttonState,
href: renewPaidPlanPurchaseUrl,
variant: 'primary',
label: __( 'Renew my plan', 'jetpack-my-jetpack' ),
...( primaryActionOverride &&
PRODUCT_STATUSES.EXPIRING_SOON in primaryActionOverride &&
primaryActionOverride[ PRODUCT_STATUSES.EXPIRING_SOON ] ),
};
case PRODUCT_STATUSES.EXPIRED:
return {
...buttonState,
href: managePaidPlanPurchaseUrl,
variant: 'primary',
label: __( 'Resume my plan', 'jetpack-my-jetpack' ),
...( primaryActionOverride &&
PRODUCT_STATUSES.EXPIRED in primaryActionOverride &&
primaryActionOverride[ PRODUCT_STATUSES.EXPIRED ] ),
};
default:
return null;
}
Expand All @@ -207,6 +227,8 @@ const ActionButton: FC< ActionButtonProps > = ( {
onManage,
primaryActionOverride,
isOwned,
managePaidPlanPurchaseUrl,
renewPaidPlanPurchaseUrl,
] );

const allActions = useMemo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,8 @@ const ProductCard: FC< ProductCardProps > = props => {
const { ownedProducts } = getMyJetpackWindowInitialState( 'lifecycleStats' );
const isOwned = ownedProducts?.includes( slug );

const isError =
status === PRODUCT_STATUSES.SITE_CONNECTION_ERROR ||
status === PRODUCT_STATUSES.USER_CONNECTION_ERROR;
const isError = status === PRODUCT_STATUSES.EXPIRED;
const isWarning = status === PRODUCT_STATUSES.EXPIRING_SOON;
const isAbsent =
status === PRODUCT_STATUSES.ABSENT || status === PRODUCT_STATUSES.ABSENT_WITH_PLAN;
const isPurchaseRequired = status === PRODUCT_STATUSES.NEEDS_PLAN;
Expand All @@ -82,6 +81,7 @@ const ProductCard: FC< ProductCardProps > = props => {
[ styles[ 'is-purchase-required' ] ]: isPurchaseRequired,
[ styles[ 'is-link' ] ]: isAbsent,
[ styles[ 'has-error' ] ]: isError,
[ styles[ 'has-warning' ] ]: isWarning,
} );

const { recordEvent } = useAnalytics();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ const getStatusLabel: StatusStateFunction = ( status, isOwned ) => {
case PRODUCT_STATUSES.ACTIVE:
case PRODUCT_STATUSES.CAN_UPGRADE:
return __( 'Active', 'jetpack-my-jetpack' );
case PRODUCT_STATUSES.EXPIRING_SOON:
return __( 'Expires soon', 'jetpack-my-jetpack' );
case PRODUCT_STATUSES.EXPIRED:
return __( 'Expired plan', 'jetpack-my-jetpack' );
case PRODUCT_STATUSES.INACTIVE:
case PRODUCT_STATUSES.MODULE_DISABLED:
case PRODUCT_STATUSES.NEEDS_ACTIVATION:
Expand Down Expand Up @@ -49,13 +53,16 @@ const getStatusClassName: StatusStateFunction = ( status, isOwned ) => {
case PRODUCT_STATUSES.ABSENT_WITH_PLAN:
case PRODUCT_STATUSES.SITE_CONNECTION_ERROR:
case PRODUCT_STATUSES.USER_CONNECTION_ERROR:
case PRODUCT_STATUSES.EXPIRING_SOON:
return styles.warning;
case PRODUCT_STATUSES.INACTIVE:
case PRODUCT_STATUSES.NEEDS_FIRST_SITE_CONNECTION:
case PRODUCT_STATUSES.NEEDS_ACTIVATION:
return styles.inactive;
case PRODUCT_STATUSES.NEEDS_PLAN:
return isOwned ? styles.warning : styles.inactive;
case PRODUCT_STATUSES.EXPIRED:
return styles.error;
default:
return styles.inactive;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ $box-shadow-color: rgba( 0, 0, 0, 0.1 );
.container {
position: relative;
min-height: 200px;
&.has-error {
border-width: 1px;
border-color: var( --jp-red-50 );
}
&.has-warning {
border-width: 1px;
border-color: #DEB100;
}
}

.description {
Expand Down
2 changes: 2 additions & 0 deletions projects/packages/my-jetpack/_inc/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@ export const PRODUCT_STATUSES = {
NEEDS_FIRST_SITE_CONNECTION: 'needs_first_site_connection',
USER_CONNECTION_ERROR: 'user_connection_error',
CAN_UPGRADE: 'can_upgrade',
EXPIRING_SOON: 'expiring',
EXPIRED: 'expired',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: added

Added "Expired" & "Expires soon" statuses to My Jetpack product cards.
2 changes: 2 additions & 0 deletions projects/packages/my-jetpack/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ interface Window {
plugin_slug: string;
post_activation_url: string;
post_checkout_url?: string;
manage_paid_plan_purchase_url?: string;
renew_paid_plan_purchase_url?: string;
pricing_for_ui?: {
available: boolean;
wpcom_product_slug: string;
Expand Down
14 changes: 14 additions & 0 deletions projects/packages/my-jetpack/src/class-products.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class Products {
const STATUS_USER_CONNECTION_ERROR = 'user_connection_error';
const STATUS_ACTIVE = 'active';
const STATUS_CAN_UPGRADE = 'can_upgrade';
const STATUS_EXPIRING_SOON = 'expiring';
const STATUS_EXPIRED = 'expired';
const STATUS_INACTIVE = 'inactive';
const STATUS_MODULE_DISABLED = 'module_disabled';
const STATUS_PLUGIN_ABSENT = 'plugin_absent';
Expand Down Expand Up @@ -76,6 +78,16 @@ class Products {
self::STATUS_CAN_UPGRADE,
);

/**
* List of statuses that display the module as active
*
* @var array
*/
public static $expiring_or_expired_module_statuses = array(
self::STATUS_EXPIRING_SOON,
self::STATUS_EXPIRED,
);

/**
* List of all statuses that a product can have
*
Expand All @@ -86,6 +98,8 @@ class Products {
self::STATUS_USER_CONNECTION_ERROR,
self::STATUS_ACTIVE,
self::STATUS_CAN_UPGRADE,
self::STATUS_EXPIRING_SOON,
self::STATUS_EXPIRED,
self::STATUS_INACTIVE,
self::STATUS_MODULE_DISABLED,
self::STATUS_PLUGIN_ABSENT,
Expand Down
10 changes: 10 additions & 0 deletions projects/packages/my-jetpack/src/class-wpcom-products.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Current_Plan;
use Automattic\Jetpack\Status\Visitor;
use Jetpack_Options;
use WP_Error;
Expand Down Expand Up @@ -349,6 +350,15 @@ public static function get_site_current_purchases() {
return $purchases;
}

/**
* Gets the site's currently active "plan" (bundle).
*
* @return array
*/
public static function get_site_current_plan() {
return Current_Plan::get();
}

/**
* Reset the request failures to retry the API requests.
*
Expand Down
55 changes: 29 additions & 26 deletions projects/packages/my-jetpack/src/products/class-anti-spam.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ class Anti_Spam extends Product {
*/
public static $plugin_slug = 'akismet';

/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'antispam';

/**
* Whether this product requires a user connection
*
Expand Down Expand Up @@ -107,41 +114,37 @@ public static function get_features() {
}

/**
* Determine if the site has an Akismet plan by checking for an API key
* Note that some Akismet Plans are free - we're just checking for an API key and don't have the perspective of the plan attached to it here
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return bool - whether an API key was found
* @return array
*/
public static function has_paid_plan_for_product() {
$products_with_anti_spam = array(
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_anti_spam',
'jetpack_complete',
'jetpack_security',
'jetpack_personal',
'jetpack_premium',
'jetpack_business',
'jetpack_anti_spam_monthly',
'jetpack_anti_spam_bi_yearly',
);
// Check if the site has an API key for Akismet
$akismet_api_key = apply_filters( 'akismet_get_api_key', defined( 'WPCOM_API_KEY' ) ? constant( 'WPCOM_API_KEY' ) : get_option( 'wordpress_api_key' ) );
$fallback = ! empty( $akismet_api_key );
}

// Check for existing plans
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return $fallback;
/**
* Determine if the site has an Akismet plan.
*
* @return bool - whether an API key was found
*/
public static function has_paid_plan_for_product() {
if ( parent::has_paid_plan_for_product() ) {
return true;
}
// As a fallback, we're checking if the site has an API key for Akismet.
// Note that some Akismet Plans are free - we're just checking for an API key and don't have the perspective of the plan attached to it here
$akismet_api_key = apply_filters( 'akismet_get_api_key', defined( 'WPCOM_API_KEY' ) ? constant( 'WPCOM_API_KEY' ) : get_option( 'wordpress_api_key' ) );
if ( ! empty( $akismet_api_key ) ) {
return true;

if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
foreach ( $products_with_anti_spam as $product ) {
if ( strpos( $purchase->product_slug, $product ) !== false ) {
return true;
}
}
}
}

return $fallback;
return false;
}

/**
Expand Down
42 changes: 29 additions & 13 deletions projects/packages/my-jetpack/src/products/class-backup.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ class Backup extends Hybrid_Product {
*/
public static $requires_plan = true;

/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'backups';

/**
* Get the product name
*
Expand Down Expand Up @@ -193,19 +200,6 @@ private static function get_state_from_wpcom() {
return $status;
}

/**
* Checks whether the current plan (or purchases) of the site already supports the product
*
* @return boolean
*/
public static function has_paid_plan_for_product() {
$rewind_data = static::get_state_from_wpcom();
if ( is_wp_error( $rewind_data ) ) {
return false;
}
return is_object( $rewind_data ) && isset( $rewind_data->state ) && 'unavailable' !== $rewind_data->state;
}

/**
* Return product bundles list
* that supports the product.
Expand Down Expand Up @@ -239,4 +233,26 @@ public static function get_manage_url() {
return Redirect::get_url( 'my-jetpack-manage-backup' );
}
}

/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_backup_daily',
'jetpack_backup_daily_monthly',
'jetpack_backup_realtime',
'jetpack_backup_realtime_monthly',
'jetpack_backup_t1_yearly',
'jetpack_backup_t1_monthly',
'jetpack_backup_t1_bi_yearly',
'jetpack_backup_t2_yearly',
'jetpack_backup_t2_monthly',
'jetpack_backup_t0_yearly',
'jetpack_backup_t0_monthly',
);
}
}
42 changes: 21 additions & 21 deletions projects/packages/my-jetpack/src/products/class-boost.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ class Boost extends Product {
*/
public static $has_free_offering = true;

/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'cloud-critical-css';

/**
* Get the product name
*
Expand Down Expand Up @@ -341,27 +348,6 @@ public static function get_pricing_for_ui() {
);
}

/**
* Checks whether the current plan (or purchases) of the site already supports the product
*
* @return boolean
*/
public static function has_paid_plan_for_product() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
// Boost is available as standalone bundle and as part of the Complete plan.
if ( strpos( $purchase->product_slug, 'jetpack_boost' ) !== false || str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ) {
return true;
}
}
}
return false;
}

/**
* Get the URL where the user manages the product
*
Expand Down Expand Up @@ -392,6 +378,20 @@ public static function do_product_specific_activation( $current_result ) {
return $product_activation;
}

/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_boost_yearly',
'jetpack_boost_monthly',
'jetpack_boost_bi_yearly',
);
}

/**
* Return product bundles list
* that supports the product.
Expand Down
Loading
Loading