Skip to content

Commit 80196e8

Browse files
dkmytanateweller
andauthored
Protect: Add onboarding dialog (#34649)
* First take at implementing onboarding flows * changelog * Move ref management to parent component * Add endpoints and handling for getting/setting onboarding dismissal status user meta * Move all onboarding logic to a custom hook * Improve variable naming, trigger closeOnboardingPopover when FIX_ALL_THREATS modal opens * Add free and paid onboarding dismissal logic * Add status.status dep to updateAnchors useEffect to force render on applicable status changes * Move ref management to dedicated hook * Include in_progress in status.status check * Simplify logic for determining popover args * Standardize anchor elements * Add handling for empty data in useThreatsList * Fix empty object handling in useThreatsList * Add isRegistered dependency to useDynamicRefs useEffect so anchors are re-evaluated after initial registration * Move onboarding step and selected variables to redux store for better global handling * Memoize onboardingPopoverArgs * Ensure anchors are updated when selected changes * Improve naming * Optimize hooks * Optimize popover arg generation process * Improve variable naming * Improve anchor ref naming * Reorganize code to hook and component to improve portability and reusability * Fix missed updates * Remove sample onboarding testing code from FirewallPage * Remove notes * Centralize main method for resetting onboarding on anchor generation * Remove unnecessary useCallback uses * Remove sample onboarding * Improve passing of common popover args * Fix scanOnboardingPopoverArgs useMemo method and deps array * Apply new approach * Fix spelling mistake * Add default props, and fix currentStep conditions * Reapply rest controller method removed in error * Fix file naming * Update completeAllCurrentSteps to set all steps, and add selected dep to currentStep useMemo to re-render on threats nav toggle * Fix format ids are set in * Improve complete_steps * Add useOnboarding hook readme * Use context for tracking onboarding steps over conditional render callbacks * Fix broken translation strings * Complete all current steps on Finish to avoid rendering relevant leftovers * Add step prefixes, conditions and handling to address upgrade/downgrade flows * Organize steps * Simply completeAllCurrentSteps steps array handling * Create a free-scan-results and paid-scan-results step * Move logic for what steps display where to UI level * Move completeAllCurrentSteps to after initialization of dependencies * Further move popover rendering logic to UI level * Add flip prop to restrict popover movement and fix positioning * Ensure redux progress array always aligns with background progress * Improve onboarding-steps efficiency * Fix inconsistent use of WP_REST_Response * Improve popover anchor useState variable naming * Fix inconsistent use of single quotes for apiFetch paths * Run tools/fixup-project-versions.sh --------- Co-authored-by: Nate Weller <[email protected]>
1 parent 5e655f9 commit 80196e8

19 files changed

+634
-77
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: added
3+
4+
Added onboarding flows

projects/plugins/protect/composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,6 @@
7979
"automattic/jetpack-autoloader": true,
8080
"automattic/jetpack-composer-plugin": true
8181
},
82-
"autoloader-suffix": "c4802e05bbcf59fd3b6350e8d3e5482c_protectⓥ2_0_1_alpha"
82+
"autoloader-suffix": "c4802e05bbcf59fd3b6350e8d3e5482c_protectⓥ2_1_0_alpha"
8383
}
8484
}

projects/plugins/protect/jetpack-protect.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Plugin Name: Jetpack Protect
44
* Plugin URI: https://wordpress.org/plugins/jetpack-protect
55
* Description: Security tools that keep your site safe and sound, from posts to plugins.
6-
* Version: 2.0.1-alpha
6+
* Version: 2.1.0-alpha
77
* Author: Automattic - Jetpack Security team
88
* Author URI: https://jetpack.com/protect/
99
* License: GPLv2 or later
@@ -32,7 +32,7 @@
3232
exit;
3333
}
3434

35-
define( 'JETPACK_PROTECT_VERSION', '2.0.1-alpha' );
35+
define( 'JETPACK_PROTECT_VERSION', '2.1.0-alpha' );
3636
define( 'JETPACK_PROTECT_DIR', plugin_dir_path( __FILE__ ) );
3737
define( 'JETPACK_PROTECT_ROOT_FILE', __FILE__ );
3838
define( 'JETPACK_PROTECT_ROOT_FILE_RELATIVE_PATH', plugin_basename( __FILE__ ) );

projects/plugins/protect/src/class-jetpack-protect.php

+14-12
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Automattic\Jetpack\My_Jetpack\Initializer as My_Jetpack_Initializer;
2020
use Automattic\Jetpack\My_Jetpack\Products as My_Jetpack_Products;
2121
use Automattic\Jetpack\Plugins_Installer;
22+
use Automattic\Jetpack\Protect\Onboarding;
2223
use Automattic\Jetpack\Protect\Plan;
2324
use Automattic\Jetpack\Protect\REST_Controller;
2425
use Automattic\Jetpack\Protect\Site_Health;
@@ -206,18 +207,19 @@ public function initial_state() {
206207
// phpcs:disable WordPress.Security.NonceVerification.Recommended
207208
$refresh_status_from_wpcom = isset( $_GET['checkPlan'] );
208209
$initial_state = array(
209-
'apiRoot' => esc_url_raw( rest_url() ),
210-
'apiNonce' => wp_create_nonce( 'wp_rest' ),
211-
'registrationNonce' => wp_create_nonce( 'jetpack-registration-nonce' ),
212-
'status' => Status::get_status( $refresh_status_from_wpcom ),
213-
'installedPlugins' => Plugins_Installer::get_plugins(),
214-
'installedThemes' => Sync_Functions::get_themes(),
215-
'wpVersion' => $wp_version,
216-
'adminUrl' => 'admin.php?page=jetpack-protect',
217-
'siteSuffix' => ( new Jetpack_Status() )->get_site_suffix(),
218-
'jetpackScan' => My_Jetpack_Products::get_product( 'scan' ),
219-
'hasRequiredPlan' => Plan::has_required_plan(),
220-
'waf' => array(
210+
'apiRoot' => esc_url_raw( rest_url() ),
211+
'apiNonce' => wp_create_nonce( 'wp_rest' ),
212+
'registrationNonce' => wp_create_nonce( 'jetpack-registration-nonce' ),
213+
'status' => Status::get_status( $refresh_status_from_wpcom ),
214+
'installedPlugins' => Plugins_Installer::get_plugins(),
215+
'installedThemes' => Sync_Functions::get_themes(),
216+
'wpVersion' => $wp_version,
217+
'adminUrl' => 'admin.php?page=jetpack-protect',
218+
'siteSuffix' => ( new Jetpack_Status() )->get_site_suffix(),
219+
'jetpackScan' => My_Jetpack_Products::get_product( 'scan' ),
220+
'hasRequiredPlan' => Plan::has_required_plan(),
221+
'onboardingProgress' => Onboarding::get_current_user_progress(),
222+
'waf' => array(
221223
'wafSupported' => Waf_Runner::is_supported_environment(),
222224
'currentIp' => IP_Utils::get_ip(),
223225
'isSeen' => self::get_waf_seen_status(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
/**
3+
* Class file for managing the user onboarding experience.
4+
*
5+
* @package automattic/jetpack-protect-plugin
6+
*/
7+
8+
namespace Automattic\Jetpack\Protect;
9+
10+
/**
11+
* Onboarding
12+
*/
13+
class Onboarding {
14+
15+
const OPTION_NAME = 'protect_onboarding_progress';
16+
17+
/**
18+
* The current user's ID
19+
*
20+
* @var int
21+
*/
22+
private static $user_id;
23+
24+
/**
25+
* Current User Progress
26+
*
27+
* @var array<string>
28+
*/
29+
private static $current_user_progress;
30+
31+
/**
32+
* Onboarding Init
33+
*
34+
* @return void
35+
*/
36+
private static function init() {
37+
self::$user_id = get_current_user_id();
38+
39+
$current_user_progress = get_user_meta( self::$user_id, self::OPTION_NAME, true );
40+
self::$current_user_progress = $current_user_progress ? $current_user_progress : array();
41+
}
42+
43+
/**
44+
* Set Onboarding Items As Completed
45+
*
46+
* @param array $step_ids The IDs of the steps to complete.
47+
* @return bool True if the update was successful, false otherwise.
48+
*/
49+
public static function complete_steps( $step_ids ) {
50+
self::init();
51+
52+
if ( empty( self::$current_user_progress ) ) {
53+
self::$current_user_progress = $step_ids;
54+
} else {
55+
// Find step IDs that are not already in the current user progress
56+
$new_steps = array_diff( $step_ids, self::$current_user_progress );
57+
58+
// Merge new steps with current progress
59+
self::$current_user_progress = array_merge( self::$current_user_progress, $new_steps );
60+
}
61+
62+
// Update the user meta only once
63+
return (bool) update_user_meta(
64+
self::$user_id,
65+
self::OPTION_NAME,
66+
self::$current_user_progress
67+
);
68+
}
69+
70+
/**
71+
* Get Current User's Onboarding Progress
72+
*
73+
* @return array<string>
74+
*/
75+
public static function get_current_user_progress() {
76+
self::init();
77+
78+
return self::$current_user_progress;
79+
}
80+
}

projects/plugins/protect/src/class-rest-controller.php

+55
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,30 @@ public static function register_rest_endpoints() {
206206
},
207207
)
208208
);
209+
210+
register_rest_route(
211+
'jetpack-protect/v1',
212+
'onboarding-progress',
213+
array(
214+
'methods' => \WP_REST_Server::READABLE,
215+
'callback' => __CLASS__ . '::api_get_onboarding_progress',
216+
'permission_callback' => function () {
217+
return current_user_can( 'manage_options' );
218+
},
219+
)
220+
);
221+
222+
register_rest_route(
223+
'jetpack-protect/v1',
224+
'onboarding-progress',
225+
array(
226+
'methods' => \WP_REST_Server::EDITABLE,
227+
'callback' => __CLASS__ . '::api_complete_onboarding_steps',
228+
'permission_callback' => function () {
229+
return current_user_can( 'manage_options' );
230+
},
231+
)
232+
);
209233
}
210234

211235
/**
@@ -424,4 +448,35 @@ public static function api_get_waf_upgrade_seen_status() {
424448
public static function api_set_waf_upgrade_seen_status() {
425449
return Jetpack_Protect::set_waf_upgrade_seen_status();
426450
}
451+
452+
/**
453+
* Gets the current user's onboarding progress for the API endpoint
454+
*
455+
* @return WP_REST_Response
456+
*/
457+
public static function api_get_onboarding_progress() {
458+
$progress = Onboarding::get_current_user_progress();
459+
return rest_ensure_response( $progress, 200 );
460+
}
461+
462+
/**
463+
* Set an onboarding step as completed for the API endpoint
464+
*
465+
* @param WP_REST_Request $request The request object.
466+
*
467+
* @return WP_REST_Response
468+
*/
469+
public static function api_complete_onboarding_steps( $request ) {
470+
if ( empty( $request['step_ids'] ) || ! is_array( $request['step_ids'] ) ) {
471+
return new WP_REST_Response( 'Missing or invalid onboarding step IDs.', 400 );
472+
}
473+
474+
$completed = Onboarding::complete_steps( $request['step_ids'] );
475+
476+
if ( ! $completed ) {
477+
return new WP_REST_Response( 'An error occured completing the onboarding step(s).', 500 );
478+
}
479+
480+
return new WP_REST_Response( 'Onboarding step(s) completed.' );
481+
}
427482
}

projects/plugins/protect/src/js/api.js

+13
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ const API = {
3232
path: 'jetpack-protect/v1/waf-upgrade-seen',
3333
method: 'POST',
3434
} ),
35+
36+
fetchOnboardingProgress: () =>
37+
apiFetch( {
38+
path: 'jetpack-protect/v1/onboarding-progress',
39+
method: 'GET',
40+
} ),
41+
42+
completeOnboardingSteps: stepIds =>
43+
apiFetch( {
44+
path: 'jetpack-protect/v1/onboarding-progress',
45+
method: 'POST',
46+
data: { step_ids: stepIds },
47+
} ),
3548
};
3649

3750
export default API;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { ActionPopover } from '@automattic/jetpack-components';
2+
import { __ } from '@wordpress/i18n';
3+
import { useContext, useEffect } from 'react';
4+
import useOnboarding, { OnboardingRenderedContext } from '../../hooks/use-onboarding';
5+
6+
const OnboardingPopover = ( { id, anchor, position } ) => {
7+
const {
8+
stepsCount,
9+
currentStep,
10+
currentStepCount,
11+
completeCurrentStep,
12+
completeAllCurrentSteps,
13+
} = useOnboarding();
14+
15+
// keep track of which onboarding steps are currently being rendered
16+
const { setRenderedSteps } = useContext( OnboardingRenderedContext );
17+
useEffect( () => {
18+
setRenderedSteps( currentRenderedSteps => [ ...currentRenderedSteps, id ] );
19+
20+
return () => {
21+
setRenderedSteps( currentRenderedSteps =>
22+
currentRenderedSteps.filter( step => step !== id )
23+
);
24+
};
25+
}, [ id, setRenderedSteps ] );
26+
27+
// do not render if this is not the current step
28+
if ( currentStep?.id !== id ) {
29+
return null;
30+
}
31+
32+
return (
33+
<ActionPopover
34+
anchor={ anchor }
35+
title={ currentStep.title }
36+
noArrow={ false }
37+
children={ currentStep.description }
38+
buttonContent={
39+
currentStepCount < stepsCount
40+
? __( 'Next', 'jetpack-protect' )
41+
: __( 'Finish', 'jetpack-protect', /* dummy arg to avoid bad minification */ 0 )
42+
}
43+
onClick={ currentStepCount < stepsCount ? completeCurrentStep : completeAllCurrentSteps }
44+
onClose={ completeAllCurrentSteps }
45+
position={ position }
46+
step={ currentStepCount }
47+
totalSteps={ stepsCount }
48+
offset={ 15 }
49+
flip={ false }
50+
/>
51+
);
52+
};
53+
54+
export default OnboardingPopover;

projects/plugins/protect/src/js/components/scan-page/index.jsx

+26-22
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useSelect, useDispatch } from '@wordpress/data';
55
import { __ } from '@wordpress/i18n';
66
import React, { useEffect } from 'react';
77
import useAnalyticsTracks from '../../hooks/use-analytics-tracks';
8+
import { OnboardingContext } from '../../hooks/use-onboarding';
89
import useProtectData from '../../hooks/use-protect-data';
910
import { STORE_ID } from '../../state/store';
1011
import AdminPage from '../admin-page';
@@ -15,6 +16,7 @@ import SeventyFiveLayout from '../seventy-five-layout';
1516
import Summary from '../summary';
1617
import ThreatsList from '../threats-list';
1718
import inProgressImage from './in-progress.png';
19+
import onboardingSteps from './onboarding-steps';
1820
import styles from './styles.module.scss';
1921
import useCredentials from './use-credentials';
2022
import useStatusPolling from './use-status-polling';
@@ -159,29 +161,31 @@ const ScanPage = () => {
159161
}
160162

161163
return (
162-
<AdminPage>
163-
<AdminSectionHero>
164-
<Container horizontalSpacing={ 0 }>
165-
{ hasConnectionError && (
166-
<Col className={ styles[ 'connection-error-col' ] }>
167-
<ConnectionError />
164+
<OnboardingContext.Provider value={ onboardingSteps }>
165+
<AdminPage>
166+
<AdminSectionHero>
167+
<Container horizontalSpacing={ 0 }>
168+
{ hasConnectionError && (
169+
<Col className={ styles[ 'connection-error-col' ] }>
170+
<ConnectionError />
171+
</Col>
172+
) }
173+
<Col>
174+
<div id="jp-admin-notices" className="my-jetpack-jitm-card" />
168175
</Col>
169-
) }
170-
<Col>
171-
<div id="jp-admin-notices" className="my-jetpack-jitm-card" />
172-
</Col>
173-
</Container>
174-
<Container horizontalSpacing={ 3 } horizontalGap={ 7 }>
175-
<Col>
176-
<Summary />
177-
</Col>
178-
<Col>
179-
<ThreatsList />
180-
</Col>
181-
</Container>
182-
</AdminSectionHero>
183-
<ScanFooter />
184-
</AdminPage>
176+
</Container>
177+
<Container horizontalSpacing={ 3 } horizontalGap={ 7 }>
178+
<Col>
179+
<Summary />
180+
</Col>
181+
<Col>
182+
<ThreatsList />
183+
</Col>
184+
</Container>
185+
</AdminSectionHero>
186+
<ScanFooter />
187+
</AdminPage>
188+
</OnboardingContext.Provider>
185189
);
186190
};
187191

0 commit comments

Comments
 (0)