From e777808779eecb2b15995cc0ed5de63222e5de76 Mon Sep 17 00:00:00 2001 From: Louis Laugesen Date: Mon, 22 Jul 2024 15:59:28 +1000 Subject: [PATCH 01/16] Add wpcom-block-editor-nux from wp-calypso's ETK --- .../features/wpcom-block-editor-nux/README.md | 7 + ...-first-post-published-modal-controller.php | 94 ++++ ...com-block-editor-nux-status-controller.php | 126 +++++ ...or-seller-celebration-modal-controller.php | 102 ++++ ...-block-editor-sharing-modal-controller.php | 89 ++++ ...tor-video-celebration-modal-controller.php | 102 ++++ .../class-wpcom-block-editor-nux.php | 104 +++++ .../features/wpcom-block-editor-nux/index.js | 6 + .../src/block-editor-nux.js | 140 ++++++ .../src/blogging-prompts-modal/icons.js | 13 + .../src/blogging-prompts-modal/index.js | 110 +++++ .../src/blogging-prompts-modal/style.scss | 53 +++ .../src/disable-core-nux.js | 43 ++ .../draft-post-modal/images/draft-post.svg | 20 + .../src/draft-post-modal/index.js | 56 +++ .../src/draft-post-modal/style.scss | 17 + .../images/post-published.svg | 15 + .../src/first-post-published-modal/index.tsx | 118 +++++ .../src/first-post-published-modal/style.scss | 23 + .../src/nux-modal/index.tsx | 58 +++ .../src/nux-modal/style.scss | 96 ++++ .../wpcom-block-editor-nux/src/public-path.js | 11 + .../src/purchase-notice/index.jsx | 34 ++ .../src/purchase-notice/style.scss | 15 + .../images/product-published.svg | 37 ++ .../src/seller-celebration-modal/index.jsx | 137 ++++++ .../src/seller-celebration-modal/style.scss | 27 ++ .../src/sharing-modal/images/illo-share.svg | 24 + .../src/sharing-modal/index.tsx | 286 ++++++++++++ .../src/sharing-modal/inline-social-logo.tsx | 48 ++ .../inline-social-logos-sprite.tsx | 286 ++++++++++++ .../src/sharing-modal/style.scss | 173 +++++++ .../src/sharing-modal/suggested-tags.tsx | 130 ++++++ .../src/sharing-modal/use-add-tags-to-post.ts | 31 ++ .../use-sharing-modal-dismissed.ts | 23 + .../wpcom-block-editor-nux/src/store.js | 152 ++++++ .../src/test/store.test.js | 127 +++++ .../src/video-celebration-modal/index.jsx | 111 +++++ .../src/video-celebration-modal/style.scss | 37 ++ .../src/welcome-modal/images/block-picker.svg | 39 ++ .../src/welcome-modal/images/editor.svg | 21 + .../src/welcome-modal/images/preview.svg | 16 + .../src/welcome-modal/images/private.svg | 26 ++ .../src/welcome-modal/style.scss | 207 ++++++++ .../src/welcome-modal/wpcom-nux.js | 154 ++++++ .../src/welcome-tour/get-editor-type.ts | 41 ++ .../src/welcome-tour/style-tour.scss | 51 ++ .../src/welcome-tour/test/tour-steps.test.ts | 110 +++++ .../src/welcome-tour/tour-launch.tsx | 277 +++++++++++ .../src/welcome-tour/tour-steps.tsx | 440 ++++++++++++++++++ 50 files changed, 4463 insertions(+) create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/README.md create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-first-post-published-modal-controller.php create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-nux-status-controller.php create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-seller-celebration-modal-controller.php create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-sharing-modal-controller.php create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-video-celebration-modal-controller.php create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/index.js create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/block-editor-nux.js create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/blogging-prompts-modal/icons.js create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/blogging-prompts-modal/index.js create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/blogging-prompts-modal/style.scss create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/disable-core-nux.js create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/draft-post-modal/images/draft-post.svg create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/draft-post-modal/index.js create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/draft-post-modal/style.scss create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/first-post-published-modal/images/post-published.svg create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/first-post-published-modal/index.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/first-post-published-modal/style.scss create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/nux-modal/index.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/nux-modal/style.scss create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/public-path.js create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/purchase-notice/index.jsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/purchase-notice/style.scss create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/seller-celebration-modal/images/product-published.svg create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/seller-celebration-modal/index.jsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/seller-celebration-modal/style.scss create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/images/illo-share.svg create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/index.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/inline-social-logo.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/inline-social-logos-sprite.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/style.scss create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/suggested-tags.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/use-add-tags-to-post.ts create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/use-sharing-modal-dismissed.ts create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/store.js create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/test/store.test.js create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/video-celebration-modal/index.jsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/video-celebration-modal/style.scss create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/images/block-picker.svg create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/images/editor.svg create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/images/preview.svg create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/images/private.svg create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/style.scss create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/wpcom-nux.js create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/get-editor-type.ts create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/style-tour.scss create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/test/tour-steps.test.ts create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/tour-launch.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/tour-steps.tsx diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/README.md b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/README.md new file mode 100644 index 0000000000000..4c7e0026936e7 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/README.md @@ -0,0 +1,7 @@ +# Nux Welcome Tour Modal + +A help tour to show new users some of the basics of using the editor. + +## Testing Instructions + +Instructions for testing the modal and its variants, and for resetting the state of `nux-status` so that the modal is shown again can be found on the PR [#47779](https://github.com/Automattic/wp-calypso/pull/47779) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-first-post-published-modal-controller.php b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-first-post-published-modal-controller.php new file mode 100644 index 0000000000000..dfd5b9590eee0 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-first-post-published-modal-controller.php @@ -0,0 +1,94 @@ +namespace = 'wpcom/v2'; + $this->rest_base = 'block-editor/should-show-first-post-published-modal'; + + add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_script' ), 100 ); + } + + /** + * Enqueue Launchpad options. + */ + public function enqueue_script() { + $launchpad_options = array( + 'launchpadScreenOption' => get_option( 'launchpad_screen' ), + 'siteUrlOption' => get_option( 'siteurl' ), + 'siteIntentOption' => get_option( 'site_intent' ), + ); + + wp_add_inline_script( + 'wpcom-block-editor-nux-script', + 'var launchpadOptions = ' . wp_json_encode( $launchpad_options, JSON_HEX_TAG | JSON_HEX_AMP ) . ';', + 'before' + ); + } + + /** + * Register available routes. + */ + public function register_rest_route() { + register_rest_route( + $this->namespace, + $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'should_show_first_post_published_modal' ), + 'permission_callback' => array( $this, 'permission_callback' ), + ), + ) + ); + } + + /** + * Callback to determine whether the request can proceed. + * + * @return boolean + */ + public function permission_callback() { + return current_user_can( 'read' ); + } + + /** + * Should we show the first post published modal + * + * @return WP_REST_Response + */ + public function should_show_first_post_published_modal() { + // As we has synced the `has_never_published_post` option to part of atomic sites but we cannot + // update the value now, always return false to avoid showing the modal at every publishing until + // we can update the value on atomic sites. See D69932-code. + if ( defined( 'IS_ATOMIC' ) && IS_ATOMIC ) { + return rest_ensure_response( + array( + 'should_show_first_post_published_modal' => false, + ) + ); + } + + $has_never_published_post = (bool) get_option( 'has_never_published_post', false ); + $intent = get_option( 'site_intent', '' ); + $should_show_first_post_published_modal = $has_never_published_post && 'write' === $intent; + + return rest_ensure_response( + array( + 'should_show_first_post_published_modal' => $should_show_first_post_published_modal, + ) + ); + } +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-nux-status-controller.php b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-nux-status-controller.php new file mode 100644 index 0000000000000..9d2af2d56b732 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-nux-status-controller.php @@ -0,0 +1,126 @@ +namespace = 'wpcom/v2'; + $this->rest_base = 'block-editor/nux'; + } + + /** + * Register available routes. + */ + public function register_rest_route() { + register_rest_route( + $this->namespace, + $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_nux_status' ), + 'permission_callback' => array( $this, 'permission_callback' ), + ), + array( + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_nux_status' ), + 'permission_callback' => array( $this, 'permission_callback' ), + ), + ) + ); + } + + /** + * Callback to determine whether the request can proceed. + * + * @return boolean + */ + public function permission_callback() { + return is_user_logged_in(); + } + + /** + * Should we show the wpcom welcome guide (i.e. welcome tour or nux modal) + * + * @param mixed $nux_status Can be "enabled", "dismissed", or undefined. + * @return boolean + */ + public function show_wpcom_welcome_guide( $nux_status ) { + return 'enabled' === $nux_status; + } + + /** + * Return the WPCOM NUX status + * + * This is only called for sites where the user hasn't already dismissed the tour. + * Once the tour has been dismissed, the closed state is saved in local storage (for the current site) + * see src/block-editor-nux.js + * + * @return WP_REST_Response + */ + public function get_nux_status() { + + $should_open_patterns_panel = (bool) get_option( 'was_created_with_blank_canvas_design' ); + + if ( $should_open_patterns_panel ) { + $variant = 'blank-canvas-tour'; + } else { + $variant = 'tour'; + } + + if ( defined( 'IS_ATOMIC' ) && IS_ATOMIC ) { + $is_p2 = false; + } else { + $blog_id = get_current_blog_id(); + $is_p2 = \WPForTeams\is_wpforteams_site( $blog_id ); + } + + if ( $is_p2 ) { + // disable welcome tour for authoring P2s. + // see: https://github.com/Automattic/wp-calypso/issues/62973. + $nux_status = 'disabled'; + } elseif ( has_filter( 'wpcom_block_editor_nux_get_status' ) ) { + $nux_status = apply_filters( 'wpcom_block_editor_nux_get_status', false ); + } elseif ( ! metadata_exists( 'user', get_current_user_id(), 'wpcom_block_editor_nux_status' ) ) { + $nux_status = 'enabled'; + } else { + $nux_status = get_user_meta( get_current_user_id(), 'wpcom_block_editor_nux_status', true ); + } + + $show_welcome_guide = $this->show_wpcom_welcome_guide( $nux_status ); + + return rest_ensure_response( + array( + 'show_welcome_guide' => $show_welcome_guide, + 'variant' => $variant, + ) + ); + } + + /** + * Update the WPCOM NUX status + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function update_nux_status( $request ) { + $params = $request->get_json_params(); + $nux_status = $params['show_welcome_guide'] ? 'enabled' : 'dismissed'; + if ( has_action( 'wpcom_block_editor_nux_update_status' ) ) { + do_action( 'wpcom_block_editor_nux_update_status', $nux_status ); + } + update_user_meta( get_current_user_id(), 'wpcom_block_editor_nux_status', $nux_status ); + return rest_ensure_response( array( 'show_welcome_guide' => $this->show_wpcom_welcome_guide( $nux_status ) ) ); + } +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-seller-celebration-modal-controller.php b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-seller-celebration-modal-controller.php new file mode 100644 index 0000000000000..1550728c1c2c8 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-seller-celebration-modal-controller.php @@ -0,0 +1,102 @@ +namespace = 'wpcom/v2'; + $this->rest_base = 'block-editor/has-seen-seller-celebration-modal'; + $this->wpcom_is_site_specific_endpoint = true; + $this->wpcom_is_wpcom_only_endpoint = true; + } + + /** + * Register available routes. + */ + public function register_rest_route() { + register_rest_route( + $this->namespace, + $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'has_seen_seller_celebration_modal' ), + 'permission_callback' => array( $this, 'permission_callback' ), + ), + ) + ); + register_rest_route( + $this->namespace, + $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'set_has_seen_seller_celebration_modal' ), + 'permission_callback' => array( $this, 'permission_callback' ), + 'args' => array( + 'has_seen_seller_celebration_modal' => array( + 'required' => true, + 'type' => 'boolean', + ), + ), + ), + ) + ); + + } + + /** + * Callback to determine whether the request can proceed. + * + * @return boolean + */ + public function permission_callback() { + return current_user_can( 'read' ); + } + + /** + * Whether the user has seen the seller celebration modal + * + * @return WP_REST_Response + */ + public function has_seen_seller_celebration_modal() { + // See D69932-code and apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-first-post-published-modal-controller.php. + if ( defined( 'IS_ATOMIC' ) && IS_ATOMIC ) { + return rest_ensure_response( + array( + 'has_seen_seller_celebration_modal' => false, + ) + ); + } + $has_seen_seller_celebration_modal = (bool) get_option( 'has_seen_seller_celebration_modal', false ); + + return rest_ensure_response( + array( + 'has_seen_seller_celebration_modal' => $has_seen_seller_celebration_modal, + ) + ); + } + + /** + * Update the option for whether the user has seen the seller celebration modal. + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function set_has_seen_seller_celebration_modal( $request ) { + $params = $request->get_json_params(); + update_option( 'has_seen_seller_celebration_modal', $params['has_seen_seller_celebration_modal'] ); + return rest_ensure_response( array( 'has_seen_seller_celebration_modal' => $params['has_seen_seller_celebration_modal'] ) ); + } +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-sharing-modal-controller.php b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-sharing-modal-controller.php new file mode 100644 index 0000000000000..ad67cf7b42002 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-sharing-modal-controller.php @@ -0,0 +1,89 @@ +namespace = 'wpcom/v2'; + $this->rest_base = 'block-editor/sharing-modal-dismissed'; + + add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_script' ), 100 ); + } + + /** + * Enqueue sharing modal options. + */ + public function enqueue_script() { + $modal_options = array( + 'isDismissed' => $this->get_wpcom_sharing_modal_dismissed(), + ); + + wp_add_inline_script( + 'wpcom-block-editor-nux-script', + 'var sharingModalOptions = ' . wp_json_encode( $modal_options, JSON_HEX_TAG | JSON_HEX_AMP ) . ';', + 'before' + ); + } + + /** + * Register available routes. + */ + public function register_rest_route() { + register_rest_route( + $this->namespace, + $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'set_wpcom_sharing_modal_dismissed' ), + 'permission_callback' => array( $this, 'permission_callback' ), + ), + ) + ); + } + + /** + * Callback to determine whether the request can proceed. + * + * @return boolean + */ + public function permission_callback() { + return current_user_can( 'read' ); + } + + /** + * Get the sharing modal dismissed status + * + * @return boolean + */ + public function get_wpcom_sharing_modal_dismissed() { + $old_sharing_modal_dismissed = (bool) get_option( 'sharing_modal_dismissed', false ); + if ( $old_sharing_modal_dismissed ) { + return true; + } + return (bool) get_option( 'wpcom_sharing_modal_dismissed', false ); + } + + /** + * Dismiss the sharing modal + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function set_wpcom_sharing_modal_dismissed( $request ) { + $params = $request->get_json_params(); + update_option( 'wpcom_sharing_modal_dismissed', $params['wpcom_sharing_modal_dismissed'] ); + return rest_ensure_response( array( 'wpcom_sharing_modal_dismissed' => $this->get_wpcom_sharing_modal_dismissed() ) ); + } +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-video-celebration-modal-controller.php b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-video-celebration-modal-controller.php new file mode 100644 index 0000000000000..5b535f95598d6 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-video-celebration-modal-controller.php @@ -0,0 +1,102 @@ +namespace = 'wpcom/v2'; + $this->rest_base = 'block-editor/has-seen-video-celebration-modal'; + $this->wpcom_is_site_specific_endpoint = true; + $this->wpcom_is_wpcom_only_endpoint = true; + } + + /** + * Register available routes. + */ + public function register_rest_route() { + register_rest_route( + $this->namespace, + $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'has_seen_video_celebration_modal' ), + 'permission_callback' => array( $this, 'permission_callback' ), + ), + ) + ); + register_rest_route( + $this->namespace, + $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'set_has_seen_video_celebration_modal' ), + 'permission_callback' => array( $this, 'permission_callback' ), + 'args' => array( + 'has_seen_video_celebration_modal' => array( + 'required' => true, + 'type' => 'boolean', + ), + ), + ), + ) + ); + + } + + /** + * Callback to determine whether the request can proceed. + * + * @return boolean + */ + public function permission_callback() { + return current_user_can( 'read' ); + } + + /** + * Whether the site has displayed the video upload celebration modal. + * + * @return WP_REST_Response + */ + public function has_seen_video_celebration_modal() { + // See D69932-code and apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-first-post-published-modal-controller.php. + if ( defined( 'IS_ATOMIC' ) && IS_ATOMIC ) { + return rest_ensure_response( + array( + 'has_seen_video_celebration_modal' => true, + ) + ); + } + $has_seen_video_celebration_modal = (bool) get_option( 'has_seen_video_celebration_modal', false ); + + return rest_ensure_response( + array( + 'has_seen_video_celebration_modal' => $has_seen_video_celebration_modal, + ) + ); + } + + /** + * Update the option for whether the user has seen the video upload celebration modal. + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function set_has_seen_video_celebration_modal( $request ) { + $params = $request->get_json_params(); + update_option( 'has_seen_video_celebration_modal', $params['has_seen_video_celebration_modal'] ); + return rest_ensure_response( array( 'has_seen_video_celebration_modal' => $params['has_seen_video_celebration_modal'] ) ); + } +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php new file mode 100644 index 0000000000000..1da8a1839f1ec --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php @@ -0,0 +1,104 @@ +register_rest_route(); + + require_once __DIR__ . '/class-wp-rest-wpcom-block-editor-first-post-published-modal-controller.php'; + $first_post_published_modal_controller = new WP_REST_WPCOM_Block_Editor_First_Post_Published_Modal_Controller(); + $first_post_published_modal_controller->register_rest_route(); + + require_once __DIR__ . '/class-wp-rest-wpcom-block-editor-seller-celebration-modal-controller.php'; + $seller_celebration_modal_controller = new WP_REST_WPCOM_Block_Editor_Seller_Celebration_Modal_Controller(); + $seller_celebration_modal_controller->register_rest_route(); + + require_once __DIR__ . '/class-wp-rest-wpcom-block-editor-video-celebration-modal-controller.php'; + $video_celebration_modal_controller = new WP_REST_WPCOM_Block_Editor_Video_Celebration_Modal_Controller(); + $video_celebration_modal_controller->register_rest_route(); + + require_once __DIR__ . '/class-wp-rest-wpcom-block-editor-sharing-modal-controller.php'; + $sharing_modal_controller = new WP_REST_WPCOM_Block_Editor_Sharing_Modal_Controller(); + $sharing_modal_controller->register_rest_route(); + } +} +add_action( 'init', array( __NAMESPACE__ . '\WPCOM_Block_Editor_NUX', 'init' ) ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/index.js b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/index.js new file mode 100644 index 0000000000000..67979daf701d1 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/index.js @@ -0,0 +1,6 @@ +import { register } from './src/store'; + +import './src/disable-core-nux'; +import './src/block-editor-nux'; + +register(); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/block-editor-nux.js b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/block-editor-nux.js new file mode 100644 index 0000000000000..233aa88660e87 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/block-editor-nux.js @@ -0,0 +1,140 @@ +/*** THIS MUST BE THE FIRST THING EVALUATED IN THIS SCRIPT *****/ +import './public-path'; + +/* eslint-disable wpcalypso/jsx-classname-namespace */ + +import { LocaleProvider, i18nDefaultLocaleSlug } from '@automattic/i18n-utils'; +import { Guide, GuidePage } from '@wordpress/components'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { useEffect, useState } from '@wordpress/element'; +import { applyFilters } from '@wordpress/hooks'; +import { registerPlugin } from '@wordpress/plugins'; +import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis'; +import { getQueryArg } from '@wordpress/url'; +import { ShouldShowFirstPostPublishedModalProvider } from '../../dotcom-fse/lib/first-post-published-modal/should-show-first-post-published-modal-context'; +import { HasSeenSellerCelebrationModalProvider } from '../../dotcom-fse/lib/seller-celebration-modal/has-seen-seller-celebration-modal-context'; +import { HasSeenVideoCelebrationModalProvider } from '../../dotcom-fse/lib/video-celebration-modal/has-seen-video-celebration-modal-context'; +import { BloggingPromptsModal } from './blogging-prompts-modal'; +import DraftPostModal from './draft-post-modal'; +import FirstPostPublishedModal from './first-post-published-modal'; +import PurchaseNotice from './purchase-notice'; +import SellerCelebrationModal from './seller-celebration-modal'; +import PostPublishedSharingModal from './sharing-modal'; +import { DEFAULT_VARIANT, BLANK_CANVAS_VARIANT } from './store'; +import VideoPressCelebrationModal from './video-celebration-modal'; +import WpcomNux from './welcome-modal/wpcom-nux'; +import LaunchWpcomWelcomeTour from './welcome-tour/tour-launch'; + +/** + * Sometimes Gutenberg doesn't allow you to re-register the module and throws an error. + * FIXME: The new version allow it by default, but we might need to ensure that all the site has the new version. + * @see https://github.com/Automattic/wp-calypso/pull/79663 + */ +let unlock; +try { + unlock = __dangerousOptInToUnstableAPIsOnlyForCoreModules( + 'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.', + '@wordpress/edit-site' + ).unlock; +} catch ( error ) { + // eslint-disable-next-line no-console + console.error( 'Error: Unable to get the unlock api. Reason: %s', error ); +} + +/** + * + */ +function WelcomeTour() { + const [ showDraftPostModal ] = useState( + getQueryArg( window.location.href, 'showDraftPostModal' ) + ); + + const { + show, + isLoaded, + variant, + isManuallyOpened, + isNewPageLayoutModalOpen, + siteEditorCanvasMode, + } = useSelect( select => { + const welcomeGuideStoreSelect = select( 'automattic/wpcom-welcome-guide' ); + const starterPageLayoutsStoreSelect = select( 'automattic/starter-page-layouts' ); + let canvasMode; + if ( unlock && select( 'core/edit-site' ) ) { + canvasMode = + select( 'core/edit-site' ) && unlock( select( 'core/edit-site' ) ).getCanvasMode(); + } + + return { + show: welcomeGuideStoreSelect.isWelcomeGuideShown(), + isLoaded: welcomeGuideStoreSelect.isWelcomeGuideStatusLoaded(), + variant: welcomeGuideStoreSelect.getWelcomeGuideVariant(), + isManuallyOpened: welcomeGuideStoreSelect.isWelcomeGuideManuallyOpened(), + isNewPageLayoutModalOpen: starterPageLayoutsStoreSelect?.isOpen(), // Handle the case where SPT is not initalized. + siteEditorCanvasMode: canvasMode, + }; + }, [] ); + + const setOpenState = useDispatch( 'automattic/starter-page-layouts' )?.setOpenState; + + const { fetchWelcomeGuideStatus } = useDispatch( 'automattic/wpcom-welcome-guide' ); + + // On mount check if the WPCOM welcome guide status exists in state (from local storage), otherwise fetch it from the API. + useEffect( () => { + if ( ! isLoaded ) { + fetchWelcomeGuideStatus(); + } + }, [ fetchWelcomeGuideStatus, isLoaded ] ); + + const filteredShow = applyFilters( 'a8c.WpcomBlockEditorWelcomeTour.show', show ); + + if ( ! filteredShow || isNewPageLayoutModalOpen ) { + return null; + } + + // Hide the Welcome Tour when not in the edit mode. Note that canvas mode is available only in the site editor + if ( siteEditorCanvasMode && siteEditorCanvasMode !== 'edit' ) { + return null; + } + + // Open patterns panel before Welcome Tour if necessary (e.g. when using Blank Canvas theme) + // Do this only when Welcome Tour is not manually opened. + // NOTE: at the moment, 'starter-page-templates' assets are not loaded on /site-editor/ page so 'setOpenState' may be undefined + if ( variant === BLANK_CANVAS_VARIANT && ! isManuallyOpened && setOpenState ) { + setOpenState( 'OPEN_FOR_BLANK_CANVAS' ); + return null; + } + + if ( variant === DEFAULT_VARIANT ) { + return ( + + { showDraftPostModal ? : } + + ); + } + + // This case is redundant now and it will be cleaned up in a follow-up PR + if ( variant === 'modal' && Guide && GuidePage ) { + return ; + } + + return null; +} + +registerPlugin( 'wpcom-block-editor-nux', { + render: () => ( + + + + + + + + + + + + + + ), +} ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/blogging-prompts-modal/icons.js b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/blogging-prompts-modal/icons.js new file mode 100644 index 0000000000000..1b968dbc81d19 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/blogging-prompts-modal/icons.js @@ -0,0 +1,13 @@ +import { Path, SVG } from '@wordpress/components'; + +export const ArrowRightIcon = () => ( + + + +); + +export const ArrowLeftIcon = () => ( + + + +); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/blogging-prompts-modal/index.js b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/blogging-prompts-modal/index.js new file mode 100644 index 0000000000000..86d194c64dabd --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/blogging-prompts-modal/index.js @@ -0,0 +1,110 @@ +import { recordTracksEvent } from '@automattic/calypso-analytics'; +import apiFetch from '@wordpress/api-fetch'; +import { createBlock } from '@wordpress/blocks'; +import { Button, Modal } from '@wordpress/components'; +import { dispatch, select } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { addQueryArgs, getQueryArg } from '@wordpress/url'; +import moment from 'moment'; +import { useEffect, useState } from 'react'; +import { ArrowLeftIcon, ArrowRightIcon } from './icons'; + +import './style.scss'; + +export const BloggingPromptsModalInner = () => { + const [ isOpen, setIsOpen ] = useState( true ); + const [ prompts, setPrompts ] = useState( [] ); + const [ promptIndex, setPromptIndex ] = useState( 0 ); + + useEffect( () => { + const siteId = window._currentSiteId; + const path = addQueryArgs( `/wpcom/v3/sites/${ siteId }/blogging-prompts`, { + per_page: 10, + after: moment().format( '--MM-DD' ), + order: 'desc', + force_year: new Date().getFullYear(), + } ); + apiFetch( { + path, + } ) + .then( result => { + recordTracksEvent( 'calypso_editor_writing_prompts_modal_viewed' ); + return setPrompts( result ); + } ) + // eslint-disable-next-line no-console + .catch( () => console.error( 'Unable to fetch writing prompts' ) ); + }, [] ); + + if ( ! isOpen || ! prompts.length ) { + return null; + } + + /** + * + */ + function selectPrompt() { + const promptId = prompts[ promptIndex ]?.id; + dispatch( 'core/editor' ).resetEditorBlocks( [ + createBlock( 'jetpack/blogging-prompt', { promptId } ), + ] ); + recordTracksEvent( 'calypso_editor_writing_prompts_modal_prompt_selected', { + prompt_id: promptId, + } ); + setIsOpen( false ); + } + + const closeModal = () => { + recordTracksEvent( 'calypso_editor_writing_prompts_modal_closed' ); + setIsOpen( false ); + }; + + return ( + +
+
+ +

{ prompts[ promptIndex ]?.text }

+ +
+ +
+
+ ); +}; + +export const BloggingPromptsModal = () => { + const hasQueryArg = getQueryArg( window.location.href, 'new_prompt' ); + const editorType = select( 'core/editor' ).getCurrentPostType(); + + const shouldOpen = hasQueryArg && editorType === 'post'; + + if ( ! shouldOpen ) { + return null; + } + return ; +}; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/blogging-prompts-modal/style.scss b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/blogging-prompts-modal/style.scss new file mode 100644 index 0000000000000..67969ac4d706b --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/blogging-prompts-modal/style.scss @@ -0,0 +1,53 @@ +@import "@automattic/typography/styles/variables"; +@import "@wordpress/base-styles/breakpoints"; +@import "@wordpress/base-styles/mixins"; + +.blogging-prompts-modal { + @include break-small { + width: 80%; + } + @include break-large { + width: 60%; + } + max-width: 800px; + margin: auto; + + .components-modal__header-heading { + font-size: $font-body; + font-weight: 400; + } +} + +.blogging-prompts-modal__prompt { + display: flex; + flex-direction: column; + align-items: flex-end; + + .blogging-prompts-modal__prompt-navigation { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; + gap: 24px; + width: 100%; + } + + .blogging-prompts-modal__prompt-navigation-button { + border-radius: 50%; + width: 44px; + height: 44px; + &.components-button:hover:not(:disabled,[aria-disabled="true"]) { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)); + } + } + + .blogging-prompts-modal__prompt-text { + font-size: 1.25rem; + font-weight: 500; + line-height: 26px; + text-align: left; + width: 100%; + text-wrap: pretty; + } +} + diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/disable-core-nux.js b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/disable-core-nux.js new file mode 100644 index 0000000000000..6591d4e0b5c2d --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/disable-core-nux.js @@ -0,0 +1,43 @@ +import { select, dispatch, subscribe } from '@wordpress/data'; + +import '@wordpress/nux'; //ensure nux store loads + +// Disable nux and welcome guide features from core. +const unsubscribe = subscribe( () => { + dispatch( 'core/nux' ).disableTips(); + if ( select( 'core/edit-post' )?.isFeatureActive( 'welcomeGuide' ) ) { + dispatch( 'core/edit-post' ).toggleFeature( 'welcomeGuide' ); + unsubscribe(); + } + if ( select( 'core/edit-site' )?.isFeatureActive( 'welcomeGuide' ) ) { + dispatch( 'core/edit-site' ).toggleFeature( 'welcomeGuide' ); + unsubscribe(); + } +} ); + +// Listen for these features being triggered to call dotcom welcome guide instead. +// Note migration of areTipsEnabled: https://github.com/WordPress/gutenberg/blob/5c3a32dabe4393c45f7fe6ac5e4d78aebd5ee274/packages/data/src/plugins/persistence/index.js#L269 +subscribe( () => { + if ( select( 'core/nux' ).areTipsEnabled() ) { + dispatch( 'core/nux' ).disableTips(); + dispatch( 'automattic/wpcom-welcome-guide' ).setShowWelcomeGuide( true ); + } + if ( select( 'core/edit-post' )?.isFeatureActive( 'welcomeGuide' ) ) { + dispatch( 'core/edit-post' ).toggleFeature( 'welcomeGuide' ); + // On mounting, the welcomeGuide feature is turned on by default. This opens the welcome guide despite `welcomeGuideStatus` value. + // This check ensures that we only listen to `welcomeGuide` changes if the welcomeGuideStatus value is loaded and respected + if ( select( 'automattic/wpcom-welcome-guide' ).isWelcomeGuideStatusLoaded() ) { + dispatch( 'automattic/wpcom-welcome-guide' ).setShowWelcomeGuide( true, { + openedManually: true, + } ); + } + } + if ( select( 'core/edit-site' )?.isFeatureActive( 'welcomeGuide' ) ) { + dispatch( 'core/edit-site' ).toggleFeature( 'welcomeGuide' ); + if ( select( 'automattic/wpcom-welcome-guide' ).isWelcomeGuideStatusLoaded() ) { + dispatch( 'automattic/wpcom-welcome-guide' ).setShowWelcomeGuide( true, { + openedManually: true, + } ); + } + } +} ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/draft-post-modal/images/draft-post.svg b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/draft-post-modal/images/draft-post.svg new file mode 100644 index 0000000000000..cf24d89ced98a --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/draft-post-modal/images/draft-post.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/draft-post-modal/index.js b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/draft-post-modal/index.js new file mode 100644 index 0000000000000..1467da2d5d9d6 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/draft-post-modal/index.js @@ -0,0 +1,56 @@ +import { recordTracksEvent } from '@automattic/calypso-analytics'; +import { Button } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { doAction, hasAction } from '@wordpress/hooks'; +import { __ } from '@wordpress/i18n'; +import NuxModal from '../nux-modal'; +import draftPostImage from './images/draft-post.svg'; +import './style.scss'; + +const CLOSE_EDITOR_ACTION = 'a8c.wpcom-block-editor.closeEditor'; + +const DraftPostModal = () => { + const siteId = window._currentSiteId; + const primaryDomain = useSelect( select => + select( 'automattic/site' ).getPrimarySiteDomain( siteId ) + ); + + const homeUrl = `/home/${ primaryDomain?.domain || window.location.hostname }`; + const [ isOpen, setIsOpen ] = useState( true ); + const closeModal = () => setIsOpen( false ); + const closeEditor = () => { + if ( hasAction( CLOSE_EDITOR_ACTION ) ) { + doAction( CLOSE_EDITOR_ACTION, homeUrl ); + } else { + window.location.href = `https://wordpress.com${ homeUrl }`; + } + }; + + return ( + + + + + } + onRequestClose={ closeModal } + onOpen={ () => recordTracksEvent( 'calypso_editor_wpcom_draft_post_modal_show' ) } + /> + ); +}; + +export default DraftPostModal; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/draft-post-modal/style.scss b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/draft-post-modal/style.scss new file mode 100644 index 0000000000000..2317634ed12b7 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/draft-post-modal/style.scss @@ -0,0 +1,17 @@ +@import "@wordpress/base-styles/breakpoints"; +@import "@wordpress/base-styles/mixins"; + +.wpcom-block-editor-draft-post-modal { + .components-modal__content { + @include break-small { + padding: 48px 128px; + } + } + + .wpcom-block-editor-nux-modal__image-container { + img { + width: 209px; + height: 95px; + } + } +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/first-post-published-modal/images/post-published.svg b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/first-post-published-modal/images/post-published.svg new file mode 100644 index 0000000000000..3b55263b209af --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/first-post-published-modal/images/post-published.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/first-post-published-modal/index.tsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/first-post-published-modal/index.tsx new file mode 100644 index 0000000000000..675601897bae8 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/first-post-published-modal/index.tsx @@ -0,0 +1,118 @@ +import { recordTracksEvent } from '@automattic/calypso-analytics'; +import { Button } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { useEffect, useRef, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { isURL } from '@wordpress/url'; +import React from 'react'; +import { useShouldShowFirstPostPublishedModal } from '../../../dotcom-fse/lib/first-post-published-modal/should-show-first-post-published-modal-context'; +import useSiteIntent from '../../../dotcom-fse/lib/site-intent/use-site-intent'; +import NuxModal from '../nux-modal'; +import postPublishedImage from './images/post-published.svg'; + +import './style.scss'; + +type CoreEditorPlaceholder = { + getCurrentPost: ( ...args: unknown[] ) => { link: string }; + getCurrentPostType: ( ...args: unknown[] ) => string; + isCurrentPostPublished: ( ...args: unknown[] ) => boolean; +}; + +/** + * Show the first post publish modal + */ +const FirstPostPublishedModalInner: React.FC = () => { + const { link } = useSelect( + select => ( select( 'core/editor' ) as CoreEditorPlaceholder ).getCurrentPost(), + [] + ); + const postType = useSelect( + select => ( select( 'core/editor' ) as CoreEditorPlaceholder ).getCurrentPostType(), + [] + ); + + const isCurrentPostPublished = useSelect( + select => ( select( 'core/editor' ) as CoreEditorPlaceholder ).isCurrentPostPublished(), + [] + ); + const previousIsCurrentPostPublished = useRef( isCurrentPostPublished ); + const shouldShowFirstPostPublishedModal = useShouldShowFirstPostPublishedModal(); + const [ isOpen, setIsOpen ] = useState( false ); + const closeModal = () => setIsOpen( false ); + + const { siteUrlOption, launchpadScreenOption, siteIntentOption } = window?.launchpadOptions || {}; + + let siteUrl = ''; + if ( isURL( siteUrlOption ) ) { + // https://mysite.wordpress.com/path becomes mysite.wordpress.com + siteUrl = new URL( siteUrlOption ).hostname; + } + + useEffect( () => { + // If the user is set to see the first post modal and current post status changes to publish, + // open the post publish modal + if ( + shouldShowFirstPostPublishedModal && + ! previousIsCurrentPostPublished.current && + isCurrentPostPublished && + postType === 'post' + ) { + previousIsCurrentPostPublished.current = isCurrentPostPublished; + + // When the post published panel shows, it is focused automatically. + // Thus, we need to delay open the modal so that the modal would not be close immediately + // because the outside of modal is focused + window.setTimeout( () => { + setIsOpen( true ); + } ); + } + }, [ postType, shouldShowFirstPostPublishedModal, isCurrentPostPublished ] ); + + const handleViewPostClick = ( event: React.MouseEvent ) => { + event.preventDefault(); + ( window.top as Window ).location.href = link; + }; + + const handleNextStepsClick = ( event: React.MouseEvent ) => { + event.preventDefault(); + ( + window.top as Window + ).location.href = `https://wordpress.com/setup/write/launchpad?siteSlug=${ siteUrl }`; + }; + return ( + + + { launchpadScreenOption === 'full' && siteIntentOption === 'write' && ( + + ) } + + } + onRequestClose={ closeModal } + onOpen={ () => recordTracksEvent( 'calypso_editor_wpcom_first_post_published_modal_show' ) } + /> + ); +}; + +const FirstPostPublishedModal = () => { + const { siteIntent: intent } = useSiteIntent(); + if ( intent === 'write' ) { + return ; + } + return null; +}; + +export default FirstPostPublishedModal; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/first-post-published-modal/style.scss b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/first-post-published-modal/style.scss new file mode 100644 index 0000000000000..ebbe84e61dfc8 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/first-post-published-modal/style.scss @@ -0,0 +1,23 @@ +@import "@wordpress/base-styles/breakpoints"; +@import "@wordpress/base-styles/mixins"; + +.wpcom-block-editor-post-published-modal { + .components-modal__content { + @include break-small { + padding: 48px 90px; + } + } + + .wpcom-block-editor-nux-modal__image-container { + img { + width: 158px; + height: 85px; + } + } + + .wpcom-block-editor-nux-modal__buttons { + .components-button { + min-width: 113px; + } + } +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/nux-modal/index.tsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/nux-modal/index.tsx new file mode 100644 index 0000000000000..e46656c0ce2e3 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/nux-modal/index.tsx @@ -0,0 +1,58 @@ +import { Modal } from '@wordpress/components'; +import { useEffect, useRef } from '@wordpress/element'; +import clsx from 'clsx'; +import React from 'react'; +import './style.scss'; + +interface Props { + isOpen: boolean; + className?: string; + title: string; + description: string; + imageSrc: string; + actionButtons: React.ReactElement; + onRequestClose: () => void; + onOpen?: () => void; +} + +const NuxModal: React.FC< Props > = ( { + isOpen, + className, + title, + description, + imageSrc, + actionButtons, + onRequestClose, + onOpen, +} ) => { + const prevIsOpen = useRef< boolean | null >( null ); + + useEffect( () => { + if ( ! prevIsOpen.current && isOpen ) { + onOpen?.(); + } + + prevIsOpen.current = isOpen; + }, [ prevIsOpen, isOpen, onOpen ] ); + + if ( ! isOpen ) { + return null; + } + + return ( + +
+ { +
+

{ title }

+

{ description }

+
{ actionButtons }
+
+ ); +}; + +export default NuxModal; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/nux-modal/style.scss b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/nux-modal/style.scss new file mode 100644 index 0000000000000..ee7ee1b10caef --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/nux-modal/style.scss @@ -0,0 +1,96 @@ +@import "@automattic/typography/styles/variables"; +@import "@wordpress/base-styles/breakpoints"; +@import "@wordpress/base-styles/mixins"; + +.wpcom-block-editor-nux-modal { + .components-modal__header { + height: auto; + padding: 10px; + border-bottom: 0; + + // Fix styles when Gutenberg is deactivated + position: absolute; + left: 0; + right: 0; + margin: 0; + background-color: transparent; + + button { + left: unset; + + svg path { + transform: scale(1.4); + transform-origin: center; + } + } + } + + .components-modal__content { + padding: 84px 20px 20px; + margin-top: 0; + + &::before { + margin: 0; + } + + @include break-mobile { + text-align: center; + } + } + + .wpcom-block-editor-nux-modal__image-container { + display: flex; + justify-content: center; + } + + .wpcom-block-editor-nux-modal__title { + margin: 34px 0 0; + font-size: $font-headline-small; + font-weight: 500; + line-height: 1.2; + + @include break-mobile { + margin-top: 24px; + } + } + + .wpcom-block-editor-nux-modal__description { + max-width: 352px; + margin: 16px 0 0; + font-size: $font-body; + + @include break-mobile { + margin: 20px auto 0; + font-size: $font-body-large; + } + } + + .wpcom-block-editor-nux-modal__buttons { + display: flex; + flex-direction: column; + justify-content: center; + margin-top: 24px; + + .components-button { + min-width: 130px; + height: 40px; + justify-content: center; + border-radius: 3px; + font-size: $font-body-small; + } + + .components-button + .components-button { + margin-top: 12px; + } + + @include break-mobile { + flex-direction: row; + margin-top: 28px; + + .components-button + .components-button { + margin-top: 0; + margin-left: 16px; + } + } + } +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/public-path.js b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/public-path.js new file mode 100644 index 0000000000000..6bab2016a50ad --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/public-path.js @@ -0,0 +1,11 @@ +/* exported __webpack_public_path__ */ +/* global __webpack_public_path__ */ + +/** + * Dynamically set WebPack's publicPath so that split assets can be found. + * @see https://webpack.js.org/guides/public-path/#on-the-fly + */ +if ( typeof window === 'object' && window.wpcomBlockEditorNuxAssetsUrl ) { + // eslint-disable-next-line no-global-assign + __webpack_public_path__ = window.wpcomBlockEditorNuxAssetsUrl; +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/purchase-notice/index.jsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/purchase-notice/index.jsx new file mode 100644 index 0000000000000..109b4a7b5d6f8 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/purchase-notice/index.jsx @@ -0,0 +1,34 @@ +import { useDispatch } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; +import { useEffect, useRef } from 'react'; +import './style.scss'; + +/** + * + */ +function PurchaseNotice() { + const hasPaymentNotice = useRef( false ); + const { createNotice } = useDispatch( noticesStore ); + + useEffect( () => { + const noticePattern = /[&?]notice=([\w_-]+)/; + const match = noticePattern.exec( document.location.search ); + const notice = match && match[ 1 ]; + if ( 'purchase-success' === notice && hasPaymentNotice.current === false ) { + hasPaymentNotice.current = true; + createNotice( + 'info', + __( 'Congrats! Premium blocks are now available to use.', 'jetpack-mu-wpcom' ), + { + isDismissible: true, + type: 'snackbar', + } + ); + } + }, [ createNotice ] ); + + return null; +} + +export default PurchaseNotice; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/purchase-notice/style.scss b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/purchase-notice/style.scss new file mode 100644 index 0000000000000..f9802fcb34e2b --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/purchase-notice/style.scss @@ -0,0 +1,15 @@ +@import "@wordpress/base-styles/breakpoints"; +@import "@wordpress/base-styles/mixins"; + +.wpcom-block-editor-purchase-notice { + position: fixed; + overflow: visible; + bottom: 0; + left: 0; + width: 100%; + z-index: 10000; + justify-content: center; + display: flex; + pointer-events: none; + padding-bottom: 1rem; +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/seller-celebration-modal/images/product-published.svg b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/seller-celebration-modal/images/product-published.svg new file mode 100644 index 0000000000000..258f49c3780c7 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/seller-celebration-modal/images/product-published.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/seller-celebration-modal/index.jsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/seller-celebration-modal/index.jsx new file mode 100644 index 0000000000000..cc21de78e15de --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/seller-celebration-modal/index.jsx @@ -0,0 +1,137 @@ +import { recordTracksEvent } from '@automattic/calypso-analytics'; +import { Button } from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { useState, useRef, useEffect } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { useHasSeenSellerCelebrationModal } from '../../../dotcom-fse/lib/seller-celebration-modal/has-seen-seller-celebration-modal-context'; +import useShouldShowSellerCelebrationModal from '../../../dotcom-fse/lib/seller-celebration-modal/use-should-show-seller-celebration-modal'; +import useSiteIntent from '../../../dotcom-fse/lib/site-intent/use-site-intent'; +import NuxModal from '../nux-modal'; +import contentSubmittedImage from './images/product-published.svg'; +import './style.scss'; + +/** + * Show the seller celebration modal + */ +const SellerCelebrationModalInner = () => { + const { addEntities } = useDispatch( 'core' ); + + useEffect( () => { + // @TODO - not sure if I actually need this; need to test with it removed. + // Teach core data about the status entity so we can use selectors like `getEntityRecords()` + addEntities( [ + { + baseURL: '/wp/v2/statuses', + key: 'slug', + kind: 'root', + name: 'status', + plural: 'statuses', + }, + ] ); + // Only register entity once + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [] ); + // conditions to show: + // - user just finished saving (check) + // - editor has not yet displayed modal once (check) + // - user is a seller (check) + // - user has not saved site before + // - content includes product block, and a user has selected it at least once (check) + const [ isModalOpen, setIsModalOpen ] = useState( false ); + const [ hasDisplayedModal, setHasDisplayedModal ] = useState( false ); + + const isSiteEditor = useSelect( select => !! select( 'core/edit-site' ) ); + const previousIsEditorSaving = useRef( false ); + + const { updateHasSeenSellerCelebrationModal } = useHasSeenSellerCelebrationModal(); + + const linkUrl = useSelect( select => { + if ( isSiteEditor ) { + const page = select( 'core/edit-site' ).getPage(); + const pageId = parseInt( page?.context?.postId ); + const pageEntity = select( 'core' ).getEntityRecord( 'postType', 'page', pageId ); + return pageEntity?.link; + } + const currentPost = select( 'core/editor' ).getCurrentPost(); + return currentPost.link; + } ); + + const shouldShowSellerCelebrationModal = useShouldShowSellerCelebrationModal(); + + const isEditorSaving = useSelect( select => { + if ( isSiteEditor ) { + const page = select( 'core/edit-site' ).getPage(); + const pageId = parseInt( page?.context?.postId ); + const isSavingSite = + select( 'core' ).isSavingEntityRecord( 'root', 'site' ) && + ! select( 'core' ).isAutosavingEntityRecord( 'root', 'site' ); + const isSavingEntity = + select( 'core' ).isSavingEntityRecord( 'postType', 'page', pageId ) && + ! select( 'core' ).isAutosavingEntityRecord( 'postType', 'page', pageId ); + + return isSavingSite || isSavingEntity; + } + const currentPost = select( 'core/editor' ).getCurrentPost(); + const isSavingEntity = + select( 'core' ).isSavingEntityRecord( 'postType', currentPost?.type, currentPost?.id ) && + ! select( 'core' ).isAutosavingEntityRecord( 'postType', currentPost?.type, currentPost?.id ); + return isSavingEntity; + } ); + + useEffect( () => { + if ( + ! isEditorSaving && + previousIsEditorSaving.current && + ! hasDisplayedModal && + shouldShowSellerCelebrationModal + ) { + setIsModalOpen( true ); + setHasDisplayedModal( true ); + updateHasSeenSellerCelebrationModal( true ); + } + previousIsEditorSaving.current = isEditorSaving; + }, [ + isEditorSaving, + hasDisplayedModal, + shouldShowSellerCelebrationModal, + updateHasSeenSellerCelebrationModal, + ] ); + + // if save state has changed and was saving on last render + // then it has finished saving + // open modal if content has sell block, + + const closeModal = () => setIsModalOpen( false ); + return ( + + + + + } + onRequestClose={ closeModal } + onOpen={ () => recordTracksEvent( 'calypso_editor_wpcom_seller_celebration_modal_show' ) } + /> + ); +}; + +const SellerCelebrationModal = () => { + const { siteIntent: intent } = useSiteIntent(); + if ( intent === 'sell' ) { + return ; + } + return null; +}; + +export default SellerCelebrationModal; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/seller-celebration-modal/style.scss b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/seller-celebration-modal/style.scss new file mode 100644 index 0000000000000..c1576d9f74db5 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/seller-celebration-modal/style.scss @@ -0,0 +1,27 @@ +@import "@wordpress/base-styles/breakpoints"; +@import "@wordpress/base-styles/mixins"; + +.wpcom-site-editor-seller-celebration-modal { + .components-modal__content { + @include break-small { + padding: 48px 90px; + } + } + + .wpcom-block-editor-nux-modal__image-container { + img { + width: 158px; + height: 85px; + } + } + + .wpcom-block-editor-nux-modal__buttons { + .components-button { + min-width: 113px; + &:not(.is-primary) { + border: 1px solid #c3c4c7; + border-radius: 4px; + } + } + } +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/images/illo-share.svg b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/images/illo-share.svg new file mode 100644 index 0000000000000..41b457cb64585 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/images/illo-share.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/index.tsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/index.tsx new file mode 100644 index 0000000000000..e1f4ba38e481c --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/index.tsx @@ -0,0 +1,286 @@ +import { recordTracksEvent } from '@automattic/calypso-analytics'; +import { FormLabel } from '@automattic/components'; +import { START_WRITING_FLOW, DESIGN_FIRST_FLOW } from '@automattic/onboarding'; +import { Modal, Button } from '@wordpress/components'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { useEffect, useRef, useState } from '@wordpress/element'; +import { Icon, globe, link as linkIcon } from '@wordpress/icons'; +import { store as noticesStore } from '@wordpress/notices'; +import { useI18n } from '@wordpress/react-i18n'; +import ClipboardButton from 'calypso/components/forms/clipboard-button'; +import FormInputCheckbox from 'calypso/components/forms/form-checkbox'; +import clsx from 'clsx'; +import React from 'react'; +import { useShouldShowFirstPostPublishedModal } from '../../../dotcom-fse/lib/first-post-published-modal/should-show-first-post-published-modal-context'; +import useShouldShowSellerCelebrationModal from '../../../dotcom-fse/lib/seller-celebration-modal/use-should-show-seller-celebration-modal'; +import useSiteIntent from '../../../dotcom-fse/lib/site-intent/use-site-intent'; +import useShouldShowVideoCelebrationModal from '../../../dotcom-fse/lib/video-celebration-modal/use-should-show-video-celebration-modal'; +import postPublishedImage from './images/illo-share.svg'; +import InlineSocialLogo from './inline-social-logo'; +import InlineSocialLogosSprite from './inline-social-logos-sprite'; +import SuggestedTags from './suggested-tags'; +import useSharingModalDismissed from './use-sharing-modal-dismissed'; + +import './style.scss'; + +type CoreEditorPlaceholder = { + getCurrentPost: ( ...args: unknown[] ) => { + link: string; + title: string; + status: string; + password: string; + }; + getCurrentPostType: ( ...args: unknown[] ) => string; + isCurrentPostPublished: ( ...args: unknown[] ) => boolean; +}; +const FB_APP_ID = '249643311490'; + +const SharingModalInner: React.FC = () => { + const isDismissedDefault = window?.sharingModalOptions?.isDismissed || false; + const { launchpadScreenOption } = window?.launchpadOptions || {}; + const { isDismissed, updateIsDismissed } = useSharingModalDismissed( isDismissedDefault ); + const { __ } = useI18n(); + const isPrivateBlog = window?.wpcomGutenberg?.blogPublic === '-1'; + + const { + link, + title, + status: postStatus, + password: postPassword, + } = useSelect( + select => ( select( 'core/editor' ) as CoreEditorPlaceholder ).getCurrentPost(), + [] + ); + const postType = useSelect( + select => ( select( 'core/editor' ) as CoreEditorPlaceholder ).getCurrentPostType(), + [] + ); + + const isCurrentPostPublished = useSelect( + select => ( select( 'core/editor' ) as CoreEditorPlaceholder ).isCurrentPostPublished(), + [] + ); + const previousIsCurrentPostPublished = useRef( isCurrentPostPublished ); + const shouldShowFirstPostPublishedModal = useShouldShowFirstPostPublishedModal(); + const shouldShowSellerCelebrationModal = useShouldShowSellerCelebrationModal(); + const shouldShowVideoCelebrationModal = + useShouldShowVideoCelebrationModal( isCurrentPostPublished ); + + const [ isOpen, setIsOpen ] = useState( false ); + const closeModal = () => setIsOpen( false ); + const { createNotice } = useDispatch( noticesStore ); + const [ shouldShowSuggestedTags, setShouldShowSuggestedTags ] = React.useState( true ); + + useEffect( () => { + // The first post will show a different modal. + if ( + ! shouldShowFirstPostPublishedModal && + ! shouldShowSellerCelebrationModal && + ! shouldShowVideoCelebrationModal && + launchpadScreenOption !== 'full' && + ! previousIsCurrentPostPublished.current && + isCurrentPostPublished && + // Ensure post is published publicly and not private or password protected. + postStatus === 'publish' && + ! postPassword && + postType === 'post' + ) { + previousIsCurrentPostPublished.current = isCurrentPostPublished; + recordTracksEvent( 'calypso_editor_sharing_dialog_show' ); + + // When the post published panel shows, it is focused automatically. + // Thus, we need to delay open the modal so that the modal would not be close immediately + // because the outside of modal is focused + window.setTimeout( () => { + setIsOpen( true ); + } ); + } + }, [ + postType, + shouldShowFirstPostPublishedModal, + shouldShowSellerCelebrationModal, + shouldShowVideoCelebrationModal, + isCurrentPostPublished, + launchpadScreenOption, + ] ); + + if ( ! isOpen || isDismissedDefault || isPrivateBlog ) { + return null; + } + + const shareTwitter = () => { + const baseUrl = new URL( 'https://twitter.com/intent/tweet' ); + const params = new URLSearchParams( { + text: title, + url: link, + } ); + baseUrl.search = params.toString(); + const twitterUrl = baseUrl.href; + + recordTracksEvent( 'calypso_editor_sharing_twitter' ); + window.open( twitterUrl, 'twitter', 'width=550,height=420,resizeable,scrollbars' ); + }; + const shareFb = () => { + const baseUrl = new URL( 'https://www.facebook.com/sharer.php' ); + const params = new URLSearchParams( { + u: link, + app_id: FB_APP_ID, + } ); + baseUrl.search = params.toString(); + const facebookUrl = baseUrl.href; + + recordTracksEvent( 'calypso_editor_sharing_facebook' ); + window.open( facebookUrl, 'facebook', 'width=626,height=436,resizeable,scrollbars' ); + }; + const shareLinkedin = () => { + const baseUrl = new URL( 'https://www.linkedin.com/shareArticle' ); + const params = new URLSearchParams( { + title, + url: link, + } ); + baseUrl.search = params.toString(); + const linkedinUrl = baseUrl.href; + + recordTracksEvent( 'calypso_editor_sharing_linkedin' ); + window.open( linkedinUrl, 'linkedin', 'width=626,height=436,resizeable,scrollbars' ); + }; + const shareTumblr = () => { + const baseUrl = new URL( 'https://www.tumblr.com/widgets/share/tool' ); + const params = new URLSearchParams( { + canonicalUrl: link, + title: title, + } ); + baseUrl.search = params.toString(); + const tumblrUrl = baseUrl.href; + + recordTracksEvent( 'calypso_editor_sharing_tumblr' ); + window.open( tumblrUrl, 'tumblr', 'width=626,height=436,resizeable,scrollbars' ); + }; + const sharePinterest = () => { + const baseUrl = new URL( 'https://pinterest.com/pin/create/button/' ); + const params = new URLSearchParams( { + url: link, + description: title, + } ); + baseUrl.search = params.toString(); + const pinterestUrl = baseUrl.href; + + recordTracksEvent( 'calypso_editor_sharing_pinterest' ); + window.open( pinterestUrl, 'pinterest', 'width=626,height=436,resizeable,scrollbars' ); + }; + const copyLinkClick = () => { + recordTracksEvent( 'calypso_editor_sharing_link_copy' ); + createNotice( 'success', __( 'Link copied to clipboard.', 'jetpack-mu-wpcom' ), { + type: 'snackbar', + } ); + }; + return ( + + +
+
+

{ __( 'Post published!', 'jetpack-mu-wpcom' ) }

+
+ + { ' ' } + { __( 'View Post', 'jetpack-mu-wpcom' ) } + + + { __( 'Copy Link', 'jetpack-mu-wpcom' ) } + +
+
+

{ __( 'Get more traffic to your post by sharing:', 'jetpack-mu-wpcom' ) }

+ + + + + +
+ + { + updateIsDismissed( ! isDismissed ); + } } + /> + { __( "Don't show again", 'jetpack-mu-wpcom' ) } + +
+
+
+ { shouldShowSuggestedTags ? ( + + ) : ( + { + ) } +
+
+
+ ); +}; + +const SharingModal = () => { + const { siteIntent: intent } = useSiteIntent(); + if ( intent === START_WRITING_FLOW || intent === DESIGN_FIRST_FLOW ) { + return null; + } + return ; +}; +export default SharingModal; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/inline-social-logo.tsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/inline-social-logo.tsx new file mode 100644 index 0000000000000..af89f4b72391a --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/inline-social-logo.tsx @@ -0,0 +1,48 @@ +import clsx from 'clsx'; +import * as React from 'react'; +import { Assign } from 'utility-types'; + +interface Props { + icon: string; + size?: number; +} + +/** + * InlineSocialLogo is a copy of client/components/social-logo that references an inline SVG sprite. + * This componenet is needed because: + * + * The XML element does not work with SVGs loaded from external domains. + * In the editor, images are loaded from the CDN (s0.wp.com) in production. + * useInline allows us to reference an svg sprite from the current page instead. + * see https://github.com/w3c/svgwg/issues/707 + * + * InlineSocialLogosSprite must be included on the page where this is used + * @param props + * @returns A Social Logo SVG + */ +function InlineSocialLogo( props: Assign< React.SVGProps< SVGSVGElement >, Props > ) { + const { size = 24, icon, className, ...otherProps } = props; + + // Using a missing icon doesn't produce any errors, just a blank icon, which is the exact intended behaviour. + // This means we don't need to perform any checks on the icon name. + const iconName = `social-logo-${ icon }`; + // The current CSS expects individual icon classes in the form of e.g. `.twitter`, but the social-logos build + // appears to generate them in the form of `.social-logo-twitter` instead. + // We add both here, to ensure compatibility. + const iconClass = clsx( 'social-logo', iconName, icon, className ); + + return ( + + + + ); +} + +export default React.memo( InlineSocialLogo ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/inline-social-logos-sprite.tsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/inline-social-logos-sprite.tsx new file mode 100644 index 0000000000000..c0c5e9388cdad --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/inline-social-logos-sprite.tsx @@ -0,0 +1,286 @@ +/** + * A hidden inline svg sprite of social logos. + * + * Sprite was coppied from https://wordpress.com/calypso/images/social-logos-d55401f99bb02ebd6cf4.svg + * @returns see above. + */ +const InlineSocialLogosSprite = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; +export default InlineSocialLogosSprite; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/style.scss b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/style.scss new file mode 100644 index 0000000000000..c7738fc6fc0d5 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/style.scss @@ -0,0 +1,173 @@ +@import "@wordpress/base-styles/breakpoints"; +@import "@wordpress/base-styles/mixins"; + +.wpcom-block-editor-post-published-sharing-modal { + .components-modal__content { + margin-top: 0; + padding-bottom: 0; + .wpcom-block-editor-post-published-sharing-modal__inner { + display: flex; + .wpcom-block-editor-post-published-sharing-modal__left { + width: 50%; + padding: 52px 40px 52px 20px; + + p a { + color: var(--color-text); + text-decoration: underline; + white-space: nowrap; + + &:hover { + color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba)); + text-decoration: none; + } + + .components-external-link__icon { + margin-left: 4px; + } + } + + .wpcom-block-editor-post-published-buttons { + display: flex; + } + } + .wpcom-block-editor-post-published-sharing-modal__right { + width: 50%; + padding: 52px 20px 52px 40px; + border-left: 1px solid var(--studio-gray-5); + display: flex; + justify-content: center; + min-width: 300px; + } + + @media only screen and (max-width: 600px) { + flex-direction: column-reverse; + + .wpcom-block-editor-post-published-sharing-modal__left { + padding: 44px 0 20px; + width: 100%; + } + .wpcom-block-editor-post-published-sharing-modal__right { + border-left: none; + padding: 52px 0 0; + width: 100%; + } + .wpcom-block-editor-post-published-sharing-modal__image { + height: 140px; + } + } + } + h1 { + margin-top: 0; + /* stylelint-disable-next-line declaration-property-unit-allowed-list */ + font-size: 26px; + @media only screen and (max-width: 600px) { + font-size: 2.25rem; + font-weight: 500; + line-height: 1; + } + } + p { + /* stylelint-disable-next-line declaration-property-unit-allowed-list */ + font-size: 18px; + @media only screen and (max-width: 600px) { + font-size: 1rem; + } + } + .link-button { + /* stylelint-disable-next-line declaration-property-unit-allowed-list */ + font-size: 14px; + font-weight: 500; + display: inline-flex; + border: none; + background: transparent; + color: var(--color-text); + padding: 0 14px 0 0; + line-height: 2.71428571; + min-height: 40px; + margin-bottom: 4px; + align-items: center; + text-decoration: underline; + white-space: nowrap; + + &:hover { + background: transparent; + color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba)); + text-decoration: none; + } + + svg { + fill: currentColor; + } + } + hr { + margin-top: 20px; + border-top: 1px solid var(--studio-gray-5); + border-bottom: none; + } + h2 { + /* stylelint-disable-next-line declaration-property-unit-allowed-list */ + font-size: 18px; + font-weight: 600px; + } + .wpcom-block-editor-post-published-sharing-modal__checkbox-section { + margin-top: 40px; + color: var(--studio-gray-60); + } + .form-checkbox { + margin-top: 1px; + height: 1rem; + width: 1rem; + &::before { + width: 1.3125rem; + height: 1.3125rem; + } + } + .wpcom-block-editor-post-published-sharing-modal__suggest-tags { + width: 250px; + flex: fit-content; + } + } +} + +.wpcom-block-editor-post-published-buttons .link-button { + gap: 4px; + padding-left: 0; +} + +.wpcom-block-editor-post-published-sharing-modal__sharing-button { + display: inline-block; + position: relative; + width: 32px; + height: 32px; + top: -2px; + margin: 8px 8px 0 0; + border-radius: 50%; + color: var(--color-neutral-80); + background: var(--studio-white); + padding: 7px; + + &:hover { + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.22), 0 0 0 1px rgba(0, 0, 0, 0.22); + } + + .social-logo { + top: 0; + color: var(--color-text-inverted); + } +} + +@mixin sharing-button-service( $name, $color ) { + .wpcom-block-editor-post-published-sharing-modal__sharing-button.share-#{ $name } { + background: $color; + + &:hover { + background: $color; + } + } +} + +@include sharing-button-service( "facebook", var( --color-facebook ) ); +@include sharing-button-service( "twitter", var( --color-twitter ) ); +@include sharing-button-service( "linkedin", var( --color-linkedin ) ); +@include sharing-button-service( "tumblr", var( --color-tumblr ) ); +@include sharing-button-service( "pinterest", var( --color-pinterest ) ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/suggested-tags.tsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/suggested-tags.tsx new file mode 100644 index 0000000000000..38ea61ebed535 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/suggested-tags.tsx @@ -0,0 +1,130 @@ +import { recordTracksEvent } from '@automattic/calypso-analytics'; +import { useLocale } from '@automattic/i18n-utils'; +import { Button, FormTokenField } from '@wordpress/components'; +import { TokenItem } from '@wordpress/components/build-types/form-token-field/types'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { useEffect } from '@wordpress/element'; +import { store as noticesStore } from '@wordpress/notices'; +import { useI18n } from '@wordpress/react-i18n'; +import * as React from 'react'; +import useAddTagsToPost from './use-add-tags-to-post'; + +type PostMeta = { + reader_suggested_tags: string; +}; + +type CoreEditorPlaceholder = { + getCurrentPost: ( ...args: unknown[] ) => { + id: number; + meta: PostMeta; + }; +}; + +type SuggestedTagsEventProps = { + number_of_original_suggested_tags: number; + number_of_selected_tags: number; + number_of_suggested_tags_selected: number; + number_of_added_tags: number; +}; + +type SuggestedTagsProps = { + setShouldShowSuggestedTags: ( shouldShow: boolean ) => void; +}; + +/** + * + * @param props + */ +function SuggestedTags( props: SuggestedTagsProps ) { + const { __, _n } = useI18n(); + const localeSlug = useLocale(); + const { id: postId, meta: postMeta } = useSelect( + select => ( select( 'core/editor' ) as CoreEditorPlaceholder ).getCurrentPost(), + [] + ); + const { createNotice } = useDispatch( noticesStore ); + const origSuggestedTags = postMeta?.reader_suggested_tags + ? JSON.parse( postMeta.reader_suggested_tags ) + : []; + const [ selectedTags, setSelectedTags ] = React.useState( origSuggestedTags ); + const onAddTagsButtonClick = ( numAddedTags: number ) => { + // Compare origSuggestedTags and selectedTags and determine the number of tags that are different + const numSuggestedTags = origSuggestedTags.length; + const numSelectedTags = selectedTags.length; + const numSameTags = origSuggestedTags.filter( ( tag: string ) => + selectedTags.includes( tag ) + ).length; + const eventProps: SuggestedTagsEventProps = { + number_of_original_suggested_tags: numSuggestedTags, + number_of_selected_tags: numSelectedTags, + number_of_suggested_tags_selected: numSameTags, + number_of_added_tags: numAddedTags, + }; + recordTracksEvent( 'calypso_reader_post_publish_add_tags', eventProps ); + if ( numAddedTags > 0 ) { + createNotice( + 'success', + _n( 'Tag Added.', 'Tags Added.', numAddedTags, 'jetpack-mu-wpcom' ), + { + type: 'snackbar', + } + ); + } else { + createNotice( 'warning', __( 'No Tags Added.', 'jetpack-mu-wpcom' ), { + type: 'snackbar', + } ); + } + props.setShouldShowSuggestedTags( false ); + }; + const { saveTags } = useAddTagsToPost( postId, selectedTags, onAddTagsButtonClick ); + + useEffect( () => { + if ( origSuggestedTags?.length === 0 ) { + // Check if localeSlug begins with 'en' + if ( localeSlug && localeSlug.startsWith( 'en' ) ) { + recordTracksEvent( 'calypso_reader_post_publish_no_suggested_tags' ); + } + props.setShouldShowSuggestedTags( false ); + } else { + recordTracksEvent( 'calypso_reader_post_publish_show_suggested_tags', { + number_of_original_suggested_tags: origSuggestedTags.length, + } ); + } + }, [] ); + + const onChangeSelectedTags = ( newTags: ( string | TokenItem )[] ) => { + setSelectedTags( newTags ); + recordTracksEvent( 'calypso_reader_post_publish_update_suggested_tags' ); + }; + + const tokenField = ( + + ); + + return ( +
+

{ __( 'Recommended tags:', 'jetpack-mu-wpcom' ) }

+

+ { __( + 'Based on the topics and themes in your post, here are some suggested tags to consider:', + 'jetpack-mu-wpcom' + ) } +

+ { tokenField } +

{ __( 'Adding tags can help drive more traffic to your post.', 'jetpack-mu-wpcom' ) }

+ +
+ ); +} + +export default React.memo( SuggestedTags ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/use-add-tags-to-post.ts b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/use-add-tags-to-post.ts new file mode 100644 index 0000000000000..22572940c6da5 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/use-add-tags-to-post.ts @@ -0,0 +1,31 @@ +import apiFetch from '@wordpress/api-fetch'; + +type HasAddedTagsResult = { + added_tags: number; + success: boolean; +}; + +type OnSaveTagsCallback = ( addedTags: number ) => void; +const useAddTagsToPost = ( postId: number, tags: string[], onSaveTags: OnSaveTagsCallback ) => { + /** + * + */ + async function saveTags() { + let addedTags = 0; + try { + const result: HasAddedTagsResult = await apiFetch( { + method: 'POST', + path: `/wpcom/v2/read/sites/${ window._currentSiteId }/posts/${ postId }/tags/add`, + data: { tags }, + } ); + addedTags = result.added_tags ?? 0; + } catch ( error ) { + // eslint-disable-next-line no-console + console.error( 'Error: Unable to add tags. Reason: %s', JSON.stringify( error ) ); + } + onSaveTags( addedTags ); + } + return { saveTags }; +}; + +export default useAddTagsToPost; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/use-sharing-modal-dismissed.ts b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/use-sharing-modal-dismissed.ts new file mode 100644 index 0000000000000..5cb0a060401d7 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/use-sharing-modal-dismissed.ts @@ -0,0 +1,23 @@ +import apiFetch from '@wordpress/api-fetch'; +import { useState } from '@wordpress/element'; + +const useSharingModalDismissed = ( initial: boolean ) => { + const [ isDismissed, setSharingModalDismissed ] = useState( initial ); + + /** + * + * @param value + */ + function updateIsDismissed( value: boolean ) { + apiFetch( { + method: 'PUT', + path: '/wpcom/v2/block-editor/sharing-modal-dismissed', + data: { wpcom_sharing_modal_dismissed: value }, + } ).finally( () => { + setSharingModalDismissed( value ); + } ); + } + return { isDismissed, updateIsDismissed }; +}; + +export default useSharingModalDismissed; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/store.js b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/store.js new file mode 100644 index 0000000000000..56eb88112eb9a --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/store.js @@ -0,0 +1,152 @@ +import apiFetch from '@wordpress/api-fetch'; +import { combineReducers, registerStore } from '@wordpress/data'; +import { apiFetch as apiFetchControls, controls } from '@wordpress/data-controls'; + +import 'a8c-fse-common-data-stores'; + +export const DEFAULT_VARIANT = 'tour'; +export const BLANK_CANVAS_VARIANT = 'blank-canvas-tour'; + +const showWelcomeGuideReducer = ( state = undefined, action ) => { + switch ( action.type ) { + case 'WPCOM_WELCOME_GUIDE_FETCH_STATUS_SUCCESS': + return action.response.show_welcome_guide; + case 'WPCOM_WELCOME_GUIDE_SHOW_SET': + return action.show; + case 'WPCOM_WELCOME_GUIDE_RESET_STORE': + return undefined; + default: + return state; + } +}; + +const welcomeGuideManuallyOpenedReducer = ( state = false, action ) => { + switch ( action.type ) { + case 'WPCOM_WELCOME_GUIDE_SHOW_SET': + if ( typeof action.openedManually !== 'undefined' ) { + return action.openedManually; + } + return state; + + case 'WPCOM_WELCOME_GUIDE_RESET_STORE': + return false; + + default: + return state; + } +}; + +// TODO: next PR convert file to Typescript to ensure control of tourRating values: null, 'thumbs-up' 'thumbs-down' +const tourRatingReducer = ( state = undefined, action ) => { + switch ( action.type ) { + case 'WPCOM_WELCOME_GUIDE_TOUR_RATING_SET': + return action.tourRating; + case 'WPCOM_WELCOME_GUIDE_RESET_STORE': + return undefined; + default: + return state; + } +}; + +const welcomeGuideVariantReducer = ( state = DEFAULT_VARIANT, action ) => { + switch ( action.type ) { + case 'WPCOM_WELCOME_GUIDE_FETCH_STATUS_SUCCESS': + return action.response.variant; + case 'WPCOM_HAS_USED_PATTERNS_MODAL': + return state === BLANK_CANVAS_VARIANT ? DEFAULT_VARIANT : state; + case 'WPCOM_WELCOME_GUIDE_RESET_STORE': + return DEFAULT_VARIANT; + default: + return state; + } +}; + +const shouldShowFirstPostPublishedModalReducer = ( state = false, action ) => { + switch ( action.type ) { + case 'WPCOM_SET_SHOULD_SHOW_FIRST_POST_PUBLISHED_MODAL': + return action.value; + case 'WPCOM_WELCOME_GUIDE_RESET_STORE': + return false; + default: + return state; + } +}; + +const reducer = combineReducers( { + welcomeGuideManuallyOpened: welcomeGuideManuallyOpenedReducer, + showWelcomeGuide: showWelcomeGuideReducer, + tourRating: tourRatingReducer, + welcomeGuideVariant: welcomeGuideVariantReducer, + shouldShowFirstPostPublishedModal: shouldShowFirstPostPublishedModalReducer, +} ); + +export const actions = { + *fetchWelcomeGuideStatus() { + const response = yield apiFetchControls( { path: '/wpcom/v2/block-editor/nux' } ); + + return { + type: 'WPCOM_WELCOME_GUIDE_FETCH_STATUS_SUCCESS', + response, + }; + }, + *fetchShouldShowFirstPostPublishedModal() { + const response = yield apiFetchControls( { + path: '/wpcom/v2/block-editor/should-show-first-post-published-modal', + } ); + + return { + type: 'WPCOM_SET_SHOULD_SHOW_FIRST_POST_PUBLISHED_MODAL', + value: response.should_show_first_post_published_modal, + }; + }, + setShowWelcomeGuide: ( show, { openedManually, onlyLocal } = {} ) => { + if ( ! onlyLocal ) { + apiFetch( { + path: '/wpcom/v2/block-editor/nux', + method: 'POST', + data: { show_welcome_guide: show }, + } ); + } + + return { + type: 'WPCOM_WELCOME_GUIDE_SHOW_SET', + show, + openedManually, + }; + }, + setTourRating: tourRating => { + return { type: 'WPCOM_WELCOME_GUIDE_TOUR_RATING_SET', tourRating }; + }, + setUsedPageOrPatternsModal: () => { + return { type: 'WPCOM_HAS_USED_PATTERNS_MODAL' }; + }, + // The `resetStore` action is only used for testing to reset the + // store inbetween tests. + resetStore: () => ( { + type: 'WPCOM_WELCOME_GUIDE_RESET_STORE', + } ), +}; + +export const selectors = { + isWelcomeGuideManuallyOpened: state => state.welcomeGuideManuallyOpened, + isWelcomeGuideShown: state => !! state.showWelcomeGuide, + isWelcomeGuideStatusLoaded: state => typeof state.showWelcomeGuide !== 'undefined', + getTourRating: state => state.tourRating, + // the 'modal' variant previously used for mobile has been removed but its slug may still be persisted in local storage + getWelcomeGuideVariant: state => + state.welcomeGuideVariant === 'modal' ? DEFAULT_VARIANT : state.welcomeGuideVariant, + getShouldShowFirstPostPublishedModal: state => state.shouldShowFirstPostPublishedModal, +}; + +/** + * + */ +export function register() { + return registerStore( 'automattic/wpcom-welcome-guide', { + reducer, + actions, + selectors, + controls, + persist: true, + } ); +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/test/store.test.js b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/test/store.test.js new file mode 100644 index 0000000000000..352fbd939e698 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/test/store.test.js @@ -0,0 +1,127 @@ +import { dispatch, select } from '@wordpress/data'; +import waitForExpect from 'wait-for-expect'; +import { register, DEFAULT_VARIANT } from '../store'; + +const STORE_KEY = 'automattic/wpcom-welcome-guide'; + +beforeAll( () => { + register(); + jest.useRealTimers(); // Required for wait-for-expect to work. +} ); + +let originalFetch; +beforeEach( () => { + dispatch( STORE_KEY ).resetStore(); + originalFetch = window.fetch; + jest.spyOn( window, 'fetch' ).mockImplementation(); +} ); + +afterEach( () => { + window.fetch = originalFetch; +} ); + +test( 'resetting the store', async () => { + window.fetch.mockResolvedValue( { + status: 200, + json: () => Promise.resolve( { show_welcome_guide: true, variant: 'modal' } ), + } ); + + dispatch( STORE_KEY ).fetchWelcomeGuideStatus(); + await waitForExpect( () => + expect( select( STORE_KEY ).isWelcomeGuideStatusLoaded() ).toBe( true ) + ); + dispatch( STORE_KEY ).setShowWelcomeGuide( true, { openedManually: true } ); + dispatch( STORE_KEY ).setTourRating( 'thumbs-up' ); + + dispatch( STORE_KEY ).resetStore(); + + expect( select( STORE_KEY ).isWelcomeGuideManuallyOpened() ).toBe( false ); + expect( select( STORE_KEY ).isWelcomeGuideShown() ).toBe( false ); + expect( select( STORE_KEY ).isWelcomeGuideStatusLoaded() ).toBe( false ); + expect( select( STORE_KEY ).getTourRating() ).toBeUndefined(); + expect( select( STORE_KEY ).getWelcomeGuideVariant() ).toBe( DEFAULT_VARIANT ); +} ); + +test( "by default the store isn't loaded", () => { + const isLoaded = select( STORE_KEY ).isWelcomeGuideStatusLoaded(); + expect( isLoaded ).toBe( false ); +} ); + +test( 'after fetching the guide status the store is loaded', async () => { + window.fetch.mockResolvedValue( { + status: 200, + json: () => Promise.resolve( { show_welcome_guide: true, variant: DEFAULT_VARIANT } ), + } ); + + dispatch( STORE_KEY ).fetchWelcomeGuideStatus(); + + await waitForExpect( () => { + const isLoaded = select( STORE_KEY ).isWelcomeGuideStatusLoaded(); + expect( isLoaded ).toBe( true ); + } ); + + expect( window.fetch ).toHaveBeenCalledWith( + '/wpcom/v2/block-editor/nux?_locale=user', + expect.anything() + ); + + // Check the store is loaded with the state that came from the server + const isWelcomeGuideShown = select( STORE_KEY ).isWelcomeGuideShown(); + expect( isWelcomeGuideShown ).toBe( true ); + const welcomeGuideVariant = select( STORE_KEY ).getWelcomeGuideVariant(); + expect( welcomeGuideVariant ).toBe( DEFAULT_VARIANT ); +} ); + +test( 'toggle welcome guide visibility', () => { + // setShowWelcomeGuide kicks off a save. This mock fixes unresolved promise + // rejection errors that appear in CLI output + window.fetch.mockResolvedValue( { status: 200, json: () => Promise.resolve( {} ) } ); + + dispatch( STORE_KEY ).setShowWelcomeGuide( true ); + expect( select( STORE_KEY ).isWelcomeGuideShown() ).toBe( true ); + + dispatch( STORE_KEY ).setShowWelcomeGuide( false ); + expect( select( STORE_KEY ).isWelcomeGuideShown() ).toBe( false ); +} ); + +test( 'guide manually opened flag is false by default', () => { + expect( select( STORE_KEY ).isWelcomeGuideManuallyOpened() ).toBe( false ); +} ); + +test( '"manually opened" flag can be set when opening welcome guide', () => { + // setShowWelcomeGuide kicks off a save. This mock fixes unresolved promise + // rejection errors that appear in CLI output + window.fetch.mockResolvedValue( { status: 200, json: () => Promise.resolve( {} ) } ); + + dispatch( STORE_KEY ).setShowWelcomeGuide( true, { openedManually: true } ); + expect( select( STORE_KEY ).isWelcomeGuideManuallyOpened() ).toBe( true ); + + dispatch( STORE_KEY ).setShowWelcomeGuide( true, { openedManually: false } ); + expect( select( STORE_KEY ).isWelcomeGuideManuallyOpened() ).toBe( false ); +} ); + +test( 'leaving `openedManually` unspecified leaves the flag unchanged', () => { + // setShowWelcomeGuide kicks off a save. This mock fixes unresolved promise + // rejection errors that appear in CLI output + window.fetch.mockResolvedValue( { status: 200, json: () => Promise.resolve( {} ) } ); + + expect( select( STORE_KEY ).isWelcomeGuideManuallyOpened() ).toBe( false ); + + dispatch( STORE_KEY ).setShowWelcomeGuide( true, { openedManually: true } ); + expect( select( STORE_KEY ).isWelcomeGuideManuallyOpened() ).toBe( true ); + + dispatch( STORE_KEY ).setShowWelcomeGuide( false ); + expect( select( STORE_KEY ).isWelcomeGuideManuallyOpened() ).toBe( true ); +} ); + +test( 'tour rating is "undefined" by default', () => { + expect( select( STORE_KEY ).getTourRating() ).toBeUndefined(); +} ); + +test( 'tour rating can be set to "thumbs-up" or "thumbs-down"', () => { + dispatch( STORE_KEY ).setTourRating( 'thumbs-up' ); + expect( select( STORE_KEY ).getTourRating() ).toBe( 'thumbs-up' ); + + dispatch( STORE_KEY ).setTourRating( 'thumbs-down' ); + expect( select( STORE_KEY ).getTourRating() ).toBe( 'thumbs-down' ); +} ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/video-celebration-modal/index.jsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/video-celebration-modal/index.jsx new file mode 100644 index 0000000000000..a909db6b5f744 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/video-celebration-modal/index.jsx @@ -0,0 +1,111 @@ +import { Button } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { useState, useRef, useEffect } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import videoSuccessImage from 'calypso/assets/images/illustrations/video-success.svg'; +import useSiteIntent from '../../../dotcom-fse/lib/site-intent/use-site-intent'; +import { useHasSeenVideoCelebrationModal } from '../../../dotcom-fse/lib/video-celebration-modal/has-seen-video-celebration-modal-context'; +import useShouldShowVideoCelebrationModal from '../../../dotcom-fse/lib/video-celebration-modal/use-should-show-video-celebration-modal'; +import NuxModal from '../nux-modal'; +import './style.scss'; + +// Shows a celebration modal after a video is first uploaded to a site and the editor is saved. +const VideoCelebrationModalInner = () => { + const [ isModalOpen, setIsModalOpen ] = useState( false ); + const [ hasDisplayedModal, setHasDisplayedModal ] = useState( false ); + const isSiteEditor = useSelect( select => !! select( 'core/edit-site' ) ); + const previousIsEditorSaving = useRef( false ); + const { updateHasSeenVideoCelebrationModal } = useHasSeenVideoCelebrationModal(); + + const { isEditorSaving } = useSelect( select => { + if ( isSiteEditor ) { + const isSavingSite = + select( 'core' ).isSavingEntityRecord( 'root', 'site' ) && + ! select( 'core' ).isAutosavingEntityRecord( 'root', 'site' ); + + const page = select( 'core/edit-site' ).getPage(); + const pageId = parseInt( page?.context?.postId ); + const isSavingEntity = + select( 'core' ).isSavingEntityRecord( 'postType', 'page', pageId ) && + ! select( 'core' ).isAutosavingEntityRecord( 'postType', 'page', pageId ); + const pageEntity = select( 'core' ).getEntityRecord( 'postType', 'page', pageId ); + + return { + isEditorSaving: isSavingSite || isSavingEntity, + linkUrl: pageEntity?.link, + }; + } + + const currentPost = select( 'core/editor' ).getCurrentPost(); + const isSavingEntity = + select( 'core' ).isSavingEntityRecord( 'postType', currentPost?.type, currentPost?.id ) && + ! select( 'core' ).isAutosavingEntityRecord( 'postType', currentPost?.type, currentPost?.id ); + + return { + isEditorSaving: isSavingEntity, + }; + } ); + const shouldShowVideoCelebrationModal = useShouldShowVideoCelebrationModal( isEditorSaving ); + + useEffect( () => { + // Conditions to show modal: + // - user just finished saving + // - celebration modal hasn't been viewed/isn't visible + // - site intent is 'videopress' + // - site has uploaded a video + if ( + ! isEditorSaving && + previousIsEditorSaving.current && + ! hasDisplayedModal && + shouldShowVideoCelebrationModal + ) { + setIsModalOpen( true ); + setHasDisplayedModal( true ); + updateHasSeenVideoCelebrationModal( true ); + } + previousIsEditorSaving.current = isEditorSaving; + }, [ + isEditorSaving, + hasDisplayedModal, + shouldShowVideoCelebrationModal, + updateHasSeenVideoCelebrationModal, + ] ); + + const closeModal = () => setIsModalOpen( false ); + return ( + + + + + } + onRequestClose={ closeModal } + /> + ); +}; + +const VideoCelebrationModal = () => { + const { siteIntent: intent } = useSiteIntent(); + if ( 'videopress' === intent ) { + return ; + } + return null; +}; + +export default VideoCelebrationModal; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/video-celebration-modal/style.scss b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/video-celebration-modal/style.scss new file mode 100644 index 0000000000000..eca641aefdaa7 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/video-celebration-modal/style.scss @@ -0,0 +1,37 @@ +@import "@wordpress/base-styles/breakpoints"; +@import "@wordpress/base-styles/mixins"; + +.wpcom-site-editor-video-celebration-modal { + .components-modal__content { + @include break-small { + padding: 48px 90px; + } + } + + .wpcom-block-editor-nux-modal__image-container { + img { + width: 158px; + height: 85px; + } + } + + .wpcom-block-editor-nux-modal__buttons { + .components-button { + min-width: 113px; + &:not(.is-primary) { + border: 1px solid #c3c4c7; + border-radius: 4px; + } + } + } + + .components-modal__header { + button { + svg { + path { + transform: scale(1); + } + } + } + } +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/images/block-picker.svg b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/images/block-picker.svg new file mode 100644 index 0000000000000..a7fe75f9d393e --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/images/block-picker.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/images/editor.svg b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/images/editor.svg new file mode 100644 index 0000000000000..b3f080ffd46fd --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/images/editor.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/images/preview.svg b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/images/preview.svg new file mode 100644 index 0000000000000..120e144993c70 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/images/preview.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/images/private.svg b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/images/private.svg new file mode 100644 index 0000000000000..6603602512258 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/images/private.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/style.scss b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/style.scss new file mode 100644 index 0000000000000..e5df19a905c27 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/style.scss @@ -0,0 +1,207 @@ +@import "@automattic/typography/styles/fonts"; +@import "@automattic/onboarding/styles/mixins"; + +$wpcom-modal-breakpoint: 660px; + +$wpcom-modal-padding-v: 40px; +$wpcom-modal-padding-h: 50px; +$wpcom-modal-content-min-height: 350px; +$wpcom-modal-footer-padding-v: 20px; +$wpcom-modal-footer-height: 30px + ( $wpcom-modal-footer-padding-v * 2 ); + +// Core modal style overrides +.wpcom-block-editor-nux { + &.components-modal__frame { + overflow: visible; + height: 65vh; + top: calc(17.5vh - #{$wpcom-modal-footer-height * 0.5}); + + @media (max-width: $wpcom-modal-breakpoint) { + width: 90vw; + min-width: 90vw; + left: 5vw; + right: 5vw; + } + + @media (min-width: $wpcom-modal-breakpoint) { + width: 720px; + height: $wpcom-modal-content-min-height; + top: calc(50% - #{$wpcom-modal-footer-height * 0.5}); + } + } + + .components-modal__header { + position: absolute; + max-width: 90%; + left: 5%; + @media (min-width: $wpcom-modal-breakpoint) { + display: none; + } + } + + .components-guide__container { + margin-top: 0; + } + + .components-guide__footer { + position: absolute; + width: 100%; + height: $wpcom-modal-footer-height; + bottom: $wpcom-modal-footer-height * -1; + left: 0; + padding: $wpcom-modal-footer-padding-v 0; + margin: 0; + display: flex; + justify-content: center; + background: var(--studio-white); + border-top: 1px solid #dcdcde; + + @media (min-width: $wpcom-modal-breakpoint) { + border-top: none; + } + } + + .components-guide__page { + position: absolute; + width: 100%; + max-width: 90vw; + height: 100%; + justify-content: start; + + @media (min-width: $wpcom-modal-breakpoint) { + max-width: 100%; + } + } + + .components-guide__page-control { + position: relative; + height: 0; + top: 100%; + overflow: visible; + margin: 0 auto; + z-index: 2; + + &::before { + display: inline-block; + content: ""; + height: $wpcom-modal-footer-height; + vertical-align: middle; + } + + li { + vertical-align: middle; + margin-bottom: 0; + } + + // Temporarily disable dots on mobile as alignment is wonky. + display: none; + @media (min-width: $wpcom-modal-breakpoint) { + display: block; + } + } +} + +.wpcom-block-editor-nux__page { + display: flex; + flex-direction: column-reverse; + justify-content: flex-end; + background: var(--studio-white); + width: 100%; + height: 90%; + max-width: 90vw; + + @media (min-width: $wpcom-modal-breakpoint) { + flex-direction: row; + justify-content: flex-start; + position: absolute; + max-width: 100%; + min-height: $wpcom-modal-content-min-height; + bottom: 0; + } +} + +.wpcom-block-editor-nux__text, +.wpcom-block-editor-nux__visual { + @media (min-width: $wpcom-modal-breakpoint) { + flex: 1 0 50%; + min-width: 290px; + } +} + +.wpcom-block-editor-nux__text { + padding: 0 25px 25px; + height: 60%; + + @media (min-width: $wpcom-modal-breakpoint) { + height: auto; + padding: $wpcom-modal-padding-v $wpcom-modal-padding-h; + } +} +.wpcom-block-editor-nux__visual { + height: 40%; + background: #1381d8; + text-align: center; + + @media (min-width: $wpcom-modal-breakpoint) { + height: auto; + } +} + +.wpcom-block-editor-nux__heading { + @include onboarding-font-recoleta; + /* Gray / Gray 90 */ + color: #1d2327; + + /* stylelint-disable-next-line declaration-property-unit-allowed-list */ + font-size: 32px; + line-height: 1.19; + + @media (min-width: $wpcom-modal-breakpoint) { + /* stylelint-disable-next-line declaration-property-unit-allowed-list */ + font-size: 42px; + } + + // TODO: remove this hack once the welcome editor deals better with + // overflowing text + body.locale-de & { + /* stylelint-disable-next-line declaration-property-unit-allowed-list */ + font-size: 24px; + + @media (min-width: $wpcom-modal-breakpoint) { + /* stylelint-disable-next-line declaration-property-unit-allowed-list */ + font-size: 28px; + } + } +} + +.wpcom-block-editor-nux__description { + /* stylelint-disable-next-line declaration-property-unit-allowed-list */ + font-size: 15px; + line-height: 22px; + + /* Gray / Gray 60 */ + color: #50575e; + + @media (min-width: $wpcom-modal-breakpoint) { + /* stylelint-disable-next-line declaration-property-unit-allowed-list */ + font-size: 17px; + line-height: 26px; + } +} + +.wpcom-block-editor-nux__image { + max-width: 100%; + height: auto; + flex: 1; + align-self: center; + + &.align-bottom { + align-self: flex-end; + } + + max-height: 100%; + + @media (min-width: $wpcom-modal-breakpoint) { + max-height: none; + } +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/wpcom-nux.js b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/wpcom-nux.js new file mode 100644 index 0000000000000..0d3c57fceb9cf --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/wpcom-nux.js @@ -0,0 +1,154 @@ +/* eslint-disable wpcalypso/jsx-classname-namespace */ + +import { recordTracksEvent } from '@automattic/calypso-analytics'; +import { Guide, GuidePage } from '@wordpress/components'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { useEffect } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import blockPickerImage from './images/block-picker.svg'; +import editorImage from './images/editor.svg'; +import previewImage from './images/preview.svg'; +import privateImage from './images/private.svg'; + +import './style.scss'; + +/** + * + */ +function WpcomNux() { + const { show, isNewPageLayoutModalOpen, isManuallyOpened } = useSelect( select => ( { + show: select( 'automattic/wpcom-welcome-guide' ).isWelcomeGuideShown(), + isNewPageLayoutModalOpen: + select( 'automattic/starter-page-layouts' ) && // Handle the case where SPT is not initalized. + select( 'automattic/starter-page-layouts' ).isOpen(), + isManuallyOpened: select( 'automattic/wpcom-welcome-guide' ).isWelcomeGuideManuallyOpened(), + } ) ); + + const { setShowWelcomeGuide } = useDispatch( 'automattic/wpcom-welcome-guide' ); + + // Track opening of the welcome guide + useEffect( () => { + if ( show && ! isNewPageLayoutModalOpen ) { + recordTracksEvent( 'calypso_editor_wpcom_nux_open', { + is_gutenboarding: window.calypsoifyGutenberg?.isGutenboarding, + is_manually_opened: isManuallyOpened, + } ); + } + }, [ isManuallyOpened, isNewPageLayoutModalOpen, show ] ); + + if ( ! show || isNewPageLayoutModalOpen ) { + return null; + } + + const dismissWpcomNux = () => { + recordTracksEvent( 'calypso_editor_wpcom_nux_dismiss', { + is_gutenboarding: window.calypsoifyGutenberg?.isGutenboarding, + } ); + setShowWelcomeGuide( false, { openedManually: false } ); + }; + + const nuxPages = getWpcomNuxPages(); + + return ( + + { nuxPages.map( ( nuxPage, index ) => ( + + ) ) } + + ); +} + +/** + * This function returns a collection of NUX slide data + * @returns { Array } a collection of props + */ +function getWpcomNuxPages() { + return [ + { + heading: __( 'Welcome to your website', 'jetpack-mu-wpcom' ), + description: __( + 'Edit your homepage, add the pages you need, and change your site’s look and feel.', + 'jetpack-mu-wpcom' + ), + imgSrc: editorImage, + alignBottom: true, + }, + { + heading: __( 'Add or edit your content', 'jetpack-mu-wpcom' ), + description: __( + 'Edit the placeholder content we’ve started you off with, or click the plus sign to add more content.', + 'jetpack-mu-wpcom' + ), + imgSrc: blockPickerImage, + }, + { + heading: __( 'Preview your site as you go', 'jetpack-mu-wpcom' ), + description: __( + 'As you edit your site content, click “Preview” to see your site the way your visitors will.', + 'jetpack-mu-wpcom' + ), + imgSrc: previewImage, + alignBottom: true, + }, + { + heading: __( 'Hidden until you’re ready', 'jetpack-mu-wpcom' ), + description: __( + 'Your site will remain hidden until launched. Click “Launch” in the toolbar to share it with the world.', + 'jetpack-mu-wpcom' + ), + imgSrc: privateImage, + alignBottom: true, + }, + ]; +} + +/** + * + * @param root0 + * @param root0.pageNumber + * @param root0.isLastPage + * @param root0.alignBottom + * @param root0.heading + * @param root0.description + * @param root0.imgSrc + */ +function NuxPage( { pageNumber, isLastPage, alignBottom = false, heading, description, imgSrc } ) { + useEffect( () => { + recordTracksEvent( 'calypso_editor_wpcom_nux_slide_view', { + slide_number: pageNumber, + is_last_slide: isLastPage, + is_gutenboarding: window.calypsoifyGutenberg?.isGutenboarding, + } ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [] ); + return ( + +
+

{ heading }

+
{ description }
+
+
+ +
+
+ ); +} + +export default WpcomNux; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/get-editor-type.ts b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/get-editor-type.ts new file mode 100644 index 0000000000000..876bd2e34a5d3 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/get-editor-type.ts @@ -0,0 +1,41 @@ +import { select } from '@wordpress/data'; + +/** + * Post (Post Type: ‘post’) + * Page (Post Type: ‘page’) + * Attachment (Post Type: ‘attachment’) + * Revision (Post Type: ‘revision’) + * Navigation menu (Post Type: ‘nav_menu_item’) + * Block templates (Post Type: ‘wp_template’) + * Template parts (Post Type: ‘wp_template_part’) + * @see https://developer.wordpress.org/themes/basics/post-types/#default-post-types + */ + +type PostType = + | 'post' + | 'page' + | 'attachment' + | 'revision' + | 'nav_menu_item' + | 'wp_template' + | 'wp_template_part' + | null; + +type EditorType = 'site' | PostType; + +export const getEditorType = (): EditorType | undefined => { + /** + * Beware when using this method to figure out if we are in the site editor. + * @see https://github.com/WordPress/gutenberg/issues/46616#issuecomment-1355301090 + * @see https://github.com/Automattic/jetpack/blob/2e56d0d/projects/plugins/jetpack/extensions/shared/get-editor-type.js + */ + if ( select( 'core/edit-site' ) ) { + return 'site'; + } + + if ( select( 'core/editor' ) ) { + return select( 'core/editor' ).getCurrentPostType() as PostType; + } + + return undefined; +}; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/style-tour.scss b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/style-tour.scss new file mode 100644 index 0000000000000..51a9eaaf2407f --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/style-tour.scss @@ -0,0 +1,51 @@ +@use "sass:math"; +@import "@wordpress/base-styles/colors"; +@import "@wordpress/base-styles/mixins"; +@import "@wordpress/base-styles/variables"; +@import "@wordpress/base-styles/z-index"; + +$welcome-tour-card-media-extra-padding: 14%; // temporary value, to match the padding of the desktop instructional graphics + +.wpcom-editor-welcome-tour { + .wpcom-editor-welcome-tour__step { + &.is-with-extra-padding { + .components-card__media { + background-color: #e7eaeb; // the color of the background used in desktop graphics + + img { + left: $welcome-tour-card-media-extra-padding; + top: $welcome-tour-card-media-extra-padding; + width: 100% - $welcome-tour-card-media-extra-padding; + } + } + } + } + + .wpcom-tour-kit-step-card-overlay-controls { + position: absolute; + } +} + +// @todo clk - it this used? +.wpcom-editor-welcome-tour-card-frame { + position: relative; + + .components-guide__page-control { + bottom: 0; + left: $grid-unit-20; + margin: 0; + position: absolute; + + li { + margin-bottom: 0; + } + } +} + +// Adding it to hide the WelcomeTour when the W-icon is pressed on mobile +#wpwrap.wp-responsive-open { + + .tour-kit.wpcom-tour-kit { + display: none; + } +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/test/tour-steps.test.ts b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/test/tour-steps.test.ts new file mode 100644 index 0000000000000..fac4d7e1547a1 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/test/tour-steps.test.ts @@ -0,0 +1,110 @@ +import '../get-editor-type'; +import getTourSteps from '../tour-steps'; + +jest.mock( '../get-editor-type', () => { + return { getEditorType: () => 'post' }; +} ); + +describe( 'Welcome Tour', () => { + describe( 'Tour Steps', () => { + it( 'should retrieve the "Welcome to WordPress!" slide', () => { + expect( getTourSteps( 'en', true ) ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + meta: expect.objectContaining( { heading: 'Welcome to WordPress!' } ), + } ), + ] ) + ); + } ); + it( 'should retrieve the "Everything is a block" slide', () => { + expect( getTourSteps( 'en', true ) ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + meta: expect.objectContaining( { heading: 'Everything is a block' } ), + } ), + ] ) + ); + } ); + it( 'should retrieve the "Adding a new block" slide', () => { + expect( getTourSteps( 'en', true ) ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + meta: expect.objectContaining( { heading: 'Adding a new block' } ), + } ), + ] ) + ); + } ); + it( 'should retrieve the "Click a block to change it" slide', () => { + expect( getTourSteps( 'en', true ) ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + meta: expect.objectContaining( { heading: 'Click a block to change it' } ), + } ), + ] ) + ); + } ); + it( 'should retrieve the "More Options" slide', () => { + expect( getTourSteps( 'en', true ) ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + meta: expect.objectContaining( { heading: 'More Options' } ), + } ), + ] ) + ); + } ); + it( 'should retrieve the "Find your way" slide', () => { + expect( getTourSteps( 'en', true ) ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + meta: expect.objectContaining( { heading: 'Find your way' } ), + } ), + ] ) + ); + } ); + it( 'should retrieve the "Undo any mistake" slide', () => { + expect( getTourSteps( 'en', true ) ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + meta: expect.objectContaining( { heading: 'Undo any mistake' } ), + } ), + ] ) + ); + } ); + it( 'should retrieve the "Drag & drop" slide', () => { + expect( getTourSteps( 'en', true ) ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + meta: expect.objectContaining( { heading: 'Undo any mistake' } ), + } ), + ] ) + ); + } ); + it( 'should retrieve the "Edit your site" slide, when in site editor', () => { + expect( getTourSteps( 'en', true, true ) ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + meta: expect.objectContaining( { heading: 'Edit your site' } ), + } ), + ] ) + ); + } ); + it( 'should not retrieve the "Edit your site" slide, when not in site editor', () => { + expect( getTourSteps( 'en', true, false ) ).not.toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + meta: expect.objectContaining( { heading: 'Edit your site' } ), + } ), + ] ) + ); + } ); + it( 'should retrieve the "Congratulations!" slide, with correct url', () => { + expect( getTourSteps( 'en', true ) ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + meta: expect.objectContaining( { heading: 'Congratulations!' } ), + } ), + ] ) + ); + } ); + } ); +} ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/tour-launch.tsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/tour-launch.tsx new file mode 100644 index 0000000000000..25fa1a953fddd --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/tour-launch.tsx @@ -0,0 +1,277 @@ +import { recordTracksEvent } from '@automattic/calypso-analytics'; +import { useLocale } from '@automattic/i18n-utils'; +import { START_WRITING_FLOW, DESIGN_FIRST_FLOW } from '@automattic/onboarding'; +import { WpcomTourKit, usePrefetchTourAssets } from '@automattic/tour-kit'; +import { isWithinBreakpoint } from '@automattic/viewport'; +import { useDispatch, useSelect, dispatch } from '@wordpress/data'; +import { useEffect, useMemo } from '@wordpress/element'; +import useSiteIntent from '../../../dotcom-fse/lib/site-intent/use-site-intent'; +import useSitePlan from '../../../dotcom-fse/lib/site-plan/use-site-plan'; +import { selectors as starterPageTemplatesSelectors } from '../../../starter-page-templates/store'; +import { selectors as wpcomBlockEditorNavSidebarSelectors } from '../../../wpcom-block-editor-nav-sidebar/src/store'; +import { + selectors as wpcomWelcomeGuideSelectors, + actions as wpcomWelcomeGuideActions, +} from '../store'; +import { getEditorType } from './get-editor-type'; +import getTourSteps from './tour-steps'; +import './style-tour.scss'; +import type { DispatchFromMap, SelectFromMap } from '@automattic/data-stores'; +import type { WpcomConfig } from '@automattic/tour-kit'; +import type { Rect, Placement } from '@popperjs/core'; + +type StarterPageTemplatesSelectors = SelectFromMap< typeof starterPageTemplatesSelectors >; +type WpcomBlockEditorNavSidebarSelectors = SelectFromMap< + typeof wpcomBlockEditorNavSidebarSelectors +>; +type WpcomWelcomeGuideSelectors = SelectFromMap< typeof wpcomWelcomeGuideSelectors >; +type WPcomWelcomeGuideActions = DispatchFromMap< typeof wpcomWelcomeGuideActions >; +type CoreEditPostPlaceholder = { + isInserterOpened: ( ...args: unknown[] ) => boolean; +}; +type CoreInterfacePlaceholder = { + getActiveComplementaryArea: ( name: string ) => string; +}; + +/** + * + */ +function LaunchWpcomWelcomeTour() { + const { show, isNewPageLayoutModalOpen, isManuallyOpened } = useSelect( + select => ( { + show: ( + select( 'automattic/wpcom-welcome-guide' ) as WpcomWelcomeGuideSelectors + ).isWelcomeGuideShown(), + // Handle the case where the new page pattern modal is initialized and open + isNewPageLayoutModalOpen: + select( 'automattic/starter-page-layouts' ) && + ( select( 'automattic/starter-page-layouts' ) as StarterPageTemplatesSelectors ).isOpen(), + isManuallyOpened: ( + select( 'automattic/wpcom-welcome-guide' ) as WpcomWelcomeGuideSelectors + ).isWelcomeGuideManuallyOpened(), + } ), + [] + ); + const { siteIntent, siteIntentFetched } = useSiteIntent(); + const localeSlug = useLocale(); + const editorType = getEditorType(); + const { siteIntent: intent } = useSiteIntent(); + // We check the URL param along with site intent because the param loads faster and prevents element flashing. + const isBlogOnboardingFlow = intent === START_WRITING_FLOW || intent === DESIGN_FIRST_FLOW; + // Preload first card image (others preloaded after open state confirmed) + usePrefetchTourAssets( [ getTourSteps( localeSlug, false, false, null, siteIntent )[ 0 ] ] ); + + useEffect( () => { + if ( isBlogOnboardingFlow ) { + return; + } + if ( ! show && ! isNewPageLayoutModalOpen ) { + return; + } + + if ( ! siteIntentFetched ) { + return; + } + + // Track opening of the Welcome Guide + recordTracksEvent( 'calypso_editor_wpcom_tour_open', { + is_gutenboarding: window.calypsoifyGutenberg?.isGutenboarding, + is_manually_opened: isManuallyOpened, + intent: siteIntent, + editor_type: editorType, + } ); + }, [ + isNewPageLayoutModalOpen, + isManuallyOpened, + show, + siteIntent, + siteIntentFetched, + editorType, + isBlogOnboardingFlow, + ] ); + + if ( ! show || isNewPageLayoutModalOpen || isBlogOnboardingFlow ) { + return null; + } + + return ; +} + +/** + * + * @param root0 + * @param root0.siteIntent + */ +function WelcomeTour( { siteIntent }: { siteIntent?: string } ) { + const sitePlan = useSitePlan( window._currentSiteId ); + const localeSlug = useLocale(); + const { setShowWelcomeGuide } = useDispatch( 'automattic/wpcom-welcome-guide' ); + const isGutenboarding = window.calypsoifyGutenberg?.isGutenboarding; + const isWelcomeTourNext = () => { + return new URLSearchParams( document.location.search ).has( 'welcome-tour-next' ); + }; + const isSiteEditor = useSelect( select => !! select( 'core/edit-site' ), [] ); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore Until `@types/wordpress__editor` (which has `@types/wordpress__core-data` as a dependency) can be upgraded to a version that includes `getCurrentTheme` + // the function has existed for many years, and works as expected on wpcom and atomic + const currentTheme = useSelect( select => select( 'core' ).getCurrentTheme() ); + const themeName = currentTheme?.name?.raw?.toLowerCase() ?? null; + + const tourSteps = getTourSteps( + localeSlug, + isWelcomeTourNext(), + isSiteEditor, + themeName, + siteIntent + ); + + // Only keep Payment block step if user comes from seller simple flow + if ( ! ( 'sell' === siteIntent && sitePlan && 'ecommerce-bundle' !== sitePlan.product_slug ) ) { + const paymentBlockIndex = tourSteps.findIndex( step => step.slug === 'payment-block' ); + tourSteps.splice( paymentBlockIndex, 1 ); + } + const { isInserterOpened, isSidebarOpened, isSettingsOpened } = useSelect( + select => ( { + isInserterOpened: ( + select( 'core/edit-post' ) as CoreEditPostPlaceholder + ).isInserterOpened(), + isSidebarOpened: + ( + select( 'automattic/block-editor-nav-sidebar' ) as + | WpcomBlockEditorNavSidebarSelectors + | undefined + )?.isSidebarOpened() ?? false, // The sidebar store may not always be loaded. + isSettingsOpened: + ( select( 'core/interface' ) as CoreInterfacePlaceholder ).getActiveComplementaryArea( + 'core/edit-post' + ) === 'edit-post/document', + } ), + [] + ); + + const isTourMinimized = + isSidebarOpened || + ( isWithinBreakpoint( '<782px' ) && ( isInserterOpened || isSettingsOpened ) ); + + const editorType = getEditorType(); + + const tourConfig: WpcomConfig = { + steps: tourSteps, + closeHandler: ( _steps, currentStepIndex, source ) => { + recordTracksEvent( 'calypso_editor_wpcom_tour_dismiss', { + is_gutenboarding: isGutenboarding, + slide_number: currentStepIndex + 1, + action: source, + intent: siteIntent, + editor_type: editorType, + } ); + setShowWelcomeGuide( false, { openedManually: false } ); + }, + isMinimized: isTourMinimized, + options: { + tourRating: { + enabled: true, + useTourRating: () => { + return useSelect( + select => + ( + select( 'automattic/wpcom-welcome-guide' ) as WpcomWelcomeGuideSelectors + ).getTourRating(), + [] + ); + }, + onTourRate: rating => { + ( + dispatch( 'automattic/wpcom-welcome-guide' ) as WPcomWelcomeGuideActions + ).setTourRating( rating ); + recordTracksEvent( 'calypso_editor_wpcom_tour_rate', { + thumbs_up: rating === 'thumbs-up', + is_gutenboarding: false, + intent: siteIntent, + editor_type: editorType, + } ); + }, + }, + callbacks: { + onMinimize: currentStepIndex => { + recordTracksEvent( 'calypso_editor_wpcom_tour_minimize', { + is_gutenboarding: isGutenboarding, + slide_number: currentStepIndex + 1, + intent: siteIntent, + editor_type: editorType, + } ); + }, + onMaximize: currentStepIndex => { + recordTracksEvent( 'calypso_editor_wpcom_tour_maximize', { + is_gutenboarding: isGutenboarding, + slide_number: currentStepIndex + 1, + intent: siteIntent, + editor_type: editorType, + } ); + }, + onStepViewOnce: currentStepIndex => { + const lastStepIndex = tourSteps.length - 1; + const { heading } = tourSteps[ currentStepIndex ].meta; + + recordTracksEvent( 'calypso_editor_wpcom_tour_slide_view', { + slide_number: currentStepIndex + 1, + is_last_slide: currentStepIndex === lastStepIndex, + slide_heading: heading, + is_gutenboarding: isGutenboarding, + intent: siteIntent, + editor_type: editorType, + } ); + }, + }, + effects: { + spotlight: isWelcomeTourNext() + ? { + styles: { + minWidth: '50px', + minHeight: '50px', + borderRadius: '2px', + }, + } + : undefined, + arrowIndicator: false, + }, + popperModifiers: [ + useMemo( + () => ( { + name: 'offset', + options: { + offset: ( { placement, reference }: { placement: Placement; reference: Rect } ) => { + if ( placement === 'bottom' ) { + const boundary = document.querySelector( '.edit-post-header' ); + + if ( ! boundary ) { + return; + } + + const boundaryRect = boundary.getBoundingClientRect(); + const boundaryBottomY = boundaryRect.height + boundaryRect.y; + const referenceBottomY = reference.height + reference.y; + + return [ 0, boundaryBottomY - referenceBottomY + 16 ]; + } + return [ 0, 0 ]; + }, + }, + } ), + [] + ), + ], + classNames: 'wpcom-editor-welcome-tour', + portalParentElement: document.getElementById( 'wpwrap' ), + }, + }; + + // Theme isn't immediately available, so we prevent rendering so the content doesn't switch after it is presented, since some content is based on theme + if ( null === themeName ) { + return null; + } + + return ; +} + +export default LaunchWpcomWelcomeTour; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/tour-steps.tsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/tour-steps.tsx new file mode 100644 index 0000000000000..5462d4ef7108f --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/tour-steps.tsx @@ -0,0 +1,440 @@ +import { recordTracksEvent } from '@automattic/calypso-analytics'; +import { localizeUrl } from '@automattic/i18n-utils'; +import { isMobile } from '@automattic/viewport'; +import { ExternalLink } from '@wordpress/components'; +import { createInterpolateElement } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { getQueryArg } from '@wordpress/url'; +import { getEditorType } from './get-editor-type'; +import type { WpcomStep } from '@automattic/tour-kit'; + +interface TourAsset { + desktop?: { src: string; type: string }; + mobile?: { src: string; type: string }; +} + +/** + * + * @param key + */ +function getTourAssets( key: string ): TourAsset | undefined { + const CDN_PREFIX = 'https://s0.wp.com/i/editor-welcome-tour'; + const tourAssets = { + addBlock: { + desktop: { src: `${ CDN_PREFIX }/slide-add-block.gif`, type: 'image/gif' }, + mobile: { src: `${ CDN_PREFIX }/slide-add-block_mobile.gif`, type: 'image/gif' }, + }, + allBlocks: { desktop: { src: `${ CDN_PREFIX }/slide-all-blocks.gif`, type: 'image/gif' } }, + finish: { desktop: { src: `${ CDN_PREFIX }/slide-finish.png`, type: 'image/gif' } }, + makeBold: { desktop: { src: `${ CDN_PREFIX }/slide-make-bold.gif`, type: 'image/gif' } }, + moreOptions: { + desktop: { src: `${ CDN_PREFIX }/slide-more-options.gif`, type: 'image/gif' }, + mobile: { src: `${ CDN_PREFIX }/slide-more-options_mobile.gif`, type: 'image/gif' }, + }, + moveBlock: { + desktop: { src: `${ CDN_PREFIX }/slide-move-block.gif`, type: 'image/gif' }, + mobile: { src: `${ CDN_PREFIX }/slide-move-block_mobile.gif`, type: 'image/gif' }, + }, + findYourWay: { + desktop: { src: `${ CDN_PREFIX }/slide-find-your-way.gif`, type: 'image/gif' }, + }, + undo: { desktop: { src: `${ CDN_PREFIX }/slide-undo.gif`, type: 'image/gif' } }, + welcome: { + desktop: { src: `${ CDN_PREFIX }/slide-welcome.png`, type: 'image/png' }, + mobile: { src: `${ CDN_PREFIX }/slide-welcome_mobile.jpg`, type: 'image/jpeg' }, + }, + editYourSite: { + desktop: { + src: `https://s.w.org/images/block-editor/edit-your-site.gif?1`, + type: 'image/gif', + }, + mobile: { + src: `https://s.w.org/images/block-editor/edit-your-site.gif?1`, + type: 'image/gif', + }, + }, + videomakerWelcome: { + desktop: { src: `${ CDN_PREFIX }/slide-videomaker-welcome.png`, type: 'image/png' }, + }, + videomakerEdit: { + desktop: { src: `${ CDN_PREFIX }/slide-videomaker-edit.png`, type: 'image/png' }, + }, + } as { [ key: string ]: TourAsset }; + + return tourAssets[ key ]; +} + +/** + * + * @param localeSlug + * @param referencePositioning + * @param isSiteEditor + * @param themeName + * @param siteIntent + */ +function getTourSteps( + localeSlug: string, + referencePositioning = false, + isSiteEditor = false, + themeName: string | null = null, + siteIntent: string | undefined = undefined +): WpcomStep[] { + const isVideoMaker = 'videomaker' === ( themeName ?? '' ); + const isPatternAssembler = !! getQueryArg( window.location.href, 'assembler' ); + const siteEditorCourseUrl = `https://wordpress.com/home/${ window.location.hostname }?courseSlug=site-editor-quick-start`; + const editorType = getEditorType(); + const onSiteEditorCourseLinkClick = () => { + recordTracksEvent( 'calypso_editor_wpcom_tour_site_editor_course_link_click', { + is_pattern_assembler: isPatternAssembler, + intent: siteIntent, + editor_type: editorType, + } ); + }; + + return [ + { + slug: 'welcome', + meta: { + heading: isPatternAssembler + ? __( 'Nice job! Your new page is set up.', 'jetpack-mu-wpcom' ) + : __( 'Welcome to WordPress!', 'jetpack-mu-wpcom' ), + descriptions: { + desktop: ( () => { + if ( isPatternAssembler ) { + return createInterpolateElement( + __( + 'This is the Site Editor, where you can change everything about your site, including adding content to your homepage. Watch these short videos and take this tour to get started.', + 'jetpack-mu-wpcom' + ), + { + link_to_site_editor_course: ( + + ), + } + ); + } + + return isSiteEditor + ? __( + 'Take this short, interactive tour to learn the fundamentals of the WordPress Site Editor.', + 'jetpack-mu-wpcom' + ) + : __( + 'Take this short, interactive tour to learn the fundamentals of the WordPress editor.', + 'jetpack-mu-wpcom' + ); + } )(), + mobile: null, + }, + imgSrc: getTourAssets( isVideoMaker ? 'videomakerWelcome' : 'welcome' ), + imgLink: isPatternAssembler + ? { + href: siteEditorCourseUrl, + playable: true, + onClick: onSiteEditorCourseLinkClick, + } + : undefined, + }, + options: { + classNames: { + desktop: 'wpcom-editor-welcome-tour__step', + mobile: [ 'is-with-extra-padding', 'calypso_editor_wpcom_draft_post_modal_show' ], + }, + }, + }, + { + slug: 'everything-is-a-block', + meta: { + heading: __( 'Everything is a block', 'jetpack-mu-wpcom' ), + descriptions: { + desktop: __( + 'In the WordPress Editor, paragraphs, images, and videos are all blocks.', + 'jetpack-mu-wpcom' + ), + mobile: null, + }, + imgSrc: getTourAssets( 'allBlocks' ), + }, + options: { + classNames: { + desktop: 'wpcom-editor-welcome-tour__step', + mobile: 'wpcom-editor-welcome-tour__step', + }, + }, + }, + { + slug: 'add-block', + ...( referencePositioning && { + referenceElements: { + mobile: + '.edit-post-header .edit-post-header__toolbar .components-button.edit-post-header-toolbar__inserter-toggle', + desktop: + '.edit-post-header .edit-post-header__toolbar .components-button.edit-post-header-toolbar__inserter-toggle', + }, + } ), + meta: { + heading: __( 'Adding a new block', 'jetpack-mu-wpcom' ), + descriptions: { + desktop: __( + 'Click + to open the inserter. Then click the block you want to add.', + 'jetpack-mu-wpcom' + ), + mobile: __( + 'Tap + to open the inserter. Then tap the block you want to add.', + 'jetpack-mu-wpcom' + ), + }, + imgSrc: getTourAssets( 'addBlock' ), + }, + options: { + classNames: { + desktop: 'wpcom-editor-welcome-tour__step', + mobile: [ 'is-with-extra-padding', 'wpcom-editor-welcome-tour__step' ], + }, + }, + }, + { + slug: 'edit-block', + meta: { + heading: __( 'Click a block to change it', 'jetpack-mu-wpcom' ), + descriptions: { + desktop: isVideoMaker + ? __( + 'Use the toolbar to change the appearance of a selected block. Try replacing a video!', + 'jetpack-mu-wpcom' + ) + : __( + 'Use the toolbar to change the appearance of a selected block. Try making it bold.', + 'jetpack-mu-wpcom' + ), + mobile: null, + }, + imgSrc: getTourAssets( isVideoMaker ? 'videomakerEdit' : 'makeBold' ), + }, + options: { + classNames: { + desktop: 'wpcom-editor-welcome-tour__step', + mobile: 'wpcom-editor-welcome-tour__step', + }, + }, + }, + { + slug: 'settings', + ...( referencePositioning && { + referenceElements: { + mobile: + '.edit-post-header .edit-post-header__settings .interface-pinned-items > button:nth-child(1)', + desktop: + '.edit-post-header .edit-post-header__settings .interface-pinned-items > button:nth-child(1)', + }, + } ), + meta: { + heading: __( 'More Options', 'jetpack-mu-wpcom' ), + descriptions: { + desktop: __( 'Click the settings icon to see even more options.', 'jetpack-mu-wpcom' ), + mobile: __( 'Tap the settings icon to see even more options.', 'jetpack-mu-wpcom' ), + }, + imgSrc: getTourAssets( 'moreOptions' ), + }, + options: { + classNames: { + desktop: 'wpcom-editor-welcome-tour__step', + mobile: [ 'is-with-extra-padding', 'wpcom-editor-welcome-tour__step' ], + }, + }, + }, + ...( ! isMobile() + ? [ + { + slug: 'find-your-way', + meta: { + heading: __( 'Find your way', 'jetpack-mu-wpcom' ), + descriptions: { + desktop: __( + "Use List View to see all the blocks you've added. Click and drag any block to move it around.", + 'jetpack-mu-wpcom' + ), + mobile: null, + }, + imgSrc: getTourAssets( 'findYourWay' ), + }, + options: { + classNames: { + desktop: [ 'is-with-extra-padding', 'wpcom-editor-welcome-tour__step' ], + mobile: 'wpcom-editor-welcome-tour__step', + }, + }, + }, + ] + : [] ), + ...( ! isMobile() + ? [ + { + slug: 'undo', + ...( referencePositioning && { + referenceElements: { + desktop: + '.edit-post-header .edit-post-header__toolbar .components-button.editor-history__undo', + }, + } ), + meta: { + heading: __( 'Undo any mistake', 'jetpack-mu-wpcom' ), + descriptions: { + desktop: __( + "Click the Undo button if you've made a mistake.", + 'jetpack-mu-wpcom' + ), + mobile: null, + }, + imgSrc: getTourAssets( 'undo' ), + }, + options: { + classNames: { + desktop: 'wpcom-editor-welcome-tour__step', + mobile: 'wpcom-editor-welcome-tour__step', + }, + }, + }, + ] + : [] ), + { + slug: 'drag-drop', + meta: { + heading: __( 'Drag & drop', 'jetpack-mu-wpcom' ), + descriptions: { + desktop: __( 'To move blocks around, click and drag the handle.', 'jetpack-mu-wpcom' ), + mobile: __( 'To move blocks around, tap the up and down arrows.', 'jetpack-mu-wpcom' ), + }, + imgSrc: getTourAssets( 'moveBlock' ), + }, + options: { + classNames: { + desktop: 'wpcom-editor-welcome-tour__step', + mobile: [ 'is-with-extra-padding', 'wpcom-editor-welcome-tour__step' ], + }, + }, + }, + { + slug: 'payment-block', + meta: { + heading: __( 'The Payments block', 'jetpack-mu-wpcom' ), + descriptions: { + desktop: ( + <> + { __( + 'The Payments block allows you to accept payments for one-time, monthly recurring, or annual payments on your website', + 'jetpack-mu-wpcom' + ) } +
+ + { __( 'Learn more', 'jetpack-mu-wpcom' ) } + + + ), + mobile: null, + }, + imgSrc: getTourAssets( 'welcome' ), + }, + options: { + classNames: { + desktop: 'wpcom-editor-welcome-tour__step', + mobile: 'wpcom-editor-welcome-tour__step', + }, + }, + }, + ...( isSiteEditor + ? [ + { + slug: 'edit-your-site', + meta: { + heading: __( 'Edit your site', 'jetpack-mu-wpcom' ), + descriptions: { + desktop: createInterpolateElement( + __( + 'Design everything on your site - from the header right down to the footer - in the Site Editor. Learn more', + 'jetpack-mu-wpcom' + ), + { + link_to_fse_docs: ( + + ), + } + ), + mobile: __( + 'Design everything on your site - from the header right down to the footer - in the Site Editor.', + 'jetpack-mu-wpcom' + ), + }, + imgSrc: getTourAssets( 'editYourSite' ), + }, + options: { + classNames: { + desktop: 'wpcom-editor-welcome-tour__step', + mobile: [ 'is-with-extra-padding', 'wpcom-editor-welcome-tour__step' ], + }, + }, + }, + ] + : [] ), + { + slug: 'congratulations', + meta: { + heading: __( 'Congratulations!', 'jetpack-mu-wpcom' ), + descriptions: { + desktop: createInterpolateElement( + __( + "You've learned the basics. Remember, your site is private until you decide to launch. View the block editing docs to learn more.", + 'jetpack-mu-wpcom' + ), + { + link_to_launch_site_docs: ( + + ), + link_to_editor_docs: ( + + ), + } + ), + mobile: null, + }, + imgSrc: getTourAssets( 'finish' ), + }, + options: { + classNames: { + desktop: 'wpcom-editor-welcome-tour__step', + mobile: 'wpcom-editor-welcome-tour__step', + }, + }, + }, + ]; +} + +export default getTourSteps; From caf2919c0c606c86cd4520dcf5f2620be003f758 Mon Sep 17 00:00:00 2001 From: Louis Laugesen Date: Mon, 22 Jul 2024 15:59:55 +1000 Subject: [PATCH 02/16] Add feature entrypoints --- .../packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php | 1 + projects/packages/jetpack-mu-wpcom/webpack.config.js | 1 + 2 files changed, 2 insertions(+) diff --git a/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php b/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php index aa7d9f2f259da..a2749e1cca762 100644 --- a/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php +++ b/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php @@ -160,6 +160,7 @@ public static function load_etk_features() { require_once __DIR__ . '/features/paragraph-block-placeholder/paragraph-block-placeholder.php'; require_once __DIR__ . '/features/tags-education/tags-education.php'; require_once __DIR__ . '/features/wpcom-block-description-links/wpcom-block-description-links.php'; + require_once __DIR__ . '/features/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php'; require_once __DIR__ . '/features/wpcom-blocks/a8c-posts-list/a8c-posts-list.php'; require_once __DIR__ . '/features/wpcom-blocks/event-countdown/event-countdown.php'; require_once __DIR__ . '/features/wpcom-blocks/timeline/timeline.php'; diff --git a/projects/packages/jetpack-mu-wpcom/webpack.config.js b/projects/packages/jetpack-mu-wpcom/webpack.config.js index ea855ad9da300..5e6511c7ab9a4 100644 --- a/projects/packages/jetpack-mu-wpcom/webpack.config.js +++ b/projects/packages/jetpack-mu-wpcom/webpack.config.js @@ -37,6 +37,7 @@ module.exports = [ 'wpcom-blocks-timeline-editor': './src/features/wpcom-blocks/timeline/editor.js', 'wpcom-blocks-timeline-view': './src/features/wpcom-blocks/timeline/view.js', 'wpcom-block-description-links': './src/features/wpcom-block-description-links/index.tsx', + 'wpcom-block-editor-nux': './src/features/wpcom-block-editor-nux/index.jsx', 'wpcom-global-styles-editor': './src/features/wpcom-global-styles/index.js', 'wpcom-global-styles-frontend': './src/features/wpcom-global-styles/wpcom-global-styles-view.js', From 9d3c4261714c78fe38e7cedd6449f48726a35f32 Mon Sep 17 00:00:00 2001 From: Louis Laugesen Date: Mon, 22 Jul 2024 16:16:52 +1000 Subject: [PATCH 03/16] Move loader to load_etk_features --- .../wpcom-block-editor-nux/class-wpcom-block-editor-nux.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php index 1da8a1839f1ec..5a12df53528c4 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php @@ -7,6 +7,8 @@ namespace A8C\FSE; +define( 'MU_WPCOM_BLOCK_EDITOR_NUX', true ); + /** * Class WPCOM_Block_Editor_NUX */ From 873b5ba24bbd813c79df02a2148d43696105e789 Mon Sep 17 00:00:00 2001 From: Louis Laugesen Date: Mon, 22 Jul 2024 16:18:50 +1000 Subject: [PATCH 04/16] Js != jsx --- projects/packages/jetpack-mu-wpcom/webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/packages/jetpack-mu-wpcom/webpack.config.js b/projects/packages/jetpack-mu-wpcom/webpack.config.js index 5e6511c7ab9a4..cfdb59d970f19 100644 --- a/projects/packages/jetpack-mu-wpcom/webpack.config.js +++ b/projects/packages/jetpack-mu-wpcom/webpack.config.js @@ -37,7 +37,7 @@ module.exports = [ 'wpcom-blocks-timeline-editor': './src/features/wpcom-blocks/timeline/editor.js', 'wpcom-blocks-timeline-view': './src/features/wpcom-blocks/timeline/view.js', 'wpcom-block-description-links': './src/features/wpcom-block-description-links/index.tsx', - 'wpcom-block-editor-nux': './src/features/wpcom-block-editor-nux/index.jsx', + 'wpcom-block-editor-nux': './src/features/wpcom-block-editor-nux/index.js', 'wpcom-global-styles-editor': './src/features/wpcom-global-styles/index.js', 'wpcom-global-styles-frontend': './src/features/wpcom-global-styles/wpcom-global-styles-view.js', From 970ea6cc94c4cbdcb41ab25858b08698948e66c2 Mon Sep 17 00:00:00 2001 From: Louis Laugesen Date: Mon, 22 Jul 2024 16:37:00 +1000 Subject: [PATCH 05/16] Package updates --- pnpm-lock.yaml | 736 ++++++++++++++++++ .../packages/jetpack-mu-wpcom/package.json | 5 +- 2 files changed, 740 insertions(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da873bc9d4b4b..8c0a6642968e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2112,9 +2112,15 @@ importers: projects/packages/jetpack-mu-wpcom: dependencies: + '@automattic/calypso-analytics': + specifier: 1.1.2 + version: 1.1.2 '@automattic/color-studio': specifier: 2.6.0 version: 2.6.0 + '@automattic/components': + specifier: 2.1.1 + version: 2.1.1(@types/react@18.3.1)(@wordpress/data@10.2.0(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@automattic/i18n-utils': specifier: 1.2.3 version: 1.2.3 @@ -2130,6 +2136,9 @@ importers: '@automattic/typography': specifier: 1.0.0 version: 1.0.0 + '@automattic/viewport': + specifier: 1.0.0 + version: 1.0.0 '@preact/signals': specifier: ^1.2.2 version: 1.3.0(preact@10.22.1) @@ -4865,6 +4874,9 @@ packages: '@automattic/babel-plugin-preserve-i18n@1.0.0': resolution: {integrity: sha512-dRmLP0Ytf2oDNbUbO8MXLKYnPZfqhtFQ8v1hgDo2Fde1Y0bUz2Ll1UmUOHdyZudnrN/8Zt95cG/fIOJ0dxHi8Q==} + '@automattic/calypso-analytics@1.1.2': + resolution: {integrity: sha512-+pPM41BL8XYxHKoEidk3/J3XjKxinVZfB0/t8E2KPBqGclDSVg7PXxslOzWsQsNju64rSWcjIY5+c8fnnWRW5g==} + '@automattic/calypso-color-schemes@3.1.3': resolution: {integrity: sha512-nzs36yfxUOcsD3HvB72IHgdUfIzTRnT7QmF78CBXEREawTEs0uDyELdx/rAOtW/PauxRYRGQ4zeK5c67FWqLxw==} @@ -4877,6 +4889,13 @@ packages: '@automattic/color-studio@2.6.0': resolution: {integrity: sha512-2LzB6bbQw1vayZxZy5Y+DnCYU7x8tPu+rZhNkWD7V8QZTSJMJO65XKZhYaCByC+C5OegXyGyZzcqEOHHdj5iiQ==} + '@automattic/components@2.1.1': + resolution: {integrity: sha512-PqGwe1CI0PtQIaTka5cbX/gKqEkhk4GYTdkExigHdzifnYtPZSmuLsw5mlPdfA8qY141s8wokARU7cDIZE0DCw==} + peerDependencies: + '@wordpress/data': ^9.26.0 + react: ^18.2.0 + react-dom: ^18.2.0 + '@automattic/explat-client-react-helpers@0.1.1': resolution: {integrity: sha512-ilebWXmuleHg3BYThJvKW/iraS5kV9iQvm+vtJn6Mkl01rkMDCmsl4MGYOYiKLi/BpUq0QVlD8qKapOsz5g3Vg==} @@ -4886,12 +4905,30 @@ packages: '@automattic/format-currency@1.0.1': resolution: {integrity: sha512-RY2eiUlDiqNSHiJzz2YmH/mw4IjAUO5hkxbwcVGHJkBZowdq/WcSG2yhXc8N9cV9N1fTO/ryCuJvGnpHUe+mAg==} + '@automattic/format-currency@2.0.0': + resolution: {integrity: sha512-9A+oKRUm+n4f+cT4FHsDkCpo4mVRa/zBAvsXXq5vZpwfOWskAyDjdxA03Jl8A+z7pHYRimysG4WLM3jMRJutLw==} + '@automattic/i18n-utils@1.2.3': resolution: {integrity: sha512-zvZlazUoEasLATrta3ljfxu2uaZWgHRNKWf56KKBlrPiIxNQvx9D7YyN2MhiV27e/PuAhB0gI4ghqp3gzurKmA==} + '@automattic/interpolate-components@1.2.1': + resolution: {integrity: sha512-YNQtJsrs9KQ3lkBdtLyDheVRijoBA3y/PuHdgJ0eB4AX9JyjkDX7jd79Inh79+01CGNLbMQGrEJby2zvbJr17A==} + peerDependencies: + '@types/react': '>=16.14.23' + react: '>=16.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + '@automattic/languages@1.0.0': resolution: {integrity: sha512-froTyDbTmLitHkvY9WLCpFdjUo6moOLkDKw63J2fLiB2gBApy2thkBV+LRx4Z0kIF5iXVkQF4yYOPYkT9Sr13Q==} + '@automattic/load-script@1.0.0': + resolution: {integrity: sha512-Hc1mRmTK12OKrONnGhe7Ht1Gpo4B/ls8WQ1IZ1/qBws1bUZ6u7Crnpv3HZkN4UI7irG3OU4l4Pn1TXtoJLcKRw==} + + '@automattic/material-design-icons@1.0.0': + resolution: {integrity: sha512-8baJ1l8ftLq/UdLeucOeGXo4/wpaB/pSOBO587/pKC/xv2Oo8Ok21g1WKwp0Y8hEq4+3JNtCzOGVxmIgDBTYvA==} + '@automattic/page-pattern-modal@1.1.5': resolution: {integrity: sha512-cFA82qWUDSSFhOHfOkOqh7X8I9As5fNGp7w3LVw7ZDRl6wSiQZveLvWp4msNDnLmeiJTpxWVOZWvCirxYUE3Sw==} peerDependencies: @@ -4918,6 +4955,11 @@ packages: '@automattic/typography@1.0.0': resolution: {integrity: sha512-TnT+vPaNUXQYwDsPCPxhNY0d4LnOKvrb0SizUCC5iybo5sfOlX/rYalGDyz6nPQDF0EBaQwMf7qhVsflFR0cBg==} + '@automattic/viewport-react@1.0.0': + resolution: {integrity: sha512-+6+l4jj14GXeoc5Jpic5E5eVvNL88Ezz8cMLmKAw0fpPDsz4gJv7o0hgShu0hjGjKTtBeUkBGfFWMCdRjZaVcA==} + peerDependencies: + react: ^16.0.0 + '@automattic/viewport@1.0.0': resolution: {integrity: sha512-aSJRuZ80kds6kO+baIw8wK4nmlHG/e/sUPa7BDwalo8vBLtBnW07GsXm1zGgz9htgEogFuuxBdKR4IjO1z8phA==} @@ -7188,6 +7230,9 @@ packages: '@tannin/postfix@1.1.0': resolution: {integrity: sha512-oocsqY7g0cR+Gur5jRQLSrX2OtpMLMse1I10JQBm8CdGMrDkh1Mg2gjsiquMHRtBs4Qwu5wgEp5GgIYHk4SNPw==} + '@tannin/sprintf@1.2.0': + resolution: {integrity: sha512-T0ORaQrH6kNFGzTg285RVPK+NCYZxOoA+r0QfKgHqK+yk5RuYPSKDa18XCLtycCNq+VWKpfyDpzGUGhYgCV+kw==} + '@tanstack/query-core@4.35.3': resolution: {integrity: sha512-PS+WEjd9wzKTyNjjQymvcOe1yg8f3wYc6mD+vb6CKyZAKvu4sIJwryfqfBULITKCla7P9C4l5e9RXePHvZOZeQ==} @@ -7472,6 +7517,9 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react-dom@16.9.24': + resolution: {integrity: sha512-Gcmq2JTDheyWn/1eteqyzzWKSqDjYU6KYsIvH7thb7CR5OYInAWOX+7WnKf6PaU/cbdOc4szJItcDEJO7UGmfA==} + '@types/react-dom@18.3.0': resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} @@ -7490,6 +7538,9 @@ packages: '@types/react-test-renderer@18.3.0': resolution: {integrity: sha512-HW4MuEYxfDbOHQsVlY/XtOvNHftCVEPhJF2pQXXwcUiUF+Oyb0usgp48HSgpK5rt8m9KZb22yqOeZm+rrVG8gw==} + '@types/react@16.14.60': + resolution: {integrity: sha512-wIFmnczGsTcgwCBeIYOuy2mdXEiKZ5znU/jNOnMZPQyCcIxauMGWlX0TNG4lZ7NxRKj7YUIZRneJQSSdB2jKgg==} + '@types/react@18.3.1': resolution: {integrity: sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==} @@ -7502,6 +7553,9 @@ packages: '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + '@types/scheduler@0.16.8': + resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} + '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} @@ -7730,6 +7784,10 @@ packages: webpack-dev-server: optional: true + '@wordpress/a11y@3.58.0': + resolution: {integrity: sha512-7NnJKl4+pxP6kV/jvXaJcZZCGzW7zaj6YeMnyjUd96cH4ta1ykBIveWgejerFOGsbK+88FnStcxSFj+dbDXs/w==} + engines: {node: '>=12'} + '@wordpress/a11y@4.2.0': resolution: {integrity: sha512-IwW1mf4zIo/BIa19MB4yGLWQl3MlzB0tu+0Coct+m0gc7uaFuSov2ub56vpDvXvNsfLISWtvWBFL4TQTInNdiA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -7758,6 +7816,9 @@ packages: resolution: {integrity: sha512-bOvP+8b0zk7wjIbpaAnJSc0kkKZA6oeyJ4/GRtp+mmcrIXJWL+LNCGYQM+aUDSGO7j1QazLpTR7p5br8ygl5/g==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/base-styles@4.49.0': + resolution: {integrity: sha512-yFRYqNtd26ULZ0oAHhCu/IcaA0XHI3E7kRCKajZqUvyRQj7YprXnpD3o0/pnwvF6ZFTXzCX8pXHjUc2TIv97ig==} + '@wordpress/base-styles@5.2.0': resolution: {integrity: sha512-yBMVbn4gvNQimVfLAZ+/F7F/Tpl+femF9ojgv90c0A0o6IDEtdC+6vUvtAxvXVoruwmxsq8ncouoorZbYDb3yg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -7801,6 +7862,13 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 + '@wordpress/components@27.6.0': + resolution: {integrity: sha512-f+fXENkgrPs5GLo2yu9fEAdVX0KriEatRcjDUyw0+DbNbJR62sCdDtGdhJRW4jPUUoUowxaGO0y4+jvQWxnbyg==} + engines: {node: '>=12'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + '@wordpress/components@28.2.0': resolution: {integrity: sha512-6yq1D4SrS5i9lvukCiN+FtKXh42JUV+T0X+e7xI3qFANaRMqjSNqEJFOu14H2oKIbZwjdKxEJ/Wck1lWBIKu/A==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -7808,6 +7876,15 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 + '@wordpress/compose@3.25.3': + resolution: {integrity: sha512-tCO2EnJCkCH548OqA0uU8V1k/1skz2QwBlHs8ZQSpimqUS4OWWsAlndCEFe4U4vDTqFt2ow7tzAir+05Cw8MAg==} + + '@wordpress/compose@6.35.0': + resolution: {integrity: sha512-PfruhCxxxJokDQHc2YBgerEiHV7BIxQk9g5vU4/f9X/0PBQWUTuxOzSFcAba03vnjfAgtPTSMp50T50hcJwXfA==} + engines: {node: '>=12'} + peerDependencies: + react: ^18.0.0 + '@wordpress/compose@7.2.0': resolution: {integrity: sha512-J2OGEatXXTgRJmXZHYcstL5GyQgQcoeSJ9dQ2wVFHJLnWoIX3hlvi5oRy10lpl1yntfm6NLkWDBuSTIbAYJzww==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -7834,6 +7911,16 @@ packages: peerDependencies: react: ^18.0.0 + '@wordpress/data@9.28.0': + resolution: {integrity: sha512-EDPpZdkngdoW7EMzPpGj0BmNcr7syJO67pgTODtN/4XFIdYL2RKzFyn3nlLBKhX17UsE/ALq9WdijacH4QJ9qw==} + engines: {node: '>=12'} + peerDependencies: + react: ^18.0.0 + + '@wordpress/date@4.58.0': + resolution: {integrity: sha512-yFT7DU0H9W0lsDytMaVMmjho08X1LeBMIQMppxdtKB04Ujx58hVh7gtunOsstUQ7pVg23nE2eLaVfx5JOdjzAw==} + engines: {node: '>=12'} + '@wordpress/date@5.2.0': resolution: {integrity: sha512-k9gFs74hg9yvrWhoUrNrCf95VIqvPkv2AysRxTxSfEZjTyMBuxNNa3amr295hCeoqcjT9mw5bptmLntawmg7nA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -7844,14 +7931,32 @@ packages: peerDependencies: webpack: ^5.0.0 + '@wordpress/deprecated@2.12.3': + resolution: {integrity: sha512-qr+yDfTQfI3M4h6oY6IeHWwoHr4jxbILjSlV+Ht6Jjto9Owap6OuzSqR13Ev4xqIoG4C7b5B3gZXVfwVDae1zg==} + + '@wordpress/deprecated@3.58.0': + resolution: {integrity: sha512-knweE2lLEUxWRr6A48sHiO0ww5pPybGe2NVIZVq/y7EaYCMdpy6gYA0ZdVqMKZvtxKKqicJfwigcn+hinsTvUQ==} + engines: {node: '>=12'} + '@wordpress/deprecated@4.2.0': resolution: {integrity: sha512-grD/IBGEvXzTLaNB45QZv8jDQYK6bMhCSa7obRahZDpyrM2lwKwa8p1oack5R+81YWkBL02gl9V5G9BrpNfBJg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/dom-ready@3.58.0': + resolution: {integrity: sha512-sDgRPjNBToRKgYrpwvMRv2Yc7/17+sp8hm/rRnbubwb+d/DbGkK4Tc/r4sNLSZCqUAtcBXq9uk1lzvhge3QUSg==} + engines: {node: '>=12'} + '@wordpress/dom-ready@4.2.0': resolution: {integrity: sha512-1rD9vcwVy7s+yGbe8DuDkBpjvA/PJtY2DnUgyHIedC/YonlswalBSiFWGZuTX5QyocdffBAWiUlvebyN4TNtOw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/dom@2.18.0': + resolution: {integrity: sha512-tM2WeQuSObl3nzWjUTF0/dyLnA7sdl/MXaSe32D64OF89bjSyJvjUipI7gjKzI3kJ7ddGhwcTggGvSB06MOoCQ==} + + '@wordpress/dom@3.58.0': + resolution: {integrity: sha512-t3xSr/nqekj2qwUGRAqSeGx6116JOBxzI+VBiUfZrjGEnuyKdLelXDEeYtcwbb7etMkj/6F60/NB7GTl5IwizQ==} + engines: {node: '>=12'} + '@wordpress/dom@4.2.0': resolution: {integrity: sha512-vkeIsFdoKWl6lZJM+E49b+HocePP8gSPiDeUaa3P82JPTLTNAAfQMGWbAG0dbQCOZa7pmF4Sh0T1iVYmznm6eA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -7870,10 +7975,24 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 + '@wordpress/element@2.20.3': + resolution: {integrity: sha512-f4ZPTDf9CxiiOXiMxc4v1K7jcBMT4dsiehVOpkKzCDKboNXp4qVf8oe5PE23VGZNEjcOj5Mkg9hB57R0nqvMTw==} + + '@wordpress/element@5.35.0': + resolution: {integrity: sha512-puswpGcIdS+0A2g28uHriMkZqqRCmzFczue5Tk99VNtzBdehyk7Ae+DZ4xw5yT6GqYai8NTqv6MRwCB78uh5Mw==} + engines: {node: '>=12'} + '@wordpress/element@6.2.0': resolution: {integrity: sha512-pRCchhYoH7eN0bxL4iUMBm82psqSUozlmk4B5IhQiqzYoOWn7OjvkGqejAnt81iDZUNZ8hIY2gLpRplgwwiZlQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/escape-html@1.12.2': + resolution: {integrity: sha512-FabgSwznhdaUwe6hr1CsGpgxQbzqEoGevv73WIL1B9GvlZ6csRWodgHfWh4P6fYqpzxFL4WYB8wPJ1PdO32XFA==} + + '@wordpress/escape-html@2.58.0': + resolution: {integrity: sha512-9YJXMNfzkrhHEVP1jFEhgijbZqW8Mt3NHIMZjIQoWtBf7QE86umpYpGGBXzYC0YlpGTRGzZTBwYaqFKxjeaSgA==} + engines: {node: '>=12'} + '@wordpress/escape-html@3.2.0': resolution: {integrity: sha512-GFJ91lrs46zN3bgRGBHREaZ4jegwUA+2Gx+P6f11VDLhihNGKyg67uNf0lXqLoLj6iQQCBDP+15k/0k2ccr3YA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -7909,14 +8028,34 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 + '@wordpress/hooks@2.12.3': + resolution: {integrity: sha512-LmKiwKldZt6UYqOxV/a6+eUFXdvALFnB/pQx3RmrMvO64sgFhfR6dhrlv+uVbuuezSuv8dce1jx8lUWAT0krMA==} + + '@wordpress/hooks@3.58.0': + resolution: {integrity: sha512-9LB0ZHnZRQlORttux9t/xbAskF+dk2ujqzPGsVzc92mSKpQP3K2a5Wy74fUnInguB1vLUNHT6nrNdkVom5qX1Q==} + engines: {node: '>=12'} + '@wordpress/hooks@4.2.0': resolution: {integrity: sha512-N2dRMIb3F6y2dXlcT6m2CH/jDi9/Fe0gaM6ev7DrvwJ8+kX1CRzwAydemmPw34EnhQKvYKQYgGqttrfzvzgKJw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/html-entities@3.58.0': + resolution: {integrity: sha512-FU7b6QZdwTCuLKq6wCl0IZqqOMcMRxMcekVVytzTse7hYk9dvL1qQL/U4eQ/CNyKqiT9u7fb5NKTQILOzoolVQ==} + engines: {node: '>=12'} + '@wordpress/html-entities@4.2.0': resolution: {integrity: sha512-NwYv/VSxASrhIqjcgVkKS9s9kAseTJ6NYVpqCm8owz81GqYcpOe2XVCKQxOKHcU8x9Gm99uHR10jztXXJr1Z2Q==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/i18n@3.20.0': + resolution: {integrity: sha512-SIoOJFB4UrrYAScS4H91CYCLW9dX3Ghv8pBKc/yHGculb1AdGr6gRMlmJxZV62Cn3CZ4Ga86c+FfR+GiBu0JPg==} + hasBin: true + + '@wordpress/i18n@4.58.0': + resolution: {integrity: sha512-VfvS3BWv/RDjRKD6PscIcvYfWKnGJcI/DEqyDgUMhxCM6NRwoL478CsUKTiGJIymeyRodNRfprdcF086DpGKYw==} + engines: {node: '>=12'} + hasBin: true + '@wordpress/i18n@5.2.0': resolution: {integrity: sha512-G8z3/o1gm158XHANzthMBLsInQ/iWBFFIUoThiOP8C+VtpVozVpmWpgdayldPoTYolhCdaW6dicNUfdX8fOBTQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -7928,6 +8067,12 @@ packages: peerDependencies: react: ^18 + '@wordpress/icons@9.49.0': + resolution: {integrity: sha512-Z8F+ledkfkcKDuS1c/RkM0dEWdfv2AXs6bCgey89p0atJSscf7qYbMJR9zE5rZ5aqXyFfV0DAFKJEgayNqneNQ==} + engines: {node: '>=12'} + peerDependencies: + react: ^18 + '@wordpress/interactivity-router@2.2.0': resolution: {integrity: sha512-HWWE+U/7hWiH2dopIcx93MraohheKMFIlWVN5CiLqcUsUp5aKiOJXyhGhPvau4qnv3Y+Tm05sTt9ZFlIPmIoeg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -7943,6 +8088,13 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 + '@wordpress/is-shallow-equal@3.1.3': + resolution: {integrity: sha512-eDLhfC4aaSgklzqwc6F/F4zmJVpTVTAvhqX+q0SP/8LPcP2HuKErPHVrEc75PMWqIutja2wJg98YSNPdewrj1w==} + + '@wordpress/is-shallow-equal@4.58.0': + resolution: {integrity: sha512-NH2lbXo/6ix1t4Zu9UBXpXNtoLwSaYmIRSyDH34XNb0ic8a7yjEOhYWVW3LTfSCv9dJVyxlM5TJPtL85q7LdeQ==} + engines: {node: '>=12'} + '@wordpress/is-shallow-equal@5.2.0': resolution: {integrity: sha512-WIsaAu+vDoAwnfGSWqyOMZiJeKXMXHNj6SzuESieASbL1VcbWgpg8FjDRjCNCEBgu7oe2VQrDOpDfajI1fgVvw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -7959,6 +8111,13 @@ packages: peerDependencies: react: ^18.0.0 + '@wordpress/keycodes@2.19.3': + resolution: {integrity: sha512-8rNdmP5M1ifTgLIL0dt/N1uTGsq/Rx1ydCXy+gg24WdxBRhyu5sudNVCtascVXo26aIfOH9OJRdqRZZTEORhog==} + + '@wordpress/keycodes@3.58.0': + resolution: {integrity: sha512-Q/LRKpx8ndzuHlkxSQ2BD+NTYYKQPIneNNMng8hTAfyU7RFwXpqj06HpeOFGh4XIdPKCs/8hmucoLJRmmLmZJA==} + engines: {node: '>=12'} + '@wordpress/keycodes@4.2.0': resolution: {integrity: sha512-FJMR+KLfltcfmd0GhpI2C+zohFaGwPwZTYx9e0+cnIDJ5c5Jw1KTToZ+gkWPoSi++U/dlqkxkKYLD4AnGg7L5Q==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -8006,20 +8165,47 @@ packages: peerDependencies: prettier: '>=3' + '@wordpress/primitives@3.56.0': + resolution: {integrity: sha512-NXBq1ODjl6inMWx/l7KCbATcjdoeIOqYeL9i9alqdAfWeKx1EH9PIvKWylIkqZk7erXxCxldiRkuyjTtwjNBxw==} + engines: {node: '>=12'} + peerDependencies: + react: ^18 + '@wordpress/primitives@4.2.0': resolution: {integrity: sha512-UofDIMe3pQ4UvubCAjm4/Y+o/niAiHFRjhavvxBOZ5iCFyjG/1knbJcWa8+0qvjIA5YTBzZxfN6PD4Je1SwtFw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} peerDependencies: react: ^18 + '@wordpress/priority-queue@1.11.2': + resolution: {integrity: sha512-ulwmUOklY3orn1xXpcPnTyGWV5B/oycxI+cHZ6EevBVgM5sq+BW3xo0PKLR/MMm6UNBtFTu/71QAJrNZcD6V1g==} + + '@wordpress/priority-queue@2.58.0': + resolution: {integrity: sha512-W+qCS8HJWsXG8gE6yK/H/IObowcghPrQMM3cQHtfd/U05yFNU1Bd/fbj3AO1fVRztktS47lIpi9m3ll1evPEHA==} + engines: {node: '>=12'} + '@wordpress/priority-queue@3.2.0': resolution: {integrity: sha512-Kz/Zv+/TzgsKi5M3/iE2w4sMSi0f2Q3KnmU6taS5bEiiKRHvuC1U629YBsXCvBfi+7QWe2L7J7OVcLRwdEzAkg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/private-apis@0.40.0': + resolution: {integrity: sha512-ZX/9Y8eA3C3K6LOj32bHFj+9tNV819CBd8+chqMmmlvQRcTngiuXbMbnSdZnnAr1gLQgNpH9PJ60dIwJnGSEtQ==} + engines: {node: '>=12'} + '@wordpress/private-apis@1.2.0': resolution: {integrity: sha512-N0eRg0IHbgg1G8Z5/UCmU/FNan+YEAkVauM0W8OPp2/jeqQHS70VlrGwzmF+I8o2yInI6gL38dPE8sYElNiBFA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/react-i18n@3.56.0': + resolution: {integrity: sha512-Qe7EDCazhhrBLsvqJOYZdIygamJFJQbZGvhBD/9O7H/PgLbrxIluRqTiY1Vo+hN6W6vTIuaOcXlCxZKEmSGC0A==} + engines: {node: '>=12'} + + '@wordpress/redux-routine@4.58.0': + resolution: {integrity: sha512-r0mMWFeJr93yPy2uY/M5+gdUUYj0Zu8+21OFFb5hyQ0z7UHIa3IdgQxzCaTbV1LDA1ZYJrjHeCnA6s4gNHjA2Q==} + engines: {node: '>=12'} + peerDependencies: + redux: '>=4' + '@wordpress/redux-routine@5.2.0': resolution: {integrity: sha512-tzUQeO8sjkOn1l5MlrzXwjFhkEYNpw21nQ/sQP/smQp70rG22hPZSYOK8/C5ls1KGbzCtfwv92NxwJEKbBypiw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -8033,6 +8219,12 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 + '@wordpress/rich-text@6.35.0': + resolution: {integrity: sha512-h6/XftSqo9UQZebuNZyLfOVu+ButBLITW/BILsKeJhSpmM19VNdz8UhVGLp+xQPE+/GPCIMJrhhqipISDfc2Ig==} + engines: {node: '>=12'} + peerDependencies: + react: ^18.0.0 + '@wordpress/rich-text@7.2.0': resolution: {integrity: sha512-cjp/Y4bPavge1dV2JneM8km3Hq166BJLPPRpOpw9wBI4xlyjgyXHnMX6tqoU/nMIBXq4asWQrXQfkzbA0iJxcA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -8068,6 +8260,10 @@ packages: resolution: {integrity: sha512-4CDvT2x+NmP255E4JSowpHiZUK2wqVOvfrcuY884mm149+ZmPp2OyGuRnn4aqFEs3+Bcxjg5NEz0o4KH0CXt5Q==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/undo-manager@0.18.0': + resolution: {integrity: sha512-upbzPEToa095XG+2JXLHaolF1LfXEMFS0lNMYV37myoUS+eZ7/tl9Gx+yU2+OqWy57TMwx33NlWUX/n+ynzPRw==} + engines: {node: '>=12'} + '@wordpress/undo-manager@1.2.0': resolution: {integrity: sha512-xOjyl2hRro5I3pBuDYvrF+cLe0617KlPiuJok9YPofjUvfDvgf6gPLSBOAPj2GzYkiGASz0fnsPcE4UxS14ApQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -8082,6 +8278,10 @@ packages: peerDependencies: react: ^18.0.0 + '@wordpress/warning@2.58.0': + resolution: {integrity: sha512-9bZlORhyMY2nbWozeyC5kqJsFzEPP4DCLhGmjtbv+YWGHttUrxUZEfrKdqO+rUODA8rP5zeIly1nCQOUnkw4Lg==} + engines: {node: '>=12'} + '@wordpress/warning@3.2.0': resolution: {integrity: sha512-i+EYX506tE45+lhOW4qAwXm/5/PmkAdvKhNt+dTxJDif+ATUqp4QaFFRl9yFIt/v+ksAapQ0bnPkKNSDsECGtA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -8660,6 +8860,9 @@ packages: caniuse-lite@1.0.30001640: resolution: {integrity: sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==} + canvas-confetti@1.9.3: + resolution: {integrity: sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==} + capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -8759,6 +8962,9 @@ packages: cjs-module-lexer@1.3.1: resolution: {integrity: sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==} + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + clean-css@5.3.3: resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} engines: {node: '>= 10.0'} @@ -9910,6 +10116,9 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + exenv@1.2.2: + resolution: {integrity: sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==} + exit@0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} @@ -10363,6 +10572,11 @@ packages: peerDependencies: react: 15 - 18 + gridicons@3.4.2: + resolution: {integrity: sha512-KC2BzPDh3F0vJzYa7KYBWJOO9gTHoKoFiHNazZEU9Gq2jIJ2zObOA67wlZjZkPHPCjZiLQrko3AYFLrMrHXKrA==} + peerDependencies: + react: 15 - 18 + gunzip-maybe@1.4.2: resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==} hasBin: true @@ -10402,6 +10616,9 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + hasha@5.2.2: resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==} engines: {node: '>=8'} @@ -10525,6 +10742,11 @@ packages: engines: {node: '>=14'} hasBin: true + i18n-calypso@7.0.0: + resolution: {integrity: sha512-GQesQzd/VYXiJOrjMixJNFOqNOcp43kKGKZTimYu70RabvcObpjfAOqtrQganszXqXWxZ7fAXOnhCTd8NVtf/Q==} + peerDependencies: + react: ^18.2.0 + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -11430,6 +11652,10 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} + lru@3.1.0: + resolution: {integrity: sha512-5OUtoiVIGU4VXBOshidmtOsvBIvcQR6FD/RzWSvaeHyxCGB+PCUCu+52lqMfdc0h/2CLvHhZS4TwUmMQrrMbBQ==} + engines: {node: '>= 0.4.0'} + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -11535,6 +11761,9 @@ packages: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} + memize@1.1.0: + resolution: {integrity: sha512-K4FcPETOMTwe7KL2LK0orMhpOmWD2wRGwWWpbZy0fyArwsyIKR8YJVz8+efBAh3BO4zPqlSICu4vsLTRRqtFAg==} + memize@2.1.0: resolution: {integrity: sha512-yywVJy8ctVlN5lNPxsep5urnZ6TTclwPEyigM9M3Bi8vseJBOfqNrGWN/r8NzuIt3PovM323W04blJfGQfQSVg==} @@ -11681,6 +11910,9 @@ packages: peerDependencies: webpack: ^5.0.0 + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -12732,6 +12964,11 @@ packages: resolution: {integrity: sha512-i8aF1nyKInZnANZ4uZrH49qn1paRgBZ7wZiCNBMnenlPzEv0mRl+ShpTVEI6wZNl8sSc79xZkivtgLKQArcanQ==} engines: {node: '>=16.14.0'} + react-dom@16.14.0: + resolution: {integrity: sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==} + peerDependencies: + react: ^16.14.0 + react-dom@18.2.0: resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: @@ -12766,6 +13003,16 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + + react-modal@3.16.1: + resolution: {integrity: sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==} + engines: {node: '>=8'} + peerDependencies: + react: ^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 + react-dom: ^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 + react-page-visibility@7.0.0: resolution: {integrity: sha512-d4Kq/8TtJSr8dQc8EJeAZcSKTrGzC5OPTm6UrMur9BnwP0fgTawI9+Nd+ZGB7vwCfn2yZS0qDF9DR3/QYTGazw==} engines: {node: '>=10'} @@ -12814,6 +13061,11 @@ packages: '@types/react': optional: true + react-resize-aware@3.1.3: + resolution: {integrity: sha512-dPacJZfDczrGOljY8MBnQRudxjI8+M9qG5GXQzU4wIl5q2T4e8UcrSl2rsEBYn9DBuGflhDnI2uYGGFJ991DfA==} + peerDependencies: + react: ^16.8.0 || 17.x || 18.x + react-router-dom@5.3.4: resolution: {integrity: sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==} peerDependencies: @@ -12925,6 +13177,10 @@ packages: react: '>=16.13' react-dom: '>=16.13' + react@16.14.0: + resolution: {integrity: sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==} + engines: {node: '>=0.10.0'} + react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -13246,6 +13502,9 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} + scheduler@0.19.1: + resolution: {integrity: sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -14207,6 +14466,11 @@ packages: '@types/react': optional: true + use-subscription@1.6.0: + resolution: {integrity: sha512-0Y/cTLlZfw547tJhJMoRA16OUbVqRm6DmvGpiGbmLST6BIA5KU5cKlvlz8DVMrACnWpyEjCkgmhLatthP4jUbA==} + peerDependencies: + react: ^18.0.0 + use-sync-external-store@1.2.2: resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} peerDependencies: @@ -14221,6 +14485,10 @@ packages: utila@0.4.0: resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} + utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -14273,6 +14541,9 @@ packages: walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + watchpack@2.4.1: resolution: {integrity: sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==} engines: {node: '>=10.13.0'} @@ -14654,6 +14925,17 @@ snapshots: '@automattic/babel-plugin-preserve-i18n@1.0.0': {} + '@automattic/calypso-analytics@1.1.2': + dependencies: + '@automattic/load-script': 1.0.0 + cookie: 0.4.1 + debug: 4.3.4 + hash.js: 1.1.7 + tslib: 2.6.3 + uuid: 9.0.1 + transitivePeerDependencies: + - supports-color + '@automattic/calypso-color-schemes@3.1.3': {} '@automattic/calypso-config@1.2.0': {} @@ -14666,6 +14948,39 @@ snapshots: '@automattic/color-studio@2.6.0': {} + '@automattic/components@2.1.1(@types/react@18.3.1)(@wordpress/data@10.2.0(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@automattic/calypso-analytics': 1.1.2 + '@automattic/calypso-url': 1.1.0 + '@automattic/format-currency': 2.0.0 + '@automattic/i18n-utils': 1.2.3 + '@automattic/material-design-icons': 1.0.0 + '@automattic/typography': 1.0.0 + '@automattic/viewport-react': 1.0.0(react@18.3.1) + '@wordpress/base-styles': 4.49.0 + '@wordpress/components': 27.6.0(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/data': 10.2.0(react@18.3.1) + '@wordpress/icons': 9.49.0(react@18.3.1) + '@wordpress/react-i18n': 3.56.0 + canvas-confetti: 1.9.3 + classnames: 2.5.1 + gridicons: 3.4.2(react@18.3.1) + i18n-calypso: 7.0.0(@types/react@18.3.1)(react@18.3.1) + lodash: 4.17.21 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-modal: 3.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router-dom: 6.21.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-slider: 2.0.5(react@18.3.1) + utility-types: 3.11.0 + uuid: 9.0.1 + transitivePeerDependencies: + - '@babel/runtime' + - '@emotion/is-prop-valid' + - '@types/react' + - supports-color + '@automattic/explat-client-react-helpers@0.1.1': dependencies: '@automattic/explat-client': 0.1.0 @@ -14680,6 +14995,10 @@ snapshots: dependencies: tslib: 2.5.0 + '@automattic/format-currency@2.0.0': + dependencies: + tslib: 2.6.3 + '@automattic/i18n-utils@1.2.3': dependencies: '@automattic/calypso-config': 1.2.0 @@ -14692,10 +15011,25 @@ snapshots: transitivePeerDependencies: - supports-color + '@automattic/interpolate-components@1.2.1(@types/react@18.3.1)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.1 + '@automattic/languages@1.0.0': dependencies: tslib: 2.5.0 + '@automattic/load-script@1.0.0': + dependencies: + '@babel/runtime': 7.24.7 + debug: 3.2.7 + transitivePeerDependencies: + - supports-color + + '@automattic/material-design-icons@1.0.0': {} + '@automattic/page-pattern-modal@1.1.5(@types/react@18.3.1)(@wordpress/data@10.2.0(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1)': dependencies: '@automattic/color-studio': 2.6.0 @@ -14748,6 +15082,12 @@ snapshots: '@automattic/typography@1.0.0': {} + '@automattic/viewport-react@1.0.0(react@18.3.1)': + dependencies: + '@automattic/viewport': 1.0.0 + '@wordpress/compose': 3.25.3(react@18.3.1) + react: 18.3.1 + '@automattic/viewport@1.0.0': {} '@automattic/webpack-rtl-plugin@6.0.0(webpack@5.76.0(webpack-cli@4.9.1))': @@ -18271,6 +18611,8 @@ snapshots: '@tannin/postfix@1.1.0': {} + '@tannin/sprintf@1.2.0': {} + '@tanstack/query-core@4.35.3': {} '@tanstack/query-core@5.0.5': {} @@ -18574,6 +18916,10 @@ snapshots: '@types/range-parser@1.2.7': {} + '@types/react-dom@16.9.24': + dependencies: + '@types/react': 16.14.60 + '@types/react-dom@18.3.0': dependencies: '@types/react': 18.3.1 @@ -18604,6 +18950,12 @@ snapshots: dependencies: '@types/react': 18.3.1 + '@types/react@16.14.60': + dependencies: + '@types/prop-types': 15.7.12 + '@types/scheduler': 0.16.8 + csstype: 3.1.3 + '@types/react@18.3.1': dependencies: '@types/prop-types': 15.7.12 @@ -18617,6 +18969,8 @@ snapshots: '@types/retry@0.12.0': {} + '@types/scheduler@0.16.8': {} + '@types/semver@7.5.8': {} '@types/send@0.17.4': @@ -18957,6 +19311,12 @@ snapshots: dependencies: webpack-cli: 4.9.1(webpack@5.76.0) + '@wordpress/a11y@3.58.0': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/dom-ready': 3.58.0 + '@wordpress/i18n': 4.58.0 + '@wordpress/a11y@4.2.0': dependencies: '@babel/runtime': 7.24.7 @@ -19003,6 +19363,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@wordpress/base-styles@4.49.0': {} + '@wordpress/base-styles@5.2.0': {} '@wordpress/blob@4.2.0': @@ -19305,6 +19667,62 @@ snapshots: - '@types/react' - supports-color + '@wordpress/components@27.6.0(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@ariakit/react': 0.3.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@babel/runtime': 7.24.7 + '@emotion/cache': 11.11.0 + '@emotion/css': 11.11.2 + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + '@emotion/serialize': 1.1.4 + '@emotion/styled': 11.11.5(@emotion/react@11.11.4(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1) + '@emotion/utils': 1.2.1 + '@floating-ui/react-dom': 2.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/gradient-parser': 0.1.3 + '@types/highlight-words-core': 1.2.1 + '@use-gesture/react': 10.3.1(react@18.3.1) + '@wordpress/a11y': 3.58.0 + '@wordpress/compose': 6.35.0(react@18.3.1) + '@wordpress/date': 4.58.0 + '@wordpress/deprecated': 3.58.0 + '@wordpress/dom': 3.58.0 + '@wordpress/element': 5.35.0 + '@wordpress/escape-html': 2.58.0 + '@wordpress/hooks': 3.58.0 + '@wordpress/html-entities': 3.58.0 + '@wordpress/i18n': 4.58.0 + '@wordpress/icons': 9.49.0(react@18.3.1) + '@wordpress/is-shallow-equal': 4.58.0 + '@wordpress/keycodes': 3.58.0 + '@wordpress/primitives': 3.56.0(react@18.3.1) + '@wordpress/private-apis': 0.40.0 + '@wordpress/rich-text': 6.35.0(react@18.3.1) + '@wordpress/warning': 2.58.0 + change-case: 4.1.2 + clsx: 2.1.1 + colord: 2.9.3 + date-fns: 3.6.0 + deepmerge: 4.3.1 + downshift: 6.1.12(react@18.3.1) + fast-deep-equal: 3.1.3 + framer-motion: 11.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + gradient-parser: 0.1.5 + highlight-words-core: 1.2.2 + is-plain-object: 5.0.0 + memize: 2.1.0 + path-to-regexp: 6.2.2 + re-resizable: 6.9.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + remove-accents: 0.5.0 + use-lilius: 2.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + uuid: 9.0.1 + transitivePeerDependencies: + - '@emotion/is-prop-valid' + - '@types/react' + - supports-color + '@wordpress/components@28.2.0(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@ariakit/react': 0.3.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -19417,6 +19835,41 @@ snapshots: - '@types/react' - supports-color + '@wordpress/compose@3.25.3(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/deprecated': 2.12.3 + '@wordpress/dom': 2.18.0 + '@wordpress/element': 2.20.3 + '@wordpress/is-shallow-equal': 3.1.3 + '@wordpress/keycodes': 2.19.3 + '@wordpress/priority-queue': 1.11.2 + clipboard: 2.0.11 + lodash: 4.17.21 + memize: 1.1.0 + mousetrap: 1.6.5 + react-resize-aware: 3.1.3(react@18.3.1) + use-memo-one: 1.1.3(react@18.3.1) + transitivePeerDependencies: + - react + + '@wordpress/compose@6.35.0(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.7 + '@types/mousetrap': 1.6.15 + '@wordpress/deprecated': 3.58.0 + '@wordpress/dom': 3.58.0 + '@wordpress/element': 5.35.0 + '@wordpress/is-shallow-equal': 4.58.0 + '@wordpress/keycodes': 3.58.0 + '@wordpress/priority-queue': 2.58.0 + '@wordpress/undo-manager': 0.18.0 + change-case: 4.1.2 + clipboard: 2.0.11 + mousetrap: 1.6.5 + react: 18.3.1 + use-memo-one: 1.1.3(react@18.3.1) + '@wordpress/compose@7.2.0(react@18.2.0)': dependencies: '@babel/runtime': 7.24.7 @@ -19599,6 +20052,32 @@ snapshots: rememo: 4.0.2 use-memo-one: 1.1.3(react@18.3.1) + '@wordpress/data@9.28.0(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/compose': 6.35.0(react@18.3.1) + '@wordpress/deprecated': 3.58.0 + '@wordpress/element': 5.35.0 + '@wordpress/is-shallow-equal': 4.58.0 + '@wordpress/priority-queue': 2.58.0 + '@wordpress/private-apis': 0.40.0 + '@wordpress/redux-routine': 4.58.0(redux@4.2.1) + deepmerge: 4.3.1 + equivalent-key-map: 0.2.2 + is-plain-object: 5.0.0 + is-promise: 4.0.0 + react: 18.3.1 + redux: 4.2.1 + rememo: 4.0.2 + use-memo-one: 1.1.3(react@18.3.1) + + '@wordpress/date@4.58.0': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/deprecated': 3.58.0 + moment: 2.29.4 + moment-timezone: 0.5.45 + '@wordpress/date@5.2.0': dependencies: '@babel/runtime': 7.24.7 @@ -19611,15 +20090,39 @@ snapshots: json2php: 0.0.7 webpack: 5.76.0(webpack-cli@4.9.1) + '@wordpress/deprecated@2.12.3': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/hooks': 2.12.3 + + '@wordpress/deprecated@3.58.0': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/hooks': 3.58.0 + '@wordpress/deprecated@4.2.0': dependencies: '@babel/runtime': 7.24.7 '@wordpress/hooks': 4.2.0 + '@wordpress/dom-ready@3.58.0': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/dom-ready@4.2.0': dependencies: '@babel/runtime': 7.24.7 + '@wordpress/dom@2.18.0': + dependencies: + '@babel/runtime': 7.24.7 + lodash: 4.17.21 + + '@wordpress/dom@3.58.0': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/deprecated': 3.58.0 + '@wordpress/dom@4.2.0': dependencies: '@babel/runtime': 7.24.7 @@ -19825,6 +20328,27 @@ snapshots: - supports-color - utf-8-validate + '@wordpress/element@2.20.3': + dependencies: + '@babel/runtime': 7.24.7 + '@types/react': 16.14.60 + '@types/react-dom': 16.9.24 + '@wordpress/escape-html': 1.12.2 + lodash: 4.17.21 + react: 16.14.0 + react-dom: 16.14.0(react@16.14.0) + + '@wordpress/element@5.35.0': + dependencies: + '@babel/runtime': 7.24.7 + '@types/react': 18.3.1 + '@types/react-dom': 18.3.0 + '@wordpress/escape-html': 2.58.0 + change-case: 4.1.2 + is-plain-object: 5.0.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@wordpress/element@6.2.0': dependencies: '@babel/runtime': 7.24.7 @@ -19836,6 +20360,14 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@wordpress/escape-html@1.12.2': + dependencies: + '@babel/runtime': 7.24.7 + + '@wordpress/escape-html@2.58.0': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/escape-html@3.2.0': dependencies: '@babel/runtime': 7.24.7 @@ -19889,14 +20421,45 @@ snapshots: - '@types/react' - supports-color + '@wordpress/hooks@2.12.3': + dependencies: + '@babel/runtime': 7.24.7 + + '@wordpress/hooks@3.58.0': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/hooks@4.2.0': dependencies: '@babel/runtime': 7.24.7 + '@wordpress/html-entities@3.58.0': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/html-entities@4.2.0': dependencies: '@babel/runtime': 7.24.7 + '@wordpress/i18n@3.20.0': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/hooks': 2.12.3 + gettext-parser: 1.4.0 + lodash: 4.17.21 + memize: 1.1.0 + sprintf-js: 1.1.3 + tannin: 1.2.0 + + '@wordpress/i18n@4.58.0': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/hooks': 3.58.0 + gettext-parser: 1.4.0 + memize: 2.1.0 + sprintf-js: 1.1.3 + tannin: 1.2.0 + '@wordpress/i18n@5.2.0': dependencies: '@babel/runtime': 7.24.7 @@ -19913,6 +20476,13 @@ snapshots: '@wordpress/primitives': 4.2.0(react@18.3.1) react: 18.3.1 + '@wordpress/icons@9.49.0(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/element': 5.35.0 + '@wordpress/primitives': 3.56.0(react@18.3.1) + react: 18.3.1 + '@wordpress/interactivity-router@2.2.0': dependencies: '@wordpress/interactivity': 6.2.0 @@ -19975,6 +20545,14 @@ snapshots: - '@types/react' - supports-color + '@wordpress/is-shallow-equal@3.1.3': + dependencies: + '@babel/runtime': 7.24.7 + + '@wordpress/is-shallow-equal@4.58.0': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/is-shallow-equal@5.2.0': dependencies: '@babel/runtime': 7.24.7 @@ -19993,6 +20571,17 @@ snapshots: '@wordpress/keycodes': 4.2.0 react: 18.3.1 + '@wordpress/keycodes@2.19.3': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/i18n': 3.20.0 + lodash: 4.17.21 + + '@wordpress/keycodes@3.58.0': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/i18n': 4.58.0 + '@wordpress/keycodes@4.2.0': dependencies: '@babel/runtime': 7.24.7 @@ -20149,6 +20738,13 @@ snapshots: dependencies: prettier: wp-prettier@3.0.3 + '@wordpress/primitives@3.56.0(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/element': 5.35.0 + clsx: 2.1.1 + react: 18.3.1 + '@wordpress/primitives@4.2.0(react@18.3.1)': dependencies: '@babel/runtime': 7.24.7 @@ -20156,15 +20752,43 @@ snapshots: clsx: 2.1.1 react: 18.3.1 + '@wordpress/priority-queue@1.11.2': + dependencies: + '@babel/runtime': 7.24.7 + + '@wordpress/priority-queue@2.58.0': + dependencies: + '@babel/runtime': 7.24.7 + requestidlecallback: 0.3.0 + '@wordpress/priority-queue@3.2.0': dependencies: '@babel/runtime': 7.24.7 requestidlecallback: 0.3.0 + '@wordpress/private-apis@0.40.0': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/private-apis@1.2.0': dependencies: '@babel/runtime': 7.24.7 + '@wordpress/react-i18n@3.56.0': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/element': 5.35.0 + '@wordpress/i18n': 4.58.0 + utility-types: 3.11.0 + + '@wordpress/redux-routine@4.58.0(redux@4.2.1)': + dependencies: + '@babel/runtime': 7.24.7 + is-plain-object: 5.0.0 + is-promise: 4.0.0 + redux: 4.2.1 + rungen: 0.3.2 + '@wordpress/redux-routine@5.2.0(redux@4.2.1)': dependencies: '@babel/runtime': 7.24.7 @@ -20219,6 +20843,20 @@ snapshots: - supports-color - utf-8-validate + '@wordpress/rich-text@6.35.0(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/a11y': 3.58.0 + '@wordpress/compose': 6.35.0(react@18.3.1) + '@wordpress/data': 9.28.0(react@18.3.1) + '@wordpress/deprecated': 3.58.0 + '@wordpress/element': 5.35.0 + '@wordpress/escape-html': 2.58.0 + '@wordpress/i18n': 4.58.0 + '@wordpress/keycodes': 3.58.0 + memize: 2.1.0 + react: 18.3.1 + '@wordpress/rich-text@7.2.0(react@18.3.1)': dependencies: '@babel/runtime': 7.24.7 @@ -20313,6 +20951,11 @@ snapshots: dependencies: '@babel/runtime': 7.24.7 + '@wordpress/undo-manager@0.18.0': + dependencies: + '@babel/runtime': 7.24.7 + '@wordpress/is-shallow-equal': 4.58.0 + '@wordpress/undo-manager@1.2.0': dependencies: '@babel/runtime': 7.24.7 @@ -20331,6 +20974,8 @@ snapshots: '@wordpress/element': 6.2.0 react: 18.3.1 + '@wordpress/warning@2.58.0': {} + '@wordpress/warning@3.2.0': {} '@wordpress/widgets@4.2.0(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -21031,6 +21676,8 @@ snapshots: caniuse-lite@1.0.30001640: {} + canvas-confetti@1.9.3: {} + capital-case@1.0.4: dependencies: no-case: 3.0.4 @@ -21174,6 +21821,8 @@ snapshots: cjs-module-lexer@1.3.1: {} + classnames@2.5.1: {} + clean-css@5.3.3: dependencies: source-map: 0.6.1 @@ -22550,6 +23199,8 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + exenv@1.2.2: {} + exit@0.1.2: {} expand-tilde@1.2.2: @@ -23096,6 +23747,11 @@ snapshots: prop-types: 15.8.1 react: 18.3.1 + gridicons@3.4.2(react@18.3.1): + dependencies: + prop-types: 15.8.1 + react: 18.3.1 + gunzip-maybe@1.4.2: dependencies: browserify-zlib: 0.1.4 @@ -23136,6 +23792,11 @@ snapshots: dependencies: has-symbols: 1.0.3 + hash.js@1.1.7: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + hasha@5.2.2: dependencies: is-stream: 2.0.1 @@ -23289,6 +23950,24 @@ snapshots: husky@8.0.3: {} + i18n-calypso@7.0.0(@types/react@18.3.1)(react@18.3.1): + dependencies: + '@automattic/interpolate-components': 1.2.1(@types/react@18.3.1)(react@18.3.1) + '@babel/runtime': 7.24.7 + '@tannin/sprintf': 1.2.0 + '@wordpress/compose': 6.35.0(react@18.3.1) + debug: 4.3.4 + events: 3.3.0 + hash.js: 1.1.7 + lodash: 4.17.21 + lru: 3.1.0 + react: 18.3.1 + tannin: 1.2.0 + use-subscription: 1.6.0(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - supports-color + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -24504,6 +25183,10 @@ snapshots: lru-cache@7.18.3: {} + lru@3.1.0: + dependencies: + inherits: 2.0.4 + lz-string@1.5.0: {} magic-string@0.27.0: @@ -24693,6 +25376,8 @@ snapshots: dependencies: fs-monkey: 1.0.6 + memize@1.1.0: {} + memize@2.1.0: {} memoizerific@1.11.3: @@ -24926,6 +25611,8 @@ snapshots: schema-utils: 4.2.0 webpack: 5.76.0(webpack-cli@4.9.1) + minimalistic-assert@1.0.1: {} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -25930,6 +26617,14 @@ snapshots: transitivePeerDependencies: - supports-color + react-dom@16.14.0(react@16.14.0): + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + prop-types: 15.8.1 + react: 16.14.0 + scheduler: 0.19.1 + react-dom@18.2.0(react@18.2.0): dependencies: loose-envify: 1.4.0 @@ -25965,6 +26660,17 @@ snapshots: react-is@18.3.1: {} + react-lifecycles-compat@3.0.4: {} + + react-modal@3.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + exenv: 1.2.2 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-lifecycles-compat: 3.0.4 + warning: 4.0.3 + react-page-visibility@7.0.0(react@18.3.1): dependencies: prop-types: 15.8.1 @@ -26036,6 +26742,10 @@ snapshots: use-callback-ref: 1.3.2(react@18.3.1) use-sidecar: 1.1.2(react@18.3.1) + react-resize-aware@3.1.3(react@18.3.1): + dependencies: + react: 18.3.1 + react-router-dom@5.3.4(react@18.3.1): dependencies: '@babel/runtime': 7.24.7 @@ -26132,6 +26842,11 @@ snapshots: prop-types: 15.8.1 react: 18.3.1 + react-slider@2.0.5(react@18.3.1): + dependencies: + prop-types: 15.8.1 + react: 18.3.1 + react-style-singleton@2.2.1(@types/react@18.3.1)(react@18.3.1): dependencies: get-nonce: 1.0.1 @@ -26170,6 +26885,12 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react@16.14.0: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + prop-types: 15.8.1 + react@18.2.0: dependencies: loose-envify: 1.4.0 @@ -26546,6 +27267,11 @@ snapshots: dependencies: xmlchars: 2.2.0 + scheduler@0.19.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -27588,6 +28314,10 @@ snapshots: react: 18.3.1 tslib: 2.5.0 + use-subscription@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + use-sync-external-store@1.2.2(react@18.3.1): dependencies: react: 18.3.1 @@ -27604,6 +28334,8 @@ snapshots: utila@0.4.0: {} + utility-types@3.11.0: {} + utils-merge@1.0.1: {} uuid@8.3.2: {} @@ -27668,6 +28400,10 @@ snapshots: dependencies: makeerror: 1.0.12 + warning@4.0.3: + dependencies: + loose-envify: 1.4.0 + watchpack@2.4.1: dependencies: glob-to-regexp: 0.4.1 diff --git a/projects/packages/jetpack-mu-wpcom/package.json b/projects/packages/jetpack-mu-wpcom/package.json index 40a68e6784eee..3f6f041240cf6 100644 --- a/projects/packages/jetpack-mu-wpcom/package.json +++ b/projects/packages/jetpack-mu-wpcom/package.json @@ -48,12 +48,15 @@ "webpack-cli": "4.9.1" }, "dependencies": { + "@automattic/calypso-analytics": "1.1.2", "@automattic/color-studio": "2.6.0", + "@automattic/components": "2.1.1", "@automattic/i18n-utils": "1.2.3", "@automattic/jetpack-base-styles": "workspace:*", "@automattic/jetpack-shared-extension-utils": "workspace:*", "@automattic/typography": "1.0.0", "@automattic/page-pattern-modal": "1.1.5", + "@automattic/viewport": "1.0.0", "@preact/signals": "^1.2.2", "@sentry/browser": "7.80.1", "@tanstack/react-query": "^5.15.5", @@ -66,9 +69,9 @@ "@wordpress/element": "6.2.0", "@wordpress/hooks": "4.2.0", "@wordpress/i18n": "5.2.0", + "@wordpress/icons": "10.2.0", "@wordpress/plugins": "7.2.0", "@wordpress/url": "4.2.0", - "@wordpress/icons": "10.2.0", "clsx": "2.1.1", "preact": "^10.13.1", "react": "^18.2.0", From a6bafb924dfe6fceeb85251dac279abfef8ce216 Mon Sep 17 00:00:00 2001 From: Louis Laugesen Date: Mon, 22 Jul 2024 16:37:53 +1000 Subject: [PATCH 06/16] changelog --- .../jetpack-mu-wpcom/changelog/wpcom-block-editor-nux | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/packages/jetpack-mu-wpcom/changelog/wpcom-block-editor-nux diff --git a/projects/packages/jetpack-mu-wpcom/changelog/wpcom-block-editor-nux b/projects/packages/jetpack-mu-wpcom/changelog/wpcom-block-editor-nux new file mode 100644 index 0000000000000..44b012c3bcd33 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/changelog/wpcom-block-editor-nux @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Added wpcom-block-editor-nux feature from calypso's ETK package. From 8787ec7d9baa694fecee5fce25c46dbf399afd73 Mon Sep 17 00:00:00 2001 From: arthur Date: Thu, 25 Jul 2024 10:30:35 +0800 Subject: [PATCH 07/16] Update namespace --- ...-editor-first-post-published-modal-controller.php | 4 ++-- ...rest-wpcom-block-editor-nux-status-controller.php | 4 ++-- ...ck-editor-seller-celebration-modal-controller.php | 5 ++--- ...t-wpcom-block-editor-sharing-modal-controller.php | 4 ++-- ...ock-editor-video-celebration-modal-controller.php | 5 ++--- .../class-wpcom-block-editor-nux.php | 12 +++++++----- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-first-post-published-modal-controller.php b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-first-post-published-modal-controller.php index dfd5b9590eee0..30fb43301b2b1 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-first-post-published-modal-controller.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-first-post-published-modal-controller.php @@ -2,10 +2,10 @@ /** * WP_REST_WPCOM_Block_Editor_First_Post_Published_Modal_Controller file. * - * @package A8C\FSE + * @package Aautomattic/jetpack-mu-wpcom */ -namespace A8C\FSE; +namespace Automattic\Jetpack\Jetpack_Mu_Wpcom\NUX; /** * Class WP_REST_WPCOM_Block_Editor_First_Post_Published_Modal_Controller. diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-nux-status-controller.php b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-nux-status-controller.php index 9d2af2d56b732..f9e370942a3d0 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-nux-status-controller.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-nux-status-controller.php @@ -2,10 +2,10 @@ /** * WP_REST_WPCOM_Block_Editor_NUX_Status_Controller file. * - * @package A8C\FSE + * @package Aautomattic/jetpack-mu-wpcom */ -namespace A8C\FSE; +namespace Automattic\Jetpack\Jetpack_Mu_Wpcom\NUX; /** * Class WP_REST_WPCOM_Block_Editor_NUX_Status_Controller. diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-seller-celebration-modal-controller.php b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-seller-celebration-modal-controller.php index 1550728c1c2c8..cb6567540fe17 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-seller-celebration-modal-controller.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-seller-celebration-modal-controller.php @@ -2,10 +2,10 @@ /** * WP_REST_WPCOM_Block_Editor_Seller_Celebration_Modal_Controller file. * - * @package A8C\FSE + * @package Aautomattic/jetpack-mu-wpcom */ -namespace A8C\FSE; +namespace Automattic\Jetpack\Jetpack_Mu_Wpcom\NUX; /** * Class WP_REST_WPCOM_Block_Editor_Seller_Celebration_Modal_Controller. @@ -53,7 +53,6 @@ public function register_rest_route() { ), ) ); - } /** diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-sharing-modal-controller.php b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-sharing-modal-controller.php index ad67cf7b42002..006ecd51087bd 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-sharing-modal-controller.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-sharing-modal-controller.php @@ -2,10 +2,10 @@ /** * WP_REST_WPCOM_Block_Editor_Sharing_Modal_Controller file. * - * @package A8C\FSE + * @package Aautomattic/jetpack-mu-wpcom */ -namespace A8C\FSE; +namespace Automattic\Jetpack\Jetpack_Mu_Wpcom\NUX; /** * Class WP_REST_WPCOM_Block_Editor_Sharing_Modal_Controller. diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-video-celebration-modal-controller.php b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-video-celebration-modal-controller.php index 5b535f95598d6..f891b1c451da1 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-video-celebration-modal-controller.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-video-celebration-modal-controller.php @@ -2,10 +2,10 @@ /** * WP_REST_WPCOM_Block_Editor_Video_Celebration_Modal_Controller file. * - * @package A8C\FSE + * @package Aautomattic/jetpack-mu-wpcom */ -namespace A8C\FSE; +namespace Automattic\Jetpack\Jetpack_Mu_Wpcom\NUX; /** * Class WP_REST_WPCOM_Block_Editor_Video_Celebration_Modal_Controller. @@ -53,7 +53,6 @@ public function register_rest_route() { ), ) ); - } /** diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php index 5a12df53528c4..c01bbef944677 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php @@ -2,10 +2,12 @@ /** * WPCOM Block Editor NUX file. * - * @package A8C\FSE + * @package automattic/jetpack-mu-wpcom */ -namespace A8C\FSE; +namespace Automattic\Jetpack\Jetpack_Mu_Wpcom\NUX; + +use Automattic\Jetpack\Jetpack_Mu_Wpcom\Common; define( 'MU_WPCOM_BLOCK_EDITOR_NUX', true ); @@ -31,7 +33,7 @@ public function __construct() { /** * Creates instance. * - * @return \A8C\FSE\WPCOM_Block_Editor_NUX + * @return \Automattic\Jetpack\Jetpack_Mu_Wpcom\NUX\WPCOM_Block_Editor_NUX */ public static function init() { if ( self::$instance === null ) { @@ -64,10 +66,10 @@ public function enqueue_script_and_style() { wp_localize_script( 'wpcom-block-editor-nux-script', 'wpcomBlockEditorNuxLocale', - \A8C\FSE\Common\get_iso_639_locale( determine_locale() ) + Common\get_iso_639_locale( determine_locale() ) ); - wp_set_script_translations( 'wpcom-block-editor-nux-script', 'full-site-editing' ); + wp_set_script_translations( 'wpcom-block-editor-nux-script', 'jetpack-mu-wpcom' ); $style_path = 'dist/wpcom-block-editor-nux' . ( is_rtl() ? '.rtl' : '' ) . '.css'; wp_enqueue_style( From 9432bd4f4dbf52c1a4e9df74a3c37a00bf58aa57 Mon Sep 17 00:00:00 2001 From: arthur Date: Thu, 25 Jul 2024 10:57:40 +0800 Subject: [PATCH 08/16] Use wpcomTrackEvent to track events --- .../src/blogging-prompts-modal/index.js | 15 +++++------ .../src/draft-post-modal/index.js | 4 +-- .../src/first-post-published-modal/index.tsx | 4 +-- .../src/seller-celebration-modal/index.jsx | 4 +-- .../src/sharing-modal/index.tsx | 18 +++++++------ .../src/sharing-modal/suggested-tags.tsx | 14 +++++----- .../src/welcome-modal/wpcom-nux.js | 27 +++++++++---------- .../src/welcome-tour/tour-launch.tsx | 21 ++++++++------- .../src/welcome-tour/tour-steps.tsx | 18 +++++++------ 9 files changed, 64 insertions(+), 61 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/blogging-prompts-modal/index.js b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/blogging-prompts-modal/index.js index 86d194c64dabd..2857b0e4e83be 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/blogging-prompts-modal/index.js +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/blogging-prompts-modal/index.js @@ -1,4 +1,3 @@ -import { recordTracksEvent } from '@automattic/calypso-analytics'; import apiFetch from '@wordpress/api-fetch'; import { createBlock } from '@wordpress/blocks'; import { Button, Modal } from '@wordpress/components'; @@ -7,6 +6,7 @@ import { __ } from '@wordpress/i18n'; import { addQueryArgs, getQueryArg } from '@wordpress/url'; import moment from 'moment'; import { useEffect, useState } from 'react'; +import { wpcomTrackEvent } from '../../../../common/tracks'; import { ArrowLeftIcon, ArrowRightIcon } from './icons'; import './style.scss'; @@ -28,7 +28,7 @@ export const BloggingPromptsModalInner = () => { path, } ) .then( result => { - recordTracksEvent( 'calypso_editor_writing_prompts_modal_viewed' ); + wpcomTrackEvent( 'calypso_editor_writing_prompts_modal_viewed' ); return setPrompts( result ); } ) // eslint-disable-next-line no-console @@ -39,22 +39,19 @@ export const BloggingPromptsModalInner = () => { return null; } - /** - * - */ - function selectPrompt() { + const selectPrompt = () => { const promptId = prompts[ promptIndex ]?.id; dispatch( 'core/editor' ).resetEditorBlocks( [ createBlock( 'jetpack/blogging-prompt', { promptId } ), ] ); - recordTracksEvent( 'calypso_editor_writing_prompts_modal_prompt_selected', { + wpcomTrackEvent( 'calypso_editor_writing_prompts_modal_prompt_selected', { prompt_id: promptId, } ); setIsOpen( false ); - } + }; const closeModal = () => { - recordTracksEvent( 'calypso_editor_writing_prompts_modal_closed' ); + wpcomTrackEvent( 'calypso_editor_writing_prompts_modal_closed' ); setIsOpen( false ); }; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/draft-post-modal/index.js b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/draft-post-modal/index.js index 1467da2d5d9d6..b456234ff0f8a 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/draft-post-modal/index.js +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/draft-post-modal/index.js @@ -1,9 +1,9 @@ -import { recordTracksEvent } from '@automattic/calypso-analytics'; import { Button } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; import { useState } from '@wordpress/element'; import { doAction, hasAction } from '@wordpress/hooks'; import { __ } from '@wordpress/i18n'; +import { wpcomTrackEvent } from '../../../../common/tracks'; import NuxModal from '../nux-modal'; import draftPostImage from './images/draft-post.svg'; import './style.scss'; @@ -48,7 +48,7 @@ const DraftPostModal = () => { } onRequestClose={ closeModal } - onOpen={ () => recordTracksEvent( 'calypso_editor_wpcom_draft_post_modal_show' ) } + onOpen={ () => wpcomTrackEvent( 'calypso_editor_wpcom_draft_post_modal_show' ) } /> ); }; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/first-post-published-modal/index.tsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/first-post-published-modal/index.tsx index 675601897bae8..404f755a2a573 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/first-post-published-modal/index.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/first-post-published-modal/index.tsx @@ -1,10 +1,10 @@ -import { recordTracksEvent } from '@automattic/calypso-analytics'; import { Button } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; import { useEffect, useRef, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { isURL } from '@wordpress/url'; import React from 'react'; +import { wpcomTrackEvent } from '../../../../common/tracks'; import { useShouldShowFirstPostPublishedModal } from '../../../dotcom-fse/lib/first-post-published-modal/should-show-first-post-published-modal-context'; import useSiteIntent from '../../../dotcom-fse/lib/site-intent/use-site-intent'; import NuxModal from '../nux-modal'; @@ -102,7 +102,7 @@ const FirstPostPublishedModalInner: React.FC = () => { } onRequestClose={ closeModal } - onOpen={ () => recordTracksEvent( 'calypso_editor_wpcom_first_post_published_modal_show' ) } + onOpen={ () => wpcomTrackEvent( 'calypso_editor_wpcom_first_post_published_modal_show' ) } /> ); }; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/seller-celebration-modal/index.jsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/seller-celebration-modal/index.jsx index cc21de78e15de..e8c3f727880b4 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/seller-celebration-modal/index.jsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/seller-celebration-modal/index.jsx @@ -1,8 +1,8 @@ -import { recordTracksEvent } from '@automattic/calypso-analytics'; import { Button } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; import { useState, useRef, useEffect } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import { wpcomTrackEvent } from '../../../../common/tracks'; import { useHasSeenSellerCelebrationModal } from '../../../dotcom-fse/lib/seller-celebration-modal/has-seen-seller-celebration-modal-context'; import useShouldShowSellerCelebrationModal from '../../../dotcom-fse/lib/seller-celebration-modal/use-should-show-seller-celebration-modal'; import useSiteIntent from '../../../dotcom-fse/lib/site-intent/use-site-intent'; @@ -121,7 +121,7 @@ const SellerCelebrationModalInner = () => { } onRequestClose={ closeModal } - onOpen={ () => recordTracksEvent( 'calypso_editor_wpcom_seller_celebration_modal_show' ) } + onOpen={ () => wpcomTrackEvent( 'calypso_editor_wpcom_seller_celebration_modal_show' ) } /> ); }; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/index.tsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/index.tsx index e1f4ba38e481c..8a419f667b0e2 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/index.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/index.tsx @@ -1,4 +1,3 @@ -import { recordTracksEvent } from '@automattic/calypso-analytics'; import { FormLabel } from '@automattic/components'; import { START_WRITING_FLOW, DESIGN_FIRST_FLOW } from '@automattic/onboarding'; import { Modal, Button } from '@wordpress/components'; @@ -11,6 +10,7 @@ import ClipboardButton from 'calypso/components/forms/clipboard-button'; import FormInputCheckbox from 'calypso/components/forms/form-checkbox'; import clsx from 'clsx'; import React from 'react'; +import { wpcomTrackEvent } from '../../../../common/tracks'; import { useShouldShowFirstPostPublishedModal } from '../../../dotcom-fse/lib/first-post-published-modal/should-show-first-post-published-modal-context'; import useShouldShowSellerCelebrationModal from '../../../dotcom-fse/lib/seller-celebration-modal/use-should-show-seller-celebration-modal'; import useSiteIntent from '../../../dotcom-fse/lib/site-intent/use-site-intent'; @@ -86,7 +86,7 @@ const SharingModalInner: React.FC = () => { postType === 'post' ) { previousIsCurrentPostPublished.current = isCurrentPostPublished; - recordTracksEvent( 'calypso_editor_sharing_dialog_show' ); + wpcomTrackEvent( 'calypso_editor_sharing_dialog_show' ); // When the post published panel shows, it is focused automatically. // Thus, we need to delay open the modal so that the modal would not be close immediately @@ -97,6 +97,8 @@ const SharingModalInner: React.FC = () => { } }, [ postType, + postPassword, + postStatus, shouldShowFirstPostPublishedModal, shouldShowSellerCelebrationModal, shouldShowVideoCelebrationModal, @@ -117,7 +119,7 @@ const SharingModalInner: React.FC = () => { baseUrl.search = params.toString(); const twitterUrl = baseUrl.href; - recordTracksEvent( 'calypso_editor_sharing_twitter' ); + wpcomTrackEvent( 'calypso_editor_sharing_twitter' ); window.open( twitterUrl, 'twitter', 'width=550,height=420,resizeable,scrollbars' ); }; const shareFb = () => { @@ -129,7 +131,7 @@ const SharingModalInner: React.FC = () => { baseUrl.search = params.toString(); const facebookUrl = baseUrl.href; - recordTracksEvent( 'calypso_editor_sharing_facebook' ); + wpcomTrackEvent( 'calypso_editor_sharing_facebook' ); window.open( facebookUrl, 'facebook', 'width=626,height=436,resizeable,scrollbars' ); }; const shareLinkedin = () => { @@ -141,7 +143,7 @@ const SharingModalInner: React.FC = () => { baseUrl.search = params.toString(); const linkedinUrl = baseUrl.href; - recordTracksEvent( 'calypso_editor_sharing_linkedin' ); + wpcomTrackEvent( 'calypso_editor_sharing_linkedin' ); window.open( linkedinUrl, 'linkedin', 'width=626,height=436,resizeable,scrollbars' ); }; const shareTumblr = () => { @@ -153,7 +155,7 @@ const SharingModalInner: React.FC = () => { baseUrl.search = params.toString(); const tumblrUrl = baseUrl.href; - recordTracksEvent( 'calypso_editor_sharing_tumblr' ); + wpcomTrackEvent( 'calypso_editor_sharing_tumblr' ); window.open( tumblrUrl, 'tumblr', 'width=626,height=436,resizeable,scrollbars' ); }; const sharePinterest = () => { @@ -165,11 +167,11 @@ const SharingModalInner: React.FC = () => { baseUrl.search = params.toString(); const pinterestUrl = baseUrl.href; - recordTracksEvent( 'calypso_editor_sharing_pinterest' ); + wpcomTrackEvent( 'calypso_editor_sharing_pinterest' ); window.open( pinterestUrl, 'pinterest', 'width=626,height=436,resizeable,scrollbars' ); }; const copyLinkClick = () => { - recordTracksEvent( 'calypso_editor_sharing_link_copy' ); + wpcomTrackEvent( 'calypso_editor_sharing_link_copy' ); createNotice( 'success', __( 'Link copied to clipboard.', 'jetpack-mu-wpcom' ), { type: 'snackbar', } ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/suggested-tags.tsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/suggested-tags.tsx index 38ea61ebed535..8f47b85adb592 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/suggested-tags.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/suggested-tags.tsx @@ -1,4 +1,3 @@ -import { recordTracksEvent } from '@automattic/calypso-analytics'; import { useLocale } from '@automattic/i18n-utils'; import { Button, FormTokenField } from '@wordpress/components'; import { TokenItem } from '@wordpress/components/build-types/form-token-field/types'; @@ -7,6 +6,7 @@ import { useEffect } from '@wordpress/element'; import { store as noticesStore } from '@wordpress/notices'; import { useI18n } from '@wordpress/react-i18n'; import * as React from 'react'; +import { wpcomTrackEvent } from '../../../../common/tracks'; import useAddTagsToPost from './use-add-tags-to-post'; type PostMeta = { @@ -32,8 +32,9 @@ type SuggestedTagsProps = { }; /** + * Display the suggested tags. * - * @param props + * @param props - The props of the component. */ function SuggestedTags( props: SuggestedTagsProps ) { const { __, _n } = useI18n(); @@ -60,7 +61,7 @@ function SuggestedTags( props: SuggestedTagsProps ) { number_of_suggested_tags_selected: numSameTags, number_of_added_tags: numAddedTags, }; - recordTracksEvent( 'calypso_reader_post_publish_add_tags', eventProps ); + wpcomTrackEvent( 'calypso_reader_post_publish_add_tags', eventProps ); if ( numAddedTags > 0 ) { createNotice( 'success', @@ -82,19 +83,20 @@ function SuggestedTags( props: SuggestedTagsProps ) { if ( origSuggestedTags?.length === 0 ) { // Check if localeSlug begins with 'en' if ( localeSlug && localeSlug.startsWith( 'en' ) ) { - recordTracksEvent( 'calypso_reader_post_publish_no_suggested_tags' ); + wpcomTrackEvent( 'calypso_reader_post_publish_no_suggested_tags' ); } props.setShouldShowSuggestedTags( false ); } else { - recordTracksEvent( 'calypso_reader_post_publish_show_suggested_tags', { + wpcomTrackEvent( 'calypso_reader_post_publish_show_suggested_tags', { number_of_original_suggested_tags: origSuggestedTags.length, } ); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [] ); const onChangeSelectedTags = ( newTags: ( string | TokenItem )[] ) => { setSelectedTags( newTags ); - recordTracksEvent( 'calypso_reader_post_publish_update_suggested_tags' ); + wpcomTrackEvent( 'calypso_reader_post_publish_update_suggested_tags' ); }; const tokenField = ( diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/wpcom-nux.js b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/wpcom-nux.js index 0d3c57fceb9cf..020c21390770a 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/wpcom-nux.js +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-modal/wpcom-nux.js @@ -1,10 +1,8 @@ -/* eslint-disable wpcalypso/jsx-classname-namespace */ - -import { recordTracksEvent } from '@automattic/calypso-analytics'; import { Guide, GuidePage } from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; import { useEffect } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import { wpcomTrackEvent } from '../../../../common/tracks'; import blockPickerImage from './images/block-picker.svg'; import editorImage from './images/editor.svg'; import previewImage from './images/preview.svg'; @@ -13,7 +11,7 @@ import privateImage from './images/private.svg'; import './style.scss'; /** - * + * The nux component. */ function WpcomNux() { const { show, isNewPageLayoutModalOpen, isManuallyOpened } = useSelect( select => ( { @@ -29,7 +27,7 @@ function WpcomNux() { // Track opening of the welcome guide useEffect( () => { if ( show && ! isNewPageLayoutModalOpen ) { - recordTracksEvent( 'calypso_editor_wpcom_nux_open', { + wpcomTrackEvent( 'calypso_editor_wpcom_nux_open', { is_gutenboarding: window.calypsoifyGutenberg?.isGutenboarding, is_manually_opened: isManuallyOpened, } ); @@ -41,7 +39,7 @@ function WpcomNux() { } const dismissWpcomNux = () => { - recordTracksEvent( 'calypso_editor_wpcom_nux_dismiss', { + wpcomTrackEvent( 'calypso_editor_wpcom_nux_dismiss', { is_gutenboarding: window.calypsoifyGutenberg?.isGutenboarding, } ); setShowWelcomeGuide( false, { openedManually: false } ); @@ -113,18 +111,19 @@ function getWpcomNuxPages() { } /** + * Display the Nux page * - * @param root0 - * @param root0.pageNumber - * @param root0.isLastPage - * @param root0.alignBottom - * @param root0.heading - * @param root0.description - * @param root0.imgSrc + * @param props - The props of the component. + * @param props.pageNumber - The number of page. + * @param props.isLastPage - Whether the current page is the last one. + * @param props.alignBottom - Whether to align bottom. + * @param props.heading - The text of heading. + * @param props.description - The text of description. + * @param props.imgSrc - The src of image. */ function NuxPage( { pageNumber, isLastPage, alignBottom = false, heading, description, imgSrc } ) { useEffect( () => { - recordTracksEvent( 'calypso_editor_wpcom_nux_slide_view', { + wpcomTrackEvent( 'calypso_editor_wpcom_nux_slide_view', { slide_number: pageNumber, is_last_slide: isLastPage, is_gutenboarding: window.calypsoifyGutenberg?.isGutenboarding, diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/tour-launch.tsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/tour-launch.tsx index 25fa1a953fddd..fe1802209e357 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/tour-launch.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/tour-launch.tsx @@ -1,10 +1,10 @@ -import { recordTracksEvent } from '@automattic/calypso-analytics'; import { useLocale } from '@automattic/i18n-utils'; import { START_WRITING_FLOW, DESIGN_FIRST_FLOW } from '@automattic/onboarding'; import { WpcomTourKit, usePrefetchTourAssets } from '@automattic/tour-kit'; import { isWithinBreakpoint } from '@automattic/viewport'; import { useDispatch, useSelect, dispatch } from '@wordpress/data'; import { useEffect, useMemo } from '@wordpress/element'; +import { wpcomTrackEvent } from '../../../../common/tracks'; import useSiteIntent from '../../../dotcom-fse/lib/site-intent/use-site-intent'; import useSitePlan from '../../../dotcom-fse/lib/site-plan/use-site-plan'; import { selectors as starterPageTemplatesSelectors } from '../../../starter-page-templates/store'; @@ -34,7 +34,7 @@ type CoreInterfacePlaceholder = { }; /** - * + * The Welcome Tour of the Launch. */ function LaunchWpcomWelcomeTour() { const { show, isNewPageLayoutModalOpen, isManuallyOpened } = useSelect( @@ -74,7 +74,7 @@ function LaunchWpcomWelcomeTour() { } // Track opening of the Welcome Guide - recordTracksEvent( 'calypso_editor_wpcom_tour_open', { + wpcomTrackEvent( 'calypso_editor_wpcom_tour_open', { is_gutenboarding: window.calypsoifyGutenberg?.isGutenboarding, is_manually_opened: isManuallyOpened, intent: siteIntent, @@ -98,9 +98,10 @@ function LaunchWpcomWelcomeTour() { } /** + * Display the welcome tour. * - * @param root0 - * @param root0.siteIntent + * @param props - The props of the component. + * @param props.siteIntent - The intent of the site. */ function WelcomeTour( { siteIntent }: { siteIntent?: string } ) { const sitePlan = useSitePlan( window._currentSiteId ); @@ -158,7 +159,7 @@ function WelcomeTour( { siteIntent }: { siteIntent?: string } ) { const tourConfig: WpcomConfig = { steps: tourSteps, closeHandler: ( _steps, currentStepIndex, source ) => { - recordTracksEvent( 'calypso_editor_wpcom_tour_dismiss', { + wpcomTrackEvent( 'calypso_editor_wpcom_tour_dismiss', { is_gutenboarding: isGutenboarding, slide_number: currentStepIndex + 1, action: source, @@ -184,7 +185,7 @@ function WelcomeTour( { siteIntent }: { siteIntent?: string } ) { ( dispatch( 'automattic/wpcom-welcome-guide' ) as WPcomWelcomeGuideActions ).setTourRating( rating ); - recordTracksEvent( 'calypso_editor_wpcom_tour_rate', { + wpcomTrackEvent( 'calypso_editor_wpcom_tour_rate', { thumbs_up: rating === 'thumbs-up', is_gutenboarding: false, intent: siteIntent, @@ -194,7 +195,7 @@ function WelcomeTour( { siteIntent }: { siteIntent?: string } ) { }, callbacks: { onMinimize: currentStepIndex => { - recordTracksEvent( 'calypso_editor_wpcom_tour_minimize', { + wpcomTrackEvent( 'calypso_editor_wpcom_tour_minimize', { is_gutenboarding: isGutenboarding, slide_number: currentStepIndex + 1, intent: siteIntent, @@ -202,7 +203,7 @@ function WelcomeTour( { siteIntent }: { siteIntent?: string } ) { } ); }, onMaximize: currentStepIndex => { - recordTracksEvent( 'calypso_editor_wpcom_tour_maximize', { + wpcomTrackEvent( 'calypso_editor_wpcom_tour_maximize', { is_gutenboarding: isGutenboarding, slide_number: currentStepIndex + 1, intent: siteIntent, @@ -213,7 +214,7 @@ function WelcomeTour( { siteIntent }: { siteIntent?: string } ) { const lastStepIndex = tourSteps.length - 1; const { heading } = tourSteps[ currentStepIndex ].meta; - recordTracksEvent( 'calypso_editor_wpcom_tour_slide_view', { + wpcomTrackEvent( 'calypso_editor_wpcom_tour_slide_view', { slide_number: currentStepIndex + 1, is_last_slide: currentStepIndex === lastStepIndex, slide_heading: heading, diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/tour-steps.tsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/tour-steps.tsx index 5462d4ef7108f..a1e1aa44ee863 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/tour-steps.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/tour-steps.tsx @@ -1,10 +1,10 @@ -import { recordTracksEvent } from '@automattic/calypso-analytics'; import { localizeUrl } from '@automattic/i18n-utils'; import { isMobile } from '@automattic/viewport'; import { ExternalLink } from '@wordpress/components'; import { createInterpolateElement } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { getQueryArg } from '@wordpress/url'; +import { wpcomTrackEvent } from '../../../../common/tracks'; import { getEditorType } from './get-editor-type'; import type { WpcomStep } from '@automattic/tour-kit'; @@ -14,8 +14,9 @@ interface TourAsset { } /** + * Get the tour asset by the key. * - * @param key + * @param key - The key of the tour asset. */ function getTourAssets( key: string ): TourAsset | undefined { const CDN_PREFIX = 'https://s0.wp.com/i/editor-welcome-tour'; @@ -65,12 +66,13 @@ function getTourAssets( key: string ): TourAsset | undefined { } /** + * Get the steps of the tour * - * @param localeSlug - * @param referencePositioning - * @param isSiteEditor - * @param themeName - * @param siteIntent + * @param localeSlug - The slug of the locale. + * @param referencePositioning - The reference positioning. + * @param isSiteEditor - Whether is the site editor. + * @param themeName - The name of the theme. + * @param siteIntent - The intent of the current site. */ function getTourSteps( localeSlug: string, @@ -84,7 +86,7 @@ function getTourSteps( const siteEditorCourseUrl = `https://wordpress.com/home/${ window.location.hostname }?courseSlug=site-editor-quick-start`; const editorType = getEditorType(); const onSiteEditorCourseLinkClick = () => { - recordTracksEvent( 'calypso_editor_wpcom_tour_site_editor_course_link_click', { + wpcomTrackEvent( 'calypso_editor_wpcom_tour_site_editor_course_link_click', { is_pattern_assembler: isPatternAssembler, intent: siteIntent, editor_type: editorType, From d1ca1e3b824d72e8ffd8a5cbf690d7de914e2b06 Mon Sep 17 00:00:00 2001 From: arthur Date: Thu, 25 Jul 2024 18:55:57 +0800 Subject: [PATCH 09/16] Port @automattic/tour-kit package and fix build --- pnpm-lock.yaml | 722 +----------------- .../packages/jetpack-mu-wpcom/package.json | 6 +- .../components/keyboard-navigation.tsx | 61 ++ .../tour-kit/components/tour-kit-frame.tsx | 286 +++++++ .../components/tour-kit-minimized.tsx | 30 + .../tour-kit/components/tour-kit-overlay.tsx | 20 + .../tour-kit-spotlight-interactivity.tsx | 38 + .../components/tour-kit-spotlight.tsx | 122 +++ .../tour-kit/components/tour-kit-step.tsx | 52 ++ .../common/tour-kit/components/tour-kit.tsx | 40 + .../src/common/tour-kit/constants.ts | 3 + ...-seen-seller-celebration-modal-context.tsx | 68 ++ ...s-seen-video-celebration-modal-context.tsx | 68 ++ .../src/common/tour-kit/contexts/index.ts | 4 + ...how-first-post-published-modal-context.jsx | 31 + .../tour-kit/contexts/tour-kit-context.tsx | 16 + .../src/common/tour-kit/error-boundary.tsx | 33 + .../src/common/tour-kit/hooks/index.ts | 9 + .../tour-kit/hooks/use-focus-handler.ts | 60 ++ .../common/tour-kit/hooks/use-focus-trap.ts | 56 ++ .../use-has-selected-payment-block-once.js | 62 ++ .../tour-kit/hooks/use-keydown-handler.ts | 52 ++ ...se-should-show-seller-celebration-modal.js | 67 ++ ...use-should-show-video-celebration-modal.ts | 51 ++ .../common/tour-kit/hooks/use-site-intent.js | 14 + .../common/tour-kit/hooks/use-site-plan.js | 24 + .../tour-kit/hooks/use-step-tracking.ts | 26 + .../src/common/tour-kit/index.ts | 6 + .../src/common/tour-kit/styles.scss | 91 +++ .../src/common/tour-kit/types.ts | 164 ++++ .../src/common/tour-kit/utils/index.ts | 16 + .../tour-kit/utils/live-resize-modifier.tsx | 111 +++ .../components/wpcom-tour-kit-minimized.tsx | 50 ++ .../wpcom-tour-kit-pagination-control.scss | 44 ++ .../wpcom-tour-kit-pagination-control.tsx | 49 ++ .../components/wpcom-tour-kit-rating.tsx | 73 ++ .../wpcom-tour-kit-step-card-navigation.tsx | 62 ++ ...om-tour-kit-step-card-overlay-controls.tsx | 41 + .../components/wpcom-tour-kit-step-card.tsx | 109 +++ .../wpcom/components/wpcom-tour-kit-step.tsx | 28 + .../wpcom/components/wpcom-tour-kit.tsx | 29 + .../wpcom/hooks/use-prefetch-tour-assets.tsx | 16 + .../variants/wpcom/icons/maximize.tsx | 13 + .../variants/wpcom/icons/minimize.tsx | 14 + .../variants/wpcom/icons/thumbs_down.tsx | 14 + .../variants/wpcom/icons/thumbs_up.tsx | 14 + .../common/tour-kit/variants/wpcom/index.ts | 2 + .../tour-kit/variants/wpcom/styles.scss | 232 ++++++ .../class-wpcom-block-editor-nux.php | 38 +- .../src/block-editor-nux.js | 19 +- .../src/first-post-published-modal/index.tsx | 3 +- .../wpcom-block-editor-nux/src/public-path.js | 11 - .../src/seller-celebration-modal/index.jsx | 8 +- .../src/sharing-modal/clipboard-button.tsx | 47 ++ .../src/sharing-modal/form-checkbox.tsx | 19 + .../src/sharing-modal/form-label.tsx | 37 + .../src/sharing-modal/index.tsx | 19 +- .../wpcom-block-editor-nux/src/store.js | 4 +- .../src/video-celebration-modal/index.jsx | 10 +- .../video-celebration-modal/video-success.svg | 56 ++ .../src/welcome-modal/style.scss | 5 +- .../{tour-launch.tsx => tour-launch.jsx} | 91 +-- .../{tour-steps.tsx => use-tour-steps.tsx} | 13 +- 63 files changed, 2743 insertions(+), 836 deletions(-) create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/keyboard-navigation.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-frame.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-minimized.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-overlay.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-spotlight-interactivity.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-spotlight.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-step.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/constants.ts create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/contexts/has-seen-seller-celebration-modal-context.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/contexts/has-seen-video-celebration-modal-context.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/contexts/index.ts create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/contexts/should-show-first-post-published-modal-context.jsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/contexts/tour-kit-context.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/error-boundary.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/index.ts create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-focus-handler.ts create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-focus-trap.ts create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-has-selected-payment-block-once.js create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-keydown-handler.ts create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-should-show-seller-celebration-modal.js create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-should-show-video-celebration-modal.ts create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-site-intent.js create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-site-plan.js create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-step-tracking.ts create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/index.ts create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/styles.scss create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/types.ts create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/utils/index.ts create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/utils/live-resize-modifier.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-minimized.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-pagination-control.scss create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-pagination-control.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-rating.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-step-card-navigation.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-step-card-overlay-controls.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-step-card.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-step.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/hooks/use-prefetch-tour-assets.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/icons/maximize.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/icons/minimize.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/icons/thumbs_down.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/icons/thumbs_up.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/index.ts create mode 100644 projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/styles.scss delete mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/public-path.js create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/clipboard-button.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/form-checkbox.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/form-label.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/video-celebration-modal/video-success.svg rename projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/{tour-launch.tsx => tour-launch.jsx} (65%) rename projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/welcome-tour/{tour-steps.tsx => use-tour-steps.tsx} (98%) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c0a6642968e0..5832cb26851a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2115,12 +2115,12 @@ importers: '@automattic/calypso-analytics': specifier: 1.1.2 version: 1.1.2 + '@automattic/calypso-color-schemes': + specifier: 3.1.3 + version: 3.1.3 '@automattic/color-studio': specifier: 2.6.0 version: 2.6.0 - '@automattic/components': - specifier: 2.1.1 - version: 2.1.1(@types/react@18.3.1)(@wordpress/data@10.2.0(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@automattic/i18n-utils': specifier: 1.2.3 version: 1.2.3 @@ -2136,9 +2136,9 @@ importers: '@automattic/typography': specifier: 1.0.0 version: 1.0.0 - '@automattic/viewport': - specifier: 1.0.0 - version: 1.0.0 + '@popperjs/core': + specifier: ^2.11.8 + version: 2.11.8 '@preact/signals': specifier: ^1.2.2 version: 1.3.0(preact@10.22.1) @@ -2187,9 +2187,15 @@ importers: clsx: specifier: 2.1.1 version: 2.1.1 + debug: + specifier: 4.3.4 + version: 4.3.4 preact: specifier: ^10.13.1 version: 10.22.1 + react-popper: + specifier: ^2.3.0 + version: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) redux: specifier: ^4.2.1 version: 4.2.1 @@ -4889,13 +4895,6 @@ packages: '@automattic/color-studio@2.6.0': resolution: {integrity: sha512-2LzB6bbQw1vayZxZy5Y+DnCYU7x8tPu+rZhNkWD7V8QZTSJMJO65XKZhYaCByC+C5OegXyGyZzcqEOHHdj5iiQ==} - '@automattic/components@2.1.1': - resolution: {integrity: sha512-PqGwe1CI0PtQIaTka5cbX/gKqEkhk4GYTdkExigHdzifnYtPZSmuLsw5mlPdfA8qY141s8wokARU7cDIZE0DCw==} - peerDependencies: - '@wordpress/data': ^9.26.0 - react: ^18.2.0 - react-dom: ^18.2.0 - '@automattic/explat-client-react-helpers@0.1.1': resolution: {integrity: sha512-ilebWXmuleHg3BYThJvKW/iraS5kV9iQvm+vtJn6Mkl01rkMDCmsl4MGYOYiKLi/BpUq0QVlD8qKapOsz5g3Vg==} @@ -4905,30 +4904,15 @@ packages: '@automattic/format-currency@1.0.1': resolution: {integrity: sha512-RY2eiUlDiqNSHiJzz2YmH/mw4IjAUO5hkxbwcVGHJkBZowdq/WcSG2yhXc8N9cV9N1fTO/ryCuJvGnpHUe+mAg==} - '@automattic/format-currency@2.0.0': - resolution: {integrity: sha512-9A+oKRUm+n4f+cT4FHsDkCpo4mVRa/zBAvsXXq5vZpwfOWskAyDjdxA03Jl8A+z7pHYRimysG4WLM3jMRJutLw==} - '@automattic/i18n-utils@1.2.3': resolution: {integrity: sha512-zvZlazUoEasLATrta3ljfxu2uaZWgHRNKWf56KKBlrPiIxNQvx9D7YyN2MhiV27e/PuAhB0gI4ghqp3gzurKmA==} - '@automattic/interpolate-components@1.2.1': - resolution: {integrity: sha512-YNQtJsrs9KQ3lkBdtLyDheVRijoBA3y/PuHdgJ0eB4AX9JyjkDX7jd79Inh79+01CGNLbMQGrEJby2zvbJr17A==} - peerDependencies: - '@types/react': '>=16.14.23' - react: '>=16.2.0' - peerDependenciesMeta: - '@types/react': - optional: true - '@automattic/languages@1.0.0': resolution: {integrity: sha512-froTyDbTmLitHkvY9WLCpFdjUo6moOLkDKw63J2fLiB2gBApy2thkBV+LRx4Z0kIF5iXVkQF4yYOPYkT9Sr13Q==} '@automattic/load-script@1.0.0': resolution: {integrity: sha512-Hc1mRmTK12OKrONnGhe7Ht1Gpo4B/ls8WQ1IZ1/qBws1bUZ6u7Crnpv3HZkN4UI7irG3OU4l4Pn1TXtoJLcKRw==} - '@automattic/material-design-icons@1.0.0': - resolution: {integrity: sha512-8baJ1l8ftLq/UdLeucOeGXo4/wpaB/pSOBO587/pKC/xv2Oo8Ok21g1WKwp0Y8hEq4+3JNtCzOGVxmIgDBTYvA==} - '@automattic/page-pattern-modal@1.1.5': resolution: {integrity: sha512-cFA82qWUDSSFhOHfOkOqh7X8I9As5fNGp7w3LVw7ZDRl6wSiQZveLvWp4msNDnLmeiJTpxWVOZWvCirxYUE3Sw==} peerDependencies: @@ -4955,11 +4939,6 @@ packages: '@automattic/typography@1.0.0': resolution: {integrity: sha512-TnT+vPaNUXQYwDsPCPxhNY0d4LnOKvrb0SizUCC5iybo5sfOlX/rYalGDyz6nPQDF0EBaQwMf7qhVsflFR0cBg==} - '@automattic/viewport-react@1.0.0': - resolution: {integrity: sha512-+6+l4jj14GXeoc5Jpic5E5eVvNL88Ezz8cMLmKAw0fpPDsz4gJv7o0hgShu0hjGjKTtBeUkBGfFWMCdRjZaVcA==} - peerDependencies: - react: ^16.0.0 - '@automattic/viewport@1.0.0': resolution: {integrity: sha512-aSJRuZ80kds6kO+baIw8wK4nmlHG/e/sUPa7BDwalo8vBLtBnW07GsXm1zGgz9htgEogFuuxBdKR4IjO1z8phA==} @@ -6329,6 +6308,9 @@ packages: engines: {node: '>=18'} hasBin: true + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@preact/signals-core@1.7.0': resolution: {integrity: sha512-bEZLgmJGSBVP5PUPDowhPW3bVdMmp9Tr5OEl+SQK+8Tv9T7UsIfyN905cfkmmeqw8z4xp8T6zrl4M1uj9+HAfg==} @@ -7230,9 +7212,6 @@ packages: '@tannin/postfix@1.1.0': resolution: {integrity: sha512-oocsqY7g0cR+Gur5jRQLSrX2OtpMLMse1I10JQBm8CdGMrDkh1Mg2gjsiquMHRtBs4Qwu5wgEp5GgIYHk4SNPw==} - '@tannin/sprintf@1.2.0': - resolution: {integrity: sha512-T0ORaQrH6kNFGzTg285RVPK+NCYZxOoA+r0QfKgHqK+yk5RuYPSKDa18XCLtycCNq+VWKpfyDpzGUGhYgCV+kw==} - '@tanstack/query-core@4.35.3': resolution: {integrity: sha512-PS+WEjd9wzKTyNjjQymvcOe1yg8f3wYc6mD+vb6CKyZAKvu4sIJwryfqfBULITKCla7P9C4l5e9RXePHvZOZeQ==} @@ -7517,9 +7496,6 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react-dom@16.9.24': - resolution: {integrity: sha512-Gcmq2JTDheyWn/1eteqyzzWKSqDjYU6KYsIvH7thb7CR5OYInAWOX+7WnKf6PaU/cbdOc4szJItcDEJO7UGmfA==} - '@types/react-dom@18.3.0': resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} @@ -7538,9 +7514,6 @@ packages: '@types/react-test-renderer@18.3.0': resolution: {integrity: sha512-HW4MuEYxfDbOHQsVlY/XtOvNHftCVEPhJF2pQXXwcUiUF+Oyb0usgp48HSgpK5rt8m9KZb22yqOeZm+rrVG8gw==} - '@types/react@16.14.60': - resolution: {integrity: sha512-wIFmnczGsTcgwCBeIYOuy2mdXEiKZ5znU/jNOnMZPQyCcIxauMGWlX0TNG4lZ7NxRKj7YUIZRneJQSSdB2jKgg==} - '@types/react@18.3.1': resolution: {integrity: sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==} @@ -7553,9 +7526,6 @@ packages: '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} - '@types/scheduler@0.16.8': - resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} - '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} @@ -7784,10 +7754,6 @@ packages: webpack-dev-server: optional: true - '@wordpress/a11y@3.58.0': - resolution: {integrity: sha512-7NnJKl4+pxP6kV/jvXaJcZZCGzW7zaj6YeMnyjUd96cH4ta1ykBIveWgejerFOGsbK+88FnStcxSFj+dbDXs/w==} - engines: {node: '>=12'} - '@wordpress/a11y@4.2.0': resolution: {integrity: sha512-IwW1mf4zIo/BIa19MB4yGLWQl3MlzB0tu+0Coct+m0gc7uaFuSov2ub56vpDvXvNsfLISWtvWBFL4TQTInNdiA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -7816,9 +7782,6 @@ packages: resolution: {integrity: sha512-bOvP+8b0zk7wjIbpaAnJSc0kkKZA6oeyJ4/GRtp+mmcrIXJWL+LNCGYQM+aUDSGO7j1QazLpTR7p5br8ygl5/g==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/base-styles@4.49.0': - resolution: {integrity: sha512-yFRYqNtd26ULZ0oAHhCu/IcaA0XHI3E7kRCKajZqUvyRQj7YprXnpD3o0/pnwvF6ZFTXzCX8pXHjUc2TIv97ig==} - '@wordpress/base-styles@5.2.0': resolution: {integrity: sha512-yBMVbn4gvNQimVfLAZ+/F7F/Tpl+femF9ojgv90c0A0o6IDEtdC+6vUvtAxvXVoruwmxsq8ncouoorZbYDb3yg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -7862,13 +7825,6 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 - '@wordpress/components@27.6.0': - resolution: {integrity: sha512-f+fXENkgrPs5GLo2yu9fEAdVX0KriEatRcjDUyw0+DbNbJR62sCdDtGdhJRW4jPUUoUowxaGO0y4+jvQWxnbyg==} - engines: {node: '>=12'} - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - '@wordpress/components@28.2.0': resolution: {integrity: sha512-6yq1D4SrS5i9lvukCiN+FtKXh42JUV+T0X+e7xI3qFANaRMqjSNqEJFOu14H2oKIbZwjdKxEJ/Wck1lWBIKu/A==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -7876,15 +7832,6 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 - '@wordpress/compose@3.25.3': - resolution: {integrity: sha512-tCO2EnJCkCH548OqA0uU8V1k/1skz2QwBlHs8ZQSpimqUS4OWWsAlndCEFe4U4vDTqFt2ow7tzAir+05Cw8MAg==} - - '@wordpress/compose@6.35.0': - resolution: {integrity: sha512-PfruhCxxxJokDQHc2YBgerEiHV7BIxQk9g5vU4/f9X/0PBQWUTuxOzSFcAba03vnjfAgtPTSMp50T50hcJwXfA==} - engines: {node: '>=12'} - peerDependencies: - react: ^18.0.0 - '@wordpress/compose@7.2.0': resolution: {integrity: sha512-J2OGEatXXTgRJmXZHYcstL5GyQgQcoeSJ9dQ2wVFHJLnWoIX3hlvi5oRy10lpl1yntfm6NLkWDBuSTIbAYJzww==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -7911,16 +7858,6 @@ packages: peerDependencies: react: ^18.0.0 - '@wordpress/data@9.28.0': - resolution: {integrity: sha512-EDPpZdkngdoW7EMzPpGj0BmNcr7syJO67pgTODtN/4XFIdYL2RKzFyn3nlLBKhX17UsE/ALq9WdijacH4QJ9qw==} - engines: {node: '>=12'} - peerDependencies: - react: ^18.0.0 - - '@wordpress/date@4.58.0': - resolution: {integrity: sha512-yFT7DU0H9W0lsDytMaVMmjho08X1LeBMIQMppxdtKB04Ujx58hVh7gtunOsstUQ7pVg23nE2eLaVfx5JOdjzAw==} - engines: {node: '>=12'} - '@wordpress/date@5.2.0': resolution: {integrity: sha512-k9gFs74hg9yvrWhoUrNrCf95VIqvPkv2AysRxTxSfEZjTyMBuxNNa3amr295hCeoqcjT9mw5bptmLntawmg7nA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -7931,32 +7868,14 @@ packages: peerDependencies: webpack: ^5.0.0 - '@wordpress/deprecated@2.12.3': - resolution: {integrity: sha512-qr+yDfTQfI3M4h6oY6IeHWwoHr4jxbILjSlV+Ht6Jjto9Owap6OuzSqR13Ev4xqIoG4C7b5B3gZXVfwVDae1zg==} - - '@wordpress/deprecated@3.58.0': - resolution: {integrity: sha512-knweE2lLEUxWRr6A48sHiO0ww5pPybGe2NVIZVq/y7EaYCMdpy6gYA0ZdVqMKZvtxKKqicJfwigcn+hinsTvUQ==} - engines: {node: '>=12'} - '@wordpress/deprecated@4.2.0': resolution: {integrity: sha512-grD/IBGEvXzTLaNB45QZv8jDQYK6bMhCSa7obRahZDpyrM2lwKwa8p1oack5R+81YWkBL02gl9V5G9BrpNfBJg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/dom-ready@3.58.0': - resolution: {integrity: sha512-sDgRPjNBToRKgYrpwvMRv2Yc7/17+sp8hm/rRnbubwb+d/DbGkK4Tc/r4sNLSZCqUAtcBXq9uk1lzvhge3QUSg==} - engines: {node: '>=12'} - '@wordpress/dom-ready@4.2.0': resolution: {integrity: sha512-1rD9vcwVy7s+yGbe8DuDkBpjvA/PJtY2DnUgyHIedC/YonlswalBSiFWGZuTX5QyocdffBAWiUlvebyN4TNtOw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/dom@2.18.0': - resolution: {integrity: sha512-tM2WeQuSObl3nzWjUTF0/dyLnA7sdl/MXaSe32D64OF89bjSyJvjUipI7gjKzI3kJ7ddGhwcTggGvSB06MOoCQ==} - - '@wordpress/dom@3.58.0': - resolution: {integrity: sha512-t3xSr/nqekj2qwUGRAqSeGx6116JOBxzI+VBiUfZrjGEnuyKdLelXDEeYtcwbb7etMkj/6F60/NB7GTl5IwizQ==} - engines: {node: '>=12'} - '@wordpress/dom@4.2.0': resolution: {integrity: sha512-vkeIsFdoKWl6lZJM+E49b+HocePP8gSPiDeUaa3P82JPTLTNAAfQMGWbAG0dbQCOZa7pmF4Sh0T1iVYmznm6eA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -7975,24 +7894,10 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 - '@wordpress/element@2.20.3': - resolution: {integrity: sha512-f4ZPTDf9CxiiOXiMxc4v1K7jcBMT4dsiehVOpkKzCDKboNXp4qVf8oe5PE23VGZNEjcOj5Mkg9hB57R0nqvMTw==} - - '@wordpress/element@5.35.0': - resolution: {integrity: sha512-puswpGcIdS+0A2g28uHriMkZqqRCmzFczue5Tk99VNtzBdehyk7Ae+DZ4xw5yT6GqYai8NTqv6MRwCB78uh5Mw==} - engines: {node: '>=12'} - '@wordpress/element@6.2.0': resolution: {integrity: sha512-pRCchhYoH7eN0bxL4iUMBm82psqSUozlmk4B5IhQiqzYoOWn7OjvkGqejAnt81iDZUNZ8hIY2gLpRplgwwiZlQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/escape-html@1.12.2': - resolution: {integrity: sha512-FabgSwznhdaUwe6hr1CsGpgxQbzqEoGevv73WIL1B9GvlZ6csRWodgHfWh4P6fYqpzxFL4WYB8wPJ1PdO32XFA==} - - '@wordpress/escape-html@2.58.0': - resolution: {integrity: sha512-9YJXMNfzkrhHEVP1jFEhgijbZqW8Mt3NHIMZjIQoWtBf7QE86umpYpGGBXzYC0YlpGTRGzZTBwYaqFKxjeaSgA==} - engines: {node: '>=12'} - '@wordpress/escape-html@3.2.0': resolution: {integrity: sha512-GFJ91lrs46zN3bgRGBHREaZ4jegwUA+2Gx+P6f11VDLhihNGKyg67uNf0lXqLoLj6iQQCBDP+15k/0k2ccr3YA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -8028,34 +7933,14 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 - '@wordpress/hooks@2.12.3': - resolution: {integrity: sha512-LmKiwKldZt6UYqOxV/a6+eUFXdvALFnB/pQx3RmrMvO64sgFhfR6dhrlv+uVbuuezSuv8dce1jx8lUWAT0krMA==} - - '@wordpress/hooks@3.58.0': - resolution: {integrity: sha512-9LB0ZHnZRQlORttux9t/xbAskF+dk2ujqzPGsVzc92mSKpQP3K2a5Wy74fUnInguB1vLUNHT6nrNdkVom5qX1Q==} - engines: {node: '>=12'} - '@wordpress/hooks@4.2.0': resolution: {integrity: sha512-N2dRMIb3F6y2dXlcT6m2CH/jDi9/Fe0gaM6ev7DrvwJ8+kX1CRzwAydemmPw34EnhQKvYKQYgGqttrfzvzgKJw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/html-entities@3.58.0': - resolution: {integrity: sha512-FU7b6QZdwTCuLKq6wCl0IZqqOMcMRxMcekVVytzTse7hYk9dvL1qQL/U4eQ/CNyKqiT9u7fb5NKTQILOzoolVQ==} - engines: {node: '>=12'} - '@wordpress/html-entities@4.2.0': resolution: {integrity: sha512-NwYv/VSxASrhIqjcgVkKS9s9kAseTJ6NYVpqCm8owz81GqYcpOe2XVCKQxOKHcU8x9Gm99uHR10jztXXJr1Z2Q==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/i18n@3.20.0': - resolution: {integrity: sha512-SIoOJFB4UrrYAScS4H91CYCLW9dX3Ghv8pBKc/yHGculb1AdGr6gRMlmJxZV62Cn3CZ4Ga86c+FfR+GiBu0JPg==} - hasBin: true - - '@wordpress/i18n@4.58.0': - resolution: {integrity: sha512-VfvS3BWv/RDjRKD6PscIcvYfWKnGJcI/DEqyDgUMhxCM6NRwoL478CsUKTiGJIymeyRodNRfprdcF086DpGKYw==} - engines: {node: '>=12'} - hasBin: true - '@wordpress/i18n@5.2.0': resolution: {integrity: sha512-G8z3/o1gm158XHANzthMBLsInQ/iWBFFIUoThiOP8C+VtpVozVpmWpgdayldPoTYolhCdaW6dicNUfdX8fOBTQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -8067,12 +7952,6 @@ packages: peerDependencies: react: ^18 - '@wordpress/icons@9.49.0': - resolution: {integrity: sha512-Z8F+ledkfkcKDuS1c/RkM0dEWdfv2AXs6bCgey89p0atJSscf7qYbMJR9zE5rZ5aqXyFfV0DAFKJEgayNqneNQ==} - engines: {node: '>=12'} - peerDependencies: - react: ^18 - '@wordpress/interactivity-router@2.2.0': resolution: {integrity: sha512-HWWE+U/7hWiH2dopIcx93MraohheKMFIlWVN5CiLqcUsUp5aKiOJXyhGhPvau4qnv3Y+Tm05sTt9ZFlIPmIoeg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -8088,13 +7967,6 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 - '@wordpress/is-shallow-equal@3.1.3': - resolution: {integrity: sha512-eDLhfC4aaSgklzqwc6F/F4zmJVpTVTAvhqX+q0SP/8LPcP2HuKErPHVrEc75PMWqIutja2wJg98YSNPdewrj1w==} - - '@wordpress/is-shallow-equal@4.58.0': - resolution: {integrity: sha512-NH2lbXo/6ix1t4Zu9UBXpXNtoLwSaYmIRSyDH34XNb0ic8a7yjEOhYWVW3LTfSCv9dJVyxlM5TJPtL85q7LdeQ==} - engines: {node: '>=12'} - '@wordpress/is-shallow-equal@5.2.0': resolution: {integrity: sha512-WIsaAu+vDoAwnfGSWqyOMZiJeKXMXHNj6SzuESieASbL1VcbWgpg8FjDRjCNCEBgu7oe2VQrDOpDfajI1fgVvw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -8111,13 +7983,6 @@ packages: peerDependencies: react: ^18.0.0 - '@wordpress/keycodes@2.19.3': - resolution: {integrity: sha512-8rNdmP5M1ifTgLIL0dt/N1uTGsq/Rx1ydCXy+gg24WdxBRhyu5sudNVCtascVXo26aIfOH9OJRdqRZZTEORhog==} - - '@wordpress/keycodes@3.58.0': - resolution: {integrity: sha512-Q/LRKpx8ndzuHlkxSQ2BD+NTYYKQPIneNNMng8hTAfyU7RFwXpqj06HpeOFGh4XIdPKCs/8hmucoLJRmmLmZJA==} - engines: {node: '>=12'} - '@wordpress/keycodes@4.2.0': resolution: {integrity: sha512-FJMR+KLfltcfmd0GhpI2C+zohFaGwPwZTYx9e0+cnIDJ5c5Jw1KTToZ+gkWPoSi++U/dlqkxkKYLD4AnGg7L5Q==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -8165,47 +8030,20 @@ packages: peerDependencies: prettier: '>=3' - '@wordpress/primitives@3.56.0': - resolution: {integrity: sha512-NXBq1ODjl6inMWx/l7KCbATcjdoeIOqYeL9i9alqdAfWeKx1EH9PIvKWylIkqZk7erXxCxldiRkuyjTtwjNBxw==} - engines: {node: '>=12'} - peerDependencies: - react: ^18 - '@wordpress/primitives@4.2.0': resolution: {integrity: sha512-UofDIMe3pQ4UvubCAjm4/Y+o/niAiHFRjhavvxBOZ5iCFyjG/1knbJcWa8+0qvjIA5YTBzZxfN6PD4Je1SwtFw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} peerDependencies: react: ^18 - '@wordpress/priority-queue@1.11.2': - resolution: {integrity: sha512-ulwmUOklY3orn1xXpcPnTyGWV5B/oycxI+cHZ6EevBVgM5sq+BW3xo0PKLR/MMm6UNBtFTu/71QAJrNZcD6V1g==} - - '@wordpress/priority-queue@2.58.0': - resolution: {integrity: sha512-W+qCS8HJWsXG8gE6yK/H/IObowcghPrQMM3cQHtfd/U05yFNU1Bd/fbj3AO1fVRztktS47lIpi9m3ll1evPEHA==} - engines: {node: '>=12'} - '@wordpress/priority-queue@3.2.0': resolution: {integrity: sha512-Kz/Zv+/TzgsKi5M3/iE2w4sMSi0f2Q3KnmU6taS5bEiiKRHvuC1U629YBsXCvBfi+7QWe2L7J7OVcLRwdEzAkg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/private-apis@0.40.0': - resolution: {integrity: sha512-ZX/9Y8eA3C3K6LOj32bHFj+9tNV819CBd8+chqMmmlvQRcTngiuXbMbnSdZnnAr1gLQgNpH9PJ60dIwJnGSEtQ==} - engines: {node: '>=12'} - '@wordpress/private-apis@1.2.0': resolution: {integrity: sha512-N0eRg0IHbgg1G8Z5/UCmU/FNan+YEAkVauM0W8OPp2/jeqQHS70VlrGwzmF+I8o2yInI6gL38dPE8sYElNiBFA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/react-i18n@3.56.0': - resolution: {integrity: sha512-Qe7EDCazhhrBLsvqJOYZdIygamJFJQbZGvhBD/9O7H/PgLbrxIluRqTiY1Vo+hN6W6vTIuaOcXlCxZKEmSGC0A==} - engines: {node: '>=12'} - - '@wordpress/redux-routine@4.58.0': - resolution: {integrity: sha512-r0mMWFeJr93yPy2uY/M5+gdUUYj0Zu8+21OFFb5hyQ0z7UHIa3IdgQxzCaTbV1LDA1ZYJrjHeCnA6s4gNHjA2Q==} - engines: {node: '>=12'} - peerDependencies: - redux: '>=4' - '@wordpress/redux-routine@5.2.0': resolution: {integrity: sha512-tzUQeO8sjkOn1l5MlrzXwjFhkEYNpw21nQ/sQP/smQp70rG22hPZSYOK8/C5ls1KGbzCtfwv92NxwJEKbBypiw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -8219,12 +8057,6 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 - '@wordpress/rich-text@6.35.0': - resolution: {integrity: sha512-h6/XftSqo9UQZebuNZyLfOVu+ButBLITW/BILsKeJhSpmM19VNdz8UhVGLp+xQPE+/GPCIMJrhhqipISDfc2Ig==} - engines: {node: '>=12'} - peerDependencies: - react: ^18.0.0 - '@wordpress/rich-text@7.2.0': resolution: {integrity: sha512-cjp/Y4bPavge1dV2JneM8km3Hq166BJLPPRpOpw9wBI4xlyjgyXHnMX6tqoU/nMIBXq4asWQrXQfkzbA0iJxcA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -8260,10 +8092,6 @@ packages: resolution: {integrity: sha512-4CDvT2x+NmP255E4JSowpHiZUK2wqVOvfrcuY884mm149+ZmPp2OyGuRnn4aqFEs3+Bcxjg5NEz0o4KH0CXt5Q==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/undo-manager@0.18.0': - resolution: {integrity: sha512-upbzPEToa095XG+2JXLHaolF1LfXEMFS0lNMYV37myoUS+eZ7/tl9Gx+yU2+OqWy57TMwx33NlWUX/n+ynzPRw==} - engines: {node: '>=12'} - '@wordpress/undo-manager@1.2.0': resolution: {integrity: sha512-xOjyl2hRro5I3pBuDYvrF+cLe0617KlPiuJok9YPofjUvfDvgf6gPLSBOAPj2GzYkiGASz0fnsPcE4UxS14ApQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -8278,10 +8106,6 @@ packages: peerDependencies: react: ^18.0.0 - '@wordpress/warning@2.58.0': - resolution: {integrity: sha512-9bZlORhyMY2nbWozeyC5kqJsFzEPP4DCLhGmjtbv+YWGHttUrxUZEfrKdqO+rUODA8rP5zeIly1nCQOUnkw4Lg==} - engines: {node: '>=12'} - '@wordpress/warning@3.2.0': resolution: {integrity: sha512-i+EYX506tE45+lhOW4qAwXm/5/PmkAdvKhNt+dTxJDif+ATUqp4QaFFRl9yFIt/v+ksAapQ0bnPkKNSDsECGtA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -8860,9 +8684,6 @@ packages: caniuse-lite@1.0.30001640: resolution: {integrity: sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==} - canvas-confetti@1.9.3: - resolution: {integrity: sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==} - capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -8962,9 +8783,6 @@ packages: cjs-module-lexer@1.3.1: resolution: {integrity: sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==} - classnames@2.5.1: - resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} - clean-css@5.3.3: resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} engines: {node: '>= 10.0'} @@ -10116,9 +9934,6 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} - exenv@1.2.2: - resolution: {integrity: sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==} - exit@0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} @@ -10572,11 +10387,6 @@ packages: peerDependencies: react: 15 - 18 - gridicons@3.4.2: - resolution: {integrity: sha512-KC2BzPDh3F0vJzYa7KYBWJOO9gTHoKoFiHNazZEU9Gq2jIJ2zObOA67wlZjZkPHPCjZiLQrko3AYFLrMrHXKrA==} - peerDependencies: - react: 15 - 18 - gunzip-maybe@1.4.2: resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==} hasBin: true @@ -10742,11 +10552,6 @@ packages: engines: {node: '>=14'} hasBin: true - i18n-calypso@7.0.0: - resolution: {integrity: sha512-GQesQzd/VYXiJOrjMixJNFOqNOcp43kKGKZTimYu70RabvcObpjfAOqtrQganszXqXWxZ7fAXOnhCTd8NVtf/Q==} - peerDependencies: - react: ^18.2.0 - iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -11652,10 +11457,6 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} - lru@3.1.0: - resolution: {integrity: sha512-5OUtoiVIGU4VXBOshidmtOsvBIvcQR6FD/RzWSvaeHyxCGB+PCUCu+52lqMfdc0h/2CLvHhZS4TwUmMQrrMbBQ==} - engines: {node: '>= 0.4.0'} - lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -11761,9 +11562,6 @@ packages: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} - memize@1.1.0: - resolution: {integrity: sha512-K4FcPETOMTwe7KL2LK0orMhpOmWD2wRGwWWpbZy0fyArwsyIKR8YJVz8+efBAh3BO4zPqlSICu4vsLTRRqtFAg==} - memize@2.1.0: resolution: {integrity: sha512-yywVJy8ctVlN5lNPxsep5urnZ6TTclwPEyigM9M3Bi8vseJBOfqNrGWN/r8NzuIt3PovM323W04blJfGQfQSVg==} @@ -12964,11 +12762,6 @@ packages: resolution: {integrity: sha512-i8aF1nyKInZnANZ4uZrH49qn1paRgBZ7wZiCNBMnenlPzEv0mRl+ShpTVEI6wZNl8sSc79xZkivtgLKQArcanQ==} engines: {node: '>=16.14.0'} - react-dom@16.14.0: - resolution: {integrity: sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==} - peerDependencies: - react: ^16.14.0 - react-dom@18.2.0: resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: @@ -12991,6 +12784,9 @@ packages: react: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0 react-dom: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0 + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -13003,22 +12799,19 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - react-lifecycles-compat@3.0.4: - resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} - - react-modal@3.16.1: - resolution: {integrity: sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==} - engines: {node: '>=8'} - peerDependencies: - react: ^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 - react-dom: ^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 - react-page-visibility@7.0.0: resolution: {integrity: sha512-d4Kq/8TtJSr8dQc8EJeAZcSKTrGzC5OPTm6UrMur9BnwP0fgTawI9+Nd+ZGB7vwCfn2yZS0qDF9DR3/QYTGazw==} engines: {node: '>=10'} peerDependencies: react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-popper@2.3.0: + resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==} + peerDependencies: + '@popperjs/core': ^2.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + react-redux@7.2.8: resolution: {integrity: sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw==} peerDependencies: @@ -13061,11 +12854,6 @@ packages: '@types/react': optional: true - react-resize-aware@3.1.3: - resolution: {integrity: sha512-dPacJZfDczrGOljY8MBnQRudxjI8+M9qG5GXQzU4wIl5q2T4e8UcrSl2rsEBYn9DBuGflhDnI2uYGGFJ991DfA==} - peerDependencies: - react: ^16.8.0 || 17.x || 18.x - react-router-dom@5.3.4: resolution: {integrity: sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==} peerDependencies: @@ -13177,10 +12965,6 @@ packages: react: '>=16.13' react-dom: '>=16.13' - react@16.14.0: - resolution: {integrity: sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==} - engines: {node: '>=0.10.0'} - react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -13502,9 +13286,6 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} - scheduler@0.19.1: - resolution: {integrity: sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==} - scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -14466,11 +14247,6 @@ packages: '@types/react': optional: true - use-subscription@1.6.0: - resolution: {integrity: sha512-0Y/cTLlZfw547tJhJMoRA16OUbVqRm6DmvGpiGbmLST6BIA5KU5cKlvlz8DVMrACnWpyEjCkgmhLatthP4jUbA==} - peerDependencies: - react: ^18.0.0 - use-sync-external-store@1.2.2: resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} peerDependencies: @@ -14485,10 +14261,6 @@ packages: utila@0.4.0: resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} - utility-types@3.11.0: - resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} - engines: {node: '>= 4'} - utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -14948,39 +14720,6 @@ snapshots: '@automattic/color-studio@2.6.0': {} - '@automattic/components@2.1.1(@types/react@18.3.1)(@wordpress/data@10.2.0(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@automattic/calypso-analytics': 1.1.2 - '@automattic/calypso-url': 1.1.0 - '@automattic/format-currency': 2.0.0 - '@automattic/i18n-utils': 1.2.3 - '@automattic/material-design-icons': 1.0.0 - '@automattic/typography': 1.0.0 - '@automattic/viewport-react': 1.0.0(react@18.3.1) - '@wordpress/base-styles': 4.49.0 - '@wordpress/components': 27.6.0(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@wordpress/data': 10.2.0(react@18.3.1) - '@wordpress/icons': 9.49.0(react@18.3.1) - '@wordpress/react-i18n': 3.56.0 - canvas-confetti: 1.9.3 - classnames: 2.5.1 - gridicons: 3.4.2(react@18.3.1) - i18n-calypso: 7.0.0(@types/react@18.3.1)(react@18.3.1) - lodash: 4.17.21 - prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-modal: 3.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-router-dom: 6.21.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-slider: 2.0.5(react@18.3.1) - utility-types: 3.11.0 - uuid: 9.0.1 - transitivePeerDependencies: - - '@babel/runtime' - - '@emotion/is-prop-valid' - - '@types/react' - - supports-color - '@automattic/explat-client-react-helpers@0.1.1': dependencies: '@automattic/explat-client': 0.1.0 @@ -14995,10 +14734,6 @@ snapshots: dependencies: tslib: 2.5.0 - '@automattic/format-currency@2.0.0': - dependencies: - tslib: 2.6.3 - '@automattic/i18n-utils@1.2.3': dependencies: '@automattic/calypso-config': 1.2.0 @@ -15011,12 +14746,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@automattic/interpolate-components@1.2.1(@types/react@18.3.1)(react@18.3.1)': - dependencies: - react: 18.3.1 - optionalDependencies: - '@types/react': 18.3.1 - '@automattic/languages@1.0.0': dependencies: tslib: 2.5.0 @@ -15028,8 +14757,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@automattic/material-design-icons@1.0.0': {} - '@automattic/page-pattern-modal@1.1.5(@types/react@18.3.1)(@wordpress/data@10.2.0(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1)': dependencies: '@automattic/color-studio': 2.6.0 @@ -15082,12 +14809,6 @@ snapshots: '@automattic/typography@1.0.0': {} - '@automattic/viewport-react@1.0.0(react@18.3.1)': - dependencies: - '@automattic/viewport': 1.0.0 - '@wordpress/compose': 3.25.3(react@18.3.1) - react: 18.3.1 - '@automattic/viewport@1.0.0': {} '@automattic/webpack-rtl-plugin@6.0.0(webpack@5.76.0(webpack-cli@4.9.1))': @@ -16711,6 +16432,8 @@ snapshots: dependencies: playwright: 1.45.1 + '@popperjs/core@2.11.8': {} + '@preact/signals-core@1.7.0': {} '@preact/signals@1.3.0(preact@10.22.1)': @@ -18611,8 +18334,6 @@ snapshots: '@tannin/postfix@1.1.0': {} - '@tannin/sprintf@1.2.0': {} - '@tanstack/query-core@4.35.3': {} '@tanstack/query-core@5.0.5': {} @@ -18916,10 +18637,6 @@ snapshots: '@types/range-parser@1.2.7': {} - '@types/react-dom@16.9.24': - dependencies: - '@types/react': 16.14.60 - '@types/react-dom@18.3.0': dependencies: '@types/react': 18.3.1 @@ -18950,12 +18667,6 @@ snapshots: dependencies: '@types/react': 18.3.1 - '@types/react@16.14.60': - dependencies: - '@types/prop-types': 15.7.12 - '@types/scheduler': 0.16.8 - csstype: 3.1.3 - '@types/react@18.3.1': dependencies: '@types/prop-types': 15.7.12 @@ -18969,8 +18680,6 @@ snapshots: '@types/retry@0.12.0': {} - '@types/scheduler@0.16.8': {} - '@types/semver@7.5.8': {} '@types/send@0.17.4': @@ -19311,12 +19020,6 @@ snapshots: dependencies: webpack-cli: 4.9.1(webpack@5.76.0) - '@wordpress/a11y@3.58.0': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/dom-ready': 3.58.0 - '@wordpress/i18n': 4.58.0 - '@wordpress/a11y@4.2.0': dependencies: '@babel/runtime': 7.24.7 @@ -19363,8 +19066,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@wordpress/base-styles@4.49.0': {} - '@wordpress/base-styles@5.2.0': {} '@wordpress/blob@4.2.0': @@ -19667,62 +19368,6 @@ snapshots: - '@types/react' - supports-color - '@wordpress/components@27.6.0(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@ariakit/react': 0.3.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@babel/runtime': 7.24.7 - '@emotion/cache': 11.11.0 - '@emotion/css': 11.11.2 - '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) - '@emotion/serialize': 1.1.4 - '@emotion/styled': 11.11.5(@emotion/react@11.11.4(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1) - '@emotion/utils': 1.2.1 - '@floating-ui/react-dom': 2.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@types/gradient-parser': 0.1.3 - '@types/highlight-words-core': 1.2.1 - '@use-gesture/react': 10.3.1(react@18.3.1) - '@wordpress/a11y': 3.58.0 - '@wordpress/compose': 6.35.0(react@18.3.1) - '@wordpress/date': 4.58.0 - '@wordpress/deprecated': 3.58.0 - '@wordpress/dom': 3.58.0 - '@wordpress/element': 5.35.0 - '@wordpress/escape-html': 2.58.0 - '@wordpress/hooks': 3.58.0 - '@wordpress/html-entities': 3.58.0 - '@wordpress/i18n': 4.58.0 - '@wordpress/icons': 9.49.0(react@18.3.1) - '@wordpress/is-shallow-equal': 4.58.0 - '@wordpress/keycodes': 3.58.0 - '@wordpress/primitives': 3.56.0(react@18.3.1) - '@wordpress/private-apis': 0.40.0 - '@wordpress/rich-text': 6.35.0(react@18.3.1) - '@wordpress/warning': 2.58.0 - change-case: 4.1.2 - clsx: 2.1.1 - colord: 2.9.3 - date-fns: 3.6.0 - deepmerge: 4.3.1 - downshift: 6.1.12(react@18.3.1) - fast-deep-equal: 3.1.3 - framer-motion: 11.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - gradient-parser: 0.1.5 - highlight-words-core: 1.2.2 - is-plain-object: 5.0.0 - memize: 2.1.0 - path-to-regexp: 6.2.2 - re-resizable: 6.9.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: 18.3.1 - react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) - remove-accents: 0.5.0 - use-lilius: 2.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - uuid: 9.0.1 - transitivePeerDependencies: - - '@emotion/is-prop-valid' - - '@types/react' - - supports-color - '@wordpress/components@28.2.0(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@ariakit/react': 0.3.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -19835,41 +19480,6 @@ snapshots: - '@types/react' - supports-color - '@wordpress/compose@3.25.3(react@18.3.1)': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/deprecated': 2.12.3 - '@wordpress/dom': 2.18.0 - '@wordpress/element': 2.20.3 - '@wordpress/is-shallow-equal': 3.1.3 - '@wordpress/keycodes': 2.19.3 - '@wordpress/priority-queue': 1.11.2 - clipboard: 2.0.11 - lodash: 4.17.21 - memize: 1.1.0 - mousetrap: 1.6.5 - react-resize-aware: 3.1.3(react@18.3.1) - use-memo-one: 1.1.3(react@18.3.1) - transitivePeerDependencies: - - react - - '@wordpress/compose@6.35.0(react@18.3.1)': - dependencies: - '@babel/runtime': 7.24.7 - '@types/mousetrap': 1.6.15 - '@wordpress/deprecated': 3.58.0 - '@wordpress/dom': 3.58.0 - '@wordpress/element': 5.35.0 - '@wordpress/is-shallow-equal': 4.58.0 - '@wordpress/keycodes': 3.58.0 - '@wordpress/priority-queue': 2.58.0 - '@wordpress/undo-manager': 0.18.0 - change-case: 4.1.2 - clipboard: 2.0.11 - mousetrap: 1.6.5 - react: 18.3.1 - use-memo-one: 1.1.3(react@18.3.1) - '@wordpress/compose@7.2.0(react@18.2.0)': dependencies: '@babel/runtime': 7.24.7 @@ -20052,32 +19662,6 @@ snapshots: rememo: 4.0.2 use-memo-one: 1.1.3(react@18.3.1) - '@wordpress/data@9.28.0(react@18.3.1)': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/compose': 6.35.0(react@18.3.1) - '@wordpress/deprecated': 3.58.0 - '@wordpress/element': 5.35.0 - '@wordpress/is-shallow-equal': 4.58.0 - '@wordpress/priority-queue': 2.58.0 - '@wordpress/private-apis': 0.40.0 - '@wordpress/redux-routine': 4.58.0(redux@4.2.1) - deepmerge: 4.3.1 - equivalent-key-map: 0.2.2 - is-plain-object: 5.0.0 - is-promise: 4.0.0 - react: 18.3.1 - redux: 4.2.1 - rememo: 4.0.2 - use-memo-one: 1.1.3(react@18.3.1) - - '@wordpress/date@4.58.0': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/deprecated': 3.58.0 - moment: 2.29.4 - moment-timezone: 0.5.45 - '@wordpress/date@5.2.0': dependencies: '@babel/runtime': 7.24.7 @@ -20090,39 +19674,15 @@ snapshots: json2php: 0.0.7 webpack: 5.76.0(webpack-cli@4.9.1) - '@wordpress/deprecated@2.12.3': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/hooks': 2.12.3 - - '@wordpress/deprecated@3.58.0': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/hooks': 3.58.0 - '@wordpress/deprecated@4.2.0': dependencies: '@babel/runtime': 7.24.7 '@wordpress/hooks': 4.2.0 - '@wordpress/dom-ready@3.58.0': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/dom-ready@4.2.0': dependencies: '@babel/runtime': 7.24.7 - '@wordpress/dom@2.18.0': - dependencies: - '@babel/runtime': 7.24.7 - lodash: 4.17.21 - - '@wordpress/dom@3.58.0': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/deprecated': 3.58.0 - '@wordpress/dom@4.2.0': dependencies: '@babel/runtime': 7.24.7 @@ -20328,27 +19888,6 @@ snapshots: - supports-color - utf-8-validate - '@wordpress/element@2.20.3': - dependencies: - '@babel/runtime': 7.24.7 - '@types/react': 16.14.60 - '@types/react-dom': 16.9.24 - '@wordpress/escape-html': 1.12.2 - lodash: 4.17.21 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - - '@wordpress/element@5.35.0': - dependencies: - '@babel/runtime': 7.24.7 - '@types/react': 18.3.1 - '@types/react-dom': 18.3.0 - '@wordpress/escape-html': 2.58.0 - change-case: 4.1.2 - is-plain-object: 5.0.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - '@wordpress/element@6.2.0': dependencies: '@babel/runtime': 7.24.7 @@ -20360,14 +19899,6 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@wordpress/escape-html@1.12.2': - dependencies: - '@babel/runtime': 7.24.7 - - '@wordpress/escape-html@2.58.0': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/escape-html@3.2.0': dependencies: '@babel/runtime': 7.24.7 @@ -20421,45 +19952,14 @@ snapshots: - '@types/react' - supports-color - '@wordpress/hooks@2.12.3': - dependencies: - '@babel/runtime': 7.24.7 - - '@wordpress/hooks@3.58.0': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/hooks@4.2.0': dependencies: '@babel/runtime': 7.24.7 - '@wordpress/html-entities@3.58.0': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/html-entities@4.2.0': dependencies: '@babel/runtime': 7.24.7 - '@wordpress/i18n@3.20.0': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/hooks': 2.12.3 - gettext-parser: 1.4.0 - lodash: 4.17.21 - memize: 1.1.0 - sprintf-js: 1.1.3 - tannin: 1.2.0 - - '@wordpress/i18n@4.58.0': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/hooks': 3.58.0 - gettext-parser: 1.4.0 - memize: 2.1.0 - sprintf-js: 1.1.3 - tannin: 1.2.0 - '@wordpress/i18n@5.2.0': dependencies: '@babel/runtime': 7.24.7 @@ -20476,13 +19976,6 @@ snapshots: '@wordpress/primitives': 4.2.0(react@18.3.1) react: 18.3.1 - '@wordpress/icons@9.49.0(react@18.3.1)': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/element': 5.35.0 - '@wordpress/primitives': 3.56.0(react@18.3.1) - react: 18.3.1 - '@wordpress/interactivity-router@2.2.0': dependencies: '@wordpress/interactivity': 6.2.0 @@ -20545,14 +20038,6 @@ snapshots: - '@types/react' - supports-color - '@wordpress/is-shallow-equal@3.1.3': - dependencies: - '@babel/runtime': 7.24.7 - - '@wordpress/is-shallow-equal@4.58.0': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/is-shallow-equal@5.2.0': dependencies: '@babel/runtime': 7.24.7 @@ -20571,17 +20056,6 @@ snapshots: '@wordpress/keycodes': 4.2.0 react: 18.3.1 - '@wordpress/keycodes@2.19.3': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/i18n': 3.20.0 - lodash: 4.17.21 - - '@wordpress/keycodes@3.58.0': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/i18n': 4.58.0 - '@wordpress/keycodes@4.2.0': dependencies: '@babel/runtime': 7.24.7 @@ -20738,13 +20212,6 @@ snapshots: dependencies: prettier: wp-prettier@3.0.3 - '@wordpress/primitives@3.56.0(react@18.3.1)': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/element': 5.35.0 - clsx: 2.1.1 - react: 18.3.1 - '@wordpress/primitives@4.2.0(react@18.3.1)': dependencies: '@babel/runtime': 7.24.7 @@ -20752,43 +20219,15 @@ snapshots: clsx: 2.1.1 react: 18.3.1 - '@wordpress/priority-queue@1.11.2': - dependencies: - '@babel/runtime': 7.24.7 - - '@wordpress/priority-queue@2.58.0': - dependencies: - '@babel/runtime': 7.24.7 - requestidlecallback: 0.3.0 - '@wordpress/priority-queue@3.2.0': dependencies: '@babel/runtime': 7.24.7 requestidlecallback: 0.3.0 - '@wordpress/private-apis@0.40.0': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/private-apis@1.2.0': dependencies: '@babel/runtime': 7.24.7 - '@wordpress/react-i18n@3.56.0': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/element': 5.35.0 - '@wordpress/i18n': 4.58.0 - utility-types: 3.11.0 - - '@wordpress/redux-routine@4.58.0(redux@4.2.1)': - dependencies: - '@babel/runtime': 7.24.7 - is-plain-object: 5.0.0 - is-promise: 4.0.0 - redux: 4.2.1 - rungen: 0.3.2 - '@wordpress/redux-routine@5.2.0(redux@4.2.1)': dependencies: '@babel/runtime': 7.24.7 @@ -20843,20 +20282,6 @@ snapshots: - supports-color - utf-8-validate - '@wordpress/rich-text@6.35.0(react@18.3.1)': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/a11y': 3.58.0 - '@wordpress/compose': 6.35.0(react@18.3.1) - '@wordpress/data': 9.28.0(react@18.3.1) - '@wordpress/deprecated': 3.58.0 - '@wordpress/element': 5.35.0 - '@wordpress/escape-html': 2.58.0 - '@wordpress/i18n': 4.58.0 - '@wordpress/keycodes': 3.58.0 - memize: 2.1.0 - react: 18.3.1 - '@wordpress/rich-text@7.2.0(react@18.3.1)': dependencies: '@babel/runtime': 7.24.7 @@ -20951,11 +20376,6 @@ snapshots: dependencies: '@babel/runtime': 7.24.7 - '@wordpress/undo-manager@0.18.0': - dependencies: - '@babel/runtime': 7.24.7 - '@wordpress/is-shallow-equal': 4.58.0 - '@wordpress/undo-manager@1.2.0': dependencies: '@babel/runtime': 7.24.7 @@ -20974,8 +20394,6 @@ snapshots: '@wordpress/element': 6.2.0 react: 18.3.1 - '@wordpress/warning@2.58.0': {} - '@wordpress/warning@3.2.0': {} '@wordpress/widgets@4.2.0(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -21676,8 +21094,6 @@ snapshots: caniuse-lite@1.0.30001640: {} - canvas-confetti@1.9.3: {} - capital-case@1.0.4: dependencies: no-case: 3.0.4 @@ -21821,8 +21237,6 @@ snapshots: cjs-module-lexer@1.3.1: {} - classnames@2.5.1: {} - clean-css@5.3.3: dependencies: source-map: 0.6.1 @@ -23199,8 +22613,6 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 - exenv@1.2.2: {} - exit@0.1.2: {} expand-tilde@1.2.2: @@ -23747,11 +23159,6 @@ snapshots: prop-types: 15.8.1 react: 18.3.1 - gridicons@3.4.2(react@18.3.1): - dependencies: - prop-types: 15.8.1 - react: 18.3.1 - gunzip-maybe@1.4.2: dependencies: browserify-zlib: 0.1.4 @@ -23950,24 +23357,6 @@ snapshots: husky@8.0.3: {} - i18n-calypso@7.0.0(@types/react@18.3.1)(react@18.3.1): - dependencies: - '@automattic/interpolate-components': 1.2.1(@types/react@18.3.1)(react@18.3.1) - '@babel/runtime': 7.24.7 - '@tannin/sprintf': 1.2.0 - '@wordpress/compose': 6.35.0(react@18.3.1) - debug: 4.3.4 - events: 3.3.0 - hash.js: 1.1.7 - lodash: 4.17.21 - lru: 3.1.0 - react: 18.3.1 - tannin: 1.2.0 - use-subscription: 1.6.0(react@18.3.1) - transitivePeerDependencies: - - '@types/react' - - supports-color - iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -25183,10 +24572,6 @@ snapshots: lru-cache@7.18.3: {} - lru@3.1.0: - dependencies: - inherits: 2.0.4 - lz-string@1.5.0: {} magic-string@0.27.0: @@ -25376,8 +24761,6 @@ snapshots: dependencies: fs-monkey: 1.0.6 - memize@1.1.0: {} - memize@2.1.0: {} memoizerific@1.11.3: @@ -26617,14 +26000,6 @@ snapshots: transitivePeerDependencies: - supports-color - react-dom@16.14.0(react@16.14.0): - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - prop-types: 15.8.1 - react: 16.14.0 - scheduler: 0.19.1 - react-dom@18.2.0(react@18.2.0): dependencies: loose-envify: 1.4.0 @@ -26652,6 +26027,8 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-is: 18.1.0 + react-fast-compare@3.2.2: {} + react-is@16.13.1: {} react-is@17.0.2: {} @@ -26660,21 +26037,18 @@ snapshots: react-is@18.3.1: {} - react-lifecycles-compat@3.0.4: {} - - react-modal@3.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-page-visibility@7.0.0(react@18.3.1): dependencies: - exenv: 1.2.2 prop-types: 15.8.1 react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-lifecycles-compat: 3.0.4 - warning: 4.0.3 - react-page-visibility@7.0.0(react@18.3.1): + react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - prop-types: 15.8.1 + '@popperjs/core': 2.11.8 react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-fast-compare: 3.2.2 + warning: 4.0.3 react-redux@7.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: @@ -26742,10 +26116,6 @@ snapshots: use-callback-ref: 1.3.2(react@18.3.1) use-sidecar: 1.1.2(react@18.3.1) - react-resize-aware@3.1.3(react@18.3.1): - dependencies: - react: 18.3.1 - react-router-dom@5.3.4(react@18.3.1): dependencies: '@babel/runtime': 7.24.7 @@ -26842,11 +26212,6 @@ snapshots: prop-types: 15.8.1 react: 18.3.1 - react-slider@2.0.5(react@18.3.1): - dependencies: - prop-types: 15.8.1 - react: 18.3.1 - react-style-singleton@2.2.1(@types/react@18.3.1)(react@18.3.1): dependencies: get-nonce: 1.0.1 @@ -26885,12 +26250,6 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react@16.14.0: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - prop-types: 15.8.1 - react@18.2.0: dependencies: loose-envify: 1.4.0 @@ -27267,11 +26626,6 @@ snapshots: dependencies: xmlchars: 2.2.0 - scheduler@0.19.1: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -28314,10 +27668,6 @@ snapshots: react: 18.3.1 tslib: 2.5.0 - use-subscription@1.6.0(react@18.3.1): - dependencies: - react: 18.3.1 - use-sync-external-store@1.2.2(react@18.3.1): dependencies: react: 18.3.1 @@ -28334,8 +27684,6 @@ snapshots: utila@0.4.0: {} - utility-types@3.11.0: {} - utils-merge@1.0.1: {} uuid@8.3.2: {} diff --git a/projects/packages/jetpack-mu-wpcom/package.json b/projects/packages/jetpack-mu-wpcom/package.json index 3f6f041240cf6..273ab7a153eba 100644 --- a/projects/packages/jetpack-mu-wpcom/package.json +++ b/projects/packages/jetpack-mu-wpcom/package.json @@ -49,14 +49,14 @@ }, "dependencies": { "@automattic/calypso-analytics": "1.1.2", + "@automattic/calypso-color-schemes": "3.1.3", "@automattic/color-studio": "2.6.0", - "@automattic/components": "2.1.1", "@automattic/i18n-utils": "1.2.3", "@automattic/jetpack-base-styles": "workspace:*", "@automattic/jetpack-shared-extension-utils": "workspace:*", "@automattic/typography": "1.0.0", "@automattic/page-pattern-modal": "1.1.5", - "@automattic/viewport": "1.0.0", + "@popperjs/core": "^2.11.8", "@preact/signals": "^1.2.2", "@sentry/browser": "7.80.1", "@tanstack/react-query": "^5.15.5", @@ -73,9 +73,11 @@ "@wordpress/plugins": "7.2.0", "@wordpress/url": "4.2.0", "clsx": "2.1.1", + "debug": "4.3.4", "preact": "^10.13.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-popper": "^2.3.0", "redux": "^4.2.1", "redux-saga": "^1.3.0", "swiper": "^8.4.5", diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/keyboard-navigation.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/keyboard-navigation.tsx new file mode 100644 index 0000000000000..7d913d40d3a7f --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/keyboard-navigation.tsx @@ -0,0 +1,61 @@ +/** + * External dependencies + */ +import * as React from 'react'; +/** + * Internal dependencies + */ +import useFocusHandler from '../hooks/use-focus-handler'; +import useFocusTrap from '../hooks/use-focus-trap'; +import useKeydownHandler from '../hooks/use-keydown-handler'; + +interface Props { + onMinimize: () => void; + onDismiss: ( target: string ) => () => void; + onNextStepProgression: () => void; + onPreviousStepProgression: () => void; + tourContainerRef: React.MutableRefObject< null | HTMLElement >; + isMinimized: boolean; +} + +const KeyboardNavigation: React.FunctionComponent< Props > = ( { + onMinimize, + onDismiss, + onNextStepProgression, + onPreviousStepProgression, + tourContainerRef, + isMinimized, +} ) => { + /** + * Expand Tour Nav + */ + function ExpandedTourNav() { + useKeydownHandler( { + onEscape: onMinimize, + onArrowRight: onNextStepProgression, + onArrowLeft: onPreviousStepProgression, + } ); + useFocusTrap( tourContainerRef ); + + return null; + } + + /** + * Minimize Tour Nav + */ + function MinimizedTourNav() { + useKeydownHandler( { onEscape: onDismiss( 'esc-key-minimized' ) } ); + + return null; + } + + const isTourFocused = useFocusHandler( tourContainerRef ); + + if ( ! isTourFocused ) { + return null; + } + + return isMinimized ? : ; +}; + +export default KeyboardNavigation; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-frame.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-frame.tsx new file mode 100644 index 0000000000000..e030a37cdda26 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-frame.tsx @@ -0,0 +1,286 @@ +/** + * External Dependencies + */ +import { useViewportMatch } from '@wordpress/compose'; +import { useEffect, useState, useCallback, useMemo, useRef } from '@wordpress/element'; +import clsx from 'clsx'; +import { usePopper } from 'react-popper'; +/** + * Internal Dependencies + */ +import useStepTracking from '../hooks/use-step-tracking'; +import { classParser } from '../utils'; +import { liveResizeModifier } from '../utils/live-resize-modifier'; +import KeyboardNavigation from './keyboard-navigation'; +import TourKitMinimized from './tour-kit-minimized'; +import Overlay from './tour-kit-overlay'; +import Spotlight from './tour-kit-spotlight'; +import TourKitStep from './tour-kit-step'; +import type { Callback, Config } from '../types'; + +const handleCallback = ( currentStepIndex: number, callback?: Callback ) => { + typeof callback === 'function' && callback( currentStepIndex ); +}; + +interface Props { + config: Config; +} + +const TourKitFrame: React.FunctionComponent< Props > = ( { config } ) => { + const [ currentStepIndex, setCurrentStepIndex ] = useState( 0 ); + const [ initialFocusedElement, setInitialFocusedElement ] = useState< HTMLElement | null >( + null + ); + const [ isMinimized, setIsMinimized ] = useState( config.isMinimized ?? false ); + + const [ popperElement, setPopperElement ] = useState< HTMLElement | null >( null ); + const [ tourReady, setTourReady ] = useState( false ); + const tourContainerRef = useRef( null ); + const isMobile = useViewportMatch( 'mobile', '<' ); + const lastStepIndex = config.steps.length - 1; + const referenceElements = config.steps[ currentStepIndex ].referenceElements; + const referenceElementSelector = + referenceElements?.[ isMobile ? 'mobile' : 'desktop' ] || referenceElements?.desktop; + const referenceElement = referenceElementSelector + ? document.querySelector< HTMLElement >( referenceElementSelector ) + : null; + + useEffect( () => { + if ( config.isMinimized ) { + setIsMinimized( true ); + } + }, [ config.isMinimized ] ); + + const showArrowIndicator = useCallback( () => { + if ( config.options?.effects?.arrowIndicator === false ) { + return false; + } + + return !! ( referenceElement && ! isMinimized && tourReady ); + }, [ config.options?.effects?.arrowIndicator, isMinimized, referenceElement, tourReady ] ); + + const showSpotlight = useCallback( () => { + if ( ! config.options?.effects?.spotlight ) { + return false; + } + + return ! isMinimized; + }, [ config.options?.effects?.spotlight, isMinimized ] ); + + const showOverlay = useCallback( () => { + if ( showSpotlight() || ! config.options?.effects?.overlay ) { + return false; + } + + return ! isMinimized; + }, [ config.options?.effects?.overlay, isMinimized, showSpotlight ] ); + + const handleDismiss = useCallback( + ( source: string ) => { + return () => { + config.closeHandler( config.steps, currentStepIndex, source ); + }; + }, + [ config, currentStepIndex ] + ); + + const handleNextStepProgression = useCallback( () => { + let newStepIndex = currentStepIndex; + if ( lastStepIndex > currentStepIndex ) { + newStepIndex = currentStepIndex + 1; + setCurrentStepIndex( newStepIndex ); + } + handleCallback( newStepIndex, config.options?.callbacks?.onNextStep ); + }, [ config.options?.callbacks?.onNextStep, currentStepIndex, lastStepIndex ] ); + + const handlePreviousStepProgression = useCallback( () => { + let newStepIndex = currentStepIndex; + if ( currentStepIndex > 0 ) { + newStepIndex = currentStepIndex - 1; + setCurrentStepIndex( newStepIndex ); + } + handleCallback( newStepIndex, config.options?.callbacks?.onPreviousStep ); + }, [ config.options?.callbacks?.onPreviousStep, currentStepIndex ] ); + + const handleGoToStep = useCallback( + ( stepIndex: number ) => { + setCurrentStepIndex( stepIndex ); + handleCallback( stepIndex, config.options?.callbacks?.onGoToStep ); + }, + [ config.options?.callbacks?.onGoToStep ] + ); + + const handleMinimize = useCallback( () => { + setIsMinimized( true ); + handleCallback( currentStepIndex, config.options?.callbacks?.onMinimize ); + }, [ config.options?.callbacks?.onMinimize, currentStepIndex ] ); + + const handleMaximize = useCallback( () => { + setIsMinimized( false ); + handleCallback( currentStepIndex, config.options?.callbacks?.onMaximize ); + }, [ config.options?.callbacks?.onMaximize, currentStepIndex ] ); + + const { + styles: popperStyles, + attributes: popperAttributes, + update: popperUpdate, + } = usePopper( referenceElement, popperElement, { + strategy: 'fixed', + placement: config?.placement ?? 'bottom', + modifiers: [ + { + name: 'preventOverflow', + options: { + rootBoundary: 'document', + padding: 16, // same as the left/margin of the tour frame + }, + }, + { + name: 'arrow', + options: { + padding: 12, + }, + }, + { + name: 'offset', + options: { + offset: [ 0, showArrowIndicator() ? 12 : 10 ], + }, + }, + { + name: 'flip', + options: { + fallbackPlacements: [ 'top', 'left', 'right' ], + }, + }, + useMemo( + () => liveResizeModifier( config.options?.effects?.liveResize ), + [ config.options?.effects?.liveResize ] + ), + ...( config.options?.popperModifiers || [] ), + ], + } ); + + const stepRepositionProps = + ! isMinimized && referenceElement && tourReady + ? { + style: popperStyles?.popper, + ...popperAttributes?.popper, + } + : null; + + const arrowPositionProps = + ! isMinimized && referenceElement && tourReady + ? { + style: popperStyles?.arrow, + ...popperAttributes?.arrow, + } + : null; + + /* + * Focus first interactive element when step renders. + */ + useEffect( () => { + setTimeout( () => initialFocusedElement?.focus() ); + }, [ initialFocusedElement ] ); + + /* + * Fixes issue with Popper misplacing the instance on mount + * See: https://stackoverflow.com/questions/65585859/react-popper-incorrect-position-on-mount + */ + useEffect( () => { + // If no reference element to position step near + if ( ! referenceElement ) { + setTourReady( true ); + return; + } + + setTourReady( false ); + + if ( popperUpdate ) { + popperUpdate() + .then( () => setTourReady( true ) ) + .catch( () => setTourReady( true ) ); + } + }, [ popperUpdate, referenceElement ] ); + + useEffect( () => { + if ( referenceElement && config.options?.effects?.autoScroll ) { + referenceElement.scrollIntoView( config.options.effects.autoScroll ); + } + }, [ config.options?.effects?.autoScroll, referenceElement ] ); + + const classes = clsx( + 'tour-kit-frame', + isMobile ? 'is-mobile' : 'is-desktop', + { 'is-visible': tourReady }, + classParser( config.options?.classNames ) + ); + + useStepTracking( currentStepIndex, config.options?.callbacks?.onStepViewOnce ); + + useEffect( () => { + if ( config.options?.callbacks?.onStepView ) { + handleCallback( currentStepIndex, config.options?.callbacks?.onStepView ); + } + }, [ config.options?.callbacks?.onStepView, currentStepIndex ] ); + + return ( + <> + +
+ { showOverlay() && } + { showSpotlight() && ( + + ) } +
) } + > + { showArrowIndicator() && ( +
) } + /> + ) } + { ! isMinimized ? ( + + ) : ( + + ) } +
+
+ + ); +}; + +export default TourKitFrame; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-minimized.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-minimized.tsx new file mode 100644 index 0000000000000..6831fbf8f6051 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-minimized.tsx @@ -0,0 +1,30 @@ +/** + * Internal Dependencies + */ +import { MinimizedTourRendererProps } from '../types'; +import type { Config } from '../types'; + +interface Props extends MinimizedTourRendererProps { + config: Config; +} + +const TourKitMinimized: React.FunctionComponent< Props > = ( { + config, + steps, + currentStepIndex, + onMaximize, + onDismiss, +} ) => { + return ( +
+ +
+ ); +}; + +export default TourKitMinimized; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-overlay.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-overlay.tsx new file mode 100644 index 0000000000000..3c440bcdc2de7 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-overlay.tsx @@ -0,0 +1,20 @@ +/** + * External Dependencies + */ +import clsx from 'clsx'; + +interface Props { + visible: boolean; +} + +const TourKitOverlay: React.FunctionComponent< Props > = ( { visible } ) => { + return ( +
+ ); +}; + +export default TourKitOverlay; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-spotlight-interactivity.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-spotlight-interactivity.tsx new file mode 100644 index 0000000000000..61b7e94bca203 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-spotlight-interactivity.tsx @@ -0,0 +1,38 @@ +import { SPOTLIT_ELEMENT_CLASS } from './tour-kit-spotlight'; + +export interface SpotlightInteractivityConfiguration { + /** If true, the user will be allowed to interact with the spotlit element. Defaults to false. */ + enabled?: boolean; + /** + * This element is the root element within which all children will have + * pointer-events disabled during the tour. Defaults to '#wpwrap' + */ + rootElementSelector?: string; +} + +export const SpotlightInteractivity: React.VFC< SpotlightInteractivityConfiguration > = ( { + enabled = false, + rootElementSelector = '#wpwrap', +}: SpotlightInteractivityConfiguration ) => { + if ( ! enabled ) { + return null; + } + return ( + + ); +}; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-spotlight.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-spotlight.tsx new file mode 100644 index 0000000000000..81e9ed80ff131 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-spotlight.tsx @@ -0,0 +1,122 @@ +import { useMemo, useState, useEffect } from '@wordpress/element'; +import clsx from 'clsx'; +import { usePopper } from 'react-popper'; +import { LiveResizeConfiguration, liveResizeModifier } from '../utils/live-resize-modifier'; +import Overlay from './tour-kit-overlay'; +import { + SpotlightInteractivity, + SpotlightInteractivityConfiguration, +} from './tour-kit-spotlight-interactivity'; +import type { Rect, Placement } from '@popperjs/core'; + +export const SPOTLIT_ELEMENT_CLASS = 'wp-tour-kit-spotlit'; +interface Props { + referenceElement: HTMLElement | null; + styles?: React.CSSProperties; + interactivity?: SpotlightInteractivityConfiguration; + liveResize?: LiveResizeConfiguration; +} + +const TourKitSpotlight: React.FunctionComponent< Props > = ( { + referenceElement, + styles, + interactivity, + liveResize, +} ) => { + const [ popperElement, sePopperElement ] = useState< HTMLElement | null >( null ); + const referenceRect = referenceElement?.getBoundingClientRect(); + + const modifiers = [ + { + name: 'flip', + enabled: false, + }, + { + name: 'preventOverflow', + options: { + mainAxis: false, // true by default + }, + }, + useMemo( + () => ( { + name: 'offset', + options: { + offset: ( { + placement, + reference, + popper, + }: { + placement: Placement; + reference: Rect; + popper: Rect; + } ): [ number, number ] => { + if ( placement === 'bottom' ) { + return [ 0, -( reference.height + ( popper.height - reference.height ) / 2 ) ]; + } + return [ 0, 0 ]; + }, + }, + } ), + [] + ), + // useMemo because https://popper.js.org/react-popper/v2/faq/#why-i-get-render-loop-whenever-i-put-a-function-inside-the-popper-configuration + useMemo( () => { + return liveResizeModifier( liveResize ); + }, [ liveResize ] ), + ]; + + const { styles: popperStyles, attributes: popperAttributes } = usePopper( + referenceElement, + popperElement, + { + strategy: 'fixed', + placement: 'bottom', + modifiers, + } + ); + + const clipDimensions = referenceRect + ? { + width: `${ referenceRect.width }px`, + height: `${ referenceRect.height }px`, + } + : null; + + const clipRepositionProps = referenceElement + ? { + style: { + ...( clipDimensions && clipDimensions ), + ...popperStyles?.popper, + ...( styles && styles ), + }, + ...popperAttributes?.popper, + } + : null; + + /** + * Add a .wp-spotlit class to the referenced element so that we can + * apply CSS styles to it, for whatever purposes such as interactivity + */ + useEffect( () => { + referenceElement?.classList.add( SPOTLIT_ELEMENT_CLASS ); + return () => { + referenceElement?.classList.remove( SPOTLIT_ELEMENT_CLASS ); + }; + }, [ referenceElement ] ); + + return ( + <> + + +
) } + /> + + ); +}; + +export default TourKitSpotlight; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-step.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-step.tsx new file mode 100644 index 0000000000000..ded21b229ed03 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-step.tsx @@ -0,0 +1,52 @@ +/** + * External Dependencies + */ +import { useViewportMatch } from '@wordpress/compose'; +import clsx from 'clsx'; +/** + * Internal Dependencies + */ +import { classParser } from '../utils'; +import type { Config, TourStepRendererProps } from '../types'; + +interface Props extends TourStepRendererProps { + config: Config; +} + +const TourKitStep: React.FunctionComponent< Props > = ( { + config, + steps, + currentStepIndex, + onMinimize, + onDismiss, + onNextStep, + onPreviousStep, + setInitialFocusedElement, + onGoToStep, +} ) => { + const isMobile = useViewportMatch( 'mobile', '<' ); + const classes = clsx( + 'tour-kit-step', + `is-step-${ currentStepIndex }`, + classParser( + config.steps[ currentStepIndex ].options?.classNames?.[ isMobile ? 'mobile' : 'desktop' ] + ) + ); + + return ( +
+ +
+ ); +}; + +export default TourKitStep; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit.tsx new file mode 100644 index 0000000000000..97d0fea0f31ec --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit.tsx @@ -0,0 +1,40 @@ +import { createPortal, useEffect, useRef } from '@wordpress/element'; +import React from 'react'; +import { TourKitContextProvider } from '../contexts'; +import ErrorBoundary from '../error-boundary'; +import TourKitFrame from './tour-kit-frame'; +import type { Config } from '../types'; + +import '../styles.scss'; + +interface Props { + config: Config; + __temp__className?: string; +} + +const TourKit: React.FunctionComponent< Props > = ( { config, __temp__className } ) => { + const portalParent = useRef( document.createElement( 'div' ) ).current; + + useEffect( () => { + const classes = [ 'tour-kit', ...( __temp__className ? [ __temp__className ] : [] ) ]; + + portalParent.classList.add( ...classes ); + + const portalParentElement = config.options?.portalParentElement || document.body; + portalParentElement.appendChild( portalParent ); + + return () => { + portalParentElement.removeChild( portalParent ); + }; + }, [ __temp__className, portalParent, config.options?.portalParentElement ] ); + + return ( + + +
{ createPortal( , portalParent ) }
+
+
+ ); +}; + +export default TourKit; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/constants.ts b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/constants.ts new file mode 100644 index 0000000000000..b8522601535a0 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/constants.ts @@ -0,0 +1,3 @@ +// Copied from https://github.com/Automattic/wp-calypso/blob/ce1a376af4bcc8987855ced4c961efacda6e1d32/packages/onboarding/src/utils/flows.ts#L32-L33 +export const START_WRITING_FLOW = 'start-writing'; +export const DESIGN_FIRST_FLOW = 'design-first'; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/contexts/has-seen-seller-celebration-modal-context.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/contexts/has-seen-seller-celebration-modal-context.tsx new file mode 100644 index 0000000000000..94bc6c49e7e13 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/contexts/has-seen-seller-celebration-modal-context.tsx @@ -0,0 +1,68 @@ +import apiFetch from '@wordpress/api-fetch'; +import { useState, useEffect } from '@wordpress/element'; +import * as React from 'react'; +import { useContext } from 'react'; + +type HasSeenSCModalResult = { + has_seen_seller_celebration_modal: boolean; +}; + +type HasSeenSellerCelebrationModalContextType = { + hasSeenSellerCelebrationModal: boolean; + updateHasSeenSellerCelebrationModal: ( value: boolean ) => void; +}; + +const HasSeenSCModalContext = React.createContext< HasSeenSellerCelebrationModalContextType >( { + hasSeenSellerCelebrationModal: false, + // eslint-disable-next-line @typescript-eslint/no-empty-function + updateHasSeenSellerCelebrationModal: () => {}, +} ); + +export const useHasSeenSellerCelebrationModal = () => { + return useContext( HasSeenSCModalContext ); +}; + +export const HasSeenSellerCelebrationModalProvider: React.FC< { children: JSX.Element } > = + function ( { children } ) { + const [ hasSeenSellerCelebrationModal, setHasSeenSellerCelebrationModal ] = useState( false ); + + /** + * Fetch the value that whether the video celebration modal has been seen. + */ + function fetchHasSeenSellerCelebrationModal() { + apiFetch< HasSeenSCModalResult >( { + path: '/wpcom/v2/block-editor/has-seen-seller-celebration-modal', + } ) + .then( ( result: HasSeenSCModalResult ) => + setHasSeenSellerCelebrationModal( result.has_seen_seller_celebration_modal ) + ) + .catch( () => setHasSeenSellerCelebrationModal( false ) ); + } + + /** + * Update the value that whether the video celebration modal has been seen. + * + * @param value - The value to update. + */ + function updateHasSeenSellerCelebrationModal( value: boolean ) { + apiFetch( { + method: 'PUT', + path: '/wpcom/v2/block-editor/has-seen-seller-celebration-modal', + data: { has_seen_seller_celebration_modal: value }, + } ).finally( () => { + setHasSeenSellerCelebrationModal( true ); + } ); + } + + useEffect( () => { + fetchHasSeenSellerCelebrationModal(); + }, [] ); + + return ( + + { children } + + ); + }; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/contexts/has-seen-video-celebration-modal-context.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/contexts/has-seen-video-celebration-modal-context.tsx new file mode 100644 index 0000000000000..18b9345434ec6 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/contexts/has-seen-video-celebration-modal-context.tsx @@ -0,0 +1,68 @@ +import apiFetch from '@wordpress/api-fetch'; +import { useState, useEffect } from '@wordpress/element'; +import * as React from 'react'; +import { useContext } from 'react'; + +type HasSeenVCModalResult = { + has_seen_video_celebration_modal: boolean; +}; + +type HasSeenVideoCelebrationModalContextType = { + hasSeenVideoCelebrationModal: boolean; + updateHasSeenVideoCelebrationModal: ( value: boolean ) => void; +}; + +const HasSeenVCModalContext = React.createContext< HasSeenVideoCelebrationModalContextType >( { + hasSeenVideoCelebrationModal: false, + // eslint-disable-next-line @typescript-eslint/no-empty-function + updateHasSeenVideoCelebrationModal: () => {}, +} ); + +export const useHasSeenVideoCelebrationModal = () => { + return useContext( HasSeenVCModalContext ); +}; + +export const HasSeenVideoCelebrationModalProvider: React.FC< { children: JSX.Element } > = + function ( { children } ) { + const [ hasSeenVideoCelebrationModal, setHasSeenVideoCelebrationModal ] = useState( false ); + + useEffect( () => { + fetchHasSeenVideoCelebrationModal(); + }, [] ); + + /** + * Fetch the value that whether the video celebration modal has been seen. + */ + function fetchHasSeenVideoCelebrationModal() { + apiFetch< HasSeenVCModalResult >( { + path: '/wpcom/v2/block-editor/has-seen-video-celebration-modal', + } ) + .then( ( result: HasSeenVCModalResult ) => + setHasSeenVideoCelebrationModal( result.has_seen_video_celebration_modal ) + ) + .catch( () => setHasSeenVideoCelebrationModal( false ) ); + } + + /** + * Update the value that whether the video celebration modal has been seen. + * + * @param value - The value to update. + */ + function updateHasSeenVideoCelebrationModal( value: boolean ) { + apiFetch( { + method: 'PUT', + path: '/wpcom/v2/block-editor/has-seen-video-celebration-modal', + data: { has_seen_video_celebration_modal: value }, + } ).finally( () => { + setHasSeenVideoCelebrationModal( value ); + } ); + } + + return ( + + { children } + + ); + }; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/contexts/index.ts b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/contexts/index.ts new file mode 100644 index 0000000000000..21d21c340259d --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/contexts/index.ts @@ -0,0 +1,4 @@ +export * from './has-seen-seller-celebration-modal-context'; +export * from './has-seen-video-celebration-modal-context'; +export * from './should-show-first-post-published-modal-context'; +export * from './tour-kit-context'; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/contexts/should-show-first-post-published-modal-context.jsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/contexts/should-show-first-post-published-modal-context.jsx new file mode 100644 index 0000000000000..d9b5b30e8ee0f --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/contexts/should-show-first-post-published-modal-context.jsx @@ -0,0 +1,31 @@ +import { useSelect, useDispatch } from '@wordpress/data'; +import { useEffect } from '@wordpress/element'; +import * as React from 'react'; +import { useContext } from 'react'; + +const ShouldShowFPPModalContext = React.createContext( false ); + +export const useShouldShowFirstPostPublishedModal = () => { + return useContext( ShouldShowFPPModalContext ); +}; + +export const ShouldShowFirstPostPublishedModalProvider = ( { children } ) => { + const value = useSelect( + select => select( 'automattic/wpcom-welcome-guide' ).getShouldShowFirstPostPublishedModal(), + [] + ); + + const { fetchShouldShowFirstPostPublishedModal } = useDispatch( + 'automattic/wpcom-welcome-guide' + ); + + useEffect( () => { + fetchShouldShowFirstPostPublishedModal(); + }, [ fetchShouldShowFirstPostPublishedModal ] ); + + return ( + + { children } + + ); +}; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/contexts/tour-kit-context.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/contexts/tour-kit-context.tsx new file mode 100644 index 0000000000000..392911e574750 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/contexts/tour-kit-context.tsx @@ -0,0 +1,16 @@ +import { createContext, useContext } from '@wordpress/element'; +import type { Config } from '../types'; + +interface TourKitContext { + config: Config; +} + +const TourKitContext = createContext< TourKitContext >( {} as TourKitContext ); + +export const TourKitContextProvider: React.FunctionComponent< + TourKitContext & { children?: React.ReactNode } +> = ( { config, children } ) => { + return { children }; +}; + +export const useTourKitContext = (): TourKitContext => useContext( TourKitContext ); diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/error-boundary.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/error-boundary.tsx new file mode 100644 index 0000000000000..250ce916b1496 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/error-boundary.tsx @@ -0,0 +1,33 @@ +import React, { ErrorInfo } from 'react'; + +type State = { + hasError: boolean; +}; + +class ErrorBoundary extends React.Component< { children: React.ReactNode }, State > { + state = { + hasError: false, + }; + + static getDerivedStateFromError() { + // Update state so the next render will show the fallback UI. + return { hasError: true }; + } + + componentDidCatch( error: Error, errorInfo: ErrorInfo ) { + // You can also log the error to an error reporting service + // eslint-disable-next-line no-console + console.error( error, errorInfo ); + } + + render() { + if ( this.state.hasError ) { + // You can render any custom fallback UI + return

Something went wrong.

; + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/index.ts b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/index.ts new file mode 100644 index 0000000000000..7c2fc8e6c2b2b --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/index.ts @@ -0,0 +1,9 @@ +export { default as useFocusHandle } from './use-focus-handler'; +export { default as useFocusTrap } from './use-focus-trap'; +export { default as useHasSelectedPaymentBlockOnce } from './use-has-selected-payment-block-once'; +export { default as useKeydownHandler } from './use-keydown-handler'; +export { default as useShouldShowSellerCelebrationModal } from './use-should-show-seller-celebration-modal'; +export { default as useShouldShowVideoCelebrationModal } from './use-should-show-video-celebration-modal'; +export { default as useSiteIntent } from './use-site-intent'; +export { default as useSitePlan } from './use-site-plan'; +export { default as useStepTracking } from './use-step-tracking'; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-focus-handler.ts b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-focus-handler.ts new file mode 100644 index 0000000000000..7e54ac45673dc --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-focus-handler.ts @@ -0,0 +1,60 @@ +/** + * External Dependencies + */ +import { useEffect, useCallback, useState } from '@wordpress/element'; + +/** + * A hook that returns true/false if ref node receives focus by either tabbing or clicking into any of its children. + * @param ref - React.MutableRefObject< null | HTMLElement > + */ +const useFocusHandler = ( ref: React.MutableRefObject< null | HTMLElement > ): boolean => { + const [ hasFocus, setHasFocus ] = useState( false ); + + const handleFocus = useCallback( () => { + if ( document.hasFocus() && ref.current?.contains( document.activeElement ) ) { + setHasFocus( true ); + } else { + setHasFocus( false ); + } + }, [ ref ] ); + + const handleMousedown = useCallback( + ( event: MouseEvent ) => { + if ( ref.current?.contains( event.target as Node ) ) { + setHasFocus( true ); + } else { + setHasFocus( false ); + } + }, + [ ref ] + ); + + const handleKeyup = useCallback( + ( event: KeyboardEvent ) => { + if ( event.key === 'Tab' ) { + if ( ref.current?.contains( event.target as Node ) ) { + setHasFocus( true ); + } else { + setHasFocus( false ); + } + } + }, + [ ref ] + ); + + useEffect( () => { + document.addEventListener( 'focusin', handleFocus ); + document.addEventListener( 'mousedown', handleMousedown ); + document.addEventListener( 'keyup', handleKeyup ); + + return () => { + document.removeEventListener( 'focusin', handleFocus ); + document.removeEventListener( 'mousedown', handleMousedown ); + document.removeEventListener( 'keyup', handleKeyup ); + }; + }, [ ref, handleFocus, handleKeyup, handleMousedown ] ); + + return hasFocus; +}; + +export default useFocusHandler; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-focus-trap.ts b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-focus-trap.ts new file mode 100644 index 0000000000000..b8e0bc7b313a9 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-focus-trap.ts @@ -0,0 +1,56 @@ +/** + * External Dependencies + */ +import { focus } from '@wordpress/dom'; +import { useEffect, useCallback, useState } from '@wordpress/element'; +/** + * A hook that constraints tabbing/focus on focuable elements in the given element ref. + * @param ref - React.MutableRefObject< null | HTMLElement > + */ +const useFocusTrap = ( ref: React.MutableRefObject< null | HTMLElement > ): void => { + const [ firstFocusableElement, setFirstFocusableElement ] = useState< HTMLElement | undefined >(); + const [ lastFocusableElement, setLastFocusableElement ] = useState< HTMLElement | undefined >(); + + const handleTrapFocus = useCallback( + ( event: KeyboardEvent ) => { + let handled = false; + + if ( event.key === 'Tab' ) { + if ( event.shiftKey ) { + // Shift + Tab + if ( document.activeElement === firstFocusableElement ) { + lastFocusableElement?.focus(); + handled = true; + } + } else if ( document.activeElement === lastFocusableElement ) { + // Tab + firstFocusableElement?.focus(); + handled = true; + } + } + + if ( handled ) { + event.preventDefault(); + event.stopPropagation(); + } + }, + [ firstFocusableElement, lastFocusableElement ] + ); + + useEffect( () => { + const focusableElements = ref.current ? focus.focusable.find( ref.current as HTMLElement ) : []; + + if ( focusableElements && focusableElements.length ) { + setFirstFocusableElement( focusableElements[ 0 ] as HTMLElement ); + setLastFocusableElement( focusableElements[ focusableElements.length - 1 ] as HTMLElement ); + } + + document.addEventListener( 'keydown', handleTrapFocus ); + + return () => { + document.removeEventListener( 'keydown', handleTrapFocus ); + }; + }, [ ref, handleTrapFocus ] ); +}; + +export default useFocusTrap; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-has-selected-payment-block-once.js b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-has-selected-payment-block-once.js new file mode 100644 index 0000000000000..f8060b9a2d232 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-has-selected-payment-block-once.js @@ -0,0 +1,62 @@ +import { useSelect } from '@wordpress/data'; +import { useState, useEffect } from '@wordpress/element'; + +/** + * Watch the user's block selection and keep a note if they ever select a payments + * block. + * A payments block is any block with 'payments' in the name, like jetpack/simple-payments + * or jetpack/recurring-payments. + * Selecting a block whose direct parent has 'payments' in the name also counts. + * This is to account for clicking inside the button in a payments block, for example. + * @returns {boolean} Has the user selected a payments block (or a direct descendant) at least once? + */ +const useHasSelectedPaymentBlockOnce = () => { + const [ hasSelectedPaymentsOnce, setHasSelectedPaymentsOnce ] = useState( false ); + + // Get the name of the currently selected block + const selectedBlockName = useSelect( select => { + // Special case: We know we're returning true, so we don't need to find block names. + if ( hasSelectedPaymentsOnce ) { + return ''; + } + + const selectedBlock = select( 'core/block-editor' ).getSelectedBlock(); + return selectedBlock?.name ?? ''; + } ); + + // Get the name of the currently selected block's direct parent, if one exists + const parentSelectedBlockName = useSelect( select => { + // Special case: We know we're returning true, so we don't need to find block names. + if ( hasSelectedPaymentsOnce ) { + return ''; + } + + const selectedBlock = select( 'core/block-editor' ).getSelectedBlock(); + if ( selectedBlock?.clientId ) { + const parentIds = select( 'core/block-editor' ).getBlockParents( selectedBlock?.clientId ); + if ( parentIds && parentIds.length ) { + const parent = select( 'core/block-editor' ).getBlock( parentIds[ parentIds.length - 1 ] ); + return parent?.name ?? ''; + } + } + return ''; + } ); + + // On selection change, set hasSelectedPaymentsOnce=true if block name or parent's block name contains 'payments' + useEffect( () => { + if ( + ! hasSelectedPaymentsOnce && + ( selectedBlockName.includes( 'payments' ) || parentSelectedBlockName.includes( 'payments' ) ) + ) { + setHasSelectedPaymentsOnce( true ); + } + }, [ + selectedBlockName, + parentSelectedBlockName, + hasSelectedPaymentsOnce, + setHasSelectedPaymentsOnce, + ] ); + + return hasSelectedPaymentsOnce; +}; +export default useHasSelectedPaymentBlockOnce; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-keydown-handler.ts b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-keydown-handler.ts new file mode 100644 index 0000000000000..6ea129fe08f2e --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-keydown-handler.ts @@ -0,0 +1,52 @@ +/* eslint-disable jsdoc/require-param */ +/** + * External Dependencies + */ +import { useEffect, useCallback } from '@wordpress/element'; + +interface Props { + onEscape?: () => void; + onArrowRight?: () => void; + onArrowLeft?: () => void; +} + +/** + * A hook the applies the respective callbacks in response to keydown events. + */ +const useKeydownHandler = ( { onEscape, onArrowRight, onArrowLeft }: Props ): void => { + const handleKeydown = useCallback( + ( event: KeyboardEvent ) => { + let handled = false; + + switch ( event.key ) { + case 'Escape': + onEscape && ( onEscape(), ( handled = true ) ); + break; + case 'ArrowRight': + onArrowRight && ( onArrowRight(), ( handled = true ) ); + break; + case 'ArrowLeft': + onArrowLeft && ( onArrowLeft(), ( handled = true ) ); + break; + default: + break; + } + + if ( handled ) { + event.preventDefault(); + event.stopPropagation(); + } + }, + [ onEscape, onArrowRight, onArrowLeft ] + ); + + useEffect( () => { + document.addEventListener( 'keydown', handleKeydown ); + + return () => { + document.removeEventListener( 'keydown', handleKeydown ); + }; + }, [ handleKeydown ] ); +}; + +export default useKeydownHandler; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-should-show-seller-celebration-modal.js b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-should-show-seller-celebration-modal.js new file mode 100644 index 0000000000000..80f37b3b530cf --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-should-show-seller-celebration-modal.js @@ -0,0 +1,67 @@ +import { useSelect } from '@wordpress/data'; +import { useState, useEffect } from '@wordpress/element'; +import { useHasSeenSellerCelebrationModal } from '../contexts/has-seen-seller-celebration-modal-context'; +import useHasSelectedPaymentBlockOnce from './use-has-selected-payment-block-once'; +import useSiteIntent from './use-site-intent'; + +const useShouldShowSellerCelebrationModal = () => { + const [ shouldShowSellerCelebrationModal, setShouldShowSellerCelebrationModal ] = + useState( false ); + + const { siteIntent: intent } = useSiteIntent(); + const hasSelectedPaymentsOnce = useHasSelectedPaymentBlockOnce(); + + const { hasSeenSellerCelebrationModal } = useHasSeenSellerCelebrationModal(); + + const hasPaymentsBlock = useSelect( select => { + const isSiteEditor = !! select( 'core/edit-site' ); + + if ( isSiteEditor ) { + const page = select( 'core/edit-site' ).getPage(); + const pageId = parseInt( page?.context?.postId ); + const pageEntity = select( 'core' ).getEntityRecord( 'postType', 'page', pageId ); + + let paymentsBlock = false; + // Only check for payment blocks if we haven't seen the celebration modal text yet + if ( ! hasSeenSellerCelebrationModal ) { + const didCountRecurringPayments = + select( 'core/block-editor' ).getGlobalBlockCount( 'jetpack/recurring-payments' ) > 0; + const didCountSimplePayments = + select( 'core/block-editor' ).getGlobalBlockCount( 'jetpack/simple-payments' ) > 0; + paymentsBlock = + ( pageEntity?.content?.raw?.includes( '' ) || + pageEntity?.content?.raw?.includes( '' ) || + didCountRecurringPayments || + didCountSimplePayments ) ?? + false; + } + + return paymentsBlock; + } + + let paymentBlockCount = 0; + // Only check for payment blocks if we haven't seen the celebration modal yet + if ( ! hasSeenSellerCelebrationModal ) { + paymentBlockCount += select( 'core/block-editor' ).getGlobalBlockCount( + 'jetpack/recurring-payments' + ); + paymentBlockCount += + select( 'core/block-editor' ).getGlobalBlockCount( 'jetpack/simple-payments' ); + } + + return paymentBlockCount > 0; + } ); + + useEffect( () => { + if ( + intent === 'sell' && + hasPaymentsBlock && + hasSelectedPaymentsOnce && + ! hasSeenSellerCelebrationModal + ) { + setShouldShowSellerCelebrationModal( true ); + } + }, [ intent, hasPaymentsBlock, hasSelectedPaymentsOnce, hasSeenSellerCelebrationModal ] ); + return shouldShowSellerCelebrationModal; +}; +export default useShouldShowSellerCelebrationModal; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-should-show-video-celebration-modal.ts b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-should-show-video-celebration-modal.ts new file mode 100644 index 0000000000000..ebbc5ce2ce1d2 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-should-show-video-celebration-modal.ts @@ -0,0 +1,51 @@ +import { useState, useEffect } from '@wordpress/element'; +import request from 'wpcom-proxy-request'; +import { useHasSeenVideoCelebrationModal } from '../contexts/has-seen-video-celebration-modal-context'; +import useSiteIntent from './use-site-intent'; + +declare global { + interface Window { + _currentSiteId: number; + _currentSiteType: string; + } +} + +interface Site { + options?: { + launchpad_checklist_tasks_statuses?: { + video_uploaded: boolean; + }; + }; +} +const useShouldShowVideoCelebrationModal = ( isEditorSaving: boolean ) => { + const { siteIntent: intent } = useSiteIntent(); + + const [ shouldShowVideoCelebrationModal, setShouldShowVideoCelebrationModal ] = useState( false ); + const { hasSeenVideoCelebrationModal } = useHasSeenVideoCelebrationModal(); + + useEffect( () => { + const maybeRenderVideoCelebrationModal = async () => { + // Get latest site options since the video may have just been uploaded. + const siteObj = ( await request( { + path: `/sites/${ window._currentSiteId }?http_envelope=1`, + apiVersion: '1.1', + } ) ) as Site; + + if ( siteObj?.options?.launchpad_checklist_tasks_statuses?.video_uploaded ) { + setShouldShowVideoCelebrationModal( true ); + } + }; + + if ( 'videopress' === intent && ! hasSeenVideoCelebrationModal ) { + maybeRenderVideoCelebrationModal(); + } else if ( hasSeenVideoCelebrationModal ) { + setShouldShowVideoCelebrationModal( false ); + } + }, [ + isEditorSaving, // included so that we check whether the video has been uploaded on save. + intent, + hasSeenVideoCelebrationModal, + ] ); + return shouldShowVideoCelebrationModal; +}; +export default useShouldShowVideoCelebrationModal; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-site-intent.js b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-site-intent.js new file mode 100644 index 0000000000000..496ee2bc23bbd --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-site-intent.js @@ -0,0 +1,14 @@ +// FIXME: We can use `useSiteIntent` from `@automattic/data-stores` and remove this. +// https://github.com/Automattic/wp-calypso/pull/73565#discussion_r1113839120 +const useSiteIntent = () => { + // We can skip the request altogether since this information is already added to the window in + // https://github.com/Automattic/jetpack/blob/e135711f9a130946dae1bca6c9c0967350331067/projects/plugins/jetpack/extensions/plugins/launchpad-save-modal/launchpad-save-modal.php#LL31C8-L31C34 + // We could update this to use the launchpad endpoint in jetpack-mu-wpcom, but that may require + // permissions changes as it requires 'manage_options' to read + // https://github.com/Automattic/jetpack/blob/e135711f9a130946dae1bca6c9c0967350331067/projects/packages/jetpack-mu-wpcom/src/features/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-launchpad.php#L121. + return { + siteIntent: window.Jetpack_LaunchpadSaveModal?.siteIntentOption, + siteIntentFetched: true, + }; +}; +export default useSiteIntent; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-site-plan.js b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-site-plan.js new file mode 100644 index 0000000000000..d3c64400da453 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-site-plan.js @@ -0,0 +1,24 @@ +import { useState, useEffect } from '@wordpress/element'; +import { useCallback } from 'react'; +import request from 'wpcom-proxy-request'; + +const useSitePlan = siteIdOrSlug => { + const [ sitePlan, setSitePlan ] = useState( {} ); + + const fetchSite = useCallback( async () => { + const siteObj = await request( { + path: `/sites/${ siteIdOrSlug }?http_envelope=1`, + apiVersion: '1.1', + } ); + if ( siteObj?.plan ) { + setSitePlan( siteObj.plan ); + } + }, [ siteIdOrSlug ] ); + + useEffect( () => { + fetchSite(); + }, [ fetchSite ] ); + + return sitePlan; +}; +export default useSitePlan; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-step-tracking.ts b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-step-tracking.ts new file mode 100644 index 0000000000000..a1e231ace668c --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-step-tracking.ts @@ -0,0 +1,26 @@ +/** + * External Dependencies + */ +import { useState, useEffect } from '@wordpress/element'; +/** + * Internal Dependencies + */ +import { Callback } from '../types'; + +const useStepTracking = ( + currentStepIndex: number, + onStepViewOnce: Callback | undefined +): void => { + const [ stepsViewed, setStepsViewed ] = useState< number[] >( [] ); + + useEffect( () => { + if ( stepsViewed.includes( currentStepIndex ) ) { + return; + } + + setStepsViewed( prev => [ ...prev, currentStepIndex ] ); + onStepViewOnce?.( currentStepIndex ); + }, [ currentStepIndex, onStepViewOnce, stepsViewed ] ); +}; + +export default useStepTracking; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/index.ts b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/index.ts new file mode 100644 index 0000000000000..2cf54b34efa8a --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/index.ts @@ -0,0 +1,6 @@ +export { default } from './components/tour-kit'; +export * from './constants'; +export { default as WpcomTourKit, usePrefetchTourAssets } from './variants/wpcom'; +export * from './contexts'; +export * from './hooks'; +export * from './types'; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/styles.scss b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/styles.scss new file mode 100644 index 0000000000000..ef38a0c05b1e1 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/styles.scss @@ -0,0 +1,91 @@ +.tour-kit-frame { + visibility: hidden; + + &.is-visible { + visibility: visible; + } +} + +.tour-kit-frame__container { + border-radius: 2px; + bottom: 44px; + display: inline; + left: 16px; + position: fixed; + z-index: 9999; + // Avoid the text cursor when the text is not selectable + cursor: default; + box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.25); + background: var(--studio-white); +} + +.tour-kit-overlay { + position: fixed; + width: 100vw; + height: 100vh; + top: 0; + left: 0; + background: rgba(0, 0, 0); + opacity: 0; + + &.is-visible { + opacity: 0.5; + } +} + +.tour-kit-spotlight { + &.is-visible { + position: fixed; + overflow: hidden; + // box-shadow: 0 0 0 9999px rgba(0, 0, 255, 0.2); + outline: 99999px solid rgba(0, 0, 0, 0.5); + z-index: 1; + } +} + +.tour-kit-frame__arrow { + visibility: hidden; +} + +.tour-kit-frame__arrow, +.tour-kit-frame__arrow::before { + position: absolute; + width: 12px; + height: 12px; + background: inherit; + z-index: -1; +} + +.tour-kit-frame__arrow::before { + visibility: visible; + content: ""; + transform: rotate(45deg); +} + +.tour-kit-frame__container[data-popper-placement^="top"] > .tour-kit-frame__arrow { + bottom: -6px; + &::before { + box-shadow: 1px 1px 2px -1px rgb(0 0 0 / 25%); + } +} + +.tour-kit-frame__container[data-popper-placement^="bottom"] > .tour-kit-frame__arrow { + top: -6px; + &::before { + box-shadow: -1px -1px 2px -1px rgb(0 0 0 / 25%); + } +} + +.tour-kit-frame__container[data-popper-placement^="left"] > .tour-kit-frame__arrow { + right: -6px; + &::before { + box-shadow: 1px -1px 2px -1px rgb(0 0 0 / 25%); + } +} + +.tour-kit-frame__container[data-popper-placement^="right"] > .tour-kit-frame__arrow { + left: -6px; + &::before { + box-shadow: -1px 1px 2px -1px rgb(0 0 0 / 25%); + } +} diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/types.ts b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/types.ts new file mode 100644 index 0000000000000..3f4ec5c080a13 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/types.ts @@ -0,0 +1,164 @@ +import * as PopperJS from '@popperjs/core'; +import React from 'react'; +import { SpotlightInteractivityConfiguration } from './components/tour-kit-spotlight-interactivity'; +import { LiveResizeConfiguration } from './utils/live-resize-modifier'; +import type { Modifier } from 'react-popper'; + +export interface Step { + slug?: string; + referenceElements?: { + desktop?: string; + mobile?: string; + }; + meta: { + [ key: string ]: unknown; + // | React.FunctionComponent< Record< string, unknown > > + // | HTMLElement + // | string + // | ... + }; + options?: { + classNames?: { + /** + * `desktop` classes are applied when min-width is larger or equal to 480px. + */ + desktop?: string | string[]; + /** + * `mobile` classes are applied when max-width is smaller than 480px. + */ + mobile?: string | string[]; + }; + }; +} + +export interface TourStepRendererProps { + steps: Step[]; + currentStepIndex: number; + onDismiss: ( source: string ) => () => void; + onNextStep: () => void; + onPreviousStep: () => void; + onMinimize: () => void; + setInitialFocusedElement: React.Dispatch< React.SetStateAction< HTMLElement | null > >; + onGoToStep: ( stepIndex: number ) => void; +} + +export interface MinimizedTourRendererProps { + steps: Step[]; + currentStepIndex: number; + onMaximize: () => void; + onDismiss: ( source: string ) => () => void; +} + +export type TourStepRenderer = React.FunctionComponent< TourStepRendererProps >; +export type MinimizedTourRenderer = React.FunctionComponent< MinimizedTourRendererProps >; +export type Callback = ( currentStepIndex: number ) => void; +export type CloseHandler = ( steps: Step[], currentStepIndex: number, source: string ) => void; +export type PopperModifier = Partial< Modifier< unknown, Record< string, unknown > > >; + +export interface Callbacks { + onMinimize?: Callback; + onMaximize?: Callback; + onGoToStep?: Callback; + onNextStep?: Callback; + onPreviousStep?: Callback; + onStepViewOnce?: Callback; + onStepView?: Callback; +} + +export interface Options { + classNames?: string | string[]; + callbacks?: Callbacks; + /** An object to enable/disable/combine various tour effects, such as spotlight, overlay, and autoscroll */ + effects?: { + /** + * Adds a semi-transparent overlay and highlights the reference element + * when provided with a transparent box over it. The existence of this configuration + * key implies enabling the spotlight effect. + */ + spotlight?: { + /** An object that configures whether the user is allowed to interact with the referenced element during the tour */ + interactivity?: SpotlightInteractivityConfiguration; + /** CSS properties that configures the styles applied to the spotlight overlay */ + styles?: React.CSSProperties; + }; + /** Shows a little triangle that points to the referenced element. Defaults to true */ + arrowIndicator?: boolean; + /** + * Includes the semi-transparent overlay for all the steps Also blocks interactions for everything except the tour dialogues, + * including the referenced elements. Refer to spotlight interactivity configuration to affect this. + * + * Defaults to false, but if spotlight is enabled it implies this is enabled as well. + */ + overlay?: boolean; + /** Configures the autoscroll behaviour. Defaults to False. More information about the configuration at: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView */ + autoScroll?: ScrollIntoViewOptions | boolean; + /** Configures the behaviour for automatically resizing the tour kit elements (TourKitFrame and Spotlight). Defaults to disabled. */ + liveResize?: LiveResizeConfiguration; + }; + popperModifiers?: PopperModifier[]; + portalParentElement?: HTMLElement | null; +} + +export interface Config { + steps: Step[]; + renderers: { + tourStep: TourStepRenderer; + tourMinimized: MinimizedTourRenderer; + }; + closeHandler: CloseHandler; + isMinimized?: boolean; + options?: Options; + placement?: PopperJS.Placement; +} + +export type Tour = React.FunctionComponent< { config: Config } >; + +/************************ + * WPCOM variant types: * + ************************/ + +export type OnTourRateCallback = ( currentStepIndex: number, liked: boolean ) => void; + +export interface WpcomStep extends Step { + meta: { + heading: string | null; + descriptions: { + desktop: string | React.ReactElement | null; + mobile: string | React.ReactElement | null; + }; + imgSrc?: { + desktop?: { + src: string; + type: string; + }; + mobile?: { + src: string; + type: string; + }; + }; + imgLink?: { + href: string; + playable?: boolean; + onClick?: () => void; + }; + }; +} + +export interface WpcomTourStepRendererProps extends TourStepRendererProps { + steps: WpcomStep[]; +} + +export interface WpcomOptions extends Options { + tourRating?: { + enabled: boolean; + useTourRating?: () => 'thumbs-up' | 'thumbs-down' | undefined; + onTourRate?: ( rating: 'thumbs-up' | 'thumbs-down' ) => void; + }; +} + +export interface WpcomConfig extends Omit< Config, 'renderers' > { + steps: WpcomStep[]; + options?: WpcomOptions; +} + +export type WpcomTour = React.FunctionComponent< { config: WpcomConfig } >; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/utils/index.ts b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/utils/index.ts new file mode 100644 index 0000000000000..3f00a820d1703 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/utils/index.ts @@ -0,0 +1,16 @@ +import debugFactory from 'debug'; + +/** + * Helper to convert CSV of `classes` to an array. + * @param classes - String or array of classes to format. + * @returns Array of classes + */ +export function classParser( classes?: string | string[] ): string[] | null { + if ( classes?.length ) { + return classes.toString().split( ',' ); + } + + return null; +} + +export const debug = debugFactory( 'tour-kit' ); diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/utils/live-resize-modifier.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/utils/live-resize-modifier.tsx new file mode 100644 index 0000000000000..ab1a14cf8568b --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/utils/live-resize-modifier.tsx @@ -0,0 +1,111 @@ +import { debug } from '../utils'; +import type { ModifierArguments, Options, State } from '@popperjs/core'; +import type { Modifier } from 'react-popper'; + +// Adds the resizeObserver and mutationObserver properties to the popper effect function argument +type ModifierArgumentsWithObserversProp = ModifierArguments< Options > & { + state: State & { + elements: State[ 'elements' ] & { + reference: State[ 'elements' ][ 'reference' ] & { + [ key: symbol ]: { + resizeObserver: ResizeObserver; + mutationObserver: MutationObserver; + }; + }; + }; + }; +}; + +export interface LiveResizeConfiguration { + /** CSS Selector for the the DOM node (and children) to observe for mutations */ + rootElementSelector?: string; + /** True to enable update on reference element resize, defaults to false */ + resize?: boolean; + /** True to enable update on node and subtree mutation, defaults to false. May be performance intensive */ + mutation?: boolean; +} + +type liveResizeModifierFactory = ( + params: LiveResizeConfiguration | undefined +) => Modifier< 'liveResizeModifier', Record< string, unknown > >; + +/** + * Function that returns a Popper modifier that observes the specified root element as well as + * reference element for any changes. The reason for being a currying function is so that + * we can customise the root element selector, otherwise observing at a higher than necessary + * level might cause unnecessary performance penalties. + * + * The Popper modifier queues an asynchronous update on the Popper instance whenever either of the + * Observers trigger its callback. + * + * @param config - The config. + * @param config.rootElementSelector - The selector of the root element. + * @param config.mutation - Whether to mutate. + * @param config.resize - Whether to resize. + * @returns custom Popper modifier. + */ +export const liveResizeModifier: liveResizeModifierFactory = ( + { rootElementSelector, mutation = false, resize = false }: LiveResizeConfiguration = { + mutation: false, + resize: false, + } +) => ( { + name: 'liveResizeModifier', + enabled: true, + phase: 'main', + fn: () => { + return; + }, + effect: arg0 => { + try { + const { state, instance } = arg0 as ModifierArgumentsWithObserversProp; // augment types here because we are mutating the properties on the argument that is passed in + + const ObserversProp = Symbol(); // use a symbol here so that we don't clash with multiple poppers using this modifier on the same reference node + const { reference } = state.elements; + + reference[ ObserversProp ] = { + resizeObserver: new ResizeObserver( () => { + instance.update(); + } ), + + mutationObserver: new MutationObserver( () => { + instance.update(); + } ), + }; + + if ( resize ) { + if ( reference instanceof Element ) { + reference[ ObserversProp ].resizeObserver.observe( reference ); + } else { + debug( + 'Error: ResizeObserver does not work with virtual elements, Tour Kit will not resize automatically if the size of the referenced element changes.' + ); + } + } + + if ( mutation ) { + const rootElementNode = document.querySelector( rootElementSelector || '#wpwrap' ); + if ( rootElementNode instanceof Element ) { + reference[ ObserversProp ].mutationObserver.observe( rootElementNode, { + attributes: true, + characterData: true, + childList: true, + subtree: true, + } ); + } else { + debug( + `Error: ${ rootElementSelector } selector did not find a valid DOM element, Tour Kit will not update automatically if the DOM layout changes.` + ); + } + } + + return () => { + reference[ ObserversProp ].resizeObserver.disconnect(); + reference[ ObserversProp ].mutationObserver.disconnect(); + delete reference[ ObserversProp ]; + }; + } catch ( error ) { + debug( 'Error: Tour Kit live resize modifier failed unexpectedly:', error ); + } + }, +} ); diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-minimized.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-minimized.tsx new file mode 100644 index 0000000000000..1afd5ce37fca4 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-minimized.tsx @@ -0,0 +1,50 @@ +import { Button, Flex } from '@wordpress/components'; +import { createInterpolateElement } from '@wordpress/element'; +import { sprintf } from '@wordpress/i18n'; +import { Icon, close } from '@wordpress/icons'; +import { useI18n } from '@wordpress/react-i18n'; +import maximize from '../icons/maximize'; +import type { MinimizedTourRendererProps } from '../../../types'; + +const WpcomTourKitMinimized: React.FunctionComponent< MinimizedTourRendererProps > = ( { + steps, + onMaximize, + onDismiss, + currentStepIndex, +} ) => { + const { __ } = useI18n(); + const lastStepIndex = steps.length - 1; + const page = currentStepIndex + 1; + const numberOfPages = lastStepIndex + 1; + + return ( + + + + + ); +}; + +export default WpcomTourKitMinimized; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-pagination-control.scss b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-pagination-control.scss new file mode 100644 index 0000000000000..fe9a19fc4b508 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-pagination-control.scss @@ -0,0 +1,44 @@ +@import "@automattic/calypso-color-schemes"; + +.wpcom-tour-kit-pagination-control { + margin: 0; + display: flex; + justify-content: center; + width: 100%; + + li { + display: inline-flex; + margin: auto 4px; + height: 18px; + align-items: center; + border: none; + } + + li.pagination-control__last-item { + margin-left: auto; + height: 32px; + } + + button.pagination-control__page { + border: none; + padding: 0; + width: 6px; + height: 6px; + border-radius: 50%; + cursor: pointer; + transition: all 0.2s ease-in-out; + background-color: var(--color-neutral-10); + + &:hover { + background-color: var(--color-neutral-40); + } + + &:disabled { + cursor: default; + } + + &.is-current { + background-color: var(--wp-admin-theme-color); + } + } +} diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-pagination-control.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-pagination-control.tsx new file mode 100644 index 0000000000000..a37ce82c23b98 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-pagination-control.tsx @@ -0,0 +1,49 @@ +import { __, sprintf } from '@wordpress/i18n'; +import clsx from 'clsx'; +import './wpcom-tour-kit-pagination-control.scss'; + +interface Props { + onChange: ( page: number ) => void; + activePageIndex: number; + numberOfPages: number; + classNames?: string | string[]; + children?: React.ReactNode; +} + +const WpcomTourKitPaginationControl: React.FunctionComponent< Props > = ( { + activePageIndex, + numberOfPages, + onChange, + classNames, + children, +} ) => { + const classes = clsx( 'wpcom-tour-kit-pagination-control', classNames ); + + return ( +
    + { Array.from( { length: numberOfPages } ).map( ( value, index ) => ( +
  • +
  • + ) ) } + { children &&
  • { children }
  • } +
+ ); +}; + +export default WpcomTourKitPaginationControl; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-rating.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-rating.tsx new file mode 100644 index 0000000000000..82c20f1410019 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-rating.tsx @@ -0,0 +1,73 @@ +import { Button } from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import { useI18n } from '@wordpress/react-i18n'; +import clsx from 'clsx'; +import { useTourKitContext } from '../../../index'; +import thumbsDown from '../icons/thumbs_down'; +import thumbsUp from '../icons/thumbs_up'; +import type { WpcomConfig } from '../../../index'; + +const WpcomTourKitRating: React.FunctionComponent = () => { + const [ tempRating, setTempRating ] = useState< 'thumbs-up' | 'thumbs-down' >(); + const context = useTourKitContext(); + const config = context.config as unknown as WpcomConfig; + const tourRating = config.options?.tourRating?.useTourRating?.() ?? tempRating; + const { __ } = useI18n(); + + let isDisabled = false; + + if ( ! config.options?.tourRating?.enabled ) { + return null; + } + + // check is on tempRating to allow rerating in a restarted tour + if ( ! isDisabled && tempRating !== undefined ) { + isDisabled = true; + } + + const rateTour = ( isThumbsUp: boolean ) => { + if ( isDisabled ) { + return; + } + + const rating = isThumbsUp ? 'thumbs-up' : 'thumbs-down'; + + if ( rating !== tourRating ) { + isDisabled = true; + setTempRating( rating ); + config.options?.tourRating?.onTourRate?.( rating ); + } + }; + + return ( + <> +

+ { __( 'Did you find this guide helpful?', 'jetpack-mu-wpcom' ) } +

+
+
+ + ); +}; + +export default WpcomTourKitRating; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-step-card-navigation.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-step-card-navigation.tsx new file mode 100644 index 0000000000000..e324f0b2a4380 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-step-card-navigation.tsx @@ -0,0 +1,62 @@ +import { Button } from '@wordpress/components'; +import { useI18n } from '@wordpress/react-i18n'; +import WpcomTourKitPaginationControl from './wpcom-tour-kit-pagination-control'; +import type { WpcomTourStepRendererProps } from '../../../types'; + +type Props = Omit< WpcomTourStepRendererProps, 'onMinimize' >; + +const WpcomTourKitStepCardNavigation: React.FunctionComponent< Props > = ( { + currentStepIndex, + onDismiss, + onGoToStep, + onNextStep, + onPreviousStep, + setInitialFocusedElement, + steps, +} ) => { + const { __ } = useI18n(); + const isFirstStep = currentStepIndex === 0; + const lastStepIndex = steps.length - 1; + + return ( + <> + + { isFirstStep ? ( +
+ + +
+ ) : ( +
+ + +
+ ) } +
+ + ); +}; + +export default WpcomTourKitStepCardNavigation; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-step-card-overlay-controls.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-step-card-overlay-controls.tsx new file mode 100644 index 0000000000000..bfad1e0204dc7 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-step-card-overlay-controls.tsx @@ -0,0 +1,41 @@ +import { Button, Flex } from '@wordpress/components'; +import { close } from '@wordpress/icons'; +import { useI18n } from '@wordpress/react-i18n'; +import minimize from '../icons/minimize'; +import type { TourStepRendererProps } from '../../../types'; + +interface Props { + onMinimize: TourStepRendererProps[ 'onMinimize' ]; + onDismiss: TourStepRendererProps[ 'onDismiss' ]; +} + +const WpcomTourKitStepCardOverlayControls: React.FunctionComponent< Props > = ( { + onMinimize, + onDismiss, +} ) => { + const { __ } = useI18n(); + + return ( +
+ + + + +
+ ); +}; + +export default WpcomTourKitStepCardOverlayControls; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-step-card.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-step-card.tsx new file mode 100644 index 0000000000000..743d4f1cce9ac --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-step-card.tsx @@ -0,0 +1,109 @@ +import { Button, Card, CardBody, CardFooter, CardMedia } from '@wordpress/components'; +import { useViewportMatch } from '@wordpress/compose'; +import { Icon } from '@wordpress/icons'; +import { useI18n } from '@wordpress/react-i18n'; +import clsx from 'clsx'; +import WpcomTourKitRating from './wpcom-tour-kit-rating'; +import WpcomTourKitStepCardNavigation from './wpcom-tour-kit-step-card-navigation'; +import WpcomTourKitStepCardOverlayControls from './wpcom-tour-kit-step-card-overlay-controls'; +import type { WpcomTourStepRendererProps } from '../../../types'; + +const WpcomTourKitStepCard: React.FunctionComponent< WpcomTourStepRendererProps > = ( { + steps, + currentStepIndex, + onMinimize, + onDismiss, + onGoToStep, + onNextStep, + onPreviousStep, + setInitialFocusedElement, +} ) => { + const { __ } = useI18n(); + const lastStepIndex = steps.length - 1; + const { descriptions, heading, imgSrc, imgLink } = steps[ currentStepIndex ].meta; + const isLastStep = currentStepIndex === lastStepIndex; + const isMobile = useViewportMatch( 'mobile', '<' ); + const description = descriptions[ isMobile ? 'mobile' : 'desktop' ] ?? descriptions.desktop; + + return ( + + + { imgSrc && ( + + + { imgSrc.mobile && ( + + ) } + { + + { imgLink && ( + + + + + } + size={ 27 } + /> + + ) } + + ) } + +

{ heading }

+

+ { description } + { isLastStep ? ( + + ) : null } +

+
+ + { isLastStep ? ( + + ) : ( + + ) } + +
+ ); +}; + +export default WpcomTourKitStepCard; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-step.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-step.tsx new file mode 100644 index 0000000000000..24064230211d2 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit-step.tsx @@ -0,0 +1,28 @@ +import WpcomTourKitStepCard from './wpcom-tour-kit-step-card'; +import type { WpcomTourStepRendererProps } from '../../../types'; + +const WpcomTourKitStep: React.FunctionComponent< WpcomTourStepRendererProps > = ( { + steps, + currentStepIndex, + onDismiss, + onNextStep, + onPreviousStep, + onMinimize, + setInitialFocusedElement, + onGoToStep, +} ) => { + return ( + + ); +}; + +export default WpcomTourKitStep; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit.tsx new file mode 100644 index 0000000000000..863d20ac6745d --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/components/wpcom-tour-kit.tsx @@ -0,0 +1,29 @@ +import TourKit from '../../../components/tour-kit'; +import usePrefetchTourAssets from '../hooks/use-prefetch-tour-assets'; +import WpcomTourKitMinimized from './wpcom-tour-kit-minimized'; +import WpcomTourKitStep from './wpcom-tour-kit-step'; +import '../styles.scss'; +import type { WpcomConfig, TourStepRenderer } from '../../../types'; + +interface Props { + config: WpcomConfig; +} + +const WpcomTourKit: React.FunctionComponent< Props > = ( { config } ) => { + usePrefetchTourAssets( config.steps ); + + return ( + + ); +}; + +export default WpcomTourKit; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/hooks/use-prefetch-tour-assets.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/hooks/use-prefetch-tour-assets.tsx new file mode 100644 index 0000000000000..76db7335bf3eb --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/hooks/use-prefetch-tour-assets.tsx @@ -0,0 +1,16 @@ +import { useEffect } from '@wordpress/element'; +import type { WpcomStep } from '../../../types'; + +/** + * The hook to prefetch the assets of the tour + * + * @param steps - The steps that require assets. + */ +export default function usePrefetchTourAssets( steps: WpcomStep[] ): void { + useEffect( () => { + steps.forEach( step => { + step.meta.imgSrc?.mobile && ( new window.Image().src = step.meta.imgSrc.mobile.src ); + step.meta.imgSrc?.desktop && ( new window.Image().src = step.meta.imgSrc.desktop.src ); + } ); + }, [ steps ] ); +} diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/icons/maximize.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/icons/maximize.tsx new file mode 100644 index 0000000000000..0d0ee00282e4c --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/icons/maximize.tsx @@ -0,0 +1,13 @@ +import { SVG, Path } from '@wordpress/primitives'; + +const minimize = ( + + + +); + +export default minimize; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/icons/minimize.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/icons/minimize.tsx new file mode 100644 index 0000000000000..e844c2cd1fc64 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/icons/minimize.tsx @@ -0,0 +1,14 @@ +import { SVG, Path } from '@wordpress/primitives'; + +const minimize = ( + + + +); + +export default minimize; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/icons/thumbs_down.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/icons/thumbs_down.tsx new file mode 100644 index 0000000000000..784e6668db965 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/icons/thumbs_down.tsx @@ -0,0 +1,14 @@ +import { SVG, Path } from '@wordpress/primitives'; + +const thumbsDown = ( + + + +); + +export default thumbsDown; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/icons/thumbs_up.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/icons/thumbs_up.tsx new file mode 100644 index 0000000000000..d57b147512eac --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/icons/thumbs_up.tsx @@ -0,0 +1,14 @@ +import { SVG, Path } from '@wordpress/primitives'; + +const thumbsUp = ( + + + +); + +export default thumbsUp; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/index.ts b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/index.ts new file mode 100644 index 0000000000000..f092f4ed3fa54 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/index.ts @@ -0,0 +1,2 @@ +export { default } from './components/wpcom-tour-kit'; +export { default as usePrefetchTourAssets } from './hooks/use-prefetch-tour-assets'; diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/styles.scss b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/styles.scss new file mode 100644 index 0000000000000..56483b6839201 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/variants/wpcom/styles.scss @@ -0,0 +1,232 @@ +@use "sass:math"; +@import "@wordpress/base-styles/colors"; +@import "@wordpress/base-styles/mixins"; +@import "@wordpress/base-styles/variables"; +@import "@wordpress/base-styles/z-index"; + +$wpcom-tour-kit-step-card-overlay-controls-button-bg-color: #32373c; // former $dark-gray-700. TODO: replace with standard color + +.wpcom-tour-kit-minimized { + border-radius: 2px; + box-shadow: + 0 2px 6px rgba(60, 66, 87, 0.08), + 0 0 0 1px rgba(60, 66, 87, 0.16), + 0 1px 1px rgba(0, 0, 0, 0.08); + background-color: $white; + color: $black; + + .components-button { + height: 44px; + + .wpcom-tour-kit-minimized__tour-index { + color: $gray-600; + } + + svg { + color: #50575e; + } + + &:hover { + .wpcom-tour-kit-minimized__tour-index, + svg { + color: inherit; + } + } + } +} + +.wpcom-tour-kit-step-card__heading { + font-size: 1.125rem; /* stylelint-disable-line scales/font-sizes */ + margin: 0.5rem 0; +} + +.wpcom-tour-kit-step-card__description { + font-size: 0.875rem; + /* stylelint-disable-next-line declaration-property-unit-allowed-list */ + line-height: 1.5rem; + margin: 0; + + .components-button { + height: auto; + line-height: 1; + text-decoration: underline; + padding: 0 0 0 4px; + } +} + +// @todo clk - update? +.wpcom-tour-kit .tour-kit-frame__container { + box-shadow: none; +} + +.wpcom-tour-kit-step-card { + width: 416px; + max-width: 92vw; + + &.wpcom-tour-kit-step-card.is-elevated { + box-shadow: rgba(0, 0, 0, 0.1) 0 0 0 1px, rgba(0, 0, 0, 0.1) 0 2px 4px 0; + } + + &.components-card { + border: none; + border-radius: 4px; + box-shadow: none; + } + + .components-card__body { + min-height: 114px; + } + + .components-card__body, + .components-card__footer { + border-top: none; + padding: $grid-unit-20 !important; + } + + .components-card__footer { + .wpcom-tour-kit-rating__end-text { + color: $gray-600; + font-size: 0.875rem; + font-style: italic; + } + + .wpcom-tour-kit-rating__end-icon.components-button.has-icon { + background-color: #f6f7f7; + border-radius: 50%; + color: $gray-600; + margin-left: 8px; + + path { + fill: $gray-600; + } + + &.active { + background-color: $black; + opacity: 1; + + path { + fill: $white; + } + } + } + } + + .components-card__media { + height: 0; + padding-top: math.percentage(math.div(math.ceil(math.div(1, 1.53) * 100), 100)); // img width:height ratio (1:1.53) + position: relative; + width: 100%; + + img { + left: 0; + position: absolute; + top: 0; + width: 100%; + } + } + + .components-guide__page-control { + margin: 0; + + .components-button { + min-width: auto; + &.has-icon { + padding: 3px; + } + } + + li { + margin-bottom: 0; + } + } +} + +.wpcom-tour-kit-step-card-overlay-controls__minimize-icon svg { + position: relative; + left: -2px; +} + +.wpcom-tour-kit-step-card-overlay-controls { + left: 0; + padding: $grid-unit-15; + right: 0; + z-index: 1; // z-index is needed because overlay controls are written before components-card__media, and so ends up under the image + + .components-button { + width: 32px; + min-width: 32px; + height: 32px; + background: $wpcom-tour-kit-step-card-overlay-controls-button-bg-color; + transition: opacity 200ms; + opacity: 0.7; + + &:active { + opacity: 0.9; + } + } + + @media (hover: hover) and (pointer: fine) { + // styles only applicable for hoverable viewports with precision pointing devices connected (eg: mouse) + .components-button { + opacity: 0; + } + + .tour-kit-frame__container:hover &, + .tour-kit-frame__container:focus-within & { + .components-button { + opacity: 0.7; + + &:hover, + &:focus { + opacity: 0.9; + } + } + } + } +} + +.wpcom-tour-kit-step-card-navigation__next-btn { + margin-left: $grid-unit-15; + justify-content: center; + min-width: 85px; +} + +.wpcom-tour-kit-step-card__media { + position: relative; +} + +// TODO: Remove once @wordpress/components/src/card/styles/card-styles.js is updated +.wpcom-tour-kit-step-card__media img { + display: block; + height: auto; + max-width: 100%; + width: 100%; +} + +.wpcom-tour-kit-step-card__media-link { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + + svg { + display: none; + transition: transform 0.15s ease-in-out; + + &:hover { + transform: scale(1.05); + } + } + + &--playable { + background-color: rgba(0, 0, 0, 0.5); + + svg { + display: block; + } + } +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php index c01bbef944677..d720a6a32b52e 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php @@ -7,10 +7,10 @@ namespace Automattic\Jetpack\Jetpack_Mu_Wpcom\NUX; -use Automattic\Jetpack\Jetpack_Mu_Wpcom\Common; - define( 'MU_WPCOM_BLOCK_EDITOR_NUX', true ); +require_once __DIR__ . '/../../utils.php'; + /** * Class WPCOM_Block_Editor_NUX */ @@ -46,38 +46,8 @@ public static function init() { * Enqueue block editor assets. */ public function enqueue_script_and_style() { - $asset_file = include plugin_dir_path( __FILE__ ) . 'dist/wpcom-block-editor-nux.asset.php'; - $script_dependencies = $asset_file['dependencies']; - $version = $asset_file['version']; - - wp_enqueue_script( - 'wpcom-block-editor-nux-script', - plugins_url( 'dist/wpcom-block-editor-nux.min.js', __FILE__ ), - is_array( $script_dependencies ) ? $script_dependencies : array(), - $version, - true - ); - - wp_localize_script( - 'wpcom-block-editor-nux-script', - 'wpcomBlockEditorNuxAssetsUrl', - plugins_url( 'dist/', __FILE__ ) - ); - wp_localize_script( - 'wpcom-block-editor-nux-script', - 'wpcomBlockEditorNuxLocale', - Common\get_iso_639_locale( determine_locale() ) - ); - - wp_set_script_translations( 'wpcom-block-editor-nux-script', 'jetpack-mu-wpcom' ); - - $style_path = 'dist/wpcom-block-editor-nux' . ( is_rtl() ? '.rtl' : '' ) . '.css'; - wp_enqueue_style( - 'wpcom-block-editor-nux-style', - plugins_url( $style_path, __FILE__ ), - array(), - filemtime( plugin_dir_path( __FILE__ ) . $style_path ) - ); + jetpack_mu_wpcom_enqueue_assets( 'wpcom-block-editor-nux', array( 'js', 'css' ) ); + wp_set_script_translations( 'wpcom-block-editor-nux', 'jetpack-mu-wpcom' ); } /** diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/block-editor-nux.js b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/block-editor-nux.js index 233aa88660e87..1749ed15908da 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/block-editor-nux.js +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/block-editor-nux.js @@ -1,9 +1,4 @@ -/*** THIS MUST BE THE FIRST THING EVALUATED IN THIS SCRIPT *****/ -import './public-path'; - -/* eslint-disable wpcalypso/jsx-classname-namespace */ - -import { LocaleProvider, i18nDefaultLocaleSlug } from '@automattic/i18n-utils'; +import { LocaleProvider } from '@automattic/i18n-utils'; import { Guide, GuidePage } from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; import { useEffect, useState } from '@wordpress/element'; @@ -11,9 +6,11 @@ import { applyFilters } from '@wordpress/hooks'; import { registerPlugin } from '@wordpress/plugins'; import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis'; import { getQueryArg } from '@wordpress/url'; -import { ShouldShowFirstPostPublishedModalProvider } from '../../dotcom-fse/lib/first-post-published-modal/should-show-first-post-published-modal-context'; -import { HasSeenSellerCelebrationModalProvider } from '../../dotcom-fse/lib/seller-celebration-modal/has-seen-seller-celebration-modal-context'; -import { HasSeenVideoCelebrationModalProvider } from '../../dotcom-fse/lib/video-celebration-modal/has-seen-video-celebration-modal-context'; +import { + HasSeenSellerCelebrationModalProvider, + HasSeenVideoCelebrationModalProvider, + ShouldShowFirstPostPublishedModalProvider, +} from '../../../common/tour-kit'; import { BloggingPromptsModal } from './blogging-prompts-modal'; import DraftPostModal from './draft-post-modal'; import FirstPostPublishedModal from './first-post-published-modal'; @@ -42,7 +39,7 @@ try { } /** - * + * The WelcomeTour component */ function WelcomeTour() { const [ showDraftPostModal ] = useState( @@ -107,7 +104,7 @@ function WelcomeTour() { if ( variant === DEFAULT_VARIANT ) { return ( - + { showDraftPostModal ? : } ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/first-post-published-modal/index.tsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/first-post-published-modal/index.tsx index 404f755a2a573..3d27771c6d50a 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/first-post-published-modal/index.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/first-post-published-modal/index.tsx @@ -4,9 +4,8 @@ import { useEffect, useRef, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { isURL } from '@wordpress/url'; import React from 'react'; +import { useSiteIntent, useShouldShowFirstPostPublishedModal } from '../../../../common/tour-kit'; import { wpcomTrackEvent } from '../../../../common/tracks'; -import { useShouldShowFirstPostPublishedModal } from '../../../dotcom-fse/lib/first-post-published-modal/should-show-first-post-published-modal-context'; -import useSiteIntent from '../../../dotcom-fse/lib/site-intent/use-site-intent'; import NuxModal from '../nux-modal'; import postPublishedImage from './images/post-published.svg'; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/public-path.js b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/public-path.js deleted file mode 100644 index 6bab2016a50ad..0000000000000 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/public-path.js +++ /dev/null @@ -1,11 +0,0 @@ -/* exported __webpack_public_path__ */ -/* global __webpack_public_path__ */ - -/** - * Dynamically set WebPack's publicPath so that split assets can be found. - * @see https://webpack.js.org/guides/public-path/#on-the-fly - */ -if ( typeof window === 'object' && window.wpcomBlockEditorNuxAssetsUrl ) { - // eslint-disable-next-line no-global-assign - __webpack_public_path__ = window.wpcomBlockEditorNuxAssetsUrl; -} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/seller-celebration-modal/index.jsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/seller-celebration-modal/index.jsx index e8c3f727880b4..070eea11fb0e2 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/seller-celebration-modal/index.jsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/seller-celebration-modal/index.jsx @@ -2,10 +2,12 @@ import { Button } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; import { useState, useRef, useEffect } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import { + useSiteIntent, + useShouldShowSellerCelebrationModal, + useHasSeenSellerCelebrationModal, +} from '../../../../common/tour-kit'; import { wpcomTrackEvent } from '../../../../common/tracks'; -import { useHasSeenSellerCelebrationModal } from '../../../dotcom-fse/lib/seller-celebration-modal/has-seen-seller-celebration-modal-context'; -import useShouldShowSellerCelebrationModal from '../../../dotcom-fse/lib/seller-celebration-modal/use-should-show-seller-celebration-modal'; -import useSiteIntent from '../../../dotcom-fse/lib/site-intent/use-site-intent'; import NuxModal from '../nux-modal'; import contentSubmittedImage from './images/product-published.svg'; import './style.scss'; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/clipboard-button.tsx b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/clipboard-button.tsx new file mode 100644 index 0000000000000..bd077bf7625cc --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-block-editor-nux/src/sharing-modal/clipboard-button.tsx @@ -0,0 +1,47 @@ +import { Button } from '@wordpress/components'; +import clsx from 'clsx'; +import { forwardRef } from 'react'; + +interface ClipboardButtonProps { + className?: string; + compact?: boolean; + disabled?: boolean; + primary?: boolean; + scary?: boolean; + busy?: boolean; + borderless?: boolean; + plain?: boolean; + transparent?: boolean; + text: string | null; + onCopy?: () => void; + onMouseLeave?: () => void; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = () => {}; + +const ClipboardButton = forwardRef< + HTMLButtonElement, + React.PropsWithChildren< ClipboardButtonProps > +>( ( { className, text, onCopy = noop, ...rest }, ref ) => { + /** + * The copy handler + */ + function onCopyHandler() { + if ( text ) { + navigator.clipboard.writeText( text ); + onCopy(); + } + } + + return ( +