Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -108,6 +108,13 @@ const ProductCard: FC< ProductCardProps > = props => {
const isLoading =
isActionLoading || ( siteIsRegistering && status === PRODUCT_STATUSES.SITE_CONNECTION_ERROR );

// Reset isActionLoading when admin becomes false to prevent stuck loading state
useEffect( () => {
if ( ! admin ) {
setIsActionLoading( false );
}
}, [ admin ] );

const manageHandler = useCallback( () => {
recordEvent( 'jetpack_myjetpack_product_card_manage_click', {
product: slug,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Text, getRedirectUrl } from '@automattic/jetpack-components';
import { ExternalLink } from '@wordpress/components';
import { createInterpolateElement } from '@wordpress/element';
import { __, _n, sprintf } from '@wordpress/i18n';
import Gridicon from 'gridicons';
import PropTypes from 'prop-types';
Expand Down Expand Up @@ -75,9 +77,10 @@ const BackupCard = props => {
const { detail } = useProduct( productSlug );
const { status, doesModuleNeedAttention } = detail;
const lastBackupFailed = !! doesModuleNeedAttention;
const { status: lastBackupStatus } = doesModuleNeedAttention || {};
const lastBackupStatus = doesModuleNeedAttention?.data?.status || '';
const hasBackups = status === PRODUCT_STATUSES.ACTIVE || status === PRODUCT_STATUSES.CAN_UPGRADE;
const noDescription = () => null;
const { siteUrl = '' } = getMyJetpackWindowInitialState();

const { reasonContent, isLoading: isBackupFailedReasonLoading } =
useGetReadableFailedBackupReason() || {};
Expand All @@ -87,11 +90,45 @@ const BackupCard = props => {
return <WithBackupsValueSection slug={ productSlug } { ...props } />;
}

// Check if backups are deactivated (INACTIVE status with info type).
const isDeactivated =
status === PRODUCT_STATUSES.INACTIVE && lastBackupStatus === 'backups-deactivated';

const isError = status === PRODUCT_STATUSES.NEEDS_ATTENTION__ERROR && lastBackupFailed;

// Build support URL with pre-filled subject and site URL
const supportUrl = getRedirectUrl( 'jetpack-backup-support-reactivate', {
site: siteUrl,
query: `subject=${ encodeURIComponent(
__( 'Please reactivate Backup on my site', 'jetpack-my-jetpack' )
) }`,
} );

return (
<ProductCard slug={ productSlug } Description={ isError && noDescription } { ...props }>
<ProductCard
{ ...props }
slug={ productSlug }
Description={ ( isError || isDeactivated ) && noDescription }
admin={ isDeactivated ? false : props.admin }
>
{ isBackupFailedReasonLoading && <LoadingBlock height="75px" width="100%" /> }
{ isDeactivated && ! isBackupFailedReasonLoading && (
<div className={ styles.backupErrorContainer }>
<div className={ styles.contentContainer }>
<Text variant="body-small">
{ createInterpolateElement(
__(
'Backup was manually turned off. Please <a>contact support</a> to reactivate it.',
'jetpack-my-jetpack'
),
{
a: <ExternalLink href={ supportUrl } />,
}
) }
</Text>
</div>
</div>
) }
{ isError && ! isBackupFailedReasonLoading && (
<div className={ styles.backupErrorContainer }>
<div className={ styles.iconContainer }>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

Fix false "backup failed" error for deactivated backups
29 changes: 28 additions & 1 deletion projects/packages/my-jetpack/src/products/class-backup.php
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ public static function does_module_need_attention() {
}
}

if ( is_array( $backup_failed_status ) && $backup_failed_status['type'] === 'error' ) {
if ( is_array( $backup_failed_status ) ) {
set_transient( self::BACKUP_STATUS_TRANSIENT_KEY, $backup_failed_status, 5 * MINUTE_IN_SECONDS );
} else {
set_transient( self::BACKUP_STATUS_TRANSIENT_KEY, 'no_errors', HOUR_IN_SECONDS );
Expand Down Expand Up @@ -459,4 +459,31 @@ public static function get_paid_plan_product_slugs() {
'jetpack_backup_t0_monthly',
);
}

/**
* Override the product status to return INACTIVE when backups are deactivated.
*
* @return string
*/
public static function get_status() {
// Get the default status from parent.
$status = parent::get_status();

// Check if backups are deactivated (not an error, just manually turned off).
$needs_attention = static::does_module_need_attention();
if (
is_array( $needs_attention ) &&
isset( $needs_attention['data']['status'] ) &&
'backups-deactivated' === $needs_attention['data']['status']
) {
// Preserve NEEDS_PLAN status - user must purchase before reactivating.
if ( \Automattic\Jetpack\My_Jetpack\Products::STATUS_NEEDS_PLAN === $status ) {
return $status;
}

return \Automattic\Jetpack\My_Jetpack\Products::STATUS_INACTIVE;
}

return $status;
}
}
131 changes: 131 additions & 0 deletions projects/packages/my-jetpack/tests/php/Backup_Product_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,135 @@ public function test_backup_post_activation_url_with_backup_connected() {
activate_plugins( Backup::get_installed_plugin_filename() );
$this->assertSame( '', Backup::get_post_activation_url() );
}

/**
* Tests get_status() returns INACTIVE when backups are deactivated
*/
public function test_get_status_returns_inactive_when_backups_deactivated() {
activate_plugins( 'jetpack/jetpack.php' );

// Mock having a Backup plan by setting the purchases transient.
$mock_purchases = array(
(object) array(
'product_slug' => 'jetpack_backup_t0_monthly',
'expiry_status' => 'active',
'expiry_date' => gmdate( 'Y-m-d H:i:s', strtotime( '+1 year' ) ),
),
);
set_transient( Wpcom_Products::MY_JETPACK_PURCHASES_TRANSIENT_KEY, $mock_purchases, HOUR_IN_SECONDS );

// Mock the backup status transient with backups-deactivated status.
$deactivated_status = array(
'type' => 'error',
'data' => array(
'source' => 'rewind',
'status' => 'backups-deactivated',
'last_updated' => time(),
),
);
set_transient( Backup::BACKUP_STATUS_TRANSIENT_KEY, $deactivated_status, HOUR_IN_SECONDS );

$status = Backup::get_status();
// User has a plan but backups are deactivated -> show INACTIVE.
$this->assertSame( Products::STATUS_INACTIVE, $status );

delete_transient( Backup::BACKUP_STATUS_TRANSIENT_KEY );
delete_transient( Wpcom_Products::MY_JETPACK_PURCHASES_TRANSIENT_KEY );
}

/**
* Tests get_status() does NOT override NEEDS_PLAN when backups are deactivated
*/
public function test_get_status_preserves_needs_plan_when_backups_deactivated() {
// Default state: no mocking any purchases, so parent returns NEEDS_PLAN.
activate_plugins( 'jetpack/jetpack.php' );

// Mock the backup status transient with backups-deactivated status.
$deactivated_status = array(
'type' => 'error',
'data' => array(
'source' => 'rewind',
'status' => 'backups-deactivated',
'last_updated' => time(),
),
);
set_transient( Backup::BACKUP_STATUS_TRANSIENT_KEY, $deactivated_status, HOUR_IN_SECONDS );

$status = Backup::get_status();
// Should preserve NEEDS_PLAN, not override to INACTIVE.
// User needs to purchase a plan before they can use backups.
$this->assertSame( Products::STATUS_NEEDS_PLAN, $status );

delete_transient( Backup::BACKUP_STATUS_TRANSIENT_KEY );
}

/**
* Tests get_status() returns ACTIVE when no errors
*/
public function test_get_status_returns_active_when_no_errors() {
// Mock site connection.
( new Tokens() )->update_blog_token( 'test.test.1' );
( new Tokens() )->update_user_token( self::$user_id, 'test.test.' . self::$user_id, true );
Jetpack_Options::update_option( 'id', 123 );

activate_plugins( 'jetpack/jetpack.php' );

// Mock having a Backup plan.
$mock_purchases = array(
(object) array(
'product_slug' => 'jetpack_backup_t0_monthly',
'expiry_status' => 'active',
'expiry_date' => gmdate( 'Y-m-d H:i:s', strtotime( '+1 year' ) ),
),
);
set_transient( Wpcom_Products::MY_JETPACK_PURCHASES_TRANSIENT_KEY, $mock_purchases, HOUR_IN_SECONDS );

// Mock no backup errors (clean state).
set_transient( Backup::BACKUP_STATUS_TRANSIENT_KEY, 'no_errors', HOUR_IN_SECONDS );

$status = Backup::get_status();
$this->assertSame( Products::STATUS_ACTIVE, $status );

delete_transient( Backup::BACKUP_STATUS_TRANSIENT_KEY );
delete_transient( Wpcom_Products::MY_JETPACK_PURCHASES_TRANSIENT_KEY );
}

/**
* Tests get_status() returns NEEDS_ATTENTION__ERROR for real backup errors
*/
public function test_get_status_returns_error_when_backup_fails() {
// Mock site connection.
( new Tokens() )->update_blog_token( 'test.test.1' );
( new Tokens() )->update_user_token( self::$user_id, 'test.test.' . self::$user_id, true );
Jetpack_Options::update_option( 'id', 123 );

activate_plugins( 'jetpack/jetpack.php' );

// Mock having a Backup plan.
$mock_purchases = array(
(object) array(
'product_slug' => 'jetpack_backup_t0_monthly',
'expiry_status' => 'active',
'expiry_date' => gmdate( 'Y-m-d H:i:s', strtotime( '+1 year' ) ),
),
);
set_transient( Wpcom_Products::MY_JETPACK_PURCHASES_TRANSIENT_KEY, $mock_purchases, HOUR_IN_SECONDS );

// Mock a real backup error (NOT backups-deactivated).
$error_status = array(
'type' => 'error',
'data' => array(
'source' => 'last_backup',
'status' => 'error',
'last_updated' => time(),
),
);
set_transient( Backup::BACKUP_STATUS_TRANSIENT_KEY, $error_status, HOUR_IN_SECONDS );

$status = Backup::get_status();
$this->assertSame( Products::STATUS_NEEDS_ATTENTION__ERROR, $status );

delete_transient( Backup::BACKUP_STATUS_TRANSIENT_KEY );
delete_transient( Wpcom_Products::MY_JETPACK_PURCHASES_TRANSIENT_KEY );
}
}
Loading