Skip to content

Commit

Permalink
Update/conditionally hide connection banner (#37707)
Browse files Browse the repository at this point in the history
* Add mechanism to track previously working plugins

* Update project version

* Rework how and when list is updated

* Update array update on my jetpack and get only array values for the array

* Remove new action

* Link connection banner to broken modules data

* Separate broken modules into site and user connection issues

* Fix a few issues with determining user vs site connection

* changelog

* Show connection banner as info conditionally

* Add constants for statuses

* Show errors before info

* Add additional tracking fields to notice if red bubble slug has additional info

* Change how additional tracks args are added

* Add all_statuses variable

* Early return red bubble connection slug if possible
  • Loading branch information
CodeyGuyDylan authored and dilirity committed Jun 11, 2024
1 parent 0924da7 commit dc156d0
Show file tree
Hide file tree
Showing 13 changed files with 213 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,13 @@ const GlobalNotice = ( { message, title, options } ) => {
const { recordEvent } = useAnalytics();

useEffect( () => {
const tracksArgs = options?.tracksArgs || {};

recordEvent( 'jetpack_myjetpack_global_notice_view', {
noticeId: options.id,
...tracksArgs,
} );
}, [ options.id, recordEvent ] );
}, [ options.id, recordEvent, options?.tracksArgs ] );

const [ isBiggerThanMedium ] = useBreakpointMatch( [ 'md' ], [ '>' ] );

Expand Down
20 changes: 12 additions & 8 deletions projects/packages/my-jetpack/_inc/context/notices/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@ export type NoticeButtonAction = NoticeAction & {
export type Notice = {
message: string | ReactNode;
title?: string;
options: {
id?: string;
level: string;
actions?: NoticeButtonAction[];
priority: number;
hideCloseButton?: boolean;
onClose?: () => void;
};
options: NoticeOptions;
};

export type NoticeOptions = {
id?: string;
level: 'error' | 'warning' | 'success' | 'info';
actions?: NoticeButtonAction[];
priority: number;
hideCloseButton?: boolean;
onClose?: () => void;
isRedBubble?: boolean;
tracksArgs?: Record< string, unknown >;
};

export type NoticeContextType< T = Notice > = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const getProductSlugsThatRequireUserConnection = ( products: {
.filter(
( { requiresUserConnection, status } ) =>
requiresUserConnection &&
( status === PRODUCT_STATUSES.ACTIVE || status === PRODUCT_STATUSES.ERROR )
( status === PRODUCT_STATUSES.ACTIVE || PRODUCT_STATUSES.USER_CONNECTION_ERROR )
)
.map( ( { name } ) => name );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useContext, useEffect } from 'react';
import { NOTICE_PRIORITY_MEDIUM } from '../../context/constants';
import { NoticeContext } from '../../context/notices/noticeContext';
import useAnalytics from '../use-analytics';
import type { NoticeOptions } from '../../context/notices/types';

type RedBubbleAlerts = Window[ 'myJetpackInitialState' ][ 'redBubbleAlerts' ];

Expand Down Expand Up @@ -40,7 +41,7 @@ const useBadInstallNotice = ( redBubbleAlerts: RedBubbleAlerts ) => {
} );
};

const noticeOptions = {
const noticeOptions: NoticeOptions = {
id: 'bad-installation-notice',
level: 'error',
actions: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useContext, useEffect } from 'react';
import { NOTICE_PRIORITY_HIGH } from '../../context/constants';
import { NoticeContext } from '../../context/notices/noticeContext';
import useAnalytics from '../use-analytics';
import type { NoticeOptions } from '../../context/notices/types';

const useConnectionErrorsNotice = () => {
const { setNotice, resetNotice } = useContext( NoticeContext );
Expand Down Expand Up @@ -48,7 +49,7 @@ const useConnectionErrorsNotice = () => {
const loadingButtonLabel = __( 'Reconnecting Jetpack…', 'jetpack-my-jetpack' );
const restoreButtonLabel = __( 'Restore Connection', 'jetpack-my-jetpack' );

const noticeOptions = {
const noticeOptions: NoticeOptions = {
id: 'connection-error-notice',
level: 'error',
actions: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,36 @@ import { useAllProducts } from '../../data/products/use-product';
import { getMyJetpackWindowRestState } from '../../data/utils/get-my-jetpack-window-state';
import getProductSlugsThatRequireUserConnection from '../../data/utils/get-product-slugs-that-require-user-connection';
import useAnalytics from '../use-analytics';
import useMyJetpackConnection from '../use-my-jetpack-connection';
import useMyJetpackNavigate from '../use-my-jetpack-navigate';
import type { NoticeOptions } from '../../context/notices/types';

type RedBubbleAlerts = Window[ 'myJetpackInitialState' ][ 'redBubbleAlerts' ];

const useSiteConnectionNotice = ( redBubbleAlerts: RedBubbleAlerts ) => {
const { recordEvent } = useAnalytics();
const { setNotice, resetNotice } = useContext( NoticeContext );
const { apiRoot, apiNonce } = getMyJetpackWindowRestState();
const { isRegistered, isUserConnected, hasConnectedOwner } = useMyJetpackConnection();
const { siteIsRegistering, handleRegisterSite } = useConnection( {
skipUserConnection: true,
apiRoot,
apiNonce,
from: 'my-jetpack',
redirectUri: null,
autoTrigger: false,
} );
const products = useAllProducts();
const navToConnection = useMyJetpackNavigate( MyJetpackRoutes.Connection );
const redBubbleSlug = 'missing-connection';
const connectionError = redBubbleAlerts[ redBubbleSlug ];

useEffect( () => {
if ( ! Object.keys( redBubbleAlerts ).includes( 'missing-site-connection' ) ) {
if ( ! connectionError ) {
return;
}

const productSlugsThatRequireUserConnection =
getProductSlugsThatRequireUserConnection( products );
const requiresUserConnection =
! hasConnectedOwner && ! isUserConnected && productSlugsThatRequireUserConnection.length > 0;

if ( ! requiresUserConnection && isRegistered ) {
return;
}
const requiresUserConnection = connectionError.type === 'user';

const onActionButtonClick = () => {
if ( requiresUserConnection ) {
Expand All @@ -62,6 +60,8 @@ const useSiteConnectionNotice = ( redBubbleAlerts: RedBubbleAlerts ) => {
onClose: resetNotice,
},
} );
delete redBubbleAlerts[ redBubbleSlug ];
window.myJetpackInitialState.redBubbleAlerts = redBubbleAlerts;
} );
};

Expand Down Expand Up @@ -95,9 +95,9 @@ const useSiteConnectionNotice = ( redBubbleAlerts: RedBubbleAlerts ) => {
title: __( 'Missing site connection', 'jetpack-my-jetpack' ),
};

const noticeOptions = {
id: requiresUserConnection ? 'user-connection-notice' : 'site-connection-notice',
level: 'info',
const noticeOptions: NoticeOptions = {
id: redBubbleSlug,
level: connectionError.is_error ? 'error' : 'info',
actions: [
{
label: requiresUserConnection
Expand All @@ -112,6 +112,10 @@ const useSiteConnectionNotice = ( redBubbleAlerts: RedBubbleAlerts ) => {
// If this notice gets into a loading state, we want to show it above the rest
priority: NOTICE_PRIORITY_HIGH + ( siteIsRegistering ? 1 : 0 ),
isRedBubble: true,
tracksArgs: {
type: connectionError.type,
isError: connectionError.is_error,
},
};

const messageContent = requiresUserConnection ? (
Expand All @@ -130,16 +134,14 @@ const useSiteConnectionNotice = ( redBubbleAlerts: RedBubbleAlerts ) => {
} );
}, [
handleRegisterSite,
hasConnectedOwner,
isRegistered,
isUserConnected,
navToConnection,
products,
recordEvent,
redBubbleAlerts,
resetNotice,
setNotice,
siteIsRegistering,
connectionError,
] );
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

Conditionally show connection banner as error or info
7 changes: 6 additions & 1 deletion projects/packages/my-jetpack/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ interface Window {
videoPressStats: boolean;
};
lifecycleStats: {
historicallyActiveModules: Array< string >;
brokenModules: Array< string >;
isSiteConnected: boolean;
isUserConnected: boolean;
jetpackPlugins: Array< string >;
Expand Down Expand Up @@ -195,7 +197,10 @@ interface Window {
} >;
};
redBubbleAlerts: {
'missing-site-connection'?: null;
'missing-connection'?: {
type: string;
is_error: boolean;
};
'welcome-banner-active'?: null;
[ key: `${ string }-bad-installation` ]: {
data: {
Expand Down
97 changes: 70 additions & 27 deletions projects/packages/my-jetpack/src/class-initializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class Initializer {

const UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY = 'update-historically-active-jetpack-modules';

const MISSING_SITE_CONNECTION_NOTIFICATION_KEY = 'missing-site-connection';
const MISSING_CONNECTION_NOTIFICATION_KEY = 'missing-connection';

/**
* Holds info/data about the site (from the /sites/%d endpoint)
Expand Down Expand Up @@ -238,6 +238,7 @@ public static function enqueue_scripts() {
'lifecycleStats' => array(
'jetpackPlugins' => self::get_installed_jetpack_plugins(),
'historicallyActiveModules' => \Jetpack_Options::get_option( 'historically_active_modules', array() ),
'brokenModules' => self::check_for_broken_modules(),
'isSiteConnected' => $connection->is_connected(),
'isUserConnected' => $connection->is_user_connected(),
'purchases' => self::get_purchases(),
Expand Down Expand Up @@ -538,46 +539,27 @@ public static function setup_historically_active_jetpack_modules_sync() {
public static function update_historically_active_jetpack_modules() {
$historically_active_modules = \Jetpack_Options::get_option( 'historically_active_modules', array() );
$products = Products::get_products();
$active_module_statuses = array(
'active',
'can_upgrade',
);
$broken_module_statuses = array(
'site_connection_error',
'user_connection_error',
);
// This is defined as the statuses in which the user willingly has the module disabled whether it be by
// default, uninstalling the plugin, disabling the module, or not renewing their plan.
$disabled_module_statuses = array(
'inactive',
'module_disabled',
'plugin_absent',
'plugin_absent_with_plan',
'needs_purchase',
'needs_purchase_or_free',
'needs_first_site_connection',
);

foreach ( $products as $product ) {
$status = $product['status'];
$product_slug = $product['slug'];
// We want to leave modules in the array if they've been active in the past
// and were not manually disabled by the user.
if ( in_array( $status, $broken_module_statuses, true ) ) {
if ( in_array( $status, Products::$broken_module_statuses, true ) ) {
continue;
}

// If the module is active and not already in the array, add it
if (
in_array( $status, $active_module_statuses, true ) &&
in_array( $status, Products::$active_module_statuses, true ) &&
! in_array( $product_slug, $historically_active_modules, true )
) {
$historically_active_modules[] = $product_slug;
}

// If the module has been disabled due to a manual user action,
// or because of a missing plan error, remove it from the array
if ( in_array( $status, $disabled_module_statuses, true ) ) {
if ( in_array( $status, Products::$disabled_module_statuses, true ) ) {
$historically_active_modules = array_values( array_diff( $historically_active_modules, array( $product_slug ) ) );
}
}
Expand Down Expand Up @@ -668,7 +650,7 @@ public static function dismiss_welcome_banner() {
/**
* Returns true if the site has file write access to the plugins folder, false otherwise.
*
* @return bool
* @return string
**/
public static function has_file_system_write_access() {

Expand Down Expand Up @@ -756,6 +738,34 @@ public static function get_red_bubble_alerts() {
return $red_bubble_alerts;
}

/**
* Check for features broken by a disconnected user or site
*
* @return array
*/
public static function check_for_broken_modules() {
$broken_modules = array(
'needs_site_connection' => array(),
'needs_user_connection' => array(),
);
$products = Products::get_products();
$historically_active_modules = \Jetpack_Options::get_option( 'historically_active_modules', array() );

foreach ( $historically_active_modules as $module ) {
$product = $products[ $module ];

// If the site or user is disconnected, and the product requires a user connection
// mark the product as a broken module needing user connection
if ( in_array( $product['status'], Products::$broken_module_statuses, true ) && $product['requires_user_connection'] ) {
$broken_modules['needs_user_connection'][] = $module;
} elseif ( $product['status'] === Products::STATUS_SITE_CONNECTION_ERROR ) {
$broken_modules['needs_site_connection'][] = $module;
}
}

return $broken_modules;
}

/**
* Add relevant red bubble notifications
*
Expand All @@ -768,7 +778,7 @@ public static function add_red_bubble_alerts( array $red_bubble_slugs ) {
$red_bubble_slugs['welcome-banner-active'] = null;
return $red_bubble_slugs;
} else {
return self::alert_if_missing_site_connection( $red_bubble_slugs );
return self::alert_if_missing_connection( $red_bubble_slugs );
}
}

Expand All @@ -778,9 +788,42 @@ public static function add_red_bubble_alerts( array $red_bubble_slugs ) {
* @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
* @return array
*/
public static function alert_if_missing_site_connection( array $red_bubble_slugs ) {
public static function alert_if_missing_connection( array $red_bubble_slugs ) {
$broken_modules = self::check_for_broken_modules();

if ( ! empty( $broken_modules['needs_user_connection'] ) ) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'user',
'is_error' => true,
);
return $red_bubble_slugs;
}

if ( ! empty( $broken_modules['needs_site_connection'] ) ) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'site',
'is_error' => true,
);
return $red_bubble_slugs;
}

if (
! ( new Connection_Manager() )->is_user_connected() &&
! ( new Connection_Manager() )->has_connected_owner()
) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'user',
'is_error' => false,
);
return $red_bubble_slugs;
}

if ( ! ( new Connection_Manager() )->is_connected() ) {
$red_bubble_slugs[ self::MISSING_SITE_CONNECTION_NOTIFICATION_KEY ] = null;
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'site',
'is_error' => false,
);
return $red_bubble_slugs;
}

return $red_bubble_slugs;
Expand Down
Loading

0 comments on commit dc156d0

Please sign in to comment.