diff --git a/projects/packages/my-jetpack/.phan/baseline.php b/projects/packages/my-jetpack/.phan/baseline.php index dd9d2bb32018e..50deef4b803f2 100644 --- a/projects/packages/my-jetpack/.phan/baseline.php +++ b/projects/packages/my-jetpack/.phan/baseline.php @@ -37,7 +37,7 @@ 'src/class-rest-zendesk-chat.php' => ['PhanParamTooMany'], 'src/class-wpcom-products.php' => ['PhanTypeMismatchReturnProbablyReal'], 'src/products/class-anti-spam.php' => ['PhanTypeMismatchArgumentNullable', 'PhanTypeMismatchPropertyDefault'], - 'src/products/class-backup.php' => ['PhanTypeMismatchArgumentNullable', 'PhanTypeMismatchPropertyDefault'], + 'src/products/class-backup.php' => ['PhanTypeMismatchArgumentNullable', 'PhanTypeMismatchPropertyDefault', 'PhanTypeSuspiciousNonTraversableForeach'], 'src/products/class-boost.php' => ['PhanTypeMismatchPropertyDefault'], 'src/products/class-creator.php' => ['PhanTypeMismatchPropertyDefault', 'PhanTypeMismatchReturnProbablyReal'], 'src/products/class-crm.php' => ['PhanTypeMismatchPropertyDefault'], diff --git a/projects/packages/my-jetpack/_inc/components/product-card/action-button.tsx b/projects/packages/my-jetpack/_inc/components/product-card/action-button.tsx index 61984095e7a30..82878556ba7c6 100644 --- a/projects/packages/my-jetpack/_inc/components/product-card/action-button.tsx +++ b/projects/packages/my-jetpack/_inc/components/product-card/action-button.tsx @@ -44,6 +44,8 @@ const ActionButton: FC< ActionButtonProps > = ( { upgradeInInterstitial, isOwned, } ) => { + const troubleshootBackupsUrl = + 'https://jetpack.com/support/backup/troubleshooting-jetpack-backup/'; const [ isDropdownOpen, setIsDropdownOpen ] = useState( false ); const [ currentAction, setCurrentAction ] = useState< ComponentProps< typeof Button > >( {} ); const { detail } = useProduct( slug ); @@ -208,6 +210,16 @@ const ActionButton: FC< ActionButtonProps > = ( { PRODUCT_STATUSES.EXPIRED in primaryActionOverride && primaryActionOverride[ PRODUCT_STATUSES.EXPIRED ] ), }; + case PRODUCT_STATUSES.NEEDS_ATTENTION: + return { + ...buttonState, + href: troubleshootBackupsUrl, + variant: 'primary', + label: __( 'Troubleshoot', 'jetpack-my-jetpack' ), + ...( primaryActionOverride && + PRODUCT_STATUSES.NEEDS_ATTENTION in primaryActionOverride && + primaryActionOverride[ PRODUCT_STATUSES.NEEDS_ATTENTION ] ), + }; default: return null; } 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 72b536197a895..5fbe96fd6f7e5 100644 --- a/projects/packages/my-jetpack/_inc/components/product-card/index.tsx +++ b/projects/packages/my-jetpack/_inc/components/product-card/index.tsx @@ -70,7 +70,8 @@ const ProductCard: FC< ProductCardProps > = props => { const { ownedProducts } = getMyJetpackWindowInitialState( 'lifecycleStats' ); const isOwned = ownedProducts?.includes( slug ); - const isError = status === PRODUCT_STATUSES.EXPIRED; + const isError = + status === PRODUCT_STATUSES.EXPIRED || status === PRODUCT_STATUSES.NEEDS_ATTENTION; const isWarning = status === PRODUCT_STATUSES.EXPIRING_SOON; const isAbsent = status === PRODUCT_STATUSES.ABSENT || status === PRODUCT_STATUSES.ABSENT_WITH_PLAN; diff --git a/projects/packages/my-jetpack/_inc/components/product-card/status.tsx b/projects/packages/my-jetpack/_inc/components/product-card/status.tsx index 51e6d4ed429e7..f46baca25a7d3 100644 --- a/projects/packages/my-jetpack/_inc/components/product-card/status.tsx +++ b/projects/packages/my-jetpack/_inc/components/product-card/status.tsx @@ -40,6 +40,8 @@ const getStatusLabel: StatusStateFunction = ( status, isOwned ) => { const inactiveText = __( 'Inactive', 'jetpack-my-jetpack' ); return isOwned ? needsPlanText : inactiveText; } + case PRODUCT_STATUSES.NEEDS_ATTENTION: + return __( 'Needs attention', 'jetpack-my-jetpack' ); default: return __( 'Inactive', 'jetpack-my-jetpack' ); } @@ -62,6 +64,7 @@ const getStatusClassName: StatusStateFunction = ( status, isOwned ) => { case PRODUCT_STATUSES.NEEDS_PLAN: return isOwned ? styles.warning : styles.inactive; case PRODUCT_STATUSES.EXPIRED: + case PRODUCT_STATUSES.NEEDS_ATTENTION: return styles.error; default: return styles.inactive; diff --git a/projects/packages/my-jetpack/_inc/constants.ts b/projects/packages/my-jetpack/_inc/constants.ts index af4cdacb11fe5..1503114ca4de0 100644 --- a/projects/packages/my-jetpack/_inc/constants.ts +++ b/projects/packages/my-jetpack/_inc/constants.ts @@ -41,4 +41,5 @@ export const PRODUCT_STATUSES = { CAN_UPGRADE: 'can_upgrade', EXPIRING_SOON: 'expiring', EXPIRED: 'expired', + NEEDS_ATTENTION: 'needs_attention', }; diff --git a/projects/packages/my-jetpack/changelog/add-backup-needs-attention-status b/projects/packages/my-jetpack/changelog/add-backup-needs-attention-status new file mode 100644 index 0000000000000..ba8a03a3a2ebc --- /dev/null +++ b/projects/packages/my-jetpack/changelog/add-backup-needs-attention-status @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +My Jetpack: Add 'Needs attention' status to Backup product card when Backups are failing. diff --git a/projects/packages/my-jetpack/src/class-products.php b/projects/packages/my-jetpack/src/class-products.php index 8f8681fdb2ab8..33e894329a507 100644 --- a/projects/packages/my-jetpack/src/class-products.php +++ b/projects/packages/my-jetpack/src/class-products.php @@ -29,6 +29,7 @@ class Products { const STATUS_NEEDS_PLAN = 'needs_plan'; const STATUS_NEEDS_ACTIVATION = 'needs_activation'; const STATUS_NEEDS_FIRST_SITE_CONNECTION = 'needs_first_site_connection'; + const STATUS_NEEDS_ATTENTION = 'needs_attention'; /** * List of statuses that display the module as disabled @@ -107,6 +108,7 @@ class Products { self::STATUS_NEEDS_PLAN, self::STATUS_NEEDS_ACTIVATION, self::STATUS_NEEDS_FIRST_SITE_CONNECTION, + self::STATUS_NEEDS_ATTENTION, ); /** diff --git a/projects/packages/my-jetpack/src/products/class-backup.php b/projects/packages/my-jetpack/src/products/class-backup.php index 8566a4b8738bf..bee360c09ee2a 100644 --- a/projects/packages/my-jetpack/src/products/class-backup.php +++ b/projects/packages/my-jetpack/src/products/class-backup.php @@ -1,6 +1,6 @@ 2 ), null, 'wpcom' ); + $response = Client::wpcom_json_api_request_as_blog( + sprintf( '/sites/%d/rewind', $site_id ) . '?force=wpcom', + '2', + array( 'timeout' => 2 ), + null, + 'wpcom' + ); if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { $status = new WP_Error( 'rewind_state_fetch_failed' ); @@ -200,6 +205,90 @@ private static function get_state_from_wpcom() { return $status; } + /** + * Hits the wpcom api to retrieve the last 10 backup records. + * + * @return Object|WP_Error + */ + public static function get_latest_backups() { + static $backups = null; + + if ( $backups !== null ) { + return $backups; + } + + $site_id = \Jetpack_Options::get_option( 'id' ); + $response = Client::wpcom_json_api_request_as_blog( + sprintf( '/sites/%d/rewind/backups', $site_id ) . '?force=wpcom', + '2', + array( 'timeout' => 2 ), + null, + 'wpcom' + ); + + if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { + $backups = new WP_Error( 'rewind_backups_fetch_failed' ); + return $backups; + } + + $body = wp_remote_retrieve_body( $response ); + $backups = json_decode( $body ); + return $backups; + } + + /** + * Determines whether the module/plugin/product needs the users attention. + * Typically due to some sort of error where user troubleshooting is needed. + * + * @return boolean|array + */ + public static function does_module_need_attention() { + $backup_failed_status = false; + // First check the status of Rewind for failure. + $rewind_state = self::get_state_from_wpcom(); + if ( ! is_wp_error( $rewind_state ) ) { + $backup_failure_reasons = array( + 'unknown_error', + 'no_site_found', + 'missing_plan', + 'no_connected_jetpack', + 'no_connected_jetpack_with_credentials', + 'multisite_not_supported', + 'host_not_supported', + 'vp_active_on_site', + ); + if ( $rewind_state->state === 'unavailable' && ! empty( $rewind_state->reason ) && in_array( $rewind_state->reason, $backup_failure_reasons, true ) ) { + $backup_failed_status = array( + 'status' => $rewind_state->reason, + 'last_updated' => $rewind_state->last_updated, + ); + } + } + // Next check for a failed last backup. + $latest_backups = self::get_latest_backups(); + if ( ! is_wp_error( $latest_backups ) ) { + // Get the last/latest backup record. + $last_backup = null; + foreach ( $latest_backups as $backup ) { + if ( $backup->is_backup ) { + $last_backup = $backup; + break; + } + } + + if ( $last_backup && isset( $last_backup->status ) ) { + if ( $last_backup->status === 'not-accessible' || $last_backup->status === 'error' || $last_backup->status === 'credential-error' ) { + $backup_failed_status = array( + 'status' => $last_backup->status, + 'last_updated' => $last_backup->last_updated, + ); + } + } + } + + return $backup_failed_status; + } + /** * Return product bundles list * that supports the product. diff --git a/projects/packages/my-jetpack/src/products/class-product.php b/projects/packages/my-jetpack/src/products/class-product.php index 128d00ea3307c..b2262eddd6b4a 100644 --- a/projects/packages/my-jetpack/src/products/class-product.php +++ b/projects/packages/my-jetpack/src/products/class-product.php @@ -10,6 +10,7 @@ use Automattic\Jetpack\Connection\Client; use Automattic\Jetpack\Connection\Manager as Connection_Manager; use Automattic\Jetpack\Modules; +use Automattic\Jetpack\My_Jetpack\Products\Backup; use Automattic\Jetpack\Plugins_Installer; use Automattic\Jetpack\Status; use Jetpack_Options; @@ -716,6 +717,9 @@ public static function get_status() { } elseif ( static::$requires_user_connection && ! ( new Connection_Manager() )->has_connected_owner() ) { $status = Products::STATUS_USER_CONNECTION_ERROR; } elseif ( static::has_paid_plan_for_product() ) { + if ( static::$slug === 'backup' && Backup::does_module_need_attention() ) { + $status = Products::STATUS_NEEDS_ATTENTION; + } if ( static::is_paid_plan_expired() ) { $status = Products::STATUS_EXPIRED; } elseif ( static::is_paid_plan_expiring() ) { @@ -987,4 +991,14 @@ public static function install_and_activate_standalone() { return true; } + + /** + * Determines whether the module/plugin/product needs the users attention. + * Typically due to some sort of error where user troubleshooting is needed. + * + * @return boolean + */ + public static function does_module_need_attention() { + return false; + } }