diff --git a/includes/api/class-plugins-controller.php b/includes/api/class-plugins-controller.php index 21e14bf631..3bd0d107c9 100644 --- a/includes/api/class-plugins-controller.php +++ b/includes/api/class-plugins-controller.php @@ -376,6 +376,9 @@ public function configure_item( $request ) { $managed_plugins = Plugin_Manager::get_managed_plugins(); \Newspack\Configuration_Managers::configure( $slug ); + + $managed_plugins[ $slug ]['Configured'] = \Newspack\Configuration_Managers::is_configured( $slug ); + return rest_ensure_response( $managed_plugins[ $slug ] ); } diff --git a/includes/class-blocks.php b/includes/class-blocks.php index a315a121af..04afcb09f1 100644 --- a/includes/class-blocks.php +++ b/includes/class-blocks.php @@ -40,13 +40,13 @@ public static function enqueue_block_editor_assets() { [ 'has_newsletters' => class_exists( 'Newspack_Newsletters_Subscription' ), 'has_reader_activation' => Reader_Activation::is_enabled(), - 'newsletters_url' => Wizards::get_wizard( 'engagement' )->newsletters_settings_url(), + 'newsletters_url' => Wizards::get_wizard( 'newsletters' )->newsletters_settings_url(), 'has_google_oauth' => Google_OAuth::is_oauth_configured(), 'google_logo_svg' => \Newspack\Newspack_UI_Icons::get_svg( 'google' ), 'reader_activation_terms' => Reader_Activation::get_setting( 'terms_text' ), 'reader_activation_url' => Reader_Activation::get_setting( 'terms_url' ), 'has_recaptcha' => Recaptcha::can_use_captcha(), - 'recaptcha_url' => admin_url( 'admin.php?page=newspack-connections-wizard' ), + 'recaptcha_url' => admin_url( 'admin.php?page=newspack-settings' ), ] ); \wp_enqueue_style( diff --git a/includes/class-donations.php b/includes/class-donations.php index fe509bff36..e8ed3bff76 100644 --- a/includes/class-donations.php +++ b/includes/class-donations.php @@ -82,6 +82,8 @@ public static function init() { add_filter( 'wcs_place_subscription_order_text', [ __CLASS__, 'order_button_text' ], 9 ); add_filter( 'woocommerce_order_button_text', [ __CLASS__, 'order_button_text' ], 9 ); add_filter( 'option_woocommerce_subscriptions_order_button_text', [ __CLASS__, 'order_button_text' ], 9 ); + + add_filter( 'render_block', [ __CLASS__, 'prevent_rendering_donate_block' ], 10, 2 ); } /** @@ -454,8 +456,7 @@ public static function get_donation_settings() { $parsed_settings['amounts'][ $frequency ] = array_map( 'floatval', $amounts ); } - $parsed_settings['platform'] = self::get_platform_slug(); - $parsed_settings['billingFields'] = self::get_billing_fields(); + $parsed_settings['platform'] = self::get_platform_slug(); // If NYP isn't available, force untiered config. if ( ! self::can_use_name_your_price() ) { @@ -486,13 +487,6 @@ public static function set_donation_settings( $args ) { if ( isset( $args['saveDonationProduct'] ) && $args['saveDonationProduct'] === true ) { self::update_donation_product( $configuration ); } - - // Update the billing fields. - $billing_fields = isset( $args['billingFields'] ) ? $args['billingFields'] : []; - if ( ! empty( $billing_fields ) ) { - $billing_fields = array_map( 'sanitize_text_field', $billing_fields ); - self::update_billing_fields( $billing_fields ); - } } Logger::log( 'Save donation settings' ); @@ -644,7 +638,6 @@ public static function get_platform_slug() { * @param string $platform Platform slug. */ public static function set_platform_slug( $platform ) { - delete_option( self::NEWSPACK_READER_REVENUE_PLATFORM ); update_option( self::NEWSPACK_READER_REVENUE_PLATFORM, $platform, true ); } @@ -1140,6 +1133,23 @@ public static function disable_coupons( $enabled ) { return false; } + /** + * Prevent rendering of Donate block if Reader Revenue platform is set to 'other. + * + * @param string $block_content The block content about to be rendered. + * @param array $block The data of the block about to be rendered. + */ + public static function prevent_rendering_donate_block( $block_content, $block ) { + if ( + isset( $block['blockName'] ) + && 'newspack-blocks/donate' === $block['blockName'] + && self::is_platform_other() + ) { + return ''; + } + return $block_content; + } + /** * Set the "Place order" button text. * diff --git a/includes/class-newspack-image-credits.php b/includes/class-newspack-image-credits.php index 739dfb4eee..349aa96b4a 100644 --- a/includes/class-newspack-image-credits.php +++ b/includes/class-newspack-image-credits.php @@ -421,8 +421,8 @@ function( $setting ) use ( $key ) { return $setting['key'] === $key; } ); - $setting_values = array_values( $setting ); - return reset( $setting_values ); + $settings_values = array_values( $setting ); + return reset( $settings_values ); } return $default_settings; diff --git a/includes/class-newspack.php b/includes/class-newspack.php index f88e57d862..92694c2c80 100644 --- a/includes/class-newspack.php +++ b/includes/class-newspack.php @@ -46,9 +46,6 @@ public function __construct() { add_action( 'current_screen', [ $this, 'wizard_redirect' ] ); add_action( 'admin_menu', [ $this, 'handle_resets' ], 1 ); add_action( 'admin_menu', [ $this, 'remove_newspack_suite_plugin_links' ], 1 ); - add_action( 'admin_notices', [ $this, 'remove_notifications' ], -9999 ); - add_action( 'network_admin_notices', [ $this, 'remove_notifications' ], -9999 ); - add_action( 'all_admin_notices', [ $this, 'remove_notifications' ], -9999 ); register_activation_hook( NEWSPACK_PLUGIN_FILE, [ $this, 'activation_hook' ] ); register_deactivation_hook( NEWSPACK_PLUGIN_FILE, [ $this, 'deactivation_hook' ] ); } @@ -72,6 +69,7 @@ private function define_constants() { define( 'NEWSPACK_ACTIVATION_TRANSIENT', '_newspack_activation_redirect' ); define( 'NEWSPACK_NRH_CONFIG', 'newspack_nrh_config' ); define( 'NEWSPACK_CLIENT_ID_COOKIE_NAME', 'newspack-cid' ); + define( 'NEWSPACK_SETUP_COMPLETE', 'newspack_setup_complete' ); } /** @@ -117,7 +115,6 @@ private function includes() { include_once NEWSPACK_ABSPATH . 'includes/oauth/class-google-oauth.php'; include_once NEWSPACK_ABSPATH . 'includes/oauth/class-google-services-connection.php'; include_once NEWSPACK_ABSPATH . 'includes/oauth/class-mailchimp-api.php'; - include_once NEWSPACK_ABSPATH . 'includes/oauth/class-fivetran-connection.php'; include_once NEWSPACK_ABSPATH . 'includes/oauth/class-google-login.php'; include_once NEWSPACK_ABSPATH . 'includes/class-blocks.php'; include_once NEWSPACK_ABSPATH . 'includes/tracking/class-pixel.php'; @@ -126,29 +123,53 @@ private function includes() { include_once NEWSPACK_ABSPATH . 'includes/revisions-control/class-revisions-control.php'; include_once NEWSPACK_ABSPATH . 'includes/authors/class-authors-custom-fields.php'; include_once NEWSPACK_ABSPATH . 'includes/corrections/class-corrections.php'; + include_once NEWSPACK_ABSPATH . 'includes/class-syndication.php'; include_once NEWSPACK_ABSPATH . 'includes/bylines/class-bylines.php'; include_once NEWSPACK_ABSPATH . 'includes/starter_content/class-starter-content-provider.php'; include_once NEWSPACK_ABSPATH . 'includes/starter_content/class-starter-content-generated.php'; include_once NEWSPACK_ABSPATH . 'includes/starter_content/class-starter-content-wordpress.php'; + include_once NEWSPACK_ABSPATH . 'includes/wizards/class-wizard.php'; + include_once NEWSPACK_ABSPATH . 'includes/wizards/class-wizard-section.php'; + include_once NEWSPACK_ABSPATH . 'includes/wizards/class-setup-wizard.php'; - include_once NEWSPACK_ABSPATH . 'includes/wizards/class-dashboard.php'; include_once NEWSPACK_ABSPATH . 'includes/wizards/class-components-demo.php'; - /* Unified Wizards */ - include_once NEWSPACK_ABSPATH . 'includes/wizards/class-settings.php'; - include_once NEWSPACK_ABSPATH . 'includes/wizards/class-advertising-wizard.php'; - include_once NEWSPACK_ABSPATH . 'includes/wizards/class-analytics-wizard.php'; - include_once NEWSPACK_ABSPATH . 'includes/wizards/class-engagement-wizard.php'; - include_once NEWSPACK_ABSPATH . 'includes/wizards/class-reader-revenue-wizard.php'; - include_once NEWSPACK_ABSPATH . 'includes/wizards/class-seo-wizard.php'; - include_once NEWSPACK_ABSPATH . 'includes/wizards/class-site-design-wizard.php'; - include_once NEWSPACK_ABSPATH . 'includes/wizards/class-syndication-wizard.php'; - include_once NEWSPACK_ABSPATH . 'includes/wizards/class-health-check-wizard.php'; - include_once NEWSPACK_ABSPATH . 'includes/wizards/class-popups-wizard.php'; - include_once NEWSPACK_ABSPATH . 'includes/wizards/class-connections-wizard.php'; + include_once NEWSPACK_ABSPATH . 'includes/wizards/traits/trait-wizards-admin-header.php'; + + // Newspack Wizards and Sections. + include_once NEWSPACK_ABSPATH . 'includes/wizards/newspack/class-newspack-dashboard.php'; + include_once NEWSPACK_ABSPATH . 'includes/wizards/newspack/class-newspack-settings.php'; + include_once NEWSPACK_ABSPATH . 'includes/wizards/newspack/class-custom-events-section.php'; + include_once NEWSPACK_ABSPATH . 'includes/wizards/newspack/class-syndication-section.php'; + include_once NEWSPACK_ABSPATH . 'includes/wizards/newspack/class-seo-section.php'; + include_once NEWSPACK_ABSPATH . 'includes/wizards/newspack/class-pixels-section.php'; + include_once NEWSPACK_ABSPATH . 'includes/wizards/newspack/class-recirculation-section.php'; + + include_once NEWSPACK_ABSPATH . 'includes/wizards/class-setup-wizard.php'; + include_once NEWSPACK_ABSPATH . 'includes/wizards/class-components-demo.php'; + + // Listings Wizard. + include_once NEWSPACK_ABSPATH . 'includes/wizards/class-listings-wizard.php'; + // Advertising Wizard. + include_once NEWSPACK_ABSPATH . 'includes/wizards/advertising/class-advertising-display-ads.php'; + include_once NEWSPACK_ABSPATH . 'includes/wizards/advertising/class-advertising-sponsors.php'; + + // Audience Wizard. + include_once NEWSPACK_ABSPATH . 'includes/wizards/audience/class-audience-wizard.php'; + include_once NEWSPACK_ABSPATH . 'includes/wizards/audience/class-audience-campaigns.php'; + include_once NEWSPACK_ABSPATH . 'includes/wizards/audience/class-audience-donations.php'; + include_once NEWSPACK_ABSPATH . 'includes/wizards/audience/class-audience-subscriptions.php'; + + // Network Wizard. + include_once NEWSPACK_ABSPATH . 'includes/wizards/class-network-wizard.php'; + + // Newsletters Wizard. + include_once NEWSPACK_ABSPATH . 'includes/wizards/class-newsletters-wizard.php'; + + /* Unified Wizards */ include_once NEWSPACK_ABSPATH . 'includes/class-wizards.php'; include_once NEWSPACK_ABSPATH . 'includes/class-handoff-banner.php'; @@ -179,6 +200,7 @@ private function includes() { include_once NEWSPACK_ABSPATH . 'includes/plugins/woocommerce-subscriptions/class-woocommerce-subscriptions-gifting.php'; include_once NEWSPACK_ABSPATH . 'includes/plugins/class-teams-for-memberships.php'; include_once NEWSPACK_ABSPATH . 'includes/plugins/class-newspack-elections.php'; + include_once NEWSPACK_ABSPATH . 'includes/plugins/class-yoast.php'; include_once NEWSPACK_ABSPATH . 'includes/class-patches.php'; include_once NEWSPACK_ABSPATH . 'includes/polyfills/class-amp-polyfills.php'; @@ -236,7 +258,7 @@ public function handle_resets() { if ( ! current_user_can( 'manage_options' ) ) { return; } - $redirect_url = admin_url( 'admin.php?page=newspack' ); + $redirect_url = admin_url( 'admin.php?page=newspack-dashboard' ); $newspack_reset = filter_input( INPUT_GET, 'newspack_reset', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); if ( 'starter-content' === $newspack_reset ) { Starter_Content::remove_starter_content(); @@ -262,19 +284,6 @@ public function handle_resets() { } } - /** - * Remove notifications. - */ - public function remove_notifications() { - $screen = get_current_screen(); - - $is_newspack_screen = ( 'newspack' === $screen->parent_base ) || ( 'admin_page_newspack-' === substr( $screen->base, 0, 20 ) ); - if ( ! $screen || ! $is_newspack_screen ) { - return; - } - remove_all_actions( current_action() ); - } - /** * Activation Hook */ @@ -334,7 +343,7 @@ public function wizard_redirect( $current_screen ) { $post_type_mapping = [ Emails::POST_TYPE => [ 'base' => 'edit', - 'url' => esc_url( admin_url( 'admin.php?page=newspack' ) ), + 'url' => esc_url( admin_url( 'admin.php?page=newspack-dashboard' ) ), ], ]; @@ -342,7 +351,7 @@ public function wizard_redirect( $current_screen ) { if ( class_exists( '\Newspack_Popups' ) ) { $post_type_mapping[ \Newspack_Popups::NEWSPACK_POPUPS_CPT ] = [ 'base' => 'edit', - 'url' => esc_url( admin_url( 'admin.php?page=newspack-popups-wizard' ) ), + 'url' => esc_url( admin_url( 'admin.php?page=newspack-audience-campaigns' ) ), ]; } @@ -379,6 +388,13 @@ public static function is_debug_mode() { return defined( 'WP_NEWSPACK_DEBUG' ) && WP_NEWSPACK_DEBUG; } + /** + * Is the Setup completed? + */ + public static function is_setup_complete() { + return '1' === get_option( NEWSPACK_SETUP_COMPLETE, '0' ); + } + /** * Load the common assets. */ @@ -401,12 +417,14 @@ public static function load_common_assets() { wp_style_add_data( 'newspack-commons', 'rtl', 'replace' ); wp_enqueue_style( 'newspack-commons' ); - \wp_enqueue_style( - 'newspack-admin', - self::plugin_url() . '/dist/admin.css', - [], - NEWSPACK_PLUGIN_VERSION - ); + if ( is_admin() ) { + \wp_enqueue_style( + 'newspack-admin', + self::plugin_url() . '/dist/admin.css', + [], + NEWSPACK_PLUGIN_VERSION + ); + } } } Newspack::instance(); diff --git a/includes/class-plugin-manager.php b/includes/class-plugin-manager.php index 3dcf6b3ed6..8e106b02ce 100644 --- a/includes/class-plugin-manager.php +++ b/includes/class-plugin-manager.php @@ -199,7 +199,7 @@ public static function get_managed_plugins() { ], 'publish-to-apple-news' => [ 'Name' => \esc_html__( 'Publish to Apple News', 'newspack-plugin' ), - 'Description' => \esc_html__( 'Export and synchronize posts to Apple format', 'newspack-plugin' ), + 'Description' => \esc_html__( 'Export and synchronize posts to Apple format.', 'newspack-plugin' ), 'Author' => \esc_html__( 'Alley Interactive', 'newspack-plugin' ), 'AuthorURI' => \esc_url( 'https://www.alleyinteractive.com' ), 'PluginURI' => \esc_url( 'https://github.com/alleyinteractive/apple-news' ), @@ -444,7 +444,7 @@ public static function get_managed_plugin_status( $plugin_slug ) { /** * Get the list of plugins which are supported, but not managed. * These plugins will not be added to the WP Admin plugins screen, - * but installing them will not raise any issues in Health Check. + * but installing them shouldn't be an issue. */ private static function get_supported_plugins_slugs() { return [ @@ -562,7 +562,7 @@ public static function activate( $plugin ) { } if ( \is_plugin_active( $installed_plugins[ $plugin_slug ] ) ) { - return new WP_Error( 'newspack_plugin_already_active', __( 'The plugin is already active.', 'newspack-plugin' ) ); + return true; } $activated = activate_plugin( $installed_plugins[ $plugin_slug ] ); diff --git a/includes/class-recaptcha.php b/includes/class-recaptcha.php index a66f1832f5..fdc04ad3b6 100644 --- a/includes/class-recaptcha.php +++ b/includes/class-recaptcha.php @@ -179,7 +179,16 @@ public static function api_permissions_check() { public static function get_settings_config() { return [ 'use_captcha' => false, - 'credentials' => [], + 'credentials' => [ + 'v3' => [ + 'site_key' => '', + 'site_secret' => '', + ], + 'v2_invisible' => [ + 'site_key' => '', + 'site_secret' => '', + ], + ], 'threshold' => 0.5, 'version' => 'v3', ]; @@ -226,7 +235,11 @@ public static function get_settings() { $config = self::get_settings_config(); $settings = []; foreach ( $config as $key => $default_value ) { - $settings[ $key ] = self::get_setting( $key ); + if ( 'credentials' === $key ) { + $settings[ $key ] = wp_parse_args( self::get_setting( $key ), $default_value ); + } else { + $settings[ $key ] = self::get_setting( $key ); + } } // Migrate reCAPTCHA settings from separate site_key/site_secret options to credentials array. @@ -328,7 +341,7 @@ public static function update_settings( $settings ) { /** * Check whether reCaptcha is enabled and that we have all required settings. * - * @param string $version If specified, chedk whether the given version of reCaptcha is enabled. + * @param string $version If specified, check whether the given version of reCaptcha is enabled. * * @return boolean True if we can use reCaptcha to secure checkout requests. */ diff --git a/includes/class-salesforce.php b/includes/class-salesforce.php index 366413fd9c..256da2efd1 100644 --- a/includes/class-salesforce.php +++ b/includes/class-salesforce.php @@ -708,7 +708,7 @@ private static function sync_salesforce( $order ) { * @return string Redirect URL. */ public static function get_redirect_url() { - return get_admin_url( null, 'admin.php?page=newspack-reader-revenue-wizard#/salesforce' ); + return get_admin_url( null, 'admin.php?page=newspack-audience' ); } /** diff --git a/includes/class-starter-content.php b/includes/class-starter-content.php index 0df7a45792..e02bb60699 100644 --- a/includes/class-starter-content.php +++ b/includes/class-starter-content.php @@ -174,7 +174,7 @@ public static function get_theme() { */ public static function upload_logo() { - $attachment_title = esc_attr__( 'Newspack Placeholder Logo', 'newspack' ); + $attachment_title = esc_attr__( 'Newspack Placeholder Logomark', 'newspack' ); $args = [ 'posts_per_page' => 1, @@ -195,9 +195,9 @@ public static function upload_logo() { } $file = wp_upload_bits( - 'newspack-logo.png', + 'newspack-logomark.png', null, - file_get_contents( NEWSPACK_ABSPATH . 'includes/raw_assets/images/newspack-logo.png' ) + file_get_contents( NEWSPACK_ABSPATH . 'includes/raw_assets/images/newspack-logomark.png' ) ); if ( ! $file || empty( $file['file'] ) ) { diff --git a/includes/wizards/class-settings.php b/includes/class-syndication.php similarity index 57% rename from includes/wizards/class-settings.php rename to includes/class-syndication.php index 73230224c3..451b1f9d5a 100644 --- a/includes/wizards/class-settings.php +++ b/includes/class-syndication.php @@ -1,6 +1,6 @@ hidden = true; } /** * Get all settings. */ - private static function get_settings() { + public static function get_settings() { $default_settings = [ self::MODULE_ENABLED_PREFIX . 'rss' => false, self::MODULE_ENABLED_PREFIX . 'media-partners' => false, ]; - return wp_parse_args( get_option( self::SETTINGS_OPTION_NAME ), $default_settings ); + return wp_parse_args( get_option( self::OPTION_NAME ), $default_settings ); } /** * Get the list of available optional modules. */ - private static function get_available_optional_modules() { + public static function get_available_optional_modules() { return [ 'rss' ]; } @@ -73,7 +67,7 @@ public static function api_get_settings() { * @param string $module_name Name of the module. */ public static function is_optional_module_active( $module_name ) { - $settings = self::api_get_settings(); + $settings = self::get_settings(); $setting_name = self::MODULE_ENABLED_PREFIX . $module_name; if ( isset( $settings[ $setting_name ] ) ) { return $settings[ $setting_name ]; @@ -100,7 +94,7 @@ private static function update_setting( $key, $value ) { $settings = self::get_settings(); if ( isset( $settings[ $key ] ) ) { $settings[ $key ] = $value; - update_option( self::SETTINGS_OPTION_NAME, $settings ); + update_option( self::OPTION_NAME, $settings ); } return $settings; } @@ -117,45 +111,8 @@ public static function api_update_settings( $request ) { $setting_name = self::MODULE_ENABLED_PREFIX . $module_name; $settings[ $setting_name ] = $request->get_param( $setting_name ); } - update_option( self::SETTINGS_OPTION_NAME, $settings ); - return self::api_get_settings(); - } - - /** - * Register the endpoints needed for the wizard screens. - */ - public function register_api_endpoints() { - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug, - [ - 'methods' => \WP_REST_Server::READABLE, - 'callback' => [ $this, 'api_get_settings' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] - ); - - $required_args = array_reduce( - self::get_available_optional_modules(), - function( $acc, $module_name ) { - $acc[ self::MODULE_ENABLED_PREFIX . $module_name ] = [ - 'required' => true, - 'sanitize_callback' => 'rest_sanitize_boolean', - ]; - return $acc; - }, - [] - ); - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug, - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_update_settings' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => $required_args, - ] - ); + update_option( self::OPTION_NAME, $settings ); + return self::get_settings(); } /** @@ -164,7 +121,7 @@ function( $acc, $module_name ) { * @return string The wizard name. */ public function get_name() { - return \esc_html__( 'Settings', 'newspack' ); + return esc_html__( 'Settings', 'newspack' ); } /** @@ -173,7 +130,7 @@ public function get_name() { * @return string The wizard description. */ public function get_description() { - return \esc_html__( 'Configure settings.', 'newspack' ); + return esc_html__( 'Configure settings.', 'newspack' ); } /** diff --git a/includes/class-wizards.php b/includes/class-wizards.php index eebce8f134..9e360ad388 100644 --- a/includes/class-wizards.php +++ b/includes/class-wizards.php @@ -7,6 +7,8 @@ namespace Newspack; +use Newspack\Wizards\Newspack\Newspack_Settings; + defined( 'ABSPATH' ) || exit; /** @@ -27,21 +29,36 @@ class Wizards { */ public static function init() { self::$wizards = [ - 'setup' => new Setup_Wizard(), - 'dashboard' => new Dashboard(), - 'site-design' => new Site_Design_Wizard(), - 'reader-revenue' => new Reader_Revenue_Wizard(), - 'advertising' => new Advertising_Wizard(), - 'syndication' => new Syndication_Wizard(), - 'analytics' => new Analytics_Wizard(), - 'components-demo' => new Components_Demo(), - 'seo' => new SEO_Wizard(), - 'health-check' => new Health_Check_Wizard(), - 'engagement' => new Engagement_Wizard(), - 'popups' => new Popups_Wizard(), - 'connections' => new Connections_Wizard(), - 'settings' => new Settings(), + 'components-demo' => new Components_Demo(), + // v2 Information Architecture. + 'newspack-dashboard' => new Newspack_Dashboard(), + 'setup' => new Setup_Wizard(), + 'newspack-settings' => new Newspack_Settings( + [ + 'sections' => [ + 'custom-events' => 'Newspack\Wizards\Newspack\Custom_Events_Section', + 'social-pixels' => 'Newspack\Wizards\Newspack\Pixels_Section', + 'recirculation' => 'Newspack\Wizards\Newspack\Recirculation_Section', + 'syndication' => 'Newspack\Wizards\Newspack\Syndication_Section', + 'seo' => 'Newspack\Wizards\Newspack\Seo_Section', + ], + ] + ), + 'advertising-display-ads' => new Advertising_Display_Ads(), + 'advertising-sponsors' => new Advertising_Sponsors(), + 'audience' => new Audience_Wizard(), + 'audience-campaigns' => new Audience_Campaigns(), + 'audience-donations' => new Audience_Donations(), + 'audience-subscriptions' => new Audience_Subscriptions(), + 'listings' => new Listings_Wizard(), + 'network' => new Network_Wizard(), + 'newsletters' => new Newsletters_Wizard(), ]; + + // Allow custom menu order. + add_filter( 'custom_menu_order', '__return_true' ); + // Fix menu order for wizards with parent menu items. + add_filter( 'menu_order', [ __CLASS__, 'menu_order' ], 11 ); } /** @@ -116,5 +133,38 @@ public static function is_completed( $wizard_slug ) { return false; } + + /** + * Update menu order for wizards with parent menu items. + * + * @param array $menu_order The current menu order. + * + * @return array The updated menu order. + */ + public static function menu_order( $menu_order ) { + $index = array_search( 'newspack-dashboard', $menu_order, true ); + if ( false === $index ) { + return $menu_order; + } + $ordered_wizards = []; + foreach ( self::$wizards as $slug => $wizard ) { + if ( ! empty( $wizard->parent_menu ) && ! empty( $wizard->parent_menu_order ) ) { + $ordered_wizards[ $wizard->parent_menu_order ] = $wizard->parent_menu; + } + } + if ( empty( $ordered_wizards ) ) { + return $menu_order; + } + ksort( $ordered_wizards ); + foreach ( array_reverse( $ordered_wizards ) as $menu_item ) { + $key = array_search( $menu_item, $menu_order, true ); + if ( false === $key ) { + continue; + } + array_splice( $menu_order, $key, 1 ); + array_splice( $menu_order, $index + 1, 0, $menu_item ); + } + return $menu_order; + } } Wizards::init(); diff --git a/includes/cli/class-ras.php b/includes/cli/class-ras.php index e3ebc61902..f21ec83643 100644 --- a/includes/cli/class-ras.php +++ b/includes/cli/class-ras.php @@ -31,7 +31,6 @@ public static function cli_setup_ras() { WP_CLI::error( __( 'Newspack Campaigns plugin not found.', 'newspack-plugin' ) ); } - if ( ! class_exists( '\Newspack_Newsletters_Subscription' ) ) { WP_CLI::error( __( 'Newspack Newsletters plugin not found.', 'newspack-plugin' ) ); } diff --git a/includes/configuration_managers/class-configuration-managers.php b/includes/configuration_managers/class-configuration-managers.php index ed8f91ee06..ce62b57b3c 100644 --- a/includes/configuration_managers/class-configuration-managers.php +++ b/includes/configuration_managers/class-configuration-managers.php @@ -117,7 +117,7 @@ public static function is_configured( $slug ) { } $configuration_manager = self::configuration_manager_class_for_plugin_slug( $slug ); if ( is_wp_error( $configuration_manager ) ) { - return false; + return true; } return $configuration_manager->is_configured(); } diff --git a/includes/configuration_managers/class-newspack-ads-configuration-manager.php b/includes/configuration_managers/class-newspack-ads-configuration-manager.php index bb3f65bdff..b409e44058 100644 --- a/includes/configuration_managers/class-newspack-ads-configuration-manager.php +++ b/includes/configuration_managers/class-newspack-ads-configuration-manager.php @@ -187,7 +187,7 @@ private function unconfigured_error() { * @return bool Is the service enabled. */ public function is_service_enabled( $service ) { - return get_option( Advertising_Wizard::NEWSPACK_ADVERTISING_SERVICE_PREFIX . $service, false ); + return get_option( Advertising_Display_Ads::NEWSPACK_ADVERTISING_SERVICE_PREFIX . $service, false ); } /** diff --git a/includes/data-events/class-api.php b/includes/data-events/class-api.php index ba13147daa..650a5585dc 100644 --- a/includes/data-events/class-api.php +++ b/includes/data-events/class-api.php @@ -9,6 +9,7 @@ use Newspack\Data_Events; use Newspack\Data_Events\Webhooks; +use WP_Error; /** * Main Class. @@ -194,7 +195,8 @@ public static function test_url( $request ) { } return \rest_ensure_response( [ - 'success' => $code && 200 >= $code && 300 > $code, + // Success if response code is in 2xx range. + 'success' => $code && $code > 199 && $code < 300, 'code' => $code, 'message' => $message, ] @@ -274,6 +276,21 @@ public static function upsert_endpoint( $request ) { $args['disabled'] ); } + if ( is_wp_error( $endpoint ) ) { + $error_code = $endpoint->get_error_code(); + if ( $error_code === 'term_exists' ) { + return new WP_Error( + 'newspack_webhooks_endpoint_exists', + /* translators: %s: URL */ + sprintf( __( 'URL "%s" is already in use.', 'newspack-plugin' ), $args['url'] ), + [ + 'status' => 400, + ] + ); + + } + return $endpoint; + } if ( is_string( $request->get_param( 'label' ) ) ) { Webhooks::update_endpoint_label( $endpoint['id'], $request->get_param( 'label' ) ); } diff --git a/includes/data-events/connectors/ga4/class-ga4.php b/includes/data-events/connectors/ga4/class-ga4.php index 673e87423a..4124b72583 100644 --- a/includes/data-events/connectors/ga4/class-ga4.php +++ b/includes/data-events/connectors/ga4/class-ga4.php @@ -64,6 +64,17 @@ public static function can_use_ga4() { return ! empty( $properties ) && ! empty( $properties[0]['measurement_id'] ); } + /** + * Gets the credentials for the GA4 API. + * + * @return array + */ + public static function get_ga4_credentials() { + $measurement_protocol_secret = get_option( 'ga4_measurement_protocol_secret', '' ); + $measurement_id = get_option( 'ga4_measurement_id', '' ); + return compact( 'measurement_protocol_secret', 'measurement_id' ); + } + /** * Get the GA4 properties to send events to. * @@ -71,7 +82,7 @@ public static function can_use_ga4() { */ private static function get_ga4_properties() { $properties = [ - Analytics_Wizard::get_ga4_credentials(), + self::get_ga4_credentials(), ]; /** diff --git a/includes/oauth/class-fivetran-connection.php b/includes/oauth/class-fivetran-connection.php deleted file mode 100644 index 5b0e0e98d9..0000000000 --- a/includes/oauth/class-fivetran-connection.php +++ /dev/null @@ -1,193 +0,0 @@ - \WP_REST_Server::READABLE, - 'callback' => [ $this, 'api_get_fivetran_connection_status' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] - ); - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/oauth/fivetran/(?P[\a-z]+)', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_create_connection' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => [ - 'service' => [ - 'required' => true, - 'sanitize_callback' => 'sanitize_text_field', - ], - ], - ] - ); - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/oauth/fivetran-tos', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_post_fivetran_tos' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => [ - 'has_accepted' => [ - 'required' => true, - 'sanitize_callback' => 'rest_sanitize_boolean', - ], - ], - ] - ); - } - - /** - * Get Fivetran connections status. - */ - public static function api_get_fivetran_connection_status() { - $url = OAuth::authenticate_proxy_url( 'fivetran', '/wp-json/newspack-fivetran/v1/connections-status' ); - $connections_statuses = self::process_proxy_response( - \wp_safe_remote_get( - $url, - [ - 'timeout' => 30, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout - ] - ) - ); - if ( is_wp_error( $connections_statuses ) ) { - return new WP_Error( - 'newspack_connections_fivetran', - $connections_statuses->get_error_message() - ); - } - $response = [ - 'connections_statuses' => $connections_statuses, - 'has_accepted_tos' => (bool) get_user_meta( get_current_user_id(), self::NEWSPACK_FIVETRAN_TOS_CONSENT_USER_META, true ), - ]; - return $response; - } - - /** - * Create a new connection. - * - * @param WP_REST_Request $request Request. - */ - public static function api_create_connection( $request ) { - $service = $request->get_param( 'service' ); - $service_data = []; - - $url = OAuth::authenticate_proxy_url( - 'fivetran', - '/wp-json/newspack-fivetran/v1/connect-card', - [ - 'service' => $service, - 'service_data' => $service_data, - 'redirect_after' => admin_url( 'admin.php?page=newspack-connections-wizard' ), - ] - ); - $response = self::process_proxy_response( \wp_safe_remote_post( $url, [ 'timeout' => 30 ] ) ); // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout - if ( is_wp_error( $response ) ) { - return $response; - } - return \rest_ensure_response( $response ); - } - - /** - * Update the user's consent for the TOS. - * - * @param WP_REST_Request $request Request. - */ - public static function api_post_fivetran_tos( $request ) { - update_user_meta( - get_current_user_id(), - self::NEWSPACK_FIVETRAN_TOS_CONSENT_USER_META, - sanitize_meta( - self::NEWSPACK_FIVETRAN_TOS_CONSENT_USER_META, - $request->get_param( 'has_accepted' ), - 'user' - ) - ); - return rest_ensure_response( [] ); - } - - /** - * Process the proxy response. - * - * @param object $result Result of a request. - * @return WP_Error|object Error or response data. - */ - private static function process_proxy_response( $result ) { - if ( is_wp_error( $result ) ) { - return $result; - } - if ( 400 <= $result['response']['code'] ) { - $error_body = json_decode( $result['body'] ); - $error_prefix = __( 'Fivetran proxy error', 'newspack' ); - if ( null !== $error_body && property_exists( $error_body, 'message' ) ) { - $error_message = $error_prefix . ': ' . $error_body->message; - } elseif ( null !== $error_body && property_exists( $error_body, 'data' ) ) { - $error_message = $error_prefix . ': ' . wp_json_encode( $error_body->data ); - } else { - $error_message = $error_prefix; - } - return new WP_Error( - 'newspack_connections_fivetran', - $error_message - ); - } - return json_decode( $result['body'] ); - } - - /** - * Check capabilities for using API. - * - * @codeCoverageIgnore - * @param WP_REST_Request $request API request object. - * @return bool|WP_Error - */ - public static function api_permissions_check( $request ) { - if ( ! current_user_can( 'manage_options' ) ) { - return new \WP_Error( - 'newspack_rest_forbidden', - esc_html__( 'You cannot use this resource.', 'newspack' ), - [ - 'status' => 403, - ] - ); - } - return true; - } -} -new Fivetran_Connection(); diff --git a/includes/oauth/class-mailchimp-api.php b/includes/oauth/class-mailchimp-api.php index 610b0f2731..1e0c39e225 100644 --- a/includes/oauth/class-mailchimp-api.php +++ b/includes/oauth/class-mailchimp-api.php @@ -7,6 +7,8 @@ namespace Newspack; +use stdClass; + defined( 'ABSPATH' ) || exit; /** @@ -111,7 +113,7 @@ public static function api_mailchimp_save_key( $request ) { */ public static function api_mailchimp_delete_key() { delete_option( 'newspack_mailchimp_api_key' ); - return \rest_ensure_response( [] ); + return rest_ensure_response( new stdClass() ); } /** diff --git a/includes/oauth/class-oauth.php b/includes/oauth/class-oauth.php index 6fc37c8757..9b06822535 100644 --- a/includes/oauth/class-oauth.php +++ b/includes/oauth/class-oauth.php @@ -77,7 +77,7 @@ public static function retrieve_csrf_token( $namespace ) { /** * Process OAuth proxy URL. * - * @param string $type 'google' or 'fivetran' for now. + * @param string $type 'google' for now. * @param string $path Path to append to base URL. * @param array $query_args Query params. * @throws \Exception If trying to authenticate a non-existent proxy. @@ -103,7 +103,7 @@ public static function authenticate_proxy_url( string $type, string $path = '', /** * Is OAuth2 configured for this instance? * - * @param string $type 'google' or 'fivetran' for now. + * @param string $type 'google' for now. */ public static function is_proxy_configured( $type ) { return self::get_proxy_url( $type ) && self::get_proxy_api_key(); @@ -112,7 +112,7 @@ public static function is_proxy_configured( $type ) { /** * Get proxy URL by type. * - * @param string $type 'google' or 'fivetran' for now. + * @param string $type 'google' for now. */ private static function get_proxy_url( $type ) { switch ( $type ) { @@ -124,14 +124,6 @@ private static function get_proxy_url( $type ) { return NEWSPACK_GOOGLE_OAUTH_PROXY; } break; - case 'fivetran': - if ( defined( 'NEWSPACK_FIVETRAN_PROXY_OVERRIDE' ) ) { - return NEWSPACK_FIVETRAN_PROXY_OVERRIDE; - } - if ( defined( 'NEWSPACK_FIVETRAN_PROXY' ) ) { - return NEWSPACK_FIVETRAN_PROXY; - } - break; } return false; } diff --git a/includes/optional-modules/class-media-partners.php b/includes/optional-modules/class-media-partners.php index 26c6d67370..149299d6b4 100644 --- a/includes/optional-modules/class-media-partners.php +++ b/includes/optional-modules/class-media-partners.php @@ -19,7 +19,7 @@ class Media_Partners { * Initialize everything. */ public static function init() { - if ( ! Settings::is_optional_module_active( 'media-partners' ) ) { + if ( ! Syndication::is_optional_module_active( 'media-partners' ) ) { return; } diff --git a/includes/optional-modules/class-rss.php b/includes/optional-modules/class-rss.php index a8fdbe995e..676a940bc1 100644 --- a/includes/optional-modules/class-rss.php +++ b/includes/optional-modules/class-rss.php @@ -21,7 +21,7 @@ class RSS { * Initialise. */ public static function init() { - if ( ! Settings::is_optional_module_active( 'rss' ) ) { + if ( ! Syndication::is_optional_module_active( 'rss' ) ) { return; } diff --git a/includes/plugins/class-yoast.php b/includes/plugins/class-yoast.php new file mode 100644 index 0000000000..ac582a7eb7 --- /dev/null +++ b/includes/plugins/class-yoast.php @@ -0,0 +1,35 @@ + self::is_terms_configured(), 'label' => __( 'Legal Pages', 'newspack-plugin' ), 'description' => __( 'Displaying legal pages like Privacy Policy and Terms of Service on your site is recommended for allowing readers to register and access their account.', 'newspack-plugin' ), - 'help_url' => 'https://help.newspack.com/engagement/reader-activation-system', + 'help_url' => 'https://help.newspack.com/engagement/audience-management-system/', 'warning' => __( 'Privacy policies that tell users how you collect and use their data are essential for running a trustworthy website. While rules and regulations can differ by country, certain legal pages might be required by law.', 'newspack-plugin' ), 'fields' => [ 'terms_text' => [ @@ -734,6 +827,8 @@ public static function get_prerequisites_status() { 'description' => __( 'URL to the page containing the privacy policy or terms of service.', 'newspack-plugin' ), ], ], + 'skippable' => true, + 'is_skipped' => self::is_skipped( 'terms_conditions' ), ], 'esp' => [ 'active' => self::is_esp_configured(), @@ -743,15 +838,15 @@ public static function get_prerequisites_status() { 'label' => __( 'Email Service Provider (ESP)', 'newspack-plugin' ), 'description' => __( 'Connect to your ESP to register readers with their email addresses and send newsletters.', 'newspack-plugin' ), 'instructions' => __( 'Connect to your email service provider (ESP) and enable at least one subscription list.', 'newspack-plugin' ), - 'help_url' => 'https://help.newspack.com/engagement/reader-activation-system', - 'href' => \admin_url( '/admin.php?page=newspack-engagement-wizard#/newsletters' ), + 'help_url' => 'https://help.newspack.com/engagement/audience-management-system/', + 'href' => \admin_url( 'edit.php?post_type=newspack_nl_cpt&page=newspack-newsletters' ), 'action_text' => __( 'ESP settings' ), ], 'emails' => [ 'active' => self::is_transactional_email_configured(), 'label' => __( 'Transactional Emails', 'newspack-plugin' ), 'description' => __( 'Your sender name and email address determines how readers find emails related to their account in their inbox. To customize the content of these emails, visit Advanced Settings below.', 'newspack-plugin' ), - 'help_url' => 'https://help.newspack.com/engagement/reader-activation-system', + 'help_url' => 'https://help.newspack.com/engagement/audience-management-system/', 'fields' => [ 'sender_name' => [ 'label' => __( 'Sender Name', 'newspack-plugin' ), @@ -768,13 +863,15 @@ public static function get_prerequisites_status() { ], ], 'recaptcha' => [ - 'active' => method_exists( '\Newspack\Recaptcha', 'can_use_captcha' ) && \Newspack\Recaptcha::can_use_captcha(), + 'active' => self::is_recaptcha_enabled(), 'label' => __( 'reCAPTCHA', 'newspack-plugin' ), 'description' => __( 'Connecting to a Google reCAPTCHA account enables enhanced anti-spam for all Newspack sign-up blocks.', 'newspack-plugin' ), 'instructions' => __( 'Enable reCAPTCHA and enter your account credentials.', 'newspack-plugin' ), - 'help_url' => 'https://help.newspack.com/engagement/reader-activation-system', - 'href' => \admin_url( '/admin.php?page=newspack-connections-wizard&scrollTo=recaptcha' ), + 'help_url' => 'https://help.newspack.com/engagement/audience-management-system/', + 'href' => \admin_url( '/admin.php?page=newspack-settings&scrollTo=newspack-settings-recaptcha' ), 'action_text' => __( 'reCAPTCHA settings' ), + 'skippable' => true, + 'is_skipped' => self::is_skipped( 'recaptcha' ), ], 'reader_revenue' => [ 'active' => self::is_reader_revenue_ready(), @@ -782,23 +879,24 @@ public static function get_prerequisites_status() { 'label' => __( 'Reader Revenue', 'newspack-plugin' ), 'description' => __( 'Setting suggested donation amounts is required for enabling a streamlined donation experience.', 'newspack-plugin' ), 'instructions' => __( 'Set platform to "Newspack" or "News Revenue Hub" and configure your default donation settings. If using News Revenue Hub, set an Organization ID and a Donor Landing Page in News Revenue Hub Settings.', 'newspack-plugin' ), - 'help_url' => 'https://help.newspack.com/engagement/reader-activation-system', - 'href' => \admin_url( '/admin.php?page=newspack-reader-revenue-wizard' ), + 'help_url' => 'https://help.newspack.com/engagement/audience-management-system/', + 'href' => \admin_url( '/admin.php?page=newspack-audience#/payment' ), 'action_text' => __( 'Reader Revenue settings' ), ], 'ras_campaign' => [ 'active' => self::is_ras_campaign_configured(), - 'is_skipped' => get_option( Engagement_Wizard::SKIP_CAMPAIGN_SETUP_OPTION, '' ) === '1', 'plugins' => [ 'newspack-popups' => class_exists( '\Newspack_Popups_Model' ), ], - 'label' => __( 'Reader Activation Campaign', 'newspack-plugin' ), - 'description' => __( 'Building a set of prompts with default segments and settings allows for an improved experience optimized for Reader Activation.', 'newspack-plugin' ), - 'help_url' => 'https://help.newspack.com/engagement/reader-activation-system', - 'href' => self::is_ras_campaign_configured() ? \admin_url( '/admin.php?page=newspack-popups-wizard#/campaigns' ) : \admin_url( '/admin.php?page=newspack-engagement-wizard#/reader-activation/campaign' ), + 'label' => __( 'Audience Management Campaign', 'newspack-plugin' ), + 'description' => __( 'Building a set of prompts with default segments and settings allows for an improved experience optimized for audience management.', 'newspack-plugin' ), + 'help_url' => 'https://help.newspack.com/engagement/audience-management-system/', + 'href' => self::is_ras_campaign_configured() ? admin_url( '/admin.php?page=newspack-audience-campaigns' ) : admin_url( '/admin.php?page=newspack-audience#/campaign' ), 'action_enabled' => self::is_ras_ready_to_configure(), - 'action_text' => __( 'Reader Activation campaign', 'newspack-plugin' ), + 'action_text' => __( 'Audience Management campaign', 'newspack-plugin' ), 'disabled_text' => __( 'Waiting for all settings to be ready', 'newspack-plugin' ), + 'skippable' => true, + 'is_skipped' => self::is_skipped( 'ras_campaign' ), ], ]; @@ -2498,5 +2596,19 @@ public static function get_post_checkout_registration_success_text() { ) ); } + + /** + * Get the checkout configuration. + * + * @return array The checkout configuration. + */ + public static function get_checkout_configuration() { + return [ + 'woocommerce_registration_required' => self::is_woocommerce_registration_required(), + 'woocommerce_post_checkout_success_text' => self::get_post_checkout_success_text(), + 'woocommerce_checkout_privacy_policy_text' => self::get_checkout_privacy_policy_text(), + 'woocommerce_post_checkout_registration_success_text' => self::get_post_checkout_registration_success_text(), + ]; + } } Reader_Activation::init(); diff --git a/includes/reader-activation/sync/class-sync.php b/includes/reader-activation/sync/class-sync.php index d642b1879a..e6df59f99f 100644 --- a/includes/reader-activation/sync/class-sync.php +++ b/includes/reader-activation/sync/class-sync.php @@ -48,14 +48,14 @@ public static function can_sync( $return_errors = false ) { if ( ! Reader_Activation::is_enabled() ) { $errors->add( 'ras_not_enabled', - __( 'Reader Activation is not enabled.', 'newspack-plugin' ) + __( 'Audience Management is not enabled.', 'newspack-plugin' ) ); } if ( class_exists( 'WCS_Staging' ) && \WCS_Staging::is_duplicate_site() ) { $errors->add( 'wcs_duplicate_site', - __( 'Reader Activation sync is disabled for cloned sites.', 'newspack-plugin' ) + __( 'Audience Management contact data syncing is disabled for cloned sites.', 'newspack-plugin' ) ); } @@ -71,7 +71,7 @@ public static function can_sync( $return_errors = false ) { ) { $errors->add( 'esp_sync_not_allowed', - __( 'Sync is disabled for staging sites. To bypass this check, set the NEWSPACK_ALLOW_READER_SYNC constant in your wp-config.php.', 'newspack-plugin' ) + __( 'Contact data syncing is disabled for staging sites. To bypass this check, set the NEWSPACK_ALLOW_READER_SYNC constant in your wp-config.php.', 'newspack-plugin' ) ); } diff --git a/includes/util.php b/includes/util.php index 994e102aae..37dfd3e8e3 100644 --- a/includes/util.php +++ b/includes/util.php @@ -12,6 +12,7 @@ defined( 'ABSPATH' ) || exit; define( 'NEWSPACK_API_NAMESPACE', 'newspack/v1' ); +define( 'NEWSPACK_API_NAMESPACE_V2', 'newspack/v2' ); define( 'NEWSPACK_API_URL', get_site_url() . '/wp-json/' . NEWSPACK_API_NAMESPACE ); /** @@ -495,6 +496,26 @@ function newspack_get_countries() { return $countries_options; } + +/** + * Determine Google Site Kit availability. + * + * @return bool True if available, false otherwise. + */ +function google_site_kit_available() { + return get_option( 'googlesitekit_has_connected_admins' ) && in_array( 'analytics', get_option( 'googlesitekit_active_modules', [] ) ); +} + +/** + * Determine if a plugin is active. Similar to WP core `is_plugin_active` but is available immediately. + * + * @param string $plugin_file `plugin-directory/plugin-file.php` path to the plugin file. + * @return bool + */ +function is_plugin_active( string $plugin_file ) { + return in_array( $plugin_file, get_option( 'active_plugins' ), true ); +} + /** * Pick either white or black, whatever has sufficient contrast with the color being passed to it. * (Copied from the Newspack theme: https://github.com/Automattic/newspack-theme/blob/6dc4e89a65c465abdd207d990e313921f2972a9a/newspack-theme/inc/template-functions.php#L547) @@ -573,7 +594,7 @@ function newspack_adjust_brightness( $hex, $steps ) { * @return array An array containing primary and secondary colors. */ function newspack_get_theme_colors() { - $default_primary_color = function_exists( 'newspack_get_primary_color' ) ? newspack_get_primary_color() : '#3366ff'; + $default_primary_color = function_exists( 'newspack_get_primary_color' ) ? newspack_get_primary_color() : '#003da5'; $default_secondary_color = function_exists( 'newspack_get_secondary_color' ) ? newspack_get_secondary_color() : '#666666'; $primary_color = get_theme_mod( 'primary_color_hex', $default_primary_color ); $secondary_color = get_theme_mod( 'secondary_color_hex', $default_secondary_color ); diff --git a/includes/wizards/class-advertising-wizard.php b/includes/wizards/advertising/class-advertising-display-ads.php similarity index 67% rename from includes/wizards/class-advertising-wizard.php rename to includes/wizards/advertising/class-advertising-display-ads.php index 7d301f9760..7c52bf9505 100644 --- a/includes/wizards/class-advertising-wizard.php +++ b/includes/wizards/advertising/class-advertising-display-ads.php @@ -18,7 +18,7 @@ /** * Easy interface for setting up general store info. */ -class Advertising_Wizard extends Wizard { +class Advertising_Display_Ads extends Wizard { const NEWSPACK_ADVERTISING_SERVICE_PREFIX = '_newspack_advertising_service_'; @@ -33,9 +33,12 @@ class Advertising_Wizard extends Wizard { /** * The slug of this wizard. * + * Note: `newspack-ads-display-ads` (vs. `newspack-advertising-display-ads`) is intentional to avoid + * Ad blockers from blocking the Advertising menu item. + * * @var string */ - protected $slug = 'newspack-advertising-wizard'; + protected $slug = 'newspack-ads-display-ads'; /** * The capability required to access this wizard. @@ -55,12 +58,34 @@ class Advertising_Wizard extends Wizard { ), ); + /** + * The parent menu item name. + * + * @var string + */ + public $parent_menu = 'newspack-ads-display-ads'; + + /** + * Order relative to the Newspack Dashboard menu item. + * + * @var int + */ + public $parent_menu_order = 4; + + /** + * Use a high priorty so that the Advertising parent menu will be created + * prior to submenu items being added. + * + * @var int. + */ + protected $admin_menu_priority = 1; + /** * Constructor. */ public function __construct() { parent::__construct(); - add_action( 'rest_api_init', [ $this, 'register_api_endpoints' ] ); + add_action( 'rest_api_init', array( $this, 'register_api_endpoints' ) ); } /** @@ -69,7 +94,7 @@ public function __construct() { * @return string The wizard name. */ public function get_name() { - return \esc_html__( 'Advertising', 'newspack' ); + return esc_html__( 'Advertising', 'newspack-plugin' ); } /** @@ -81,218 +106,196 @@ public function register_api_endpoints() { register_rest_route( NEWSPACK_API_NAMESPACE, '/wizard/billboard/', - [ + array( 'methods' => \WP_REST_Server::READABLE, - 'callback' => [ $this, 'api_get_advertising' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] + 'callback' => array( $this, 'api_get_advertising' ), + 'permission_callback' => array( $this, 'api_permissions_check' ), + ) ); // Enable one service. register_rest_route( NEWSPACK_API_NAMESPACE, '/wizard/billboard/service/(?P[\a-z]+)', - [ + array( 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_enable_service' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => [ - 'service' => [ - 'sanitize_callback' => [ $this, 'sanitize_service' ], - ], - ], - ] + 'callback' => array( $this, 'api_enable_service' ), + 'permission_callback' => array( $this, 'api_permissions_check' ), + 'args' => array( + 'service' => array( + 'sanitize_callback' => array( $this, 'sanitize_service' ), + ), + ), + ) ); // Disable one service. register_rest_route( NEWSPACK_API_NAMESPACE, '/wizard/billboard/service/(?P[\a-z]+)', - [ + array( 'methods' => \WP_REST_Server::DELETABLE, - 'callback' => [ $this, 'api_disable_service' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => [ - 'service' => [ - 'sanitize_callback' => [ $this, 'sanitize_service' ], - ], - ], - ] + 'callback' => array( $this, 'api_disable_service' ), + 'permission_callback' => array( $this, 'api_permissions_check' ), + 'args' => array( + 'service' => array( + 'sanitize_callback' => array( $this, 'sanitize_service' ), + ), + ), + ) ); // Update GAM credentials. \register_rest_route( NEWSPACK_API_NAMESPACE, '/wizard/billboard/credentials', - [ + array( 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_update_gam_credentials' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => [ - 'onboarding' => [ + 'callback' => array( $this, 'api_update_gam_credentials' ), + 'permission_callback' => array( $this, 'api_permissions_check' ), + 'args' => array( + 'onboarding' => array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', 'default' => false, - ], - 'credentials' => [ + ), + 'credentials' => array( 'type' => 'object', - 'properties' => [ - 'type' => [ + 'properties' => array( + 'type' => array( 'required' => true, 'type' => 'string', - ], - 'project_id' => [ + ), + 'project_id' => array( 'required' => true, 'type' => 'string', - ], - 'private_key_id' => [ + ), + 'private_key_id' => array( 'required' => true, 'type' => 'string', - ], - 'private_key' => [ + ), + 'private_key' => array( 'required' => true, 'type' => 'string', - ], - 'client_email' => [ + ), + 'client_email' => array( 'required' => true, 'type' => 'string', - ], - 'client_id' => [ + ), + 'client_id' => array( 'required' => true, 'type' => 'string', - ], - 'auth_uri' => [ + ), + 'auth_uri' => array( 'required' => true, 'type' => 'string', - ], - 'token_uri' => [ + ), + 'token_uri' => array( 'required' => true, 'type' => 'string', - ], - 'auth_provider_x509_cert_url' => [ + ), + 'auth_provider_x509_cert_url' => array( 'required' => true, 'type' => 'string', - ], - 'client_x509_cert_url' => [ + ), + 'client_x509_cert_url' => array( 'required' => true, 'type' => 'string', - ], - ], - ], - ], - ] + ), + ), + ), + ), + ) ); // Remove GAM credentials. \register_rest_route( NEWSPACK_API_NAMESPACE, '/wizard/billboard/credentials', - [ + array( 'methods' => \WP_REST_Server::DELETABLE, - 'callback' => [ $this, 'api_remove_gam_credentials' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] + 'callback' => array( $this, 'api_remove_gam_credentials' ), + 'permission_callback' => array( $this, 'api_permissions_check' ), + ) ); // Save a ad unit. \register_rest_route( NEWSPACK_API_NAMESPACE, '/wizard/billboard/ad_unit/(?P\d+)', - [ + array( 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_update_adunit' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => [ - 'id' => [ + 'callback' => array( $this, 'api_update_adunit' ), + 'permission_callback' => array( $this, 'api_permissions_check' ), + 'args' => array( + 'id' => array( 'sanitize_callback' => 'absint', - ], - 'name' => [ + ), + 'name' => array( 'sanitize_callback' => 'sanitize_text_field', - ], - 'sizes' => [ - 'sanitize_callback' => [ $this, 'sanitize_sizes' ], - ], - 'fluid' => [ + ), + 'sizes' => array( + 'sanitize_callback' => array( $this, 'sanitize_sizes' ), + ), + 'fluid' => array( 'sanitize_callback' => 'rest_sanitize_boolean', - ], - 'ad_service' => [ + ), + 'ad_service' => array( 'sanitize_callback' => 'sanitize_text_field', - ], - ], - ] + ), + ), + ) ); // Delete a ad unit. \register_rest_route( NEWSPACK_API_NAMESPACE, '/wizard/billboard/ad_unit/(?P\d+)', - [ + array( 'methods' => \WP_REST_Server::DELETABLE, - 'callback' => [ $this, 'api_delete_adunit' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => [ - 'id' => [ + 'callback' => array( $this, 'api_delete_adunit' ), + 'permission_callback' => array( $this, 'api_permissions_check' ), + 'args' => array( + 'id' => array( 'sanitize_callback' => 'absint', - ], - ], - ] + ), + ), + ) ); // Update network code. \register_rest_route( NEWSPACK_API_NAMESPACE, '/wizard/billboard/network_code', - [ + array( 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_update_network_code' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => [ - 'network_code' => [ - 'sanitize_callback' => function( $value ) { + 'callback' => array( $this, 'api_update_network_code' ), + 'permission_callback' => array( $this, 'api_permissions_check' ), + 'args' => array( + 'network_code' => array( + 'sanitize_callback' => function ( $value ) { $raw_codes = explode( ',', $value ); $sanitized_codes = array_reduce( $raw_codes, - function( $acc, $code ) { + function ( $acc, $code ) { $sanitized_code = absint( trim( $code ) ); if ( ! empty( $sanitized_code ) ) { $acc[] = $sanitized_code; } return $acc; }, - [] + array() ); return implode( ',', $sanitized_codes ); }, - ], - 'is_gam' => [ + ), + 'is_gam' => array( 'sanitize_callback' => 'rest_sanitize_boolean', 'default' => false, - ], - ], - ] - ); - - // Create the Media Kit page. - \register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/billboard/media-kit', - [ - 'methods' => \WP_REST_Server::CREATABLE, - 'callback' => [ $this, 'api_create_media_kit_page' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] - ); - - // Unpublish the Media Kit page. - \register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/billboard/media-kit', - [ - 'methods' => \WP_REST_Server::DELETABLE, - 'callback' => [ $this, 'api_unpublish_media_kit_page' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] + ), + ), + ) ); } @@ -306,7 +309,7 @@ public function api_update_network_code( $request ) { // Update GAM or legacy network code. $option_name = $request['is_gam'] ? GAM_Model::OPTION_NAME_GAM_NETWORK_CODE : GAM_Model::OPTION_NAME_LEGACY_NETWORK_CODE; update_option( $option_name, $request['network_code'] ); - return \rest_ensure_response( [] ); + return \rest_ensure_response( array() ); } /** @@ -383,12 +386,12 @@ public function api_update_adunit( $request ) { $configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'newspack-ads' ); $params = $request->get_params(); - $adunit = [ + $adunit = array( 'id' => 0, 'name' => '', - 'sizes' => [], + 'sizes' => array(), 'ad_service' => '', - ]; + ); $args = \wp_parse_args( $params, $adunit ); // Update and existing or add a new ad unit. $adunit = ( 0 === $args['id'] ) @@ -419,48 +422,6 @@ public function api_delete_adunit( $request ) { return \rest_ensure_response( $this->retrieve_data() ); } - /** - * Create the Media Kit page. - */ - public static function api_create_media_kit_page() { - $configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'newspack-ads' ); - $edit_url = $configuration_manager->get_media_kit_page_edit_url(); - if ( ! $edit_url ) { - $configuration_manager->create_media_kit_page(); - } - return \rest_ensure_response( - [ - 'edit_url' => $configuration_manager->get_media_kit_page_edit_url(), - 'page_status' => $configuration_manager->get_media_kit_page_status(), - ] - ); - } - - /** - * Unpublish (revert to draft) the Media Kit page. - */ - public static function api_unpublish_media_kit_page() { - $configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'newspack-ads' ); - $page_id = $configuration_manager->get_media_kit_page_id(); - if ( $page_id ) { - $update = wp_update_post( - [ - 'ID' => $page_id, - 'post_status' => 'draft', - ] - ); - if ( $update === 0 || is_wp_error( $update ) ) { - return rest_ensure_response( new WP_Error( 'update_failed', __( 'Failed to update page status.', 'newspack' ) ) ); - } - } - return \rest_ensure_response( - [ - 'edit_url' => $configuration_manager->get_media_kit_page_edit_url(), - 'page_status' => $configuration_manager->get_media_kit_page_status(), - ] - ); - } - /** * Retrieve all advertising data. * @@ -475,7 +436,7 @@ private function retrieve_data() { $ad_units = $configuration_manager->get_ad_units(); } catch ( \Exception $error ) { $message = $error->getMessage(); - $error = new WP_Error( 'newspack_ad_units', $message ? $message : __( 'Ad Units failed to fetch.', 'newspack' ) ); + $error = new WP_Error( 'newspack_ad_units', $message ? $message : __( 'Ad Units failed to fetch.', 'newspack-plugin' ) ); } if ( \is_wp_error( $ad_units ) ) { @@ -484,7 +445,7 @@ private function retrieve_data() { return array( 'services' => $services, - 'ad_units' => \is_wp_error( $ad_units ) ? [] : $ad_units, + 'ad_units' => \is_wp_error( $ad_units ) ? array() : $ad_units, 'error' => $error, ); } @@ -551,32 +512,30 @@ public function enqueue_scripts_and_styles() { return; } - \wp_enqueue_script( - 'newspack-advertising-wizard', + wp_enqueue_script( + $this->slug, Newspack::plugin_url() . '/dist/billboard.js', $this->get_script_dependencies(), NEWSPACK_PLUGIN_VERSION, true ); - \wp_register_style( - 'newspack-advertising-wizard', + wp_register_style( + $this->slug, Newspack::plugin_url() . '/dist/billboard.css', $this->get_style_dependencies(), NEWSPACK_PLUGIN_VERSION ); - \wp_style_add_data( 'newspack-advertising-wizard', 'rtl', 'replace' ); - \wp_enqueue_style( 'newspack-advertising-wizard' ); + wp_style_add_data( $this->slug, 'rtl', 'replace' ); + wp_enqueue_style( $this->slug ); - $configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'newspack-ads' ); - \wp_localize_script( - 'newspack-advertising-wizard', + wp_localize_script( + $this->slug, 'newspack_ads_wizard', array( - 'iab_sizes' => function_exists( '\Newspack_Ads\get_iab_sizes' ) ? \Newspack_Ads\get_iab_sizes() : [], - 'media_kit_page_edit_url' => $configuration_manager->get_media_kit_page_edit_url(), - 'media_kit_page_status' => $configuration_manager->get_media_kit_page_status(), - 'can_connect_google' => OAuth::is_proxy_configured( 'google' ), + 'iab_sizes' => function_exists( '\Newspack_Ads\get_iab_sizes' ) ? \Newspack_Ads\get_iab_sizes() : array(), + 'mediakit_edit_url' => get_option( 'pmk-page' ) ? get_edit_post_link( get_option( 'pmk-page' ) ) : '', + 'can_connect_google' => OAuth::is_proxy_configured( 'google' ), ) ); } @@ -588,10 +547,10 @@ public function enqueue_scripts_and_styles() { * @return array Sanitized array. */ public static function sanitize_sizes( $sizes ) { - $sizes = is_array( $sizes ) ? $sizes : []; - $sanitized = []; + $sizes = is_array( $sizes ) ? $sizes : array(); + $sanitized = array(); foreach ( $sizes as $size ) { - $size = is_array( $size ) && 2 === count( $size ) ? $size : [ 0, 0 ]; + $size = is_array( $size ) && 2 === count( $size ) ? $size : array( 0, 0 ); $size[0] = absint( $size[0] ); $size[1] = absint( $size[1] ); @@ -599,4 +558,28 @@ public static function sanitize_sizes( $sizes ) { } return $sanitized; } + + /** + * Add a parent menu for all the Advertising wizards (Ads, Sponsors Plugin CPT + Settings tab), + * and a first menu item too. + */ + public function add_page() { + $icon = 'data:image/svg+xml;base64,' . base64_encode( '' ); + add_menu_page( + $this->get_name(), + $this->get_name(), + $this->capability, + $this->slug, + array( $this, 'render_wizard' ), + $icon + ); + add_submenu_page( + $this->slug, + __( 'Advertising / Display Ads', 'newspack-plugin' ), + __( 'Display Ads', 'newspack-plugin' ), + $this->capability, + $this->slug, + array( $this, 'render_wizard' ) + ); + } } diff --git a/includes/wizards/advertising/class-advertising-sponsors.php b/includes/wizards/advertising/class-advertising-sponsors.php new file mode 100644 index 0000000000..68ff301848 --- /dev/null +++ b/includes/wizards/advertising/class-advertising-sponsors.php @@ -0,0 +1,212 @@ +is_wizard_page() ) { + // Initialize Wizards Admin Header. + $this->admin_header_init( + [ + 'tabs' => [ + [ + 'textContent' => esc_html__( 'All Sponsors', 'newspack-plugin' ), + 'href' => admin_url( static::URL ), + ], + [ + 'textContent' => esc_html__( 'Settings', 'newspack-plugin' ), + 'href' => admin_url( static::URL . '&page=newspack-sponsors-settings-admin' ), + ], + ], + 'title' => $this->get_name(), + ] + ); + } + } + + /** + * Get the name for this wizard. + * + * @return string The wizard name. + */ + public function get_name() { + return esc_html__( 'Advertising / Sponsors', 'newspack-plugin' ); + } + + /** + * Check if we are on the Sponsors CPT edit screen. + * + * @return bool true if browsing `edit.php?post_type=newspack_spnsrs_cpt`, false otherwise. + */ + public function is_wizard_page() { + global $pagenow; + if ( 'edit.php' !== $pagenow ) { + return false; + } + return isset( $_GET['post_type'] ) && $_GET['post_type'] === static::CPT_NAME; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + } + + /** + * Enqueue scripts and styles. + */ + public function enqueue_scripts_and_styles() { + if ( ! $this->is_wizard_page() ) { + return; + } + Newspack::load_common_assets(); + wp_register_style( + $this->parent_menu, + Newspack::plugin_url() . '/dist/billboard.css', + $this->get_style_dependencies(), + NEWSPACK_PLUGIN_VERSION + ); + wp_style_add_data( $this->parent_menu, 'rtl', 'replace' ); + wp_enqueue_style( $this->parent_menu ); + } + + /** + * Move Sponsors CPT menu item under the ($) Advertising menu. + */ + public function add_page() { + global $submenu; + if ( isset( $submenu[ $this->parent_menu ] ) ) { + $submenu[ $this->parent_menu ][] = array( // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + __( 'Sponsors', 'newspack-plugin' ), + 'manage_options', + static::URL, + ); + } + + // Register Settings page. + add_submenu_page( + '', // No parent menu item, means its not on the menu. + __( 'Newspack Sponsors: Site-Wide Settings', 'newspack-sponsors' ), + __( 'Settings', 'newspack-sponsors' ), + 'manage_options', + 'newspack-sponsors-settings-admin', + [ Settings::class, 'create_admin_page' ] + ); + } + + /** + * Update the Sponsor CPT args. + * + * @param array $args The sponsor args. + * @param array $post_type The post type name. + * @return array Modified sponsor cpt args. + */ + public function update_sponsors_cpt_args( $args, $post_type ) { + if ( $post_type === static::CPT_NAME ) { + // Move the CPT under the Advertising menu. Necessary to hide default Sponsors CPT menu item. + $args['show_in_menu'] = static::PARENT_URL; + } + return $args; + } + + /** + * Parent file filter. Used to determine active menu items. + * + * @param string $parent_file Parent file to be overridden. + * @return string + */ + public function parent_file( $parent_file ) { + global $pagenow, $typenow; + + if ( in_array( $pagenow, [ 'post.php', 'post-new.php' ] ) && $typenow === static::CPT_NAME ) { + return $this->parent_menu; + } + + if ( isset( $_GET['page'] ) && $_GET['page'] === 'newspack-sponsors-settings-admin' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return static::PARENT_URL; + } + + return $parent_file; + } + + /** + * Submenu file filter. Used to determine active submenu items. + * + * @param string $submenu_file Submenu file to be overridden. + * @return string + */ + public function submenu_file( $submenu_file ) { + if ( isset( $_GET['page'] ) && $_GET['page'] === 'newspack-sponsors-settings-admin' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return static::URL; + } + + return $submenu_file; + } +} diff --git a/includes/wizards/class-popups-wizard.php b/includes/wizards/audience/class-audience-campaigns.php similarity index 90% rename from includes/wizards/class-popups-wizard.php rename to includes/wizards/audience/class-audience-campaigns.php index c0565d2648..326b9af45b 100644 --- a/includes/wizards/class-popups-wizard.php +++ b/includes/wizards/audience/class-audience-campaigns.php @@ -1,37 +1,32 @@ is_wizard_page() ) { + return; + } + + parent::enqueue_scripts_and_styles(); + + $preview_post = ''; + if ( method_exists( 'Newspack_Popups', 'preview_post_permalink' ) ) { + $preview_post = \Newspack_Popups::preview_post_permalink(); + } + + $preview_archive = ''; + if ( method_exists( 'Newspack_Popups', 'preview_archive_permalink' ) ) { + $preview_archive = \Newspack_Popups::preview_archive_permalink(); + } + + $criteria = []; + if ( method_exists( 'Newspack_Popups_Criteria', 'get_registered_criteria' ) ) { + $criteria = \Newspack_Popups_Criteria::get_registered_criteria(); + } + + $newspack_popups_configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'newspack-popups' ); + $custom_placements = $newspack_popups_configuration_manager->get_custom_placements(); + $overlay_placements = $newspack_popups_configuration_manager->get_overlay_placements(); + $overlay_sizes = $newspack_popups_configuration_manager->get_overlay_sizes(); + $preview_query_keys = $newspack_popups_configuration_manager->preview_query_keys(); + + wp_enqueue_script( 'newspack-wizards' ); + + \wp_localize_script( + 'newspack-wizards', + 'newspackAudienceCampaigns', + [ + 'api' => '/' . NEWSPACK_API_NAMESPACE . '/wizard/' . $this->slug, + 'preview_post' => $preview_post, + 'preview_archive' => $preview_archive, + 'frontend_url' => get_site_url(), + 'custom_placements' => $custom_placements, + 'overlay_placements' => $overlay_placements, + 'overlay_sizes' => $overlay_sizes, + 'preview_query_keys' => $preview_query_keys, + 'experimental' => Reader_Activation::is_enabled(), + 'criteria' => $criteria, + ] + ); + } + + /** + * Add Audience top-level and Campaigns subpage to the /wp-admin menu. + */ + public function add_page() { + add_submenu_page( + $this->parent_slug, + $this->get_name(), + esc_html__( 'Campaigns', 'newspack-plugin' ), + $this->capability, + $this->slug, + [ $this, 'render_wizard' ] + ); } /** @@ -69,7 +133,7 @@ public function register_api_endpoints() { '/wizard/' . $this->slug . '/(?P\d+)', [ 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_update_popup' ], + 'callback' => [ $this, 'api_update_prompt' ], 'permission_callback' => [ $this, 'api_permissions_check' ], 'args' => [ 'id' => [ @@ -83,7 +147,7 @@ public function register_api_endpoints() { '/wizard/' . $this->slug . '/(?P\d+)', [ 'methods' => \WP_REST_Server::DELETABLE, - 'callback' => [ $this, 'api_delete_popup' ], + 'callback' => [ $this, 'api_delete_prompt' ], 'permission_callback' => [ $this, 'api_permissions_check' ], 'args' => [ 'id' => [ @@ -97,7 +161,7 @@ public function register_api_endpoints() { '/wizard/' . $this->slug . '/(?P\d+)/restore', [ 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_restore_popup' ], + 'callback' => [ $this, 'api_restore_prompt' ], 'permission_callback' => [ $this, 'api_permissions_check' ], 'args' => [ 'id' => [ @@ -128,7 +192,7 @@ public function register_api_endpoints() { '/wizard/' . $this->slug . '/(?P\d+)/duplicate', [ 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_duplicate_popup' ], + 'callback' => [ $this, 'api_duplicate_prompt' ], 'permission_callback' => [ $this, 'api_permissions_check' ], 'args' => [ 'id' => [ @@ -145,7 +209,7 @@ public function register_api_endpoints() { '/wizard/' . $this->slug . '/(?P\d+)/publish', [ 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_publish_popup' ], + 'callback' => [ $this, 'api_publish_prompt' ], 'permission_callback' => [ $this, 'api_permissions_check' ], 'args' => [ 'id' => [ @@ -159,7 +223,7 @@ public function register_api_endpoints() { '/wizard/' . $this->slug . '/(?P\d+)/publish', [ 'methods' => \WP_REST_Server::DELETABLE, - 'callback' => [ $this, 'api_unpublish_popup' ], + 'callback' => [ $this, 'api_unpublish_prompt' ], 'permission_callback' => [ $this, 'api_permissions_check' ], 'args' => [ 'id' => [ @@ -378,7 +442,7 @@ public function register_api_endpoints() { ], ] ); - + register_rest_route( NEWSPACK_API_NAMESPACE, '/wizard/' . $this->slug . '/subscription-products', @@ -395,71 +459,6 @@ public function register_api_endpoints() { ); } - /** - * Enqueue Wizard scripts and styles. - */ - public function enqueue_scripts_and_styles() { - parent::enqueue_scripts_and_styles(); - - if ( filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) !== $this->slug ) { - return; - } - - \wp_enqueue_script( - 'newspack-popups-wizard', - Newspack::plugin_url() . '/dist/popups.js', - $this->get_script_dependencies( [ 'wp-html-entities', 'wp-date' ] ), - NEWSPACK_PLUGIN_VERSION, - true - ); - - $preview_post = ''; - if ( method_exists( 'Newspack_Popups', 'preview_post_permalink' ) ) { - $preview_post = \Newspack_Popups::preview_post_permalink(); - } - - $preview_archive = ''; - if ( method_exists( 'Newspack_Popups', 'preview_archive_permalink' ) ) { - $preview_archive = \Newspack_Popups::preview_archive_permalink(); - } - - $criteria = []; - if ( method_exists( 'Newspack_Popups_Criteria', 'get_registered_criteria' ) ) { - $criteria = \Newspack_Popups_Criteria::get_registered_criteria(); - } - - $newspack_popups_configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'newspack-popups' ); - $custom_placements = $newspack_popups_configuration_manager->get_custom_placements(); - $overlay_placements = $newspack_popups_configuration_manager->get_overlay_placements(); - $overlay_sizes = $newspack_popups_configuration_manager->get_overlay_sizes(); - $preview_query_keys = $newspack_popups_configuration_manager->preview_query_keys(); - - \wp_localize_script( - 'newspack-popups-wizard', - 'newspack_popups_wizard_data', - [ - 'preview_post' => $preview_post, - 'preview_archive' => $preview_archive, - 'frontend_url' => get_site_url(), - 'custom_placements' => $custom_placements, - 'overlay_placements' => $overlay_placements, - 'overlay_sizes' => $overlay_sizes, - 'preview_query_keys' => $preview_query_keys, - 'experimental' => Reader_Activation::is_enabled(), - 'criteria' => $criteria, - ] - ); - - \wp_register_style( - 'newspack-popups-wizard', - Newspack::plugin_url() . '/dist/popups.css', - $this->get_style_dependencies(), - NEWSPACK_PLUGIN_VERSION - ); - \wp_style_add_data( 'newspack-popups-wizard', 'rtl', 'replace' ); - \wp_enqueue_style( 'newspack-popups-wizard' ); - } - /** * API endpoint callbacks */ @@ -500,34 +499,12 @@ function( $prompt ) { } /** - * Set terms for one Popup. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Response with the info. - */ - public function api_set_popup_terms( $request ) { - $id = $request['id']; - $terms = $request['terms']; - $taxonomy = $request['taxonomy']; - - $newspack_popups_configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'newspack-popups' ); - - $response = $newspack_popups_configuration_manager->set_popup_terms( $id, $terms, $taxonomy ); - - if ( is_wp_error( $response ) ) { - return $response; - } - - return $this->api_get_settings(); - } - - /** - * Update settings for a Pop-up. + * Update settings for a prompt. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response with the info. */ - public function api_update_popup( $request ) { + public function api_update_prompt( $request ) { $id = $request['id']; $config = $request['config']; @@ -557,12 +534,12 @@ public function api_update_popup( $request ) { } /** - * Delete a Pop-up. + * Delete a prompt. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response with complete info to render the Engagement Wizard. */ - public function api_delete_popup( $request ) { + public function api_delete_prompt( $request ) { $id = $request['id']; $popup = get_post( $id ); @@ -578,12 +555,12 @@ public function api_delete_popup( $request ) { } /** - * Restore a deleted a Pop-up. + * Restore a deleted a prompt. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response with complete info to render the Engagement Wizard. */ - public function api_restore_popup( $request ) { + public function api_restore_prompt( $request ) { $id = $request['id']; $popup = get_post( $id ); @@ -607,24 +584,24 @@ public function api_get_duplicate_title( $request ) { } /** - * Duplicate a Pop-up. + * Duplicate a prompt. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response with complete info to render the Engagement Wizard. */ - public function api_duplicate_popup( $request ) { + public function api_duplicate_prompt( $request ) { $cm = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'newspack-popups' ); $duplicate_id = $cm->duplicate_popup( $request['id'], $request['title'] ); return $this->api_get_settings( [ 'duplicated' => $duplicate_id ] ); } /** - * Publish a Pop-up. + * Publish a prompt. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response with complete info to render the Engagement Wizard. */ - public function api_publish_popup( $request ) { + public function api_publish_prompt( $request ) { $id = $request['id']; $popup = get_post( $id ); @@ -635,12 +612,12 @@ public function api_publish_popup( $request ) { } /** - * Unpublish a Pop-up. + * Unpublish a prompt. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response with complete info to render the Engagement Wizard. */ - public function api_unpublish_popup( $request ) { + public function api_unpublish_prompt( $request ) { $id = $request['id']; $popup = get_post( $id ); @@ -937,7 +914,7 @@ public function api_campaign_rename( $request ) { /** * Get non-donation subscription products. - * + * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ @@ -980,9 +957,9 @@ function( $post ) { /** * We only want to show Memberships criteria if the WooCommerce Memberships extension is active. * Otherwise these criteria are meaningless. - * + * * @param array $criteria Registered criteria. - * + * * @return array Filtered criteria. */ public function maybe_unregister_memberships_criteria( $criteria ) { @@ -1000,4 +977,44 @@ function( $config ) use ( $memberships_criteria ) { return $criteria; } + + /** + * Parent file filter. Used to determine active menu items. + * + * @param string $parent_file Parent file to be overridden. + * @return string + */ + public function parent_file( $parent_file ) { + global $pagenow, $typenow; + + if ( ! class_exists( 'Newspack_Popups' ) ) { + return $parent_file; + } + + if ( in_array( $pagenow, [ 'post.php', 'post-new.php' ] ) && $typenow === \Newspack_Popups::NEWSPACK_POPUPS_CPT ) { + return 'newspack-audience-configuration'; + } + + return $parent_file; + } + + /** + * Submenu file filter. Used to determine active submenu items. + * + * @param string $submenu_file Submenu file to be overridden. + * @return string + */ + public function submenu_file( $submenu_file ) { + global $pagenow, $typenow; + + if ( ! class_exists( 'Newspack_Popups' ) ) { + return $submenu_file; + } + + if ( in_array( $pagenow, [ 'post.php', 'post-new.php' ] ) && $typenow === \Newspack_Popups::NEWSPACK_POPUPS_CPT ) { + return $this->slug; + } + + return $submenu_file; + } } diff --git a/includes/wizards/audience/class-audience-donations.php b/includes/wizards/audience/class-audience-donations.php new file mode 100644 index 0000000000..27500d71d2 --- /dev/null +++ b/includes/wizards/audience/class-audience-donations.php @@ -0,0 +1,360 @@ +parent_slug, + $this->get_name(), + esc_html__( 'Donations', 'newspack-plugin' ), + $this->capability, + $this->slug, + [ $this, 'render_wizard' ] + ); + } + + /** + * Enqueue scripts and styles. + */ + public function enqueue_scripts_and_styles() { + if ( ! $this->is_wizard_page() ) { + return; + } + + parent::enqueue_scripts_and_styles(); + + wp_enqueue_script( 'newspack-wizards' ); + + \wp_localize_script( + 'newspack-wizards', + 'newspackAudienceDonations', + [ + 'can_use_name_your_price' => Donations::can_use_name_your_price(), + 'revenue_link' => admin_url( 'admin.php?page=wc-reports' ), + ] + ); + } + + /** + * Register the endpoints needed for the wizard screens. + */ + public function register_api_endpoints() { + // Get donations settings. + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug, + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [ $this, 'api_get_donation_settings' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ] + ); + + // Update donations settings. + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug, + [ + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'api_update_donation_settings' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + 'args' => [ + 'amounts' => [ + 'required' => false, + ], + 'tiered' => [ + 'required' => false, + 'sanitize_callback' => 'Newspack\newspack_string_to_bool', + ], + 'disabledFrequencies' => [ + 'required' => false, + ], + 'platform' => [ + 'required' => false, + 'sanitize_callback' => 'sanitize_text_field', + ], + ], + ] + ); + + // Save additional settings. + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/settings/', + [ + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'api_update_additional_settings' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + 'args' => [ + 'fee_multiplier' => [ + 'sanitize_callback' => 'Newspack\newspack_clean', + 'validate_callback' => function ( $value ) { + if ( (float) $value > 10 ) { + return new WP_Error( + 'newspack_invalid_param', + __( 'Fee multiplier must be smaller than 10.', 'newspack' ) + ); + } + return true; + }, + ], + 'fee_static' => [ + 'sanitize_callback' => 'Newspack\newspack_clean', + ], + 'allow_covering_fees' => [ + 'sanitize_callback' => 'Newspack\newspack_string_to_bool', + ], + 'allow_covering_fees_default' => [ + 'sanitize_callback' => 'Newspack\newspack_string_to_bool', + ], + 'allow_covering_fees_label' => [ + 'sanitize_callback' => 'Newspack\newspack_clean', + ], + 'location_code' => [ + 'sanitize_callback' => 'Newspack\newspack_clean', + ], + ], + ] + ); + + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/emails/(?P\d+)', + [ + 'methods' => \WP_REST_Server::DELETABLE, + 'callback' => [ $this, 'api_reset_donation_email' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ] + ); + } + + /** + * Handler for setting additional settings. + * + * @param object $settings Settings. + * @return WP_REST_Response with the latest settings. + */ + public function update_additional_settings( $settings ) { + if ( isset( $settings['allow_covering_fees'] ) ) { + update_option( 'newspack_donations_allow_covering_fees', intval( $settings['allow_covering_fees'] ) ); + } + if ( isset( $settings['allow_covering_fees_default'] ) ) { + update_option( 'newspack_donations_allow_covering_fees_default', $settings['allow_covering_fees_default'] ); + } + + if ( isset( $settings['allow_covering_fees_label'] ) ) { + update_option( 'newspack_donations_allow_covering_fees_label', sanitize_text_field( $settings['allow_covering_fees_label'] ) ); + } + if ( isset( $settings['fee_multiplier'] ) ) { + update_option( 'newspack_blocks_donate_fee_multiplier', $settings['fee_multiplier'] ); + } + if ( isset( $settings['fee_static'] ) ) { + update_option( 'newspack_blocks_donate_fee_static', $settings['fee_static'] ); + } + return $this->fetch_all_data(); + } + + /** + * Save additional payment method settings (e.g. transaction fees). + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response. + */ + public function api_update_additional_settings( $request ) { + $params = $request->get_params(); + $result = $this->update_additional_settings( $params ); + return \rest_ensure_response( $result ); + } + + /** + * API endpoint for setting the donation settings. + * + * @param WP_REST_Request $request Request containing settings. + * @return WP_REST_Response with the latest settings. + */ + public function api_update_donation_settings( $request ) { + return $this->update_donation_settings( $request->get_params() ); + } + + /** + * Handler for setting the donation settings. + * + * @param object $settings Donation settings. + * @return WP_REST_Response with the latest settings. + */ + public function update_donation_settings( $settings ) { + $donations_response = Donations::set_donation_settings( $settings ); + if ( is_wp_error( $donations_response ) ) { + return rest_ensure_response( $donations_response ); + } + return \rest_ensure_response( $this->fetch_all_data() ); + } + + /** + * Fetch all data needed to render the Wizard + * + * @return Array + */ + public function fetch_all_data() { + $platform = Donations::get_platform_slug(); + + $args = [ + 'platform_data' => [ + 'platform' => $platform, + ], + 'additional_settings' => [ + 'allow_covering_fees' => boolval( get_option( 'newspack_donations_allow_covering_fees', true ) ), + 'allow_covering_fees_default' => boolval( get_option( 'newspack_donations_allow_covering_fees_default', false ) ), + 'allow_covering_fees_label' => get_option( 'newspack_donations_allow_covering_fees_label', '' ), + 'fee_multiplier' => get_option( 'newspack_blocks_donate_fee_multiplier', '2.9' ), + 'fee_static' => get_option( 'newspack_blocks_donate_fee_static', '0.3' ), + ], + 'donation_data' => Donations::get_donation_settings(), + 'donation_page' => Donations::get_donation_page_info(), + ]; + if ( 'wc' === $platform ) { + $plugin_status = true; + $managed_plugins = Plugin_Manager::get_managed_plugins(); + $required_plugins = [ + 'woocommerce', + 'woocommerce-subscriptions', + ]; + foreach ( $required_plugins as $required_plugin ) { + if ( 'active' !== $managed_plugins[ $required_plugin ]['Status'] ) { + $plugin_status = false; + } + } + $args = wp_parse_args( + [ + 'plugin_status' => $plugin_status, + ], + $args + ); + } elseif ( Donations::is_platform_nrh() ) { + $nrh_config = NRH::get_settings(); + $args['platform_data'] = wp_parse_args( $nrh_config, $args['platform_data'] ); + } + return $args; + } + + /** + * API endpoint for getting donation settings. + * + * @return WP_REST_Response containing info. + */ + public function api_get_donation_settings() { + if ( Donations::is_platform_wc() ) { + $required_plugins_installed = $this->check_required_plugins_installed(); + if ( is_wp_error( $required_plugins_installed ) ) { + return rest_ensure_response( $required_plugins_installed ); + } + } + + return rest_ensure_response( $this->fetch_all_data() ); + } + + /** + * Reset donation email template. + * We acheive this by trashing the email template post. + * + * @param WP_REST_Request $request Request object. + * + * @return WP_Error|WP_REST_Response + */ + public function api_reset_donation_email( $request ) { + $params = $request->get_params(); + $id = $params['id']; + $email = get_post( $id ); + + if ( $email === null || $email->post_type !== Emails::POST_TYPE ) { + return new WP_Error( + 'newspack_reset_donation_email_invalid_arg', + esc_html__( 'Invalid argument: no email template matches the provided id.', 'newspack-plugin' ), + [ + 'status' => 400, + 'level' => 'notice', + ] + ); + } + + if ( ! wp_trash_post( $id ) ) { + return new WP_Error( + 'newspack_reset_donation_email_reset_failed', + esc_html__( 'Reset failed: unable to reset email template.', 'newspack-plugin' ), + [ + 'status' => 400, + 'level' => 'notice', + ] + ); + } + + return rest_ensure_response( Emails::get_emails( array_values( Reader_Revenue_Emails::EMAIL_TYPES ), false ) ); + } + + /** + * Check whether WooCommerce is installed and active. + * + * @return bool | WP_Error True on success, WP_Error on failure. + */ + protected function check_required_plugins_installed() { + if ( ! function_exists( 'WC' ) ) { + return new WP_Error( + 'newspack_missing_required_plugin', + esc_html__( 'The WooCommerce plugin is not installed and activated. Install and/or activate it to access this feature.', 'newspack' ), + [ + 'status' => 400, + 'level' => 'fatal', + ] + ); + } + + return true; + } +} diff --git a/includes/wizards/audience/class-audience-subscriptions.php b/includes/wizards/audience/class-audience-subscriptions.php new file mode 100644 index 0000000000..1a8bdc6234 --- /dev/null +++ b/includes/wizards/audience/class-audience-subscriptions.php @@ -0,0 +1,99 @@ +parent_slug, + $this->get_name(), + esc_html__( 'Subscriptions', 'newspack-plugin' ), + $this->capability, + $this->slug, + [ $this, 'render_wizard' ] + ); + } + + /** + * Enqueue scripts and styles. + */ + public function enqueue_scripts_and_styles() { + if ( ! $this->is_wizard_page() ) { + return; + } + + parent::enqueue_scripts_and_styles(); + wp_enqueue_script( 'newspack-wizards' ); + wp_localize_script( + 'newspack-wizards', + 'newspackAudienceSubscriptions', + [ + 'tabs' => [ + [ + 'path' => '/configuration', + 'title' => esc_html__( 'Configuration', 'newspack-plugin' ), + 'header' => esc_html__( 'Manage Subscriptions settings in Woo Memberships', 'newspack-plugin' ), + 'description' => esc_html__( 'You can manage the details of your subscription offerings in the Woo Memberships plugin.', 'newspack-plugin' ), + 'href' => admin_url( 'edit.php?post_type=wc_membership_plan' ), + 'btn_text' => esc_html__( 'Manage Subscriptions', 'newspack-plugin' ), + ], + /** + * TODO: Add revenue tab when `custom revenue report` is completed, [see related comment](https://github.com/Automattic/newspack-plugin/pull/3565#discussion_r1891884248). + */ + + // phpcs:disable Squiz.PHP.CommentedOutCode.Found + + /* + [ + 'path' => '/revenue', + 'title' => esc_html__( 'Revenue', 'newspack-plugin' ), + 'header' => esc_html__( 'View Subscription Revenue in WooCommerce', 'newspack-plugin' ), + 'description' => esc_html__( 'You can view revenue data from Donations and Subscriptions in the WooCommerce Plugin.', 'newspack-plugin' ), + 'href' => admin_url( 'admin.php?page=wc-reports' ), + 'btn_text' => esc_html__( 'View Subscription Revenue', 'newspack-plugin' ), + ], + */ + + // phpcs:enable Squiz.PHP.CommentedOutCode.Found + ], + ] + ); + } +} diff --git a/includes/wizards/audience/class-audience-wizard.php b/includes/wizards/audience/class-audience-wizard.php new file mode 100644 index 0000000000..a54143841c --- /dev/null +++ b/includes/wizards/audience/class-audience-wizard.php @@ -0,0 +1,845 @@ +is_wizard_page() ) { + return; + } + parent::enqueue_scripts_and_styles(); + $salesforce_settings = Salesforce::get_salesforce_settings(); + $data = [ + 'has_memberships' => class_exists( 'WC_Memberships' ), + 'reader_activation_url' => admin_url( 'admin.php?page=newspack-audience#/' ), + 'esp_metadata_fields' => Reader_Activation\Sync\Metadata::get_default_fields(), + 'can_use_salesforce' => ! empty( $salesforce_settings['client_id'] ), + 'salesforce_redirect_url' => Salesforce::get_redirect_url(), + ]; + + if ( method_exists( 'Newspack\Newsletters\Subscription_Lists', 'get_add_new_url' ) ) { + $data['new_subscription_lists_url'] = Newsletters\Subscription_Lists::get_add_new_url(); + } + + if ( method_exists( 'Newspack_Newsletters_Subscription', 'get_lists' ) ) { + $data['available_newsletter_lists'] = Newspack_Newsletters_Subscription::get_lists(); + } + + $newspack_popups = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'newspack-popups' ); + + if ( $newspack_popups->is_configured() ) { + $data['preview_query_keys'] = $newspack_popups->preview_query_keys(); + $data['preview_post'] = $newspack_popups->preview_post(); + $data['preview_archive'] = $newspack_popups->preview_archive(); + } + + $data['is_skipped_campaign_setup'] = Reader_Activation::is_skipped( 'ras_campaign' ); + + wp_enqueue_script( 'newspack-wizards' ); + + wp_localize_script( + 'newspack-wizards', + 'newspackAudience', + $data + ); + } + + /** + * Add Audience top-level and Configuration subpage to the /wp-admin menu. + */ + public function add_page() { + // svg source - https://wphelpers.dev/icons/people + // SVG generated via https://boxy-svg.com/ with path width/height 20px. + $icon = 'data:image/svg+xml;base64,' . base64_encode( + ' + +' + ); + add_menu_page( + $this->get_name(), + __( 'Audience', 'newspack-plugin' ), + $this->capability, + $this->slug, + [ $this, 'render_wizard' ], + $icon + ); + add_submenu_page( + $this->slug, + $this->get_name(), + __( 'Setup', 'newspack-plugin' ), + $this->capability, + $this->slug, + [ $this, 'render_wizard' ] + ); + } + + /** + * Register the endpoints needed for the wizard screens. + */ + public function register_api_endpoints() { + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/audience-management', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'api_get_reader_activation_settings' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ] + ); + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/audience-management', + [ + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'api_update_reader_activation_settings' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ] + ); + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/audience-management/activate', + [ + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'api_activate_reader_activation' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ] + ); + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/audience-management/emails/(?P\d+)', + [ + 'methods' => \WP_REST_Server::DELETABLE, + 'callback' => [ $this, 'api_reset_reader_activation_email' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ] + ); + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/audience-management/skip', + [ + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'api_skip_prerequisite' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + 'args' => [ + 'prerequisite' => [ + 'sanitize_callback' => 'sanitize_text_field', + ], + 'skip' => [ + 'sanitize_callback' => 'Newspack\newspack_string_to_bool', + ], + ], + ] + ); + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/content-gating', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'api_get_content_gating_settings' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ] + ); + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/content-gating', + [ + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'api_update_content_gating_settings' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ] + ); + + // Get Salesforce settings. + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/salesforce', + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [ $this, 'api_get_salesforce_settings' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ] + ); + + // Save Salesforce settings. + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/salesforce', + [ + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'api_update_salesforce_settings' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + 'args' => [ + 'client_id' => [ + 'sanitize_callback' => 'sanitize_text_field', + ], + 'client_secret' => [ + 'sanitize_callback' => 'sanitize_text_field', + ], + 'access_token' => [ + 'sanitize_callback' => 'sanitize_text_field', + ], + 'refresh_token' => [ + 'sanitize_callback' => 'sanitize_text_field', + ], + ], + ] + ); + + // Get payment settings data. + \register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/payment', + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [ $this, 'api_get_payment_settings' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ] + ); + + // Save basic data about reader revenue platform. + \register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/payment', + [ + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'api_update_payment_settings' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + 'args' => [ + 'platform' => [ + 'sanitize_callback' => 'Newspack\newspack_clean', + 'validate_callback' => [ $this, 'api_validate_platform' ], + ], + 'nrh_organization_id' => [ + 'sanitize_callback' => 'Newspack\newspack_clean', + 'validate_callback' => [ $this, 'api_validate_not_empty' ], + ], + 'nrh_custom_domain' => [ + 'sanitize_callback' => 'Newspack\newspack_clean', + ], + 'nrh_salesforce_campaign_id' => [ + 'sanitize_callback' => 'Newspack\newspack_clean', + ], + 'donor_landing_page' => [ + 'sanitize_callback' => 'Newspack\newspack_clean', + ], + ], + ] + ); + + // Get billing fields info. + \register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/billing-fields', + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [ $this, 'api_get_billing_fields' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ] + ); + + // Update billing fields info. + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/billing-fields', + [ + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'api_update_billing_fields' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + 'args' => [ + 'billing_fields' => [ + 'sanitize_callback' => [ $this, 'sanitize_string_array' ], + 'validate_callback' => [ $this, 'api_validate_not_empty' ], + ], + ], + ] + ); + + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/checkout-configuration', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ Reader_Activation::class, 'get_checkout_configuration' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ] + ); + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/checkout-configuration', + [ + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'api_update_checkout_configuration' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + 'args' => [ + 'billing_fields' => [ + 'sanitize_callback' => [ $this, 'sanitize_string_array' ], + 'validate_callback' => [ $this, 'api_validate_not_empty' ], + ], + ], + ] + ); + + // Save Stripe info. + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/payment/stripe/', + [ + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'api_update_stripe_settings' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + 'args' => [ + 'activate' => [ + 'sanitize_callback' => 'Newspack\newspack_string_to_bool', + ], + 'enabled' => [ + 'sanitize_callback' => 'Newspack\newspack_string_to_bool', + ], + 'location_code' => [ + 'sanitize_callback' => 'Newspack\newspack_clean', + ], + ], + ] + ); + + // Save payment gatway info. + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/payment/gateway/', + [ + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'api_update_gateway_settings' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + 'args' => [ + 'activate' => [ + 'sanitize_callback' => 'Newspack\newspack_string_to_bool', + ], + 'enabled' => [ + 'sanitize_callback' => 'Newspack\newspack_string_to_bool', + ], + 'slug' => [ + 'sanitize_callback' => 'Newspack\newspack_clean', + ], + ], + ] + ); + } + + /** + * Get reader activation settings. + * + * @return WP_REST_Response + */ + public function api_get_reader_activation_settings() { + return rest_ensure_response( + [ + 'config' => Reader_Activation::get_settings(), + 'prerequisites_status' => Reader_Activation::get_prerequisites_status(), + 'memberships' => self::get_memberships_settings(), + 'can_esp_sync' => Reader_Activation\ESP_Sync::can_esp_sync( true ), + ] + ); + } + + /** + * Update reader activation settings. + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response + */ + public function api_update_reader_activation_settings( $request ) { + $args = $request->get_params(); + foreach ( $args as $key => $value ) { + Reader_Activation::update_setting( $key, $value ); + } + + return rest_ensure_response( + [ + 'config' => Reader_Activation::get_settings(), + 'prerequisites_status' => Reader_Activation::get_prerequisites_status(), + 'memberships' => self::get_memberships_settings(), + 'can_esp_sync' => Reader_Activation\ESP_Sync::can_esp_sync( true ), + ] + ); + } + + /** + * Reset reader activation email template. + * We acheive this by trashing the email template post. + * + * @param WP_REST_Request $request Request object. + * + * @return WP_Error|WP_REST_Response + */ + public function api_reset_reader_activation_email( $request ) { + $params = $request->get_params(); + $id = $params['id']; + $email = get_post( $id ); + + if ( $email === null || $email->post_type !== Emails::POST_TYPE ) { + return new WP_Error( + 'newspack_reset_reader_activation_email_invalid_arg', + esc_html__( 'Invalid argument: no email template matches the provided id.', 'newspack-plugin' ), + [ + 'status' => 400, + 'level' => 'notice', + ] + ); + } + + if ( ! \wp_trash_post( $id ) ) { + return new WP_Error( + 'newspack_reset_reader_activation_email_reset_failed', + esc_html__( 'Reset failed: unable to reset email template.', 'newspack-plugin' ), + [ + 'status' => 400, + 'level' => 'notice', + ] + ); + } + + return rest_ensure_response( Emails::get_emails( array_values( Reader_Activation_Emails::EMAIL_TYPES ), false ) ); + } + + /** + * Activate reader activation and publish RAS prompts/segments. + * + * @param WP_REST_Request $request WP Rest Request object. + * @return WP_REST_Response + */ + public function api_activate_reader_activation( WP_REST_Request $request ) { + $skip_activation = $request->get_param( 'skip_activation' ) ?? false; + $response = $skip_activation ? true : Reader_Activation::activate(); + + if ( is_wp_error( $response ) ) { + return new WP_REST_Response( [ 'message' => $response->get_error_message() ], 400 ); + } + + if ( true === $response ) { + Reader_Activation::update_setting( 'enabled', true ); + } + + return rest_ensure_response( $response ); + } + + /** + * Activate reader activation and publish RAS prompts/segments. + * + * @param WP_REST_Request $request WP Rest Request object. + * @return WP_REST_Response + */ + public function api_skip_prerequisite( WP_REST_Request $request ) { + $preqrequisite = $request->get_param( 'prerequisite' ); + $skip = $request->get_param( 'skip' ); + $skip_campaign_setup = Reader_Activation::skip( $preqrequisite, $skip ); + if ( ! $skip_campaign_setup ) { + return new WP_REST_Response( [ 'message' => __( 'Error skipping prerequisite.', 'newspack-plugin' ) ], 400 ); + } + + return rest_ensure_response( + [ + 'config' => Reader_Activation::get_settings(), + 'prerequisites_status' => Reader_Activation::get_prerequisites_status(), + 'memberships' => self::get_memberships_settings(), + 'can_esp_sync' => Reader_Activation\ESP_Sync::can_esp_sync( true ), + ] + ); + } + + /** + * Get content gating settings. + * + * @return WP_REST_Response + */ + public function api_get_content_gating_settings() { + return rest_ensure_response( self::get_memberships_settings() ); + } + + /** + * Update content gating settings. + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response + */ + public function api_update_content_gating_settings( $request ) { + $args = $request->get_params(); + if ( isset( $args['require_all_plans'] ) ) { + Memberships::set_require_all_plans_setting( (bool) $args['require_all_plans'] ); + } + if ( isset( $args['show_on_subscription_tab'] ) ) { + Memberships::set_show_on_subscription_tab_setting( (bool) $args['show_on_subscription_tab'] ); + } + return rest_ensure_response( self::get_memberships_settings() ); + } + + /** + * API endpoint to get Salesforce settings. + * + * @return WP_REST_Response with Salesforce settings. + */ + public function api_get_salesforce_settings() { + return \rest_ensure_response( Salesforce::get_salesforce_settings() ); + } + + /** + * API endpoint for setting Salesforce settings. + * + * @param WP_REST_Request $request Request containing settings. + * @return WP_REST_Response with the latest settings. + */ + public function api_update_salesforce_settings( $request ) { + $salesforce_response = Salesforce::set_salesforce_settings( $request->get_params() ); + if ( is_wp_error( $salesforce_response ) ) { + return rest_ensure_response( $salesforce_response ); + } + return \rest_ensure_response( Salesforce::get_salesforce_settings() ); + } + + /** + * Validate platform ID. + * + * @param mixed $value A param value. + * @return bool + */ + public function api_validate_platform( $value ) { + return in_array( $value, [ 'nrh', 'wc', 'other' ] ); + } + + /** + * Get payment settings. + * + * @return WP_REST_Response containing ad units info. + */ + public function api_get_payment_settings() { + return \rest_ensure_response( $this->get_payment_data() ); + } + + /** + * Sanitize payment billing fields. + * + * @param mixed $value A param value. + * + * @return array + */ + public function sanitize_string_array( $value ) { + return is_array( $value ) ? array_map( 'sanitize_text_field', $value ) : []; + } + + /** + * Set payment settings. + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Boolean success. + */ + public function api_update_payment_settings( $request ) { + $params = $request->get_params(); + + Donations::set_platform_slug( $params['platform'] ); + + // Update NRH settings. + if ( Donations::is_platform_nrh() ) { + NRH::update_settings( $params ); + } + + // Ensure that any Reader Revenue settings changed while the platform wasn't WC are persisted to WC products. + if ( Donations::is_platform_wc() ) { + Donations::update_donation_product( Donations::get_donation_settings() ); + } + + return \rest_ensure_response( $this->get_payment_data() ); + } + + /** + * API callback to get billing fields. + * + * @return WP_REST_Response Response. + */ + public function api_get_billing_fields() { + return \rest_ensure_response( $this->get_billing_fields() ); + } + + /** + * Update billing fields. + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response. + */ + public function api_update_billing_fields( $request ) { + $params = $request->get_params(); + Donations::update_billing_fields( $params['billing_fields'] ); + return \rest_ensure_response( $this->get_billing_fields() ); + } + + /** + * API callback to update checkout configuration. + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response. + */ + public function api_update_checkout_configuration( WP_REST_Request $request ): WP_REST_Response { + $params = $request->get_params(); + $checkout_options = [ + [ Reader_Activation::OPTIONS_PREFIX . 'woocommerce_registration_required', $params['woocommerce_registration_required'] ], + [ Reader_Activation::OPTIONS_PREFIX . 'woocommerce_post_checkout_success_text', $params['woocommerce_post_checkout_success_text'] ], + [ Reader_Activation::OPTIONS_PREFIX . 'woocommerce_checkout_privacy_policy_text', $params['woocommerce_checkout_privacy_policy_text'] ], + [ Reader_Activation::OPTIONS_PREFIX . 'woocommerce_post_checkout_registration_success_text', $params['woocommerce_post_checkout_registration_success_text'] ], + ]; + foreach ( $checkout_options as $option ) { + [ $key, $value ] = $option; + update_option( $key, $value ); + } + return rest_ensure_response( Reader_Activation::get_checkout_configuration() ); + } + + /** + * Get billing fields data. + */ + public function get_billing_fields() { + $wc_installed = 'active' === Plugin_Manager::get_managed_plugin_status( 'woocommerce' ); + + $available_billing_fields = []; + $order_notes_field = []; + + if ( $wc_installed && Donations::is_platform_wc() ) { + $checkout = new \WC_Checkout(); + $fields = $checkout->get_checkout_fields(); + if ( ! empty( $fields['order']['order_comments'] ) ) { + $order_notes_field = $fields['order']['order_comments']; + } + if ( ! empty( $fields['billing'] ) ) { + $available_billing_fields = $fields['billing']; + } + } + return [ + 'available_billing_fields' => $available_billing_fields, + 'billing_fields' => Donations::get_billing_fields(), + 'order_notes_field' => $order_notes_field, + ]; + } + + /** + * Save Stripe settings. + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response. + */ + public function api_update_stripe_settings( $request ) { + $params = $request->get_params(); + $result = $this->update_stripe_settings( $params ); + return \rest_ensure_response( $result ); + } + + /** + * Handler for setting Stripe settings. + * + * @param object $settings Stripe settings. + * @return WP_REST_Response with the latest settings. + */ + public function update_stripe_settings( $settings ) { + if ( ! empty( $settings['activate'] ) ) { + // If activating the Stripe Gateway plugin, let's enable it. + $settings = [ 'enabled' => true ]; + } + $result = Stripe_Connection::update_stripe_data( $settings ); + if ( \is_wp_error( $result ) ) { + return $result; + } + + return $this->get_payment_data(); + } + + /** + * Save WooPayments settings. + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response. + */ + public function api_update_gateway_settings( $request ) { + $wc_configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'woocommerce' ); + + $params = $request->get_params(); + if ( ! isset( $params['slug'] ) ) { + return \rest_ensure_response( + new WP_Error( 'newspack_invalid_param', __( 'Gateway slug is required.', 'newspack' ) ) + ); + } + $slug = $params['slug']; + unset( $params['slug'] ); + $result = $wc_configuration_manager->update_gateway_settings( $slug, $params ); + return \rest_ensure_response( $result ); + } + + /** + * Get payment data for the wizard. + * + * @return Array + */ + public function get_payment_data() { + $platform = Donations::get_platform_slug(); + $wc_configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'woocommerce' ); + + $args = [ + 'payment_gateways' => [ + 'stripe' => Stripe_Connection::get_stripe_data(), + 'woocommerce_payments' => $wc_configuration_manager->gateway_data( 'woocommerce_payments' ), + 'ppcp-gateway' => $wc_configuration_manager->gateway_data( 'ppcp-gateway' ), + ], + 'platform_data' => [ + 'platform' => $platform, + ], + 'is_ssl' => is_ssl(), + 'errors' => [], + ]; + if ( 'wc' === $platform ) { + $plugin_status = true; + $managed_plugins = Plugin_Manager::get_managed_plugins(); + $required_plugins = [ + 'woocommerce', + 'woocommerce-subscriptions', + ]; + foreach ( $required_plugins as $required_plugin ) { + if ( 'active' !== $managed_plugins[ $required_plugin ]['Status'] ) { + $plugin_status = false; + } + } + $args = wp_parse_args( + [ + 'plugin_status' => $plugin_status, + ], + $args + ); + } elseif ( Donations::is_platform_nrh() ) { + $nrh_config = NRH::get_settings(); + $args['platform_data'] = wp_parse_args( $nrh_config, $args['platform_data'] ); + } + return $args; + } + + /** + * Get memberships settings. + * + * @return array + */ + private static function get_memberships_settings() { + return [ + 'edit_gate_url' => Memberships::get_edit_gate_url(), + 'gate_status' => get_post_status( Memberships::get_gate_post_id() ), + 'plans' => Memberships::get_plans(), + 'require_all_plans' => Memberships::get_require_all_plans_setting(), + 'show_on_subscription_tab' => Memberships::get_show_on_subscription_tab_setting(), + ]; + } + + /** + * Parent file filter. Used to determine active menu items. + * + * @param string $parent_file Parent file to be overridden. + * @return string + */ + public function parent_file( $parent_file ) { + global $pagenow, $typenow; + + $cpts = [ + Memberships::GATE_CPT, + Emails::POST_TYPE, + ]; + + if ( in_array( $pagenow, [ 'post.php', 'post-new.php' ] ) && in_array( $typenow, $cpts ) ) { + return $this->slug; + } + + return $parent_file; + } + + /** + * Submenu file filter. Used to determine active submenu items. + * + * @param string $submenu_file Submenu file to be overridden. + * @return string + */ + public function submenu_file( $submenu_file ) { + global $pagenow, $typenow; + + $cpts = [ + Memberships::GATE_CPT, + Emails::POST_TYPE, + ]; + + if ( in_array( $pagenow, [ 'post.php', 'post-new.php' ] ) && in_array( $typenow, $cpts ) ) { + return $this->slug; + } + + return $submenu_file; + } +} diff --git a/includes/wizards/class-analytics-wizard.php b/includes/wizards/class-analytics-wizard.php deleted file mode 100644 index 082d12dfb7..0000000000 --- a/includes/wizards/class-analytics-wizard.php +++ /dev/null @@ -1,159 +0,0 @@ - \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_set_ga4_credentials' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => [ - 'measurement_id' => [ - 'sanitize_callback' => 'sanitize_text_field', - 'validate_callback' => [ $this, 'validate_measurement_id' ], - ], - 'measurement_protocol_secret' => [ - 'sanitize_callback' => 'sanitize_text_field', - ], - ], - ] - ); - } - - /** - * Validates the Measurement ID - * - * @param string $value The value to validate. - * @return bool - */ - public function validate_measurement_id( $value ) { - return is_string( $value ) && strpos( $value, 'G-' ) === 0; - } - - /** - * Gets the credentials for the GA4 API. - * - * @return array - */ - public static function get_ga4_credentials() { - $measurement_protocol_secret = get_option( 'ga4_measurement_protocol_secret', '' ); - $measurement_id = get_option( 'ga4_measurement_id', '' ); - return compact( 'measurement_protocol_secret', 'measurement_id' ); - } - - /** - * Updates the GA4 crendetials - * - * @param WP_REST_Request $request The REST request. - * @return WP_REST_Response|WP_Error - */ - public function api_set_ga4_credentials( $request ) { - $measurement_id = $request->get_param( 'measurement_id' ); - $measurement_protocol_secret = $request->get_param( 'measurement_protocol_secret' ); - - if ( ! $measurement_id || ! $measurement_protocol_secret ) { - return new \WP_Error( - 'newspack_analytics_wizard_invalid_params', - \esc_html__( 'Invalid parameters.', 'newspack' ), - [ 'status' => 400 ] - ); - } - - update_option( 'ga4_measurement_id', $measurement_id ); - update_option( 'ga4_measurement_protocol_secret', $measurement_protocol_secret ); - - return rest_ensure_response( $this->get_ga4_credentials() ); - } - - /** - * Enqueue Subscriptions Wizard scripts and styles. - */ - public function enqueue_scripts_and_styles() { - parent::enqueue_scripts_and_styles(); - - if ( filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) !== $this->slug ) { - return; - } - - \wp_enqueue_script( - 'newspack-analytics-wizard', - Newspack::plugin_url() . '/dist/analytics.js', - [ 'wp-components', 'wp-api-fetch' ], - NEWSPACK_PLUGIN_VERSION, - true - ); - - \wp_localize_script( - 'newspack-analytics-wizard', - 'newspack_analytics_wizard_data', - [ - 'ga4_credentials' => $this->get_ga4_credentials(), - ] - ); - - \wp_register_style( - 'newspack-analytics-wizard', - Newspack::plugin_url() . '/dist/analytics.css', - $this->get_style_dependencies(), - NEWSPACK_PLUGIN_VERSION - ); - \wp_style_add_data( 'newspack-analytics-wizard', 'rtl', 'replace' ); - \wp_enqueue_style( 'newspack-analytics-wizard' ); - } -} diff --git a/includes/wizards/class-components-demo.php b/includes/wizards/class-components-demo.php index cfe3138782..4f0fd7a20c 100644 --- a/includes/wizards/class-components-demo.php +++ b/includes/wizards/class-components-demo.php @@ -30,13 +30,6 @@ class Components_Demo extends Wizard { */ protected $capability = 'manage_options'; - /** - * Priority setting for ordering admin submenu items. - * - * @var int. - */ - protected $menu_priority = 100; - /** * Whether the wizard should be displayed in the Newspack submenu. * diff --git a/includes/wizards/class-connections-wizard.php b/includes/wizards/class-connections-wizard.php deleted file mode 100644 index c5cb4293e9..0000000000 --- a/includes/wizards/class-connections-wizard.php +++ /dev/null @@ -1,83 +0,0 @@ -slug ) { - return; - } - - \wp_register_script( - 'newspack-connections-wizard', - Newspack::plugin_url() . '/dist/connections.js', - $this->get_script_dependencies( [] ), - NEWSPACK_PLUGIN_VERSION, - true - ); - \wp_localize_script( - 'newspack-connections-wizard', - 'newspack_connections_data', - [ - 'can_connect_google' => OAuth::is_proxy_configured( 'google' ), - 'can_connect_fivetran' => OAuth::is_proxy_configured( 'fivetran' ), - 'can_use_webhooks' => defined( 'NEWSPACK_EXPERIMENTAL_WEBHOOKS' ) && NEWSPACK_EXPERIMENTAL_WEBHOOKS, - 'can_use_everlit' => Everlit_Configuration_Manager::is_enabled(), - 'can_use_jetpack_sso' => class_exists( 'Jetpack' ) && defined( 'NEWSPACK_MANAGER_FILE' ), - ] - ); - \wp_enqueue_script( 'newspack-connections-wizard' ); - - \wp_register_style( - 'newspack-connections-wizard', - Newspack::plugin_url() . '/dist/connections.css', - $this->get_style_dependencies(), - NEWSPACK_PLUGIN_VERSION - ); - \wp_style_add_data( 'newspack-connections-wizard', 'rtl', 'replace' ); - \wp_enqueue_style( 'newspack-connections-wizard' ); - } -} diff --git a/includes/wizards/class-dashboard.php b/includes/wizards/class-dashboard.php deleted file mode 100644 index 1e390116c2..0000000000 --- a/includes/wizards/class-dashboard.php +++ /dev/null @@ -1,198 +0,0 @@ - 'site-design', - 'name' => Wizards::get_name( 'site-design' ), - 'url' => Wizards::get_url( 'site-design' ), - 'description' => esc_html__( 'Customize the look and feel of your site', 'newspack' ), - ], - [ - 'slug' => 'reader-revenue', - 'name' => Wizards::get_name( 'reader-revenue' ), - 'url' => Wizards::get_url( 'reader-revenue' ), - 'description' => esc_html__( 'Generate revenue from your customers', 'newspack' ), - ], - [ - 'slug' => 'advertising', - 'name' => Wizards::get_name( 'advertising' ), - 'url' => Wizards::get_url( 'advertising' ), - 'description' => esc_html__( 'Monetize your content through ads', 'newspack' ), - ], - [ - 'slug' => 'syndication', - 'name' => Wizards::get_name( 'syndication' ), - 'url' => Wizards::get_url( 'syndication' ), - 'description' => esc_html__( 'Distribute your content across multiple websites', 'newspack' ), - ], - [ - 'slug' => 'analytics', - 'name' => Wizards::get_name( 'analytics' ), - 'url' => Wizards::get_url( 'analytics' ), - 'description' => esc_html__( 'Track traffic and activity', 'newspack' ), - ], - [ - 'slug' => 'seo', - 'name' => Wizards::get_name( 'seo' ), - 'url' => Wizards::get_url( 'seo' ), - 'description' => esc_html__( 'Configure basic SEO settings', 'newspack' ), - ], - [ - 'slug' => 'health-check', - 'name' => Wizards::get_name( 'health-check' ), - 'url' => Wizards::get_url( 'health-check' ), - 'description' => esc_html__( 'Verify and correct site health issues', 'newspack' ), - ], - [ - 'slug' => 'engagement', - 'name' => Wizards::get_name( 'engagement' ), - 'url' => Wizards::get_url( 'engagement' ), - 'description' => esc_html__( 'Newsletters, social, recirculation', 'newspack' ), - ], - [ - 'slug' => 'popups', - 'name' => Wizards::get_name( 'popups' ), - 'url' => Wizards::get_url( 'popups' ), - 'description' => esc_html__( 'Reach your readers with configurable campaigns', 'newspack' ), - ], - [ - 'slug' => 'connections', - 'name' => Wizards::get_name( 'connections' ), - 'url' => Wizards::get_url( 'connections' ), - 'description' => esc_html__( 'Connections to third-party services', 'newspack' ), - ], - ]; - - /** - * Filters the dashboard items. - * - * @param array $dashboard { - * Dashboard items. - * - * @type string $slug Slug. - * @type string $name Displayed name. - * @type string $url URL to redirect to. - * @type string $description Item description. - * @type bool $is_externam If true, the URL will be opened in a new window. Optional. - * } - */ - return apply_filters( 'newspack_plugin_dashboard_items', $dashboard ); - } - - /** - * Get the name for this wizard. - * - * @return string The wizard name. - */ - public function get_name() { - return esc_html__( 'Newspack', 'newspack' ); - } - - /** - * Add an admin page for the wizard to live on. - */ - public function add_page() { - $icon = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjE4cHgiIGhlaWdodD0iNjE4cHgiIHZpZXdCb3g9IjAgMCA2MTggNjE4IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgogICAgPGcgaWQ9IlBhZ2UtMSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPHBhdGggZD0iTTMwOSwwIEM0NzkuNjU2NDk1LDAgNjE4LDEzOC4zNDQyOTMgNjE4LDMwOS4wMDE3NTkgQzYxOCw0NzkuNjU5MjI2IDQ3OS42NTY0OTUsNjE4IDMwOSw2MTggQzEzOC4zNDM1MDUsNjE4IDAsNDc5LjY1OTIyNiAwLDMwOS4wMDE3NTkgQzAsMTM4LjM0NDI5MyAxMzguMzQzNTA1LDAgMzA5LDAgWiBNMTc0LDE3MSBMMTc0LDI2Mi42NzEzNTYgTDE3NS4zMDUsMjY0IEwxNzQsMjY0IEwxNzQsNDQ2IEwyNDEsNDQ2IEwyNDEsMzMwLjkxMyBMMzUzLjk5Mjk2Miw0NDYgTDQ0NCw0NDYgTDE3NCwxNzEgWiBNNDQ0LDI5OSBMMzg5LDI5OSBMNDEwLjQ3NzYxLDMyMSBMNDQ0LDMyMSBMNDQ0LDI5OSBaIE00NDQsMjM1IEwzMjcsMjM1IEwzNDguMjQ1OTE5LDI1NyBMNDQ0LDI1NyBMNDQ0LDIzNSBaIE00NDQsMTcxIEwyNjQsMTcxIEwyODUuMjkwNTEyLDE5MyBMNDQ0LDE5MyBMNDQ0LDE3MSBaIiBpZD0iQ29tYmluZWQtU2hhcGUiIGZpbGw9IiMyQTdERTEiPjwvcGF0aD4KICAgIDwvZz4KPC9zdmc+'; - add_menu_page( - $this->get_name(), - $this->get_name(), - $this->capability, - $this->slug, - [ $this, 'render_wizard' ], - $icon, - 3 - ); - $first_subnav_title = get_option( NEWSPACK_SETUP_COMPLETE ) ? __( 'Dashboard', 'newspack' ) : __( 'Setup', 'newspack' ); - add_submenu_page( - $this->slug, - $first_subnav_title, - $first_subnav_title, - $this->capability, - $this->slug, - [ $this, 'render_wizard' ] - ); - } - - /** - * Load up JS/CSS. - */ - public function enqueue_scripts_and_styles() { - parent::enqueue_scripts_and_styles(); - - if ( filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) !== $this->slug ) { - return; - } - - wp_register_script( - 'newspack-dashboard', - Newspack::plugin_url() . '/dist/dashboard.js', - $this->get_script_dependencies(), - NEWSPACK_PLUGIN_VERSION, - true - ); - wp_localize_script( 'newspack-dashboard', 'newspack_dashboard', $this->get_dashboard() ); - wp_enqueue_script( 'newspack-dashboard' ); - - wp_register_style( - 'newspack-dashboard', - Newspack::plugin_url() . '/dist/dashboard.css', - $this->get_style_dependencies(), - NEWSPACK_PLUGIN_VERSION - ); - wp_style_add_data( 'newspack-dashboard', 'rtl', 'replace' ); - wp_enqueue_style( 'newspack-dashboard' ); - } -} diff --git a/includes/wizards/class-engagement-wizard.php b/includes/wizards/class-engagement-wizard.php deleted file mode 100644 index 043523fe10..0000000000 --- a/includes/wizards/class-engagement-wizard.php +++ /dev/null @@ -1,551 +0,0 @@ -slug . '/related-content', - [ - 'methods' => \WP_REST_Server::READABLE, - 'callback' => [ $this, 'api_get_related_content_settings' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] - ); - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/related-posts-max-age', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_update_related_posts_max_age' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'sanitize_callback' => 'sanitize_text_field', - ] - ); - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/reader-activation', - [ - 'methods' => \WP_REST_Server::READABLE, - 'callback' => [ $this, 'api_get_reader_activation_settings' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] - ); - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/reader-activation', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_update_reader_activation_settings' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] - ); - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/reader-activation/activate', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_activate_reader_activation' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] - ); - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/reader-activation/emails/(?P\d+)', - [ - 'methods' => \WP_REST_Server::DELETABLE, - 'callback' => [ $this, 'api_reset_reader_activation_email' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] - ); - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/reader-activation/skip-campaign-setup', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => function( $request ) { - $skip = $request->get_param( 'skip' ); - $skip_campaign_setup = update_option( static::SKIP_CAMPAIGN_SETUP_OPTION, $skip ); - return rest_ensure_response( - [ - 'skipped' => $skip, - 'updated' => $skip_campaign_setup, - ] - ); - }, - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] - ); - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/newsletters', - [ - 'methods' => \WP_REST_Server::READABLE, - 'callback' => [ $this, 'api_get_newsletters_settings' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] - ); - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/newsletters', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_update_newsletters_settings' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] - ); - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/newsletters/lists', - [ - 'methods' => \WP_REST_Server::READABLE, - 'callback' => [ $this, 'api_get_newsletters_lists' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] - ); - - $meta_pixel = new Meta_Pixel(); - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/social/meta_pixel', - [ - [ - 'methods' => \WP_REST_Server::READABLE, - 'callback' => [ $meta_pixel, 'api_get' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ], - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $meta_pixel, 'api_save' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => [ - 'active' => [ - 'type' => 'boolean', - 'required' => true, - 'validate_callback' => [ $meta_pixel, 'validate_active' ], - ], - 'pixel_id' => [ - 'type' => [ 'integer', 'string' ], - 'required' => true, - 'validate_callback' => [ $meta_pixel, 'validate_pixel_id' ], - ], - ], - ], - ] - ); - - $twitter_pixel = new Twitter_Pixel(); - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/social/twitter_pixel', - [ - [ - 'methods' => \WP_REST_Server::READABLE, - 'callback' => [ $twitter_pixel, 'api_get' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ], - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $twitter_pixel, 'api_save' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => [ - 'active' => [ - 'type' => 'boolean', - 'required' => true, - 'validate_callback' => [ $twitter_pixel, 'validate_active' ], - ], - 'pixel_id' => [ - 'type' => [ 'integer', 'string' ], - 'required' => true, - 'validate_callback' => [ $twitter_pixel, 'validate_pixel_id' ], - ], - ], - ], - ] - ); - } - - /** - * Get memberships settings. - * - * @return array - */ - private static function get_memberships_settings() { - return [ - 'edit_gate_url' => Memberships::get_edit_gate_url(), - 'gate_status' => \get_post_status( Memberships::get_gate_post_id() ), - 'plans' => Memberships::get_plans(), - 'require_all_plans' => Memberships::get_require_all_plans_setting(), - 'show_on_subscription_tab' => Memberships::get_show_on_subscription_tab_setting(), - ]; - } - - /** - * Get reader activation settings. - * - * @return WP_REST_Response - */ - public function api_get_reader_activation_settings() { - return rest_ensure_response( - [ - 'config' => Reader_Activation::get_settings(), - 'prerequisites_status' => Reader_Activation::get_prerequisites_status(), - 'memberships' => self::get_memberships_settings(), - 'can_esp_sync' => Reader_Activation\ESP_Sync::can_esp_sync( true ), - ] - ); - } - - /** - * Update reader activation settings. - * - * @param WP_REST_Request $request Request object. - * - * @return WP_REST_Response - */ - public function api_update_reader_activation_settings( $request ) { - $args = $request->get_params(); - foreach ( $args as $key => $value ) { - Reader_Activation::update_setting( $key, $value ); - } - - // Update Memberships options. - if ( isset( $args['memberships_require_all_plans'] ) ) { - Memberships::set_require_all_plans_setting( (bool) $args['memberships_require_all_plans'] ); - } - - // Update Memberships options. - if ( isset( $args['memberships_show_on_subscription_tab'] ) ) { - Memberships::set_show_on_subscription_tab_setting( (bool) $args['memberships_show_on_subscription_tab'] ); - } - - return rest_ensure_response( - [ - 'config' => Reader_Activation::get_settings(), - 'prerequisites_status' => Reader_Activation::get_prerequisites_status(), - 'memberships' => self::get_memberships_settings(), - 'can_esp_sync' => Reader_Activation\ESP_Sync::can_esp_sync( true ), - ] - ); - } - - /** - * Reset reader activation email template. - * We acheive this by trashing the email template post. - * - * @param WP_REST_Request $request Request object. - * - * @return WP_Error|WP_REST_Response - */ - public function api_reset_reader_activation_email( $request ) { - $params = $request->get_params(); - $id = $params['id']; - $email = get_post( $id ); - - if ( $email === null || $email->post_type !== Emails::POST_TYPE ) { - return new WP_Error( - 'newspack_reset_reader_activation_email_invalid_arg', - esc_html__( 'Invalid argument: no email template matches the provided id.', 'newspack-plugin' ), - [ - 'status' => 400, - 'level' => 'notice', - ] - ); - } - - if ( ! \wp_trash_post( $id ) ) { - return new WP_Error( - 'newspack_reset_reader_activation_email_reset_failed', - esc_html__( 'Reset failed: unable to reset email template.', 'newspack-plugin' ), - [ - 'status' => 400, - 'level' => 'notice', - ] - ); - } - - return rest_ensure_response( Emails::get_emails( array_values( Reader_Activation_Emails::EMAIL_TYPES ), false ) ); - } - - /** - * Activate reader activation and publish RAS prompts/segments. - * - * @param WP_REST_Request $request WP Rest Request object. - * @return WP_REST_Response - */ - public function api_activate_reader_activation( WP_REST_Request $request ) { - $skip_activation = $request->get_param( 'skip_activation' ) ?? false; - $response = $skip_activation ? true : Reader_Activation::activate(); - - if ( \is_wp_error( $response ) ) { - return new \WP_REST_Response( [ 'message' => $response->get_error_message() ], 400 ); - } - - if ( true === $response ) { - Reader_Activation::update_setting( 'enabled', true ); - } - - return rest_ensure_response( $response ); - } - - /** - * Get lists of configured ESP. - */ - public static function api_get_newsletters_lists() { - $newsletters_configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'newspack-newsletters' ); - return $newsletters_configuration_manager->get_lists(); - } - - /** - * Get Newspack Newsletters setttings. - * - * @return object with the info. - */ - private static function get_newsletters_settings() { - $newsletters_configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'newspack-newsletters' ); - $settings = array_reduce( - $newsletters_configuration_manager->get_settings(), - function ( $acc, $value ) { - $acc[ $value['key'] ] = $value; - return $acc; - }, - [] - ); - return [ - 'configured' => $newsletters_configuration_manager->is_configured(), - 'settings' => $settings, - ]; - } - - /** - * Get Newspack Newsletters setttings API response. - * - * @return WP_REST_Response with the info. - */ - public function api_get_newsletters_settings() { - return rest_ensure_response( self::get_newsletters_settings() ); - } - - /** - * Get Newspack Newsletters setttings. - * - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response with the info. - */ - public function api_update_newsletters_settings( $request ) { - $args = $request->get_params(); - $newsletters_configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'newspack-newsletters' ); - $newsletters_configuration_manager->update_settings( $args ); - return $this->api_get_newsletters_settings(); - } - - /** - * Update the Related Posts Max Age setting. - * - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response|WP_Error Updated value, if successful, or WP_Error. - */ - public function api_update_related_posts_max_age( $request ) { - $args = $request->get_params(); - - if ( is_numeric( $args['relatedPostsMaxAge'] ) && 0 <= $args['relatedPostsMaxAge'] ) { - update_option( $this->related_posts_option, $args['relatedPostsMaxAge'] ); - } else { - return new WP_Error( - 'newspack_related_posts_invalid_arg', - esc_html__( 'Invalid argument: max age must be a number greater than zero.', 'newspack' ), - [ - 'status' => 400, - 'level' => 'notice', - ] - ); - } - - return \rest_ensure_response( - [ - 'relatedPostsMaxAge' => $args['relatedPostsMaxAge'], - ] - ); - } - - /** - * Restrict the age of related content shown by Jetpack Related Posts. - * - * @param array $date_range Array of start and end dates. - * @return array Filtered array of start/end dates. - */ - public function restrict_age_of_related_posts( $date_range ) { - $related_posts_max_age = get_option( $this->related_posts_option ); - - if ( is_numeric( $related_posts_max_age ) && 0 < $related_posts_max_age ) { - $date_range['from'] = strtotime( '-' . $related_posts_max_age . ' months' ); - $date_range['to'] = time(); - } - - return $date_range; - } - - /** - * Get the Jetpack connection settings. - * - * @return WP_REST_Response with the info. - */ - public function api_get_related_content_settings() { - $jetpack_configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'jetpack' ); - return rest_ensure_response( - [ - 'relatedPostsEnabled' => $jetpack_configuration_manager->is_related_posts_enabled(), - 'relatedPostsMaxAge' => get_option( $this->related_posts_option, 0 ), - ] - ); - } - - /** - * Enqueue Subscriptions Wizard scripts and styles. - */ - public function enqueue_scripts_and_styles() { - parent::enqueue_scripts_and_styles(); - - if ( filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) !== $this->slug ) { - return; - } - - \wp_enqueue_script( - 'newspack-engagement-wizard', - Newspack::plugin_url() . '/dist/engagement.js', - $this->get_script_dependencies( array( 'wp-html-entities' ) ), - NEWSPACK_PLUGIN_VERSION, - true - ); - - $data = [ - 'has_memberships' => class_exists( 'WC_Memberships' ), - 'reader_activation_url' => \admin_url( 'admin.php?page=newspack-engagement-wizard#/reader-activation' ), - 'esp_metadata_fields' => Reader_Activation\Sync\Metadata::get_default_fields(), - ]; - - if ( method_exists( 'Newspack\Newsletters\Subscription_Lists', 'get_add_new_url' ) ) { - $data['new_subscription_lists_url'] = \Newspack\Newsletters\Subscription_Lists::get_add_new_url(); - } - - if ( method_exists( 'Newspack_Newsletters_Subscription', 'get_lists' ) ) { - $data['available_newsletter_lists'] = \Newspack_Newsletters_Subscription::get_lists(); - } - - $newspack_popups = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'newspack-popups' ); - if ( $newspack_popups->is_configured() ) { - $data['preview_query_keys'] = $newspack_popups->preview_query_keys(); - $data['preview_post'] = $newspack_popups->preview_post(); - $data['preview_archive'] = $newspack_popups->preview_archive(); - } - - $data['is_skipped_campaign_setup'] = get_option( static::SKIP_CAMPAIGN_SETUP_OPTION, '' ); - - \wp_localize_script( - 'newspack-engagement-wizard', - 'newspack_engagement_wizard', - $data - ); - - \wp_register_style( - 'newspack-engagement-wizard', - Newspack::plugin_url() . '/dist/engagement.css', - $this->get_style_dependencies(), - NEWSPACK_PLUGIN_VERSION - ); - \wp_style_add_data( 'newspack-engagement-wizard', 'rtl', 'replace' ); - \wp_enqueue_style( 'newspack-engagement-wizard' ); - } - - /** - * Sanitize array of categories. - * - * @param array $categories Array of categories to sanitize. - * @return array Sanitized array. - */ - public static function sanitize_categories( $categories ) { - $categories = is_array( $categories ) ? $categories : []; - $sanitized = []; - foreach ( $categories as $category ) { - $category['id'] = isset( $category['id'] ) ? absint( $category['id'] ) : null; - $category['name'] = isset( $category['name'] ) ? sanitize_title( $category['name'] ) : null; - $sanitized[] = $category; - } - return $sanitized; - } - - /** - * Set the newsletters settings url - * - * @param string $url URL to the Newspack Newsletters settings page. - * - * @return string URL to the Newspack Newsletters settings page. - */ - public function newsletters_settings_url( $url = '' ) { - return admin_url( 'admin.php?page=newspack-engagement-wizard#/newsletters' ); - } -} diff --git a/includes/wizards/class-health-check-wizard.php b/includes/wizards/class-health-check-wizard.php deleted file mode 100644 index f0ff890b16..0000000000 --- a/includes/wizards/class-health-check-wizard.php +++ /dev/null @@ -1,132 +0,0 @@ -slug, - [ - 'methods' => \WP_REST_Server::READABLE, - 'callback' => [ $this, 'api_get_health_data' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] - ); - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/unsupported_plugins', - [ - 'methods' => \WP_REST_Server::DELETABLE, - 'callback' => [ $this, 'api_delete_unsupported_plugins' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] - ); - } - - /** - * Get all data needed to render the Wizard. - */ - public function api_get_health_data() { - return \rest_ensure_response( $this->retrieve_data() ); - } - - /** - * Delete all unsupported plugins - */ - public function api_delete_unsupported_plugins() { - $unsupported_plugins = Plugin_Manager::get_unsupported_plugins(); - foreach ( $unsupported_plugins as $slug => $data ) { - Plugin_Manager::deactivate( $slug ); - } - return \rest_ensure_response( $this->retrieve_data() ); - } - - /** - * Retrieve all advertising data. - * - * @return array Advertising data. - */ - public static function retrieve_data() { - $jetpack_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'jetpack' ); - $sitekit_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'google-site-kit' ); - - return array( - 'unsupported_plugins' => Plugin_Manager::get_unsupported_plugins(), - 'missing_plugins' => Plugin_Manager::get_missing_plugins(), - 'configuration_status' => [ - 'jetpack' => $jetpack_manager->is_configured(), - 'sitekit' => $sitekit_manager->is_configured(), - ], - ); - } - - /** - * Enqueue Subscriptions Wizard scripts and styles. - */ - public function enqueue_scripts_and_styles() { - parent::enqueue_scripts_and_styles(); - - if ( filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) !== $this->slug ) { - return; - } - - \wp_enqueue_script( - 'newspack-health-check-wizard', - Newspack::plugin_url() . '/dist/health-check.js', - [ 'wp-components', 'wp-api-fetch' ], - NEWSPACK_PLUGIN_VERSION, - true - ); - } -} diff --git a/includes/wizards/class-listings-wizard.php b/includes/wizards/class-listings-wizard.php new file mode 100644 index 0000000000..7521caa091 --- /dev/null +++ b/includes/wizards/class-listings-wizard.php @@ -0,0 +1,214 @@ +admin_screens = [ + // Admin post types. + 'newspack_lst_event' => __( 'Listings / Events', 'newspack-plugin' ), + 'newspack_lst_generic' => __( 'Listings / Generic Listings', 'newspack-plugin' ), + 'newspack_lst_mktplce' => __( 'Listings / Marketplace Listings', 'newspack-plugin' ), + 'newspack_lst_place' => __( 'Listings / Places', 'newspack-plugin' ), + // Admin pages. + 'newspack-listings-settings-admin' => __( 'Listings / Settings', 'newspack-plugin' ), + ]; + + // Remove Listings plugin's menu setup. + remove_action( 'admin_menu', [ Newspack_Listings_Core::class, 'add_plugin_page' ] ); + + // Hooks: admin_menu/add_page, admin_enqueue_scripts/enqueue_scripts_and_styles, admin_body_class/add_body_class. + parent::__construct(); + + add_filter( 'submenu_file', [ $this, 'submenu_file' ] ); + + // Display screen. + if ( $this->is_wizard_page() ) { + + $this->admin_header_init( + [ + 'title' => $this->get_name(), + ] + ); + + } + } + + /** + * Add the Listings menu page. Called from parent constructor 'admin_menu'. + * + * Replaces Listings Plugin's 'admin_menu' action => Newspack_Listings\Core => 'add_plugin_page' + */ + public function add_page() { + // Top-level menu item. + add_menu_page( + __( 'Newspack Listings', 'newspack-plugin' ), + __( 'Listings', 'newspack-plugin' ), + 'edit_posts', // Copied from Listings plugin...see docblock note above. + $this->slug, + '', + 'data:image/svg+xml;base64,' . base64_encode( '' ) + ); + + if ( is_callable( [ Newspack_Listings_Settings::class, 'create_admin_page' ] ) ) { + // Settings menu link. + add_submenu_page( + $this->slug, + __( 'Newspack Listings: Site-Wide Settings', 'newspack-plugin' ), + __( 'Settings', 'newspack-plugin' ), + 'manage_options', // Copied from Listings plugin...see docblock note above. + 'newspack-listings-settings-admin', + [ Newspack_Listings_Settings::class, 'create_admin_page' ] + ); + } + } + + /** + * Enqueue scripts and styles. Called by parent constructor 'admin_enqueue_scripts'. + */ + public function enqueue_scripts_and_styles() { + // Don't output anything...scripts and styles are enqueued by Admin Header. + } + + /** + * Get the name for this current screen's wizard. Required by parent abstract. + * + * @return string The wizard name. + */ + public function get_name() { + return esc_html( $this->admin_screens[ $this->get_screen_slug() ] ); + } + + /** + * Get slug if we're currently viewing a Listings screen. + * + * @return string + */ + private function get_screen_slug() { + + global $pagenow; + + if ( isset( $this->screen_slug ) ) { + return $this->screen_slug; + } + + $sanitized_page = sanitize_text_field( $_GET['page'] ?? '' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $sanitized_post_type = sanitize_text_field( $_GET['post_type'] ?? '' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + if ( 'admin.php' === $pagenow && isset( $this->admin_screens[ $sanitized_page ] ) ) { + // admin page screen: admin.php?page={page} . + $this->screen_slug = $sanitized_page; + } elseif ( 'edit.php' === $pagenow && isset( $this->admin_screens[ $sanitized_post_type ] ) ) { + // post type list screen: edit.php?post_type={post_type} . + $this->screen_slug = $sanitized_post_type; + } else { + $this->screen_slug = ''; + } + + return $this->screen_slug; + } + + /** + * Is a Listings admin page or post_type being viewed. Needed for parent constructor => 'add_body_class' callback. + * + * @return bool Is current wizard page or not. + */ + public function is_wizard_page() { + return isset( $this->admin_screens[ $this->get_screen_slug() ] ); + } + + /** + * Menu file filter. Used to determine active menu items. + * + * Because the CPTs are registered with 'show_in_menu' => 'newspack-listings', + * the submenu_file is set to 'newspack-listings'. To work around this, we'll + * use the $pagenow and $_GET['post_type'] to determine the correct + * submenu_file. + * + * @param string $submenu_file Submenu file to be overridden. + * + * @return string + */ + public function submenu_file( $submenu_file ) { + $cpts = array_keys( $this->admin_screens ); + global $pagenow; + if ( ! in_array( $pagenow, [ 'post-new.php', 'post.php' ] ) ) { + return $submenu_file; + } + $post_type = sanitize_text_field( $_GET['post_type'] ?? '' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $post_id = sanitize_text_field( $_GET['post'] ?? '' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( ! $post_type && $post_id ) { + $post_type = get_post_type( $post_id ); + } + if ( ! $post_type ) { + return $submenu_file; + } + foreach ( $cpts as $listing_cpt ) { + if ( post_type_exists( $listing_cpt ) && strpos( $post_type, $listing_cpt ) !== false ) { + return 'edit.php?post_type=' . $post_type; + } + } + return $submenu_file; + } +} diff --git a/includes/wizards/class-network-wizard.php b/includes/wizards/class-network-wizard.php new file mode 100644 index 0000000000..4ee043aee8 --- /dev/null +++ b/includes/wizards/class-network-wizard.php @@ -0,0 +1,432 @@ +admin_screens = [ + // admin pages. + 'newspack-network' => __( 'Network / Settings', 'newspack-plugin' ), + 'newspack-network-event-log' => __( 'Network / Event Log', 'newspack-plugin' ), + 'newspack-network-membership-plans' => __( 'Network / Membership Plans', 'newspack-plugin' ), + 'newspack-network-distribution-settings' => __( 'Network / Content Distribution', 'newspack-plugin' ), + 'newspack-network-distributor-settings' => __( 'Network / Distributor Settings', 'newspack-plugin' ), + 'newspack-network-node' => __( 'Network / Node Settings', 'newspack-plugin' ), + // post types. + 'newspack_hub_nodes' => __( 'Network / Nodes', 'newspack-plugin' ), + 'np_hub_orders' => __( 'Network / Orders', 'newspack-plugin' ), + 'np_hub_subscriptions' => __( 'Network / Subscriptions', 'newspack-plugin' ), + ]; + + // Hooks: admin_menu/add_page, admin_enqueue_scripts/enqueue_scripts_and_styles, admin_body_class/add_body_class . + parent::__construct(); + + // Display screen. + if ( $this->is_wizard_page() ) { + + // Set active menu items for hidden screens. + add_filter( 'parent_file', [ $this, 'parent_file' ] ); + add_filter( 'submenu_file', [ $this, 'submenu_file' ] ); + + // Display header. + $this->admin_header_init( + [ + 'title' => $this->get_name(), + 'tabs' => $this->get_tabs(), + ] + ); + } + } + + /** + * Get the name for this current screen's wizard. Required by parent abstract. + * + * @return string The wizard name. + */ + public function get_name() { + return esc_html( $this->admin_screens[ $this->get_screen_slug() ] ); + } + + /** + * Get slug if we're currently viewing a Network screen. + * + * @return string + */ + private function get_screen_slug() { + + global $pagenow; + + static $screen_slug; + if ( isset( $screen_slug ) ) { + return $screen_slug; + } + $screen_slug = ''; + + $sanitized_action = filter_input( INPUT_GET, 'action', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); + $sanitized_page = filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); + $sanitized_post_type = filter_input( INPUT_GET, 'post_type', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); + $sanitized_post_id = filter_input( INPUT_GET, 'post', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); + + $screen_slug = match ( true ) { + // admin page screen: admin.php?page={page} . + 'admin.php' === $pagenow && isset( $this->admin_screens[ $sanitized_page ] ) + => $sanitized_page, + // post type list screen: edit.php?post_type={post_type} . + 'edit.php' === $pagenow && isset( $this->admin_screens[ $sanitized_post_type ] ) + => $sanitized_post_type, + // add new node screen: post-new.php?post_type=newspack_hub_nodes . + // note: assumes non-block editor, otherwise we need to not set this. + 'post-new.php' === $pagenow && 'newspack_hub_nodes' === $sanitized_post_type + => $sanitized_post_type, + // edit node screen: post.php?post={ID}&action=edit + // note: assumes non-block editor, otherwise we need to not set this. + 'post.php' === $pagenow && 'edit' === $sanitized_action && 'newspack_hub_nodes' === get_post_type( $sanitized_post_id ) + => 'newspack_hub_nodes', + default => '', + }; + + return $screen_slug; + } + + /** + * Wrapper for Network Plugin's is_node/is_hub functions. Also called by Newspack_Dashboard. + * + * @return string Blank '', 'node', or 'hub'. + */ + public static function get_site_role(): string { + + static $site_role; + if ( isset( $site_role ) ) { + return $site_role; + } + + // Function must exist and be callable. + $fn_get_role = [ Newspack_Network_Site_Role::class, 'get' ]; + if ( ! is_callable( $fn_get_role ) ) { + return ''; + } + + // Get the role. + $site_role = call_user_func( $fn_get_role ); + + // In the case where return value isn't a string (possibly option/value not set yet), return blank. + if ( ! is_string( $site_role ) ) { + return ''; + } + + return $site_role; + } + + /** + * Get admin header tabs (if exists) for current sreen. + * + * @return array Tabs. Default [] + */ + private function get_tabs() { + + if ( in_array( $this->get_screen_slug(), [ 'newspack-network', 'newspack-network-node', 'newspack-network-distributor-settings', 'newspack-network-distribution-settings' ], true ) ) { + + if ( '' === static::get_site_role() ) { + return []; + } + + $tabs = [ + [ + 'textContent' => esc_html__( 'Site Role', 'newspack-plugin' ), + 'href' => admin_url( 'admin.php?page=newspack-network' ), + ], + ]; + + if ( 'node' === static::get_site_role() ) { + $tabs[] = [ + 'textContent' => esc_html__( 'Node Settings', 'newspack-plugin' ), + 'href' => admin_url( 'admin.php?page=newspack-network-node' ), + ]; + } + + // Once "Content Distribution" is outside the feature flag, + // this tab should be removed. + if ( 'hub' === static::get_site_role() && ( ! defined( 'NEWPACK_NETWORK_CONTENT_DISTRIBUTION' ) || ! NEWPACK_NETWORK_CONTENT_DISTRIBUTION ) ) { + $tabs[] = [ + 'textContent' => esc_html__( 'Distributor Settings', 'newspack-plugin' ), + 'href' => admin_url( 'admin.php?page=newspack-network-distributor-settings' ), + ]; + } + + if ( defined( 'NEWPACK_NETWORK_CONTENT_DISTRIBUTION' ) && NEWPACK_NETWORK_CONTENT_DISTRIBUTION ) { + $tabs[] = [ + 'textContent' => esc_html__( 'Content Distribution', 'newspack-plugin' ), + 'href' => admin_url( 'admin.php?page=newspack-network-distribution-settings' ), + ]; + } + + return $tabs; + + } + + return []; + } + + /** + * Callback for 'admin_enqueue_scripts' => 'enqueue_scripts_and_styles' inside parent::__construct(). + */ + public function enqueue_scripts_and_styles() { + // No scripts or styles for this wizard besides whatever the Network Plugin itself enqueues. + } + + /** + * Is a Network admin page or post_type being viewed. Needed for 'add_body_class' callback. + * + * @return bool Is current wizard page or not. + */ + public function is_wizard_page() { + return isset( $this->admin_screens[ $this->get_screen_slug() ] ); + } + + /** + * Admin Menu hook to modify Network admin menu. + * + * The code below will modify the global $menu instead of overriding the different add_menu_page/add_submenu_page functions. + * It's just a lot easier to use the code below because the Network Plugin has different submenu pages for each of the + * different Site Roles (none, is_node, is_hub). It became difficult to try to rebuild the menu/submenus based on the current + * Site Role, some of which have a first submenu item of an admin page vs post type. + * + * Network Plugin's normal submenu loading order: + * + * No site role: + * MENU PARENT URL: admin.php?page=newspack-network + * site role - not shown: because wp hides single item menus. + * node settings - not shown: because is_node is false. + * Node role: + * MENU PARENT URL: admin.php?page=newspack-network + * site role - is shown. + * node settings - is shown. + * Hub role: + * MENU PARENT URL: edit.php?post_type=newspack_hub_nodes + * nodes (post type) - is shown: 'show_in_menu' is set to Network_Admin::PAGE_SLUG . + * subscriptions (post type) - is shown: 'show_in_menu' is set to Network_Admin::PAGE_SLUG . + * orders (post type) - is shown: 'show_in_menu' is set to Network_Admin::PAGE_SLUG . + * site role - is shown: this defines the parent $menu but shows 4th since register_post_type(s) run first. + * event log - is shown. + * membership plans - is shown. + * distributor settings - is shown. + * + * @return void + */ + public function add_page() { + global $menu; + + // Find the Newspack Network menu item in the admin menu. + $network_key = null; + foreach ( $menu as $k => $v ) { + // Get the network key from the menu array. + if ( $v[2] === $this->parent_menu ) { + $network_key = $k; + break; + } + } + + // Verify a key was found. + if ( empty( $network_key ) ) { + return; + } + + // Adjust the network menu attributes. + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $menu[ $network_key ][0] = __( 'Network', 'newspack-plugin' ); + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $menu[ $network_key ][6] = 'data:image/svg+xml;base64,' . base64_encode( '' ); + + // Adjust submenu items. + if ( 'node' === static::get_site_role() ) { + + // Re-add "Node Settings" as hidden page. + // Note: this will leave only "Site Role" in the submenu, so WordPress will collapse the menu. + if ( is_callable( [ Newspack_Network_Node_Settings::class, 'render' ] ) ) { + remove_submenu_page( $this->parent_menu, 'newspack-network-node' ); + $title = __( 'Node Settings', 'newspack-plugin' ); + $hook = add_submenu_page( + '', // hidden. + $title, + __( 'Node Settings', 'newspack-plugin' ), + 'manage_options', // copied from original. + 'newspack-network-node', + [ Newspack_Network_Node_Settings::class, 'render' ] + ); + $this->set_html_title( $hook, $title ); + } + } + + // Adjust submenu items. + if ( 'hub' === static::get_site_role() ) { + + // Re-add "Site Role" as "Settings" and put at bottom of submenu. + if ( is_callable( [ Newspack_Network_Admin::class, 'render_page' ] ) ) { + remove_submenu_page( $this->parent_menu, 'newspack-network' ); + add_submenu_page( + $this->parent_menu, + __( 'Site Role', 'newspack-plugin' ), + __( 'Settings', 'newspack-plugin' ), + 'manage_options', // copied from original. + 'newspack-network', + [ Newspack_Network_Admin::class, 'render_page' ], + 5 // last submenu item. + ); + } + + // Re-add "Distributor Settings" as hidden page. + if ( is_callable( [ Newspack_Network_Hub_Distributor_Settings::class, 'render' ] ) ) { + remove_submenu_page( $this->parent_menu, 'newspack-network-distributor-settings' ); + $title = __( 'Distributor Settings', 'newspack-plugin' ); + $hook = add_submenu_page( + '', // hidden. + $title, + __( 'Distributor Settings', 'newspack-plugin' ), + 'manage_options', // copied from original. + 'newspack-network-distributor-settings', + [ Newspack_Network_Hub_Distributor_Settings::class, 'render' ] + ); + $this->set_html_title( $hook, $title ); + } + } + + if ( defined( 'NEWPACK_NETWORK_CONTENT_DISTRIBUTION' ) && NEWPACK_NETWORK_CONTENT_DISTRIBUTION ) { + + // Re-add "Content Distribution" as hidden page. + if ( is_callable( [ \Newspack_Network\Content_Distribution\Admin::class, 'render' ] ) ) { + remove_submenu_page( $this->parent_menu, 'newspack-network-distribution-settings' ); + $title = __( 'Content Distribution', 'newspack-plugin' ); + $hook = add_submenu_page( + '', // hidden. + $title, + __( 'Content Distribution', 'newspack-plugin' ), + 'manage_options', // copied from original. + 'newspack-network-distribution-settings', + [ \Newspack_Network\Content_Distribution\Admin::class, 'render' ] + ); + $this->set_html_title( $hook, $title ); + } + } + } + + /** + * Parent file filter. Used to determine active parent menu. + * + * @param string $parent_file Parent file to be overridden. + * + * @return string + */ + public function parent_file( $parent_file ) { + + global $_wp_menu_nopriv, $_wp_real_parent_file; + + $sanitized_page = filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); + + // Note: get_admin_page_parent() in wp-admin/menu-header.php (line 50) could reset the returned parent_file. + // Hack: Try to make the returned value not get reset by adding to _wp_ arrays. + if ( empty( $parent_file ) && in_array( $sanitized_page, [ 'newspack-network-node', 'newspack-network-distributor-settings', 'newspack-network-distribution-settings' ] ) ) { + $_wp_menu_nopriv[ $sanitized_page ] = true; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $_wp_real_parent_file[ $sanitized_page ] = 'newspack-network'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + return 'newspack-network'; + } + + return $parent_file; + } + + /** + * Set HTML + * + * In cases where the $submenu hidden item array ( $submenu[''] = array of hidden submenu items ) is defined after the parent_slug's + * item array ( $submenu['post type url or menu-slug'] = array of submenu items ), the HTML <title> will not be set and a debug.log + * deprecated notice will be written: PHP Deprecated: strip_tags(): Passing null ... is deprecated in wp-admin/admin-header.php on line 36 + * + * If the hidden array is defined before the parent slug array, then the HTML <title> is shown and no debug.log notice. To avoid this + * issue completely, so we don't need to worry about where things are in the $submenu array, we'll proactivally set the title here just in case. + * + * @param string $hook Submenu hook. + * @param string $title HTML <title>. + * + * @return void + */ + public function set_html_title( $hook, $title ) { + add_action( + "load-{$hook}", + fn() => $GLOBALS['title'] = $title // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited, Squiz.PHP.DisallowMultipleAssignments.Found + ); + } + + /** + * Submenu file filter. Used to determine active submenu items. + * + * For admin pages return slug only. + * For admin post types return url: edit.php?post_type={post_type} + * + * @param string $submenu_file Submenu file to be overridden. + * + * @return string + */ + public function submenu_file( $submenu_file ) { + + if ( in_array( filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ), [ 'newspack-network-distributor-settings', 'newspack-network-distribution-settings' ] ) ) { + return 'newspack-network'; + } + + return $submenu_file; + } +} diff --git a/includes/wizards/class-newsletters-wizard.php b/includes/wizards/class-newsletters-wizard.php new file mode 100644 index 0000000000..561b1324e5 --- /dev/null +++ b/includes/wizards/class-newsletters-wizard.php @@ -0,0 +1,523 @@ +<?php +/** + * Newsletters (Plugin) Wizard + * + * @package Newspack + */ + +namespace Newspack; + +use Newspack\Wizards\Traits\Admin_Header; +use Newspack_Newsletters; +use Newspack_Newsletters_Ads; +use Newspack_Newsletters_Settings; +use Newspack_Newsletters\Tracking\Admin as Newspack_Newsletters_Tracking_Admin; + +defined( 'ABSPATH' ) || exit; + +/** + * Easy interface for setting up info. + */ +class Newsletters_Wizard extends Wizard { + + use Admin_Header; + + /** + * The slug of this wizard. + * + * @var string + */ + protected $slug = 'newspack-newsletters'; + + /** + * The capability required to access this wizard. + * + * @var string + */ + protected $capability = 'manage_options'; + + /** + * Newsletters plugin's Admin screen definitions (see constructor). + * + * @var array + */ + private $admin_screens = []; + + /** + * The parent menu item name. + * + * @var string + */ + public $parent_menu = 'edit.php?post_type=newspack_nl_cpt'; + + /** + * Order relative to the Newspack Dashboard menu item. + * + * @var int + */ + public $parent_menu_order = 2; + + /** + * Constructor. + */ + public function __construct() { + + if ( ! defined( 'NEWSPACK_NEWSLETTERS_PLUGIN_FILE' ) ) { + return; + } + + // Define admin screens based on Newspack Newsletters plugin's admin pages, post types, and taxonomies. + $this->admin_screens = [ + // Admin pages. + 'newspack-newsletters-settings' => __( 'Newsletters / Settings', 'newspack-plugin' ), + // Admin post types. + 'newspack_nl_cpt' => __( 'Newsletters / All Newsletters', 'newspack-plugin' ), + 'newspack_nl_ads_cpt' => __( 'Newsletters / Advertising', 'newspack-plugin' ), + // Admin taxonomies. + 'newspack_nl_advertiser' => __( 'Newsletters / Advertising', 'newspack-plugin' ), + // Admin Newsletter Lists. + 'newspack_nl_list' => __( 'Newsletters / Lists', 'newspack-plugin' ), + ]; + + // Menu removals. + remove_action( 'admin_menu', [ Newspack_Newsletters_Ads::class, 'add_ads_page' ] ); + remove_action( 'admin_menu', [ Newspack_Newsletters_Settings::class, 'add_plugin_page' ] ); + remove_action( 'admin_menu', [ Newspack_Newsletters_Tracking_Admin::class, 'add_settings_page' ] ); + + // Hooks: admin_menu/add_page, admin_enqueue_scripts/enqueue_scripts_and_styles, admin_body_class/add_body_class . + parent::__construct(); + + // Adjust post types. + add_action( 'registered_post_type', [ $this, 'registered_post_type_newsletters' ] ); + + // Adjust taxonomies. + add_action( 'registered_taxonomy', [ $this, 'registered_taxonomy_advertiser' ] ); + + // Set active menu items for hidden screens. + add_filter( 'submenu_file', [ $this, 'submenu_file' ] ); + add_filter( 'parent_file', [ $this, 'parent_file' ] ); + + // Display screen. + if ( $this->is_wizard_page() ) { + + // Remove Newsletters branding (blue banner bar) from all screens. + remove_action( 'admin_enqueue_scripts', [ Newspack_Newsletters::class, 'branding_scripts' ] ); + + // Only show the admin header on non-wizard pages. + if ( filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) !== $this->slug ) { + // Add the admin header. + $this->admin_header_init( + [ + 'title' => $this->get_name(), + 'tabs' => $this->get_tabs(), + ] + ); + } + } + + // Wizard REST API. + add_action( 'rest_api_init', [ $this, 'register_api_endpoints' ] ); + + // Modify newsletters settings URL. + add_filter( 'newspack_newsletters_settings_url', [ $this, 'newsletters_settings_url' ] ); + } + + /** + * Modify newsletters settings URL. + */ + public function newsletters_settings_url() { + return admin_url( 'edit.php?post_type=newspack_nl_cpt&page=newspack-newsletters' ); + } + + /** + * Register REST API endpoints. + */ + public function register_api_endpoints() { + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/settings', + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [ $this, 'api_get_newsletters_settings' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ] + ); + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/settings', + [ + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'api_update_newsletters_settings' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ] + ); + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/settings/lists', + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [ $this, 'api_get_newsletters_lists' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ] + ); + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/settings/tracking', + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [ $this, 'api_get_tracking' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ] + ); + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/settings/tracking', + [ + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'api_update_tracking' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + 'args' => [ + 'click' => [ + 'type' => 'boolean', + 'description' => __( 'Whether click tracking is enabled.', 'newspack-plugin' ), + 'required' => true, + ], + 'pixel' => [ + 'type' => 'boolean', + 'description' => __( 'Whether the tracking pixel is enabled.', 'newspack-plugin' ), + 'required' => true, + ], + ], + ] + ); + } + + /** + * Get lists of configured ESP. + */ + public static function api_get_newsletters_lists() { + $newsletters_configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'newspack-newsletters' ); + return $newsletters_configuration_manager->get_lists(); + } + + /** + * Get Newspack Newsletters setttings. + * + * @return object with the info. + */ + private static function get_newsletters_settings() { + $newsletters_configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'newspack-newsletters' ); + $settings = array_reduce( + $newsletters_configuration_manager->get_settings(), + function ( $acc, $value ) { + $acc[ $value['key'] ] = $value; + return $acc; + }, + [] + ); + return [ + 'configured' => $newsletters_configuration_manager->is_configured(), + 'settings' => $settings, + ]; + } + + /** + * Get Newspack Newsletters setttings API response. + * + * @return WP_REST_Response with the info. + */ + public function api_get_newsletters_settings() { + return rest_ensure_response( self::get_newsletters_settings() ); + } + + /** + * Get Newspack Newsletters setttings. + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response with the info. + */ + public function api_update_newsletters_settings( $request ) { + $args = $request->get_params(); + $newsletters_configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'newspack-newsletters' ); + $newsletters_configuration_manager->update_settings( $args ); + return $this->api_get_newsletters_settings(); + } + + /** + * Get tracking settings. + * + * @return WP_REST_Response with the info. + */ + public function api_get_tracking() { + $tracking = [ + 'click' => false, + 'pixel' => false, + ]; + if ( method_exists( 'Newspack_Newsletters\Tracking\Admin', 'is_tracking_click_enabled' ) ) { + $tracking['click'] = Newspack_Newsletters_Tracking_Admin::is_tracking_click_enabled(); + } + if ( method_exists( 'Newspack_Newsletters\Tracking\Admin', 'is_tracking_pixel_enabled' ) ) { + $tracking['pixel'] = Newspack_Newsletters_Tracking_Admin::is_tracking_pixel_enabled(); + } + return rest_ensure_response( $tracking ); + } + + /** + * Update tracking settings. + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response with the info. + */ + public function api_update_tracking( $request ) { + update_option( 'newspack_newsletters_use_click_tracking', intval( $request->get_param( 'click' ) ) ); + update_option( 'newspack_newsletters_use_tracking_pixel', intval( $request->get_param( 'pixel' ) ) ); + return rest_ensure_response( true ); + } + + /** + * Adjusts the Newsletters menu. Called from parent constructor 'admin_menu'. + */ + public function add_page() { + // Remove "Add New" menu item. + remove_submenu_page( 'edit.php?post_type=' . Newspack_Newsletters::NEWSPACK_NEWSLETTERS_CPT, 'post-new.php?post_type=' . Newspack_Newsletters::NEWSPACK_NEWSLETTERS_CPT ); + + // Remove catetory and tags. For remove_submenu_page() to match (===) on submenu slug: "&" in urls need be replaced with "&". + remove_submenu_page( 'edit.php?post_type=' . Newspack_Newsletters::NEWSPACK_NEWSLETTERS_CPT, 'edit-tags.php?taxonomy=category&post_type=' . Newspack_Newsletters::NEWSPACK_NEWSLETTERS_CPT ); + remove_submenu_page( 'edit.php?post_type=' . Newspack_Newsletters::NEWSPACK_NEWSLETTERS_CPT, 'edit-tags.php?taxonomy=post_tag&post_type=' . Newspack_Newsletters::NEWSPACK_NEWSLETTERS_CPT ); + + // Re-add Ads (Advertising) item with updated title. ( See 'remove_action' above. See Newsletters Plugin: Newspack_Newsletters_Ads > 'add_ads_page' ) . + add_submenu_page( + 'edit.php?post_type=' . Newspack_Newsletters::NEWSPACK_NEWSLETTERS_CPT, + __( 'Newsletters Advertising', 'newspack-plugin' ), + __( 'Advertising', 'newspack-plugin' ), + 'edit_others_posts', // As defined in original callback. + '/edit.php?post_type=' . Newspack_Newsletters_Ads::CPT + ); + + add_submenu_page( + 'edit.php?post_type=' . Newspack_Newsletters::NEWSPACK_NEWSLETTERS_CPT, + __( 'Newsletters Settings', 'newspack-plugin' ), + __( 'Settings', 'newspack-plugin' ), + $this->capability, + $this->slug, + [ $this, 'render_wizard' ] + ); + } + + /** + * Enqueue scripts and styles. Called by parent constructor 'admin_enqueue_scripts'. + */ + public function enqueue_scripts_and_styles() { + parent::enqueue_scripts_and_styles(); + + if ( filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) !== $this->slug ) { + return; + } + + \wp_enqueue_script( + 'newspack-newsletters-wizard', + Newspack::plugin_url() . '/dist/newsletters.js', + $this->get_script_dependencies(), + NEWSPACK_PLUGIN_VERSION, + true + ); + + $data = []; + if ( method_exists( 'Newspack\Newsletters\Subscription_Lists', 'get_add_new_url' ) ) { + $data['new_subscription_lists_url'] = \Newspack\Newsletters\Subscription_Lists::get_add_new_url(); + } + + \wp_localize_script( + 'newspack-newsletters-wizard', + 'newspack_newsletters_wizard', + $data + ); + } + + /** + * Get the name for this current screen's wizard. Required by parent abstract. + * + * @return string The wizard name. + */ + public function get_name() { + return esc_html( $this->admin_screens[ $this->get_screen_slug() ] ); + } + + /** + * Get slug if we're currently viewing a Newsletters screen. + * + * @return string + */ + private function get_screen_slug() { + + global $pagenow; + + static $screen_slug; + + if ( isset( $screen_slug ) ) { + return $screen_slug; + } + + $sanitized_page = sanitize_text_field( $_GET['page'] ?? '' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $sanitized_post_type = sanitize_text_field( $_GET['post_type'] ?? '' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $sanitized_post_id = sanitize_text_field( $_GET['post'] ?? '' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $sanitized_taxonomy = sanitize_text_field( $_GET['taxonomy'] ?? '' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + if ( 'admin.php' === $pagenow && isset( $this->admin_screens[ $sanitized_page ] ) ) { + // admin page screen: admin.php?page={page} . + $screen_slug = $sanitized_page; + } elseif ( 'edit.php' === $pagenow ) { + if ( ! $sanitized_post_type ) { + $sanitized_post_type = get_post_type( $sanitized_post_id ); + } + if ( isset( $this->admin_screens[ $sanitized_post_type ] ) && isset( $this->admin_screens[ $sanitized_page ] ) ) { + // post type with page: edit.php?post_type={post_type}&page={page} . + $screen_slug = $sanitized_page; + } elseif ( isset( $this->admin_screens[ $sanitized_post_type ] ) ) { + // post type list screen: edit.php?post_type={post_type} . + $screen_slug = $sanitized_post_type; + } else { + $screen_slug = ''; + } + } elseif ( 'edit-tags.php' === $pagenow && isset( $this->admin_screens[ $sanitized_taxonomy ] ) && isset( $this->admin_screens[ $sanitized_post_type ] ) ) { + // taxonomy list: edit-tags.php?taxonomy={taxonomy}&post_type={post_type} / phpcs:ignore Squiz.PHP.CommentedOutCode.Found . + $screen_slug = $sanitized_taxonomy; + } elseif ( 'term.php' === $pagenow && isset( $this->admin_screens[ $sanitized_taxonomy ] ) && isset( $this->admin_screens[ $sanitized_post_type ] ) ) { + // taxonomy edit: term.php?taxonomy={taxonomy}&post_type={post_type}.... / phpcs:ignore Squiz.PHP.CommentedOutCode.Found . + $screen_slug = $sanitized_taxonomy; + } else { + $screen_slug = ''; + } + + return $screen_slug; + } + + /** + * Get admin header tabs (if exists) for current sreen. + * + * @return array Tabs. Default [] + */ + private function get_tabs() { + + if ( in_array( $this->get_screen_slug(), [ 'newspack_nl_ads_cpt', 'newspack_nl_advertiser' ], true ) ) { + + return [ + [ + 'textContent' => esc_html__( 'Ads', 'newspack-plugin' ), + 'href' => admin_url( 'edit.php?post_type=newspack_nl_ads_cpt' ), + ], + [ + 'textContent' => esc_html__( 'Advertisers', 'newspack-plugin' ), + 'href' => admin_url( 'edit-tags.php?taxonomy=newspack_nl_advertiser&post_type=newspack_nl_cpt' ), + // also force selected tab for url: term.php?taxonomy=newspack_nl_advertiser&tag_ID=32&post_type=newspack_nl_cpt... + 'forceSelected' => ( 'newspack_nl_advertiser' === $this->get_screen_slug() ), + ], + ]; + + } + + return []; + } + + /** + * Is a Newsletters admin page or post_type being viewed. Needed for parent constructor => 'add_body_class' callback. + * + * @return bool Is current wizard page or not. + */ + public function is_wizard_page() { + return isset( $this->admin_screens[ $this->get_screen_slug() ] ); + } + + /** + * Callback when Newsletters CPT is registered. + * + * @param string $post_type Post type to check. + * @return void + */ + public function registered_post_type_newsletters( $post_type ) { + + global $wp_post_types; + + if ( Newspack_Newsletters::NEWSPACK_NEWSLETTERS_CPT !== $post_type ) { + return; + } + + if ( empty( $wp_post_types[ $post_type ] ) ) { + return; + } + + // Change menu icon. + $wp_post_types[ $post_type ]->menu_icon = 'data:image/svg+xml;base64,' . base64_encode( '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M3 7c0-1.1.9-2 2-2h14a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Zm2-.5h14c.3 0 .5.2.5.5v1L12 13.5 4.5 7.9V7c0-.3.2-.5.5-.5Zm-.5 3.3V17c0 .3.2.5.5.5h14c.3 0 .5-.2.5-.5V9.8L12 15.4 4.5 9.8Z"></path></svg>' ); + } + + /** + * Callback when Advertiser Taxonomy is registered. Do not show in menu for IA Epic. + * + * @param string $taxonomy Taxonomy to check. + * @return void + */ + public function registered_taxonomy_advertiser( $taxonomy ) { + + global $wp_taxonomies; + + if ( Newspack_Newsletters_Ads::ADVERTISER_TAX !== $taxonomy ) { + return; + } + + if ( empty( $wp_taxonomies[ $taxonomy ] ) ) { + return; + } + + $wp_taxonomies[ $taxonomy ]->show_in_menu = false; + } + + /** + * Menu file filter. Used to determine active menu items. + * + * @param string $submenu_file Submenu file to be overridden. + * + * @return string + */ + public function submenu_file( $submenu_file ) { + // Move newsletter ads menu file. + if ( ! empty( $submenu_file ) && strpos( $submenu_file, 'newspack_nl_ads_cpt' ) !== false ) { + return 'edit.php?post_type=newspack_nl_ads_cpt'; + } + // Move newsletter ads taxonomy menu submenu_file. + if ( ! empty( $submenu_file ) && strpos( $submenu_file, 'newspack_nl_advertiser' ) !== false ) { + return 'edit.php?post_type=newspack_nl_ads_cpt'; + } + + // Move new newsletter menu submenu_file. + if ( 'post-new.php?post_type=newspack_nl_cpt' === $submenu_file ) { + return 'edit.php?post_type=newspack_nl_cpt'; + } + + // Move newsletter subscription list submenu_file. + if ( ! empty( $submenu_file ) && strpos( $submenu_file, 'newspack_nl_list' ) !== false ) { + return $this->slug; + } + + return $submenu_file; + } + + /** + * Modify the parent file. + * + * @param string $parent_file Parent file to be overridden. + * + * @return string + */ + public function parent_file( $parent_file ) { + if ( + strpos( $parent_file, 'newspack_nl_ads_cpt' ) !== false || // Newsletter Ads. + strpos( $parent_file, 'newspack_nl_advertiser' ) !== false || // Newsletter Advertisers. + strpos( $parent_file, 'newspack_nl_list' ) !== false // Newsletter Subscription Lists. + ) { + return 'edit.php?post_type=newspack_nl_cpt'; + } + return $parent_file; + } +} diff --git a/includes/wizards/class-reader-revenue-wizard.php b/includes/wizards/class-reader-revenue-wizard.php deleted file mode 100644 index 416d7ab167..0000000000 --- a/includes/wizards/class-reader-revenue-wizard.php +++ /dev/null @@ -1,690 +0,0 @@ -<?php -/** - * Newspack's Reader Revenue Wizard - * - * @package Newspack - */ - -namespace Newspack; - -use WP_Error; - -defined( 'ABSPATH' ) || exit; - -require_once NEWSPACK_ABSPATH . '/includes/wizards/class-wizard.php'; - -/** - * Easy interface for setting up general store info. - */ -class Reader_Revenue_Wizard extends Wizard { - /** - * The slug of this wizard. - * - * @var string - */ - protected $slug = 'newspack-reader-revenue-wizard'; - - /** - * The capability required to access this wizard. - * - * @var string - */ - protected $capability = 'manage_options'; - - /** - * Get the name for this wizard. - * - * @return string The wizard name. - */ - public function get_name() { - return \esc_html__( 'Reader Revenue', 'newspack' ); - } - - /** - * Constructor. - */ - public function __construct() { - parent::__construct(); - add_action( 'rest_api_init', [ $this, 'register_api_endpoints' ] ); - add_filter( 'render_block', [ $this, 'prevent_rendering_donate_block' ], 10, 2 ); - } - - /** - * Prevent rendering of Donate block if Reader Revenue platform is set to 'other. - * - * @param string $block_content The block content about to be rendered. - * @param array $block The data of the block about to be rendered. - */ - public static function prevent_rendering_donate_block( $block_content, $block ) { - if ( - isset( $block['blockName'] ) - && 'newspack-blocks/donate' === $block['blockName'] - && Donations::is_platform_other() - ) { - return ''; - } - return $block_content; - } - - /** - * Register the endpoints needed for the wizard screens. - */ - public function register_api_endpoints() { - // Get all data required to render the Wizard. - \register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug, - [ - 'methods' => \WP_REST_Server::READABLE, - 'callback' => [ $this, 'api_fetch' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] - ); - // Save basic data about reader revenue platform. - \register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug, - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_update' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => [ - 'platform' => [ - 'sanitize_callback' => 'Newspack\newspack_clean', - 'validate_callback' => [ $this, 'api_validate_platform' ], - ], - 'nrh_organization_id' => [ - 'sanitize_callback' => 'Newspack\newspack_clean', - 'validate_callback' => [ $this, 'api_validate_not_empty' ], - ], - 'nrh_custom_domain' => [ - 'sanitize_callback' => 'Newspack\newspack_clean', - ], - 'nrh_salesforce_campaign_id' => [ - 'sanitize_callback' => 'Newspack\newspack_clean', - ], - 'donor_landing_page' => [ - 'sanitize_callback' => 'Newspack\newspack_clean', - ], - ], - ] - ); - - // Save location info. - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/location/', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_update_location' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => [ - 'countrystate' => [ - 'sanitize_callback' => 'Newspack\newspack_clean', - 'validate_callback' => [ $this, 'api_validate_not_empty' ], - ], - 'address1' => [ - 'sanitize_callback' => 'Newspack\newspack_clean', - 'validate_callback' => [ $this, 'api_validate_not_empty' ], - ], - 'address2' => [ - 'sanitize_callback' => 'Newspack\newspack_clean', - ], - 'city' => [ - 'sanitize_callback' => 'Newspack\newspack_clean', - 'validate_callback' => [ $this, 'api_validate_not_empty' ], - ], - 'postcode' => [ - 'sanitize_callback' => 'Newspack\newspack_clean', - 'validate_callback' => [ $this, 'api_validate_not_empty' ], - ], - 'currency' => [ - 'sanitize_callback' => 'Newspack\newspack_clean', - 'validate_callback' => [ $this, 'api_validate_not_empty' ], - ], - ], - ] - ); - - // Save Stripe info. - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/stripe/', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_update_stripe_settings' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => [ - 'activate' => [ - 'sanitize_callback' => 'Newspack\newspack_string_to_bool', - ], - 'enabled' => [ - 'sanitize_callback' => 'Newspack\newspack_string_to_bool', - ], - 'location_code' => [ - 'sanitize_callback' => 'Newspack\newspack_clean', - ], - ], - ] - ); - - // Save payment gateway info. - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/gateway/', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_update_gateway_settings' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => [ - 'activate' => [ - 'sanitize_callback' => 'Newspack\newspack_string_to_bool', - ], - 'enabled' => [ - 'sanitize_callback' => 'Newspack\newspack_string_to_bool', - ], - 'slug' => [ - 'sanitize_callback' => 'Newspack\newspack_clean', - ], - ], - ] - ); - - // Save additional settings info. - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/settings/', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_update_additional_settings' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => [ - 'fee_multiplier' => [ - 'sanitize_callback' => 'Newspack\newspack_clean', - 'validate_callback' => function ( $value ) { - if ( (float) $value > 10 ) { - return new WP_Error( - 'newspack_invalid_param', - __( 'Fee multiplier must be smaller than 10.', 'newspack' ) - ); - } - return true; - }, - ], - 'fee_static' => [ - 'sanitize_callback' => 'Newspack\newspack_clean', - ], - 'allow_covering_fees' => [ - 'sanitize_callback' => 'Newspack\newspack_string_to_bool', - ], - 'allow_covering_fees_default' => [ - 'sanitize_callback' => 'Newspack\newspack_string_to_bool', - ], - 'allow_covering_fees_label' => [ - 'sanitize_callback' => 'Newspack\newspack_clean', - ], - 'location_code' => [ - 'sanitize_callback' => 'Newspack\newspack_clean', - ], - ], - ] - ); - - // Update Donations settings. - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/donations/', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_update_donation_settings' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => [ - 'amounts' => [ - 'required' => false, - ], - 'tiered' => [ - 'required' => false, - 'sanitize_callback' => 'Newspack\newspack_string_to_bool', - ], - 'disabledFrequencies' => [ - 'required' => false, - ], - 'platform' => [ - 'required' => false, - 'sanitize_callback' => 'sanitize_text_field', - ], - ], - ] - ); - - // Save Salesforce settings. - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/salesforce/', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_update_salesforce_settings' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => [ - 'client_id' => [ - 'sanitize_callback' => 'sanitize_text_field', - ], - 'client_secret' => [ - 'sanitize_callback' => 'sanitize_text_field', - ], - 'access_token' => [ - 'sanitize_callback' => 'sanitize_text_field', - ], - 'refresh_token' => [ - 'sanitize_callback' => 'sanitize_text_field', - ], - ], - ] - ); - - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/donations/', - [ - 'methods' => \WP_REST_Server::READABLE, - 'callback' => [ $this, 'api_get_donation_settings' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] - ); - - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/donations/emails/(?P<id>\d+)', - [ - 'methods' => \WP_REST_Server::DELETABLE, - 'callback' => [ $this, 'api_reset_donation_email' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] - ); - } - - /** - * Get all Wizard Data - * - * @return WP_REST_Response containing ad units info. - */ - public function api_fetch() { - return \rest_ensure_response( $this->fetch_all_data() ); - } - - /** - * Save top-level data. - * - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response Boolean success. - */ - public function api_update( $request ) { - $params = $request->get_params(); - Donations::set_platform_slug( $params['platform'] ); - - // Update NRH settings. - if ( Donations::is_platform_nrh() ) { - NRH::update_settings( $params ); - } - - // Ensure that any Reader Revenue settings changed while the platform wasn't WC are persisted to WC products. - if ( Donations::is_platform_wc() ) { - Donations::update_donation_product( Donations::get_donation_settings() ); - } - - return \rest_ensure_response( $this->fetch_all_data() ); - } - - /** - * Save location info. - * - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response Boolean success. - */ - public function api_update_location( $request ) { - $required_plugins_installed = $this->check_required_plugins_installed(); - if ( is_wp_error( $required_plugins_installed ) ) { - return rest_ensure_response( $required_plugins_installed ); - } - $wc_configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'woocommerce' ); - - $params = $request->get_params(); - - $defaults = [ - 'countrystate' => '', - 'address1' => '', - 'address2' => '', - 'city' => '', - 'postcode' => '', - 'currency' => '', - ]; - - $args = wp_parse_args( $params, $defaults ); - $wc_configuration_manager->update_location( $args ); - - // @todo when is the best time to do this? - $wc_configuration_manager->set_smart_defaults(); - - return \rest_ensure_response( $this->fetch_all_data() ); - } - - /** - * Handler for setting Stripe settings. - * - * @param object $settings Stripe settings. - * @return WP_REST_Response with the latest settings. - */ - public function update_stripe_settings( $settings ) { - if ( ! empty( $settings['activate'] ) ) { - // If activating the Stripe Gateway plugin, let's enable it. - $settings = [ 'enabled' => true ]; - } - $result = Stripe_Connection::update_stripe_data( $settings ); - if ( \is_wp_error( $result ) ) { - return $result; - } - - return $this->fetch_all_data(); - } - - /** - * Handler for setting additional settings. - * - * @param object $settings Settings. - * @return WP_REST_Response with the latest settings. - */ - public function update_additional_settings( $settings ) { - if ( isset( $settings['allow_covering_fees'] ) ) { - update_option( 'newspack_donations_allow_covering_fees', intval( $settings['allow_covering_fees'] ) ); - } - if ( isset( $settings['allow_covering_fees_default'] ) ) { - update_option( 'newspack_donations_allow_covering_fees_default', $settings['allow_covering_fees_default'] ); - } - - if ( isset( $settings['allow_covering_fees_label'] ) ) { - update_option( 'newspack_donations_allow_covering_fees_label', sanitize_text_field( $settings['allow_covering_fees_label'] ) ); - } - if ( isset( $settings['fee_multiplier'] ) ) { - update_option( 'newspack_blocks_donate_fee_multiplier', $settings['fee_multiplier'] ); - } - if ( isset( $settings['fee_static'] ) ) { - update_option( 'newspack_blocks_donate_fee_static', $settings['fee_static'] ); - } - return $this->fetch_all_data(); - } - - /** - * Save Stripe settings. - * - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response Response. - */ - public function api_update_stripe_settings( $request ) { - $params = $request->get_params(); - $result = $this->update_stripe_settings( $params ); - return \rest_ensure_response( $result ); - } - - /** - * Save payment gateway settings. - * - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response Response. - */ - public function api_update_gateway_settings( $request ) { - $wc_configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'woocommerce' ); - - $params = $request->get_params(); - if ( ! isset( $params['slug'] ) ) { - return \rest_ensure_response( - new WP_Error( 'newspack_invalid_param', __( 'Gateway slug is required.', 'newspack' ) ) - ); - } - $slug = $params['slug']; - unset( $params['slug'] ); - $result = $wc_configuration_manager->update_gateway_settings( $slug, $params ); - return \rest_ensure_response( $result ); - } - - /** - * Save additional payment method settings (e.g. transaction fees). - * - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response Response. - */ - public function api_update_additional_settings( $request ) { - $params = $request->get_params(); - $result = $this->update_additional_settings( $params ); - return \rest_ensure_response( $result ); - } - - /** - * Handler for setting the donation settings. - * - * @param object $settings Donation settings. - * @return WP_REST_Response with the latest settings. - */ - public function update_donation_settings( $settings ) { - $donations_response = Donations::set_donation_settings( $settings ); - if ( is_wp_error( $donations_response ) ) { - return rest_ensure_response( $donations_response ); - } - return \rest_ensure_response( $this->fetch_all_data() ); - } - - /** - * API endpoint for setting the donation settings. - * - * @param WP_REST_Request $request Request containing settings. - * @return WP_REST_Response with the latest settings. - */ - public function api_update_donation_settings( $request ) { - return $this->update_donation_settings( $request->get_params() ); - } - - /** - * API endpoint for setting Salesforce settings. - * - * @param WP_REST_Request $request Request containing settings. - * @return WP_REST_Response with the latest settings. - */ - public function api_update_salesforce_settings( $request ) { - $salesforce_response = Salesforce::set_salesforce_settings( $request->get_params() ); - if ( is_wp_error( $salesforce_response ) ) { - return rest_ensure_response( $salesforce_response ); - } - return \rest_ensure_response( $this->fetch_all_data() ); - } - - /** - * Fetch all data needed to render the Wizard - * - * @return Array - */ - public function fetch_all_data() { - $platform = Donations::get_platform_slug(); - $wc_configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'woocommerce' ); - $wc_installed = 'active' === Plugin_Manager::get_managed_plugin_status( 'woocommerce' ); - - $billing_fields = null; - $order_notes_field = []; - if ( $wc_installed && Donations::is_platform_wc() ) { - $checkout = new \WC_Checkout(); - $fields = $checkout->get_checkout_fields(); - $checkout_fields = $fields; - if ( ! empty( $fields['billing'] ) ) { - $billing_fields = $fields['billing']; - } - if ( ! empty( $fields['order']['order_comments'] ) ) { - $order_notes_field = $fields['order']['order_comments']; - } - } - - $args = [ - 'country_state_fields' => newspack_get_countries(), - 'currency_fields' => newspack_get_currencies_options(), - 'location_data' => [], - 'payment_gateways' => [ - 'stripe' => Stripe_Connection::get_stripe_data(), - 'woocommerce_payments' => $wc_configuration_manager->gateway_data( 'woocommerce_payments' ), - 'ppcp-gateway' => $wc_configuration_manager->gateway_data( 'ppcp-gateway' ), - ], - 'additional_settings' => [ - 'allow_covering_fees' => boolval( get_option( 'newspack_donations_allow_covering_fees', true ) ), - 'allow_covering_fees_default' => boolval( get_option( 'newspack_donations_allow_covering_fees_default', false ) ), - 'allow_covering_fees_label' => get_option( 'newspack_donations_allow_covering_fees_label', '' ), - 'fee_multiplier' => get_option( 'newspack_blocks_donate_fee_multiplier', '2.9' ), - 'fee_static' => get_option( 'newspack_blocks_donate_fee_static', '0.3' ), - ], - 'donation_data' => Donations::get_donation_settings(), - 'donation_page' => Donations::get_donation_page_info(), - 'available_billing_fields' => $billing_fields, - 'order_notes_field' => $order_notes_field, - 'salesforce_settings' => [], - 'platform_data' => [ - 'platform' => $platform, - ], - 'is_ssl' => is_ssl(), - 'errors' => [], - ]; - if ( 'wc' === $platform ) { - $plugin_status = true; - $managed_plugins = Plugin_Manager::get_managed_plugins(); - $required_plugins = [ - 'woocommerce', - 'woocommerce-subscriptions', - ]; - foreach ( $required_plugins as $required_plugin ) { - if ( 'active' !== $managed_plugins[ $required_plugin ]['Status'] ) { - $plugin_status = false; - } - } - $args = wp_parse_args( - [ - 'salesforce_settings' => Salesforce::get_salesforce_settings(), - 'plugin_status' => $plugin_status, - ], - $args - ); - } elseif ( Donations::is_platform_nrh() ) { - $nrh_config = NRH::get_settings(); - $args['platform_data'] = wp_parse_args( $nrh_config, $args['platform_data'] ); - } - return $args; - } - - /** - * API endpoint for getting donation settings. - * - * @return WP_REST_Response containing info. - */ - public function api_get_donation_settings() { - if ( Donations::is_platform_wc() ) { - $required_plugins_installed = $this->check_required_plugins_installed(); - if ( is_wp_error( $required_plugins_installed ) ) { - return rest_ensure_response( $required_plugins_installed ); - } - } - - return rest_ensure_response( Donations::get_donation_settings() ); - } - - - /** - * Reset donation email template. - * We acheive this by trashing the email template post. - * - * @param WP_REST_Request $request Request object. - * - * @return WP_Error|WP_REST_Response - */ - public function api_reset_donation_email( $request ) { - $params = $request->get_params(); - $id = $params['id']; - $email = get_post( $id ); - - if ( $email === null || $email->post_type !== Emails::POST_TYPE ) { - return new WP_Error( - 'newspack_reset_donation_email_invalid_arg', - esc_html__( 'Invalid argument: no email template matches the provided id.', 'newspack-plugin' ), - [ - 'status' => 400, - 'level' => 'notice', - ] - ); - } - - if ( ! \wp_trash_post( $id ) ) { - return new WP_Error( - 'newspack_reset_donation_email_reset_failed', - esc_html__( 'Reset failed: unable to reset email template.', 'newspack-plugin' ), - [ - 'status' => 400, - 'level' => 'notice', - ] - ); - } - - return rest_ensure_response( Emails::get_emails( array_values( Reader_Revenue_Emails::EMAIL_TYPES ), false ) ); - } - - - /** - * Check whether WooCommerce is installed and active. - * - * @return bool | WP_Error True on success, WP_Error on failure. - */ - protected function check_required_plugins_installed() { - if ( ! function_exists( 'WC' ) ) { - return new WP_Error( - 'newspack_missing_required_plugin', - esc_html__( 'The WooCommerce plugin is not installed and activated. Install and/or activate it to access this feature.', 'newspack' ), - [ - 'status' => 400, - 'level' => 'fatal', - ] - ); - } - - return true; - } - - /** - * Enqueue Subscriptions Wizard scripts and styles. - */ - public function enqueue_scripts_and_styles() { - parent::enqueue_scripts_and_styles(); - if ( filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) !== $this->slug ) { - return; - } - \wp_enqueue_media(); - \wp_register_script( - 'newspack-reader-revenue-wizard', - Newspack::plugin_url() . '/dist/readerRevenue.js', - $this->get_script_dependencies(), - NEWSPACK_PLUGIN_VERSION, - true - ); - \wp_localize_script( - 'newspack-reader-revenue-wizard', - 'newspack_reader_revenue', - [ - 'emails' => Emails::get_emails( array_values( Reader_Revenue_Emails::EMAIL_TYPES ), false ), - 'email_cpt' => Emails::POST_TYPE, - 'salesforce_redirect_url' => Salesforce::get_redirect_url(), - 'can_use_name_your_price' => Donations::can_use_name_your_price(), - ] - ); - \wp_enqueue_script( 'newspack-reader-revenue-wizard' ); - } - - /** - * Validate platform ID. - * - * @param mixed $value A param value. - * @return bool - */ - public function api_validate_platform( $value ) { - return in_array( $value, [ 'nrh', 'wc', 'other' ] ); - } -} diff --git a/includes/wizards/class-setup-wizard.php b/includes/wizards/class-setup-wizard.php index a3539a9c7e..f59a2545a8 100644 --- a/includes/wizards/class-setup-wizard.php +++ b/includes/wizards/class-setup-wizard.php @@ -9,9 +9,6 @@ use WP_Error, WP_REST_Server; defined( 'ABSPATH' ) || exit; -require_once NEWSPACK_ABSPATH . '/includes/wizards/class-wizard.php'; - -define( 'NEWSPACK_SETUP_COMPLETE', 'newspack_setup_complete' ); /** * Setup Newspack. @@ -36,6 +33,20 @@ class Setup_Wizard extends Wizard { */ protected $slug = 'newspack-setup-wizard'; + /** + * The parent menu item name. + * + * @var string + */ + public $parent_menu = 'newspack-dashboard'; + + /** + * Make sure Setup is first submenu item (after the dashboard wizard creates the "Newspack" menu). + * + * @var int. + */ + protected $admin_menu_priority = 2; + /** * The capability required to access this wizard. * @@ -56,7 +67,7 @@ class Setup_Wizard extends Wizard { public function __construct() { parent::__construct(); add_action( 'rest_api_init', [ $this, 'register_api_endpoints' ] ); - if ( ! get_option( NEWSPACK_SETUP_COMPLETE ) ) { + if ( ! Newspack::is_setup_complete() ) { add_action( 'current_screen', [ $this, 'redirect_to_setup' ] ); add_action( 'admin_menu', [ $this, 'hide_non_setup_menu_items' ], 1000 ); } @@ -511,6 +522,9 @@ public function api_update_theme_with_mods( $request ) { // All-posts updates: featured image and post template. if ( substr_compare( $key, '_all_posts', -strlen( '_all_posts' ) ) === 0 ) { + if ( 'none' === $value ) { + continue; + } switch ( $key ) { case 'featured_image_all_posts': self::update_meta_key_in_batches( 'newspack_featured_image_position', $value ); @@ -543,11 +557,16 @@ private static function update_meta_key_in_batches( $meta_key, $value, $page = 0 'post_type' => 'post', 'fields' => 'ids', 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'relation' => 'OR', [ 'key' => $meta_key, 'value' => $value, 'compare' => '!=', ], + [ + 'key' => $meta_key, + 'compare' => 'NOT EXISTS', + ], ], ]; $results = new \WP_Query( $args ); @@ -601,25 +620,26 @@ public function api_update_services( $request ) { } if ( true === $request['reader-revenue']['is_service_enabled'] ) { Plugin_Manager::activate( 'woocommerce' ); - $rr_wizard = new Reader_Revenue_Wizard(); if ( isset( $request['reader-revenue']['donation_data'] ) ) { + $rr_wizard = new Audience_Donations(); $rr_wizard->update_donation_settings( $request['reader-revenue']['donation_data'] ); } if ( ! empty( $request['reader-revenue']['payment_gateways']['stripe'] ) ) { + $audience_wizard = new Audience_Wizard(); $stripe_settings = $request['reader-revenue']['payment_gateways']['stripe']; $stripe_settings['enabled'] = true; - $rr_wizard->update_stripe_settings( $stripe_settings ); + $audience_wizard->update_stripe_settings( $stripe_settings ); } } if ( true === $request['google-ad-manager']['is_service_enabled'] ) { $service = 'google_ad_manager'; - update_option( Advertising_Wizard::NEWSPACK_ADVERTISING_SERVICE_PREFIX . $service, true ); + update_option( Advertising_Display_Ads::NEWSPACK_ADVERTISING_SERVICE_PREFIX . $service, true ); if ( isset( $request['google-ad-manager']['networkCode'] ) && ! empty( $request['google-ad-manager']['networkCode'] ) ) { $network_code = $request['google-ad-manager']['networkCode']; // Update legacy network code in case service account credentials are not set. - update_option( Advertising_Wizard::OPTION_NAME_LEGACY_NETWORK_CODE, $network_code ); + update_option( Advertising_Display_Ads::OPTION_NAME_LEGACY_NETWORK_CODE, $network_code ); // Update network code used by authenticated credentials. Ensures use of desired code in case the credentials are for multiple networks. - update_option( Advertising_Wizard::OPTION_NAME_GAM_NETWORK_CODE, $network_code ); + update_option( Advertising_Display_Ads::OPTION_NAME_GAM_NETWORK_CODE, $network_code ); } Plugin_Manager::activate( 'newspack-ads' ); } @@ -674,9 +694,9 @@ public function hide_non_setup_menu_items() { if ( ! current_user_can( $this->capability ) ) { return; } - foreach ( $submenu['newspack'] as $key => $value ) { + foreach ( $submenu['newspack-dashboard'] as $key => $value ) { if ( 'newspack-setup-wizard' !== $value[2] ) { - unset( $submenu['newspack'][ $key ] ); + unset( $submenu['newspack-dashboard'][ $key ] ); } } } @@ -686,7 +706,7 @@ public function hide_non_setup_menu_items() { */ public function redirect_to_setup() { $screen = get_current_screen(); - if ( $screen && 'toplevel_page_newspack' === $screen->id ) { + if ( $screen && 'toplevel_page_newspack-dashboard' === $screen->id ) { $setup_url = Wizards::get_url( 'setup' ); wp_safe_redirect( esc_url( $setup_url ) ); exit; diff --git a/includes/wizards/class-site-design-wizard.php b/includes/wizards/class-site-design-wizard.php deleted file mode 100644 index f7d36a7171..0000000000 --- a/includes/wizards/class-site-design-wizard.php +++ /dev/null @@ -1,71 +0,0 @@ -<?php -/** - * Newspack's Site Design Wizard - * - * @package Newspack - */ - -namespace Newspack; - -use WP_Error, WP_Query; - -defined( 'ABSPATH' ) || exit; - -require_once NEWSPACK_ABSPATH . '/includes/wizards/class-wizard.php'; - -/** - * Site Design - */ -class Site_Design_Wizard extends Wizard { - - /** - * The slug of this wizard. - * - * @var string - */ - protected $slug = 'newspack-site-design-wizard'; - - /** - * The capability required to access this wizard. - * - * @var string - */ - protected $capability = 'manage_options'; - - /** - * Get the name for this wizard. - * - * @return string The wizard name. - */ - public function get_name() { - return \esc_html__( 'Site Design', 'newspack' ); - } - - /** - * Enqueue Subscriptions Wizard scripts and styles. - */ - public function enqueue_scripts_and_styles() { - parent::enqueue_scripts_and_styles(); - - if ( filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) !== $this->slug ) { - return; - } - - \wp_enqueue_script( - 'newspack-site-design-wizard', - Newspack::plugin_url() . '/dist/site-design.js', - $this->get_script_dependencies(), - NEWSPACK_PLUGIN_VERSION, - true - ); - - \wp_register_style( - 'newspack-site-design-wizard', - Newspack::plugin_url() . '/dist/site-design.css', - $this->get_style_dependencies(), - NEWSPACK_PLUGIN_VERSION - ); - \wp_style_add_data( 'newspack-site-design-wizard', 'rtl', 'replace' ); - \wp_enqueue_style( 'newspack-site-design-wizard' ); - } -} diff --git a/includes/wizards/class-syndication-wizard.php b/includes/wizards/class-syndication-wizard.php deleted file mode 100644 index 2c127e3220..0000000000 --- a/includes/wizards/class-syndication-wizard.php +++ /dev/null @@ -1,75 +0,0 @@ -<?php -/** - * Newspack's Syndication Wizard - * - * @package Newspack - */ - -namespace Newspack; - -use WP_Error, WP_Query; - -defined( 'ABSPATH' ) || exit; - -require_once NEWSPACK_ABSPATH . '/includes/wizards/class-wizard.php'; - -/** - * Syndication - */ -class Syndication_Wizard extends Wizard { - - /** - * The slug of this wizard. - * - * @var string - */ - protected $slug = 'newspack-syndication-wizard'; - - /** - * The capability required to access this wizard. - * - * @var string - */ - protected $capability = 'manage_options'; - - /** - * Constructor. - */ - public function __construct() { - parent::__construct(); - add_action( 'rest_api_init', [ $this, 'register_api_endpoints' ] ); - } - - /** - * Get the name for this wizard. - * - * @return string The wizard name. - */ - public function get_name() { - return \esc_html__( 'Syndication', 'newspack' ); - } - - /** - * Register the endpoints needed for the wizard screens. - */ - public function register_api_endpoints() {} - - /** - * Enqueue Subscriptions Wizard scripts and styles. - */ - public function enqueue_scripts_and_styles() { - parent::enqueue_scripts_and_styles(); - - if ( filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) !== $this->slug ) { - return; - } - - \wp_enqueue_script( - 'newspack-syndication-wizard', - Newspack::plugin_url() . '/dist/syndication.js', - $this->get_script_dependencies(), - NEWSPACK_PLUGIN_VERSION, - true - ); - } -} diff --git a/includes/wizards/class-wizard-section.php b/includes/wizards/class-wizard-section.php new file mode 100644 index 0000000000..746d254d0a --- /dev/null +++ b/includes/wizards/class-wizard-section.php @@ -0,0 +1,61 @@ +<?php +/** + * Wizard Section Object, for inheritence. + * + * @package Newspack + */ + +namespace Newspack\Wizards; + +use WP_Error; + +/** + * Abstract class for wizard sections. + */ +abstract class Wizard_Section { + + /** + * The WP capability required to access this section. + * + * @var string + */ + protected $capability = 'manage_options'; + + /** + * Parent tab slug. + * + * @var string + */ + protected $wizard_slug = ''; + + /** + * Initialize. + * + * @param array $args Section arguments. + */ + public function __construct( $args = [] ) { + $this->wizard_slug = $args['wizard_slug'] ?? ''; + // If inheriting class has method `register_rest_routes` bind method to action. + if ( method_exists( $this, 'register_rest_routes' ) ) { + add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] ); + } + } + + /** + * Check capabilities for using API. + * + * @return bool|WP_Error + */ + public function api_permissions_check() { + if ( ! current_user_can( $this->capability ) ) { + return new WP_Error( + 'newspack_rest_forbidden', + esc_html__( 'You cannot use this resource.', 'newspack' ), + [ + 'status' => 403, + ] + ); + } + return true; + } +} diff --git a/includes/wizards/class-wizard.php b/includes/wizards/class-wizard.php index 2d87d389dd..5292864a23 100644 --- a/includes/wizards/class-wizard.php +++ b/includes/wizards/class-wizard.php @@ -8,6 +8,7 @@ namespace Newspack; use Newspack\Starter_Content; + defined( 'ABSPATH' ) || exit; define( 'NEWSPACK_WIZARD_COMPLETED_OPTION_PREFIX', 'newspack_wizard_completed_' ); @@ -39,18 +40,64 @@ abstract class Wizard { protected $hidden = false; /** - * Priority setting for ordering admin submenu items. + * Array to store instances of section objects. + * + * @var Wizards\Section[] + */ + protected $sections = []; + + /** + * The parent menu item name. + * + * @var string + */ + public $parent_menu = ''; + + /** + * Parent menu order relative to the Newspack Dashboard menu item. + * + * @var int + */ + public $parent_menu_order = 0; + + /** + * Priority for when 'admin_menu'/'add_page' callback fires. Default is WordPress's default hook priority of 10 + * Override this on a per-wizard basis if there is a need to adjust the order that 'admin_menu'/'add_page' fires. + * Example: set priority < 10 so add_page is called before other pages. Or > 10 so it's called after other + * pages or plugins are loaded. * * @var int. */ - protected $menu_priority = 2; + protected $admin_menu_priority = 10; + + /** + * Remove notifications from the wizard screen. + * + * @var bool + */ + protected $remove_notifications = true; /** * Initialize. + * + * @param array $args Array of optional arguments. i.e. `sections`. + * @return void + * + * @example + * $my_wizard = new My_Wizard( [ 'sections' => [ 'my-wizard-section' => 'Newspack\Wizards\My_Wizard\My_Wizard_Section' ] ] ); */ - public function __construct() { - add_action( 'admin_menu', [ $this, 'add_page' ], $this->menu_priority ); + public function __construct( $args = [] ) { + add_action( 'admin_menu', [ $this, 'add_page' ], $this->admin_menu_priority ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts_and_styles' ] ); + if ( isset( $args['sections'] ) ) { + $this->load_wizard_sections( $args['sections'] ); + } + add_filter( 'admin_body_class', [ $this, 'add_body_class' ] ); + + // Remove Notices. + add_action( 'admin_notices', [ $this, 'remove_notifications' ], -9999 ); + add_action( 'all_admin_notices', [ $this, 'remove_notifications' ], -9999 ); + add_action( 'network_admin_notices', [ $this, 'remove_notifications' ], -9999 ); } /** @@ -58,7 +105,7 @@ public function __construct() { */ public function add_page() { add_submenu_page( - $this->hidden ? 'hidden' : 'newspack', + $this->hidden ? 'hidden' : $this->parent_menu, $this->get_name(), $this->get_name(), $this->capability, @@ -77,6 +124,15 @@ public function render_wizard() { <?php } + /** + * Is Wizard admin page being viewed. + * + * @return bool + */ + public function is_wizard_page() { + return filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) === $this->slug; + } + /** * Load up common JS/CSS for wizards. */ @@ -95,7 +151,7 @@ public function enqueue_scripts_and_styles() { $support_email = ( defined( 'NEWSPACK_SUPPORT_EMAIL' ) && NEWSPACK_SUPPORT_EMAIL ) ? NEWSPACK_SUPPORT_EMAIL : false; $urls = [ - 'dashboard' => Wizards::get_url( 'dashboard' ), + 'dashboard' => Wizards::get_url( 'newspack-dashboard' ), 'public_path' => Newspack::plugin_url() . '/dist/', 'bloginfo' => [ 'name' => get_bloginfo( 'name' ), @@ -109,15 +165,13 @@ public function enqueue_scripts_and_styles() { 'support_email' => $support_email, ]; - $screen = get_current_screen(); - if ( Starter_Content::has_created_starter_content() && current_user_can( 'manage_options' ) ) { $urls['remove_starter_content'] = esc_url( add_query_arg( array( 'newspack_reset' => 'starter-content', ), - Wizards::get_url( 'dashboard' ) + Wizards::get_url( 'newspack-dashboard' ) ) ); } @@ -130,7 +184,7 @@ public function enqueue_scripts_and_styles() { array( 'newspack_reset' => 'reset', ), - Wizards::get_url( 'dashboard' ) + Wizards::get_url( 'newspack-dashboard' ) ) ); } @@ -146,6 +200,18 @@ public function enqueue_scripts_and_styles() { wp_localize_script( 'newspack_data', 'newspack_urls', $urls ); wp_localize_script( 'newspack_data', 'newspack_aux_data', $aux_data ); wp_enqueue_script( 'newspack_data' ); + + /** + * Register wizards.js with cache busting + */ + $asset_file = include dirname( NEWSPACK_PLUGIN_FILE ) . '/dist/wizards.asset.php'; + wp_register_script( + 'newspack-wizards', + Newspack::plugin_url() . '/dist/wizards.js', + $this->get_script_dependencies(), + NEWSPACK_PLUGIN_VERSION, + true + ); } /** @@ -237,4 +303,53 @@ public function get_style_dependencies( $dependencies = [] ) { * @return string The wizard name. */ abstract public function get_name(); + + /** + * Load wizard sections. + * + * @param string[] $sections Array of Section class names. + */ + public function load_wizard_sections( $sections ) { + foreach ( $sections as $section_slug => $section_class ) { + if ( ! class_exists( $section_class ) ) { + wp_die( '<pre>' . esc_html( $section_class ) . '</pre> class does not exist.' ); + } + $this->sections[ $section_slug ] = new $section_class( [ 'wizard_slug' => $this->slug ] ); + } + } + + /** + * Add body class for wizard pages. + * + * @param string $classes The current body classes. + */ + public function add_body_class( $classes ) { + if ( ! $this->is_wizard_page() ) { + return $classes; + } + $classes .= ' newspack-wizard-page'; + return $classes; + } + + /** + * Remove notifications. + * + * Note: Many of our admin-header-only wizards are CPT list pages where users can do actions such + * as "trash" a post or "bulk actions" like "edit (multiple)" in the dropbown. Keep in mind + * that these actions will still show notices like "1 post was trashed" or "5 posts were + * updated" since WordPress shows these notices outside the actions that the function below + * is removing. + * + * Also "settings saved" notices on post-back of a custom options pages are not removed either. See core + * function 'settings_errors()' here: https://developer.wordpress.org/plugins/settings/custom-settings-page/ + */ + public function remove_notifications() { + if ( ! $this->is_wizard_page() ) { + return; + } + if ( ! $this->remove_notifications ) { + return; + } + remove_all_actions( current_action() ); + } } diff --git a/includes/wizards/newspack/class-custom-events-section.php b/includes/wizards/newspack/class-custom-events-section.php new file mode 100644 index 0000000000..f2466d8cfa --- /dev/null +++ b/includes/wizards/newspack/class-custom-events-section.php @@ -0,0 +1,129 @@ +<?php +/** + * Custom Events Section Object. + * + * @package Newspack + */ + +namespace Newspack\Wizards\Newspack; + +/** + * WordPress dependencies + */ +use WP_Error, WP_REST_Server; + +/** + * Internal dependencies + */ +use Newspack\Wizards\Wizard_Section; + +/** + * Custom Events Section Object. + * + * @package Newspack\Wizards\Newspack + */ +class Custom_Events_Section extends Wizard_Section { + + /** + * Register Wizard Section specific endpoints. + * + * @return void + */ + public function register_rest_routes() { + register_rest_route( + NEWSPACK_API_NAMESPACE_V2, + '/wizard/analytics/ga4-credentials', + [ + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'api_set_ga4_credentials' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + 'args' => [ + 'measurement_id' => [ + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => [ $this, 'validate_measurement_id' ], + ], + 'measurement_protocol_secret' => [ + 'sanitize_callback' => 'sanitize_text_field', + ], + ], + ] + ); + register_rest_route( + NEWSPACK_API_NAMESPACE_V2, + '/wizard/analytics/ga4-credentials/reset', + [ + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'api_reset_ga4_credentials' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ] + ); + } + + /** + * Updates the GA4 crendetials + * + * @param WP_REST_Request $request The REST request. + * @return WP_REST_Response|WP_Error + */ + public function api_set_ga4_credentials( $request ) { + $measurement_id = $request->get_param( 'measurement_id' ); + $measurement_protocol_secret = $request->get_param( 'measurement_protocol_secret' ); + + if ( ! $measurement_protocol_secret ) { + return new WP_Error( + 'newspack_analytics_wizard_invalid_params', + esc_html__( 'Invalid Measurement Protocol API Secret.', 'newspack' ), + [ 'status' => 400 ] + ); + } + + update_option( 'ga4_measurement_id', sanitize_text_field( $measurement_id ) ); + update_option( 'ga4_measurement_protocol_secret', sanitize_text_field( $measurement_protocol_secret ) ); + + return rest_ensure_response( + [ + 'measurement_id' => esc_html( $measurement_id ), + 'measurement_protocol_secret' => esc_html( $measurement_protocol_secret ), + ] + ); + } + + /** + * Reset the GA4 crendetials + * + * @return WP_REST_Response|WP_Error + */ + public function api_reset_ga4_credentials() { + delete_option( 'ga4_measurement_id' ); + delete_option( 'ga4_measurement_protocol_secret' ); + return rest_ensure_response( + [ + 'measurement_id' => '', + 'measurement_protocol_secret' => '', + ] + ); + } + + /** + * Gets the credentials for the GA4 API. + * + * @return array + */ + public static function get_data() { + return [ + 'measurement_id' => esc_html( get_option( 'ga4_measurement_id', '' ) ), + 'measurement_protocol_secret' => esc_html( get_option( 'ga4_measurement_protocol_secret', '' ) ), + ]; + } + + /** + * Validates the Measurement ID + * + * @link https://measureschool.com/ga4-measurement-id/ + * @param string $value The value to validate. + * @return bool + */ + public function validate_measurement_id( $value ) { + return is_string( $value ) && preg_match( '/^G-[A-Za-z0-9]{10,}$/', $value ); + } +} diff --git a/includes/wizards/newspack/class-newspack-dashboard.php b/includes/wizards/newspack/class-newspack-dashboard.php new file mode 100644 index 0000000000..5044cf2523 --- /dev/null +++ b/includes/wizards/newspack/class-newspack-dashboard.php @@ -0,0 +1,507 @@ +<?php +/** + * Newspack dashboard. + * + * @package Newspack + */ + +namespace Newspack; + +defined( 'ABSPATH' ) || exit; + +/** + * Common functionality for admin wizards. Override this class. + */ +class Newspack_Dashboard extends Wizard { + + /** + * The slug of this wizard. + * + * @var string + */ + protected $slug = 'newspack-dashboard'; + + /** + * The capability required to access this. + * + * @var string + */ + protected $capability = 'manage_options'; + + /** + * Use a high priorty so that the Newspack parent menu will be created + * prior to submenu items being added. + * + * @var int + */ + protected $admin_menu_priority = 1; + + /** + * Get Dashboard data + * + * @return array Dashboard sections and their configurations. + */ + public function get_dashboard() { + $dashboard = array( + 'audience_development' => array( + 'title' => __( 'Audience Management', 'newspack-plugin' ), + 'desc' => __( 'Engage your readers more deeply with tools to build customer relationships that drive towards sustainable revenue.', 'newspack-plugin' ), + 'cards' => array( + array( + 'icon' => 'settings', + 'title' => __( 'Configuration', 'newspack-plugin' ), + 'desc' => __( 'Manage your Audience Management setup.', 'newspack-plugin' ), + 'href' => admin_url( 'admin.php?page=newspack-audience' ), + ), + array( + 'icon' => 'megaphone', + 'title' => __( 'Campaigns', 'newspack-plugin' ), + 'desc' => __( 'Coordinate prompts across your site to drive metrics.', 'newspack-plugin' ), + 'href' => admin_url( 'admin.php?page=newspack-audience-campaigns' ), + ), + array( + 'icon' => 'gift', + 'title' => __( 'Donations', 'newspack-plugin' ), + 'desc' => __( 'Bring in revenue through voluntary gifts.', 'newspack-plugin' ), + 'href' => admin_url( 'admin.php?page=newspack-audience-donations' ), + ), + array( + 'icon' => 'payment', + 'title' => __( 'Subscriptions', 'newspack-plugin' ), + 'desc' => __( 'Gate your site\'s content behind a paywall.', 'newspack-plugin' ), + 'href' => admin_url( 'admin.php?page=newspack-audience-subscriptions' ), + ), + ), + ), + ); + + // Newspack Newsletters Plugin. + if ( defined( 'NEWSPACK_NEWSLETTERS_PLUGIN_FILE' ) ) { + $dashboard['newsletters'] = array( + 'title' => __( 'Newsletters', 'newspack-plugin' ), + 'desc' => __( 'Engage your readers directly in their email inbox.', 'newspack-plugin' ), + 'dependencies' => array( + 'newspack-newsletters', + ), + 'cards' => array( + array( + 'icon' => 'envelope', + 'title' => __( 'All Newsletters', 'newspack-plugin' ), + 'desc' => __( 'See all newsletters you\'ve sent out, and start new ones.', 'newspack-plugin' ), + 'href' => admin_url( 'edit.php?post_type=newspack_nl_cpt' ), + ), + array( + 'icon' => 'pullquote', + 'title' => __( 'Advertising', 'newspack-plugin' ), + 'desc' => __( 'Get advertising revenue from your newsletters.', 'newspack-plugin' ), + 'href' => admin_url( 'edit.php?post_type=newspack_nl_ads_cpt' ), + ), + array( + 'icon' => 'tool', + 'title' => __( 'Settings', 'newspack-plugin' ), + 'desc' => __( 'Configure tracking and other newsletter settings.', 'newspack-plugin' ), + 'href' => admin_url( 'edit.php?post_type=newspack_nl_cpt&page=newspack-newsletters' ), + ), + ), + ); + } + + $dashboard['advertising'] = array( + 'title' => __( 'Advertising', 'newspack-plugin' ), + 'desc' => __( 'Sell space on your site to fund your operations.', 'newspack-plugin' ), + 'dependencies' => array( + 'newspack-ads', + ), + 'cards' => array( + array( + 'icon' => 'pullquote', + 'title' => __( 'Display Ads', 'newspack-plugin' ), + 'desc' => __( 'Sell programmatic advertising on your site to drive revenue.', 'newspack-plugin' ), + 'href' => admin_url( 'admin.php?page=newspack-ads-display-ads#/' ), + ), + array( + 'icon' => 'currencyDollar', + 'title' => __( 'Sponsors', 'newspack-plugin' ), + 'desc' => __( 'Sell sponsored content directly to purchasers.', 'newspack-plugin' ), + 'href' => admin_url( 'edit.php?post_type=newspack_spnsrs_cpt' ), + ), + ), + ); + + // Newspack Listings Plugin. + if ( defined( 'NEWSPACK_LISTINGS_FILE' ) ) { + $dashboard['listings'] = array( + 'title' => __( 'Listings', 'newspack-plugin' ), + 'desc' => __( 'Build databases of reusable or user-generated content to use on your site.', 'newspack-plugin' ), + 'dependencies' => array( + 'newspack-listings', + ), + 'cards' => array( + array( + 'icon' => 'postDate', + 'title' => __( 'Events', 'newspack-plugin' ), + 'desc' => __( 'Easily use the same event information across multiple posts.', 'newspack-plugin' ), + 'href' => admin_url( 'edit.php?post_type=newspack_lst_event' ), + ), + array( + 'icon' => 'store', + 'title' => __( 'Marketplace Listings', 'newspack-plugin' ), + 'desc' => __( 'Allow users to list items and services for sale.', 'newspack-plugin' ), + 'href' => admin_url( 'edit.php?post_type=newspack_lst_mktplce' ), + ), + array( + 'icon' => 'postList', + 'title' => __( 'Generic Listing', 'newspack-plugin' ), + 'desc' => __( 'Manage any structured data for use in posts.', 'newspack-plugin' ), + 'href' => admin_url( 'edit.php?post_type=newspack_lst_generic' ), + ), + array( + 'icon' => 'mapMarker', + 'title' => __( 'Places', 'newspack-plugin' ), + 'desc' => __( 'Create a database of places in your coverage area.', 'newspack-plugin' ), + 'href' => admin_url( 'edit.php?post_type=newspack_lst_place' ), + ), + array( + 'icon' => 'tool', + 'title' => __( 'Settings', 'newspack-plugin' ), + 'desc' => __( 'Configure the way that Listings work on your site.', 'newspack-plugin' ), + 'href' => admin_url( 'admin.php?page=newspack-listings-settings-admin' ), + ), + ), + ); + } + + // Newspack Network Plugin. + if ( is_plugin_active( 'newspack-network/newspack-network.php' ) ) { + $dashboard['network'] = array( + 'title' => __( 'Network', 'newspack-plugin' ), + 'desc' => __( 'Manage the way your site\'s content flows across your publishing network.', 'newspack-plugin' ), + 'dependencies' => array( + 'newspack-network', + ), + 'cards' => $this->get_dashboard_network_cards(), + ); + } + + return $dashboard; + } + + /** + * Get Newspack Network plugin dashboard cards. + * + * @return array Array of dashboard cards. + */ + public function get_dashboard_network_cards() { + // Get the site role. + $site_role = Network_Wizard::get_site_role(); + + // Reusable card. + $settings_card = array( + 'icon' => 'tool', + 'title' => __( 'Settings', 'newspack-plugin' ), + 'desc' => __( 'Configure how Newspack Network functions.', 'newspack-plugin' ), + 'href' => admin_url( 'admin.php?page=newspack-network' ), + ); + + // If hub. + if ( 'hub' === $site_role ) { + return array( + array( + 'icon' => 'globe', + 'title' => __( 'Nodes', 'newspack-plugin' ), + 'desc' => __( 'Manage which sites are part of your content network.', 'newspack-plugin' ), + 'href' => admin_url( 'edit.php?post_type=newspack_hub_nodes' ), + ), + array( + 'icon' => 'rotateRight', + 'title' => __( 'Subscriptions', 'newspack-plugin' ), + 'desc' => __( 'View all subscriptions across your network.', 'newspack-plugin' ), + 'href' => admin_url( 'edit.php?post_type=np_hub_subscriptions' ), + ), + array( + 'icon' => 'currencyDollar', + 'title' => __( 'Orders', 'newspack-plugin' ), + 'desc' => __( 'View all payments across your network.', 'newspack-plugin' ), + 'href' => admin_url( 'edit.php?post_type=np_hub_orders' ), + ), + array( + 'icon' => 'formatListBullets', + 'title' => __( 'Event Log', 'newspack-plugin' ), + 'desc' => __( 'Troubleshoot issues by viewing all events across your network.', 'newspack-plugin' ), + 'href' => admin_url( 'admin.php?page=newspack-network-event-log' ), + ), + $settings_card, + ); + } + + // Node or no role. + return array( + $settings_card, + ); + } + + /** + * Get Dashboard local data + * + * @return array Dashboard local data. + */ + public function get_local_data() { + $site_name = get_bloginfo( 'name' ); + $theme_mods = get_theme_mods(); + $local_data = array( + 'settings' => array( + 'siteName' => $site_name, + 'headerBgColor' => $theme_mods['header_color_hex'] ?? '', + ), + 'sections' => $this->get_dashboard(), + 'plugins' => get_plugins(), + 'siteStatuses' => array( + 'readerActivation' => array( + 'label' => __( 'Audience Management', 'newspack-plugin' ), + 'statuses' => array( + 'success' => __( 'Enabled', 'newspack-plugin' ), + 'error' => __( 'Disabled', 'newspack-plugin' ), + ), + 'endpoint' => '/newspack/v1/wizard/newspack-audience/audience-management', + 'configLink' => admin_url( 'admin.php?page=newspack-audience#/' ), + 'dependencies' => array( + 'woocommerce' => array( + 'label' => __( 'Woocommerce', 'newspack-plugin' ), + 'isActive' => is_plugin_active( 'woocommerce/woocommerce.php' ), + ), + ), + ), + 'googleAdManager' => array( + 'label' => __( 'Google Ad Manager', 'newspack-plugin' ), + 'statuses' => array( + 'error-preflight' => __( 'Disconnected', 'newspack-plugin' ), + ), + 'endpoint' => '/newspack/v1/wizard/billboard', + 'isPreflightValid' => ( new Newspack_Ads_Configuration_Manager() )->is_gam_connected(), + 'configLink' => admin_url( 'admin.php?page=newspack-ads-display-ads' ), + 'dependencies' => array( + 'newspack-ads' => array( + 'label' => __( 'Newspack Ads', 'newspack-plugin' ), + 'isActive' => is_plugin_active( 'newspack-ads/newspack-ads.php' ), + ), + ), + ), + 'googleAnalytics' => array( + 'label' => __( 'Google Analytics', 'newspack-plugin' ), + 'endpoint' => '/google-site-kit/v1/modules/analytics-4/data/settings', + 'configLink' => in_array( 'analytics', get_option( 'googlesitekit_active_modules', array() ), true ) ? + admin_url( 'admin.php?page=googlesitekit-settings#/connected-services/analytics-4' ) : + admin_url( 'admin.php?page=googlesitekit-splash' ), + 'dependencies' => array( + 'google-site-kit' => array( + 'label' => __( 'Google Site Kit', 'newspack-plugin' ), + 'isActive' => is_plugin_active( 'google-site-kit/google-site-kit.php' ), + ), + ), + ), + ), + 'quickActions' => array(), + 'availableQuickActions' => array(), + ); + + /** + * User's saved quick action preferences. + * + * @var array|false + */ + $saved_quick_actions = get_user_meta( get_current_user_id(), 'newspack_quick_actions', true ); + + /** + * All available quick actions that can be configured by the user. + * + * @var array + */ + $available_actions = array( + array( + 'href' => admin_url( 'post-new.php' ), + 'title' => __( 'Start a new post', 'newspack-plugin' ), + 'icon' => 'post', + 'id' => 'new-post', + ), + ); + + if ( defined( 'NEWSPACK_NEWSLETTERS_PLUGIN_FILE' ) ) { + $available_actions[] = array( + 'href' => admin_url( 'post-new.php?post_type=newspack_nl_cpt' ), + 'title' => __( 'Draft a newsletter', 'newspack-plugin' ), + 'icon' => 'envelope', + 'id' => 'new-newsletter', + ); + } + + $available_actions = array_merge( + $available_actions, + array( + array( + 'href' => 'https://lookerstudio.google.com/u/0/reporting/b7026fea-8c2c-4c4b-be95-f582ed94f097/page/p_3eqlhk5odd', + 'title' => __( 'Open data dashboard', 'newspack-plugin' ), + 'icon' => 'chartBar', + 'id' => 'data-dashboard', + ), + array( + 'href' => admin_url( 'edit.php' ), + 'title' => __( 'View all posts', 'newspack-plugin' ), + 'icon' => 'postList', + 'id' => 'view-posts', + ), + array( + 'href' => admin_url( 'admin.php?page=newspack-audience-campaigns#/campaigns' ), + 'title' => __( 'Manage campaigns', 'newspack-plugin' ), + 'icon' => 'megaphone', + 'id' => 'manage-campaigns', + ), + array( + 'href' => admin_url( 'admin.php?page=newspack-ads-display-ads' ), + 'title' => __( 'Manage advertising', 'newspack-plugin' ), + 'icon' => 'pullquote', + 'id' => 'manage-ads', + ), + ) + ); + + if ( $saved_quick_actions && is_array( $saved_quick_actions ) ) { + // Reorder available actions based on saved preferences. + $ordered_actions = array(); + foreach ( $saved_quick_actions as $action_id ) { + foreach ( $available_actions as $action ) { + if ( $action['id'] === $action_id ) { + $ordered_actions[] = $action; + break; + } + } + } + $local_data['quickActions'] = $ordered_actions; + } else { + // Default to first 3 actions if no preferences saved. + $local_data['quickActions'] = array_slice( $available_actions, 0, 3 ); + } + + $local_data['availableQuickActions'] = $available_actions; + + return $local_data; + } + + /** + * Get the name for this wizard. + * + * @return string The wizard name. + */ + public function get_name() { + return esc_html_x( 'Newspack', 'Plugin name', 'newspack' ); + } + + /** + * Add a parent menu for Newspack and the first submenu item. + */ + public function add_page() { + $icon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSI+PHBhdGggZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMjIgMTJjMCA1LjUyMy00LjQ3NyAxMC0xMCAxMFMyIDE3LjUyMyAyIDEyIDYuNDc3IDIgMTIgMnMxMCA0LjQ3NyAxMCAxMFptLTUuNDU1IDQuNTQ2LTkuMDktOS4wOTF2OS4wOWgxLjgxOHYtNC42OTdsNC42OTcgNC42OTdoMi41NzVabS01LjE1MS03LjcyOGg1LjE1MlY3LjQ1NUgxMC4wM2wxLjM2NCAxLjM2M1ptNS4xNTIgMi41NzZIMTMuOTdsLTEuMzY0LTEuMzY0aDMuOTR2MS4zNjRabTAgMS4yMTJ2MS4zNjRsLTEuMzY0LTEuMzY0aDEuMzY0WiIgY2xpcC1ydWxlPSJldmVub2RkIi8+PC9zdmc+'; + add_menu_page( + $this->get_name(), + $this->get_name(), + $this->capability, + $this->slug, + array( $this, 'render_wizard' ), + $icon, + 3 + ); + $first_subnav_title = get_option( NEWSPACK_SETUP_COMPLETE ) ? __( 'Dashboard', 'newspack' ) : __( 'Setup', 'newspack' ); + add_submenu_page( + $this->slug, + $first_subnav_title, + $first_subnav_title, + $this->capability, + $this->slug, + array( $this, 'render_wizard' ) + ); + } + + /** + * Load up JS/CSS. + */ + public function enqueue_scripts_and_styles() { + parent::enqueue_scripts_and_styles(); + + if ( filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) !== $this->slug ) { + return; + } + + /** + * JavaScript + */ + wp_localize_script( + 'newspack-wizards', + 'newspackDashboard', + $this->get_local_data() + ); + wp_enqueue_script( 'newspack-wizards' ); + } + + /** + * Constructor. + */ + public function __construct() { + parent::__construct(); + add_action( 'rest_api_init', array( $this, 'register_api_endpoints' ) ); + } + + /** + * Register the REST API endpoints. + */ + public function register_api_endpoints() { + register_rest_route( + 'wp/v2/newspack', + '/quick-actions', + array( + 'methods' => 'POST', + 'callback' => array( $this, 'save_quick_action_preferences' ), + 'permission_callback' => array( $this, 'api_permissions_check' ), + ) + ); + } + + /** + * Check capabilities for using API. + * + * @param WP_REST_Request $request The request object. + * @return bool|WP_Error + */ + public function api_permissions_check( $request ) { + if ( ! current_user_can( 'manage_options' ) ) { + return new \WP_Error( + 'newspack_rest_forbidden', + esc_html__( 'You cannot use this resource.', 'newspack-plugin' ), + array( + 'status' => 403, + ) + ); + } + return true; + } + + /** + * Save user's quick action preferences. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function save_quick_action_preferences( $request ) { + $action_ids = $request->get_json_params(); + if ( ! is_array( $action_ids ) ) { + return new \WP_Error( + 'newspack_invalid_data', + esc_html__( 'Invalid data format.', 'newspack-plugin' ), + array( + 'status' => 400, + ) + ); + } + + update_user_meta( get_current_user_id(), 'newspack_quick_actions', $action_ids ); + return rest_ensure_response( array( 'success' => true ) ); + } +} diff --git a/includes/wizards/newspack/class-newspack-settings.php b/includes/wizards/newspack/class-newspack-settings.php new file mode 100644 index 0000000000..6eb5216916 --- /dev/null +++ b/includes/wizards/newspack/class-newspack-settings.php @@ -0,0 +1,176 @@ +<?php +/** + * Newspack Settings Admin Page + * + * @package Newspack + */ + +namespace Newspack\Wizards\Newspack; + +use Newspack\Emails; +use Newspack\OAuth; +use Newspack\Wizard; +use Newspack\Reader_Revenue_Emails; +use Newspack\Everlit_Configuration_Manager; +use function Newspack\google_site_kit_available; + +defined( 'ABSPATH' ) || exit; + +/** + * Common functionality for admin wizards. Override this class. + */ +class Newspack_Settings extends Wizard { + + /** + * The slug of this wizard. + * + * @var string + */ + protected $slug = 'newspack-settings'; + + /** + * The capability required to access this. + * + * @var string + */ + protected $capability = 'manage_options'; + + /** + * Get Settings local data + * + * @return [] + */ + public function get_local_data() { + $google_site_kit_url = google_site_kit_available() ? admin_url( 'admin.php?page=googlesitekit-settings#/connected-services/analytics-4' ) : admin_url( 'admin.php?page=googlesitekit-splash' ); + $newspack_settings = [ + 'connections' => [ + 'label' => __( 'Connections', 'newspack-plugin' ), + 'path' => '/', + 'sections' => [ + 'plugins' => [ + 'editLink' => [ + 'everlit' => 'admin.php?page=everlit_settings', + 'jetpack' => 'admin.php?page=jetpack#/settings', + 'google-site-kit' => $google_site_kit_url, + ], + 'enabled' => [ + 'everlit' => Everlit_Configuration_Manager::is_enabled(), + ], + ], + 'apis' => [ + 'dependencies' => [ + 'googleOAuth' => OAuth::is_proxy_configured( 'google' ), + ], + ], + 'jetpack_sso' => [ + 'dependencies' => [ + 'jetpack_sso' => class_exists( 'Jetpack' ) && defined( 'NEWSPACK_MANAGER_FILE' ), + ], + ], + 'recaptcha' => [], + 'analytics' => [ + 'editLink' => $google_site_kit_url, + 'measurement_id' => get_option( 'ga4_measurement_id', '' ), + 'measurement_protocol_secret' => get_option( 'ga4_measurement_protocol_secret', '' ), + ], + 'customEvents' => $this->sections['custom-events']->get_data(), + ], + ], + 'emails' => [ + 'label' => __( 'Emails', 'newspack-plugin' ), + 'sections' => [ + 'emails' => [ + 'dependencies' => [ + 'newspackNewsletters' => is_plugin_active( 'newspack-newsletters/newspack-newsletters.php' ), + ], + 'all' => Emails::get_emails( array_values( Reader_Revenue_Emails::EMAIL_TYPES ), false ), + 'postType' => Emails::POST_TYPE, + ], + ], + ], + 'social' => [ + 'label' => __( 'Social', 'newspack-plugin' ), + ], + 'syndication' => [ + 'label' => __( 'Syndication', 'newspack-plugin' ), + ], + 'seo' => [ + 'label' => __( 'SEO', 'newspack-plugin' ), + ], + 'theme-and-brand' => [ + 'label' => __( 'Theme and Brand', 'newspack-plugin' ), + ], + 'display-settings' => [ + 'label' => __( 'Display Settings', 'newspack-plugin' ), + ], + ]; + if ( defined( 'NEWSPACK_MULTIBRANDED_SITE_PLUGIN_FILE' ) ) { + $newspack_settings['additional-brands'] = [ + 'label' => __( 'Additional Brands', 'newspack-plugin' ), + 'activeTabPaths' => [ + '/additional-brands/*', + ], + 'sections' => [ + 'additionalBrands' => [ + 'themeColors' => \Newspack_Multibranded_Site\Customizations\Theme_Colors::get_registered_theme_colors(), + 'menuLocations' => get_registered_nav_menus(), + 'menus' => array_map( + function( $menu ) { + return array( + 'value' => $menu->term_id, + 'label' => $menu->name, + ); + }, + wp_get_nav_menus() + ), + ], + ], + ]; + } + return $newspack_settings; + } + + /** + * Get the name for this wizard. + * + * @return string The wizard name. + */ + public function get_name() { + return esc_html__( 'Newspack', 'newspack' ); + } + + /** + * Add an admin page for the wizard to live on. + */ + public function add_page() { + add_submenu_page( + 'newspack-dashboard', + __( 'Newspack / Settings', 'newspack-plugin' ), + __( 'Settings', 'newspack-plugin' ), + $this->capability, + $this->slug, + [ $this, 'render_wizard' ] + ); + } + + /** + * Load up JS/CSS. + */ + public function enqueue_scripts_and_styles() { + parent::enqueue_scripts_and_styles(); + + if ( filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) !== $this->slug ) { + return; + } + + /** + * JavaScript + */ + wp_localize_script( + 'newspack-wizards', + 'newspackSettings', + $this->get_local_data() + ); + wp_enqueue_script( 'newspack-wizards' ); + } +} diff --git a/includes/wizards/newspack/class-pixels-section.php b/includes/wizards/newspack/class-pixels-section.php new file mode 100644 index 0000000000..42a9d26d34 --- /dev/null +++ b/includes/wizards/newspack/class-pixels-section.php @@ -0,0 +1,95 @@ +<?php +/** + * Tracking Pixels Section Object. Supports Meta and X Pixels. + * + * @package Newspack + */ + +namespace Newspack\Wizards\Newspack; + +/** + * WordPress dependencies + */ +use WP_REST_Server; + +/** + * Internal dependencies + */ +use Newspack\Meta_Pixel; +use Newspack\Twitter_Pixel; +use Newspack\Wizards\Wizard_Section; + +/** + * Tracking Pixels Section Object. + * + * @package Newspack\Wizards\Newspack + */ +class Pixels_Section extends Wizard_Section { + + /** + * Register Wizard Section specific endpoints. + * + * @return void + */ + public function register_rest_routes() { + $meta_pixel = new Meta_Pixel(); + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->wizard_slug . '/social/meta_pixel', + [ + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $meta_pixel, 'api_get' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ], + [ + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => [ $meta_pixel, 'api_save' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + 'args' => [ + 'active' => [ + 'type' => 'boolean', + 'required' => true, + 'validate_callback' => [ $meta_pixel, 'validate_active' ], + ], + 'pixel_id' => [ + 'type' => [ 'string' ], + 'required' => true, + 'validate_callback' => [ $meta_pixel, 'validate_pixel_id' ], + ], + ], + ], + ] + ); + + $x_pixel = new Twitter_Pixel(); + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->wizard_slug . '/social/x_pixel', + [ + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $x_pixel, 'api_get' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ], + [ + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => [ $x_pixel, 'api_save' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + 'args' => [ + 'active' => [ + 'type' => 'boolean', + 'required' => true, + 'validate_callback' => [ $x_pixel, 'validate_active' ], + ], + 'pixel_id' => [ + 'type' => [ 'integer', 'string' ], + 'required' => true, + 'validate_callback' => [ $x_pixel, 'validate_pixel_id' ], + ], + ], + ], + ] + ); + } +} diff --git a/includes/wizards/newspack/class-recirculation-section.php b/includes/wizards/newspack/class-recirculation-section.php new file mode 100644 index 0000000000..b8aba29303 --- /dev/null +++ b/includes/wizards/newspack/class-recirculation-section.php @@ -0,0 +1,107 @@ +<?php +/** + * Newspack > Settings > Display Settings Section Object. + * + * @package Newspack + */ + +namespace Newspack\Wizards\Newspack; + +/** + * WordPress dependencies + */ + +use Newspack\Configuration_Managers; + +/** + * Internal dependencies + */ +use Newspack\Wizards\Wizard_Section; +use WP_Error; + +/** + * Custom Events Section Object. + * + * @package Newspack\Wizards\Newspack + */ +class Recirculation_Section extends Wizard_Section { + + /** + * The name of the option for Related Posts max age. + * + * @var string + */ + protected $related_posts_option = 'newspack_related_posts_max_age'; + + /** + * Register Wizard Section specific endpoints. + * + * @return void + */ + public function register_rest_routes() { + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->wizard_slug . '/related-content', + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [ $this, 'api_get_related_content_settings' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ] + ); + + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->wizard_slug . '/related-posts-max-age', + [ + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'api_update_related_posts_max_age' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + 'sanitize_callback' => 'sanitize_text_field', + ] + ); + } + + /** + * Get the Jetpack connection settings. + * + * @return WP_REST_Response with the info. + */ + public function api_get_related_content_settings() { + $jetpack_configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'jetpack' ); + return rest_ensure_response( + [ + 'relatedPostsEnabled' => $jetpack_configuration_manager->is_related_posts_enabled(), + 'relatedPostsMaxAge' => get_option( $this->related_posts_option, 0 ), + ] + ); + } + + /** + * Update the Related Posts Max Age setting. + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response|WP_Error Updated value, if successful, or WP_Error. + */ + public function api_update_related_posts_max_age( $request ) { + $args = $request->get_params(); + + if ( is_numeric( $args['relatedPostsMaxAge'] ) && 0 <= $args['relatedPostsMaxAge'] ) { + update_option( $this->related_posts_option, $args['relatedPostsMaxAge'] ); + } else { + return new WP_Error( + 'newspack_related_posts_invalid_arg', + esc_html__( 'Invalid argument: max age must be a number greater than zero.', 'newspack' ), + [ + 'status' => 400, + 'level' => 'notice', + ] + ); + } + + return rest_ensure_response( + [ + 'relatedPostsMaxAge' => $args['relatedPostsMaxAge'], + ] + ); + } +} diff --git a/includes/wizards/class-seo-wizard.php b/includes/wizards/newspack/class-seo-section.php similarity index 68% rename from includes/wizards/class-seo-wizard.php rename to includes/wizards/newspack/class-seo-section.php index 71864c107e..3b599a11df 100644 --- a/includes/wizards/class-seo-wizard.php +++ b/includes/wizards/newspack/class-seo-section.php @@ -1,62 +1,39 @@ <?php /** - * Newspack's SEO Wizard + * Newspack's SEO Section. * * @package Newspack */ -namespace Newspack; +namespace Newspack\Wizards\Newspack; use WP_Error, WP_Query; +use Newspack\Configuration_Managers; +use Newspack\Wizards\Wizard_Section; defined( 'ABSPATH' ) || exit; -require_once NEWSPACK_ABSPATH . '/includes/wizards/class-wizard.php'; - /** - * Easy interface for setting up general store info. + * SEO Section Class. */ -class SEO_Wizard extends Wizard { - - /** - * The slug of this wizard. - * - * @var string - */ - protected $slug = 'newspack-seo-wizard'; +class SEO_Section extends Wizard_Section { /** - * The capability required to access this wizard. + * Containing wizard slug. * * @var string */ - protected $capability = 'manage_options'; - - /** - * Constructor. - */ - public function __construct() { - parent::__construct(); - add_action( 'rest_api_init', [ $this, 'register_api_endpoints' ] ); - add_filter( 'wpseo_image_image_weight_limit', [ $this, 'ignore_yoast_weight_limit' ] ); - } - - /** - * Get the name for this wizard. - * - * @return string The wizard name. - */ - public function get_name() { - return \esc_html__( 'SEO', 'newspack' ); - } + protected $wizard_slug = 'newspack-settings'; /** * Register the endpoints needed for the wizard screens. + * + * @return void */ - public function register_api_endpoints() { + public function register_rest_routes() { register_rest_route( NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/settings', + '/wizard/' . $this->wizard_slug . '/seo', [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'api_get_seo_settings' ], @@ -65,7 +42,7 @@ public function register_api_endpoints() { ); register_rest_route( NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/settings', + '/wizard/' . $this->wizard_slug . '/seo', [ 'methods' => \WP_REST_Server::EDITABLE, 'callback' => [ $this, 'api_update_seo_settings' ], @@ -171,35 +148,4 @@ public function get_seo_settings() { ]; return $response; } - - /** - * Enqueue Subscriptions Wizard scripts and styles. - */ - public function enqueue_scripts_and_styles() { - parent::enqueue_scripts_and_styles(); - - if ( filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) !== $this->slug ) { - return; - } - - \wp_enqueue_script( - 'newspack-seo-wizard', - Newspack::plugin_url() . '/dist/seo.js', - $this->get_script_dependencies( [ 'wp-html-entities' ] ), - NEWSPACK_PLUGIN_VERSION, - true - ); - } - - /** - * We don't want Yoast to exclude large images from og:image tags for 2 reasons: - * 1. Yoast cannot calculate the image size for images served via Jetpack CDN, so any calculations will be incorrect. - * 2. It increases support burden since Yoast doesn't provide the user any explanation for why the image was excluded. - * - * @param int $limit Max image size in bytes. - * @return int Modified $limit. - */ - public function ignore_yoast_weight_limit( $limit ) { - return PHP_INT_MAX; - } } diff --git a/includes/wizards/newspack/class-syndication-section.php b/includes/wizards/newspack/class-syndication-section.php new file mode 100644 index 0000000000..3c8f65555e --- /dev/null +++ b/includes/wizards/newspack/class-syndication-section.php @@ -0,0 +1,74 @@ +<?php +/** + * Syndication Section Object. + * + * @package Newspack + */ + +namespace Newspack\Wizards\Newspack; + +/** + * WordPress dependencies + */ + +use Newspack\Syndication; +use WP_REST_Server; + +/** + * Internal dependencies + */ +use Newspack\Wizards\Wizard_Section; + +/** + * Syndication Section Object. + * + * @package Newspack\Wizards\Newspack + */ +class Syndication_Section extends Wizard_Section { + + /** + * Containing wizard slug. + * + * @var string + */ + protected $wizard_slug = 'newspack-settings'; + + /** + * Register Wizard Section specific endpoints. + * + * @return void + */ + public function register_rest_routes() { + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->wizard_slug . '/syndication', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ Syndication::class, 'api_get_settings' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ] + ); + + $required_args = array_reduce( + Syndication::get_available_optional_modules(), + function( $acc, $module_name ) { + $acc[ Syndication::MODULE_ENABLED_PREFIX . $module_name ] = [ + 'required' => true, + 'sanitize_callback' => 'rest_sanitize_boolean', + ]; + return $acc; + }, + [] + ); + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->wizard_slug . '/syndication', + [ + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => [ Syndication::class, 'api_update_settings' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + 'args' => $required_args, + ] + ); + } +} diff --git a/includes/wizards/traits/trait-wizards-admin-header.php b/includes/wizards/traits/trait-wizards-admin-header.php new file mode 100644 index 0000000000..a0be4305c3 --- /dev/null +++ b/includes/wizards/traits/trait-wizards-admin-header.php @@ -0,0 +1,128 @@ +<?php +/** + * Wizard Traits - Admin Header + * + * @package Newspack + */ + +namespace Newspack\Wizards\Traits; + +use Newspack\Newspack; + +/** + * Trait Admin_Header + * + * Provides methods to enqueue admin header CSS, JavaScript, and localize script data. + * + * @package Newspack\Wizards\Traits + */ +trait Admin_Header { + /** + * Holds the admin tabs data. + * + * @var array + */ + protected $tabs = []; + + /** + * Holds the admin title. + * + * @var string + */ + protected $title = ''; + + /** + * Initialize the admin header script with localized data. + * + * @param array $args Title and tabs array. + */ + public function admin_header_init( $args = [] ) { + $this->tabs = $args['tabs'] ?? array(); + $this->title = $args['title'] ?? __( 'Newspack Settings', 'newspack-plugin' ); + add_action( 'admin_enqueue_scripts', [ $this, 'admin_header_enqueue' ] ); + add_action( 'in_admin_header', [ $this, 'admin_header_render' ] ); + add_filter( 'admin_body_class', [ $this, 'admin_header_body_class' ] ); + } + + /** + * Enqueue the admin header css, JavaScript file, and localize the data. + */ + public function admin_header_enqueue() { + + Newspack::load_common_assets(); + + // JS. + $wizards_admin_header = include dirname( NEWSPACK_PLUGIN_FILE ) . '/dist/admin-header.asset.php'; + wp_register_script( + 'newspack-wizards-admin-header', + Newspack::plugin_url() . '/dist/admin-header.js', + [ 'wp-components' ], + $wizards_admin_header['version'] ?? NEWSPACK_PLUGIN_VERSION, + true + ); + + // Localized data. + wp_enqueue_script( 'newspack-wizards-admin-header' ); + wp_localize_script( + 'newspack-wizards-admin-header', + 'newspackWizardsAdminHeader', + [ + 'tabs' => $this->tabs, + 'title' => $this->title, + ] + ); + + // CSS. + wp_register_style( + 'newspack-wizards-admin-header', + Newspack::plugin_url() . '/dist/admin-header.css', + [], + NEWSPACK_PLUGIN_VERSION + ); + wp_style_add_data( 'newspack-wizards-admin-header', 'rtl', 'replace' ); + wp_enqueue_style( 'newspack-wizards-admin-header' ); + } + + /** + * Add necessary markup to bind React app to. The initial markup is replaced by React app and serves as a loading screen. + */ + public function admin_header_render() { + ?> + <div id="newspack-wizards-admin-header" class="newspack-wizards-admin-header"> + <div class="newspack-wizard__header"> + <div class="newspack-wizard__header__inner"> + <div class="newspack-wizard__title"> + <svg xmlns="http://www.w3.org/2000/svg" height="36" width="36" viewBox="0 0 32 32" class="newspack-icon" aria-hidden="true" focusable="false"> + <path fill-rule="evenodd" clip-rule="evenodd" d="M32 16c0 8.836-7.164 16-16 16-8.837 0-16-7.164-16-16S7.164 0 16 0s16 7.164 16 16zm-10.732.622h1.72v-1.124h-2.823l1.103 1.124zm-3.249-3.31h4.97v-1.124h-6.072l1.102 1.124zm-3.248-3.31h8.217V8.877h-9.32l1.103 1.125zM9.01 8.877l13.977 14.246h-4.66l-5.866-5.98v5.98h-3.45V8.877z" /> + </svg> + <div> + <h2><?php _e( 'Loading...', 'newspack-plugin' ); ?></h2> + </div> + </div> + </div> + </div> + <?php + if ( ! empty( $this->tabs ) ) { + // phpcs:ignore Generic.WhiteSpace.ScopeIndent.IncorrectExact + ?> + <div class="newspack-tabbed-navigation"> + <ul> + <li><a href="#"></a></li> + </ul> + </div> + <?php // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect + } + ?> + </div> + <?php + } + + /** + * Add body class for admin header. + * + * @param string $classes The current body classes. + */ + public function admin_header_body_class( $classes ) { + return $classes . ' newspack-admin-header'; + } +} diff --git a/package-lock.json b/package-lock.json index ee0e9b6eb4..1c1eef5019 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@testing-library/react": "^12.1.4", "@types/qs": "^6.9.17", "@types/react": "^17.0.75", + "@types/wordpress__api-fetch": "^3.23.1", "@wordpress/browserslist-config": "^6.18.0", "eslint": "^8.57.0", "lint-staged": "^15.4.3", @@ -1944,8 +1945,9 @@ "license": "MIT" }, "node_modules/@babel/runtime": { - "version": "7.24.7", - "license": "MIT", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", + "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -3531,22 +3533,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/@playwright/test": { - "version": "1.45.3", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.3.tgz", - "integrity": "sha512-UKF4XsBfy+u3MFWEH44hva1Q8Da28G6RFtR2+5saw+jgAFQV5yYnB1fu68Mz7fO+5GJF3wgwAIs0UelU8TxFrA==", - "dev": true, - "peer": true, - "dependencies": { - "playwright": "1.45.3" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.15", "dev": true, @@ -5474,6 +5460,16 @@ "node": ">=0.10.0" } }, + "node_modules/@types/wordpress__api-fetch": { + "version": "3.23.1", + "resolved": "https://registry.npmjs.org/@types/wordpress__api-fetch/-/wordpress__api-fetch-3.23.1.tgz", + "integrity": "sha512-uyv+1AVutyIjRE308HAjwFnpLf0D3pRhN928i4HmKZ3Uu8C0RkJJYye3md4jg/YsgZBMbAed2baCsEKg7gdmoQ==", + "deprecated": "This is a stub types definition. @wordpress/api-fetch provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "@wordpress/api-fetch": "*" + } + }, "node_modules/@types/ws": { "version": "8.5.10", "dev": true, @@ -6582,14 +6578,15 @@ } }, "node_modules/@wordpress/element": { - "version": "6.1.0", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-6.17.0.tgz", + "integrity": "sha512-mRLFDPmZiI3+POi/iUGoof/9fQi4YTJ/RAuIUipr7yG7l4SwOoQy4eSJy6QTyqtJxZ+/7qA+b/+Ek15UzFst5Q==", "dev": true, - "license": "GPL-2.0-or-later", "dependencies": { - "@babel/runtime": "^7.16.0", + "@babel/runtime": "7.25.7", "@types/react": "^18.2.79", "@types/react-dom": "^18.2.25", - "@wordpress/escape-html": "^3.1.0", + "@wordpress/escape-html": "^3.17.0", "change-case": "^4.1.2", "is-plain-object": "^5.0.0", "react": "^18.3.0", @@ -6618,11 +6615,12 @@ } }, "node_modules/@wordpress/escape-html": { - "version": "3.1.0", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.17.0.tgz", + "integrity": "sha512-yOfJwgmrtIXQDwX6zTC0L7ymYBXz3K3hlW0nDdtYy+bCw5z0gbrEOnBotOD6YdXlejAgnaAH+K1VSf0xxG5uGA==", "dev": true, - "license": "GPL-2.0-or-later", "dependencies": { - "@babel/runtime": "^7.16.0" + "@babel/runtime": "7.25.7" }, "engines": { "node": ">=18.12.0", @@ -6739,13 +6737,14 @@ } }, "node_modules/@wordpress/icons": { - "version": "10.1.0", + "version": "10.17.0", + "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-10.17.0.tgz", + "integrity": "sha512-qzWFrMfa5HZdGxGq7I+s9bmUJqZrFfx6ow/slY1USKJqp1uRHRekAbq6UrOrJscs8rSUQiV/aNNPDgSfqBEM6A==", "dev": true, - "license": "GPL-2.0-or-later", "dependencies": { - "@babel/runtime": "^7.16.0", - "@wordpress/element": "^6.1.0", - "@wordpress/primitives": "^4.1.0" + "@babel/runtime": "7.25.7", + "@wordpress/element": "^6.17.0", + "@wordpress/primitives": "^4.17.0" }, "engines": { "node": ">=18.12.0", @@ -7036,17 +7035,21 @@ } }, "node_modules/@wordpress/primitives": { - "version": "4.1.0", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-4.17.0.tgz", + "integrity": "sha512-O1dysI/Y9xv5uUMllH2VIxuBDCOVUX8WmouE9KKr11Yv4gkHzxzaU2M5rFtu7RbUCv6jtkvjidy2cuZuNpEIHQ==", "dev": true, - "license": "GPL-2.0-or-later", "dependencies": { - "@babel/runtime": "^7.16.0", - "@wordpress/element": "^6.1.0", + "@babel/runtime": "7.25.7", + "@wordpress/element": "^6.17.0", "clsx": "^2.1.1" }, "engines": { "node": ">=18.12.0", "npm": ">=8.19.2" + }, + "peerDependencies": { + "react": "^18.0.0" } }, "node_modules/@wordpress/priority-queue": { @@ -10770,17 +10773,6 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/domhandler/node_modules/domelementtype": { - "version": "2.3.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, "node_modules/domutils": { "version": "3.1.0", "dev": true, @@ -21253,53 +21245,6 @@ "node": ">=8" } }, - "node_modules/playwright": { - "version": "1.45.3", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.3.tgz", - "integrity": "sha512-QhVaS+lpluxCaioejDZ95l4Y4jSFCsBvl2UZkpeXlzxmqS+aABr5c82YmfMHrL6x27nvrvykJAFpkzT2eWdJww==", - "dev": true, - "peer": true, - "dependencies": { - "playwright-core": "1.45.3" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.45.3", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.3.tgz", - "integrity": "sha512-+ym0jNbcjikaOwwSZycFbwkWgfruWvYlJfThKYAlImbxUgdWFO2oW70ojPm4OpE4t6TAo2FY/smM+hpVTtkhDA==", - "dev": true, - "peer": true, - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/plur": { "version": "4.0.0", "dev": true, @@ -22398,6 +22343,7 @@ }, "node_modules/react": { "version": "18.3.1", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -22459,6 +22405,7 @@ }, "node_modules/react-dom": { "version": "18.3.1", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", @@ -23356,6 +23303,7 @@ }, "node_modules/scheduler": { "version": "0.23.2", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" diff --git a/package.json b/package.json index e283e5a2aa..a4a072b5fd 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "devDependencies": { "@rushstack/eslint-patch": "^1.10.5", "@testing-library/react": "^12.1.4", + "@types/wordpress__api-fetch": "^3.23.1", "@types/qs": "^6.9.17", "@types/react": "^17.0.75", "@wordpress/browserslist-config": "^6.18.0", @@ -34,6 +35,7 @@ "start": "npm ci --legacy-peer-deps && npm run watch", "watch": "npm run clean && newspack-scripts wp-scripts start", "test": "newspack-scripts test", + "tsc": "tsc --watch", "lint": "npm run lint:scss && npm run lint:js", "lint:js": "newspack-scripts wp-scripts lint-js '**/{src,includes}/**/*.{js,jsx,ts,tsx}'", "lint:js:staged": "newspack-scripts wp-scripts lint-js --ext .js,.jsx,.ts,.tsx", @@ -62,4 +64,4 @@ "recursive-copy": "^2.0.14", "tachyons": "^4.12.0" } -} +} \ No newline at end of file diff --git a/src/admin/style.scss b/src/admin/style.scss index 72e7bd3776..ae39f4a8c7 100644 --- a/src/admin/style.scss +++ b/src/admin/style.scss @@ -1 +1,328 @@ -@import url( "~tachyons/css/tachyons.min.css" ); +@use "../../node_modules/tachyons/css/tachyons.min.css"; +@use "~@wordpress/base-styles/colors" as wp-colors; + +:root { + /* Sections */ + --newspack-wizard-section-width: 1040px; + --newspack-wizard-section-space: 24px; + --newspack-wizard-section-child-space: 16px; + + /** + * Dimensions + */ + --np-wizard-tabs-height: 60px; + + /** + * WP Admin + */ + --wp-adminbar-height: 32px; +} + +html { + font-size: 16px; + scroll-padding-top: calc(var(--wp-adminbar-height) + var(--np-wizard-tabs-height)); + + @media (max-width: 782px) { + --wp-adminbar-height: 46px; + --np-wizard-tabs-height: 48px; + + scroll-padding-top: calc(var(--wp-adminbar-height) + var(--np-wizard-tabs-height) + 1rem); + } + + @media screen and (max-width: 600px) { + scroll-padding-top: 1rem; + } +} + +h1 { + font-size: 2rem; + font-weight: 400; + line-height: 1.25; + margin-block: 0 2.5rem; +} + +/** + * Utils + */ +.gray-700 { + color: wp-colors.$gray-700; +} +.is-fetching { + * { + cursor: progress; + } + // Inputs + input[type="text"], + input[type="number"] { + pointer-events: none; + } + + // Universal borders and background color + textarea, + input[type="text"], + input[type="number"], + input[type="checkbox"], + .components-checkbox-control__input[type="checkbox"], + .components-checkbox-control__input[type="checkbox"]:checked { + background-color: wp-colors.$gray-100; + border: 1px solid wp-colors.$gray-100; + } + // Universal animations + textarea, + input[type="text"], + input[type="number"], + .components-checkbox-control { + animation: opacity-pulse 1.4s infinite; + } + // Transparent color for values + textarea, + input[type="text"], + input[type="number"] { + color: transparent; + + &::placeholder { + color: transparent; + } + } + + // Theme + .newspack-style-card { + opacity: 0.9; + + .newspack-style-card__image { + overflow: hidden; + animation: opacity-pulse 1.4s infinite; + } + + .newspack-style-card__image-html:empty { + background-color: wp-colors.$gray-100; + } + + .newspack-style-card__actions { + display: none; + } + } + + // Color + .newspack-color-picker { + .newspack-color-picker__expander { + background-color: wp-colors.$gray-100; + animation: opacity-pulse 1.4s infinite; + } + } + + .newspack-select-control { + animation: opacity-pulse 1.4s infinite; + + // Specificity necessary to override @emotion/* styles + select.components-select-control__input { + background-color: wp-colors.$gray-100; + border: 1px solid wp-colors.$gray-100; + cursor: progress; + } + + // Specificity necessary to override @emotion/* styles + div.components-input-control__backdrop { + border: none; + } + + optgroup, + option, + .components-input-control__suffix, + .components-select-control__arrow-wrapper { + display: none; + } + } + + .newspack-image-upload__image { + animation: opacity-pulse 1.4s infinite; + } +} + +/** + * Wizards + */ +// Only apply styles if there are sections and is immediate descendent. +.newspack-wizard__content:has(> .newspack-wizard__sections) { + margin: 0; + max-width: 100%; + padding: 0 0 32px; + + * { + box-sizing: border-box; + } +} +.newspack-wizard-page:not(.newspack-admin-header) { + #screen-meta-links { + position: absolute; + right: 0; + } +} +.newspack-wizard { + .newspack-wizard__loader { + height: 100%; + position: absolute; + width: 100%; + display: flex; + + > div { + position: relative; + margin: auto; + text-align: center; + } + + span { + display: block; + color: #757575; + animation: opacity-pulse 1.4s infinite ease-in-out; + } + } + + &.newspack-dashboard { + .newspack-dashboard__section { + &:first-of-type { + margin-top: 3.5rem; + } + } + } +} + +.newspack-wizard__sections { + margin: 0 auto; + padding: 2.5rem 1rem 0; + max-width: calc(calc(var(--newspack-wizard-section-space) * 2) + var(--newspack-wizard-section-width)); + &__description { + margin-bottom: 2rem; + } +} + +.newspack-wizard__section { + margin-block-end: 4rem; + + &:last-child { + margin-block-end: 0; + } +} + +.newspack-section-header__container { + p { + margin-block: 0 1.5rem; + } +} + +.newspack-dashboard__section { + margin: 0 auto; + max-width: calc(calc(var(--newspack-wizard-section-space) * 2) + var(--newspack-wizard-section-width)); + padding: 0 var(--newspack-wizard-section-space); + + > h3 { + font-size: 1.25rem; + font-weight: normal; + line-height: 1.75; + margin-bottom: calc(var(--newspack-wizard-section-child-space) / 2); + } + + > p { + color: wp-colors.$gray-700; + margin: 0; + } + + a { + text-decoration: none; + } + + .newspack-grid:not(.newspack-grid--no-margin) { + --np-dash-card-icon-size: 80px; + + margin: 1.5rem 0 2rem; + } +} + +// tachyons overrides +table.fixed { + position: static; + table-layout: fixed; +} + +// Overrides +.newspack-wizard__sections .newspack-wizard__section { + .newspack-section-header:first-child { + margin-block: 0 2rem; + } +} + +.newspack-card.newspack-action-card + .newspack-card.newspack-action-card { + margin-top: 0; +} + +.newspack-card.newspack-action-card.is-small + .newspack-card.newspack-action-card.is-small { + margin-block: 0 1rem; +} + +.newspack-card { + margin-block: 0 1rem; + + &:last-child { + margin-bottom: 0; + } + + &.newspack-action-card { + .newspack-action-card__notification { + &.newspack-action-card__region-children { + .newspack-notice { + margin-top: 1rem; + } + } + } + + .newspack-action-card__region-children:not(:last-child) { + padding-bottom: 0; + padding-top: 2rem; + + .newspack-notice { + margin-top: 0; + } + } + } + + &.newspack-card__is-narrow { + padding: 3rem 4rem; + + h2 { + margin-block-start: 0; + } + + p:last-of-type { + margin-block: 0 2rem; + } + } +} + +.newspack-style-card .newspack-style-card__image { + position: relative; + padding-bottom: 75%; + + > img, + .newspack-style-card__image-html { + height: 100%; + position: absolute; + width: 100%; + } +} + +@keyframes gradient-left-to-right { + to { + transform: translateX(100%); + } +} + +@keyframes opacity-pulse { + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.5; + } +} diff --git a/src/blocks/reader-registration/index.js b/src/blocks/reader-registration/index.js index 34c9608338..9e6696ffd7 100644 --- a/src/blocks/reader-registration/index.js +++ b/src/blocks/reader-registration/index.js @@ -28,7 +28,7 @@ export { metadata, name }; export const settings = { icon: { src: icon, - foreground: '#36f', + foreground: '#406ebc', }, edit, save: () => <div { ...useInnerBlocksProps.save( useBlockProps.save() ) } />, diff --git a/src/components/src/action-card/index.js b/src/components/src/action-card/index.js index a47f6e739a..55727fa15c 100644 --- a/src/components/src/action-card/index.js +++ b/src/components/src/action-card/index.js @@ -105,7 +105,7 @@ class ActionCard extends Component { const isDisplayingSecondaryAction = secondaryActionText && onSecondaryActionClick; const badges = ! Array.isArray( badge ) && badge ? [ badge ] : badge; return ( - <Card className={ classes } onClick={ simple && onClick } noBorder={ noBorder }> + <Card className={ classes } onClick={ simple && onClick } id={ this.props.id ?? null } noBorder={ noBorder }> <div className="newspack-action-card__region newspack-action-card__region-top"> { toggleOnChange && ( <ToggleControl diff --git a/src/components/src/action-card/style.scss b/src/components/src/action-card/style.scss index 5f52c50a83..afc7a88385 100644 --- a/src/components/src/action-card/style.scss +++ b/src/components/src/action-card/style.scss @@ -434,6 +434,10 @@ .newspack-action-card__region-children { border-top: 1px solid wp-colors.$gray-100; padding-top: 24px; + + + .newspack-action-card__region-children { + border-top: none; + } } &.is-medium .newspack-action-card__region-children { diff --git a/src/components/src/box-contrast/index.tsx b/src/components/src/box-contrast/index.tsx new file mode 100644 index 0000000000..71e15767fd --- /dev/null +++ b/src/components/src/box-contrast/index.tsx @@ -0,0 +1,41 @@ +/** + * Box Contrast + * + * can be used to dynamically assign black or white color/background-color based on a hex color. + * Black/white can be assigned to either text or background-color. + */ + +/** + * Dependencies + */ +import { getContrast } from '../utils/color'; + +/** + * Box Contrast component + * + * @return JSX.Element + */ +const BoxContrast = ( { + hexColor, + isInverted = false, + children, + ...props +}: { + children: string | JSX.Element; + hexColor: string; + isInverted?: boolean; + className?: string; +} ) => { + const contrastColor = getContrast( hexColor ); + const style = isInverted + ? { color: hexColor, backgoundColor: contrastColor } + : { backgroundColor: hexColor, color: contrastColor }; + + return ( + <div { ...props } style={ style }> + { children } + </div> + ); +}; + +export default BoxContrast; diff --git a/src/components/src/color-picker/index.js b/src/components/src/color-picker/index.js index e426e610b5..48857b5c1b 100644 --- a/src/components/src/color-picker/index.js +++ b/src/components/src/color-picker/index.js @@ -21,6 +21,16 @@ import './style.scss'; extend( [ a11yPlugin ] ); const { InteractiveDiv } = utils; +/** + * ColorPicker component. + * + * @param {Object} props - Component props. + * @param {JSX.Element|string} props.label - Label for the color picker. + * @param {string} [props.color] - Default color. + * @param {Function} props.onChange - Function to call when the color changes. + * @param {string} [props.className] - Additional class name. + * @return {JSX.Element} ColorPicker component. + */ const ColorPicker = ( { label, color = '#fff', onChange, className } ) => { const [ isExpanded, setIsExpanded ] = useState( false ); const ref = useRef(); diff --git a/src/components/src/custom-select-control/style.scss b/src/components/src/custom-select-control/style.scss index d1d5b4c3c2..10c957e5c8 100644 --- a/src/components/src/custom-select-control/style.scss +++ b/src/components/src/custom-select-control/style.scss @@ -10,7 +10,7 @@ margin-bottom: 8px; } - .components-custom-select-control__button { + button { border-color: wp-colors.$gray-700; border-radius: 2px; color: wp-colors.$gray-900; @@ -41,6 +41,10 @@ } } + button + div { + min-width: 200px; + } + .components-custom-select-control__menu { border-color: wp-colors.$gray-700; border-radius: 2px; @@ -54,7 +58,6 @@ cursor: pointer; display: flex; line-height: inherit; - margin: 0 0 0 28px; padding: 6px 8px; transition: color 125ms ease-in-out; @@ -77,16 +80,8 @@ &.is-selected { margin-left: 0; - - &::before { - background: - url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij48cGF0aCBkPSJNMTguMyA1LjZMOS45IDE2LjlsLTQuNi0zLjQtLjkgMS4yIDUuOCA0LjMgOS4zLTEyLjZ6IiBmaWxsPSIjMzM2NmZmIj48L3BhdGg+PC9zdmc+Cg==") - 0 0 no-repeat; - content: ""; - display: block; - height: 24px; - margin: 0 4px 0 0; - width: 24px; + span svg { + fill: var(--wp-admin-theme-color); } } } diff --git a/src/components/src/date-range-picker/style.scss b/src/components/src/date-range-picker/style.scss index 6701dbcd57..ccb16a0238 100644 --- a/src/components/src/date-range-picker/style.scss +++ b/src/components/src/date-range-picker/style.scss @@ -100,7 +100,7 @@ &__CalendarSelection, &__CalendarHighlight { - border-color: colors.$primary-500; + border-color: var(--wp-admin-theme-color); bottom: 4px; top: 4px; @@ -129,11 +129,11 @@ } &__CalendarSelection { - background-color: colors.$primary-500; + background-color: var(--wp-admin-theme-color); } &__CalendarHighlight { - background: colors.$primary-500; + background: var(--wp-admin-theme-color); border: none; bottom: 0; top: 0; diff --git a/src/components/src/footer/index.js b/src/components/src/footer/index.js index 1083b7620c..c146006884 100644 --- a/src/components/src/footer/index.js +++ b/src/components/src/footer/index.js @@ -11,10 +11,9 @@ import { ExternalLink } from '@wordpress/components'; /** * Internal dependencies. */ -import { PatronsLogo } from '../'; import './style.scss'; -const Footer = ( { simple } ) => { +const Footer = ( { simple = undefined } ) => { const { components_demo: componentsDemo = false, support = false, @@ -75,23 +74,18 @@ const Footer = ( { simple } ) => { return ( <div className="newspack-footer"> { ! simple && ( - <div className="newspack-footer__inner"> - <ul> - { footerElements.map( ( { url, label, external }, index ) => ( - <li key={ index }> - { external ? ( - <ExternalLink href={ url }>{ label }</ExternalLink> - ) : ( - <a href={ url }>{ label }</a> - ) } - </li> - ) ) } - </ul> - </div> + <ul> + { footerElements.map( ( { url, label, external }, index ) => ( + <li key={ index }> + { external ? ( + <ExternalLink href={ url }>{ label }</ExternalLink> + ) : ( + <a href={ url }>{ label }</a> + ) } + </li> + ) ) } + </ul> ) } - <div className="newspack-footer__logo"> - <PatronsLogo /> - </div> </div> ); }; diff --git a/src/components/src/footer/style.scss b/src/components/src/footer/style.scss index 5261590bb9..47bc11d3ee 100644 --- a/src/components/src/footer/style.scss +++ b/src/components/src/footer/style.scss @@ -5,16 +5,13 @@ @use "~@wordpress/base-styles/colors" as wp-colors; .newspack-footer { - align-items: stretch; background: white; border: 0 solid wp-colors.$gray-300; border-width: 1px 0; bottom: 0; color: wp-colors.$gray-700; display: flex; - flex-wrap: wrap; font-size: 12px; - justify-content: space-between; left: 0; line-height: 16px; position: absolute; @@ -22,17 +19,6 @@ @media screen and ( min-width: 783px ) { bottom: 40px; - flex-wrap: nowrap; - } - - &__inner { - align-items: center; - display: flex; - flex: 0 0 100%; - - @media screen and ( min-width: 783px ) { - flex: 1 1 auto; - } } ul { @@ -57,26 +43,4 @@ } } } - - &__logo { - align-items: center; - background: wp-colors.$gray-900; - display: flex; - margin: 0 0 -1px; - padding: 16px; - width: 100%; - - @media screen and ( min-width: 783px ) { - margin: -1px 0 -1px 8px; - max-width: 33.33%; - width: auto; - } - - .patrons-logo { - display: block; - fill: white; - max-height: 16px; - width: auto; - } - } } diff --git a/src/components/src/handoff-message/index.js b/src/components/src/handoff-message/index.js index ba8ecaa290..ea4bcf1e73 100644 --- a/src/components/src/handoff-message/index.js +++ b/src/components/src/handoff-message/index.js @@ -30,9 +30,6 @@ export default function HandoffMessage() { setHandoffMessage( false ); } }, 100 ); - - // Clean up the notification when unmounting. - return () => window.localStorage.removeItem( HANDOFF_KEY ); }, [] ); if ( ! handoffMessage ) { return null; diff --git a/src/components/src/hooks/useObjectState.js b/src/components/src/hooks/useObjectState.js index e334472636..58f3c9d1ae 100644 --- a/src/components/src/hooks/useObjectState.js +++ b/src/components/src/hooks/useObjectState.js @@ -6,6 +6,11 @@ import { useState } from '@wordpress/element'; import isArray from 'lodash/isArray'; import mergeWith from 'lodash/mergeWith'; +/** + * @typedef {Object} StateObjectUpdate + * @property {Object.<string, any>} [update] The update object with key-value pairs to merge into the state. + */ + const mergeCustomizer = ( objValue, srcValue ) => { if ( isArray( objValue ) ) { // If it's an array, replace it (instead of concatenating). @@ -16,6 +21,10 @@ const mergeCustomizer = ( objValue, srcValue ) => { /** * A useState for an object. * Nested objects will be nested, but arrays replaced. + * + * @template T + * @param {T} initial Initial state object. + * @return {[T, (keyOrUpdate: string | Partial<T>) => (value?: any) => void]} The state object and a function to update it. */ export default ( initial = {} ) => { const [ stateObject, setStateObject ] = useState( initial ); diff --git a/src/components/src/index.js b/src/components/src/index.js index 0702fce778..69d997e8aa 100644 --- a/src/components/src/index.js +++ b/src/components/src/index.js @@ -4,6 +4,7 @@ export { default as AutocompleteTokenField } from './autocomplete-tokenfield'; export { default as AutocompleteWithSuggestions } from './autocomplete-with-suggestions'; export { default as Button } from './button'; export { default as ButtonCard } from './button-card'; +export { default as BoxContrast } from './box-contrast'; export { default as Card } from './card'; export { default as CategoryAutocomplete } from './category-autocomplete'; export { default as ColorPicker } from './color-picker'; @@ -18,9 +19,7 @@ export { default as GlobalNotices } from './global-notices'; export { default as Grid } from './grid'; export { default as Modal } from './modal'; export { default as NewspackIcon } from './newspack-icon'; -export { default as NewspackLogo } from './newspack-logo'; export { default as Notice } from './notice'; -export { default as PatronsLogo } from './patrons-logo'; export { default as PluginInstaller } from './plugin-installer'; export { default as PluginSettings } from './plugin-settings'; export { default as PluginToggle } from './plugin-toggle'; diff --git a/src/components/src/modal/index.js b/src/components/src/modal/index.js index 514db445a1..028dbb6490 100644 --- a/src/components/src/modal/index.js +++ b/src/components/src/modal/index.js @@ -18,11 +18,12 @@ import './style.scss'; */ import classnames from 'classnames'; -function Modal( { className, isWide, isNarrow, ...otherProps }, ref ) { +function Modal( { className, isWide, isNarrow, hideTitle, ...otherProps }, ref ) { const classes = classnames( 'newspack-modal', isWide && 'newspack-modal--wide', isNarrow && 'newspack-modal--narrow', + hideTitle && 'newspack-modal--hide-title', // Note: also hides the X close button. className ); diff --git a/src/components/src/modal/style.scss b/src/components/src/modal/style.scss index 55bf6d60d8..01de3604c8 100644 --- a/src/components/src/modal/style.scss +++ b/src/components/src/modal/style.scss @@ -2,16 +2,16 @@ * Modal */ -@use "../../../shared/scss/colors"; +@use "../../../shared/scss/colors" as colors; .newspack-modal { // Color - --wp-admin-theme-color: #{colors.$primary-500}; - --wp-admin-theme-color--rgb: #{colors.$primary-500--rgb}; - --wp-admin-theme-color-darker-10: #{colors.$primary-600}; - --wp-admin-theme-color-darker-10--rgb: #{colors.$primary-600--rgb}; - --wp-admin-theme-color-darker-20: #{colors.$primary-700}; - --wp-admin-theme-color-darker-20--rgb: #{colors.$primary-700--rgb}; + --wp-admin-theme-color: #{colors.$primary-600}; + --wp-admin-theme-color--rgb: rgb(#{colors.$primary-600--rgb}); + --wp-admin-theme-color-darker-10: #{colors.$primary-700}; + --wp-admin-theme-color-darker-10--rgb: rgb(#{colors.$primary-700--rgb}); + --wp-admin-theme-color-darker-20: #{colors.$primary-800}; + --wp-admin-theme-color-darker-20--rgb: rgb(#{colors.$primary-800--rgb}); @media screen and ( min-width: 744px ) { max-width: 680px; @@ -28,6 +28,17 @@ width: 360px; } + &--hide-title { + .components-modal__header { + display: none; + } + + .components-modal__content { + margin-top: 0; + padding-top: 32px; + } + } + @media screen and ( min-width: 960px ) { max-height: calc(100% - 128px); } diff --git a/src/components/src/newspack-icon/index.js b/src/components/src/newspack-icon/index.js index 98bd8dc612..b61fd34775 100644 --- a/src/components/src/newspack-icon/index.js +++ b/src/components/src/newspack-icon/index.js @@ -35,13 +35,13 @@ class NewspackIcon extends Component { xmlns="http://www.w3.org/2000/svg" height={ size } width={ size } - viewBox="0 0 32 32" + viewBox="0 0 24 24" className={ classes } > <Path fillRule="evenodd" clipRule="evenodd" - d="M32 16c0 8.836-7.164 16-16 16-8.837 0-16-7.164-16-16S7.164 0 16 0s16 7.164 16 16zm-10.732.622h1.72v-1.124h-2.823l1.103 1.124zm-3.249-3.31h4.97v-1.124h-6.072l1.102 1.124zm-3.248-3.31h8.217V8.877h-9.32l1.103 1.125zM9.01 8.877l13.977 14.246h-4.66l-5.866-5.98v5.98h-3.45V8.877z" + d="M24 12C24 18.6271 18.6271 24 12 24C5.37213 24 0 18.6271 0 12C0 5.3729 5.3729 0 12 0C18.6271 0 24 5.3729 24 12ZM17.4545 17.4546L6.54545 6.54545V17.4545H8.72727V11.8182L14.3636 17.4546H17.4545ZM11.2727 8.18182H17.4545V6.54545H9.63636L11.2727 8.18182ZM17.4545 11.2727H14.3636L12.7273 9.63636H17.4545V11.2727ZM17.4545 12.7273V14.3636L15.8182 12.7273H17.4545Z" /> </SVG> ); diff --git a/src/components/src/newspack-icon/style.scss b/src/components/src/newspack-icon/style.scss index f1757b360f..14a9ca6c5c 100644 --- a/src/components/src/newspack-icon/style.scss +++ b/src/components/src/newspack-icon/style.scss @@ -5,18 +5,18 @@ @use "../../../shared/scss/colors"; .newspack-icon { - background: colors.$primary-500; + background: var(--wp-admin-theme-color); fill: white; padding: 12px; &--white { background: white; - fill: colors.$primary-500; + fill: var(--wp-admin-theme-color); } &--simple { background: none; - fill: colors.$primary-500; + fill: var(--wp-admin-theme-color); padding: 0; } diff --git a/src/components/src/newspack-logo/index.js b/src/components/src/newspack-logo/index.js deleted file mode 100644 index 4e10761b08..0000000000 --- a/src/components/src/newspack-logo/index.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Newspack Logo. - */ - -/** - * WordPress dependencies. - */ -import { Component } from '@wordpress/element'; -import { Path, SVG } from '@wordpress/components'; - -/** - * External dependencies. - */ -import classnames from 'classnames'; - -/** - * Internal dependencies. - */ -import './style.scss'; - -class NewspackLogo extends Component { - /** - * Render - */ - render() { - const { centered, className, height } = this.props; - const classes = classnames( 'newspack-logo__svg', centered && 'is-centered', className ); - return ( - <SVG - xmlns="http://www.w3.org/2000/svg" - height={ height } - viewBox="0 0 221 64" - className={ classes } - > - <Path d="M32 0c17.672 0 32 14.328 32 32S49.672 64 32 64C14.326 64 0 49.672 0 32S14.328 0 32 0zm130.57 27.247c3.611 0 6.475 2.521 6.475 7.974 0 5.417-3.134 8.995-8.314 8.995-1.262 0-2.248-.172-3.305-.376v8.551h-4.022V27.67h3.85v1.486c1.534-1.191 3.237-1.908 5.316-1.908zM18.023 17.755v28.49h6.902V34.287l11.733 11.958h9.319l-27.954-28.49zm178.552 9.495c2.009 0 3.219.34 4.581.784v3.407c-1.158-.444-2.878-.921-4.513-.921-2.453 0-4.532 1.294-4.532 5.076 0 4.156 2.077 5.383 4.736 5.387 1.262 0 2.676-.273 4.516-.99v3.34c-1.636.544-3.118.92-5.025.92-6.033 0-8.417-3.442-8.417-8.45 0-5.283 3.305-8.553 8.654-8.553zm-18.195-.003c3.919 0 6.44 1.567 6.435 6.235V43.84h-3.747v-1.771h-.103c-1.327 1.022-2.93 2.079-5.314 2.079-2.112 0-4.36-1.534-4.36-4.6 0-4.133 3.545-4.912 5.997-5.252l3.51-.477v-.47c0-2.181-.886-2.897-2.93-2.897-.989 0-3.338.305-5.28 1.09l-.341-3.237c1.738-.614 4.123-1.058 6.133-1.058zm-73.55-.12c5.145 0 6.951 3.579 6.951 6.987 0 .936-.047 1.518-.102 2.06l-.034.325h-10.358c.101 3.542 2.106 4.36 5.104 4.36 1.637 0 3.158-.372 4.845-.991v3.342c-2.093.644-3.89.921-5.968.921-5.141 0-8.308-2.556-8.308-8.586 0-4.398 2.69-8.417 7.87-8.417zm39.998 0c1.771 0 3.305.172 4.804.614v3.338s-2.188-.714-4.497-.714c-1.43 0-2.76.477-2.76 1.703 0 2.725 8.042 1.262 8.04 6.883 0 4.053-3.611 5.178-7.394 5.178-1.738 0-3.487-.467-4.736-.958v-3.305c1.5.52 3.578 1.026 4.873 1.026 1.84 0 3.1-.374 3.1-1.771 0-2.829-8.04-1.33-8.04-7.156 0-2.998 2.724-4.837 6.61-4.837zm64.01-6.114v13.63c.34-.376.615-.785 5.657-6.952h5.213l-6.543 7.666 7.156 8.485h-5.248l-6.235-7.665v7.665h-4.02v-22.83h4.02zm-127.634 0l8.633 15.263V21.013h4.022v22.83h-4.416L80.81 28.578v15.263h-4.021v-22.83h4.416zm36.758 6.678l2.476 11.469 2.801-11.469h3.572l2.438 11.469 2.75-11.469h4.038l-4.54 16.151h-4.83l-1.714-9.495-2.325 9.495h-4.814l-4.268-16.15h4.416zm62.836 8.616l-3.305.512c-.987.136-2.044.732-2.044 2.231 0 1.322.75 2.067 1.84 2.067 1.157 0 2.452-.716 3.509-1.466v-3.344zm-19.318-5.752c-1.193 0-2.657.545-4.054 1.771v8.415c.885.17 1.77.306 2.997.306 2.829 0 4.464-1.805 4.464-5.517 0-3.476-1.158-4.975-3.407-4.975zm-56.685-.394c-2.009 0-3.235 1.43-3.475 3.678h6.268c0-1.975-.646-3.678-2.793-3.678zm-58.818.836H40.33l2.205 2.248h3.442v-2.248zm0-6.621H33.833l2.205 2.248h9.939v-2.248zm0-6.621h-18.64l2.204 2.248h16.436v-2.248z" /> - </SVG> - ); - } -} - -NewspackLogo.defaultProps = { - height: 56, -}; - -export default NewspackLogo; diff --git a/src/components/src/newspack-logo/style.scss b/src/components/src/newspack-logo/style.scss deleted file mode 100644 index 8557df150e..0000000000 --- a/src/components/src/newspack-logo/style.scss +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Newspack Logo - */ - -@use "../../../shared/scss/colors"; - -.newspack-logo { - &__svg { - display: block; - fill: currentcolor; - margin: 0; - padding-bottom: 32px; - width: auto; - - &.is-centered { - margin-left: auto; - margin-right: auto; - } - } - - &__wrapper { - background: colors.$primary-500; - color: white; - position: relative; - - a { - color: inherit; - } - } -} diff --git a/src/components/src/notice/index.js b/src/components/src/notice/index.js index d39d7fd694..337b8c3d8b 100644 --- a/src/components/src/notice/index.js +++ b/src/components/src/notice/index.js @@ -7,7 +7,6 @@ */ import { Component, RawHTML } from '@wordpress/element'; import { Icon, bug, check, help, info } from '@wordpress/icons'; -import { __ } from '@wordpress/i18n'; /** * Internal dependencies. @@ -62,7 +61,6 @@ class Notice extends Component { { <Icon icon={ noticeIcon } /> } <div className="newspack-notice__content"> { rawHTML ? <RawHTML>{ noticeText }</RawHTML> : noticeText } - { debugMode && __( 'Debug Mode', 'newspack-plugin' ) } { children || null } </div> </div> diff --git a/src/components/src/notice/style.scss b/src/components/src/notice/style.scss index e55005b338..79070abe0f 100644 --- a/src/components/src/notice/style.scss +++ b/src/components/src/notice/style.scss @@ -23,19 +23,21 @@ } &__is-debug { - background: colors.$primary-700; - border-radius: 2px; + background: colors.$primary-600; + border-radius: 50%; bottom: 16px; box-shadow: 0 0 8px 4px rgba(black, 0.08); color: white; font-weight: bold; margin: 0 16px; + padding: 6px; position: fixed; text-transform: uppercase; z-index: 9997; > svg { fill: white; + margin: 0; } } @@ -64,7 +66,7 @@ background: colors.$primary-050; > svg { - fill: colors.$primary-500; + fill: var(--wp-admin-theme-color); } } diff --git a/src/components/src/patrons-logo/index.js b/src/components/src/patrons-logo/index.js deleted file mode 100644 index 40116ec502..0000000000 --- a/src/components/src/patrons-logo/index.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * WordPress.com and Google News Initiative Logos. - */ - -/** - * WordPress dependencies - */ -import { Component } from '@wordpress/element'; -import { Path, SVG } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; - -class PatronsLogo extends Component { - /** - * Render. - */ - render = () => { - return ( - <SVG - xmlns="http://www.w3.org/2000/svg" - width="219" - height="16" - viewBox="0 0 438 32" - className="patrons-logo" - aria-label={ __( - 'A project of WordPress.com and the Google News Initiative', - 'newspack-plugin' - ) } - > - <Path - fillRule="evenodd" - clipRule="evenodd" - d="M0 16C0 7.179 7.146 0 15.93 0c8.784 0 15.93 7.178 15.93 16 0 8.823-7.146 16-15.93 16C7.145 32 0 24.823 0 16zm27.506-.271L23.13 28.435a14.349 14.349 0 005.214-5.263 14.424 14.424 0 001.906-7.171 14.36 14.36 0 00-1.754-6.902c.062.458.097.95.097 1.477 0 1.461-.271 3.102-1.088 5.153zM18.96 9.177c-.291.027-.665.057-1.056.077l5.177 15.463 1.427-4.792c.728-1.87 1.09-3.42 1.09-4.653 0-1.775-.635-3.007-1.179-3.964l-.154-.25-.014-.022c-.66-1.071-1.239-2.008-1.239-3.105 0-1.32.997-2.55 2.404-2.55.052 0 .1.004.148.008l.037.003a14.226 14.226 0 00-9.672-3.777 14.26 14.26 0 00-6.797 1.723 14.334 14.334 0 00-5.167 4.76c.337.01.654.019.922.019 1.497 0 3.817-.185 3.817-.185.772-.046.863 1.095.093 1.186 0 0-.234.028-.589.06-.291.026-.663.056-1.053.076l5.214 15.582 3.135-9.44-2.23-6.14a26.784 26.784 0 01-1.502-.138c-.772-.047-.681-1.23.091-1.186 0 0 2.364.185 3.771.185 1.498 0 3.818-.185 3.818-.185.772-.046.863 1.095.09 1.186 0 0-.23.027-.582.06zm-7.076 20.624a14.282 14.282 0 008.803-.23 1.188 1.188 0 01-.103-.199l-4.402-12.115-4.298 12.544zM2.846 10.145a14.434 14.434 0 00.945 13.493 14.345 14.345 0 005.888 5.308l-6.833-18.8z" - /> - <Path d="M56.125 8.758l-3.172 12.258-3.178-12.258h-3.209l.747 2.626-2.797 10.047L41.48 8.758h-3.087l4.334 16.01h3.362l2.596-8.56 2.431 8.56h3.361l4.549-16.01h-2.9zM151.302 24.767h-2.818v-2.915h2.818v2.915z" /> - <Path - fillRule="evenodd" - clipRule="evenodd" - d="M64.38 13.13c-3.878 0-5.71 2.509-5.71 5.973 0 3.465 1.832 5.95 5.71 5.95 3.854 0 5.71-2.485 5.71-5.95 0-3.464-1.856-5.973-5.71-5.973zm0 9.654c-1.833 0-2.807-1.194-2.807-3.681 0-2.461.974-3.68 2.807-3.68 1.83 0 2.783 1.196 2.783 3.68 0 2.462-.953 3.68-2.783 3.68zM86.344 13.13c.69 0 1.285.096 2.117.262V8.756h2.807v16.01h-2.663v-1.052c-.906.767-2.404 1.34-3.64 1.34-2.5 0-4.616-1.745-4.616-5.52 0-3.798 2.117-6.404 5.995-6.404zm-.548 9.654c.904 0 1.76-.455 2.665-1.29v-5.758a5.2 5.2 0 00-1.83-.36c-2.144 0-3.38 1.362-3.38 3.968 0 2.411 1 3.44 2.545 3.44zM99.941 8.758h-5.199v16.01h2.998v-5.57h2.201c3.403 0 5.879-1.84 5.879-5.28 0-3.466-2.476-5.16-5.879-5.16zm.024 8.1H97.74v-5.734h2.225c1.809 0 2.713 1.003 2.713 2.795 0 1.768-.834 2.938-2.713 2.938zM115.57 19.032c0-3.083 1.879-5.902 5.496-5.902 3.593 0 4.854 2.509 4.854 4.9 0 .767-.046 1.195-.092 1.636l-.004.035h-7.232c.072 2.487 1.47 3.059 3.564 3.059 1.141 0 2.204-.262 3.383-.697v2.344c-1.463.453-2.718.646-4.169.646-3.588 0-5.8-1.79-5.8-6.02zm5.472-3.775c-1.403 0-2.26 1.002-2.426 2.581h4.377c0-1.387-.453-2.582-1.951-2.582z" - /> - <Path d="M130.798 16.595c0-.861.926-1.196 1.925-1.196 1.613 0 3.14.504 3.14.504v-2.344c-1.047-.31-2.117-.43-3.353-.43-2.712 0-4.615 1.29-4.615 3.394 0 2.408 1.95 3.032 3.551 3.544 1.116.357 2.063.66 2.063 1.474 0 .98-.88 1.241-2.164 1.241-.906 0-2.357-.352-3.402-.717v2.316c.871.345 2.093.672 3.306.672 2.641 0 5.161-.787 5.161-3.63 0-2.325-1.95-2.922-3.552-3.412-1.114-.342-2.06-.631-2.06-1.416z" /> - <Path - fillRule="evenodd" - clipRule="evenodd" - d="M164.046 19.103c0-3.464 1.833-5.971 5.711-5.971 3.853 0 5.71 2.507 5.71 5.97 0 3.466-1.857 5.951-5.71 5.951-3.878 0-5.711-2.485-5.711-5.95zm2.904 0c0 2.487.974 3.679 2.807 3.679 1.832 0 2.783-1.217 2.783-3.68 0-2.483-.951-3.678-2.783-3.678-1.833 0-2.807 1.218-2.807 3.679z" - /> - <Path d="M191.528 13.132c-1.213 0-2.592.667-3.855 1.504l-.333.215c-.594-1.268-1.784-1.721-3.092-1.721-1.213 0-2.592.62-3.854 1.458v-1.172h-2.664v11.351h2.831V16.62c1.047-.692 2.165-1.123 2.973-1.123.927 0 1.498.5 1.498 2.102v7.17h2.807v-8.126c1.049-.693 2.167-1.146 2.975-1.146.927 0 1.498.5 1.498 2.102v7.17h2.809V16.93c0-2.2-1.309-3.798-3.593-3.798zM110.191 15.28h.095c.786-1.41 1.832-2.005 3.354-2.005.357 0 .762.046.762.046v2.461h-.285c-1.784 0-2.855.552-3.735 2.223v6.762h-2.807V13.416h2.616v1.865zM75.066 15.28h-.095v-1.864h-2.618v11.351h2.809v-6.762c.88-1.671 1.95-2.223 3.733-2.223h.287v-2.46s-.405-.047-.762-.047c-1.522 0-2.568.596-3.354 2.006zM158.983 15.424c-1.712 0-3.164.908-3.164 3.562 0 2.912 1.452 3.774 3.308 3.774.881 0 1.867-.192 3.151-.693v2.34c-1.14.383-2.176.648-3.507.648-4.213 0-5.877-2.416-5.877-5.928 0-3.703 2.306-5.997 6.043-5.997 1.403 0 2.248.24 3.2.55v2.388c-.81-.31-2.011-.644-3.154-.644zM141.016 16.595c0-.861.926-1.196 1.926-1.196 1.611 0 3.14.504 3.14.504v-2.344c-1.046-.31-2.117-.43-3.356-.43-2.711 0-4.615 1.29-4.615 3.394 0 2.408 1.95 3.032 3.552 3.544 1.116.357 2.063.66 2.063 1.474 0 .98-.88 1.241-2.165 1.241-.904 0-2.354-.352-3.401-.717v2.316c.87.345 2.094.672 3.307.672 2.642 0 5.161-.787 5.161-3.63 0-2.325-1.951-2.922-3.552-3.412-1.115-.342-2.06-.631-2.06-1.416zM300.756 24.617V8.443h2.523l7.837 12.582h.092l-.092-3.117V8.443h2.069v16.174h-2.162l-8.198-13.194h-.088l.088 3.118v10.076h-2.069z" /> - <Path - fillRule="evenodd" - clipRule="evenodd" - d="M318.835 24.585c.704.288 1.461.42 2.22.39v-.016a5.175 5.175 0 003.041-.866 5 5 0 001.776-2.158l-1.845-.768c-.527 1.262-1.532 1.899-3.02 1.899a3.36 3.36 0 01-2.396-.98 3.775 3.775 0 01-1.118-2.642h8.73l.024-.381a6.096 6.096 0 00-1.463-4.292 5.095 5.095 0 00-3.953-1.586 4.923 4.923 0 00-3.904 1.742 6.116 6.116 0 00-1.517 4.155 5.926 5.926 0 001.566 4.223 5.302 5.302 0 001.859 1.28zm4.324-8.66c.49.485.797 1.125.868 1.811l-6.397.015a3.654 3.654 0 011.161-1.958 2.925 2.925 0 011.991-.724 3.098 3.098 0 012.377.857z" - /> - <Path d="M339.793 24.617h-2.117l-2.748-8.496-2.722 8.496h-2.099l-3.557-11.07h2.162l2.454 8.359h.025l2.722-8.359h2.143l2.722 8.359h.025l2.43-8.359h2.118l-3.558 11.07zM345.498 24.069c.889.63 1.961.949 3.049.905v-.024a4.768 4.768 0 003.128-.979 3.073 3.073 0 001.22-2.481 2.987 2.987 0 00-.746-1.987 4.102 4.102 0 00-2.299-1.243l-2.181-.519c-.976-.23-1.464-.655-1.464-1.287a1.18 1.18 0 01.63-1.042 2.92 2.92 0 011.551-.382c1.276 0 2.117.49 2.523 1.468l1.806-.749a3.774 3.774 0 00-1.645-1.908 5.16 5.16 0 00-2.615-.666 5.214 5.214 0 00-3.074.916 2.787 2.787 0 00-1.294 2.383 2.49 2.49 0 00.947 2.045 5.298 5.298 0 002.006 1.028l2.225.543c1.008.261 1.512.75 1.512 1.468a1.312 1.312 0 01-.619 1.13 2.717 2.717 0 01-1.591.431 3.039 3.039 0 01-2.991-2.08l-1.85.769a5.333 5.333 0 001.772 2.26zM362.084 8.443h-2.074v16.17h2.074V8.442zM366.895 15.084v-1.537h-1.991v11.07h2.069v-6.122a3.726 3.726 0 01.815-2.383 2.55 2.55 0 012.093-1.028c1.815 0 2.723 1.024 2.723 3.073v6.46h2.084v-6.778a5.026 5.026 0 00-1.084-3.386 3.9 3.9 0 00-3.127-1.268 4.127 4.127 0 00-2.074.553 3.713 3.713 0 00-1.42 1.346h-.088zM379.094 10.768a1.414 1.414 0 01-1.039.43 1.396 1.396 0 01-1.035-.43 1.47 1.47 0 011.598-2.397 1.47 1.47 0 01.476 2.397zM379.094 24.617h-2.074v-11.07h2.074v11.07zM384.709 24.642c.457.157.942.209 1.421.151a4.17 4.17 0 001.664-.293l-.717-1.782c-.299.128-.622.19-.947.181-.992 0-1.488-.602-1.488-1.806v-5.647h2.703v-1.899h-2.703v-3.386h-2.074v3.386h-1.952v1.9h1.952v5.803a3.22 3.22 0 00.927 2.635c.343.34.758.6 1.214.757zM391.278 10.768a1.465 1.465 0 01-2.076.007 1.474 1.474 0 01.468-2.401 1.458 1.458 0 011.596.316 1.475 1.475 0 01.003 2.078h.009zM391.269 24.617h-2.074v-11.07h2.074v11.07z" /> - <Path - fillRule="evenodd" - clipRule="evenodd" - d="M394.24 23.961a4.356 4.356 0 002.928 1.018l.025.01a4.055 4.055 0 003.625-1.899h.088v1.522h1.952v-6.68a4.459 4.459 0 00-1.362-3.509 5.1 5.1 0 00-3.513-1.243 4.837 4.837 0 00-2.976.847 4.306 4.306 0 00-1.508 1.84l1.893.817c.18-.5.536-.917 1-1.174a3.152 3.152 0 011.635-.43 2.993 2.993 0 012.05.733c.276.243.494.545.637.884.144.339.21.706.192 1.074v.318a5.84 5.84 0 00-3.006-.68 5.645 5.645 0 00-3.445 1.027 3.323 3.323 0 00-1.4 2.834 3.385 3.385 0 001.185 2.692zm5.583-1.85a3.383 3.383 0 01-2.333.98 2.776 2.776 0 01-1.586-.49 1.467 1.467 0 01-.673-1.282 1.966 1.966 0 01.771-1.537 3.409 3.409 0 012.274-.656 4.045 4.045 0 012.63.724 2.99 2.99 0 01-1.083 2.261z" - /> - <Path d="M409.533 24.793a3.19 3.19 0 01-2.631-.91 3.207 3.207 0 01-.926-2.633v-5.804h-1.952v-1.899h1.952v-3.386h2.069v3.386h2.703v1.9h-2.703v5.646c0 1.204.496 1.806 1.488 1.806.325.008.648-.053.947-.18l.717 1.78a4.17 4.17 0 01-1.664.294zM414.072 11.135a1.47 1.47 0 00-.43-2.873 1.462 1.462 0 00-1.454 1.327 1.468 1.468 0 001.884 1.546zM412.617 24.617h2.064l.01-11.07h-2.074v11.07zM420.459 24.617l-4.455-11.07h2.249l3.245 8.584h.044l3.289-8.584h2.205l-4.503 11.07h-2.074z" /> - <Path - fillRule="evenodd" - clipRule="evenodd" - d="M430.621 24.58a5.31 5.31 0 002.213.394v-.015a5.173 5.173 0 003.04-.866 4.972 4.972 0 001.781-2.158l-1.845-.768c-.527 1.262-1.532 1.899-3.02 1.899a3.353 3.353 0 01-2.396-.98 3.742 3.742 0 01-1.118-2.642h8.691l.024-.381a6.076 6.076 0 00-1.464-4.292 5.063 5.063 0 00-3.903-1.586 4.926 4.926 0 00-3.904 1.742 6.097 6.097 0 00-1.513 4.155 5.934 5.934 0 001.562 4.223c.52.553 1.151.987 1.852 1.275zm4.321-8.654c.49.484.797 1.124.868 1.81l-6.397.015a3.676 3.676 0 011.156-1.957 2.968 2.968 0 011.996-.725 3.12 3.12 0 012.377.857zM276.919 23.82V13.21l-2.454-.005v.979h-.088a4.086 4.086 0 00-3.104-1.317c-2.947 0-5.645 2.594-5.645 5.931a5.882 5.882 0 001.64 4.08 5.847 5.847 0 004.005 1.793 4 4 0 003.104-1.346h.088v.847c0 2.26-1.196 3.47-3.148 3.47a3.262 3.262 0 01-2.967-2.104l-2.254.944a5.634 5.634 0 002.079 2.546 5.61 5.61 0 003.142.948c3.035 0 5.602-1.791 5.602-6.157zm-5.421-8.643c1.761 0 3.147 1.522 3.147 3.607 0 2.055-1.361 3.557-3.147 3.557-1.786 0-3.279-1.473-3.279-3.557 0-2.085 1.517-3.607 3.279-3.607z" - /> - <Path d="M226.063 24.034a9.134 9.134 0 003.568.637c2.737 0 4.747-.905 6.397-2.57 1.649-1.663 2.166-3.993 2.166-5.872a7.91 7.91 0 00-.136-1.566h-8.427v2.506h5.982a5.262 5.262 0 01-1.361 3.156 6.128 6.128 0 01-4.621 1.835 6.64 6.64 0 01-4.703-1.953 6.677 6.677 0 01-1.948-4.717c0-1.769.7-3.465 1.948-4.716a6.64 6.64 0 014.703-1.954 6.368 6.368 0 014.523 1.791l1.747-1.766a8.686 8.686 0 00-6.27-2.53 9.132 9.132 0 00-6.61 2.611 9.2 9.2 0 000 13.133 9.158 9.158 0 003.042 1.975z" /> - <Path - fillRule="evenodd" - clipRule="evenodd" - d="M250.189 21.972a5.832 5.832 0 001.019-3.213 5.801 5.801 0 00-1.661-4.182 5.76 5.76 0 00-4.146-1.724c-1.145 0-2.264.34-3.217.976a5.833 5.833 0 00-.917 8.936 5.792 5.792 0 008.922-.793zm-4.788-6.795c1.752 0 3.265 1.458 3.265 3.582 0 2.114-1.508 3.582-3.265 3.582-1.756 0-3.264-1.468-3.264-3.582s1.513-3.582 3.264-3.582zM293.646 22.048l-2.01-1.341a3.374 3.374 0 01-2.899 1.634 2.995 2.995 0 01-2.859-1.79l7.881-3.27-.269-.67c-.493-1.312-2.01-3.759-5.045-3.759s-5.475 2.394-5.475 5.907c0 3.313 2.415 5.912 5.767 5.912a5.843 5.843 0 004.909-2.623zm-2.923-5.662l-5.27 2.197a3.258 3.258 0 01.817-2.374 3.228 3.228 0 012.263-1.076 2.285 2.285 0 012.19 1.253zM264.227 18.76a5.839 5.839 0 01-1.018 3.212 5.813 5.813 0 01-2.623 2.108 5.792 5.792 0 01-6.3-1.315 5.841 5.841 0 01-1.222-6.336 5.82 5.82 0 012.139-2.6 5.796 5.796 0 013.217-.976 5.75 5.75 0 014.148 1.723 5.791 5.791 0 011.659 4.183zm-2.542 0c0-2.125-1.513-3.583-3.265-3.583-1.751 0-3.264 1.468-3.264 3.582s1.508 3.582 3.264 3.582c1.757 0 3.265-1.468 3.265-3.582z" - /> - <Path d="M281.457 6.941h-2.591v17.373h2.591V6.94z" /> - </SVG> - ); - }; -} - -export default PatronsLogo; diff --git a/src/components/src/plugin-settings/SettingsSection.js b/src/components/src/plugin-settings/SettingsSection.js index 740523536d..95e7f89616 100644 --- a/src/components/src/plugin-settings/SettingsSection.js +++ b/src/components/src/plugin-settings/SettingsSection.js @@ -89,6 +89,7 @@ const SettingsSection = props => { } return ( <ActionCard + id={ props.id ?? null } isMedium disabled={ disabled } title={ title } diff --git a/src/components/src/plugin-settings/index.js b/src/components/src/plugin-settings/index.js index 282af7cde5..77c71534c8 100644 --- a/src/components/src/plugin-settings/index.js +++ b/src/components/src/plugin-settings/index.js @@ -187,6 +187,7 @@ class PluginSettings extends Component { <SettingsSection key={ sectionKey } disabled={ inFlight } + id={ `plugin-settings-${ sectionKey }` } sectionKey={ sectionKey } title={ this.getSectionTitle( sectionKey ) } description={ this.getSectionDescription( sectionKey ) } diff --git a/src/components/src/plugin-toggle/README.md b/src/components/src/plugin-toggle/README.md index 5a76583790..ed2659ece0 100644 --- a/src/components/src/plugin-toggle/README.md +++ b/src/components/src/plugin-toggle/README.md @@ -26,11 +26,11 @@ ActionCards for WooCommerce and Instant Articles for WP with custom text and URL plugins={ { woocommerce: { actionText: __( 'Use WooCommerce' ), - href: '/wp-admin/admin.php?page=newspack', + href: '/wp-admin/admin.php?page=newspack-dashboard', }, 'fb-instant-articles': { actionText: __( 'Configure Instant Articles' ), - href: '/wp-admin/admin.php?page=newspack', + href: '/wp-admin/admin.php?page=newspack-dashboard', }, } } /> @@ -46,7 +46,7 @@ ActionCards for WooCommerce and Instant Articles for WP. Page will refresh after }, 'fb-instant-articles': { actionText: __( 'Configure Instant Articles' ), - href: '/wp-admin/admin.php?page=newspack', + href: '/wp-admin/admin.php?page=newspack-dashboard', }, } } /> diff --git a/src/components/src/plugin-toggle/index.js b/src/components/src/plugin-toggle/index.js index 61f9a4911f..57b8366e0e 100644 --- a/src/components/src/plugin-toggle/index.js +++ b/src/components/src/plugin-toggle/index.js @@ -97,6 +97,7 @@ class PluginToggle extends Component { description={ description } actionText={ this.actionTextForPlugin( plugin ) } handoff={ handoff } + onClick={ plugin.onClick ?? null } href={ href } toggle toggleChecked={ this.isPluginInstalledAndActive( plugin ) } diff --git a/src/components/src/popover/style.scss b/src/components/src/popover/style.scss index 86954e770c..678a0bf3bc 100644 --- a/src/components/src/popover/style.scss +++ b/src/components/src/popover/style.scss @@ -16,7 +16,7 @@ padding: 6px 8px; &:hover { - color: colors.$primary-500; + color: var(--wp-admin-theme-color); .components-menu-items__item-icon { fill: currentcolor; @@ -24,7 +24,7 @@ } &:focus:not(:disabled) { - box-shadow: inset 0 0 0 2px colors.$primary-500; + box-shadow: inset 0 0 0 2px var(--wp-admin-theme-color); position: relative; z-index: 1; } @@ -42,7 +42,7 @@ &:focus, &:focus:enabled &:hover, &:not(:disabled, [aria-disabled="true"], .is-default):hover { - color: colors.$primary-500; + color: var(--wp-admin-theme-color); } } @@ -89,8 +89,8 @@ min-height: 36px; &:focus ~ div.components-input-control__backdrop { - border-color: colors.$primary-500; - box-shadow: inset 0 0 0 1px colors.$primary-500; + border-color: var(--wp-admin-theme-color); + box-shadow: inset 0 0 0 1px var(--wp-admin-theme-color); } } diff --git a/src/components/src/progress-bar/style.scss b/src/components/src/progress-bar/style.scss index 45c588d7db..fb3cbb690b 100644 --- a/src/components/src/progress-bar/style.scss +++ b/src/components/src/progress-bar/style.scss @@ -35,7 +35,7 @@ } &__bar { - background-color: colors.$primary-500; + background-color: var(--wp-admin-theme-color); height: 8px; transition: width 125ms ease-in-out; } diff --git a/src/components/src/radio-control/style.scss b/src/components/src/radio-control/style.scss index 332a7b7c59..245b0fe542 100644 --- a/src/components/src/radio-control/style.scss +++ b/src/components/src/radio-control/style.scss @@ -55,8 +55,8 @@ } &:checked { - background-color: colors.$primary-500; - border-color: colors.$primary-500; + background-color: var(--wp-admin-theme-color); + border-color: var(--wp-admin-theme-color); &::before { border: none; @@ -64,8 +64,8 @@ } &:focus { - border-color: colors.$primary-500; - box-shadow: 0 0 0 2px white, 0 0 0 3.5px colors.$primary-500; + border-color: var(--wp-admin-theme-color); + box-shadow: 0 0 0 2px white, 0 0 0 3.5px var(--wp-admin-theme-color); } } diff --git a/src/components/src/section-header/index.js b/src/components/src/section-header/index.js index 7f5a7aa883..c608f44cce 100644 --- a/src/components/src/section-header/index.js +++ b/src/components/src/section-header/index.js @@ -18,6 +18,25 @@ import './style.scss'; */ import classnames from 'classnames'; +/** + * Represents a section header component. + * + * @typedef {Object} SectionHeaderProps + * @property {boolean} [centered=false] - Indicates if the header is centered. + * @property {?string} [className=null] - Additional CSS class name. + * @property {string} [description] - Description of the section. + * @property {number} [heading=2] - HTML heading level, e.g., 1 for h1, 2 for h2, etc. + * @property {boolean} [isWhite=false] - Indicates if the header should use a white theme. + * @property {boolean} [noMargin=false] - Indicates if the header should have no margin. + * @property {string} title - The title of the section. + * @property {?string} [id=null] - Optional ID for the header element. + */ + +/** + * Creates a section header. + * + * @param {SectionHeaderProps} props - The properties for the section header. + */ const SectionHeader = ( { centered = false, className = null, @@ -31,13 +50,19 @@ const SectionHeader = ( { // If id is in the URL as a scrollTo param, scroll to it on render. const ref = useRef(); useEffect( () => { - const params = new Proxy( new URLSearchParams( window.location.search ), { - get: ( searchParams, prop ) => searchParams.get( prop ), - } ); + const params = new Proxy( + new URLSearchParams( window.location.search ), + { + get: ( searchParams, prop ) => searchParams.get( prop ), + } + ); const scrollToId = params.scrollTo; if ( scrollToId && scrollToId === id ) { // Let parent scroll action run before running this. - window.setTimeout( () => ref.current.scrollIntoView( { behavior: 'smooth' } ), 250 ); + window.setTimeout( + () => ref.current.scrollIntoView( { behavior: 'smooth' } ), + 250 + ); } }, [] ); @@ -52,12 +77,24 @@ const SectionHeader = ( { const HeadingTag = `h${ heading }`; return ( - <div id={ id } className="newspack-section-header__container" ref={ ref }> + <div + id={ id } + className="newspack-section-header__container" + ref={ ref } + > <Grid columns={ 1 } gutter={ 8 } className={ classes }> - { typeof title === 'string' && <HeadingTag>{ title }</HeadingTag> } - { typeof title === 'function' && <HeadingTag>{ title() }</HeadingTag> } - { description && typeof description === 'string' && <p>{ description }</p> } - { typeof description === 'function' && <p>{ description() }</p> } + { typeof title === 'string' && ( + <HeadingTag>{ title }</HeadingTag> + ) } + { typeof title === 'function' && ( + <HeadingTag>{ title() }</HeadingTag> + ) } + { description && typeof description === 'string' && ( + <p>{ description }</p> + ) } + { typeof description === 'function' && ( + <p>{ description() }</p> + ) } </Grid> </div> ); diff --git a/src/components/src/steps-list-item/style.scss b/src/components/src/steps-list-item/style.scss index f9857ceb1c..cd4993fb54 100644 --- a/src/components/src/steps-list-item/style.scss +++ b/src/components/src/steps-list-item/style.scss @@ -35,7 +35,7 @@ background: #fff; border: 1px solid currentcolor; border-radius: 100%; - color: colors.$primary-500; + color: var(--wp-admin-theme-color); display: flex; height: 28px; justify-content: center; diff --git a/src/components/src/style-card/index.js b/src/components/src/style-card/index.js index 3666f64f71..7c9304407b 100644 --- a/src/components/src/style-card/index.js +++ b/src/components/src/style-card/index.js @@ -36,7 +36,7 @@ class StyleCard extends Component { <div className={ classes } id={ id }> <div className="newspack-style-card__image"> { imageType === 'html' ? ( - <div dangerouslySetInnerHTML={ image } /> + <div className="newspack-style-card__image-html" dangerouslySetInnerHTML={ image } /> ) : ( <img src={ image } alt={ cardTitle + ' ' + __( 'Thumbnail', 'newspack-plugin' ) } /> ) } diff --git a/src/components/src/style-card/style.scss b/src/components/src/style-card/style.scss index f9b6e3148a..6cb63bea85 100644 --- a/src/components/src/style-card/style.scss +++ b/src/components/src/style-card/style.scss @@ -15,7 +15,7 @@ text-align: center; .newspack-style-card__is-active & { - color: colors.$primary-500; + color: var(--wp-admin-theme-color); } } @@ -48,8 +48,8 @@ } .newspack-style-card__is-active & { - border-color: colors.$primary-500; - box-shadow: inset 0 0 0 1px colors.$primary-500; + border-color: var(--wp-admin-theme-color); + box-shadow: inset 0 0 0 1px var(--wp-admin-theme-color); img { position: relative; @@ -88,7 +88,7 @@ } .newspack-style-card__is-active & { - box-shadow: inset 0 0 0 1px colors.$primary-500; + box-shadow: inset 0 0 0 1px var(--wp-admin-theme-color); } } } diff --git a/src/components/src/style.scss b/src/components/src/style.scss index 543b6ea281..84dd198ea0 100644 --- a/src/components/src/style.scss +++ b/src/components/src/style.scss @@ -1,4 +1,15 @@ @use "~@wordpress/base-styles/colors" as wp-colors; +@use "../../shared/scss/colors" as colors; + +:root { + // WP Admin + --wp-admin-theme-color: #{colors.$primary-600}; + --wp-admin-theme-color--rgb: rgb(#{colors.$primary-600--rgb}); + --wp-admin-theme-color-darker-10: #{colors.$primary-700}; + --wp-admin-theme-color-darker-10--rgb: rgb(#{colors.$primary-700--rgb}); + --wp-admin-theme-color-darker-20: #{colors.$primary-800}; + --wp-admin-theme-color-darker-20--rgb: rgb(#{colors.$primary-800--rgb}); +} // Buttons Card diff --git a/src/components/src/tabbed-navigation/index.js b/src/components/src/tabbed-navigation/index.js index 5facf75c05..b96fe36dc4 100644 --- a/src/components/src/tabbed-navigation/index.js +++ b/src/components/src/tabbed-navigation/index.js @@ -16,6 +16,23 @@ const TabbedNavigation = ( { items, className, disableUpcoming, children = null const displayedItems = items.filter( item => ! item.isHiddenInTabbedNavigation ); const { location } = useHistory(); const currentIndex = findIndex( displayedItems, [ 'path', location.pathname ] ); + + function isActive( item, match, pathname ) { + if ( item.path === pathname ) { + return true; + } + if ( Array.isArray( item?.activeTabPaths ) ) { + return item.activeTabPaths.some( path => { + if ( path.endsWith( '*' ) ) { + const basePath = path.slice( 0, -1 ); + return pathname.startsWith( basePath ); + } + return item.activeTabPaths.includes( pathname ); + } ); + } + return match; + } + return ( <div className={ classnames( 'newspack-tabbed-navigation', className ) }> <ul> @@ -23,13 +40,8 @@ const TabbedNavigation = ( { items, className, disableUpcoming, children = null <li key={ index }> <NavLink to={ item.path } - isActive={ ( match, { pathname } ) => { - if ( item.activeTabPaths ) { - return item.activeTabPaths.includes( pathname ); - } - return match; - } } - exact + isActive={ ( match, { pathname } ) => isActive(item, match, pathname) } + exact={ item.hasOwnProperty( 'exact' ) ? item.exact : true } activeClassName={ 'selected' } className={ classnames( { disabled: disableUpcoming && index > currentIndex, @@ -45,4 +57,4 @@ const TabbedNavigation = ( { items, className, disableUpcoming, children = null ); }; -export default TabbedNavigation; +export default TabbedNavigation; \ No newline at end of file diff --git a/src/components/src/tabbed-navigation/style.scss b/src/components/src/tabbed-navigation/style.scss index 6c17c6a837..3703aab3d5 100644 --- a/src/components/src/tabbed-navigation/style.scss +++ b/src/components/src/tabbed-navigation/style.scss @@ -39,6 +39,7 @@ display: flex; font-weight: bold; height: 48px; + outline: none; padding: 12px 15px; text-decoration: none; @@ -51,15 +52,10 @@ &:focus, &:hover { color: var(--wp-admin-theme-color); - outline: none; - } - - &:focus { - box-shadow: none; } &:focus-visible { - outline: 2px solid; + outline: 2px solid var(--wp-admin-theme-color); outline-offset: -2px; } diff --git a/src/components/src/utils/color.ts b/src/components/src/utils/color.ts new file mode 100644 index 0000000000..9cc10557f4 --- /dev/null +++ b/src/components/src/utils/color.ts @@ -0,0 +1,32 @@ +/** + * Determine if black or white should be used based on a contrast ratio. + * + * @param hexcolor Hex code for determining contrast + * @return black or white string + */ +export function getContrast( hexcolor: string ) { + if ( hexcolor.charAt( 0 ) === '#' ) { + hexcolor = hexcolor.slice( 1 ); + } + + // Normalize to 6 character hex code if needed + if ( hexcolor.length === 3 ) { + hexcolor = hexcolor + .split( '' ) + .map( function ( hex ) { + return hex + hex; + } ) + .join( '' ); + } + + // Convert to RGB value + const r = parseInt( hexcolor.substring( 0, 2 ), 16 ); + const g = parseInt( hexcolor.substring( 2, 4 ), 16 ); + const b = parseInt( hexcolor.substring( 4 ), 16 ); + + // Get YIQ ratio + const yiq = ( r * 299 + g * 587 + b * 114 ) / 1000; + + // Check contrast + return yiq >= 128 ? 'black' : 'white'; +} diff --git a/src/components/src/web-preview/style.scss b/src/components/src/web-preview/style.scss index 175d8afb0d..de4c42f99b 100644 --- a/src/components/src/web-preview/style.scss +++ b/src/components/src/web-preview/style.scss @@ -7,12 +7,12 @@ .newspack-web-preview { // Color - --wp-admin-theme-color: #{colors.$primary-500}; - --wp-admin-theme-color--rgb: #{colors.$primary-500--rgb}; - --wp-admin-theme-color-darker-10: #{colors.$primary-600}; - --wp-admin-theme-color-darker-10--rgb: #{colors.$primary-600--rgb}; - --wp-admin-theme-color-darker-20: #{colors.$primary-700}; - --wp-admin-theme-color-darker-20--rgb: #{colors.$primary-700--rgb}; + --wp-admin-theme-color: #{colors.$primary-600}; + --wp-admin-theme-color--rgb: rgb(#{colors.$primary-600--rgb}); + --wp-admin-theme-color-darker-10: #{colors.$primary-700}; + --wp-admin-theme-color-darker-10--rgb: rgb(#{colors.$primary-700--rgb}); + --wp-admin-theme-color-darker-20: #{colors.$primary-800}; + --wp-admin-theme-color-darker-20--rgb: rgb(#{colors.$primary-800--rgb}); background: rgba(black, 0.7); box-sizing: border-box; @@ -102,7 +102,7 @@ padding: 0 !important; &.is-selected { - background: colors.$primary-500; + background: var(--wp-admin-theme-color); color: white; &:active, @@ -114,7 +114,7 @@ } &:focus { - box-shadow: inset 0 0 0 1px colors.$primary-500, inset 0 0 0 3px white !important; + box-shadow: inset 0 0 0 1px var(--wp-admin-theme-color), inset 0 0 0 3px white !important; } } diff --git a/src/components/src/with-wizard-screen/style.scss b/src/components/src/with-wizard-screen/style.scss index bbfc2d209e..e989580c10 100644 --- a/src/components/src/with-wizard-screen/style.scss +++ b/src/components/src/with-wizard-screen/style.scss @@ -7,12 +7,12 @@ .newspack-wizard { // Color - --wp-admin-theme-color: #{colors.$primary-500}; - --wp-admin-theme-color--rgb: #{colors.$primary-500--rgb}; - --wp-admin-theme-color-darker-10: #{colors.$primary-600}; - --wp-admin-theme-color-darker-10--rgb: #{colors.$primary-600--rgb}; - --wp-admin-theme-color-darker-20: #{colors.$primary-700}; - --wp-admin-theme-color-darker-20--rgb: #{colors.$primary-700--rgb}; + --wp-admin-theme-color: #{colors.$primary-600}; + --wp-admin-theme-color--rgb: rgb(#{colors.$primary-600--rgb}); + --wp-admin-theme-color-darker-10: #{colors.$primary-700}; + --wp-admin-theme-color-darker-10--rgb: rgb(#{colors.$primary-700--rgb}); + --wp-admin-theme-color-darker-20: #{colors.$primary-800}; + --wp-admin-theme-color-darker-20--rgb: rgb(#{colors.$primary-800--rgb}); color: wp-colors.$gray-900; @@ -83,7 +83,7 @@ } &:not(.newspack-icon) { - background: colors.$primary-500; + background: var(--wp-admin-theme-color); left: 50%; margin: -28px 0 0 -28px !important; opacity: 0; @@ -125,7 +125,7 @@ padding: 0 16px; @media screen and ( min-width: 744px ) { - padding: 32px 84px; + padding: 40px 84px; } // Typography diff --git a/src/components/src/with-wizard/index.js b/src/components/src/with-wizard/index.js index 35f26c0fed..a4aea0cfb4 100644 --- a/src/components/src/with-wizard/index.js +++ b/src/components/src/with-wizard/index.js @@ -4,7 +4,7 @@ import { Component, createRef, Fragment } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import apiFetch from '@wordpress/api-fetch'; -import { home } from '@wordpress/icons'; +import { category } from '@wordpress/icons'; /** * Internal dependencies. @@ -28,6 +28,7 @@ export default function withWizard( WrappedComponent, requiredPlugins ) { error: null, loading: requiredPlugins && requiredPlugins.length > 0 ? 1 : 0, quietLoading: false, + confirmation: null, }; this.wrappedComponentRef = createRef(); } @@ -221,7 +222,7 @@ export default function withWizard( WrappedComponent, requiredPlugins ) { href={ newspack_urls.dashboard } label={ __( 'Return to Dashboard', 'newspack-plugin' ) } showTooltip={ true } - icon={ home } + icon={ category } iconSize={ 36 } > <NewspackIcon size={ 36 } /> @@ -249,6 +250,69 @@ export default function withWizard( WrappedComponent, requiredPlugins ) { ); }; + /** + * Build a confirmation modal with the given title & message. + * Execute {callback} if confirmed. + * + * @property {Object} options Options for the confirmation modal. + * @property {string} options.title The title for the modal component. + * @property {string} options.message The message for the modal component body. + * @property {string} options.confirmText The text for the confirmation button. + * @property {string} options.cancelText The text for the cancel button. + * @property {Function} options.callback A function to call if the user confirms the action. + */ + confirmAction = ( options ) => { + const modalOptions = { + title: null, + message: __( 'Are you sure?', 'newpack-plugin' ), + confirmText: __( 'OK', 'newspack-plugin' ), + cancelText: __( 'Cancel', 'newspack-plugin' ), + callback: null, + ...options, + } + this.setState( { confirmation: modalOptions } ); + } + + /** + * Show a confirmation modal with the given title & message. + * Execute {callback} if confirmed. + * + * @return {Component} <Modal> + */ + getModal = () => { + if ( ! this.state.confirmation ) { + return null; + } + const { title, message, confirmText, cancelText, callback } = this.state.confirmation; + return message && callback && ( + <Modal + isNarrow + hideTitle={ ! title } + title={ title } + onRequestClose={ () => this.setState( { confirmation: null } ) } + > + <p>{ message }</p> + <Card buttonsCard noBorder className="justify-end"> + <Button + variant="secondary" + onClick={ () => this.setState( { confirmation: null } ) } + > + { cancelText } + </Button> + <Button + variant="primary" + onClick={ () => { + this.setState( { confirmation: null } ); + callback(); + } } + > + { confirmText } + </Button> + </Card> + </Modal> + ); + } + getFallbackURL = () => { if ( typeof newspack_urls !== 'undefined' ) { return newspack_urls.dashboard; @@ -270,8 +334,10 @@ export default function withWizard( WrappedComponent, requiredPlugins ) { return ( <Fragment> { this.getError() } + { this.getModal() } <div className={ loadingClasses.join( ' ' ) }> <WrappedComponent + confirmAction={ this.confirmAction } pluginRequirements={ requiredPlugins && this.pluginRequirements() } clearError={ this.clearError } getError={ this.getError } diff --git a/src/components/src/with-wizard/style.scss b/src/components/src/with-wizard/style.scss index 21a1c08b69..5071717bf9 100644 --- a/src/components/src/with-wizard/style.scss +++ b/src/components/src/with-wizard/style.scss @@ -5,13 +5,12 @@ @use "~@wordpress/base-styles/colors" as wp-colors; @use "../../../shared/scss/colors"; -// Reset Padding of the Admin Page +// Styling for full-page-react Wizards (ignoring admin-header-only wizards). +body.newspack-wizard-page:not(.newspack-admin-header) { -.toplevel_page_newspack:not(.menu-top), -body[class*="newspack_page_newspack-"], -body[class*="admin_page_newspack-"] { background: white; + // Reset Padding #wpcontent { padding-left: 0; } @@ -20,6 +19,13 @@ body[class*="admin_page_newspack-"] { padding-bottom: 220px; min-height: 100vh; + // For admin notices directly ">" at top of page above the Wizard header. + // PHP code should hide these notices, but just incase add this style. + > .notice { + margin-bottom: 20px; + margin-left: 22px; + } + @media screen and ( min-width: 783px ) { padding-bottom: 202px; } @@ -30,6 +36,24 @@ body[class*="admin_page_newspack-"] { } } +// Styling for wizards that are admin-header-only. +body.newspack-wizard-page.newspack-admin-header { + + // For mobile. + @media screen and (max-width: 600px) { + + // The header bar (and tabs) need padding. + #newspack-wizards-admin-header { + padding-top: 46px; + } + + // Since the padding was added to the header bar, remove from body. + #wpbody { + padding-top: 0; + } + } +} + svg { &.newspack--error { fill: wp-colors.$alert-red; @@ -44,12 +68,12 @@ svg { .newspack-wizard__blue { #wpwrap { - background: colors.$primary-500; + background: var(--wp-admin-theme-color); } .newspack-footer { - background: colors.$primary-500; - border-color: colors.$primary-500; + background: var(--wp-admin-theme-color); + border-color: var(--wp-admin-theme-color); justify-content: center; &__logo { @@ -92,7 +116,7 @@ svg { } &::before { - background: colors.$primary-500; + background: var(--wp-admin-theme-color); box-shadow: 24px 0 0 white, 69px 0 0 -15px wp-colors.$gray-900, @@ -137,7 +161,7 @@ svg { // Blue Screen .newspack-wizard__blue & { - background: colors.$primary-500; + background: var(--wp-admin-theme-color); &::before { animation: loading-quiet 1.25s ease-in-out infinite; @@ -193,7 +217,7 @@ svg { &::before { animation: loading-quiet 1.25s ease-in-out infinite; - background: colors.$primary-500; + background: var(--wp-admin-theme-color); height: 8px; right: 100%; z-index: 9999; diff --git a/src/components/src/wizard/index.js b/src/components/src/wizard/index.js index e43572f4ad..d84cde0b23 100644 --- a/src/components/src/wizard/index.js +++ b/src/components/src/wizard/index.js @@ -32,28 +32,55 @@ registerStore(); const { HashRouter, Redirect, Route, Switch } = Router; +/** + * @typedef {Object} WizardProps + * @property {string} headerText The header text. + * @property {string} [subHeaderText] The sub-header text, optional. + * @property {string} [apiSlug] The API slug, optional. + * @property {string} [className] CSS classes, optional. + * @property {any[]} sections Array of sections. + * @property {boolean} [hasSimpleFooter] Indicates if a simple footer is used, optional. + * @property {() => void} [renderAboveSections] Function to render content above sections, optional. + * @property {string[]} [requiredPlugins] Array of required plugin strings, optional. + * @property {boolean} [isInitialFetchTriggered] Indicates if the initial fetch should be triggered, optional. + */ + +/** + * Wizard Component + * + * Provides a tabbed UI with history. + * + * @param {WizardProps} props + * @return {JSX.Element} Wizard component + */ const Wizard = ( { sections = [], - apiSlug, headerText, + apiSlug, subHeaderText, hasSimpleFooter, className, renderAboveSections, requiredPlugins = [], + isInitialFetchTriggered = true, } ) => { - const isLoading = useSelect( select => select( WIZARD_STORE_NAMESPACE ).isLoading() ); - const isQuietLoading = useSelect( select => select( WIZARD_STORE_NAMESPACE ).isQuietLoading() ); + const isLoading = useSelect( select => + select( WIZARD_STORE_NAMESPACE ).isLoading() + ); + const isQuietLoading = useSelect( select => + select( WIZARD_STORE_NAMESPACE ).isQuietLoading() + ); // Trigger initial data fetch. Some sections might not use the wizard data, // but for consistency, fetching is triggered regardless of the section. - useSelect( select => select( WIZARD_STORE_NAMESPACE ).getWizardAPIData( apiSlug ) ); + useSelect( select => + isInitialFetchTriggered && select( WIZARD_STORE_NAMESPACE ).getWizardAPIData( apiSlug ) + ); let displayedSections = sections.filter( section => ! section.isHidden ); - const [ pluginRequirementsSatisfied, setPluginRequirementsSatisfied ] = useState( - requiredPlugins.length === 0 - ); + const [ pluginRequirementsSatisfied, setPluginRequirementsSatisfied ] = + useState( requiredPlugins.length === 0 ); if ( ! pluginRequirementsSatisfied ) { headerText = requiredPlugins.length > 1 @@ -65,7 +92,9 @@ const Wizard = ( { render: () => ( <PluginInstaller plugins={ requiredPlugins } - onStatus={ ( { complete } ) => setPluginRequirementsSatisfied( complete ) } + onStatus={ ( { complete } ) => + setPluginRequirementsSatisfied( complete ) + } /> ), }, @@ -76,7 +105,9 @@ const Wizard = ( { <> <div className={ classnames( - isLoading ? 'newspack-wizard__is-loading' : 'newspack-wizard__is-loaded', + isLoading + ? 'newspack-wizard__is-loading' + : 'newspack-wizard__is-loaded', { 'newspack-wizard__is-loading-quiet': isQuietLoading, } @@ -90,7 +121,10 @@ const Wizard = ( { <Button isLink href={ newspack_urls.dashboard } - label={ __( 'Return to Dashboard', 'newspack-plugin' ) } + label={ __( + 'Return to Dashboard', + 'newspack-plugin' + ) } showTooltip={ true } icon={ category } iconSize={ 36 } @@ -99,7 +133,9 @@ const Wizard = ( { </Button> <div> { headerText && <h2>{ headerText }</h2> } - { subHeaderText && <span>{ subHeaderText }</span> } + { subHeaderText && ( + <span>{ subHeaderText }</span> + ) } </div> </div> </div> @@ -116,14 +152,21 @@ const Wizard = ( { { displayedSections.map( ( section, index ) => { const SectionComponent = section.render; return ( - <Route key={ index } path={ section.path }> + <Route + key={ index } + exact={ section.exact ?? false } + path={ section.path } + > <div className={ classnames( - 'newspack-wizard newspack-wizard__content', + 'newspack-wizard__content', className ) } > - { 'function' === typeof renderAboveSections ? renderAboveSections() : null } + { 'function' === + typeof renderAboveSections + ? renderAboveSections() + : null } <SectionComponent /> </div> </Route> diff --git a/src/components/src/wizard/store/index.js b/src/components/src/wizard/store/index.js index e0ee0e88f2..59394aa7ff 100644 --- a/src/components/src/wizard/store/index.js +++ b/src/components/src/wizard/store/index.js @@ -16,7 +16,7 @@ import { createAction } from './utils.js'; export const WIZARD_STORE_NAMESPACE = 'newspack/wizards'; const DEFAULT_STATE = { - isLoading: true, + isLoading: false, isQuietLoading: false, apiData: {}, error: null, @@ -94,6 +94,7 @@ const selectors = { isLoading: state => state.isLoading, isQuietLoading: state => state.isQuietLoading, getWizardAPIData: ( state, slug ) => state.apiData[ slug ] || {}, + getWizardData: ( state, slug ) => state.apiData[ slug ] ?? {}, getError: state => state.error, }; @@ -104,8 +105,9 @@ const store = createReduxStore( WIZARD_STORE_NAMESPACE, { controls: { FETCH_FROM_API: action => { + const { isLocalError = false, isQuietFetch = false } = action.payload; dispatch( WIZARD_STORE_NAMESPACE ).startLoadingData( { - isQuietLoading: Boolean( action.payload.isQuietFetch ), + isQuietLoading: Boolean( isQuietFetch ), } ); return apiFetch( action.payload ) .then( data => { @@ -113,8 +115,10 @@ const store = createReduxStore( WIZARD_STORE_NAMESPACE, { return data; } ) .catch( error => { + if ( isLocalError ) { + throw error; + } dispatch( WIZARD_STORE_NAMESPACE ).setError( error ); - return { error }; } ) .finally( result => { dispatch( WIZARD_STORE_NAMESPACE ).finishLoadingData(); diff --git a/src/components/src/wizard/store/utils.js b/src/components/src/wizard/store/utils.js index e9f17efb20..c1c83857ea 100644 --- a/src/components/src/wizard/store/utils.js +++ b/src/components/src/wizard/store/utils.js @@ -10,7 +10,8 @@ import { WIZARD_STORE_NAMESPACE } from '.'; export const createAction = type => payload => ( { type, payload } ); -export const useWizardData = ( wizardName, defaultValue = {} ) => - useSelect( select => - select( WIZARD_STORE_NAMESPACE ).getWizardAPIData( `newspack-${ wizardName }-wizard` ) +export const useWizardData = ( wizardName, defaultValue = {} ) => { + return useSelect( select => + select( WIZARD_STORE_NAMESPACE ).getWizardAPIData( wizardName ) ) || defaultValue; +}; diff --git a/src/newspack-ui/scss/variables/_colors.scss b/src/newspack-ui/scss/variables/_colors.scss index 7d94487f21..ba75da85e3 100644 --- a/src/newspack-ui/scss/variables/_colors.scss +++ b/src/newspack-ui/scss/variables/_colors.scss @@ -14,25 +14,18 @@ --newspack-ui-color-neutral-100: #000; // Primary: - --newspack-ui-color-primary-0: #f5fdff; - --newspack-ui-color-primary-5: #d6f6ff; - --newspack-ui-color-primary-10: #b4e4ff; - --newspack-ui-color-primary-20: #93ccfd; - --newspack-ui-color-primary-30: #72affb; - --newspack-ui-color-primary-40: #528dfc; - --newspack-ui-color-primary-50: #36f; - --newspack-ui-color-primary-60: #2240d5; - --newspack-ui-color-primary-70: #1522af; - --newspack-ui-color-primary-80: #0b0b8d; - --newspack-ui-color-primary-90: #0d046e; - --newspack-ui-color-primary-100: #0e0052; - - // Secondary: - --newspack-ui-color-secondary-30: #fffff0; - --newspack-ui-color-secondary-40: #ffffd3; - --newspack-ui-color-secondary-50: #ff0; - --newspack-ui-color-secondary-60: #f2f200; - --newspack-ui-color-secondary-70: #e4e400; + --newspack-ui-color-primary-0: #dfe7f4; + --newspack-ui-color-primary-5: #bfcfe9; + --newspack-ui-color-primary-10: #9fb6dd; + --newspack-ui-color-primary-20: #809ed2; + --newspack-ui-color-primary-30: #6086c7; + --newspack-ui-color-primary-40: #406ebc; + --newspack-ui-color-primary-50: #2055b0; + --newspack-ui-color-primary-60: #003da5; + --newspack-ui-color-primary-70: #00296e; + --newspack-ui-color-primary-80: #001f53; + --newspack-ui-color-primary-90: #001437; + --newspack-ui-color-primary-100: #000a1c; // Success: --newspack-ui-color-success-0: #edfaef; diff --git a/src/shared/scss/_colors.scss b/src/shared/scss/_colors.scss index a8867a6917..836d966c3b 100644 --- a/src/shared/scss/_colors.scss +++ b/src/shared/scss/_colors.scss @@ -1,27 +1,27 @@ // HEX -$primary-000: #f5fdff; -$primary-050: #d6f6ff; -$primary-100: #b4e4ff; -$primary-200: #93ccfd; -$primary-300: #72affb; -$primary-400: #528dfc; -$primary-500: #36f; -$primary-600: #2240d5; -$primary-700: #1522af; -$primary-800: #0b0b8d; -$primary-900: #0d046e; -$primary-950: #0e0052; +$primary-000: #dfe7f4; +$primary-050: #bfcfe9; +$primary-100: #9fb6dd; +$primary-200: #809ed2; +$primary-300: #6086c7; +$primary-400: #406ebc; +$primary-500: #2055b0; +$primary-600: #003da5; +$primary-700: #00296e; +$primary-800: #001f53; +$primary-900: #001437; +$primary-1000: #000a1c; // RGB -$primary-000--rgb: 245, 253, 255; -$primary-050--rgb: 214, 246, 255; -$primary-100--rgb: 180, 228, 255; -$primary-200--rgb: 147, 204, 253; -$primary-300--rgb: 114, 175, 251; -$primary-400--rgb: 82, 141, 252; -$primary-500--rgb: 51, 102, 255; -$primary-600--rgb: 34, 64, 213; -$primary-700--rgb: 21, 34, 175; -$primary-800--rgb: 11, 11, 141; -$primary-900--rgb: 13, 4, 110; -$primary-950--rgb: 14, 0, 82; +$primary-000--rgb: 223, 231, 244; +$primary-050--rgb: 191, 207, 233; +$primary-100--rgb: 159, 182, 221; +$primary-200--rgb: 128, 158, 210; +$primary-300--rgb: 96, 134, 199; +$primary-400--rgb: 64, 110, 188; +$primary-500--rgb: 32, 85, 176; +$primary-600--rgb: 0, 61, 165; +$primary-700--rgb: 0, 41, 110; +$primary-800--rgb: 0, 31, 83; +$primary-900--rgb: 0, 20, 55; +$primary-1000--rgb: 0, 10, 28; diff --git a/src/wizards/admin-header/index.tsx b/src/wizards/admin-header/index.tsx new file mode 100644 index 0000000000..104f1eb6be --- /dev/null +++ b/src/wizards/admin-header/index.tsx @@ -0,0 +1,61 @@ +import { render, Fragment } from '@wordpress/element'; +import { NewspackIcon } from '../../components/src'; +import './style.scss'; + +export function WizardsAdminHeader( { + title, + tabs, +}: { + title: string; + tabs: Array< { + textContent: string; + href: string; + forceSelected: boolean; + } >; +} ) { + return ( + <Fragment> + <div className="newspack-wizard__header"> + <div className="newspack-wizard__header__inner"> + <div className="newspack-wizard__title"> + <NewspackIcon size={ 36 } /> + <div> + <h2>{ title }</h2> + </div> + </div> + </div> + </div> + { tabs.length > 0 && ( + <div className="newspack-tabbed-navigation"> + <ul> + { tabs.map( ( tab, i ) => { + const selected = tab.forceSelected ? true : window.location.href === tab.href; + return ( + <li key={ `${ tab.textContent }:${ i }` }> + <a + href={ tab.href } + className={ + selected + ? 'selected' + : '' + } + > + { tab.textContent } + </a> + </li> + ); + } ) } + </ul> + </div> + ) } + </Fragment> + ); +} + +render( + <WizardsAdminHeader + title={ window.newspackWizardsAdminHeader.title } + tabs={ window.newspackWizardsAdminHeader.tabs } + />, + document.getElementById( 'newspack-wizards-admin-header' ) +); diff --git a/src/wizards/admin-header/style.scss b/src/wizards/admin-header/style.scss new file mode 100644 index 0000000000..1cc5f703d2 --- /dev/null +++ b/src/wizards/admin-header/style.scss @@ -0,0 +1,3 @@ +.newspack-wizards-admin-header { + margin-left: -20px; +} diff --git a/src/wizards/advertising/components/ad-refresh-control/index.js b/src/wizards/advertising/components/ad-refresh-control/index.js index 105e6fd02b..35c432fd01 100644 --- a/src/wizards/advertising/components/ad-refresh-control/index.js +++ b/src/wizards/advertising/components/ad-refresh-control/index.js @@ -132,6 +132,7 @@ export default function AdRefreshControlSettings() { return ( <PluginSettings.Section error={ error } + id="ad-refresh-control" disabled={ inFlight } sectionKey="ad-refresh-control" title={ __( 'Ad Refresh Control', 'newspack-plugin' ) } diff --git a/src/wizards/advertising/components/ad-unit-size-control/index.js b/src/wizards/advertising/components/ad-unit-size-control/index.js index 420c4be0e9..c3e0a737df 100644 --- a/src/wizards/advertising/components/ad-unit-size-control/index.js +++ b/src/wizards/advertising/components/ad-unit-size-control/index.js @@ -87,7 +87,7 @@ const AdUnitSizeControl = ( { value, selectedOptions, onChange } ) => { hideLabelFromVision /> { value === 'fluid' && ! isCustom ? ( - <div className="newspack-advertising-wizard__ad-unit-fluid"> + <div className="newspack-ads-display-ads__ad-unit-fluid"> { __( 'Fluid is a native ad size that allows more flexibility when styling your ad. It automatically sizes the ad by filling the width of the enclosing column and adjusting the height as appropriate.', 'newspack-plugin' diff --git a/src/wizards/advertising/components/add-ons/index.js b/src/wizards/advertising/components/add-ons/index.js index d42b8a25c4..ebac48384a 100644 --- a/src/wizards/advertising/components/add-ons/index.js +++ b/src/wizards/advertising/components/add-ons/index.js @@ -106,11 +106,19 @@ export default () => ( plugins={ { 'super-cool-ad-inserter': { actionText: __( 'Configure', 'newspack-plugin' ), - href: '#/settings', + href: '#/settings/scaip', + shouldRefreshAfterUpdate: true, + onClick() { + document.getElementById( 'plugin-settings-scaip' ).scrollIntoView(); + }, }, 'ad-refresh-control': { actionText: __( 'Configure', 'newspack-plugin' ), - href: '#/settings', + href: '#/settings/ad-refresh-control', + shouldRefreshAfterUpdate: true, + onClick() { + document.getElementById( 'ad-refresh-control' ).scrollIntoView(); + }, }, } } /> diff --git a/src/wizards/advertising/components/onboarding/index.js b/src/wizards/advertising/components/onboarding/index.js index abe7c6a49e..d3ad6cbfc5 100644 --- a/src/wizards/advertising/components/onboarding/index.js +++ b/src/wizards/advertising/components/onboarding/index.js @@ -11,7 +11,7 @@ import { useEffect, useState, useRef, Fragment } from '@wordpress/element'; * Internal dependencies. */ import { Card, ButtonCard, Notice, TextControl } from '../../../../components/src'; -import GoogleOAuth from '../../../connections/views/main/google'; +import GoogleOAuth from '../../../newspack/views/settings/connections/google-oauth'; import { handleJSONFile } from '../utils'; export default function AdsOnboarding( { onUpdate, onSuccess } ) { diff --git a/src/wizards/advertising/index.js b/src/wizards/advertising/index.js index c8e77da27c..889b5f798a 100644 --- a/src/wizards/advertising/index.js +++ b/src/wizards/advertising/index.js @@ -15,7 +15,7 @@ import { __ } from '@wordpress/i18n'; */ import { withWizard, utils } from '../../components/src'; import Router from '../../components/src/proxied-imports/router'; -import { AdUnit, AdUnits, Providers, Settings, Placements, Suppression, AddOns } from './views'; +import { AdUnit, AdUnits, Providers, Settings, Placements } from './views'; import { getSizes } from './components/ad-unit-size-control'; import './style.scss'; @@ -158,14 +158,6 @@ class AdvertisingWizard extends Component { label: __( 'Settings', 'newspack-plugin' ), path: '/settings', }, - { - label: __( 'Suppression', 'newspack-plugin' ), - path: '/suppression', - }, - { - label: __( 'Add-Ons', 'newspack-plugin' ), - path: '/addons', - }, ]; return ( <Fragment> @@ -177,11 +169,7 @@ class AdvertisingWizard extends Component { exact render={ () => ( <Providers - headerText="Advertising" - subHeaderText={ __( - 'Manage ad providers and their settings.', - 'newspack-plugin' - ) } + headerText={ __( 'Advertising / Display Ads', 'newspack-plugin' ) } services={ services } toggleService={ this.toggleService } fetchAdvertisingData={ this.fetchAdvertisingData } @@ -193,11 +181,7 @@ class AdvertisingWizard extends Component { path="/placements" render={ () => ( <Placements - headerText={ __( 'Advertising', 'newspack-plugin' ) } - subHeaderText={ __( - 'Define global advertising placements to serve ad units on your site', - 'newspack-plugin' - ) } + headerText={ __( 'Advertising / Display Ads', 'newspack-plugin' ) } tabbedNavigation={ tabs } /> ) } @@ -206,11 +190,7 @@ class AdvertisingWizard extends Component { path="/settings" render={ () => ( <Settings - headerText={ __( 'Advertising', 'newspack-plugin' ) } - subHeaderText={ __( - 'Configure display and advanced settings for your ads', - 'newspack-plugin' - ) } + headerText={ __( 'Advertising / Display Ads', 'newspack-plugin' ) } tabbedNavigation={ tabs } /> ) } @@ -292,31 +272,6 @@ class AdvertisingWizard extends Component { ); } } /> - <Route - path="/suppression" - render={ () => ( - <Suppression - headerText={ __( 'Advertising', 'newspack-plugin' ) } - subHeaderText={ __( - 'Allows you to manage site-wide ad suppression', - 'newspack-plugin' - ) } - tabbedNavigation={ tabs } - config={ advertisingData.suppression } - onChange={ config => this.updateAdSuppression( config ) } - /> - ) } - /> - <Route - path="/addons" - render={ () => ( - <AddOns - headerText={ __( 'Advertising', 'newspack-plugin' ) } - subHeaderText={ __( 'Add-ons for enhanced advertising', 'newspack-plugin' ) } - tabbedNavigation={ tabs } - /> - ) } - /> <Redirect to="/" /> </Switch> </HashRouter> @@ -327,5 +282,5 @@ class AdvertisingWizard extends Component { render( createElement( withWizard( AdvertisingWizard, [ 'newspack-ads' ] ) ), - document.getElementById( 'newspack-advertising-wizard' ) + document.getElementById( 'newspack-ads-display-ads' ) ); diff --git a/src/wizards/advertising/style.scss b/src/wizards/advertising/style.scss index 8207b08840..fd44a3141b 100644 --- a/src/wizards/advertising/style.scss +++ b/src/wizards/advertising/style.scss @@ -1,4 +1,4 @@ -.newspack-advertising-wizard { +.newspack-ads-display-ads { .newspack-button-card .newspack-notice { margin: 24px 0 0; } @@ -28,4 +28,24 @@ margin-top: 64px; } } + + .newspack-section-header { + margin: 0; + } + + .newspack-section-header__container { + margin-bottom: 40px; + + .heading-1 :is(h1, h2, h3, h4, h5, h6) { + font-size: 2rem; + font-weight: 400; + line-height: 1.25; + } + } + + .newspack-plugin-settings { + :nth-last-child(1 of .newspack-card) { + margin-bottom: 64px; + } + } } diff --git a/src/wizards/advertising/views/index.js b/src/wizards/advertising/views/index.js index d1babc6e0d..14261c786a 100644 --- a/src/wizards/advertising/views/index.js +++ b/src/wizards/advertising/views/index.js @@ -3,5 +3,3 @@ export { default as Placements } from './placements'; export { default as Settings } from './settings'; export { default as AdUnits } from './ad-units'; export { default as AdUnit } from './ad-unit'; -export { default as Suppression } from './suppression'; -export { default as AddOns } from './add-ons'; diff --git a/src/wizards/advertising/views/placements/index.js b/src/wizards/advertising/views/placements/index.js index b83358dd95..d976315179 100644 --- a/src/wizards/advertising/views/placements/index.js +++ b/src/wizards/advertising/views/placements/index.js @@ -131,6 +131,7 @@ const Placements = () => { return ( <Fragment> + <h1>{ __( 'Placements', 'newspack-plugin' ) }</h1> { ! inFlight && ! providers.length && ( <Notice isWarning diff --git a/src/wizards/advertising/views/providers/index.js b/src/wizards/advertising/views/providers/index.js index 0b603d16ce..b16a9816c6 100644 --- a/src/wizards/advertising/views/providers/index.js +++ b/src/wizards/advertising/views/providers/index.js @@ -6,7 +6,7 @@ * WordPress dependencies */ import { ExternalLink } from '@wordpress/components'; -import { useState } from '@wordpress/element'; +import { Fragment, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import apiFetch from '@wordpress/api-fetch'; @@ -77,7 +77,8 @@ const Providers = ( { services, fetchAdvertisingData, toggleService } ) => { } return ( - <> + <Fragment> + <h1>{ __( 'Providers', 'newspack-plugin' ) }</h1> <ActionCard title={ __( 'Google Ad Manager', 'newspack-plugin' ) } description={ __( @@ -139,7 +140,7 @@ const Providers = ( { services, fetchAdvertisingData, toggleService } ) => { </Card> </Modal> ) } - </> + </Fragment> ); }; diff --git a/src/wizards/advertising/views/suppression/index.js b/src/wizards/advertising/views/suppression/index.js index 959706a093..dd680fe35c 100644 --- a/src/wizards/advertising/views/suppression/index.js +++ b/src/wizards/advertising/views/suppression/index.js @@ -18,7 +18,6 @@ import { CategoryAutocomplete, SectionHeader, Waiting, - withWizardScreen, } from '../../../../components/src'; const Suppression = () => { @@ -70,6 +69,7 @@ const Suppression = () => { { error && <Notice isError noticeText={ error.message } /> } <SectionHeader title={ __( 'Post Types', 'newspack-plugin' ) } + heading={ 3 } description={ __( 'Suppress ads on specific post types.', 'newspack-plugin' ) } /> <Grid columns={ 3 } gutter={ 16 }> @@ -92,6 +92,7 @@ const Suppression = () => { </Grid> <SectionHeader title={ __( 'Tags', 'newspack-plugin' ) } + heading={ 3 } description={ __( 'Suppress ads on specific tags and their archive pages.', 'newspack-plugin' @@ -118,6 +119,7 @@ const Suppression = () => { /> <SectionHeader title={ __( 'Categories', 'newspack-plugin' ) } + heading={ 3 } description={ __( 'Suppress ads on specific categories and their archive pages.', 'newspack-plugin' @@ -143,6 +145,7 @@ const Suppression = () => { /> <SectionHeader title={ __( 'Author Archive Pages', 'newspack-plugin' ) } + heading={ 3 } description={ __( 'Suppress ads on automatically generated pages displaying a list of posts by an author.', 'newspack-plugin' @@ -165,4 +168,4 @@ const Suppression = () => { ); }; -export default withWizardScreen( Suppression ); +export default Suppression; diff --git a/src/wizards/analytics/index.js b/src/wizards/analytics/index.js deleted file mode 100644 index ce351058fd..0000000000 --- a/src/wizards/analytics/index.js +++ /dev/null @@ -1,70 +0,0 @@ -import '../../shared/js/public-path'; - -/** - * Analytics - */ - -/** - * WordPress dependencies. - */ -import { Component, render, Fragment, createElement } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies. - */ -import { withWizard } from '../../components/src'; -import Router from '../../components/src/proxied-imports/router'; -import { Plugins, NewspackCustomEvents } from './views'; -import './style.scss'; - -const { HashRouter, Redirect, Route, Switch } = Router; - -const TABS = [ - { - label: __( 'Plugins', 'newspack-plugin' ), - path: '/', - exact: true, - }, - { - label: __( 'Newspack Custom Events', 'newspack-plugin' ), - path: '/newspack-custom-events', - }, -]; - -class AnalyticsWizard extends Component { - /** - * Render - */ - render() { - const { pluginRequirements, wizardApiFetch, isLoading } = this.props; - const sharedProps = { - headerText: __( 'Analytics', 'newspack-plugin' ), - subHeaderText: __( 'Manage Google Analytics Configuration', 'newspack-plugin' ), - tabbedNavigation: TABS, - wizardApiFetch, - isLoading, - }; - return ( - <Fragment> - <HashRouter hashType="slash"> - <Switch> - { pluginRequirements } - <Route - path="/newspack-custom-events" - exact - render={ () => <NewspackCustomEvents { ...sharedProps } /> } - /> - <Route path="/" exact render={ () => <Plugins { ...sharedProps } /> } /> - <Redirect to="/" /> - </Switch> - </HashRouter> - </Fragment> - ); - } -} - -render( - createElement( withWizard( AnalyticsWizard, [ 'google-site-kit' ] ) ), - document.getElementById( 'newspack-analytics-wizard' ) -); diff --git a/src/wizards/analytics/style.scss b/src/wizards/analytics/style.scss deleted file mode 100644 index 08d718eca2..0000000000 --- a/src/wizards/analytics/style.scss +++ /dev/null @@ -1,70 +0,0 @@ -@use "~@wordpress/base-styles/colors" as wp-colors; - -.newspack__analytics-configuration { - &__header { - display: flex; - flex-wrap: wrap; - justify-content: space-between; - margin: 32px 0; - } - th { - background: wp-colors.$gray-100; - border-bottom: 1px solid wp-colors.$gray-300; - border-top: 1px solid wp-colors.$gray-300; - color: wp-colors.$gray-900; - font-weight: normal; - padding: 8px; - text-align: left; - } - table { - width: 100%; - margin: 32px 0; - border-spacing: 0; - } - tbody { - td { - padding: 8px; - border-bottom: 1px solid wp-colors.$gray-300; - } - .newspack-checkbox-control { - margin: 0; - } - } - &__form { - display: flex; - align-items: flex-end; - margin: 32px 0; - @media screen and ( max-width: 600px ) { - flex-wrap: wrap; - } - .newspack-select-control, - .newspack-text-control { - flex: 1; - - .components-base-control__field { - margin: 0; - } - } - > * { - margin: 0 !important; - &:not(:last-child) { - margin-right: 32px !important; - } - } - button { - overflow: visible; - @media screen and ( max-width: 600px ) { - margin-top: 10px !important; - } - } - } - &__select { - .components-base-control__field { - margin: 0; - } - } -} - -.newspack__analytics-newspack-custom-events__save-button { - margin-top: 15px; -} diff --git a/src/wizards/analytics/views/index.js b/src/wizards/analytics/views/index.js deleted file mode 100644 index ca85057b21..0000000000 --- a/src/wizards/analytics/views/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as Plugins } from './plugins'; -export { default as NewspackCustomEvents } from './newspack-custom-events'; diff --git a/src/wizards/analytics/views/newspack-custom-events/index.js b/src/wizards/analytics/views/newspack-custom-events/index.js deleted file mode 100644 index a3005eca15..0000000000 --- a/src/wizards/analytics/views/newspack-custom-events/index.js +++ /dev/null @@ -1,119 +0,0 @@ -/* global newspack_analytics_wizard_data */ - -/** - * WordPress dependencies - */ -import { Component } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { - Button, - Grid, - Notice, - SectionHeader, - TextControl, - withWizardScreen, -} from '../../../../components/src'; - -/** - * Analytics Custom Events screen. - */ -class NewspackCustomEvents extends Component { - state = { - ga4Credendials: newspack_analytics_wizard_data.ga4_credentials, - error: false, - }; - - handleAPIError = ( { message: error } ) => this.setState( { error } ); - - updateGa4Credentials = () => { - const { wizardApiFetch } = this.props; - wizardApiFetch( { - path: '/newspack/v1/wizard/analytics/ga4-credentials', - method: 'POST', - quiet: true, - data: { - measurement_id: this.state.ga4Credendials.measurement_id, - measurement_protocol_secret: this.state.ga4Credendials.measurement_protocol_secret, - }, - } ) - .then( response => this.setState( { ga4Credendials: response, error: false } ) ) - .catch( this.handleAPIError ); - }; - - render() { - const { error, ga4Credendials } = this.state; - const { isLoading } = this.props; - - return ( - <div className="newspack__analytics-configuration"> - <div className="newspack__analytics-configuration__header"> - <SectionHeader - title={ __( 'Activate Newspack Custom Events', 'newspack-plugin' ) } - description={ __( - 'Allows Newspack to send enhanced custom event data to your Google Analytics.', - 'newspack-plugin' - ) } - noMargin - /> - <p> - { __( - "Newspack already sends some custom event data to your GA account, but adding the credentials below enables enhanced events that are fired from your site's backend. For example, when a donation is confirmed or when a user successfully subscribes to a newsletter.", - 'newspack-plugin' - ) } - </p> - </div> - - { error && <Notice isError noticeText={ error } /> } - <Grid noMargin rowGap={ 16 }> - <TextControl - value={ ga4Credendials?.measurement_id } - label={ __( 'Measurement ID', 'newspack-plugin' ) } - help={ __( - 'You can find this in Site Kit Settings, or in Google Analytics > Admin > Data Streams and clickng the data stream. Example: G-ABCD1234', - 'newspack-plugin' - ) } - onChange={ value => - this.setState( { - ...this.state, - ga4Credendials: { ...ga4Credendials, measurement_id: value }, - } ) - } - disabled={ isLoading } - autoComplete="off" - /> - <TextControl - type="password" - value={ ga4Credendials?.measurement_protocol_secret } - label={ __( 'Measurement Protocol API Secret', 'newspack-plugin' ) } - help={ __( - 'Generate an API secret from your GA dashboard in Admin > Data Streams and opening your data stream. Select "Measurement Protocol API secrets" under the Events section. Create a new secret.', - 'newspack-plugin' - ) } - onChange={ value => - this.setState( { - ...this.state, - ga4Credendials: { ...ga4Credendials, measurement_protocol_secret: value }, - } ) - } - disabled={ isLoading } - autoComplete="off" - /> - </Grid> - <Button - className="newspack__analytics-newspack-custom-events__save-button" - variant="primary" - disabled={ isLoading } - onClick={ this.updateGa4Credentials } - > - { __( 'Save', 'newspack-plugin' ) } - </Button> - </div> - ); - } -} - -export default withWizardScreen( NewspackCustomEvents ); diff --git a/src/wizards/analytics/views/plugins/index.js b/src/wizards/analytics/views/plugins/index.js deleted file mode 100644 index 01a8ea4499..0000000000 --- a/src/wizards/analytics/views/plugins/index.js +++ /dev/null @@ -1,44 +0,0 @@ -/* global newspack_analytics_wizard_data */ - -/** - * Analytics Plugins View - */ - -/** - * WordPress dependencies - */ -import { Component, Fragment } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { ActionCard, withWizardScreen } from '../../../../components/src'; - -/** - * Analytics Plugins screen. - */ -class Plugins extends Component { - /** - * Render. - */ - render() { - return ( - <Fragment> - <ActionCard - title={ __( 'Google Analytics', 'newspack-plugin' ) } - description={ __( 'Configure and view site analytics', 'newspack-plugin' ) } - actionText={ __( 'View', 'newspack-plugin' ) } - handoff="google-site-kit" - editLink={ - newspack_analytics_wizard_data.analyticsConnectionError - ? undefined - : 'admin.php?page=googlesitekit-module-analytics' - } - /> - </Fragment> - ); - } -} - -export default withWizardScreen( Plugins ); diff --git a/src/wizards/engagement/components/active-campaign.js b/src/wizards/audience/components/active-campaign.js similarity index 100% rename from src/wizards/engagement/components/active-campaign.js rename to src/wizards/audience/components/active-campaign.js diff --git a/src/wizards/audience/components/billing-fields/index.tsx b/src/wizards/audience/components/billing-fields/index.tsx new file mode 100644 index 0000000000..14eb57657f --- /dev/null +++ b/src/wizards/audience/components/billing-fields/index.tsx @@ -0,0 +1,118 @@ +/** + * WordPress dependencies. + */ +import { useDispatch, useSelect } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { CheckboxControl } from '@wordpress/components'; + +/** + * Internal dependencies. + */ +import { Button, Grid, Wizard } from '../../../../components/src'; +import WizardsSection from '../../../wizards-section'; + +const BillingFields = () => { + const wizardData = Wizard.useWizardData( + 'newspack-audience/billing-fields' + ); + const { updateWizardSettings, saveWizardSettings } = useDispatch( + Wizard.STORE_NAMESPACE + ); + const isQuietLoading = useSelect( + ( select: any ) => + select( Wizard.STORE_NAMESPACE ).isQuietLoading() ?? false, + [] + ); + + if ( ! wizardData ) { + return null; + } + + const changeHandler = ( value: any ) => + updateWizardSettings( { + slug: 'newspack-audience/billing-fields', + path: [ 'billing_fields' ], + value, + } ); + + const onSave = () => + saveWizardSettings( { + slug: 'newspack-audience/billing-fields', + } ); + + const availableFields = wizardData.available_billing_fields; + const orderNotesField = wizardData.order_notes_field; + if ( ! availableFields || ! Object.keys( availableFields ).length ) { + return null; + } + + const billingFields = wizardData.billing_fields.length + ? wizardData.billing_fields + : Object.keys( availableFields ); + + return ( + <WizardsSection + title={ __( 'Billing Fields', 'newspack-plugin' ) } + description={ __( + 'Configure the billing fields shown in the modal checkout form. Fields marked with (*) are required if shown. Note that for shippable products, address fields will always be shown.', + 'newspack-plugin' + ) } + className={ isQuietLoading ? 'is-fetching' : '' } + > + <Grid columns={ 3 } rowGap={ 16 }> + { Object.keys( availableFields ).map( fieldKey => ( + <CheckboxControl + key={ fieldKey } + label={ + availableFields[ fieldKey ].label + + ( availableFields[ fieldKey ].required ? ' *' : '' ) + } + checked={ billingFields.includes( fieldKey ) } + disabled={ fieldKey === 'billing_email' } // Email is always required. + onChange={ () => { + let newFields = [ ...billingFields ]; + if ( billingFields.includes( fieldKey ) ) { + newFields = newFields.filter( + field => field !== fieldKey + ); + } else { + newFields = [ ...newFields, fieldKey ]; + } + changeHandler( newFields ); + } } + /> + ) ) } + { orderNotesField && ( + <CheckboxControl + label={ orderNotesField.label } + checked={ billingFields.includes( 'order_comments' ) } + onChange={ () => { + let newFields = [ ...billingFields ]; + if ( billingFields.includes( 'order_comments' ) ) { + newFields = newFields.filter( + field => field !== 'order_comments' + ); + } else { + newFields = [ ...newFields, 'order_comments' ]; + } + changeHandler( newFields ); + } } + /> + ) } + </Grid> + <div className="newspack-buttons-card"> + <Button + variant="primary" + onClick={ onSave } + disabled={ isQuietLoading } + > + { isQuietLoading + ? __( 'Saving…', 'newspack-plugin' ) + : __( 'Save Settings', 'newspack-plugin' ) } + </Button> + </div> + </WizardsSection> + ); +}; + +export default BillingFields; diff --git a/src/wizards/popups/components/campaign-management-popover/index.js b/src/wizards/audience/components/campaign-management-popover/index.js similarity index 100% rename from src/wizards/popups/components/campaign-management-popover/index.js rename to src/wizards/audience/components/campaign-management-popover/index.js diff --git a/src/wizards/audience/components/checkout-configuration/index.tsx b/src/wizards/audience/components/checkout-configuration/index.tsx new file mode 100644 index 0000000000..4e06a0d294 --- /dev/null +++ b/src/wizards/audience/components/checkout-configuration/index.tsx @@ -0,0 +1,134 @@ +/** + * WordPress dependencies. + */ +import { useDispatch, useSelect } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { ToggleControl, TextareaControl } from '@wordpress/components'; + +/** + * Internal dependencies. + */ +import { Button, Grid, Wizard } from '../../../../components/src'; +import WizardsSection from '../../../wizards-section'; + +const DATA_STORE_KEY = 'newspack-audience/checkout-configuration'; + +function CheckoutConfiguration() { + const config = Wizard.useWizardData( DATA_STORE_KEY ); + const { updateWizardSettings, saveWizardSettings } = useDispatch( + Wizard.STORE_NAMESPACE + ); + const isQuietLoading = useSelect( + ( select: any ) => + select( Wizard.STORE_NAMESPACE ).isQuietLoading() ?? false, + [] + ); + + const onChange = ( value: any, key: string ) => + updateWizardSettings( { + slug: DATA_STORE_KEY, + path: [ key ], + value, + } ); + + function onSave() { + saveWizardSettings( { + slug: DATA_STORE_KEY, + } ); + } + + return ( + <WizardsSection + title={ __( 'Checkout Configuration', 'newspack-plugin' ) } + className={ isQuietLoading ? 'is-fetching' : '' } + > + <ToggleControl + label={ __( + 'Require sign in or create account before checkout', + 'newspack-plugin' + ) } + help={ __( + 'Prompt users who are not logged in to sign in or register a new account before proceeding to checkout. When disabled, an account will automatically be created with the email address used at checkout.', + 'newspack-plugin' + ) } + checked={ config.woocommerce_registration_required ?? false } + onChange={ value => + onChange( value, 'woocommerce_registration_required' ) + } + disabled={ isQuietLoading } + /> + <Grid> + <TextareaControl + label={ __( + 'Post-checkout success message', + 'newspack-plugin' + ) } + help={ __( + 'The success message to display to readers after completing checkout.', + 'newspack-plugin' + ) } + value={ config.woocommerce_post_checkout_success_text } + onChange={ value => + onChange( + value, + 'woocommerce_post_checkout_success_text' + ) + } + /> + { ! config.woocommerce_registration_required && ( + <TextareaControl + label={ __( + 'Post-checkout registration success message', + 'newspack-plugin' + ) } + help={ __( + 'The success message to display to new readers that have an account automatically created after completing checkout.', + 'newspack-plugin' + ) } + value={ + config.woocommerce_post_checkout_registration_success_text + } + onChange={ value => + onChange( + value, + 'woocommerce_post_checkout_registration_success_text' + ) + } + /> + ) } + </Grid> + <Grid> + <TextareaControl + label={ __( + 'Checkout privacy policy text', + 'newspack-plugin' + ) } + help={ __( + 'The privacy policy text to display at time of checkout for existing users. This will not show up unless a privacy page is set.', + 'newspack-plugin' + ) } + value={ config.woocommerce_checkout_privacy_policy_text } + onChange={ value => + onChange( + value, + 'woocommerce_checkout_privacy_policy_text' + ) + } + /> + </Grid> + <div className="newspack-buttons-card"> + <Button + variant="primary" + onClick={ onSave } + disabled={ isQuietLoading } + > + { isQuietLoading + ? __( 'Saving…', 'newspack-plugin' ) + : __( 'Save Settings', 'newspack-plugin' ) } + </Button> + </div> + </WizardsSection> + ); +} + +export default CheckoutConfiguration; diff --git a/src/wizards/audience/components/cover-fees-settings/index.js b/src/wizards/audience/components/cover-fees-settings/index.js new file mode 100644 index 0000000000..7b11c063f4 --- /dev/null +++ b/src/wizards/audience/components/cover-fees-settings/index.js @@ -0,0 +1,95 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { CheckboxControl } from '@wordpress/components'; +import { useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { + ActionCard, + Button, + Grid, + TextControl, + Wizard, +} from '../../../../components/src'; +import { AUDIENCE_DONATIONS_WIZARD_SLUG } from '../../constants'; + +export const CoverFeesSettings = () => { + const { additional_settings: settings = {} } = Wizard.useWizardData( AUDIENCE_DONATIONS_WIZARD_SLUG ); + const { updateWizardSettings } = useDispatch( Wizard.STORE_NAMESPACE ); + const changeHandler = ( key, value ) => + updateWizardSettings( { + slug: AUDIENCE_DONATIONS_WIZARD_SLUG, + path: [ 'additional_settings', key ], + value, + } ); + + const { saveWizardSettings } = useDispatch( Wizard.STORE_NAMESPACE ); + const onSave = () => + saveWizardSettings( { + slug: AUDIENCE_DONATIONS_WIZARD_SLUG, + section: 'settings', + payloadPath: [ 'additional_settings' ], + } ); + + return ( + <ActionCard + isMedium + title={ __( 'Collect transaction fees', 'newspack-plugin' ) } + description={ __( 'Allow donors to optionally cover transaction fees imposed by payment processors.', 'newspack-plugin' ) } + notificationLevel="info" + toggleChecked={ settings.allow_covering_fees } + toggleOnChange={ () => { + changeHandler( 'allow_covering_fees', ! settings.allow_covering_fees ); + onSave(); + } } + hasGreyHeader={ settings.allow_covering_fees } + hasWhiteHeader={ ! settings.allow_covering_fees } + actionContent={ settings.allow_covering_fees && ( + <Button isPrimary onClick={ onSave }> + { __( 'Save Settings', 'newspack-plugin' ) } + </Button> + ) } + > + { settings.allow_covering_fees && ( + <Grid noMargin rowGap={ 16 }> + <TextControl + type="number" + step="0.1" + value={ settings.fee_multiplier } + label={ __( 'Fee multiplier', 'newspack-plugin' ) } + onChange={ value => changeHandler( 'fee_multiplier', value ) } + /> + <TextControl + type="number" + step="0.1" + value={ settings.fee_static } + label={ __( 'Fee static portion', 'newspack-plugin' ) } + onChange={ value => changeHandler( 'fee_static', value ) } + /> + <TextControl + value={ settings.allow_covering_fees_label } + label={ __( 'Custom message', 'newspack-plugin' ) } + placeholder={ __( + 'A message to explain the transaction fee option (optional).', + 'newspack-plugin' + ) } + onChange={ value => changeHandler( 'allow_covering_fees_label', value ) } + /> + <CheckboxControl + label={ __( 'Cover fees by default', 'newspack-plugin' ) } + checked={ settings.allow_covering_fees_default } + onChange={ () => changeHandler( 'allow_covering_fees_default', ! settings.allow_covering_fees_default ) } + help={ __( + 'If enabled, the option to cover the transaction fee will be checked by default.', + 'newspack-plugin' + ) } + /> + </Grid> + ) } + </ActionCard> + ); +} diff --git a/src/wizards/popups/components/lists-control/index.js b/src/wizards/audience/components/lists-control/index.js similarity index 100% rename from src/wizards/popups/components/lists-control/index.js rename to src/wizards/audience/components/lists-control/index.js diff --git a/src/wizards/engagement/components/mailchimp.js b/src/wizards/audience/components/mailchimp.js similarity index 100% rename from src/wizards/engagement/components/mailchimp.js rename to src/wizards/audience/components/mailchimp.js diff --git a/src/wizards/engagement/components/metadata-fields.js b/src/wizards/audience/components/metadata-fields.js similarity index 100% rename from src/wizards/engagement/components/metadata-fields.js rename to src/wizards/audience/components/metadata-fields.js diff --git a/src/wizards/readerRevenue/components/money-input/index.js b/src/wizards/audience/components/money-input/index.js similarity index 100% rename from src/wizards/readerRevenue/components/money-input/index.js rename to src/wizards/audience/components/money-input/index.js diff --git a/src/wizards/readerRevenue/components/money-input/style.scss b/src/wizards/audience/components/money-input/style.scss similarity index 100% rename from src/wizards/readerRevenue/components/money-input/style.scss rename to src/wizards/audience/components/money-input/style.scss diff --git a/src/wizards/readerRevenue/views/nrh-settings/index.js b/src/wizards/audience/components/nrh-settings/index.js similarity index 79% rename from src/wizards/readerRevenue/views/nrh-settings/index.js rename to src/wizards/audience/components/nrh-settings/index.js index 8ebeab10dd..ad23a0a220 100644 --- a/src/wizards/readerRevenue/views/nrh-settings/index.js +++ b/src/wizards/audience/components/nrh-settings/index.js @@ -9,18 +9,17 @@ import { useEffect, useState } from '@wordpress/element'; * Internal dependencies */ import { - ActionCard, AutocompleteWithSuggestions, Button, Grid, TextControl, Wizard, } from '../../../../components/src'; -import { READER_REVENUE_WIZARD_SLUG } from '../../constants'; +import WizardsSection from '../../../wizards-section'; const NRHSettings = () => { const [ selectedPage, setSelectedPage ] = useState( null ); - const wizardData = Wizard.useWizardData( 'reader-revenue' ); + const wizardData = Wizard.useWizardData( 'newspack-audience/payment' ); const { updateWizardSettings, saveWizardSettings } = useDispatch( Wizard.STORE_NAMESPACE ); useEffect( () => { @@ -31,48 +30,41 @@ const NRHSettings = () => { const changeHandler = ( key, value ) => { return updateWizardSettings( { - slug: READER_REVENUE_WIZARD_SLUG, + slug: 'newspack-audience/payment', path: [ 'platform_data', key ], value, } ); }; const onSave = () => saveWizardSettings( { - slug: READER_REVENUE_WIZARD_SLUG, + slug: 'newspack-audience/payment', payloadPath: [ 'platform_data' ], } ); const settings = wizardData?.platform_data || {}; return ( - <ActionCard - hasGreyHeader - isMedium - title={ __( 'News Revenue Hub Settings', 'newspack' ) } - description={ __( 'Configure your site’s connection to News Revenue Hub.', 'newspack' ) } - actionContent={ - <Button isPrimary onClick={ onSave }> - { __( 'Save Settings' ) } - </Button> - } + <WizardsSection + title={ __( 'News Revenue Hub Settings', 'newspack-plugin' ) } + description={ __( 'Configure your site’s connection to News Revenue Hub.', 'newspack-plugin' ) } > <div> <Grid columns={ 3 }> <TextControl - label={ __( 'Organization ID', 'newspack' ) } + label={ __( 'Organization ID', 'newspack-plugin' ) } placeholder="exampleid" value={ settings?.nrh_organization_id || '' } onChange={ value => changeHandler( 'nrh_organization_id', value ) } /> <TextControl - label={ __( 'Custom domain (optional)', 'newspack' ) } + label={ __( 'Custom domain (optional)', 'newspack-plugin' ) } help={ __( 'Enter the raw domain without protocol or slashes.' ) } placeholder="donate.example.com" value={ settings?.nrh_custom_domain || '' } onChange={ value => changeHandler( 'nrh_custom_domain', value ) } /> <TextControl - label={ __( 'Salesforce Campaign ID (optional)', 'newspack' ) } + label={ __( 'Salesforce Campaign ID (optional)', 'newspack-plugin' ) } placeholder="exampleid" value={ settings?.nrh_salesforce_campaign_id || '' } onChange={ value => changeHandler( 'nrh_salesforce_campaign_id', value ) } @@ -82,7 +74,7 @@ const NRHSettings = () => { { settings.hasOwnProperty( 'donor_landing_page' ) && ( <div> <hr /> - <h3>{ __( 'Donor Landing Page', 'newspack' ) }</h3> + <h3>{ __( 'Donor Landing Page', 'newspack-plugin' ) }</h3> <p className="components-base-control__help"> { __( 'Set a page on your site as a donor landing page. Once a reader donates and lands on this page, they will be considered a donor.', @@ -90,7 +82,7 @@ const NRHSettings = () => { ) } </p> <AutocompleteWithSuggestions - label={ __( 'Search for a New Donor Landing Page', 'newspack' ) } + label={ __( 'Search for a New Donor Landing Page', 'newspack-plugin' ) } help={ __( 'Begin typing page title, click autocomplete result to select.', 'newspack' @@ -105,13 +97,18 @@ const NRHSettings = () => { return changeHandler( 'donor_landing_page', item ); } } postTypes={ [ { slug: 'page', label: 'Page' } ] } - postTypeLabel={ __( 'page', 'newspack' ) } - postTypeLabelPlural={ __( 'pages', 'newspack' ) } + postTypeLabel={ __( 'page', 'newspack-plugin' ) } + postTypeLabelPlural={ __( 'pages', 'newspack-plugin' ) } selectedItems={ selectedPage ? [ selectedPage ] : [] } /> </div> ) } - </ActionCard> + <div className="newspack-buttons-card"> + <Button isPrimary onClick={ onSave }> + { __( 'Save Settings' ) } + </Button> + </div> + </WizardsSection> ); }; diff --git a/src/wizards/readerRevenue/views/payment-methods/index.js b/src/wizards/audience/components/payment-methods/index.js similarity index 58% rename from src/wizards/readerRevenue/views/payment-methods/index.js rename to src/wizards/audience/components/payment-methods/index.js index 05f4a2d539..950748aac0 100644 --- a/src/wizards/readerRevenue/views/payment-methods/index.js +++ b/src/wizards/audience/components/payment-methods/index.js @@ -7,10 +7,10 @@ import { ExternalLink } from '@wordpress/components'; /** * Internal dependencies */ -import { AdditionalSettings } from './additional-settings'; import { Stripe } from './stripe'; +import { Notice, Wizard } from '../../../../components/src'; +import WizardsSection from '../../../wizards-section'; import { PaymentGateway } from './payment-gateway'; -import { Notice, SectionHeader, Wizard } from '../../../../components/src'; import './style.scss'; const PaymentGateways = () => { @@ -18,34 +18,35 @@ const PaymentGateways = () => { payment_gateways: paymentGateways = {}, is_ssl, errors = [], - additional_settings: settings = {}, plugin_status, platform_data = {}, - } = Wizard.useWizardData( 'reader-revenue' ); + } = Wizard.useWizardData( 'newspack-audience/payment' ); if ( false === plugin_status || 'wc' !== platform_data?.platform ) { return null; } - const hasPaymentGateway = Object.keys( paymentGateways ).some( gateway => paymentGateways[ gateway ]?.enabled ); return ( - <> - <SectionHeader - title={ __( 'Payment Gateways', 'newspack-plugin' ) } - description={ () => ( - <> - { __( - 'Configure Newspack-supported payment gateways for WooCommerce. Payment gateways allow you to accept various payment methods from your readers. ', - 'newspack-plugin' - ) } - <ExternalLink href="https://woocommerce.com/document/premium-payment-gateway-extensions/"> - { __( 'Learn more', 'newspack-plugin' ) } - </ExternalLink> - </> - ) } - /> + <WizardsSection + title={ __( 'Payment Gateways', 'newspack-plugin' ) } + description={ () => ( + <> + { __( + 'Configure Newspack-supported payment gateways for WooCommerce. Payment gateways allow you to accept various payment methods from your readers. ', + 'newspack-plugin' + ) } + <ExternalLink href="https://woocommerce.com/document/premium-payment-gateway-extensions/"> + { __( 'Learn more', 'newspack-plugin' ) } + </ExternalLink> + </> + ) } + > { errors.length > 0 && errors.map( ( error, index ) => ( - <Notice isError key={ index } noticeText={ <span>{ error.message }</span> } /> + <Notice + isError + key={ index } + noticeText={ <span>{ error.message }</span> } + /> ) ) } { is_ssl === false && ( <Notice @@ -72,12 +73,7 @@ const PaymentGateways = () => { return <PaymentGateway key={ gateway } gateway={ paymentGateways[ gateway ] } />; } ) } - { hasPaymentGateway && ( - <AdditionalSettings - settings={ settings } - /> - ) } - </> + </WizardsSection> ); }; diff --git a/src/wizards/readerRevenue/views/payment-methods/payment-gateway.js b/src/wizards/audience/components/payment-methods/payment-gateway.js similarity index 95% rename from src/wizards/readerRevenue/views/payment-methods/payment-gateway.js rename to src/wizards/audience/components/payment-methods/payment-gateway.js index 401cae6711..eb4093ea76 100644 --- a/src/wizards/readerRevenue/views/payment-methods/payment-gateway.js +++ b/src/wizards/audience/components/payment-methods/payment-gateway.js @@ -13,7 +13,6 @@ import { Button, Wizard, } from '../../../../components/src'; -import { READER_REVENUE_WIZARD_SLUG } from '../../constants'; export const PaymentGateway = ( { gateway } ) => { const isLoading = useSelect( select => select( Wizard.STORE_NAMESPACE ).isLoading() ); @@ -21,7 +20,7 @@ export const PaymentGateway = ( { gateway } ) => { const { updateWizardSettings } = useDispatch( Wizard.STORE_NAMESPACE ); const changeHandler = ( key, value ) => updateWizardSettings( { - slug: READER_REVENUE_WIZARD_SLUG, + slug: 'newspack-audience/payment', path: [ 'payment_gateways', gateway.slug, key ], value, } ); @@ -29,7 +28,7 @@ export const PaymentGateway = ( { gateway } ) => { const { saveWizardSettings } = useDispatch( Wizard.STORE_NAMESPACE ); const onSave = () => saveWizardSettings( { - slug: READER_REVENUE_WIZARD_SLUG, + slug: 'newspack-audience/payment', section: 'gateway', payloadPath: [ 'payment_gateways', gateway.slug ], } ); diff --git a/src/wizards/readerRevenue/views/payment-methods/stripe.js b/src/wizards/audience/components/payment-methods/stripe.js similarity index 95% rename from src/wizards/readerRevenue/views/payment-methods/stripe.js rename to src/wizards/audience/components/payment-methods/stripe.js index f1df8df4c1..9142566189 100644 --- a/src/wizards/readerRevenue/views/payment-methods/stripe.js +++ b/src/wizards/audience/components/payment-methods/stripe.js @@ -13,7 +13,6 @@ import { Button, Wizard, } from '../../../../components/src'; -import { READER_REVENUE_WIZARD_SLUG } from '../../constants'; export const Stripe = ( { stripe } ) => { const isLoading = useSelect( select => select( Wizard.STORE_NAMESPACE ).isLoading() ); @@ -21,7 +20,7 @@ export const Stripe = ( { stripe } ) => { const { updateWizardSettings } = useDispatch( Wizard.STORE_NAMESPACE ); const changeHandler = ( key, value ) => updateWizardSettings( { - slug: READER_REVENUE_WIZARD_SLUG, + slug: 'newspack-audience/payment', path: [ 'payment_gateways', 'stripe', key ], value, } ); @@ -29,7 +28,7 @@ export const Stripe = ( { stripe } ) => { const { saveWizardSettings } = useDispatch( Wizard.STORE_NAMESPACE ); const onSave = () => saveWizardSettings( { - slug: READER_REVENUE_WIZARD_SLUG, + slug: 'newspack-audience/payment', section: 'stripe', payloadPath: [ 'payment_gateways', 'stripe' ], } ); diff --git a/src/wizards/readerRevenue/views/payment-methods/style.scss b/src/wizards/audience/components/payment-methods/style.scss similarity index 100% rename from src/wizards/readerRevenue/views/payment-methods/style.scss rename to src/wizards/audience/components/payment-methods/style.scss diff --git a/src/wizards/audience/components/payment-methods/woopayments.js b/src/wizards/audience/components/payment-methods/woopayments.js new file mode 100644 index 0000000000..eb4093ea76 --- /dev/null +++ b/src/wizards/audience/components/payment-methods/woopayments.js @@ -0,0 +1,110 @@ +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { ExternalLink } from '@wordpress/components'; +import { useDispatch, useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { + ActionCard, + Button, + Wizard, +} from '../../../../components/src'; + +export const PaymentGateway = ( { gateway } ) => { + const isLoading = useSelect( select => select( Wizard.STORE_NAMESPACE ).isLoading() ); + const isQuietLoading = useSelect( select => select( Wizard.STORE_NAMESPACE ).isQuietLoading() ); + const { updateWizardSettings } = useDispatch( Wizard.STORE_NAMESPACE ); + const changeHandler = ( key, value ) => + updateWizardSettings( { + slug: 'newspack-audience/payment', + path: [ 'payment_gateways', gateway.slug, key ], + value, + } ); + + const { saveWizardSettings } = useDispatch( Wizard.STORE_NAMESPACE ); + const onSave = () => + saveWizardSettings( { + slug: 'newspack-audience/payment', + section: 'gateway', + payloadPath: [ 'payment_gateways', gateway.slug ], + } ); + const testMode = gateway?.test_mode; + const isConnected = gateway?.is_connected; + const getConnectionStatus = () => { + if ( ! gateway?.enabled ) { + return null; + } + if ( isLoading || isQuietLoading ) { + return __( 'Loading…', 'newspack-plugin' ); + } + if ( ! isConnected ) { + return __( 'Not connected', 'newspack-plugin' ); + } + if ( testMode ) { + return __( 'Connected - test mode', 'newspack-plugin' ); + } + return __( 'Connected', 'newspack-plugin' ); + } + const getBadgeLevel = () => { + if ( ! gateway?.enabled || isLoading || isQuietLoading ) { + return 'info'; + } + if ( ! isConnected ) { + return 'error'; + } + return 'success'; + } + + return ( + <ActionCard + isMedium + title={ gateway.name } + description={ () => ( + <> + { sprintf( + // Translators: %s is the payment gateway name. + __( 'Enable %s. ', 'newspack-plugin' ), + gateway.name + ) } + { gateway.url && ( + <ExternalLink href={ gateway.url }> + { __( 'Learn more', 'newspack-plugin' ) } + </ExternalLink> + ) } + </> + ) } + hasWhiteHeader + toggleChecked={ !! gateway.enabled } + toggleOnChange={ () => { + changeHandler( 'enabled', ! gateway.enabled ); + onSave(); + } } + badge={ getConnectionStatus() } + badgeLevel={ getBadgeLevel() } + // eslint-disable-next-line no-nested-ternary + actionContent={ ( ! gateway?.enabled || isLoading || isQuietLoading ) ? null : isConnected ? ( + <Button + variant="secondary" + href={ gateway.settings } + target="_blank" + rel="noreferrer" + > + { __( 'Configure', 'newspack-plugin' ) } + </Button> + ) : ( + <Button + variant="primary" + href={ gateway.connect } + target="_blank" + rel="noreferrer" + > + { __( 'Connect', 'newspack-plugin' ) } + </Button> + ) } + /> + ); +} \ No newline at end of file diff --git a/src/wizards/readerRevenue/views/platform/index.js b/src/wizards/audience/components/platform/index.js similarity index 86% rename from src/wizards/readerRevenue/views/platform/index.js rename to src/wizards/audience/components/platform/index.js index 4f090092a3..5039067552 100644 --- a/src/wizards/readerRevenue/views/platform/index.js +++ b/src/wizards/audience/components/platform/index.js @@ -2,7 +2,6 @@ * WordPress dependencies */ import { useDispatch } from '@wordpress/data'; -import { Fragment } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; /** @@ -10,15 +9,16 @@ import { __ } from '@wordpress/i18n'; */ import { Card, PluginInstaller, SelectControl, Wizard } from '../../../../components/src'; import { NEWSPACK, NRH, OTHER } from '../../constants'; +import WizardsSection from '../../../wizards-section'; /** * Platform Selection Screen Component */ const Platform = () => { - const wizardData = Wizard.useWizardData( 'reader-revenue' ); + const wizardData = Wizard.useWizardData( 'newspack-audience/payment' ); const { saveWizardSettings, updateWizardSettings } = useDispatch( Wizard.STORE_NAMESPACE ); return ( - <Fragment> + <WizardsSection> <Card noBorder> <SelectControl label={ __( 'Select Reader Revenue Platform', 'newspack' ) } @@ -39,7 +39,7 @@ const Platform = () => { ] } onChange={ value => { saveWizardSettings( { - slug: 'newspack-reader-revenue-wizard', + slug: 'newspack-audience/payment', payloadPath: [ 'platform_data' ], updatePayload: { path: [ 'platform_data', 'platform' ], @@ -55,7 +55,7 @@ const Platform = () => { onStatus={ ( { complete } ) => { if ( complete ) { updateWizardSettings( { - slug: 'newspack-reader-revenue-wizard', + slug: 'newspack-audience/payment', path: [ 'plugin_status' ], value: true, } ); @@ -64,7 +64,7 @@ const Platform = () => { withoutFooterButton={ true } /> ) } - </Fragment> + </WizardsSection> ); }; diff --git a/src/wizards/audience/components/prerequisite.tsx b/src/wizards/audience/components/prerequisite.tsx new file mode 100644 index 0000000000..e8546a2dde --- /dev/null +++ b/src/wizards/audience/components/prerequisite.tsx @@ -0,0 +1,246 @@ +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { ExternalLink } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { ActionCard, Button, Grid, TextControl } from '../../../components/src'; +import { HANDOFF_KEY } from '../../../components/src/consts'; + +/** + * Expandable ActionCard for RAS prerequisites checklist. + */ +export default function Prerequisite( { + slug, + config, + getSharedProps, + inFlight, + prerequisite, + saveConfig, + skipPrerequisite, +}: PrequisiteProps ) { + const { href } = prerequisite; + const isSkipped = Boolean( prerequisite.is_skipped ); + const isValid = Boolean( isSkipped || prerequisite.active ); + + // If the prerequisite is active but has empty fields, show a warning. + const hasEmptyFields = () => { + if ( isValid && prerequisite.fields && prerequisite.warning ) { + const emptyValues = Object.keys( prerequisite.fields ).filter( + fieldName => '' === config[ fieldName as keyof Config ] + ); + if ( emptyValues.length ) { + return prerequisite.warning; + } + } + return null; + }; + + const fieldKeys = Object.keys( prerequisite.fields || {} ) as ConfigKey[]; + + const renderInnerContent = () => ( + // Inner card content. + <> + { prerequisite.description && ( + <p> + { prerequisite.description } + { prerequisite.help_url && ( + <> + { ' ' } + <ExternalLink href={ prerequisite.help_url }> + { __( 'Learn more', 'newspack-plugin' ) } + </ExternalLink> + </> + ) } + </p> + ) } + { + ( prerequisite.fields || prerequisite.action_text ) && ( + <Grid columns={ 2 } gutter={ 16 }> + <div className="button-group"> + { + // Form fields. + prerequisite.fields && ( + <> + { fieldKeys.map( fieldName => { + if ( + ! prerequisite.fields || + ! prerequisite.fields[ fieldName ] + ) { + return undefined; + } + return ( + <TextControl + key={ fieldName } + label={ + prerequisite.fields[ fieldName ] + .label + } + help={ + prerequisite.fields[ fieldName ] + .description + } + { ...getSharedProps( + fieldName, + 'text' + ) } + /> + ); + } ) } + + <Button + variant={ 'primary' } + onClick={ () => { + const dataToSave: Partial< Config > = {}; + fieldKeys.forEach( fieldName => { + if ( config[ fieldName ] ) { + // @ts-ignore - not sure what's the issue here. + dataToSave[ fieldName ] = + config[ fieldName ]; + } + } ); + saveConfig( dataToSave ); + } } + disabled={ inFlight } + > + { inFlight + ? __( 'Saving…', 'newspack-plugin' ) + : sprintf( + // Translators: Save or Update settings. + __( + '%s settings', + 'newspack-plugin' + ), + isValid + ? __( + 'Update', + 'newspack-plugin' + ) + : __( + 'Save', + 'newspack-plugin' + ) + ) } + </Button> + </> + ) + } + { + // Link to another settings page or update config in place. + href && prerequisite.action_text && ( + <> + { ( ! prerequisite.hasOwnProperty( + 'action_enabled' + ) || + prerequisite.action_enabled ) && ( + <Button + variant={ 'primary' } + onClick={ () => { + // Set up a handoff to indicate that the user is coming from the RAS wizard page. + if ( prerequisite.instructions ) { + window.localStorage.setItem( + HANDOFF_KEY, + JSON.stringify( { + message: sprintf( + // Translators: %s is specific instructions for satisfying the prerequisite. + __( + '%1$s%2$sReturn to the Audience Configuration page to complete the settings and activate%3$s.', + 'newspack-plugin' + ), + prerequisite.instructions + + ' ', + window + .newspackAudience + ?.reader_activation_url + ? `<a href="${ window.newspackAudience.reader_activation_url }">` + : '', + window + .newspackAudience + ?.reader_activation_url + ? '</a>' + : '' + ), + url: href, + } ) + ); + } + + window.location.href = href; + } } + > + { /* eslint-disable no-nested-ternary */ } + { ( isValid + ? __( 'Update ', 'newspack-plugin' ) + : prerequisite.fields + ? __( 'Save ', 'newspack-plugin' ) + : __( + 'Configure ', + 'newspack-plugin' + ) ) + prerequisite.action_text } + </Button> + ) } + { prerequisite.hasOwnProperty( 'action_enabled' ) && + ! prerequisite.action_enabled && ( + <Button variant={ 'secondary' } disabled> + { prerequisite.disabled_text || + prerequisite.action_text } + </Button> + ) } + </> + ) + } + + { prerequisite.skippable && ! prerequisite.active && ! isSkipped && ( + <Button + variant={ 'secondary' } + isDestructive + onClick={ () => { + skipPrerequisite( { + prerequisite: slug, + skip: true, + } ); + } } + > + { __( 'Skip', 'newspack-plugin' ) } + </Button> + ) } + </div> + </Grid> + ) } + </> + ); + + let status = __( 'Pending', 'newspack-plugin' ); + if ( isValid ) { + status = __( 'Ready', 'newspack-plugin' ); + } + if ( isSkipped && ! prerequisite.active ) { + status = __( 'Skipped', 'newspack-plugin' ); + } + if ( prerequisite.is_unavailable ) { + status = __( 'Unavailable', 'newspack-plugin' ); + } + + return ( + <ActionCard + className="newspack-ras-wizard__prerequisite" + isMedium + expandable={ ! prerequisite.is_unavailable } + collapse={ isValid } + title={ prerequisite.label } + description={ sprintf( + /* translators: %s: Prerequisite status */ + __( 'Status: %s', 'newspack-plugin' ), + status + ) } + checkbox={ isValid && ! isSkipped ? 'checked' : 'unchecked' } + notificationLevel="info" + notification={ hasEmptyFields() } + > + { prerequisite.is_unavailable ? null : renderInnerContent() } + </ActionCard> + ); +} diff --git a/src/wizards/popups/components/prompt-action-card/index.js b/src/wizards/audience/components/prompt-action-card/index.js similarity index 95% rename from src/wizards/popups/components/prompt-action-card/index.js rename to src/wizards/audience/components/prompt-action-card/index.js index 3eb8ef3fc1..2375303c4f 100644 --- a/src/wizards/popups/components/prompt-action-card/index.js +++ b/src/wizards/audience/components/prompt-action-card/index.js @@ -1,3 +1,4 @@ +/* globals newspackAudienceCampaigns */ /** * Prompt Action Card */ @@ -17,7 +18,7 @@ import { moreVertical, settings } from '@wordpress/icons'; import { ActionCard, Button, Card, Modal, Notice, TextControl } from '../../../../components/src'; import PrimaryPromptPopover from '../prompt-popovers/primary'; import PromptSettingsModal from '../settings-modal'; -import { placementForPopup } from '../../utils'; +import { placementForPopup } from '../../views/campaigns/utils'; import './style.scss'; const PromptActionCard = props => { @@ -49,7 +50,7 @@ const PromptActionCard = props => { const promptToDuplicate = parseInt( prompt?.duplicate_of || prompt.id ); try { const defaultTitle = await apiFetch( { - path: `/newspack/v1/wizard/newspack-popups-wizard/${ promptToDuplicate }/${ prompt.id }/duplicate`, + path: `${ newspackAudienceCampaigns.api }/${ promptToDuplicate }/${ prompt.id }/duplicate`, } ); setDuplicateTitle( defaultTitle ); @@ -72,7 +73,7 @@ const PromptActionCard = props => { notificationLevel="error" actionText={ <> - <div className="newspack-popups-wizard__buttons"> + <div className="newspack-audience-campaigns__buttons"> <Button className={ isSettingsModalVisible && 'popover-active' } onClick={ () => setIsSettingsModalVisible( ! isSettingsModalVisible ) } diff --git a/src/wizards/popups/components/prompt-action-card/style.scss b/src/wizards/audience/components/prompt-action-card/style.scss similarity index 97% rename from src/wizards/popups/components/prompt-action-card/style.scss rename to src/wizards/audience/components/prompt-action-card/style.scss index f8729d7a65..7fdbb2d14f 100644 --- a/src/wizards/popups/components/prompt-action-card/style.scss +++ b/src/wizards/audience/components/prompt-action-card/style.scss @@ -4,7 +4,7 @@ @use "~@wordpress/base-styles/colors" as wp-colors; -.newspack-popups-wizard { +.newspack-audience-campaigns { .newspack-action-card { display: flex; flex-direction: column; diff --git a/src/wizards/popups/components/prompt-popovers/primary.js b/src/wizards/audience/components/prompt-popovers/primary.js similarity index 100% rename from src/wizards/popups/components/prompt-popovers/primary.js rename to src/wizards/audience/components/prompt-popovers/primary.js diff --git a/src/wizards/popups/components/prompt-popovers/style.scss b/src/wizards/audience/components/prompt-popovers/style.scss similarity index 100% rename from src/wizards/popups/components/prompt-popovers/style.scss rename to src/wizards/audience/components/prompt-popovers/style.scss diff --git a/src/wizards/audience/components/prompt.tsx b/src/wizards/audience/components/prompt.tsx new file mode 100644 index 0000000000..4db4aad3f8 --- /dev/null +++ b/src/wizards/audience/components/prompt.tsx @@ -0,0 +1,563 @@ +/* eslint-disable no-nested-ternary */ + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { + BaseControl, + CheckboxControl, + ExternalLink, + Path, + SVG, + TextareaControl, +} from '@wordpress/components'; +import apiFetch from '@wordpress/api-fetch'; +import { Fragment, useEffect, useState } from '@wordpress/element'; + +/** + * External dependencies + */ +import { stringify } from 'qs'; + +/** + * Internal dependencies + */ +import { + ActionCard, + Button, + Grid, + ImageUpload, + Notice, + TextControl, + WebPreview, + hooks, +} from '../../../components/src'; + +type Attachment = { + id?: number; + source_url?: string; + url: string; +}; + +// Note: Schema and types for the `prompt` prop is defined in Newspack Campaigns: https://github.com/Automattic/newspack-popups/blob/trunk/includes/schemas/class-prompts.php +export default function Prompt( { + inFlight, + prompt, + setInFlight, + setPrompts, +}: PromptProps ) { + const [ values, setValues ] = useState< + InputValues | Record< string, never > + >( {} ); + const [ error, setError ] = useState< false | { message: string } >( + false + ); + const [ isDirty, setIsDirty ] = useState< boolean >( false ); + const [ success, setSuccess ] = useState< false | string >( false ); + const [ image, setImage ] = useState< null | Attachment >( null ); + const [ isSavingFromPreview, setIsSavingFromPreview ] = useState( false ); + + useEffect( () => { + if ( Array.isArray( prompt?.user_input_fields ) ) { + const fields = { ...values }; + prompt.user_input_fields.forEach( ( field: InputField ) => { + fields[ field.name ] = field.value || field.default; + } ); + setValues( fields ); + } + + if ( prompt.featured_image_id ) { + setInFlight( true ); + apiFetch< Attachment >( { + path: `/wp/v2/media/${ prompt.featured_image_id }`, + } ) + .then( ( attachment: Attachment ) => { + if ( attachment?.source_url || attachment?.url ) { + setImage( { + url: attachment.source_url || attachment.url, + } ); + } + } ) + .catch( setError ) + .finally( () => { + setInFlight( false ); + } ); + } + }, [ prompt ] ); + + // Clear success message after a few seconds. + useEffect( () => { + setTimeout( () => setSuccess( false ), 5000 ); + }, [ success ] ); + + const previewIcon = ( + <SVG + xmlns="http://www.w3.org/2000/svg" + height="24" + viewBox="0 0 24 24" + width="24" + > + <Path + fillRule="evenodd" + clipRule="evenodd" + d="M4.5001 13C5.17092 13.3354 5.17078 13.3357 5.17066 13.3359L5.17346 13.3305C5.1767 13.3242 5.18233 13.3135 5.19036 13.2985C5.20643 13.2686 5.23209 13.2218 5.26744 13.1608C5.33819 13.0385 5.44741 12.8592 5.59589 12.6419C5.89361 12.2062 6.34485 11.624 6.95484 11.0431C8.17357 9.88241 9.99767 8.75 12.5001 8.75C15.0025 8.75 16.8266 9.88241 18.0454 11.0431C18.6554 11.624 19.1066 12.2062 19.4043 12.6419C19.5528 12.8592 19.662 13.0385 19.7328 13.1608C19.7681 13.2218 19.7938 13.2686 19.8098 13.2985C19.8179 13.3135 19.8235 13.3242 19.8267 13.3305L19.8295 13.3359C19.8294 13.3357 19.8293 13.3354 20.5001 13C21.1709 12.6646 21.1708 12.6643 21.1706 12.664L21.1702 12.6632L21.1693 12.6614L21.1667 12.6563L21.1588 12.6408C21.1522 12.6282 21.1431 12.6108 21.1315 12.5892C21.1083 12.5459 21.0749 12.4852 21.0311 12.4096C20.9437 12.2584 20.8146 12.0471 20.6428 11.7956C20.2999 11.2938 19.7823 10.626 19.0798 9.9569C17.6736 8.61759 15.4977 7.25 12.5001 7.25C9.50252 7.25 7.32663 8.61759 5.92036 9.9569C5.21785 10.626 4.70033 11.2938 4.35743 11.7956C4.1856 12.0471 4.05654 12.2584 3.96909 12.4096C3.92533 12.4852 3.89191 12.5459 3.86867 12.5892C3.85705 12.6108 3.84797 12.6282 3.84141 12.6408L3.83346 12.6563L3.8309 12.6614L3.82997 12.6632L3.82959 12.664C3.82943 12.6643 3.82928 12.6646 4.5001 13ZM12.5001 16C14.4331 16 16.0001 14.433 16.0001 12.5C16.0001 10.567 14.4331 9 12.5001 9C10.5671 9 9.0001 10.567 9.0001 12.5C9.0001 14.433 10.5671 16 12.5001 16Z" + fill={ inFlight ? '#828282' : '#3366FF' } + /> + </SVG> + ); + + const getPreviewUrl = ( { + options, + slug, + }: { + options: PromptOptions; + slug: string; + } ) => { + const { placement, trigger_type: triggerType } = options; + const previewQueryKeys = + window.newspackAudience.preview_query_keys; + const abbreviatedKeys = { preset: slug, values }; + const optionsKeys = Object.keys( + options + ) as Array< PromptOptionsBaseKey >; + optionsKeys.forEach( key => { + if ( previewQueryKeys.hasOwnProperty( key ) ) { + // @ts-ignore To be fixed in the future perhaps. + abbreviatedKeys[ previewQueryKeys[ key ] ] = options[ key ]; + } + } ); + + let previewURL = '/'; + if ( + 'archives' === placement && + window.newspackAudience?.preview_archive + ) { + previewURL = window.newspackAudience.preview_archive; + } else if ( + ( 'inline' === placement || 'scroll' === triggerType ) && + window && + window.newspackAudience?.preview_post + ) { + previewURL = window.newspackAudience?.preview_post; + } + + return `${ previewURL }?${ stringify( { ...abbreviatedKeys } ) }`; + }; + + const unblock = hooks.usePrompt( + isDirty, + __( 'You have unsaved changes. Discard changes?', 'newspack-plugin' ) + ); + + const savePrompt = ( slug: string, data: InputValues ) => { + return new Promise< void >( ( res, rej ) => { + if ( unblock ) { + unblock(); + } + setError( false ); + setSuccess( false ); + setInFlight( true ); + apiFetch< [ PromptType ] >( { + path: '/newspack-popups/v1/audience-management/campaign', + method: 'post', + data: { + slug, + data, + }, + } ) + .then( ( fetchedPrompts: Array< PromptType > ) => { + setPrompts( fetchedPrompts ); + setSuccess( __( 'Prompt saved.', 'newspack-plugin' ) ); + setIsDirty( false ); + res(); + } ) + .catch( err => { + setError( err ); + rej( err ); + } ) + .finally( () => { + setInFlight( false ); + } ); + } ); + }; + + const helpInfo = prompt.help_info || null; + + return ( + <ActionCard + isMedium + expandable + collapse={ prompt.ready && ! isSavingFromPreview } + title={ prompt.title } + description={ sprintf( + // Translators: Status of the prompt. + __( 'Status: %s', 'newspack-plugin' ), + isDirty + ? __( 'Unsaved changes', 'newspack-plugin' ) + : prompt.ready + ? __( 'Ready', 'newspack-plugin' ) + : __( 'Pending', 'newspack-plugin' ) + ) } + checkbox={ prompt.ready && ! isDirty ? 'checked' : 'unchecked' } + > + { + <Grid + columns={ 2 } + gutter={ 64 } + className="newspack-ras-campaign__grid" + > + <div className="newspack-ras-campaign__fields"> + { prompt.user_input_fields.map( + ( field: InputField ) => ( + // @ts-ignore TS doesn't like Fragments when used in a map function in this way. + <Fragment key={ field.name }> + { 'array' === field.type && + Array.isArray( field.options ) && ( + <BaseControl + id={ `newspack-engagement-wizard__${ field.name }` } + label={ field.label } + > + { field.options.map( option => ( + <BaseControl + key={ option.id } + id={ `newspack-engagement-wizard__${ option.id }` } + className="newspack-checkbox-control" + help={ + option.description + } + > + <CheckboxControl + disabled={ + inFlight + } + label={ + option.label + } + value={ option.id } + checked={ + values[ + field.name + // @ts-ignore To be fixed in the future perhaps. + ]?.indexOf( + option.id + ) > -1 + } + onChange={ ( + value: boolean + ) => { + const toUpdate = + { + ...values, + }; + if ( + ! value && + toUpdate[ + field + .name + // @ts-ignore To be fixed in the future perhaps. + ].indexOf( + option.id + ) > -1 + ) { + toUpdate[ + field.name + // @ts-ignore To be fixed in the future perhaps. + ].value = + toUpdate[ + field + .name + // @ts-ignore To be fixed in the future perhaps. + ].splice( + toUpdate[ + field + .name + // @ts-ignore To be fixed in the future perhaps. + ].indexOf( + option.id + ), + 1 + ); + } + if ( + value && + toUpdate[ + field + .name + // @ts-ignore To be fixed in the future perhaps. + ].indexOf( + option.id + ) === -1 + ) { + toUpdate[ + field + .name + // @ts-ignore To be fixed in the future perhaps. + ].push( + option.id + ); + } + setValues( + toUpdate + ); + setIsDirty( + true + ); + } } + /> + </BaseControl> + ) ) } + </BaseControl> + ) } + { 'string' === field.type && + field.max_length && + Array.isArray( values ) && + 150 < field.max_length && ( + <TextareaControl + className="newspack-textarea-control" + label={ field.label } + disabled={ inFlight } + help={ `${ + ( + values[ field.name ] as + | string + | undefined + )?.length || 0 + } / ${ field.max_length }` } + onChange={ ( + value: string + ) => { + if ( + value.length > + // @ts-ignore There's a check for max_length above. + field.max_length + ) { + return; + } + + const toUpdate = { + ...values, + }; + toUpdate[ field.name ] = + value; + setValues( toUpdate ); + setIsDirty( true ); + } } + placeholder={ + typeof field.default === + 'string' + ? field.default + : '' + } + rows={ 10 } + // @ts-ignore TS still does not see it as a string. + value={ + typeof values[ + field.name + ] === 'string' + ? values[ field.name ] + : '' + } + /> + ) } + { 'string' === field.type && + field.max_length && + 150 >= field.max_length && ( + <TextControl + label={ field.label } + disabled={ inFlight } + help={ `${ + // @ts-ignore To be fixed in the future perhaps. + values[ field.name ]?.length || 0 + } / ${ field.max_length }` } + onChange={ ( + value: string + ) => { + if ( + value.length > + // @ts-ignore There's a check for max_length above. + field.max_length + ) { + return; + } + + const toUpdate = { + ...values, + }; + toUpdate[ field.name ] = + value; + setValues( toUpdate ); + setIsDirty( true ); + } } + placeholder={ field.default } + value={ + values[ field.name ] || '' + } + /> + ) } + { 'int' === field.type && + 'featured_image_id' === field.name && ( + <BaseControl + id={ `newspack-engagement-wizard__${ field.name }` } + label={ field.label } + > + <ImageUpload + buttonLabel={ __( + 'Select file', + 'newspack-plugin' + ) } + disabled={ inFlight } + image={ image } + onChange={ ( + attachment: Attachment + ) => { + const toUpdate = { + ...values, + }; + toUpdate[ field.name ] = + attachment?.id || 0; + if ( + toUpdate[ + field.name + ] !== + values[ field.name ] + ) { + } + setValues( toUpdate ); + setIsDirty( true ); + if ( attachment?.url ) { + setImage( + attachment + ); + } else { + setImage( null ); + } + } } + /> + </BaseControl> + ) } + </Fragment> + ) + ) } + { error && ( + <Notice + noticeText={ + error?.message || + __( + 'Something went wrong.', + 'newspack-plugin' + ) + } + isError + /> + ) } + { success && ( + <Notice noticeText={ success } isSuccess /> + ) } + <div className="newspack-buttons-card"> + <Button + isPrimary + onClick={ () => { + setIsSavingFromPreview( false ); + savePrompt( prompt.slug, values ); + } } + disabled={ inFlight } + > + { inFlight + ? __( 'Saving…', 'newspack-plugin' ) + : sprintf( + // Translators: Save or Update settings. + __( + '%s prompt settings', + 'newspack-plugin' + ), + prompt.ready + ? __( + 'Update', + 'newspack-plugin' + ) + : __( + 'Save', + 'newspack-plugin' + ) + ) } + </Button> + <WebPreview + url={ getPreviewUrl( prompt ) } + renderButton={ ( { + showPreview, + }: { + showPreview: () => void; + } ) => ( + <Button + disabled={ inFlight } + icon={ previewIcon } + isSecondary + onClick={ async () => showPreview() } + > + { __( + 'Preview prompt', + 'newspack-plugin' + ) } + </Button> + ) } + /> + </div> + </div> + { helpInfo && ( + <div className="newspack-ras-campaign__help"> + { helpInfo.screenshot && ( + <img + src={ helpInfo.screenshot } + alt={ prompt.title } + /> + ) } + { helpInfo.description && ( + <p> + <span + dangerouslySetInnerHTML={ { + __html: helpInfo.description, + } } + />{ ' ' } + { helpInfo.url && ( + <ExternalLink + href={ 'https://none.com' } + > + { __( + 'Learn more', + 'newspack-plugin' + ) } + </ExternalLink> + ) } + </p> + ) } + { helpInfo.recommendations && ( + <> + <h4 className="newspack-ras-campaign__recommendation-heading"> + { __( + 'We recommend', + 'newspack-plugin' + ) } + </h4> + <ul> + { helpInfo.recommendations.map( + ( recommendation, index ) => ( + <li key={ index }> + <span + dangerouslySetInnerHTML={ { + __html: recommendation, + } } + /> + </li> + ) + ) } + </ul> + </> + ) } + </div> + ) } + </Grid> + } + </ActionCard> + ); +} diff --git a/src/wizards/readerRevenue/views/salesforce/index.js b/src/wizards/audience/components/salesforce/index.js similarity index 90% rename from src/wizards/readerRevenue/views/salesforce/index.js rename to src/wizards/audience/components/salesforce/index.js index 9712b61278..111c6ff9d4 100644 --- a/src/wizards/readerRevenue/views/salesforce/index.js +++ b/src/wizards/audience/components/salesforce/index.js @@ -16,20 +16,18 @@ import { addQueryArgs } from '@wordpress/url'; * Internal dependencies. */ import { PluginSettings, Notice, Wizard } from '../../../../components/src'; -import { READER_REVENUE_WIZARD_SLUG } from '../../constants'; const Salesforce = () => { - const { salesforce_redirect_url: redirectUrl } = window?.newspack_reader_revenue || {}; + const { salesforce_redirect_url: redirectUrl } = window?.newspackAudience || {}; const [ hasCopied, setHasCopied ] = useState( false ); - const { salesforce_settings: salesforceData = {} } = Wizard.useWizardData( 'reader-revenue' ); + const salesforceData = Wizard.useWizardData( 'newspack-audience/salesforce' ); const [ isConnected, setIsConnected ] = useState( salesforceData.refresh_token ); const [ error, setError ] = useState( null ); const { saveWizardSettings, wizardApiFetch } = useDispatch( Wizard.STORE_NAMESPACE ); const saveAllSettings = value => saveWizardSettings( { - slug: READER_REVENUE_WIZARD_SLUG, - section: 'salesforce', + slug: 'newspack-audience/salesforce', payloadPath: [ 'salesforce_settings' ], updatePayload: { path: [ 'salesforce_settings' ], @@ -73,7 +71,7 @@ const Salesforce = () => { setError( __( 'We couldn’t establish a connection to Salesforce. Please verify your Consumer Key and Secret and try connecting again.', - 'newspack' + 'newspack-plugin' ) ); } @@ -143,21 +141,21 @@ const Salesforce = () => { } } } pluginSlug="newspack/salesforce" - title={ __( 'Salesforce Settings', 'newspack' ) } + title={ __( 'Salesforce Settings', 'newspack-plugin' ) } description={ () => ( <> { error && <Notice noticeText={ error } isWarning /> } { isConnected && ! error && ( <Notice - noticeText={ __( 'Your site is connected to Salesforce.', 'newspack' ) } + noticeText={ __( 'Your site is connected to Salesforce.', 'newspack-plugin' ) } isSuccess /> ) } { __( 'Establish a connection to sync WooCommerce order data to Salesforce. To connect with Salesforce, create or choose a Connected App for this site in your Salesforce dashboard. Make sure to paste the full URL for this page (', - 'newspack' + 'newspack-plugin' ) } <ClipboardButton @@ -167,17 +165,17 @@ const Salesforce = () => { onFinishCopy={ () => setHasCopied( false ) } > { hasCopied - ? __( 'copied to clipboard!', 'newspack' ) - : __( 'copy to clipboard', 'newspack' ) } + ? __( 'copied to clipboard!', 'newspack-plugin' ) + : __( 'copy to clipboard', 'newspack-plugin' ) } </ClipboardButton> { __( ') into the “Callback URL” field in the Connected App’s settings. ', - 'newspack' + 'newspack-plugin' ) } <ExternalLink href="https://help.salesforce.com/articleView?id=connected_app_create.htm"> - { __( 'Learn how to create a Connected App', 'newspack' ) } + { __( 'Learn how to create a Connected App', 'newspack-plugin' ) } </ExternalLink> </> ) } diff --git a/src/wizards/popups/components/segment-group/SegmentGroup.test.js b/src/wizards/audience/components/segment-group/SegmentGroup.test.js similarity index 99% rename from src/wizards/popups/components/segment-group/SegmentGroup.test.js rename to src/wizards/audience/components/segment-group/SegmentGroup.test.js index 9f31bd06ef..d311e41d50 100644 --- a/src/wizards/popups/components/segment-group/SegmentGroup.test.js +++ b/src/wizards/audience/components/segment-group/SegmentGroup.test.js @@ -299,7 +299,7 @@ const PROMPTS = { describe( 'A segment with conflicting prompts', () => { beforeEach( () => { // Mock global vars for custom placements. - window.newspack_popups_wizard_data = { + window.newspackAudienceCampaigns = { custom_placements: { custom1: 'Custom Placement 1', }, diff --git a/src/wizards/popups/components/segment-group/icons.js b/src/wizards/audience/components/segment-group/icons.js similarity index 100% rename from src/wizards/popups/components/segment-group/icons.js rename to src/wizards/audience/components/segment-group/icons.js diff --git a/src/wizards/popups/components/segment-group/index.js b/src/wizards/audience/components/segment-group/index.js similarity index 97% rename from src/wizards/popups/components/segment-group/index.js rename to src/wizards/audience/components/segment-group/index.js index b0151685d6..91137908bc 100644 --- a/src/wizards/popups/components/segment-group/index.js +++ b/src/wizards/audience/components/segment-group/index.js @@ -20,7 +20,7 @@ import { segmentDescription, getCardClassName, warningForPopup, -} from '../../utils'; +} from '../../views/campaigns/utils'; import { iconInline, iconOverlayBottom, @@ -189,7 +189,7 @@ const SegmentGroup = props => { /> ) ) } </Card> - { prompts.length < 1 ? <p>{ emptySegmentText }</p> : '' } + { prompts.length < 1 ? <p className="newspack-campaigns__segment-group__empty-segment-text">{ emptySegmentText }</p> : '' } </Card> ); }; diff --git a/src/wizards/popups/components/segment-group/style.scss b/src/wizards/audience/components/segment-group/style.scss similarity index 89% rename from src/wizards/popups/components/segment-group/style.scss rename to src/wizards/audience/components/segment-group/style.scss index 7095005076..4d3cc6da20 100644 --- a/src/wizards/popups/components/segment-group/style.scss +++ b/src/wizards/audience/components/segment-group/style.scss @@ -22,10 +22,7 @@ &__card { border-color: wp-colors.$gray-300; overflow: hidden; - - & > &__segment { - margin: -16px -16px 0 !important; - } + padding: 0; &__segment { align-items: center; @@ -63,23 +60,23 @@ } &__action-cards { - margin: 0 -16px; + margin: 0; .newspack-card.newspack-action-card { border-bottom: 0; border-left: 0; border-radius: 0; border-right: 0; - margin-bottom: 16px; + margin-bottom: 0; + margin-block: 0 !important; &:first-child { border-top: 0; margin-top: 0; } - - &:last-child { - margin-bottom: -16px; - } } } + &__empty-segment-text { + padding: 0 16px 16px; + } } diff --git a/src/wizards/popups/components/segmentation-preview/index.js b/src/wizards/audience/components/segmentation-preview/index.js similarity index 93% rename from src/wizards/popups/components/segmentation-preview/index.js rename to src/wizards/audience/components/segmentation-preview/index.js index 4f77c7ecb9..9449fea3a7 100644 --- a/src/wizards/popups/components/segmentation-preview/index.js +++ b/src/wizards/audience/components/segmentation-preview/index.js @@ -18,8 +18,8 @@ const SegmentationPreview = props => { const [ decoratedUrl, setDecoratedUrl ] = useState( null ); const [ isOpen, setIsOpen ] = useState( false ); const [ sessionId, setSessionId ] = useState( Math.floor( Math.random() * 9999 ) ); // A random ID that can be used to tie together all pageviews in a single preview session. - const postPreviewLink = window?.newspack_popups_wizard_data?.preview_post; - const frontendUrl = window?.newspack_popups_wizard_data?.frontend_url || '/'; + const postPreviewLink = window?.newspackAudienceCampaigns?.preview_post; + const frontendUrl = window?.newspackAudienceCampaigns?.frontend_url || '/'; const { campaign = false, diff --git a/src/wizards/popups/components/settings-modal/index.js b/src/wizards/audience/components/settings-modal/index.js similarity index 99% rename from src/wizards/popups/components/settings-modal/index.js rename to src/wizards/audience/components/settings-modal/index.js index 472f3b67da..b66a52c150 100644 --- a/src/wizards/popups/components/settings-modal/index.js +++ b/src/wizards/audience/components/settings-modal/index.js @@ -22,7 +22,7 @@ import { isOverlay, placementsForPopups, overlaySizesForPopups, -} from '../../utils'; +} from '../../views/campaigns/utils'; const { SettingsCard } = Settings; diff --git a/src/wizards/readerRevenue/constants.js b/src/wizards/audience/constants.js similarity index 61% rename from src/wizards/readerRevenue/constants.js rename to src/wizards/audience/constants.js index 3407971a56..46d5c9d56c 100644 --- a/src/wizards/readerRevenue/constants.js +++ b/src/wizards/audience/constants.js @@ -6,4 +6,4 @@ export const NRH = 'nrh'; export const NEWSPACK = 'wc'; export const OTHER = 'other'; -export const READER_REVENUE_WIZARD_SLUG = 'newspack-reader-revenue-wizard'; +export const AUDIENCE_DONATIONS_WIZARD_SLUG = 'newspack-audience-donations'; diff --git a/src/wizards/popups/contexts/Campaigns.js b/src/wizards/audience/contexts/Campaigns.js similarity index 100% rename from src/wizards/popups/contexts/Campaigns.js rename to src/wizards/audience/contexts/Campaigns.js diff --git a/src/wizards/popups/contexts/index.js b/src/wizards/audience/contexts/index.js similarity index 100% rename from src/wizards/popups/contexts/index.js rename to src/wizards/audience/contexts/index.js diff --git a/src/wizards/engagement/components/types.ts b/src/wizards/audience/types/index.d.ts similarity index 79% rename from src/wizards/engagement/components/types.ts rename to src/wizards/audience/types/index.d.ts index dc577e72e3..43c3db8d40 100644 --- a/src/wizards/engagement/components/types.ts +++ b/src/wizards/audience/types/index.d.ts @@ -2,7 +2,7 @@ * Types for the Prequisite component. */ -export type PromptOptionsBase = { +type PromptOptionsBase = { background_color: string; display_title: boolean; hide_border: boolean; @@ -26,27 +26,7 @@ export type PromptOptionsBase = { utm_suppression: string; }; -export type PromptOptionsBaseKey = keyof PromptOptionsBase; - -declare global { - interface Window { - // Localized data on engagement wizard script. - newspack_engagement_wizard: { - has_reader_activation: boolean; - has_memberships: boolean; - new_subscription_lists_url: string; - reader_activation_url: string; - preview_query_keys: { - [ K in PromptOptionsBaseKey ]: string; - }; - preview_post: string; - preview_archive: string; - }; - newspack_reader_revenue: { - can_use_name_your_price: boolean; - }; - } -} +type PromptOptionsBaseKey = keyof PromptOptionsBase; // Available transactional email slugs. type EmailSlugs = @@ -57,7 +37,7 @@ type EmailSlugs = | 'reader-activation-delete-account'; // RAS config inherited from RAS wizard view. -export type Config = { +type Config = { enabled?: boolean; enabled_account_link?: boolean; account_link_menu_locations?: [ 'tertiary-menu' ]; @@ -88,11 +68,12 @@ export type Config = { contact_email_address?: string; }; -export type ConfigKey = keyof Config; +type ConfigKey = keyof Config; // Props for the Prequisite component. -export type PrequisiteProps = { +type PrequisiteProps = { config: Config; + slug: string; getSharedProps: ( configKey: string, type: string @@ -104,6 +85,12 @@ export type PrequisiteProps = { }; inFlight: boolean; saveConfig: ( config: Config ) => void; + skipPrerequisite: ( + data: { + prerequisite: string; + skip: boolean; + } + ) => void; // Schema for prequisite object is defined in PHP class Reader_Activation::get_prerequisites_status(). prerequisite: { @@ -128,10 +115,11 @@ export type PrequisiteProps = { disabled_text?: string; is_unavailable?: boolean; is_skipped?: boolean; + skippable?: boolean; }; }; -export type InputField = { +type InputField = { name: string; type: string; label: string; @@ -147,7 +135,7 @@ export type InputField = { }; // Schema is defined in Newspack Campaigns: https://github.com/Automattic/newspack-popups/blob/trunk/includes/schemas/class-prompts.php -export type PromptType = { +type PromptType = { status: string; slug: string; title: string; @@ -157,7 +145,7 @@ export type PromptType = { { id: number; name: string; - } + }, ]; options: PromptOptions; user_input_fields: [ InputField ]; @@ -170,7 +158,7 @@ export type PromptType = { ready?: boolean; }; -export type PromptOptions = PromptOptionsBase & { +type PromptOptions = PromptOptionsBase & { post_types: Array< string >; archive_page_types: Array< string >; additional_classes: string; @@ -178,46 +166,45 @@ export type PromptOptions = PromptOptionsBase & { { id: number; name: string; - } + }, ]; excluded_tags: [ { id: number; name: string; - } + }, ]; categories: [ { id: number; name: string; - } + }, ]; tags: [ { id: number; name: string; - } + }, ]; campaign_groups: [ { id: number; name: string; - } + }, ]; }; -export type Attachment = { - id?: number; - source_url?: string; - url: string; -}; - -export type InputValues = { - [ fieldName: string ]: string | number | Array< string > | Array< number > | boolean; +type InputValues = { + [ fieldName: string ]: + | string + | number + | Array< string > + | Array< number > + | boolean; }; // Props for the Prompt component. -export type PromptProps = { +type PromptProps = { inFlight: boolean; setInFlight: ( inFlight: boolean ) => void; prompt: PromptType; diff --git a/src/wizards/audience/views/campaigns/analytics/index.js b/src/wizards/audience/views/campaigns/analytics/index.js new file mode 100644 index 0000000000..068ca9fe57 --- /dev/null +++ b/src/wizards/audience/views/campaigns/analytics/index.js @@ -0,0 +1,45 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies. + */ +import { Button, Card, withWizardScreen } from '../../../../../components/src'; +import './style.scss'; + +/** + * Popups Analytics screen. + */ +const PopupAnalytics = () => ( + <div className="newspack-campaigns-wizard-analytics__wrapper"> + <Card isNarrow> + <h2>{ __( 'Coming soon', 'newspack-plugin' ) }</h2> + <p> + <> + { __( + 'We’re currently redesigning this dashboard to accommodate GA4 and give you deeper insights into Campaign performance. In the meantime, you can find Campaign event data within your GA account. Review this ', + 'newspack-plugin' + ) } + <a target="_blank" rel="noopener noreferrer" href="https://help.newspack.com/analytics/"> + { __( 'help page', 'newspack-plugin' ) } + </a> + { __( ' to see how Campaign data is being recorded in GA.', 'newspack-plugin' ) }, + </> + </p> + <Card buttonsCard noBorder> + <Button + target="_blank" + rel="noopener noreferrer" + href="https://help.newspack.com/analytics/" + isPrimary + > + { __( 'View the help page', 'newspack-plugin' ) } + </Button> + </Card> + </Card> + </div> +); + +export default withWizardScreen( PopupAnalytics ); diff --git a/src/wizards/audience/views/campaigns/analytics/style.scss b/src/wizards/audience/views/campaigns/analytics/style.scss new file mode 100644 index 0000000000..2b2bb09b31 --- /dev/null +++ b/src/wizards/audience/views/campaigns/analytics/style.scss @@ -0,0 +1,87 @@ +/** + * Popups Analytics + */ + +@use "~@wordpress/base-styles/colors" as wp-colors; +@use "../../../../../shared/scss/colors"; + +.newspack-campaigns-wizard-analytics { + &__wrapper { + transition: opacity 125ms; + &--loading { + opacity: 0.5; + } + } + &__filters { + margin: 32px 0; + &, + &__group, + &__date { + display: flex; + justify-content: space-between; + } + &__date { + .newspack-button.has-icon.has-text { + font-size: 17px !important; + padding-left: 8px !important; + padding-right: 0 !important; + &:focus, + &:hover { + background: white !important; + border-color: wp-colors.$gray-300 !important; + color: wp-colors.$gray-900 !important; + } + &:focus { + border-color: var(--newspack-ui-color-primary) !important; + box-shadow: inset 0 0 0 2px var(--newspack-ui-color-primary) !important; + } + svg { + order: 2; + margin-left: 3px; + margin-right: 3px; + } + } + } + .newspack-select-control { + margin: 0 8px 0 0; + } + } + &__info { + &__sections { + display: flex; + margin: 32px 0; + &__section { + display: flex; + flex: 1; + justify-content: center; + align-items: center; + flex-direction: column; + transition: opacity 125ms; + h2 { + margin: 0; + font-size: 36px; + line-height: 48px; + } + &--with-separator { + position: relative; + &::after { + content: ""; + position: absolute; + height: 100%; + width: 1px; + background-color: wp-colors.$gray-300; + right: 0; + top: 0; + } + } + &--dimmed { + color: wp-colors.$gray-600; + h2 { + color: inherit; + font-weight: normal; + } + } + } + } + } +} diff --git a/src/wizards/popups/views/campaigns/index.js b/src/wizards/audience/views/campaigns/campaigns/index.js similarity index 97% rename from src/wizards/popups/views/campaigns/index.js rename to src/wizards/audience/views/campaigns/campaigns/index.js index 0625fd096f..5288ae904c 100644 --- a/src/wizards/popups/views/campaigns/index.js +++ b/src/wizards/audience/views/campaigns/campaigns/index.js @@ -21,11 +21,11 @@ import { Router, TextControl, withWizardScreen, -} from '../../../../components/src'; -import CampaignManagementPopover from '../../components/campaign-management-popover'; -import SegmentGroup from '../../components/segment-group'; -import { dataForCampaignId } from '../../utils'; -import { CampaignsContext } from '../../contexts'; +} from '../../../../../components/src'; +import CampaignManagementPopover from '../../../components/campaign-management-popover'; +import SegmentGroup from '../../../components/segment-group'; +import { dataForCampaignId } from '../utils'; +import { CampaignsContext } from '../../../contexts'; import './style.scss'; /** diff --git a/src/wizards/popups/views/campaigns/style.scss b/src/wizards/audience/views/campaigns/campaigns/style.scss similarity index 85% rename from src/wizards/popups/views/campaigns/style.scss rename to src/wizards/audience/views/campaigns/campaigns/style.scss index 96f7b6f909..9dfcd325ba 100644 --- a/src/wizards/popups/views/campaigns/style.scss +++ b/src/wizards/audience/views/campaigns/campaigns/style.scss @@ -3,7 +3,7 @@ */ @use "~@wordpress/base-styles/colors" as wp-colors; -@use "../../../../shared/scss/colors"; +@use "../../../../../shared/scss/colors"; .newspack-campaigns__campaign-group { &__card { @@ -53,12 +53,12 @@ } .newspack-button.is-link { - color: colors.$primary-500; + color: var(--newspack-ui-color-primary); padding: 0; text-decoration: underline; &:hover { - color: colors.$primary-600; + color: var(--newspack-ui-color-primary); } } } @@ -78,11 +78,11 @@ &:active, &:focus, &:hover { - color: colors.$primary-500; + color: var(--newspack-ui-color-primary); svg { background: wp-colors.$gray-100; - border-color: colors.$primary-500; + border-color: var(--newspack-ui-color-primary); } } @@ -90,7 +90,7 @@ border: 1px solid wp-colors.$gray-300; border-radius: 2px; display: block; - fill: colors.$primary-500; + fill: var(--newspack-ui-color-primary); margin: 0 0 8px; padding: 23px; transition: background-color 125ms ease-in-out, border-color 125ms ease-in-out; diff --git a/src/wizards/popups/index.js b/src/wizards/audience/views/campaigns/index.js similarity index 81% rename from src/wizards/popups/index.js rename to src/wizards/audience/views/campaigns/index.js index a15a54b902..41feabdf83 100644 --- a/src/wizards/popups/index.js +++ b/src/wizards/audience/views/campaigns/index.js @@ -1,13 +1,14 @@ -import '../../shared/js/public-path'; +/* globals newspackAudienceCampaigns */ +import '../../../../shared/js/public-path'; /** - * Pop-ups Wizard + * Campaigns Wizard */ /** * WordPress dependencies. */ -import { Component, render, createElement } from '@wordpress/element'; +import { Component } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { addQueryArgs } from '@wordpress/url'; @@ -19,15 +20,14 @@ import { stringify } from 'qs'; /** * Internal dependencies. */ -import { WebPreview, withWizard } from '../../components/src'; -import Router from '../../components/src/proxied-imports/router'; +import { WebPreview, withWizard } from '../../../../components/src'; +import Router from '../../../../components/src/proxied-imports/router'; import { Campaigns, Settings, Segments } from './views'; -import { CampaignsContext } from './contexts'; +import { CampaignsContext } from '../../contexts'; const { HashRouter, Redirect, Route, Switch } = Router; -const headerText = __( 'Campaigns', 'newspack-plugin' ); -const subHeaderText = __( 'Reach your readers with configurable campaigns', 'newspack-plugin' ); +const headerText = __( 'Audience Management / Campaigns', 'newspack-plugin' ); const tabbedNavigation = [ { @@ -38,7 +38,7 @@ const tabbedNavigation = [ { label: __( 'Segments', 'newpack-plugin' ), path: '/segments', - exact: true, + exact: false, }, { label: __( 'Settings', 'newpack-plugin' ), @@ -47,7 +47,7 @@ const tabbedNavigation = [ }, ]; -class PopupsWizard extends Component { +class AudienceCampaigns extends Component { constructor( props ) { super( props ); this.state = { @@ -67,7 +67,7 @@ class PopupsWizard extends Component { refetch = () => { const { setError, wizardApiFetch } = this.props; wizardApiFetch( { - path: '/newspack/v1/wizard/newspack-popups-wizard/', + path: newspackAudienceCampaigns.api, } ) .then( this.updateAfterAPI ) .catch( error => setError( error ) ); @@ -77,7 +77,7 @@ class PopupsWizard extends Component { const { setError, wizardApiFetch } = this.props; this.setState( { inFlight: true } ); return wizardApiFetch( { - path: `/newspack/v1/wizard/newspack-popups-wizard/${ id }`, + path: `${ newspackAudienceCampaigns.api }/${ id }`, method: 'POST', data: { config: promptConfig }, quiet: true, @@ -94,7 +94,7 @@ class PopupsWizard extends Component { deletePopup = popupId => { const { setError, wizardApiFetch } = this.props; return wizardApiFetch( { - path: `/newspack/v1/wizard/newspack-popups-wizard/${ popupId }`, + path: `${ newspackAudienceCampaigns.api }/${ popupId }`, method: 'DELETE', quiet: true, } ) @@ -110,7 +110,7 @@ class PopupsWizard extends Component { restorePopup = popupId => { const { setError, wizardApiFetch } = this.props; return wizardApiFetch( { - path: `/newspack/v1/wizard/newspack-popups-wizard/${ popupId }/restore`, + path: `${ newspackAudienceCampaigns.api }/${ popupId }/restore`, method: 'POST', quiet: true, } ) @@ -126,7 +126,7 @@ class PopupsWizard extends Component { publishPopup = popupId => { const { setError, wizardApiFetch } = this.props; return wizardApiFetch( { - path: `/newspack/v1/wizard/newspack-popups-wizard/${ popupId }/publish`, + path: `${ newspackAudienceCampaigns.api }/${ popupId }/publish`, method: 'POST', quiet: true, } ) @@ -142,7 +142,7 @@ class PopupsWizard extends Component { unpublishPopup = popupId => { const { setError, wizardApiFetch } = this.props; return wizardApiFetch( { - path: `/newspack/v1/wizard/newspack-popups-wizard/${ popupId }/publish`, + path: `${ newspackAudienceCampaigns.api }/${ popupId }/publish`, method: 'DELETE', quiet: true, } ) @@ -160,7 +160,7 @@ class PopupsWizard extends Component { const { setError, wizardApiFetch } = this.props; this.setState( { inFlight: true } ); return wizardApiFetch( { - path: addQueryArgs( `/newspack/v1/wizard/newspack-popups-wizard/${ popupId }/duplicate`, { + path: addQueryArgs( `${ newspackAudienceCampaigns.api }/${ popupId }/duplicate`, { title, } ), method: 'POST', @@ -177,7 +177,7 @@ class PopupsWizard extends Component { previewUrlForPopup = ( { options, id } ) => { const { placement, trigger_type: triggerType } = options; - const previewQueryKeys = window.newspack_popups_wizard_data?.preview_query_keys || {}; + const previewQueryKeys = window.newspackAudienceCampaigns?.preview_query_keys || {}; const abbreviatedKeys = {}; Object.keys( options ).forEach( key => { if ( previewQueryKeys.hasOwnProperty( key ) ) { @@ -186,14 +186,14 @@ class PopupsWizard extends Component { } ); let previewURL = '/'; - if ( 'archives' === placement && window.newspack_popups_wizard_data?.preview_archive ) { - previewURL = window.newspack_popups_wizard_data.preview_archive; + if ( 'archives' === placement && window.newspackAudienceCampaigns?.preview_archive ) { + previewURL = window.newspackAudienceCampaigns.preview_archive; } else if ( ( 'inline' === placement || 'scroll' === triggerType ) && window && - window.newspack_popups_wizard_data?.preview_post + window.newspackAudienceCampaigns?.preview_post ) { - previewURL = window.newspack_popups_wizard_data?.preview_post; + previewURL = window.newspackAudienceCampaigns?.preview_post; } return `${ previewURL }?${ stringify( { ...abbreviatedKeys, pid: id } ) }`; @@ -205,7 +205,7 @@ class PopupsWizard extends Component { manageCampaignGroup = ( campaigns, method = 'POST' ) => { const { setError, wizardApiFetch } = this.props; return wizardApiFetch( { - path: '/newspack/v1/wizard/newspack-popups-wizard/batch-publish/', + path: `${ newspackAudienceCampaigns.api }/batch-publish/`, data: { ids: campaigns.map( campaign => campaign.id ) }, method, quiet: true, @@ -224,7 +224,6 @@ class PopupsWizard extends Component { renderButton={ ( { showPreview } ) => { const sharedProps = { headerText, - subHeaderText, tabbedNavigation, setError, isLoading, @@ -264,7 +263,7 @@ class PopupsWizard extends Component { const archiveCampaignGroup = ( id, status ) => { return wizardApiFetch( { - path: `/newspack/v1/wizard/newspack-popups-wizard/archive-campaign/${ id }`, + path: `${ newspackAudienceCampaigns.api }/archive-campaign/${ id }`, method: status ? 'POST' : 'DELETE', quiet: true, } ) @@ -273,7 +272,7 @@ class PopupsWizard extends Component { }; const createCampaignGroup = name => { return wizardApiFetch( { - path: `/newspack/v1/wizard/newspack-popups-wizard/create-campaign/`, + path: `${ newspackAudienceCampaigns.api }/create-campaign/`, method: 'POST', data: { name }, quiet: true, @@ -291,7 +290,7 @@ class PopupsWizard extends Component { }; const deleteCampaignGroup = id => { return wizardApiFetch( { - path: `/newspack/v1/wizard/newspack-popups-wizard/delete-campaign/${ id }`, + path: `${ newspackAudienceCampaigns.api }/delete-campaign/${ id }`, method: 'DELETE', quiet: true, } ) @@ -308,7 +307,7 @@ class PopupsWizard extends Component { }; const duplicateCampaignGroup = ( id, name ) => { return wizardApiFetch( { - path: `/newspack/v1/wizard/newspack-popups-wizard/duplicate-campaign/${ id }`, + path: `${ newspackAudienceCampaigns.api }/duplicate-campaign/${ id }`, method: 'POST', data: { name }, quiet: true, @@ -326,7 +325,7 @@ class PopupsWizard extends Component { }; const renameCampaignGroup = ( id, name ) => { return wizardApiFetch( { - path: `/newspack/v1/wizard/newspack-popups-wizard/rename-campaign/${ id }`, + path: `${ newspackAudienceCampaigns.api }/rename-campaign/${ id }`, method: 'POST', data: { name }, quiet: true, @@ -372,7 +371,4 @@ class PopupsWizard extends Component { } } -render( - createElement( withWizard( PopupsWizard, [ 'newspack-popups' ] ) ), - document.getElementById( 'newspack-popups-wizard' ) -); +export default withWizard( AudienceCampaigns ); diff --git a/src/wizards/popups/views/segments/index.js b/src/wizards/audience/views/campaigns/segments/index.js similarity index 89% rename from src/wizards/popups/views/segments/index.js rename to src/wizards/audience/views/campaigns/segments/index.js index 2c58e93710..d3d7dc9ed6 100644 --- a/src/wizards/popups/views/segments/index.js +++ b/src/wizards/audience/views/campaigns/segments/index.js @@ -1,7 +1,7 @@ /** * Internal dependencies. */ -import { withWizardScreen } from '../../../../components/src'; +import { withWizardScreen } from '../../../../../components/src'; import SegmentsList from './segments-list'; import SingleSegment from './single-segment'; import './style.scss'; diff --git a/src/wizards/popups/views/segments/segments-list.js b/src/wizards/audience/views/campaigns/segments/segments-list.js similarity index 96% rename from src/wizards/popups/views/segments/segments-list.js rename to src/wizards/audience/views/campaigns/segments/segments-list.js index 9a083424df..4d297a5d89 100644 --- a/src/wizards/popups/views/segments/segments-list.js +++ b/src/wizards/audience/views/campaigns/segments/segments-list.js @@ -1,3 +1,4 @@ +/* globals newspackAudienceCampaigns */ /** * WordPress dependencies. */ @@ -10,8 +11,8 @@ import { Icon, chevronDown, chevronUp, dragHandle, moreVertical } from '@wordpre /** * Internal dependencies. */ -import { ActionCard, Button, Card, Notice, Popover, Router } from '../../../../components/src'; -import { segmentDescription } from '../../utils'; +import { ActionCard, Button, Card, Notice, Popover, Router } from '../../../../../components/src'; +import { segmentDescription } from '../utils'; const { NavLink, useHistory } = Router; @@ -252,7 +253,7 @@ const SegmentsList = ( { wizardApiFetch, segments, setSegments, isLoading } ) => setInFlight( true ); setError( null ); wizardApiFetch( { - path: `/newspack/v1/wizard/newspack-popups-wizard/segmentation/${ segment.id }`, + path: `${ newspackAudienceCampaigns.api }/segmentation/${ segment.id }`, method: 'POST', quiet: true, data: { @@ -276,7 +277,7 @@ const SegmentsList = ( { wizardApiFetch, segments, setSegments, isLoading } ) => setInFlight( true ); setError( null ); wizardApiFetch( { - path: `/newspack/v1/wizard/newspack-popups-wizard/segmentation/${ segment.id }`, + path: `${ newspackAudienceCampaigns.api }/segmentation/${ segment.id }`, method: 'DELETE', quiet: true, } ) @@ -293,7 +294,7 @@ const SegmentsList = ( { wizardApiFetch, segments, setSegments, isLoading } ) => setSortedSegments( segmentsToSort ); setInFlight( true ); wizardApiFetch( { - path: `/newspack/v1/wizard/newspack-popups-wizard/segmentation-sort`, + path: `${ newspackAudienceCampaigns.api }/segmentation-sort`, method: 'POST', data: { segmentIds: segmentsToSort.map( _segment => _segment.id ) }, quiet: true, diff --git a/src/wizards/popups/views/segments/single-segment.js b/src/wizards/audience/views/campaigns/segments/single-segment.js similarity index 95% rename from src/wizards/popups/views/segments/single-segment.js rename to src/wizards/audience/views/campaigns/segments/single-segment.js index 14217ab06a..31f0e4d32e 100644 --- a/src/wizards/popups/views/segments/single-segment.js +++ b/src/wizards/audience/views/campaigns/segments/single-segment.js @@ -1,3 +1,4 @@ +/* globals newspackAudienceCampaigns */ /** * WordPress dependencies. */ @@ -18,8 +19,8 @@ import { Settings, TextControl, hooks, -} from '../../../../components/src'; -import ListsControl from '../../components/lists-control'; +} from '../../../../../components/src'; +import ListsControl from '../../../components/lists-control'; const { useHistory } = Router; const { SettingsCard, SettingsSection, MinMaxSetting } = Settings; @@ -29,7 +30,7 @@ const DEFAULT_CONFIG = { }; const SingleSegment = ( { segmentId, setSegments, wizardApiFetch } ) => { - const allCriteria = window.newspack_popups_wizard_data?.criteria || []; + const allCriteria = window.newspackAudienceCampaigns?.criteria || []; const [ segmentConfig, updateSegmentConfig ] = hooks.useObjectState( DEFAULT_CONFIG ); const [ name, setName ] = useState( '' ); @@ -62,7 +63,7 @@ const SingleSegment = ( { segmentId, setSegments, wizardApiFetch } ) => { useEffect( () => { if ( ! isNew ) { wizardApiFetch( { - path: `/newspack/v1/wizard/newspack-popups-wizard/segmentation`, + path: `${ newspackAudienceCampaigns.api }/segmentation`, } ).then( segments => { const foundSegment = find( segments, ( { id } ) => id === segmentId ); if ( foundSegment ) { @@ -85,8 +86,8 @@ const SingleSegment = ( { segmentId, setSegments, wizardApiFetch } ) => { unblock(); const path = isNew - ? `/newspack/v1/wizard/newspack-popups-wizard/segmentation` - : `/newspack/v1/wizard/newspack-popups-wizard/segmentation/${ segmentId }`; + ? `${ newspackAudienceCampaigns.api }/segmentation` + : `${ newspackAudienceCampaigns.api }/segmentation/${ segmentId }`; wizardApiFetch( { path, method: 'POST', @@ -364,7 +365,7 @@ addFilter( return ( <ListsControl placeholder={ __( 'Start typing to search for products…', 'newspack-plugin' ) } - path="/newspack/v1/wizard/newspack-popups-wizard/subscription-products" + path={ `${ newspackAudienceCampaigns.api }/subscription-products` } value={ value } onChange={ update } /> diff --git a/src/wizards/popups/views/segments/single-segment.test.js b/src/wizards/audience/views/campaigns/segments/single-segment.test.js similarity index 98% rename from src/wizards/popups/views/segments/single-segment.test.js rename to src/wizards/audience/views/campaigns/segments/single-segment.test.js index b5a7d0dea3..d7b2344fcf 100644 --- a/src/wizards/popups/views/segments/single-segment.test.js +++ b/src/wizards/audience/views/campaigns/segments/single-segment.test.js @@ -67,7 +67,7 @@ describe( 'A new segment creation', () => { }; beforeEach( () => { - window.newspack_popups_wizard_data = { criteria }; + window.newspackAudienceConfiguration = { criteria }; render( <MemoryRouter> <SingleSegment { ...mockProps } /> diff --git a/src/wizards/popups/views/segments/style.scss b/src/wizards/audience/views/campaigns/segments/style.scss similarity index 94% rename from src/wizards/popups/views/segments/style.scss rename to src/wizards/audience/views/campaigns/segments/style.scss index dd529b3a87..d31f0dccc2 100644 --- a/src/wizards/popups/views/segments/style.scss +++ b/src/wizards/audience/views/campaigns/segments/style.scss @@ -1,5 +1,5 @@ -@use "../../../../shared/scss/colors"; @use "~@wordpress/base-styles/colors" as wp-colors; +@use "../../../../../shared/scss/colors"; .newspack-campaigns-wizard-segments { &__list { @@ -40,7 +40,7 @@ &.is-drop-target::before, &.drop-target-after::after { - background-color: colors.$primary-500; + background-color: var(--newspack-ui-color-primary); content: ""; display: block; height: 2px; diff --git a/src/wizards/audience/views/campaigns/settings/index.js b/src/wizards/audience/views/campaigns/settings/index.js new file mode 100644 index 0000000000..529512366e --- /dev/null +++ b/src/wizards/audience/views/campaigns/settings/index.js @@ -0,0 +1,10 @@ +/** + * Internal dependencies + */ +import { withWizardScreen, PluginSettings } from '../../../../../components/src'; + +const Settings = () => { + return <PluginSettings pluginSlug="newspack-audience-campaigns" isWizard={ true } title={ null } />; +}; + +export default withWizardScreen( Settings ); diff --git a/src/wizards/popups/utils.js b/src/wizards/audience/views/campaigns/utils.js similarity index 95% rename from src/wizards/popups/utils.js rename to src/wizards/audience/views/campaigns/utils.js index 47f036ce13..f4e8de4867 100644 --- a/src/wizards/popups/utils.js +++ b/src/wizards/audience/views/campaigns/utils.js @@ -1,3 +1,4 @@ +/* globals newspackAudienceCampaigns */ /** * WordPress dependencies. */ @@ -13,7 +14,7 @@ import { useEffect, useState, Fragment } from '@wordpress/element'; import memoize from 'lodash/memoize'; import compact from 'lodash/compact'; -const allCriteria = window.newspack_popups_wizard_data?.criteria || []; +const allCriteria = window.newspackAudienceCampaigns?.criteria || []; /** * Check whether the given popup is an overlay. @@ -22,7 +23,7 @@ const allCriteria = window.newspack_popups_wizard_data?.criteria || []; * @return {boolean} True if the popup is an overlay, otherwise false. */ export const isOverlay = popup => { - const overlayPlacements = window.newspack_popups_wizard_data?.overlay_placements || []; + const overlayPlacements = window.newspackAudienceCampaigns?.overlay_placements || []; return -1 < overlayPlacements.indexOf( popup.options.placement ); }; @@ -35,7 +36,7 @@ export const isOverlay = popup => { export const isAboveHeader = popup => 'above_header' === popup.options.placement; export const isCustomPlacement = popup => { - const customPlacements = window.newspack_popups_wizard_data?.custom_placements || {}; + const customPlacements = window.newspackAudienceCampaigns?.custom_placements || {}; return -1 < Object.keys( customPlacements ).indexOf( popup.options.placement ); }; @@ -65,7 +66,7 @@ const placementMap = { }; export const placementForPopup = ( { options: { frequency, placement } } ) => { - const customPlacements = window.newspack_popups_wizard_data?.custom_placements || {}; + const customPlacements = window.newspackAudienceCampaigns?.custom_placements || {}; if ( 'manual' === frequency || customPlacements.hasOwnProperty( placement ) ) { return __( 'Custom Placement', 'newspack-plugin' ); } @@ -73,8 +74,8 @@ export const placementForPopup = ( { options: { frequency, placement } } ) => { }; export const placementsForPopups = prompt => { - const customPlacements = window.newspack_popups_wizard_data?.custom_placements; - const overlayPlacements = window.newspack_popups_wizard_data?.overlay_placements; + const customPlacements = window.newspackAudienceCampaigns?.custom_placements; + const overlayPlacements = window.newspackAudienceCampaigns?.overlay_placements; const options = Object.keys( placementMap ) .filter( key => isOverlay( prompt ) @@ -108,7 +109,7 @@ export const frequenciesForPopup = () => { }; export const overlaySizesForPopups = () => { - return window.newspack_popups_wizard_data?.overlay_sizes; + return window.newspackAudienceCampaigns?.overlay_sizes; }; export const getCardClassName = ( status, forceDisabled = false ) => { @@ -333,7 +334,7 @@ addFilter( : __( 'Does not have active subscription(s):', 'newspack-plugin' ) } ids={ item.value } - path="/newspack/v1/wizard/newspack-popups-wizard/subscription-products" + path={ `${ newspackAudienceCampaigns.api }/subscription-products` } /> ); } diff --git a/src/wizards/popups/views/index.js b/src/wizards/audience/views/campaigns/views.js similarity index 100% rename from src/wizards/popups/views/index.js rename to src/wizards/audience/views/campaigns/views.js diff --git a/src/wizards/audience/views/donations/configuration/index.tsx b/src/wizards/audience/views/donations/configuration/index.tsx new file mode 100644 index 0000000000..fa7105fe28 --- /dev/null +++ b/src/wizards/audience/views/donations/configuration/index.tsx @@ -0,0 +1,409 @@ +/** + * WordPress dependencies. + */ +import { __, sprintf } from '@wordpress/i18n'; +import { useDispatch } from '@wordpress/data'; +import { ToggleControl } from '@wordpress/components'; + +/** + * Internal dependencies. + */ +import MoneyInput from '../../../components/money-input'; +import { + Button, + Card, + Grid, + Notice, + SectionHeader, + SelectControl, + TextControl, + Wizard, +} from '../../../../../components/src'; +import WizardsTab from '../../../../wizards-tab'; +import { AUDIENCE_DONATIONS_WIZARD_SLUG } from '../../../constants'; +import { CoverFeesSettings } from '../../../components/cover-fees-settings'; + +type FrequencySlug = 'once' | 'month' | 'year'; + +const FREQUENCIES: { + [ Key in FrequencySlug as string ]: { + tieredLabel: string; + staticLabel: string; + }; +} = { + once: { + tieredLabel: __( 'One-time donations' ), + staticLabel: __( 'Suggested one-time donation amount' ), + }, + month: { + tieredLabel: __( 'Monthly donations' ), + staticLabel: __( 'Suggested donation amount per month' ), + }, + year: { + tieredLabel: __( 'Annual donations' ), + staticLabel: __( 'Suggested donation amount per year' ), + }, +}; +const FREQUENCY_SLUGS: FrequencySlug[] = Object.keys( + FREQUENCIES +) as FrequencySlug[]; + +export const DonationAmounts = () => { + const wizardData = Wizard.useWizardData( + AUDIENCE_DONATIONS_WIZARD_SLUG + ) as AudienceDonationsWizardData; + const { updateWizardSettings } = useDispatch( Wizard.STORE_NAMESPACE ); + + if ( ! wizardData.donation_data || 'errors' in wizardData.donation_data ) { + return null; + } + + const { + amounts, + currencySymbol, + tiered, + disabledFrequencies, + minimumDonation, + trashed, + } = wizardData.donation_data; + + const changeHandler = ( path: ( string | number )[] ) => ( value: any ) => + updateWizardSettings( { + slug: AUDIENCE_DONATIONS_WIZARD_SLUG, + path: [ 'donation_data', ...path ], + value, + } ); + + const availableFrequencies = FREQUENCY_SLUGS.map( slug => ( { + key: slug, + ...FREQUENCIES[ slug ], + } ) ); + + // Minimum donation is returned by the REST API as a string. + const minimumDonationFloat = parseFloat( minimumDonation ); + + // Whether we can use the Name Your Price extension. If not, layout is forced to Tiered. + const canUseNameYourPrice = + window.newspackAudienceDonations?.can_use_name_your_price; + + return ( + <> + <Card headerActions noBorder> + <SectionHeader + title={ __( 'Suggested Donations', 'newspack-plugin' ) } + description={ __( + 'Set suggested donation amounts. These will be the default settings for the Donate block.', + 'newspack-plugin' + ) } + noMargin + /> + { canUseNameYourPrice && ( + <SelectControl + label={ __( 'Donation Type', 'newspack-plugin' ) } + onChange={ () => + changeHandler( [ 'tiered' ] )( ! tiered ) + } + buttonOptions={ [ + { + value: true, + label: __( 'Tiered', 'newspack-plugin' ), + }, + { + value: false, + label: __( 'Untiered', 'newspack-plugin' ), + }, + ] } + buttonSmall + value={ tiered } + hideLabelFromVision + /> + ) } + </Card> + { + Array.isArray( trashed ) && 0 < trashed.length && ( + <Notice isError> + { <span + dangerouslySetInnerHTML={ + { __html: sprintf( + // Translators: %1$s is a link to the trashed products. %2$s is a comma-separated list of trashed product names. + __( + 'One or more donation products is in trash. Please <a href="%1$s">restore the product(s)</a> to continue using donation features: %2$s', + 'newspack-plugin' + ), + '/wp-admin/edit.php?post_status=trash&post_type=product', + trashed.join( ', ' ) + ) + } + } + /> } + </Notice> + ) + } + { tiered ? ( + <Grid columns={ 1 }> + { availableFrequencies.map( section => { + const isFrequencyDisabled = + disabledFrequencies[ section.key ]; + const isOneFrequencyActive = + Object.values( disabledFrequencies ).filter( + Boolean + ).length === + FREQUENCY_SLUGS.length - 1; + return ( + <Card noBorder key={ section.key }> + <Grid columns={ 1 } gutter={ 8 }> + <ToggleControl + checked={ ! isFrequencyDisabled } + onChange={ () => + changeHandler( [ + 'disabledFrequencies', + section.key, + ] )( ! isFrequencyDisabled ) + } + label={ section.tieredLabel } + disabled={ + ! isFrequencyDisabled && + isOneFrequencyActive + } + /> + { ! isFrequencyDisabled && ( + <Grid columns={ 3 } rowGap={ 16 }> + <MoneyInput + currencySymbol={ + currencySymbol + } + label={ __( 'Low-tier' ) } + error={ + amounts[ + section.key + ][ 0 ] < + minimumDonationFloat + ? __( + 'Warning: suggested donations should be at least the minimum donation amount.', + 'newspack-plugin' + ) + : null + } + value={ + amounts[ section.key ][ 0 ] + } + min={ minimumDonationFloat } + onChange={ changeHandler( [ + 'amounts', + section.key, + 0, + ] ) } + /> + <MoneyInput + currencySymbol={ + currencySymbol + } + label={ __( 'Mid-tier' ) } + error={ + amounts[ + section.key + ][ 1 ] < + minimumDonationFloat + ? __( + 'Warning: suggested donations should be at least the minimum donation amount.', + 'newspack-plugin' + ) + : null + } + value={ + amounts[ section.key ][ 1 ] + } + min={ minimumDonationFloat } + onChange={ changeHandler( [ + 'amounts', + section.key, + 1, + ] ) } + /> + <MoneyInput + currencySymbol={ + currencySymbol + } + label={ __( 'High-tier' ) } + error={ + amounts[ + section.key + ][ 2 ] < + minimumDonationFloat + ? __( + 'Warning: suggested donations should be at least the minimum donation amount.', + 'newspack-plugin' + ) + : null + } + value={ + amounts[ section.key ][ 2 ] + } + min={ minimumDonationFloat } + onChange={ changeHandler( [ + 'amounts', + section.key, + 2, + ] ) } + /> + </Grid> + ) } + </Grid> + </Card> + ); + } ) } + </Grid> + ) : ( + <Grid columns={ 1 }> + <Card noBorder> + <Grid columns={ 3 } rowGap={ 16 }> + { availableFrequencies.map( section => { + const isFrequencyDisabled = + disabledFrequencies[ section.key ]; + const isOneFrequencyActive = + Object.values( disabledFrequencies ).filter( + Boolean + ).length === + FREQUENCY_SLUGS.length - 1; + return ( + <Grid + columns={ 1 } + gutter={ 16 } + key={ section.key } + > + <ToggleControl + checked={ ! isFrequencyDisabled } + onChange={ () => + changeHandler( [ + 'disabledFrequencies', + section.key, + ] )( ! isFrequencyDisabled ) + } + label={ section.tieredLabel } + disabled={ + ! isFrequencyDisabled && + isOneFrequencyActive + } + /> + { ! isFrequencyDisabled && ( + <MoneyInput + currencySymbol={ + currencySymbol + } + label={ section.staticLabel } + value={ + amounts[ section.key ][ 3 ] + } + min={ minimumDonationFloat } + error={ + amounts[ + section.key + ][ 3 ] < + minimumDonationFloat + ? __( + 'Warning: suggested donations should be at least the minimum donation amount.', + 'newspack-plugin' + ) + : null + } + onChange={ changeHandler( [ + 'amounts', + section.key, + 3, + ] ) } + key={ section.key } + /> + ) } + </Grid> + ); + } ) } + </Grid> + </Card> + </Grid> + ) } + <Grid columns={ 3 }> + <TextControl + label={ __( 'Minimum donation', 'newspack-plugin' ) } + help={ __( + 'Set minimum donation amount. Setting a reasonable minimum donation amount can help protect your site from bot attacks.', + 'newspack-plugin' + ) } + type="number" + min={ 1 } + value={ minimumDonationFloat } + onChange={ ( value: string ) => + changeHandler( [ 'minimumDonation' ] )( value ) + } + /> + </Grid> + </> + ); +}; + +const Donation = () => { + const wizardData = Wizard.useWizardData( + AUDIENCE_DONATIONS_WIZARD_SLUG + ) as AudienceDonationsWizardData; + const { saveWizardSettings } = useDispatch( Wizard.STORE_NAMESPACE ); + const onSaveDonationSettings = () => + saveWizardSettings( { + slug: AUDIENCE_DONATIONS_WIZARD_SLUG, + payloadPath: [ 'donation_data' ], + auxData: { saveDonationProduct: true }, + } ); + + return ( + <WizardsTab title={ __( 'Configuration', 'newspack-plugin' ) }> + { wizardData.donation_page && ( + <> + <Card noBorder headerActions> + <SectionHeader + title={ __( + 'Donations Landing Page', + 'newspack-plugin' + ) } + noMargin + /> + <Button + variant="secondary" + isSmall + href={ wizardData.donation_page.editUrl } + onClick={ undefined } + > + { __( 'Edit Page' ) } + </Button> + </Card> + { 'publish' === wizardData.donation_page.status ? ( + <Notice + isSuccess + noticeText={ __( + 'Your donations landing page is published.', + 'newspack-plugin' + ) } + /> + ) : ( + <Notice + isError + noticeText={ __( + 'Your donations landing page is not yet published.', + 'newspack-plugin' + ) } + /> + ) } + </> + ) } + <DonationAmounts /> + <div className="newspack-buttons-card"> + <Button variant="primary" onClick={ onSaveDonationSettings }> + { __( 'Save Settings', 'newspack-plugin' ) } + </Button> + </div> + <SectionHeader + title={ __( 'Additional Settings', 'newspack-plugin' ) } + /> + <CoverFeesSettings /> + </WizardsTab> + ); +}; + +export default Donation; diff --git a/src/wizards/audience/views/donations/index.js b/src/wizards/audience/views/donations/index.js new file mode 100644 index 0000000000..77a9c4e05e --- /dev/null +++ b/src/wizards/audience/views/donations/index.js @@ -0,0 +1,53 @@ +import '../../../../shared/js/public-path'; + +/** + * External dependencies. + */ +import values from 'lodash/values'; + +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies. + */ +import { Wizard, Notice, withWizard } from '../../../../components/src'; +import Configuration from './configuration'; +import Revenue from './revenue'; +import { AUDIENCE_DONATIONS_WIZARD_SLUG, NEWSPACK, OTHER } from '../../constants'; + +const AudienceDonations = () => { + const { platform_data, donation_data } = Wizard.useWizardData( AUDIENCE_DONATIONS_WIZARD_SLUG ); + const usedPlatform = platform_data?.platform; + const sections = [ + { + label: __( 'Configuration', 'newspack-plugin' ), + path: '/configuration', + render: Configuration, + isHidden: usedPlatform === OTHER, + }, + { + label: __( 'Revenue', 'newspack-plugin' ), + path: '/revenue', + render: Revenue, + isHidden: usedPlatform !== NEWSPACK, + }, + ]; + return ( + <Wizard + headerText={ __( 'Audience Management / Donations', 'newspack-plugin' ) } + sections={ sections } + apiSlug={ AUDIENCE_DONATIONS_WIZARD_SLUG } + renderAboveSections={ () => + values( donation_data?.errors ).map( ( error, i ) => ( + <Notice key={ i } isError noticeText={ error } /> + ) ) + } + requiredPlugins={ [ 'newspack-blocks' ] } + /> + ); +}; + +export default withWizard( AudienceDonations ); diff --git a/src/wizards/audience/views/donations/revenue/index.js b/src/wizards/audience/views/donations/revenue/index.js new file mode 100644 index 0000000000..ab80281431 --- /dev/null +++ b/src/wizards/audience/views/donations/revenue/index.js @@ -0,0 +1,39 @@ +/* globals newspackAudienceDonations */ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies. + */ +import { Button, Card } from '../../../../../components/src'; +import WizardsTab from '../../../../wizards-tab'; + +/** + * Donation Revenues screen. + */ +const DonationsRevenue = () => ( + <WizardsTab title={ __( 'Revenue', 'newspack-plugin' ) }> + <div className="newspack-campaigns-wizard-analytics__wrapper"> + <Card isNarrow> + <h2>{ __( 'View Donation Revenue in WooCommerce', 'newspack-plugin' ) }</h2> + <p> + { + __( + 'You can view revenue from donations and subscriptions in the WooCommerce plugin.', + 'newspack-plugin' + ) + } + </p> + <Card buttonsCard noBorder> + <Button isPrimary href={ newspackAudienceDonations.revenue_link }> + { __( 'See Revenue Data', 'newspack-plugin' ) } + </Button> + </Card> + </Card> + </div> + </WizardsTab> +); + +export default DonationsRevenue; diff --git a/src/wizards/audience/views/setup/campaign.js b/src/wizards/audience/views/setup/campaign.js new file mode 100644 index 0000000000..ab8a7556e8 --- /dev/null +++ b/src/wizards/audience/views/setup/campaign.js @@ -0,0 +1,129 @@ +/* global newspackAudience */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import apiFetch from '@wordpress/api-fetch'; +import { useEffect, useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import WizardsTab from '../../../wizards-tab'; +import { + Button, + Notice, + Waiting, + withWizardScreen, +} from '../../../../components/src'; +import Prompt from '../../components/prompt'; +import Router from '../../../../components/src/proxied-imports/router'; +import './style.scss'; + +const { useHistory } = Router; + +const AudienceCampaign = withWizardScreen( ( { error, setError, skipPrerequisite } ) => { + const { reader_activation_url } = newspackAudience; + const [ inFlight, setInFlight ] = useState( false ); + const [ prompts, setPrompts ] = useState( null ); + const [ allReady, setAllReady ] = useState( false ); + const history = useHistory(); + + const fetchPrompts = () => { + setError( false ); + setInFlight( true ); + apiFetch( { + path: '/newspack-popups/v1/audience-management/campaign', + } ) + .then( fetchedPrompts => { + setPrompts( fetchedPrompts ); + } ) + .catch( setError ) + .finally( () => setInFlight( false ) ); + }; + + useEffect( () => { + window.scrollTo( 0, 0 ); + fetchPrompts(); + }, [] ); + + useEffect( () => { + if ( Array.isArray( prompts ) && 0 < prompts.length ) { + setAllReady( prompts.every( prompt => prompt.ready ) ); + } + }, [ prompts ] ); + + return ( + <WizardsTab + title={ __( + 'Set Up Audience Management Campaign', + 'newspack-plugin' + ) } + description={ __( + 'Preview and customize the prompts, or use our suggested defaults.', + 'newspack-plugin' + ) } + > + { error && ( + <Notice + noticeText={ + error?.message || + __( 'Something went wrong.', 'newspack-plugin' ) + } + isError + /> + ) } + { ! prompts && ! error && ( + <> + <Waiting isLeft /> + { __( 'Retrieving prompts…', 'newspack-plugin' ) } + </> + ) } + { prompts && + prompts.map( prompt => ( + <Prompt + key={ prompt.slug } + prompt={ prompt } + inFlight={ inFlight } + setInFlight={ setInFlight } + setPrompts={ setPrompts } + /> + ) ) } + <div className="newspack-buttons-card"> + <Button + variant={ 'secondary' } + isDestructive + disabled={ inFlight } + onClick={ () => { + skipPrerequisite( + { + prerequisite: 'ras_campaign', + skip: true, + }, + () => history.push( '/complete' ) + ); + } } + > + { __( 'Skip', 'newspack-plugin' ) } + </Button> + <Button + isPrimary + disabled={ inFlight || ! allReady } + href={ `${ reader_activation_url }complete` } + > + { __( 'Continue', 'newspack-plugin' ) } + </Button> + <Button + isSecondary + disabled={ inFlight } + href={ reader_activation_url } + > + { __( 'Back', 'newspack-plugin' ) } + </Button> + </div> + </WizardsTab> + ); +} ); + +export default AudienceCampaign; \ No newline at end of file diff --git a/src/wizards/engagement/views/reader-activation/complete.js b/src/wizards/audience/views/setup/complete.js similarity index 67% rename from src/wizards/engagement/views/reader-activation/complete.js rename to src/wizards/audience/views/setup/complete.js index e04124527b..1be39a8827 100644 --- a/src/wizards/engagement/views/reader-activation/complete.js +++ b/src/wizards/audience/views/setup/complete.js @@ -1,4 +1,4 @@ -/* global newspack_engagement_wizard */ +/* global newspackAudience */ /** * WordPress dependencies @@ -11,9 +11,9 @@ import { useEffect, useRef, useState } from '@wordpress/element'; /** * Internal dependencies */ +import WizardsTab from '../../../wizards-tab'; import { Button, - SectionHeader, withWizardScreen, Card, Notice, @@ -37,7 +37,7 @@ const listItems = [ }, { text: __( - 'The <strong>Reader Activation campaign</strong> will be activated with default segments and settings.', + 'The <strong>Audience Management campaign</strong> will be activated with default segments and settings.', 'newspack-plugin' ), isSkipped: '<span class="is-skipped">[skipped]</span>', @@ -47,7 +47,7 @@ const listItems = [ const DEFAULT_ACTIVATION_STEPS = { campaignsSegments: __( 'Setting up new segments…', 'newspack-plugin' ), readerRegistration: __( 'Activating reader registration…', 'newspack-plugin' ), - campaignsPrompts: __( 'Activating Reader Activation Campaign…', 'newspack-plugin' ), + campaignsPrompts: __( 'Activating Audience Management Campaign…', 'newspack-plugin' ), }; /** @@ -61,7 +61,7 @@ const generateRandomNumber = ( min, max ) => { return min + Math.random() * ( max - min ); }; -export default withWizardScreen( () => { +export default withWizardScreen( ( { fetchConfig } ) => { const [ inFlight, setInFlight ] = useState( false ); const [ error, setError ] = useState( false ); const [ progress, setProgress ] = useState( null ); @@ -71,7 +71,7 @@ export default withWizardScreen( () => { const [ activationSteps, setActivationSteps ] = useState( Object.values( DEFAULT_ACTIVATION_STEPS ) ); - const { reader_activation_url, is_skipped_campaign_setup = '' } = newspack_engagement_wizard; + const { reader_activation_url, is_skipped_campaign_setup = '' } = newspackAudience; const isSkippedCampaignSetup = is_skipped_campaign_setup === '1'; useEffect( () => { @@ -111,8 +111,10 @@ export default withWizardScreen( () => { setProgress( activationSteps.length + 1 ); // Plus one to account for the "Done!" step. setProgressLabel( __( 'Done!', 'newspack-plugin' ) ); setTimeout( () => { - setInFlight( false ); - window.location.replace( reader_activation_url ); + fetchConfig().finally( () => { + setInFlight( false ); + window.location.replace( reader_activation_url ); + } ); }, 3000 ); } }, [ completed, progress ] ); @@ -125,7 +127,7 @@ export default withWizardScreen( () => { try { setCompleted( await apiFetch( { - path: '/newspack/v1/wizard/newspack-engagement-wizard/reader-activation/activate', + path: '/newspack/v1/wizard/newspack-audience/audience-management/activate', method: 'post', data: { skip_activation: isSkippedCampaignSetup, @@ -139,9 +141,9 @@ export default withWizardScreen( () => { return ( <div className="newspack-ras-campaign__completed"> - <SectionHeader - title={ __( 'Enable Reader Activation', 'newspack-plugin' ) } - description={ () => ( + <WizardsTab + title={ __( 'Enable Audience Management', 'newspack-plugin' ) } + description={ <> { __( 'An easy way to let your readers register for your site, sign up for newsletters, or become donors and paid members. ', @@ -153,46 +155,48 @@ export default withWizardScreen( () => { { __( 'Learn more', 'newspack-plugin' ) } </ExternalLink> </> - ) } - /> - { inFlight && ( - <Card className="newspack-ras-campaign__completed-card"> - <ProgressBar - completed={ progress } - displayFraction={ false } - total={ activationSteps.length + 1 } // Plus one to account for the "Done!" step. - label={ progressLabel } - /> - </Card> - ) } - { ! inFlight && ( - <Card className="newspack-ras-campaign__completed-card"> - <h2>{ __( "You're all set to enable Reader Activation!", 'newspack-plugin' ) }</h2> - <p>{ __( 'This is what will happen next:', 'newspack-plugin' ) }</p> - - <Card noBorder className="justify-center"> - <StepsList stepsListItems={ listItems } narrowList /> - </Card> - - { error && ( - <Notice - noticeText={ error?.message || __( 'Something went wrong.', 'newspack-plugin' ) } - isError + } + > + + { inFlight && ( + <Card className="newspack-ras-campaign__completed-card"> + <ProgressBar + completed={ progress } + displayFraction={ false } + total={ activationSteps.length + 1 } // Plus one to account for the "Done!" step. + label={ progressLabel } /> - ) } + </Card> + ) } + { ! inFlight && ( + <Card className="newspack-ras-campaign__completed-card"> + <h2>{ __( "You're all set to enable Audience Management!", 'newspack-plugin' ) }</h2> + <p>{ __( 'This is what will happen next:', 'newspack-plugin' ) }</p> + + <Card noBorder className="justify-center"> + <StepsList stepsListItems={ listItems } narrowList /> + </Card> + + { error && ( + <Notice + noticeText={ error?.message || __( 'Something went wrong.', 'newspack-plugin' ) } + isError + /> + ) } - <Card buttonsCard noBorder className="justify-center"> - <Button isPrimary onClick={ () => activate() }> - { __( 'Enable Reader Activation', 'newspack-plugin' ) } - </Button> + <Card buttonsCard noBorder className="justify-center"> + <Button isPrimary onClick={ () => activate() }> + { __( 'Enable Audience Management', 'newspack-plugin' ) } + </Button> + </Card> </Card> - </Card> - ) } - <div className="newspack-buttons-card"> - <Button isSecondary disabled={ inFlight } href={ `${ reader_activation_url }/campaign` }> - { __( 'Back', 'newspack-plugin' ) } - </Button> - </div> + ) } + <div className="newspack-buttons-card"> + <Button isSecondary disabled={ inFlight } href={ `${ reader_activation_url }campaign` }> + { __( 'Back', 'newspack-plugin' ) } + </Button> + </div> + </WizardsTab> </div> ); } ); diff --git a/src/wizards/audience/views/setup/content-gating.js b/src/wizards/audience/views/setup/content-gating.js new file mode 100644 index 0000000000..fafc3b65a6 --- /dev/null +++ b/src/wizards/audience/views/setup/content-gating.js @@ -0,0 +1,141 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { ExternalLink } from '@wordpress/components'; +import apiFetch from '@wordpress/api-fetch'; +import { useEffect, useState } from '@wordpress/element'; + +import { ActionCard, Notice, withWizardScreen } from '../../../../components/src'; +import WizardsTab from '../../../wizards-tab'; + +export default withWizardScreen( () => { + const [ inFlight, setInFlight ] = useState( false ); + const [ error, setError ] = useState( false ); + const [ config, setConfig ] = useState( {} ); + + useEffect( () => { + fetchConfig(); + }, [] ); + + const fetchConfig = () => { + setError( false ); + setInFlight( true ); + apiFetch( { + path: '/newspack/v1/wizard/newspack-audience/content-gating', + } ) + .then( ( data ) => { + setConfig( data ); + } ) + .catch( setError ) + .finally( () => setInFlight( false ) ); + }; + + const updateConfig = newConfig => { + setError( false ); + setInFlight( true ); + apiFetch( { + path: '/newspack/v1/wizard/newspack-audience/content-gating', + method: 'POST', + data: newConfig, + } ) + .then( ( data ) => { + setConfig( data ); + } ) + .catch( setError ) + .finally( () => setInFlight( false ) ); + } + + const getContentGateDescription = () => { + let message = __( + 'Configure the gate rendered on content with restricted access.', + 'newspack-plugin' + ); + if ( 'publish' === config?.gate_status ) { + message += ' ' + __( 'The gate is currently published.', 'newspack-plugin' ); + } else if ( + 'draft' === config?.gate_status || + 'trash' === config?.gate_status + ) { + message += ' ' + __( 'The gate is currently a draft.', 'newspack-plugin' ); + } + return message; + }; + + return ( + <WizardsTab + title={ __( 'Content Gating', 'newspack-plugin' ) } + description={ + <> + { __( + "WooCommerce Memberships integration to improve the reader experience with content gating. ", + 'newspack-plugin' + ) } + <ExternalLink + href={ + 'https://help.newspack.com/engagement/audience-management-system/content-gating/' + } + > + { __( 'Learn more', 'newspack-plugin' ) } + </ExternalLink> + </> + } + > + { error && ( + <Notice + noticeText={ + error?.message || + __( 'Something went wrong.', 'newspack-plugin' ) + } + isError + /> + ) } + <ActionCard + title={ __( + 'Content Gate', + 'newspack-plugin' + ) } + titleLink={ config.edit_gate_url } + href={ config.edit_gate_url } + description={ getContentGateDescription() } + actionText={ __( + 'Configure', + 'newspack-plugin' + ) } + /> + { config?.plans && + 1 < config.plans.length && ( + <ActionCard + title={ __( + 'Require membership in all plans', + 'newspack-plugin' + ) } + description={ __( + 'When enabled, readers must belong to all membership plans that apply to a restricted content item before they are granted access. Otherwise, they will be able to unlock access to that item with membership in any single plan that applies to it.', + 'newspack-plugin' + ) } + toggleOnChange={ value => updateConfig( { require_all_plans: value } ) } + toggleChecked={ + config.require_all_plans + } + disabled={ inFlight } + /> + ) } + <ActionCard + title={ __( + 'Display memberships on the subscriptions tab', + 'newspack-plugin' + ) } + description={ __( + "Display memberships that don't have active subscriptions on the My Account Subscriptions tab, so readers can see information like expiration dates.", + 'newspack-plugin' + ) } + toggleOnChange={ value => updateConfig( { show_on_subscription_tab: value } ) } + toggleChecked={ + config.show_on_subscription_tab + } + disabled={ inFlight } + /> + </WizardsTab> + ); +} ); diff --git a/src/wizards/audience/views/setup/index.js b/src/wizards/audience/views/setup/index.js new file mode 100644 index 0000000000..09e6694a09 --- /dev/null +++ b/src/wizards/audience/views/setup/index.js @@ -0,0 +1,220 @@ +/* globals newspackAudience */ +/** + * Configuration + */ + +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; +import { useEffect, useState } from '@wordpress/element'; + +/** + * Internal dependencies. + */ +import Setup from './setup'; +import Campaign from './campaign'; +import Complete from './complete'; +import { withWizard } from '../../../../components/src'; +import Router from '../../../../components/src/proxied-imports/router'; +import ContentGating from './content-gating'; +import TransactionalEmails from './transactional-emails'; +import Payment from './payment'; + +const { HashRouter, Redirect, Route, Switch } = Router; + +function AudienceWizard( { confirmAction, pluginRequirements, wizardApiFetch } ) { + const [ inFlight, setInFlight ] = useState( false ); + const [ config, setConfig ] = useState( {} ); + const [ prerequisites, setPrerequisites ] = useState( null ); + const [ error, setError ] = useState( false ); + const [ espSyncErrors, setEspSyncErrors ] = useState( [] ); + + const fetchConfig = () => { + setError( false ); + setInFlight( true ); + return wizardApiFetch( { + path: '/newspack/v1/wizard/newspack-audience/audience-management', + } ) + .then( ( { config: fetchedConfig, prerequisites_status, can_esp_sync } ) => { + setPrerequisites( prerequisites_status ); + setConfig( fetchedConfig ); + setEspSyncErrors( can_esp_sync.errors ); + } ) + .catch( setError ) + .finally( () => setInFlight( false ) ); + }; + const updateConfig = ( key, val ) => { + setConfig( { ...config, [ key ]: val } ); + }; + const saveConfig = data => { + setError( false ); + setInFlight( true ); + wizardApiFetch( { + path: '/newspack/v1/wizard/newspack-audience/audience-management', + method: 'post', + quiet: true, + data, + } ) + .then( ( { config: fetchedConfig, prerequisites_status, can_esp_sync } ) => { + setPrerequisites( prerequisites_status ); + setConfig( fetchedConfig ); + setEspSyncErrors( can_esp_sync.errors ); + } ) + .catch( setError ) + .finally( () => setInFlight( false ) ); + }; + const skipPrerequisite = ( data, callback = null ) => { + confirmAction( + { + message: __( + 'Are you sure you want to skip this step? You can always come back later.', + 'newspack-plugin' + ), + confirmText: __( 'Skip', 'newspack-plugin' ), + callback: () => { + setError( false ); + setInFlight( true ); + wizardApiFetch( { + path: '/newspack/v1/wizard/newspack-audience/audience-management/skip', + method: 'post', + quiet: true, + data, + } ) + .then( ( { config: fetchedConfig, prerequisites_status, can_esp_sync } ) => { + setPrerequisites( prerequisites_status ); + setConfig( fetchedConfig ); + setEspSyncErrors( can_esp_sync.errors ); + if ( callback ) { + callback(); + } + } ) + .catch( setError ) + .finally( () => setInFlight( false ) ); + }, + } + ); + }; + + useEffect( () => { + window.scrollTo( 0, 0 ); + fetchConfig(); + }, [] ); + + const emails = Object.values( config.emails || {} ); + + let tabs = null; + + if ( config.enabled ) { + tabs = [ + { + label: __( 'Setup', 'newspack-plugin' ), + path: '/', + }, + newspackAudience.has_memberships && { + label: __( 'Content Gating', 'newspack-plugin' ), + path: '/content-gating', + }, + emails.length > 0 && { + label: __( 'Transactional Emails', 'newspack-plugin' ), + path: '/transactional-emails', + }, + { + label: __( 'Checkout & Payment', 'newspack-plugin' ), + path: '/payment', + }, + ]; + tabs = tabs.filter( tab => tab ); + } + + const getSharedProps = ( configKey, type = 'checkbox' ) => { + const props = { + onChange: val => updateConfig( configKey, val ), + }; + if ( configKey !== 'enabled' ) { + props.disabled = inFlight; + } + switch ( type ) { + case 'checkbox': + props.checked = Boolean( config[ configKey ] ); + break; + case 'text': + props.value = config[ configKey ] || ''; + break; + } + + return props; + }; + + const props = { + headerText: __( + 'Audience Management', + 'newspack-plugin' + ), + tabbedNavigation: tabs, + wizardApiFetch, + inFlight, + error, + fetchConfig, + updateConfig, + saveConfig, + skipPrerequisite, + setInFlight, + setError, + getSharedProps, + espSyncErrors, + prerequisites, + config, + emails, + }; + + return ( + <> + <HashRouter hashType="slash"> + <Switch> + { pluginRequirements } + <Route + path="/" + exact + render={ () => ( + <Setup { ...props } /> + ) } + /> + <Route + path="/content-gating" + render={ () => ( + <ContentGating { ...props } /> + ) } + /> + <Route + path="/transactional-emails" + render={ () => ( + <TransactionalEmails { ...props } /> + ) } + /> + <Route + path="/payment" + render={ () => ( + <Payment { ...props } /> + ) } + /> + <Route + path="/campaign" + render={ () => ( + <Campaign { ...props } /> + ) } + /> + <Route + path="/complete" + render={ () => ( + <Complete { ...props } /> + ) } + /> + <Redirect to="/" /> + </Switch> + </HashRouter> + </> + ); +} + +export default withWizard( AudienceWizard ); diff --git a/src/wizards/audience/views/setup/payment.js b/src/wizards/audience/views/setup/payment.js new file mode 100644 index 0000000000..e6126c72e9 --- /dev/null +++ b/src/wizards/audience/views/setup/payment.js @@ -0,0 +1,37 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies. + */ +import { + Wizard, + withWizardScreen, +} from '../../../../components/src'; +import WizardsTab from '../../../wizards-tab'; +import Platform from '../../components/platform'; +import PaymentGateways from '../../components/payment-methods'; +import NRHSettings from '../../components/nrh-settings'; +import BillingFields from '../../components/billing-fields'; +import CheckoutConfiguration from '../../components/checkout-configuration'; + +export default withWizardScreen( function () { + const data = Wizard.useWizardData( 'newspack-audience/payment' ); + return ( + <WizardsTab + title={ __( 'Checkout & Payment', 'newspack-plugin' ) } + description={ __( + 'Reader revenue configuration for donations and subscriptions.', + 'newspack-plugin' + ) } + > + <Platform /> + { data?.platform_data?.platform === 'wc' && <PaymentGateways /> } + { data?.platform_data?.platform === 'wc' && <BillingFields /> } + { data?.platform_data?.platform === 'nrh' && <NRHSettings /> } + <CheckoutConfiguration /> + </WizardsTab> + ); +} ); diff --git a/src/wizards/audience/views/setup/setup.js b/src/wizards/audience/views/setup/setup.js new file mode 100644 index 0000000000..8371ad7e2f --- /dev/null +++ b/src/wizards/audience/views/setup/setup.js @@ -0,0 +1,390 @@ +/* global newspackAudience */ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { ExternalLink } from '@wordpress/components'; +import apiFetch from '@wordpress/api-fetch'; +import { useEffect, useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { + ActionCard, + Button, + Card, + Notice, + PluginInstaller, + SectionHeader, + TextControl, + Waiting, + withWizardScreen, +} from '../../../../components/src'; +import WizardsTab from '../../../wizards-tab'; +import Prerequisite from '../../components/prerequisite'; +import ActiveCampaign from '../../components/active-campaign'; +import MetadataFields from '../../components/metadata-fields'; +import Mailchimp from '../../components/mailchimp'; +import { HANDOFF_KEY } from '../../../../components/src/consts'; +import SortableNewsletterListControl from '../../../../components/src/sortable-newsletter-list-control'; +import Salesforce from '../../components/salesforce'; + +export default withWizardScreen( + ( + { + config, + fetchConfig, + updateConfig, + getSharedProps, + saveConfig, + skipPrerequisite, + prerequisites, + espSyncErrors, + error, + inFlight + } + ) => { + const [ allReady, setAllReady ] = useState( false ); + const [ isActiveCampaign, setIsActiveCampaign ] = useState( false ); + const [ isMailchimp, setIsMailchimp ] = useState( false ); + const [ missingPlugins, setMissingPlugins ] = useState( [] ); + + useEffect( () => { + window.scrollTo( 0, 0 ); + // Clear the handoff when the component mounts. + window.localStorage.removeItem( HANDOFF_KEY ); + }, [] ); + + useEffect( () => { + apiFetch( { + path: '/newspack/v1/wizard/newspack-newsletters/settings', + } ).then( data => { + setIsMailchimp( + data?.settings?.newspack_newsletters_service_provider?.value === 'mailchimp' + ); + setIsActiveCampaign( + data?.settings?.newspack_newsletters_service_provider?.value === 'active_campaign' + ); + } ); + }, [] ); + + useEffect( () => { + const _allReady = + ! missingPlugins.length && + prerequisites && + Object.keys( prerequisites ).every( + key => prerequisites[ key ]?.active || prerequisites[ key ]?.is_skipped + ); + + setAllReady( _allReady ); + + if ( prerequisites ) { + setMissingPlugins( + Object.keys( prerequisites ).reduce( ( acc, slug ) => { + const prerequisite = prerequisites[ slug ]; + if ( prerequisite.plugins ) { + for ( const pluginSlug in prerequisite.plugins ) { + if ( ! prerequisite.plugins[ pluginSlug ] ) { + acc.push( pluginSlug ); + } + } + } + return acc; + }, [] ) + ); + } + }, [ prerequisites ] ); + + return ( + <WizardsTab + title={ __( 'Audience Management', 'newspack-plugin' ) } + description={ + <> + { __( + "Newspack's Audience Management system is a set of features that aim to increase reader loyalty, promote engagement, and drive revenue. ", + 'newspack-plugin' + ) } + <ExternalLink + href={ + 'https://help.newspack.com/engagement/audience-management-system' + } + > + { __( 'Learn more', 'newspack-plugin' ) } + </ExternalLink> + </> + } + > + { error && ( + <Notice + noticeText={ + error?.message || + __( 'Something went wrong.', 'newspack-plugin' ) + } + isError + /> + ) } + { 0 < missingPlugins.length && ( + <Notice + noticeText={ __( + 'The following plugins are required.', + 'newspack-plugin' + ) } + isWarning + /> + ) } + { 0 === missingPlugins.length && prerequisites && ! allReady && ( + <Notice + noticeText={ __( + 'Complete these settings to enable Audience Management.', + 'newspack-plugin' + ) } + isWarning + /> + ) } + { prerequisites && allReady && config.enabled && ( + <Notice + noticeText={ __( + 'Audience Management is enabled.', + 'newspack-plugin' + ) } + isSuccess + /> + ) } + { ! prerequisites && ( + <> + <Waiting isLeft /> + { __( 'Fetching status…', 'newspack-plugin' ) } + </> + ) } + { 0 < missingPlugins.length && prerequisites && ( + <PluginInstaller + plugins={ missingPlugins } + withoutFooterButton + onStatus={ ( { complete } ) => complete && fetchConfig() } + /> + ) } + { ! missingPlugins.length && + prerequisites && + Object.keys( prerequisites ).map( key => ( + <Prerequisite + key={ key } + slug={ key } + config={ config } + getSharedProps={ getSharedProps } + inFlight={ inFlight } + prerequisite={ prerequisites[ key ] } + fetchConfig={ fetchConfig } + saveConfig={ saveConfig } + skipPrerequisite={ skipPrerequisite } + /> + ) ) } + { config.enabled && ( + <Card noBorder> + <hr /> + <SectionHeader + title={ __( + 'Newsletter Subscription Lists', + 'newspack-plugin' + ) } + /> + <ActionCard + title={ __( + 'Present newsletter signup after checkout and registration', + 'newspack-plugin' + ) } + description={ __( + 'Ask readers to sign up for newsletters after creating an account or completing a purchase.', + 'newspack-plugin' + ) } + toggleChecked={ config.use_custom_lists } + toggleOnChange={ value => + updateConfig( 'use_custom_lists', value ) + } + /> + { config.use_custom_lists && ( + <SortableNewsletterListControl + lists={ + newspackAudience.available_newsletter_lists + } + selected={ config.newsletter_lists } + onChange={ selected => + updateConfig( 'newsletter_lists', selected ) + } + /> + ) } + + <hr /> + + <SectionHeader + title={ __( + 'Email Service Provider (ESP) Advanced Settings', + 'newspack-plugin' + ) } + description={ __( + 'Settings for Newspack Newsletters integration.', + 'newspack-plugin' + ) } + /> + <TextControl + label={ __( + 'Newsletter subscription text on registration', + 'newspack-plugin' + ) } + help={ __( + 'The text to display while subscribing to newsletters from the sign-in modal.', + 'newspack-plugin' + ) } + { ...getSharedProps( 'newsletters_label', 'text' ) } + /> + <ActionCard + description={ __( + 'Configure options for syncing reader data to the connected ESP.', + 'newspack-plugin' + ) } + hasGreyHeader={ true } + isMedium + title={ __( + 'Sync contacts to ESP', + 'newspack-plugin' + ) } + toggleChecked={ config.sync_esp } + toggleOnChange={ value => + updateConfig( 'sync_esp', value ) + } + > + { config.sync_esp && ( + <> + { 0 < Object.keys( espSyncErrors ).length && ( + <Notice + noticeText={ Object.values( + espSyncErrors + ).join( ' ' ) } + isError + /> + ) } + { isMailchimp && ( + <Mailchimp + value={ { + audienceId: + config.mailchimp_audience_id, + readerDefaultStatus: + config.mailchimp_reader_default_status, + } } + onChange={ ( key, value ) => { + if ( key === 'audienceId' ) { + updateConfig( + 'mailchimp_audience_id', + value + ); + } + if ( + key === 'readerDefaultStatus' + ) { + updateConfig( + 'mailchimp_reader_default_status', + value + ); + } + } } + /> + ) } + { isActiveCampaign && ( + <ActiveCampaign + value={ { + masterList: + config.active_campaign_master_list, + } } + onChange={ ( key, value ) => { + if ( key === 'masterList' ) { + updateConfig( + 'active_campaign_master_list', + value + ); + } + } } + /> + ) } + <MetadataFields + availableFields={ + newspackAudience.esp_metadata_fields || + [] + } + selectedFields={ config.metadata_fields } + updateConfig={ updateConfig } + getSharedProps={ getSharedProps } + /> + </> + ) } + </ActionCard> + <div className="newspack-buttons-card"> + <Button + isPrimary + onClick={ () => { + if ( config.sync_esp ) { + if ( + isMailchimp && + config.mailchimp_audience_id === '' + ) { + // eslint-disable-next-line no-alert + alert( + __( + 'Please select a Mailchimp Audience ID.', + 'newspack-plugin' + ) + ); + return; + } + if ( + isActiveCampaign && + config.active_campaign_master_list === + '' + ) { + // eslint-disable-next-line no-alert + alert( + __( + 'Please select an ActiveCampaign Master List.', + 'newspack-plugin' + ) + ); + return; + } + } + saveConfig( { + newsletters_label: config.newsletters_label, // TODO: Deprecate this in favor of user input via the prompt copy wizard. + mailchimp_audience_id: + config.mailchimp_audience_id, + mailchimp_reader_default_status: + config.mailchimp_reader_default_status, + active_campaign_master_list: + config.active_campaign_master_list, + use_custom_lists: config.use_custom_lists, + newsletter_lists: config.newsletter_lists, + sync_esp: config.sync_esp, + metadata_fields: config.metadata_fields, + metadata_prefix: config.metadata_prefix, + woocommerce_registration_required: config.woocommerce_registration_required, + woocommerce_checkout_privacy_policy_text: config.woocommerce_checkout_privacy_policy_text, + woocommerce_post_checkout_success_text: config.woocommerce_post_checkout_success_text, + woocommerce_post_checkout_registration_success_text: config.woocommerce_post_checkout_registration_success_text, + } ); + } } + disabled={ inFlight } + > + { __( + 'Save Settings', + 'newspack-plugin' + ) } + </Button> + </div> + { newspackAudience.can_use_salesforce && ( + <> + <hr /> + <Salesforce /> + </> + ) } + </Card> + ) } + </WizardsTab> + ); +} ); diff --git a/src/wizards/engagement/views/reader-activation/style.scss b/src/wizards/audience/views/setup/style.scss similarity index 97% rename from src/wizards/engagement/views/reader-activation/style.scss rename to src/wizards/audience/views/setup/style.scss index ac8694b6bf..f5602972a5 100644 --- a/src/wizards/engagement/views/reader-activation/style.scss +++ b/src/wizards/audience/views/setup/style.scss @@ -111,5 +111,8 @@ span.is-skipped { .newspack-action-card__notification.newspack-action-card__region-children .newspack-notice { margin-top: 0; } + .button-group button { + margin-right: 8px; + } } } diff --git a/src/wizards/audience/views/setup/transactional-emails.js b/src/wizards/audience/views/setup/transactional-emails.js new file mode 100644 index 0000000000..6a7e9a03ae --- /dev/null +++ b/src/wizards/audience/views/setup/transactional-emails.js @@ -0,0 +1,62 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies. + */ +import WizardsTab from '../../../wizards-tab'; +import { utils, ActionCard, withWizardScreen } from '../../../../components/src'; + +export default withWizardScreen( + ( { emails, saveConfig, wizardApiFetch, setInFlight, setError, config } ) => { + const resetEmail = postId => { + setError( false ); + setInFlight( true ); + wizardApiFetch( { + path: `/newspack/v1/wizard/newspack-audience/audience-management/emails/${ postId }`, + method: 'DELETE', + quiet: true, + } ) + .then( e => saveConfig( { ...config, emails: e } ) ) + .catch( setError ) + .finally( () => setInFlight( false ) ); + }; + return ( + <WizardsTab + title={ __( 'Transactional Emails', 'newspack-plugin' ) } + description={ __( + 'Customize the content of transactional emails.', + 'newspack-plugin' + ) } + > + { emails.map( email => ( + <ActionCard + key={ email.post_id } + title={ email.label } + titleLink={ email.edit_link } + href={ email.edit_link } + description={ email.description } + actionText={ __( 'Edit', 'newspack-plugin' ) } + onSecondaryActionClick={ () => { + if ( + utils.confirmAction( + __( + 'Are you sure you want to reset the contents of this email?', + 'newspack-plugin' + ) + ) + ) { + resetEmail( email.post_id ); + } + } } + secondaryActionText={ __( 'Reset', 'newspack-plugin' ) } + secondaryDestructive={ true } + isSmall + /> + ) ) } + </WizardsTab> + ); + } +); diff --git a/src/wizards/audience/views/subscriptions/index.tsx b/src/wizards/audience/views/subscriptions/index.tsx new file mode 100644 index 0000000000..43787206b0 --- /dev/null +++ b/src/wizards/audience/views/subscriptions/index.tsx @@ -0,0 +1,49 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies. + */ +import { Button, Card, Wizard, withWizard } from '../../../../components/src'; +import WizardsTab from '../../../wizards-tab'; +import WizardSection from '../../../wizards-section'; + +const subscriptionTabs = window.newspackAudienceSubscriptions.tabs; + +function AudienceSubscriptions() { + const tabs = subscriptionTabs.map( tab => { + const render = () => ( + <WizardsTab title={ tab.title }> + <WizardSection> + <Card isNarrow> + <h2>{ tab.header }</h2> + <p>{ tab.description }</p> + <Button variant="primary" href={ tab.href }> + { tab.btn_text } + </Button> + </Card> + </WizardSection> + </WizardsTab> + ); + return { + label: tab.title, + path: tab.path, + render, + }; + } ); + + return ( + <Wizard + headerText={ __( + 'Audience Management / Subscriptions', + 'newspack-plugin' + ) } + sections={ tabs } + requiredPlugins={ [ 'woocommerce', 'woocommerce-memberships' ] } + /> + ); +} + +export default withWizard( AudienceSubscriptions ); diff --git a/src/wizards/componentsDemo/index.js b/src/wizards/componentsDemo/index.js index cdba721b6a..95a814db39 100644 --- a/src/wizards/componentsDemo/index.js +++ b/src/wizards/componentsDemo/index.js @@ -11,7 +11,7 @@ import '../../shared/js/public-path'; */ import { Component, Fragment, render } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { audio, home, plus, reusableBlock, typography } from '@wordpress/icons'; +import { audio, category, plus, reusableBlock, typography } from '@wordpress/icons'; /** * Internal dependencies. @@ -19,6 +19,7 @@ import { audio, home, plus, reusableBlock, typography } from '@wordpress/icons'; import { ActionCard, AutocompleteWithSuggestions, + BoxContrast, Button, ButtonCard, Card, @@ -54,7 +55,7 @@ class ComponentsDemo extends Component { selectValue3: '', selectValues: [], modalShown: false, - color1: '#3366ff', + color1: '#003da5', }; } @@ -84,7 +85,7 @@ class ComponentsDemo extends Component { href={ newspack_urls.dashboard } label={ __( 'Return to Dashboard', 'newspack-plugin' ) } showTooltip={ true } - icon={ home } + icon={ category } iconSize={ 36 } > <NewspackIcon size={ 36 } /> @@ -148,7 +149,7 @@ class ComponentsDemo extends Component { }, 'fb-instant-articles': { actionText: __( 'Configure Instant Articles', 'newspack-plugin' ), - href: '/wp-admin/admin.php?page=newspack', + href: '/wp-admin/admin.php?page=newspack-dashboard', }, } } /> @@ -636,8 +637,8 @@ class ComponentsDemo extends Component { <Card> <h2>{ __( 'ButtonCard', 'newspack-plugin' ) }</h2> <ButtonCard - href="admin.php?page=newspack-site-design-wizard" - title={ __( 'Site Design', 'newspack-plugin' ) } + href="admin.php?page=newspack-settings#/theme-and-brand" + title={ __( 'Theme and Brand', 'newspack-plugin' ) } desc={ __( 'Customize the look and feel of your site', 'newspack-plugin' ) } icon={ typography } chevron @@ -742,6 +743,28 @@ class ComponentsDemo extends Component { } } /> </Card> + <Card> + <h2>{ __( 'Box Contrast', 'newspack-plugin' ) }</h2> + <p> + Component for adding color black/white depending on contrast ratio for{ ' ' } + <code>hexColor</code> prop value. + </p> + <h3>{ __( 'Demo 1:', 'newspack-plugin' ) }</h3> + <BoxContrast hexColor="#e5bd13">#e5bd13</BoxContrast> + <BoxContrast hexColor="#e5bd13" isInverted> + #e5bd13 / Inverted + </BoxContrast> + <h3>{ __( 'Demo 2:', 'newspack-plugin' ) }</h3> + <BoxContrast hexColor="#003da5">#003da5</BoxContrast> + <BoxContrast hexColor="#003da5" isInverted> + #003da5 / Inverted + </BoxContrast> + <h3>{ __( 'Demo 3:', 'newspack-plugin' ) }</h3> + <BoxContrast hexColor="#51f1ff">#e5bd13</BoxContrast> + <BoxContrast hexColor="#51f1ff" isInverted> + #51f1ff / Inverted + </BoxContrast> + </Card> </div> <Footer /> </Fragment> diff --git a/src/wizards/connections/index.js b/src/wizards/connections/index.js deleted file mode 100644 index 9cc651d955..0000000000 --- a/src/wizards/connections/index.js +++ /dev/null @@ -1,44 +0,0 @@ -import '../../shared/js/public-path'; - -/** - * WordPress dependencies. - */ -import { render, createElement } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies. - */ -import { withWizard, withWizardScreen } from '../../components/src'; -import Router from '../../components/src/proxied-imports/router'; -import { Main } from './views'; - -import './style.scss'; - -const { HashRouter, Redirect, Route, Switch } = Router; - -const MainScreen = withWizardScreen( Main ); - -const ConnectionsWizard = ( { pluginRequirements, wizardApiFetch, startLoading, doneLoading } ) => { - const wizardScreenProps = { - headerText: __( 'Connections', 'newspack-plugin' ), - subHeaderText: __( 'Connections to third-party services', 'newspack-plugin' ), - wizardApiFetch, - startLoading, - doneLoading, - }; - return ( - <HashRouter hashType="slash"> - <Switch> - { pluginRequirements } - <Route exact path="/" render={ () => <MainScreen { ...wizardScreenProps } /> } /> - <Redirect to="/" /> - </Switch> - </HashRouter> - ); -}; - -render( - createElement( withWizard( ConnectionsWizard ) ), - document.getElementById( 'newspack-connections-wizard' ) -); diff --git a/src/wizards/connections/views/index.js b/src/wizards/connections/views/index.js deleted file mode 100644 index dc91e6bec3..0000000000 --- a/src/wizards/connections/views/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as Main } from './main'; diff --git a/src/wizards/connections/views/main/fivetran.js b/src/wizards/connections/views/main/fivetran.js deleted file mode 100644 index ba478068b3..0000000000 --- a/src/wizards/connections/views/main/fivetran.js +++ /dev/null @@ -1,137 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { useEffect, useState } from '@wordpress/element'; -import apiFetch from '@wordpress/api-fetch'; -import { Button, CheckboxControl } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { ActionCard } from '../../../../components/src'; - -/** - * External dependencies - */ -import classnames from 'classnames'; -import get from 'lodash/get'; - -const CONNECTORS = [ - { - service: 'stripe', - label: __( 'Stripe', 'newspack-plugin' ), - }, -]; - -const getConnectionStatus = ( item, connections ) => { - const hasConnections = connections !== undefined; - const setupState = get( connections, [ item.service, 'setup_state' ] ); - const syncState = get( connections, [ item.service, 'sync_state' ] ); - const schemaStatus = get( connections, [ item.service, 'schema_status' ] ); - const isPending = ( schemaStatus && 'ready' !== schemaStatus ) || 'paused' === syncState; - let label = '-'; - if ( setupState ) { - if ( 'ready' === schemaStatus ) { - label = `${ setupState }, ${ syncState }`; - } else if ( isPending ) { - label = `${ setupState }, ${ syncState }. ${ __( - 'Sync is in progress – please check back in a while.', - 'newspack-plugin' - ) }`; - } - } else if ( hasConnections ) { - label = __( 'Not connected', 'newspack-plugin' ); - } - return { - label, - isConnected: setupState === 'connected', - isPending, - }; -}; - -const FivetranConnection = ( { setError } ) => { - const [ connections, setConnections ] = useState(); - const [ inFlight, setInFlight ] = useState( false ); - const [ hasAcceptedTOS, setHasAcceptedTOS ] = useState( null ); - - const handleError = err => - setError( err.message || __( 'Something went wrong.', 'newspack-plugin' ) ); - - const hasConnections = connections !== undefined; - const isDisabled = inFlight || ! hasConnections || ! hasAcceptedTOS; - - useEffect( () => { - setInFlight( true ); - apiFetch( { path: '/newspack/v1/oauth/fivetran' } ) - .then( response => { - setConnections( response.connections_statuses ); - setHasAcceptedTOS( response.has_accepted_tos ); - } ) - .catch( handleError ) - .finally( () => setInFlight( false ) ); - }, [] ); - - const createConnection = ( { service } ) => { - setInFlight( true ); - apiFetch( { - path: `/newspack/v1/oauth/fivetran/${ service }`, - method: 'POST', - data: { - service, - }, - } ) - .then( ( { url } ) => ( window.location = url ) ) - .catch( handleError ); - }; - - return ( - <> - <div> - { __( 'In order to use the this features, you must read and accept', 'newspack-plugin' ) }{ ' ' } - <a href="https://newspack.com/terms-of-service/"> - { __( 'Newspack Terms of Service', 'newspack-plugin' ) } - </a> - : - </div> - <CheckboxControl - className={ classnames( 'mt1', { 'o-50': hasAcceptedTOS === null } ) } - checked={ hasAcceptedTOS } - disabled={ hasAcceptedTOS === null } - onChange={ has_accepted => { - apiFetch( { - path: `/newspack/v1/oauth/fivetran-tos`, - method: 'POST', - data: { - has_accepted, - }, - } ); - setHasAcceptedTOS( has_accepted ); - } } - label={ __( "I've read and accept Newspack Terms of Service", 'newspack-plugin' ) } - /> - { CONNECTORS.map( item => { - const status = getConnectionStatus( item, connections ); - return ( - <ActionCard - key={ item.service } - title={ item.label } - description={ `${ __( 'Status:', 'newspack-plugin' ) } ${ status.label }` } - isPending={ status.isPending } - actionText={ - <Button disabled={ isDisabled } onClick={ () => createConnection( item ) } isLink> - { status.isConnected - ? __( 'Re-connect', 'newspack-plugin' ) - : __( 'Connect', 'newspack-plugin' ) } - </Button> - } - checkbox={ status.isConnected ? 'checked' : 'unchecked' } - isMedium - /> - ); - } ) } - </> - ); -}; - -export default FivetranConnection; diff --git a/src/wizards/connections/views/main/google.js b/src/wizards/connections/views/main/google.js deleted file mode 100644 index 800bf417fe..0000000000 --- a/src/wizards/connections/views/main/google.js +++ /dev/null @@ -1,178 +0,0 @@ -/** - * External dependencies. - */ -import qs from 'qs'; - -/** - * WordPress dependencies. - */ -import { useEffect, useState } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; -import apiFetch from '@wordpress/api-fetch'; - -/** - * Internal dependencies. - */ -import { ActionCard, Button } from '../../../../components/src'; - -const getURLParams = () => { - return qs.parse( window.location.search.replace( /^\?/, '' ) ); -}; - -const GoogleOAuth = ( { setError, onInit, onSuccess, isOnboarding } ) => { - const [ authState, setAuthState ] = useState( {} ); - - const userBasicInfo = authState.user_basic_info; - - const [ inFlight, setInFlight ] = useState( false ); - const [ localError, setLocalError ] = useState( null ); - const handleError = res => { - const message = res.message || __( 'Something went wrong.', 'newspack-plugin' ); - setLocalError( message ); - if ( typeof setError === 'function' ) { - setError( message ); - } - }; - - const isConnected = Boolean( userBasicInfo && userBasicInfo.email ); - - useEffect( () => { - if ( isConnected && ! userBasicInfo.has_refresh_token ) { - setError( [ - __( 'Missing Google refresh token. Please', 'newspack-plugin' ), - ' ', - <a - key="link" - target="_blank" - rel="noreferrer" - href="https://myaccount.google.com/permissions" - > - { __( 'revoke credentials', 'newspack-plugin' ) } - </a>, - ' ', - __( 'and authorize the site again.', 'newspack-plugin' ), - ] ); - } - }, [ isConnected ] ); - - const getCurrentAuth = () => { - const params = getURLParams(); - if ( ! params.access_token ) { - let error = null; - setInFlight( true ); - apiFetch( { path: '/newspack/v1/oauth/google' } ) - .then( data => { - setAuthState( data ); - setError(); - setLocalError(); - if ( data?.user_basic_info && typeof onSuccess === 'function' ) { - onSuccess( data ); - } - } ) - .catch( err => { - error = err; - handleError( err ); - } ) - .finally( () => { - setInFlight( false ); - if ( typeof onInit === 'function' ) { - onInit( error ); - } - } ); - } - }; - - // We only want to autofetch the current auth state if we're not onboarding. - useEffect( () => { - if ( ! isOnboarding ) { - getCurrentAuth(); - } - }, [] ); - - const openAuth = () => { - const authWindow = window.open( - 'about:blank', - 'newspack_google_oauth', - 'width=500,height=600' - ); - setInFlight( true ); - apiFetch( { - path: '/newspack/v1/oauth/google/start', - } ) - .then( url => { - /** authWindow can be 'null' due to browser's popup blocker. */ - if ( authWindow ) { - authWindow.location = url; - const interval = setInterval( () => { - if ( authWindow?.closed ) { - clearInterval( interval ); - getCurrentAuth(); - } - }, 500 ); - } - } ) - .catch( err => { - if ( authWindow ) { - authWindow.close(); - } - handleError( err ); - setInFlight( false ); - } ); - }; - - // Redirect user to Google auth screen. - const disconnect = () => { - setInFlight( true ); - apiFetch( { - path: '/newspack/v1/oauth/google/revoke', - method: 'DELETE', - } ) - .then( () => { - setAuthState( {} ); - setError(); - setLocalError(); - } ) - .catch( handleError ) - .finally( () => setInFlight( false ) ); - }; - - const getDescription = () => { - if ( localError ) { - return localError; - } - if ( inFlight ) { - return __( 'Loading…', 'newspack-plugin' ); - } - if ( isConnected ) { - return sprintf( - // Translators: connected user's email address. - __( 'Connected as %s', 'newspack-plugin' ), - userBasicInfo.email - ); - } - return __( 'Not connected', 'newspack-plugin' ); - }; - - return ( - <ActionCard - title={ __( 'Google', 'newspack-plugin' ) } - description={ `${ __( 'Status:', 'newspack-plugin' ) } ${ getDescription() }` } - checkbox={ isConnected ? 'checked' : 'unchecked' } - actionText={ - <Button - isLink - isDestructive={ isConnected } - onClick={ isConnected ? disconnect : openAuth } - disabled={ inFlight } - > - { isConnected - ? __( 'Disconnect', 'newspack-plugin' ) - : __( 'Connect', 'newspack-plugin' ) } - </Button> - } - isMedium - /> - ); -}; - -export default GoogleOAuth; diff --git a/src/wizards/connections/views/main/index.js b/src/wizards/connections/views/main/index.js deleted file mode 100644 index 74c589a6c8..0000000000 --- a/src/wizards/connections/views/main/index.js +++ /dev/null @@ -1,50 +0,0 @@ -/* global newspack_connections_data */ - -/** - * WordPress dependencies. - */ -import { __ } from '@wordpress/i18n'; -import { useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { Notice, SectionHeader } from '../../../../components/src'; -import Plugins from './plugins'; -import GoogleAuth from './google'; -import Mailchimp from './mailchimp'; -import FivetranConnection from './fivetran'; -import JetpackSSO from './jetpack-sso'; -import Recaptcha from './recaptcha'; -import Webhooks from './webhooks'; - -const Main = () => { - const [ error, setError ] = useState(); - const setErrorWithPrefix = prefix => err => setError( err ? prefix + err : null ); - - return ( - <> - { error && <Notice isError noticeText={ error } /> } - <SectionHeader title={ __( 'Plugins', 'newspack-plugin' ) } /> - <Plugins /> - <SectionHeader title={ __( 'APIs', 'newspack-plugin' ) } /> - { newspack_connections_data.can_connect_google && ( - <GoogleAuth setError={ setErrorWithPrefix( __( 'Google: ', 'newspack-plugin' ) ) } /> - ) } - <Mailchimp setError={ setErrorWithPrefix( __( 'Mailchimp: ', 'newspack-plugin' ) ) } /> - { newspack_connections_data.can_connect_fivetran && ( - <> - <SectionHeader title="Fivetran" /> - <FivetranConnection - setError={ setErrorWithPrefix( __( 'Fivetran: ', 'newspack-plugin' ) ) } - /> - </> - ) } - <JetpackSSO setError={ setErrorWithPrefix( __( 'Jetpack SSO: ', 'newspack-plugin' ) ) } /> - <Recaptcha setError={ setErrorWithPrefix( __( 'reCAPTCHA: ', 'newspack-plugin' ) ) } /> - { newspack_connections_data.can_use_webhooks && <Webhooks /> } - </> - ); -}; - -export default Main; diff --git a/src/wizards/connections/views/main/mailchimp.js b/src/wizards/connections/views/main/mailchimp.js deleted file mode 100644 index f8c5c360d2..0000000000 --- a/src/wizards/connections/views/main/mailchimp.js +++ /dev/null @@ -1,173 +0,0 @@ -/** - * WordPress dependencies. - */ -import { useEffect, useState, useRef } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; -import apiFetch from '@wordpress/api-fetch'; -import { ENTER } from '@wordpress/keycodes'; -import { ExternalLink } from '@wordpress/components'; - -/** - * Internal dependencies. - */ -import { ActionCard, Button, Card, Grid, Modal, TextControl } from '../../../../components/src'; - -const Mailchimp = ( { setError } ) => { - const [ authState, setAuthState ] = useState( {} ); - const [ isModalOpen, setisModalOpen ] = useState( false ); - const [ apiKey, setAPIKey ] = useState(); - const [ isLoading, setIsLoading ] = useState( false ); - - const modalTextRef = useRef( null ); - const isConnected = Boolean( authState && authState.username ); - - const handleError = res => - setError( res.message || __( 'Something went wrong.', 'newspack-plugin' ) ); - - const openModal = () => setisModalOpen( true ); - const closeModal = () => { - setisModalOpen( false ); - setAPIKey(); - }; - - // Check the Mailchimp connectivity status. - useEffect( () => { - setIsLoading( true ); - apiFetch( { path: '/newspack/v1/oauth/mailchimp' } ) - .then( res => { - setAuthState( res ); - } ) - .catch( handleError ) - .finally( () => setIsLoading( false ) ); - }, [] ); - - useEffect( () => { - if ( isModalOpen ) { - modalTextRef.current.querySelector( 'input' ).focus(); - } - }, [ isModalOpen ] ); - - const submitAPIKey = () => { - setError(); - setIsLoading( true ); - apiFetch( { - path: '/newspack/v1/oauth/mailchimp', - method: 'POST', - data: { - api_key: apiKey, - }, - } ) - .then( res => { - setAuthState( res ); - } ) - .catch( e => { - setError( - e.message || - __( - 'Something went wrong during verification of your Mailchimp API key.', - 'newspack-plugin' - ) - ); - } ) - .finally( () => { - setIsLoading( false ); - closeModal(); - } ); - }; - - const disconnect = () => { - setIsLoading( true ); - apiFetch( { - path: '/newspack/v1/oauth/mailchimp', - method: 'DELETE', - } ) - .then( () => { - setAuthState( {} ); - setIsLoading( false ); - } ) - .catch( handleError ); - }; - - const getDescription = () => { - if ( isLoading ) { - return __( 'Loading…', 'newspack-plugin' ); - } - if ( isConnected ) { - // Translators: user connection status message. - return sprintf( __( 'Connected as %s', 'newspack-plugin' ), authState.username ); - } - return __( 'Not connected', 'newspack-plugin' ); - }; - - const getModalButtonText = () => { - if ( isLoading ) { - return __( 'Connecting…', 'newspack-plugin' ); - } - if ( isConnected ) { - return __( 'Connected', 'newspack-plugin' ); - } - return __( 'Connect', 'newspack-plugin' ); - }; - - return ( - <> - <ActionCard - title="Mailchimp" - description={ `${ __( 'Status:', 'newspack-plugin' ) } ${ getDescription() }` } - checkbox={ isConnected ? 'checked' : 'unchecked' } - actionText={ - <Button - isLink - isDestructive={ isConnected } - onClick={ isConnected ? disconnect : openModal } - disabled={ isLoading } - > - { isConnected - ? __( 'Disconnect', 'newspack-plugin' ) - : __( 'Connect', 'newspack-plugin' ) } - </Button> - } - isMedium - /> - { isModalOpen && ( - <Modal - title={ __( 'Add Mailchimp API Key', 'newspack-plugin' ) } - onRequestClose={ closeModal } - > - <div ref={ modalTextRef }> - <Grid columns={ 1 } gutter={ 8 }> - <TextControl - placeholder="123457103961b1f4dc0b2b2fd59c137b-us1" - label={ __( 'Mailchimp API Key', 'newspack-plugin' ) } - hideLabelFromVision={ true } - value={ apiKey } - onChange={ setAPIKey } - onKeyDown={ event => { - if ( ENTER === event.keyCode && '' !== apiKey ) { - event.preventDefault(); - submitAPIKey(); - } - } } - /> - <p> - <ExternalLink href="https://mailchimp.com/help/about-api-keys/#Find_or_generate_your_API_key"> - { __( 'Find or generate your API key', 'newspack-plugin' ) } - </ExternalLink> - </p> - </Grid> - </div> - <Card buttonsCard noBorder className="justify-end"> - <Button isSecondary onClick={ closeModal }> - { __( 'Cancel', 'newspack-plugin' ) } - </Button> - <Button isPrimary disabled={ ! apiKey } onClick={ submitAPIKey }> - { getModalButtonText() } - </Button> - </Card> - </Modal> - ) } - </> - ); -}; - -export default Mailchimp; diff --git a/src/wizards/connections/views/main/plugins.js b/src/wizards/connections/views/main/plugins.js deleted file mode 100644 index 55257930af..0000000000 --- a/src/wizards/connections/views/main/plugins.js +++ /dev/null @@ -1,207 +0,0 @@ -/** - * WordPress dependencies - */ -import { __, sprintf } from '@wordpress/i18n'; -import { useEffect } from '@wordpress/element'; -import apiFetch from '@wordpress/api-fetch'; - -/** - * Internal dependencies - */ -import { ActionCard, Button, hooks, Waiting } from '../../../../components/src'; - -async function fetchHandler( slug, action = '' ) { - const path = action - ? `/newspack/v1/plugins/${ slug }/${ action }` - : `/newspack/v1/plugins/${ slug }`; - const method = action ? 'POST' : 'GET'; - const result = await apiFetch( { path, method } ); - return { - status: result.Status, - configured: result.Configured, - }; -} - -const PLUGINS = { - jetpack: { - pluginSlug: 'jetpack', - editLink: 'admin.php?page=jetpack#/settings', - title: 'Jetpack', - init: () => fetchHandler( 'jetpack' ), - activate: () => fetchHandler( 'jetpack', 'activate' ), - install: () => fetchHandler( 'jetpack', 'install' ), - }, - 'google-site-kit': { - pluginSlug: 'google-site-kit', - editLink: 'admin.php?page=googlesitekit-splash', - title: __( 'Site Kit by Google', 'newspack-plugin' ), - statusDescription: { - notConfigured: __( 'Not connected for this user', 'newspack-plugin' ), - }, - init: () => fetchHandler( 'google-site-kit' ), - activate: () => fetchHandler( 'google-site-kit', 'activate' ), - install: () => fetchHandler( 'google-site-kit', 'install' ), - }, - everlit: { - pluginSlug: 'everlit', - editLink: 'admin.php?page=everlit_settings', - title: __( 'Everlit', 'newspack-plugin' ), - subTitle: __( 'AI-Generated Audio Stories', 'newspack-plugin' ), - hidden: window.newspack_connections_data.can_use_everlit !== '1', - description: ( - <> - { __( - 'Complete setup and licensing agreement to unlock 5 free audio stories per month.', - 'newspack-plugin' - ) }{ ' ' } - <a href="https://everlit.audio/" target="_blank" rel="noreferrer"> - { __( 'Learn more', 'newspack-plugin' ) } - </a> - </> - ), - statusDescription: { - uninstalled: __( 'Not installed.', 'newspack-plugin' ), - inactive: __( 'Inactive.', 'newspack-plugin' ), - notConfigured: __( 'Pending.', 'newspack-plugin' ), - }, - init: () => fetchHandler( 'everlit' ), - activate: () => fetchHandler( 'everlit', 'activate' ), - install: () => fetchHandler( 'everlit', 'install' ), - }, -}; - -const pluginConnectButton = ( { - isLoading, - isSetup, - isActive, - onActivate, - onInstall, - isInstalled, - ...plugin -} ) => { - if ( plugin.status === 'page-reload' ) { - return <span className="gray">{ __( 'Page reloading…', 'newspack-plugin' ) }</span>; - } - if ( isLoading ) { - return <Waiting />; - } - if ( ! isInstalled ) { - return ( - <Button isLink onClick={ onInstall }> - { - /* translators: %s: Plugin name */ - sprintf( __( 'Install %s', 'newspack-plugin' ), plugin.title ) - } - </Button> - ); - } - if ( ! isActive ) { - return ( - <Button isLink onClick={ onActivate }> - { - /* translators: %s: Plugin name */ - sprintf( __( 'Activate %s', 'newspack-plugin' ), plugin.title ) - } - </Button> - ); - } - if ( ! isSetup ) { - return <a href={ plugin.editLink }>{ __( 'Complete Setup', 'newspack-plugin' ) }</a>; - } -}; - -const Plugin = ( { plugin, setError } ) => { - const [ pluginState, setPluginState ] = hooks.useObjectState( plugin ); - const { title, subTitle } = pluginState; - const isActive = pluginState.status === 'active'; - const isInstalled = pluginState.status !== 'uninstalled'; - const isConfigured = pluginState.configured; - const isSetup = isActive && isConfigured; - const isLoading = ! pluginState.status; - - useEffect( () => { - plugin - .init() - .then( update => setPluginState( update ) ) - .catch( setError ); - }, [] ); - - const getDescription = () => { - if ( isLoading ) { - return __( 'Loading…', 'newspack-plugin' ); - } - const descriptionSuffix = plugin.description ?? ''; - let description = ''; - if ( ! isInstalled ) { - description = - pluginState.statusDescription?.uninstalled ?? __( 'Uninstalled.', 'newspack-plugin' ); - } else if ( ! isActive ) { - description = pluginState.statusDescription?.inactive ?? __( 'Inactive.', 'newspack-plugin' ); - } else if ( ! isConfigured ) { - description = - pluginState.statusDescription?.notConfigured ?? __( 'Not connected.', 'newspack-plugin' ); - } else { - description = __( 'Connected', 'newspack-plugin' ); - } - return ( - <> - { __( 'Status:', 'newspack-plugin' ) } { description }{ ' ' } - { ! isSetup ? descriptionSuffix : '' } - </> - ); - }; - - const onActivate = () => { - setPluginState( { status: '' } ); - pluginState - .activate() - .then( () => setPluginState( { status: 'page-reload' } ) ) - .finally( () => { - window.location.reload(); - } ); - }; - - const onInstall = () => { - setPluginState( { status: '' } ); - pluginState.install().then( update => setPluginState( update ) ); - }; - - return ( - <ActionCard - title={ `${ title }${ subTitle ? `: ${ subTitle }` : '' }` } - description={ getDescription } - actionText={ - ! isSetup - ? pluginConnectButton( { - isSetup, - isActive, - isLoading, - isInstalled, - onActivate, - onInstall, - ...pluginState, - } ) - : null - } - disabled={ isLoading } - checkbox={ ! isSetup || isLoading ? 'unchecked' : 'checked' } - isMedium - /> - ); -}; - -const Plugins = ( { setError } ) => { - return ( - <> - { Object.entries( PLUGINS ).map( ( [ slug, plugin ] ) => { - return ( - ! Boolean( plugin.hidden ) && ( - <Plugin key={ slug } plugin={ plugin } setError={ setError } /> - ) - ); - } ) } - </> - ); -}; - -export default Plugins; diff --git a/src/wizards/connections/views/main/recaptcha.js b/src/wizards/connections/views/main/recaptcha.js deleted file mode 100644 index ad55e0b9d8..0000000000 --- a/src/wizards/connections/views/main/recaptcha.js +++ /dev/null @@ -1,218 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { BaseControl, ExternalLink } from '@wordpress/components'; -import { useEffect, useState } from '@wordpress/element'; -import apiFetch from '@wordpress/api-fetch'; - -/** - * Internal dependencies - */ -import { - ActionCard, - Button, - Grid, - Notice, - SectionHeader, - SelectControl, - TextControl, -} from '../../../../components/src'; - -const Recaptcha = () => { - const [ error, setError ] = useState( null ); - const [ isLoading, setIsLoading ] = useState( false ); - const [ settings, setSettings ] = useState( {} ); - const [ settingsToUpdate, setSettingsToUpdate ] = useState( {} ); - const credentials = settingsToUpdate?.credentials || {}; - const versionCredentials = credentials[ settingsToUpdate?.version ]; - - // Check the reCAPTCHA connectivity status. - useEffect( () => { - const fetchSettings = async () => { - setIsLoading( true ); - try { - const fetchedSettings = await apiFetch( { path: '/newspack/v1/recaptcha' } ); - setSettings( fetchedSettings ); - setSettingsToUpdate( fetchedSettings ); - } catch ( e ) { - setError( e.message || __( 'Error fetching settings.', 'newspack-plugin' ) ); - } finally { - setIsLoading( false ); - } - }; - fetchSettings(); - }, [] ); - - // Clear out site key + secret if changing the version. - useEffect( () => { - if ( settingsToUpdate?.version !== settings?.version ) { - const newCredentials = versionCredentials || {}; - if ( ! newCredentials.site_key || ! newCredentials.site_secret ) { - setError( - __( - 'Your site key and secret must match the selected reCAPTCHA version. Please enter new credentials.', - 'newspack-plugin' - ) - ); - } - } - }, [ settingsToUpdate?.version ] ); - - const updateSettings = async data => { - setError( null ); - setIsLoading( true ); - try { - const newSettings = await apiFetch( { - path: '/newspack/v1/recaptcha', - method: 'POST', - data, - } ); - setSettings( newSettings ); - setSettingsToUpdate( newSettings ); - } catch ( e ) { - setError( e?.message || __( 'Error updating settings.', 'newspack-plugin' ) ); - } finally { - setIsLoading( false ); - } - }; - - const isV3 = 'v3' === settingsToUpdate?.version; - const hasRequiredSettings = versionCredentials - ? versionCredentials.site_key && versionCredentials.site_secret - : false; - - return ( - <> - <SectionHeader id="recaptcha" title={ __( 'reCAPTCHA', 'newspack-plugin' ) } /> - <ActionCard - isMedium - title={ __( 'Use reCAPTCHA', 'newspack-plugin' ) } - description={ () => ( - <> - { __( - 'Enabling reCAPTCHA can help protect your site against bot attacks and credit card testing.', - 'newspack-plugin' - ) }{ ' ' } - <ExternalLink href="https://www.google.com/recaptcha/admin/create"> - { __( 'Get started', 'newspack-plugin' ) } - </ExternalLink> - </> - ) } - hasGreyHeader={ !! settings.use_captcha } - toggleChecked={ !! settings.use_captcha } - toggleOnChange={ () => updateSettings( { use_captcha: ! settings.use_captcha } ) } - actionContent={ - settings.use_captcha && ( - <Button - variant="primary" - disabled={ isLoading || ! Object.keys( settingsToUpdate ).length } - onClick={ () => updateSettings( settingsToUpdate ) } - > - { __( 'Save Settings', 'newspack-plugin' ) } - </Button> - ) - } - disabled={ isLoading } - > - { settings.use_captcha && ( - <> - { error && <Notice isError noticeText={ error } /> } - { ! hasRequiredSettings && ( - <Notice - noticeText={ __( - 'You must enter a valid site key and secret to use reCAPTCHA.', - 'newspack-plugin' - ) } - /> - ) } - <Grid noMargin rowGap={ 16 }> - <BaseControl - id="recaptcha-version" - label={ __( 'reCAPTCHA Version', 'newspack-plugin' ) } - help={ - <ExternalLink href="https://developers.google.com/recaptcha/docs/versions"> - { __( 'Learn more about reCAPTCHA versions', 'newspack-plugin' ) } - </ExternalLink> - } - > - <SelectControl - label={ __( 'reCAPTCHA Version', 'newspack-plugin' ) } - hideLabelFromVision - value={ settingsToUpdate?.version || 'v3' } - onChange={ value => - setSettingsToUpdate( { ...settingsToUpdate, version: value } ) - } - // Note: add 'v2_checkbox' here and in Recaptcha::SUPPORTED_VERSIONS to add support for the Checkbox flavor of reCAPTCHA v2. - options={ [ - { value: 'v3', label: __( 'Score based (v3)', 'newspack-plugin' ) }, - { - value: 'v2_invisible', - label: __( 'Challenge (v2) - invisible reCAPTCHA badge', 'newspack-plugin' ), - }, - ] } - /> - </BaseControl> - </Grid> - <Grid noMargin rowGap={ 16 }> - <TextControl - value={ versionCredentials?.site_key || '' } - label={ __( 'Site Key', 'newspack-plugin' ) } - onChange={ value => { - const newSettings = { ...settingsToUpdate }; - const newCredentials = { ...credentials }; - newCredentials[ newSettings.version ] = - newCredentials[ newSettings.version ] || {}; - newCredentials[ newSettings.version ].site_key = value; - newSettings.credentials = newCredentials; - - setSettingsToUpdate( newSettings ); - } } - disabled={ isLoading } - autoComplete="off" - /> - <TextControl - type="password" - value={ versionCredentials?.site_secret || '' } - label={ __( 'Site Secret', 'newspack-plugin' ) } - onChange={ value => { - const newSettings = { ...settingsToUpdate }; - const newCredentials = { ...credentials }; - newCredentials[ newSettings.version ] = - newCredentials[ newSettings.version ] || {}; - newCredentials[ newSettings.version ].site_secret = value; - newSettings.credentials = newCredentials; - - setSettingsToUpdate( newSettings ); - } } - disabled={ isLoading } - autoComplete="off" - /> - { isV3 && ( - <TextControl - type="number" - step="0.05" - min="0" - max="1" - value={ parseFloat( settingsToUpdate?.threshold || '' ) } - label={ __( 'Threshold', 'newspack-plugin' ) } - onChange={ value => - setSettingsToUpdate( { ...settingsToUpdate, threshold: value } ) - } - disabled={ isLoading } - help={ - <ExternalLink href="https://developers.google.com/recaptcha/docs/v3#interpreting_the_score"> - { __( 'Learn more about the threshold value', 'newspack-plugin' ) } - </ExternalLink> - } - /> - ) } - </Grid> - </> - ) } - </ActionCard> - </> - ); -}; - -export default Recaptcha; diff --git a/src/wizards/connections/views/main/webhooks.js b/src/wizards/connections/views/main/webhooks.js deleted file mode 100644 index c37fd6e7a7..0000000000 --- a/src/wizards/connections/views/main/webhooks.js +++ /dev/null @@ -1,591 +0,0 @@ -/** - * External dependencies - */ -import moment from 'moment'; - -/** - * WordPress dependencies - */ -import { sprintf, __ } from '@wordpress/i18n'; -import { CheckboxControl, MenuItem } from '@wordpress/components'; -import { useEffect, useState, useRef } from '@wordpress/element'; -import apiFetch from '@wordpress/api-fetch'; -import { Icon, settings, check, close, reusableBlock, moreVertical } from '@wordpress/icons'; -import { ESCAPE } from '@wordpress/keycodes'; - -/** - * Internal dependencies - */ -import { - Card, - ActionCard, - Button, - Grid, - Notice, - SectionHeader, - Modal, - TextControl, - Popover, -} from '../../../../components/src'; - -const getDisplayUrl = url => { - let displayUrl = url.slice( 8 ); - if ( url.length > 45 ) { - displayUrl = `${ url.slice( 8, 38 ) }...${ url.slice( -10 ) }`; - } - return displayUrl; -}; - -const getEndpointLabel = endpoint => { - const { label, url } = endpoint; - return label || getDisplayUrl( url ); -}; - -const getEndpointTitle = endpoint => { - const { label, url } = endpoint; - return ( - <> - { label && <span className="newspack-webhooks__endpoint__label">{ label }</span> } - <span className="newspack-webhooks__endpoint__url">{ getDisplayUrl( url ) }</span> - </> - ); -}; - -const getRequestStatusIcon = status => { - const icons = { - pending: reusableBlock, - finished: check, - killed: close, - }; - return icons[ status ] || settings; -}; - -const hasEndpointErrors = endpoint => { - return endpoint.requests.some( request => request.errors.length ); -}; - -const EndpointActions = ( { - disabled, - position = 'bottom left', - isSystem, - onEdit = () => {}, - onDelete = () => {}, - onView = () => {}, -} ) => { - const [ popoverVisible, setPopoverVisible ] = useState( false ); - useEffect( () => { - setPopoverVisible( false ); - }, [ disabled ] ); - return ( - <> - <Button - className={ popoverVisible && 'popover-active' } - onClick={ () => setPopoverVisible( ! popoverVisible ) } - icon={ moreVertical } - disabled={ disabled } - label={ __( 'Endpoint Actions', 'newspack-plugin' ) } - tooltipPosition={ position } - /> - { popoverVisible && ( - <Popover - position={ position } - onFocusOutside={ () => setPopoverVisible( false ) } - onKeyDown={ event => ESCAPE === event.keyCode && setPopoverVisible( false ) } - > - <MenuItem onClick={ () => setPopoverVisible( false ) } className="screen-reader-text"> - { __( 'Close Endpoint Actions', 'newspack-plugin' ) } - </MenuItem> - <MenuItem onClick={ onView } className="newspack-button"> - { __( 'View Requests', 'newspack-plugin' ) } - </MenuItem> - { ! isSystem && ( - <MenuItem onClick={ onEdit } className="newspack-button"> - { __( 'Edit', 'newspack-plugin' ) } - </MenuItem> - ) } - { ! isSystem && ( - <MenuItem onClick={ onDelete } className="newspack-button" isDestructive> - { __( 'Remove', 'newspack-plugin' ) } - </MenuItem> - ) } - </Popover> - ) } - </> - ); -}; - -const ConfirmationModal = ( { disabled, onConfirm, onClose, title, description } ) => { - return ( - <Modal title={ title } onRequestClose={ onClose }> - <p>{ description }</p> - <Card buttonsCard noBorder className="justify-end"> - <Button isSecondary onClick={ onClose } disabled={ disabled }> - { __( 'Cancel', 'newspack-plugin' ) } - </Button> - <Button isPrimary onClick={ onConfirm } disabled={ disabled }> - { __( 'Confirm', 'newspack-plugin' ) } - </Button> - </Card> - </Modal> - ); -}; - -const Webhooks = () => { - const [ inFlight, setInFlight ] = useState( false ); - const [ error, setError ] = useState( false ); - - const [ actions, setActions ] = useState( [] ); - const fetchActions = () => { - apiFetch( { path: '/newspack/v1/data-events/actions' } ) - .then( response => { - setActions( response ); - } ) - .catch( err => { - setError( err ); - } ); - }; - - const [ endpoints, setEndpoints ] = useState( [] ); - const [ deleting, setDeleting ] = useState( false ); - const [ toggling, setToggling ] = useState( false ); - const [ viewing, setViewing ] = useState( false ); - const [ editing, setEditing ] = useState( false ); - const [ editingError, setEditingError ] = useState( false ); - - const modalRef = useRef( null ); - - const fetchEndpoints = () => { - setInFlight( true ); - apiFetch( { path: '/newspack/v1/webhooks/endpoints' } ) - .then( response => { - setEndpoints( response ); - } ) - .catch( err => { - setError( err ); - } ) - .finally( () => { - setInFlight( false ); - } ); - }; - const toggleEndpoint = endpoint => { - setInFlight( true ); - apiFetch( { - path: `/newspack/v1/webhooks/endpoints/${ endpoint.id }`, - method: 'POST', - data: { disabled: ! endpoint.disabled }, - } ) - .then( response => { - setEndpoints( response ); - } ) - .catch( err => { - setError( err ); - } ) - .finally( () => { - setInFlight( false ); - setToggling( false ); - } ); - }; - const deleteEndpoint = endpoint => { - setInFlight( true ); - apiFetch( { - path: `/newspack/v1/webhooks/endpoints/${ endpoint.id }`, - method: 'DELETE', - } ) - .then( response => { - setEndpoints( response ); - } ) - .catch( err => { - setError( err ); - } ) - .finally( () => { - setInFlight( false ); - setDeleting( false ); - } ); - }; - const validateEndpoint = endpoint => { - const errors = []; - if ( ! endpoint.url ) { - errors.push( __( 'URL is required.', 'newspack-plugin' ) ); - } - if ( ! endpoint.actions || ! endpoint.actions.length ) { - errors.push( __( 'At least one action is required.', 'newspack-plugin' ) ); - } - if ( errors.length ) { - setEditingError( { message: errors.join( ' ' ) } ); - } else { - setEditingError( false ); - } - return errors; - } - const upsertEndpoint = endpoint => { - const errors = validateEndpoint( endpoint ); - if ( errors.length ) { - return; - } - setInFlight( true ); - apiFetch( { - path: `/newspack/v1/webhooks/endpoints/${ endpoint.id || '' }`, - method: 'POST', - data: endpoint, - } ) - .then( response => { - setEndpoints( response ); - setEditing( false ); - } ) - .catch( err => { - setEditingError( err ); - } ) - .finally( () => { - setInFlight( false ); - } ); - }; - - const [ testResponse, setTestResponse ] = useState( false ); - const [ testError, setTestError ] = useState( false ); - const sendTestRequest = ( url, bearer_token ) => { - setInFlight( true ); - setTestError( false ); - setTestResponse( false ); - apiFetch( { - path: '/newspack/v1/webhooks/endpoints/test', - method: 'POST', - data: { url, bearer_token }, - } ) - .then( response => { - setTestResponse( response ); - } ) - .catch( err => { - setTestError( err ); - } ) - .finally( () => { - setInFlight( false ); - } ); - }; - - useEffect( fetchActions, [] ); - useEffect( fetchEndpoints, [] ); - - useEffect( () => { - setTestResponse( false ); - setEditingError( false ); - setTestError( false ); - }, [ editing ] ); - - useEffect( () => { - if ( editingError ) { - modalRef?.current?.querySelector('.components-modal__content')?.scrollTo( { top: 0, left: 0, behavior: 'smooth' } ); - } - }, [ editingError ] ); - - return ( - <Card noBorder className="mt64"> - { false !== error && <Notice isError noticeText={ error.message } /> } - - <div className="flex justify-between items-end"> - <SectionHeader - title={ __( 'Webhook Endpoints', 'newspack-plugin' ) } - description={ __( - 'Register webhook endpoints to integrate reader activity data to third-party services or private APIs', - 'newspack-plugin' - ) } - noMargin - /> - <Button - variant="primary" - onClick={ () => setEditing( {} ) } - disabled={ inFlight } - > - { __( 'Add New Endpoint', 'newspack-plugin' ) } - </Button> - </div> - - { endpoints.length > 0 && ( - <> - { endpoints.map( endpoint => ( - <ActionCard - isMedium - className="newspack-webhooks__endpoint mt16" - toggleChecked={ ! endpoint.disabled } - toggleOnChange={ () => setToggling( endpoint ) } - key={ endpoint.id } - title={ getEndpointTitle( endpoint ) } - disabled={ endpoint.system } - description={ () => { - if ( endpoint.disabled && endpoint.disabled_error ) { - return ( - __( - 'This endpoint is disabled due to excessive request errors', - 'newspack-plugin' - ) + - ': ' + - endpoint.disabled_error - ); - } - return ( - <> - { __( 'Actions:', 'newspack-plugin' ) }{ ' ' } - { endpoint.actions.map( action => ( - <span key={ action } className="newspack-webhooks__endpoint__action"> - { action } - </span> ) ) } - </> - ); - } } - actionText={ - <EndpointActions - onEdit={ () => setEditing( endpoint ) } - onDelete={ () => setDeleting( endpoint ) } - onView={ () => setViewing( endpoint ) } - isSystem={ endpoint.system } - /> - } - /> - ) ) } - </> - ) } - { false !== deleting && ( - <ConfirmationModal - title={ __( 'Remove Endpoint', 'newspack-plugin' ) } - description={ sprintf( - /* translators: %s: endpoint title */ - __( 'Are you sure you want to remove the endpoint %s?', 'newspack-plugin' ), - `"${ getDisplayUrl( deleting.url ) }"` - ) } - onClose={ () => setDeleting( false ) } - onConfirm={ () => deleteEndpoint( deleting ) } - disabled={ inFlight } - /> - ) } - { false !== toggling && ( - <ConfirmationModal - title={ - toggling.disabled - ? __( 'Enable Endpoint', 'newspack-plugin' ) - : __( 'Disable Endpoint', 'newspack-plugin' ) - } - description={ - toggling.disabled - ? sprintf( - /* translators: %s: endpoint title */ - __( 'Are you sure you want to enable the endpoint %s?', 'newspack-plugin' ), - `"${ getDisplayUrl( toggling.url ) }"` - ) : sprintf( - /* translators: %s: endpoint title */ - __( 'Are you sure you want to disable the endpoint %s?', 'newspack-plugin' ), - `"${ getDisplayUrl( toggling.url ) }"` - ) - } - endpoint={ toggling } - onClose={ () => setToggling( false ) } - onConfirm={ () => toggleEndpoint( toggling ) } - disabled={ inFlight } - /> - ) } - { false !== viewing && ( - <Modal - title={ __( 'Latest Requests', 'newspack-plugin' ) } - onRequestClose={ () => setViewing( false ) } - > - <p> - { sprintf( - // translators: %s is the endpoint title (shortened URL). - __( 'Most recent requests for %s', 'newspack-plugin' ), - getEndpointLabel( viewing ) - ) } - </p> - { viewing.requests.length > 0 ? ( - <table - className={ `newspack-webhooks__requests ${ - hasEndpointErrors( viewing ) ? 'has-error' : '' - }` } - > - <tr> - <th /> - <th colSpan="2">{ __( 'Action', 'newspack-plugin' ) }</th> - { hasEndpointErrors( viewing ) && ( - <th colSpan="2">{ __( 'Error', 'newspack-plugin' ) }</th> - ) } - </tr> - { viewing.requests.map( request => ( - <tr key={ request.id }> - <td className={ `status status--${ request.status }` }> - <Icon icon={ getRequestStatusIcon( request.status ) } /> - </td> - <td className="action-name">{ request.action_name }</td> - <td className="scheduled"> - { 'pending' === request.status - ? sprintf( - // translators: %s is a human-readable time difference. - __( 'sending in %s', 'newspack-plugin' ), - moment( parseInt( request.scheduled ) * 1000 ).fromNow( true ) - ) - : sprintf( - // translators: %s is a human-readable time difference. - __( 'processed %s', 'newspack-plugin' ), - moment( parseInt( request.scheduled ) * 1000 ).fromNow() - ) } - </td> - { hasEndpointErrors( viewing ) && ( - <> - <td className="error"> - { request.errors && request.errors.length > 0 - ? request.errors[ request.errors.length - 1 ] - : '--' } - </td> - <td> - <span className="error-count"> - { sprintf( - // translators: %s is the number of errors. - __( 'Attempt #%s', 'newspack-plugin' ), - request.errors.length - ) } - </span> - </td> - </> - ) } - </tr> - ) ) } - </table> - ) : ( - <Notice - noticeText={ __( - "This endpoint hasn't received any requests yet.", - 'newspack-plugin' - ) } - /> - ) } - </Modal> - ) } - { false !== editing && ( - <Modal - ref={ modalRef } - title={ __( 'Webhook Endpoint', 'newspack-plugin' ) } - onRequestClose={ () => { - setEditing( false ); - setEditingError( false ); - } } - > - { false !== editingError && <Notice isError noticeText={ editingError.message } /> } - { true === editing.disabled && ( - <Notice - noticeText={ __( 'This webhook endpoint is currently disabled.', 'newspack-plugin' ) } - className="mt0" - /> - ) } - { editing.disabled && editing.disabled_error && ( - <Notice - isError - noticeText={ __( 'Request Error: ', 'newspack-plugin' ) + editing.disabled_error } - className="mt0" - /> - ) } - { testError && ( - <Notice - isError - noticeText={ __( 'Test Error: ', 'newspack-plugin' ) + testError.message } - className="mt0" - /> - ) } - <Grid columns={ 1 } gutter={ 16 } className="mt0"> - <TextControl - label={ __( 'URL', 'newspack-plugin' ) } - help={ __( - "The URL to send requests to. It's required for the URL to be under a valid TLS/SSL certificate. You can use the test button below to verify the endpoint response.", - 'newspack-plugin' - ) } - className="code" - value={ editing.url } - onChange={ value => setEditing( { ...editing, url: value } ) } - disabled={ inFlight } - /> - <TextControl - label={ __( 'Authentication token (optional)', 'newspack-plugin' ) } - help={ __( - 'If your endpoint requires a token authentication, enter it here. It will be sent as a Bearer token in the Authorization header.', - 'newspack-plugin' - ) } - value={ editing.bearer_token } - onChange={ value => setEditing( { ...editing, bearer_token: value } ) } - disabled={ inFlight } - /> - <Card buttonsCard noBorder className="justify-end"> - { testResponse && ( - <div - className={ `newspack-webhooks__test-response status--${ - testResponse.success ? 'success' : 'error' - }` } - > - <span className="message">{ testResponse.message }</span> - <span className="code">{ testResponse.code }</span> - </div> - ) } - <Button - isSecondary - onClick={ () => sendTestRequest( editing.url, editing.bearer_token ) } - disabled={ inFlight || ! editing.url } - > - { __( 'Send a test request', 'newspack-plugin' ) } - </Button> - </Card> - </Grid> - <hr /> - <TextControl - label={ __( 'Label (optional)', 'newspack-plugin' ) } - help={ __( - 'A label to help you identify this endpoint. It will not be sent to the endpoint.', - 'newspack-plugin' - ) } - value={ editing.label } - onChange={ value => setEditing( { ...editing, label: value } ) } - disabled={ inFlight } - /> - <Grid columns={ 1 } gutter={ 16 }> - <h3>{ __( 'Actions', 'newspack-plugin' ) }</h3> - { actions.length > 0 && ( - <> - <p> - { __( - 'Select which actions should trigger this endpoint:', - 'newspack-plugin' - ) } - </p> - <Grid columns={ 2 } gutter={ 16 }> - { actions.map( ( action, i ) => ( - <CheckboxControl - key={ i } - disabled={ inFlight } - label={ action } - checked={ ( editing.actions && editing.actions.includes( action ) ) || false } - onChange={ () => { - const currentActions = editing.actions || []; - if ( currentActions.includes( action ) ) { - currentActions.splice( currentActions.indexOf( action ), 1 ); - } else { - currentActions.push( action ); - } - setEditing( { ...editing, actions: currentActions } ); - } } - /> - ) ) } - </Grid> - </> - ) } - <Card buttonsCard noBorder className="justify-end"> - <Button - isPrimary - onClick={ () => { - upsertEndpoint( editing ); - } } - disabled={ inFlight } - > - { __( 'Save', 'newspack-plugin' ) } - </Button> - </Card> - </Grid> - </Modal> - ) } - </Card> - ); -}; - -export default Webhooks; diff --git a/src/wizards/dashboard/index.js b/src/wizards/dashboard/index.js deleted file mode 100644 index 750c9bc17a..0000000000 --- a/src/wizards/dashboard/index.js +++ /dev/null @@ -1,41 +0,0 @@ -/* global newspack_dashboard */ - -import '../../shared/js/public-path'; - -/** - * WordPress dependencies. - */ -import { Fragment, render } from '@wordpress/element'; - -/** - * Internal dependencies. - */ -import { GlobalNotices, Footer, Grid, NewspackIcon, Notice } from '../../components/src'; -import DashboardCard from './views/dashboardCard'; -import './style.scss'; - -const Dashboard = ( { items } ) => { - return ( - <Fragment> - <GlobalNotices /> - { newspack_aux_data.is_debug_mode && <Notice debugMode /> } - <div className="newspack-wizard__header"> - <div className="newspack-wizard__header__inner"> - <div className="newspack-wizard__title"> - <NewspackIcon size={ 36 } /> - </div> - </div> - </div> - - <div className="newspack-wizard newspack-wizard__content"> - <Grid columns={ 3 } gutter={ 32 }> - { items.map( card => ( - <DashboardCard { ...card } key={ card.slug } /> - ) ) } - </Grid> - </div> - <Footer /> - </Fragment> - ); -}; -render( <Dashboard items={ newspack_dashboard } />, document.getElementById( 'newspack' ) ); diff --git a/src/wizards/dashboard/style.scss b/src/wizards/dashboard/style.scss deleted file mode 100644 index be06307d3c..0000000000 --- a/src/wizards/dashboard/style.scss +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Dashboard - */ - -@use "~@wordpress/base-styles/colors" as wp-colors; -@use "../../shared/scss/colors"; - -.newspack-dashboard-card { - align-items: center; - border-color: wp-colors.$gray-300; - display: grid; - grid-gap: 16px; - grid-template-columns: 48px auto; - margin: 0; - padding: 32px; - - h2, - p { - margin: 0; - } - - svg { - fill: colors.$primary-500; - } -} diff --git a/src/wizards/dashboard/views/dashboardCard.js b/src/wizards/dashboard/views/dashboardCard.js deleted file mode 100644 index f4ae10dcfb..0000000000 --- a/src/wizards/dashboard/views/dashboardCard.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Dashboard Card - */ - -/** - * WordPress dependencies. - */ -import * as icons from '@wordpress/icons'; - -/** - * Internal dependencies. - */ -import { ButtonCard } from '../../../components/src'; - -const ICON_MAP = { - 'site-design': icons.typography, - 'reader-revenue': icons.payment, - advertising: icons.stretchWide, - syndication: icons.rss, - analytics: icons.chartBar, - seo: icons.search, - 'health-check': icons.lifesaver, - engagement: icons.postComments, - popups: icons.megaphone, - support: icons.help, - connections: icons.plugins, -}; -const DashboardCard = ( { name, description, slug, url, is_external } ) => ( - <ButtonCard - href={ url } - { ...( is_external && { target: '_blank' } ) } - title={ name } - desc={ description } - icon={ ICON_MAP[ slug.replace( /newspack-(.*)-wizard/, '$1' ) ] || icons.plugins } - /> -); -export default DashboardCard; diff --git a/src/wizards/engagement/components/prerequisite.tsx b/src/wizards/engagement/components/prerequisite.tsx deleted file mode 100644 index b11c25be77..0000000000 --- a/src/wizards/engagement/components/prerequisite.tsx +++ /dev/null @@ -1,188 +0,0 @@ -/** - * WordPress dependencies - */ -import { __, sprintf } from '@wordpress/i18n'; -import { ExternalLink } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { PrequisiteProps } from './types'; -import { ActionCard, Button, Grid, TextControl } from '../../../components/src'; -import { HANDOFF_KEY } from '../../../components/src/consts'; -import type { Config, ConfigKey } from './types'; - -/** - * Expandable ActionCard for RAS prerequisites checklist. - */ -export default function Prerequisite( { - config, - getSharedProps, - inFlight, - prerequisite, - saveConfig, -}: PrequisiteProps ) { - const { href } = prerequisite; - const isValid = Boolean( prerequisite.active || prerequisite.is_skipped ); - - // If the prerequisite is active but has empty fields, show a warning. - const hasEmptyFields = () => { - if ( isValid && prerequisite.fields && prerequisite.warning ) { - const emptyValues = Object.keys( prerequisite.fields ).filter( - fieldName => '' === config[ fieldName as keyof Config ] - ); - if ( emptyValues.length ) { - return prerequisite.warning; - } - } - return null; - }; - - const fieldKeys = Object.keys( prerequisite.fields || {} ) as ConfigKey[]; - - const renderInnerContent = () => ( - // Inner card content. - <> - { prerequisite.description && ( - <p> - { prerequisite.description } - { prerequisite.help_url && ( - <> - { ' ' } - <ExternalLink href={ prerequisite.help_url }> - { __( 'Learn more', 'newspack-plugin' ) } - </ExternalLink> - </> - ) } - </p> - ) } - { - // Form fields. - prerequisite.fields && ( - <Grid columns={ 2 } gutter={ 16 }> - <div> - { fieldKeys.map( fieldName => { - if ( ! prerequisite.fields || ! prerequisite.fields[ fieldName ] ) { - return undefined; - } - return ( - <TextControl - key={ fieldName } - label={ prerequisite.fields[ fieldName ].label } - help={ prerequisite.fields[ fieldName ].description } - { ...getSharedProps( fieldName, 'text' ) } - /> - ); - } ) } - - <Button - variant={ 'primary' } - onClick={ () => { - const dataToSave: Partial< Config > = {}; - fieldKeys.forEach( fieldName => { - if ( config[ fieldName ] ) { - // @ts-ignore - not sure what's the issue here. - dataToSave[ fieldName ] = config[ fieldName ]; - } - } ); - saveConfig( dataToSave ); - } } - disabled={ inFlight } - > - { inFlight - ? __( 'Saving…', 'newspack-plugin' ) - : sprintf( - // Translators: Save or Update settings. - __( '%s settings', 'newspack-plugin' ), - isValid ? __( 'Update', 'newspack-plugin' ) : __( 'Save', 'newspack-plugin' ) - ) } - </Button> - </div> - </Grid> - ) - } - { - // Link to another settings page or update config in place. - href && prerequisite.action_text && ( - <Grid columns={ 2 } gutter={ 16 }> - <div> - { ( ! prerequisite.hasOwnProperty( 'action_enabled' ) || - prerequisite.action_enabled ) && ( - <Button - variant={ 'primary' } - onClick={ () => { - // Set up a handoff to indicate that the user is coming from the RAS wizard page. - if ( prerequisite.instructions ) { - window.localStorage.setItem( - HANDOFF_KEY, - JSON.stringify( { - message: sprintf( - // Translators: %s is specific instructions for satisfying the prerequisite. - __( - '%1$s%2$sReturn to the Reader Activation page to complete the settings and activate%3$s.', - 'newspack-plugin' - ), - prerequisite.instructions + ' ', - window.newspack_engagement_wizard?.reader_activation_url - ? `<a href="${ window.newspack_engagement_wizard.reader_activation_url }">` - : '', - window.newspack_engagement_wizard?.reader_activation_url ? '</a>' : '' - ), - url: href, - } ) - ); - } - - window.location.href = href; - } } - > - { /* eslint-disable no-nested-ternary */ } - { ( isValid - ? __( 'Update ', 'newspack-plugin' ) - : prerequisite.fields - ? __( 'Save ', 'newspack-plugin' ) - : __( 'Configure ', 'newspack-plugin' ) ) + prerequisite.action_text } - </Button> - ) } - { prerequisite.hasOwnProperty( 'action_enabled' ) && ! prerequisite.action_enabled && ( - <Button variant={ 'secondary' } disabled> - { prerequisite.disabled_text || prerequisite.action_text } - </Button> - ) } - </div> - </Grid> - ) - } - </> - ); - - let status = __( 'Pending', 'newspack-plugin' ); - if ( isValid ) { - status = `${ __( 'Ready', 'newspack-plugin' ) } ${ - prerequisite.is_skipped ? `(${ __( 'Skipped', 'newspack-plugin' ) })` : '' - }`; - } - if ( prerequisite.is_unavailable ) { - status = __( 'Unavailable', 'newspack-plugin' ); - } - - return ( - <ActionCard - className="newspack-ras-wizard__prerequisite" - isMedium - expandable={ ! prerequisite.is_unavailable } - collapse={ isValid } - title={ prerequisite.label } - description={ sprintf( - /* translators: %s: Prerequisite status */ - __( 'Status: %s', 'newspack-plugin' ), - status - ) } - checkbox={ isValid ? 'checked' : 'unchecked' } - notificationLevel="info" - notification={ hasEmptyFields() } - > - { prerequisite.is_unavailable ? null : renderInnerContent() } - </ActionCard> - ); -} diff --git a/src/wizards/engagement/components/prompt.tsx b/src/wizards/engagement/components/prompt.tsx deleted file mode 100644 index f51a2e27b7..0000000000 --- a/src/wizards/engagement/components/prompt.tsx +++ /dev/null @@ -1,373 +0,0 @@ -/* eslint-disable no-nested-ternary */ - -/** - * WordPress dependencies - */ -import { __, sprintf } from '@wordpress/i18n'; -import { - BaseControl, - CheckboxControl, - ExternalLink, - Path, - SVG, - TextareaControl, -} from '@wordpress/components'; -import apiFetch from '@wordpress/api-fetch'; -import { Fragment, useEffect, useState } from '@wordpress/element'; - -/** - * External dependencies - */ -import { stringify } from 'qs'; - -/** - * Internal dependencies - */ -import { - Attachment, - InputField, - InputValues, - PromptOptions, - PromptProps, - PromptType, - PromptOptionsBaseKey, -} from './types'; -import { - ActionCard, - Button, - Grid, - ImageUpload, - Notice, - TextControl, - WebPreview, - hooks, -} from '../../../components/src'; - -// Note: Schema and types for the `prompt` prop is defined in Newspack Campaigns: https://github.com/Automattic/newspack-popups/blob/trunk/includes/schemas/class-prompts.php -export default function Prompt( { inFlight, prompt, setInFlight, setPrompts }: PromptProps ) { - const [ values, setValues ] = useState< InputValues | Record< string, never > >( {} ); - const [ error, setError ] = useState< false | { message: string } >( false ); - const [ isDirty, setIsDirty ] = useState< boolean >( false ); - const [ success, setSuccess ] = useState< false | string >( false ); - const [ image, setImage ] = useState< null | Attachment >( null ); - const [ isSavingFromPreview, setIsSavingFromPreview ] = useState( false ); - - useEffect( () => { - if ( Array.isArray( prompt?.user_input_fields ) ) { - const fields = { ...values }; - prompt.user_input_fields.forEach( ( field: InputField ) => { - fields[ field.name ] = field.value || field.default; - } ); - setValues( fields ); - } - - if ( prompt.featured_image_id ) { - setInFlight( true ); - apiFetch< Attachment >( { - path: `/wp/v2/media/${ prompt.featured_image_id }`, - } ) - .then( ( attachment: Attachment ) => { - if ( attachment?.source_url || attachment?.url ) { - setImage( { url: attachment.source_url || attachment.url } ); - } - } ) - .catch( setError ) - .finally( () => { - setInFlight( false ); - } ); - } - }, [ prompt ] ); - - // Clear success message after a few seconds. - useEffect( () => { - setTimeout( () => setSuccess( false ), 5000 ); - }, [ success ] ); - - const previewIcon = ( - <SVG xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"> - <Path - fillRule="evenodd" - clipRule="evenodd" - d="M4.5001 13C5.17092 13.3354 5.17078 13.3357 5.17066 13.3359L5.17346 13.3305C5.1767 13.3242 5.18233 13.3135 5.19036 13.2985C5.20643 13.2686 5.23209 13.2218 5.26744 13.1608C5.33819 13.0385 5.44741 12.8592 5.59589 12.6419C5.89361 12.2062 6.34485 11.624 6.95484 11.0431C8.17357 9.88241 9.99767 8.75 12.5001 8.75C15.0025 8.75 16.8266 9.88241 18.0454 11.0431C18.6554 11.624 19.1066 12.2062 19.4043 12.6419C19.5528 12.8592 19.662 13.0385 19.7328 13.1608C19.7681 13.2218 19.7938 13.2686 19.8098 13.2985C19.8179 13.3135 19.8235 13.3242 19.8267 13.3305L19.8295 13.3359C19.8294 13.3357 19.8293 13.3354 20.5001 13C21.1709 12.6646 21.1708 12.6643 21.1706 12.664L21.1702 12.6632L21.1693 12.6614L21.1667 12.6563L21.1588 12.6408C21.1522 12.6282 21.1431 12.6108 21.1315 12.5892C21.1083 12.5459 21.0749 12.4852 21.0311 12.4096C20.9437 12.2584 20.8146 12.0471 20.6428 11.7956C20.2999 11.2938 19.7823 10.626 19.0798 9.9569C17.6736 8.61759 15.4977 7.25 12.5001 7.25C9.50252 7.25 7.32663 8.61759 5.92036 9.9569C5.21785 10.626 4.70033 11.2938 4.35743 11.7956C4.1856 12.0471 4.05654 12.2584 3.96909 12.4096C3.92533 12.4852 3.89191 12.5459 3.86867 12.5892C3.85705 12.6108 3.84797 12.6282 3.84141 12.6408L3.83346 12.6563L3.8309 12.6614L3.82997 12.6632L3.82959 12.664C3.82943 12.6643 3.82928 12.6646 4.5001 13ZM12.5001 16C14.4331 16 16.0001 14.433 16.0001 12.5C16.0001 10.567 14.4331 9 12.5001 9C10.5671 9 9.0001 10.567 9.0001 12.5C9.0001 14.433 10.5671 16 12.5001 16Z" - fill={ inFlight ? '#828282' : '#3366FF' } - /> - </SVG> - ); - - const getPreviewUrl = ( { options, slug }: { options: PromptOptions; slug: string } ) => { - const { placement, trigger_type: triggerType } = options; - const previewQueryKeys = window.newspack_engagement_wizard.preview_query_keys; - const abbreviatedKeys = { preset: slug, values }; - const optionsKeys = Object.keys( options ) as Array< PromptOptionsBaseKey >; - optionsKeys.forEach( key => { - if ( previewQueryKeys.hasOwnProperty( key ) ) { - // @ts-ignore To be fixed in the future perhaps. - abbreviatedKeys[ previewQueryKeys[ key ] ] = options[ key ]; - } - } ); - - let previewURL = '/'; - if ( 'archives' === placement && window.newspack_engagement_wizard?.preview_archive ) { - previewURL = window.newspack_engagement_wizard.preview_archive; - } else if ( - ( 'inline' === placement || 'scroll' === triggerType ) && - window && - window.newspack_engagement_wizard?.preview_post - ) { - previewURL = window.newspack_engagement_wizard?.preview_post; - } - - return `${ previewURL }?${ stringify( { ...abbreviatedKeys } ) }`; - }; - - const unblock = hooks.usePrompt( - isDirty, - __( 'You have unsaved changes. Discard changes?', 'newspack-plugin' ) - ); - - const savePrompt = ( slug: string, data: InputValues ) => { - return new Promise< void >( ( res, rej ) => { - if ( unblock ) { - unblock(); - } - setError( false ); - setSuccess( false ); - setInFlight( true ); - apiFetch< [ PromptType ] >( { - path: '/newspack-popups/v1/reader-activation/campaign', - method: 'post', - data: { - slug, - data, - }, - } ) - .then( ( fetchedPrompts: Array< PromptType > ) => { - setPrompts( fetchedPrompts ); - setSuccess( __( 'Prompt saved.', 'newspack-plugin' ) ); - setIsDirty( false ); - res(); - } ) - .catch( err => { - setError( err ); - rej( err ); - } ) - .finally( () => { - setInFlight( false ); - } ); - } ); - }; - - const helpInfo = prompt.help_info || null; - - return ( - <ActionCard - isMedium - expandable - collapse={ prompt.ready && ! isSavingFromPreview } - title={ prompt.title } - description={ sprintf( - // Translators: Status of the prompt. - __( 'Status: %s', 'newspack-plugin' ), - isDirty - ? __( 'Unsaved changes', 'newspack-plugin' ) - : prompt.ready - ? __( 'Ready', 'newspack-plugin' ) - : __( 'Pending', 'newspack-plugin' ) - ) } - checkbox={ prompt.ready && ! isDirty ? 'checked' : 'unchecked' } - > - { - <Grid columns={ 2 } gutter={ 64 } className="newspack-ras-campaign__grid"> - <div className="newspack-ras-campaign__fields"> - { prompt.user_input_fields.map( ( field: InputField ) => ( - // @ts-ignore TS doesn't like Fragments when used in a map function in this way. - <Fragment key={ field.name }> - { 'array' === field.type && Array.isArray( field.options ) && ( - <BaseControl - id={ `newspack-engagement-wizard__${ field.name }` } - label={ field.label } - > - { field.options.map( option => ( - <BaseControl - key={ option.id } - id={ `newspack-engagement-wizard__${ option.id }` } - className="newspack-checkbox-control" - help={ option.description } - > - <CheckboxControl - disabled={ inFlight } - label={ option.label } - value={ option.id } - // @ts-ignore To be fixed in the future perhaps. - checked={ values[ field.name ]?.indexOf( option.id ) > -1 } - onChange={ ( value: boolean ) => { - const toUpdate = { ...values }; - // @ts-ignore To be fixed in the future perhaps. - if ( ! value && toUpdate[ field.name ].indexOf( option.id ) > -1 ) { - // @ts-ignore To be fixed in the future perhaps. - toUpdate[ field.name ].value = toUpdate[ field.name ].splice( - // @ts-ignore To be fixed in the future perhaps. - toUpdate[ field.name ].indexOf( option.id ), - 1 - ); - } - // @ts-ignore To be fixed in the future perhaps. - if ( value && toUpdate[ field.name ].indexOf( option.id ) === -1 ) { - // @ts-ignore To be fixed in the future perhaps. - toUpdate[ field.name ].push( option.id ); - } - setValues( toUpdate ); - setIsDirty( true ); - } } - /> - </BaseControl> - ) ) } - </BaseControl> - ) } - { 'string' === field.type && field.max_length && 150 < field.max_length && ( - <TextareaControl - className="newspack-textarea-control" - label={ field.label } - disabled={ inFlight } - // @ts-ignore To be fixed in the future perhaps. - help={ `${ values[ field.name ]?.length || 0 } / ${ field.max_length }` } - onChange={ ( value: string ) => { - // @ts-ignore There's a check for max_length above. - if ( value.length > field.max_length ) { - return; - } - - const toUpdate = { ...values }; - toUpdate[ field.name ] = value; - setValues( toUpdate ); - setIsDirty( true ); - } } - placeholder={ typeof field.default === 'string' ? field.default : '' } - rows={ 10 } - // @ts-ignore TS still does not see it as a string. - value={ typeof values[ field.name ] === 'string' ? values[ field.name ] : '' } - /> - ) } - { 'string' === field.type && field.max_length && 150 >= field.max_length && ( - <TextControl - label={ field.label } - disabled={ inFlight } - // @ts-ignore To be fixed in the future perhaps. - help={ `${ values[ field.name ]?.length || 0 } / ${ field.max_length }` } - onChange={ ( value: string ) => { - // @ts-ignore There's a check for max_length above. - if ( value.length > field.max_length ) { - return; - } - - const toUpdate = { ...values }; - toUpdate[ field.name ] = value; - setValues( toUpdate ); - setIsDirty( true ); - } } - placeholder={ field.default } - value={ values[ field.name ] || '' } - /> - ) } - { 'int' === field.type && 'featured_image_id' === field.name && ( - <BaseControl - id={ `newspack-engagement-wizard__${ field.name }` } - label={ field.label } - > - <ImageUpload - buttonLabel={ __( 'Select file', 'newspack-plugin' ) } - disabled={ inFlight } - image={ image } - onChange={ ( attachment: Attachment ) => { - const toUpdate = { ...values }; - toUpdate[ field.name ] = attachment?.id || 0; - if ( toUpdate[ field.name ] !== values[ field.name ] ) { - } - setValues( toUpdate ); - setIsDirty( true ); - if ( attachment?.url ) { - setImage( attachment ); - } else { - setImage( null ); - } - } } - /> - </BaseControl> - ) } - </Fragment> - ) ) } - { error && ( - <Notice - noticeText={ error?.message || __( 'Something went wrong.', 'newspack-plugin' ) } - isError - /> - ) } - { success && <Notice noticeText={ success } isSuccess /> } - <div className="newspack-buttons-card"> - <Button - isPrimary - onClick={ () => { - setIsSavingFromPreview( false ); - savePrompt( prompt.slug, values ); - } } - disabled={ inFlight } - > - { inFlight - ? __( 'Saving…', 'newspack-plugin' ) - : sprintf( - // Translators: Save or Update settings. - __( '%s prompt settings', 'newspack-plugin' ), - prompt.ready - ? __( 'Update', 'newspack-plugin' ) - : __( 'Save', 'newspack-plugin' ) - ) } - </Button> - <WebPreview - url={ getPreviewUrl( prompt ) } - renderButton={ ( { showPreview }: { showPreview: () => void } ) => ( - <Button - disabled={ inFlight } - icon={ previewIcon } - isSecondary - onClick={ async () => showPreview() } - > - { __( 'Preview prompt', 'newspack-plugin' ) } - </Button> - ) } - /> - </div> - </div> - { helpInfo && ( - <div className="newspack-ras-campaign__help"> - { helpInfo.screenshot && <img src={ helpInfo.screenshot } alt={ prompt.title } /> } - { helpInfo.description && ( - <p> - <span dangerouslySetInnerHTML={ { __html: helpInfo.description } } />{ ' ' } - { helpInfo.url && ( - <ExternalLink href={ helpInfo.url }> - { __( 'Learn more', 'newspack-plugin' ) } - </ExternalLink> - ) } - </p> - ) } - { helpInfo.recommendations && ( - <> - <h4 className="newspack-ras-campaign__recommendation-heading"> - { __( 'We recommend', 'newspack-plugin' ) } - </h4> - <ul> - { helpInfo.recommendations.map( ( recommendation, index ) => ( - <li key={ index }> - <span dangerouslySetInnerHTML={ { __html: recommendation } } /> - </li> - ) ) } - </ul> - </> - ) } - </div> - ) } - </Grid> - } - </ActionCard> - ); -} diff --git a/src/wizards/engagement/index.js b/src/wizards/engagement/index.js deleted file mode 100644 index e9a2847348..0000000000 --- a/src/wizards/engagement/index.js +++ /dev/null @@ -1,206 +0,0 @@ -import '../../shared/js/public-path'; - -/** - * Engagement - */ - -/** - * WordPress dependencies. - */ -import { Component, render, Fragment, createElement } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies. - */ -import { withWizard } from '../../components/src'; -import Router from '../../components/src/proxied-imports/router'; -import { - ReaderActivation, - ReaderActivationCampaign, - ReaderActivationComplete, - Newsletters, - Social, - RelatedContent, -} from './views'; - -const { HashRouter, Redirect, Route, Switch } = Router; - -class EngagementWizard extends Component { - constructor( props ) { - super( props ); - this.state = { - relatedPostsEnabled: false, - relatedPostsMaxAge: 0, - relatedPostsUpdated: false, - relatedPostsError: null, - }; - } - - /** - * Figure out whether to use the WooCommerce or Jetpack Mailchimp wizards and get appropriate settings. - */ - onWizardReady = () => { - const { setError, wizardApiFetch } = this.props; - return wizardApiFetch( { - path: '/newspack/v1/wizard/newspack-engagement-wizard/related-content', - } ) - .then( data => this.setState( data ) ) - .catch( error => setError( error ) ); - }; - - /** - * Update Related Content settings. - */ - updatedRelatedContentSettings = async () => { - const { wizardApiFetch } = this.props; - const { relatedPostsMaxAge } = this.state; - - try { - await wizardApiFetch( { - path: '/newspack/v1/wizard/newspack-engagement-wizard/related-posts-max-age', - method: 'POST', - data: { relatedPostsMaxAge }, - } ); - this.setState( { relatedPostsError: null, relatedPostsUpdated: false } ); - } catch ( e ) { - this.setState( { - relatedPostsError: - e.message || - __( 'There was an error saving settings. Please try again.', 'newspack-plugin' ), - } ); - } - }; - - /** - * Render - */ - render() { - const { pluginRequirements, wizardApiFetch } = this.props; - const { relatedPostsEnabled, relatedPostsError, relatedPostsMaxAge, relatedPostsUpdated } = - this.state; - - const defaultPath = '/reader-activation'; - const tabbed_navigation = [ - { - label: __( 'Reader Activation', 'newspack-plugin' ), - path: '/reader-activation', - exact: true, - activeTabPaths: [ - '/reader-activation', - '/reader-activation/campaign', - '/reader-activation/complete', - ], - }, - { - label: __( 'Newsletters', 'newspack-plugin' ), - path: '/newsletters', - exact: true, - }, - { - label: __( 'Social', 'newspack-plugin' ), - path: '/social', - exact: true, - }, - { - label: __( 'Recirculation', 'newspack-plugin' ), - path: '/recirculation', - }, - ]; - const props = { - headerText: __( 'Engagement', 'newspack-plugin' ), - tabbedNavigation: tabbed_navigation, - wizardApiFetch, - }; - return ( - <Fragment> - <HashRouter hashType="slash"> - <Switch> - { pluginRequirements } - <Route - path="/reader-activation" - exact - render={ () => ( - <ReaderActivation - subHeaderText={ __( - 'Configure your reader activation settings', - 'newspack-plugin' - ) } - { ...props } - /> - ) } - /> - <Route - path="/reader-activation/campaign" - render={ () => ( - <ReaderActivationCampaign - subHeaderText={ __( - 'Preview and customize the reader activation prompts', - 'newspack-plugin' - ) } - { ...props } - /> - ) } - /> - <Route - path="/reader-activation/complete" - render={ () => ( - <ReaderActivationComplete - subHeaderText={ __( - 'Preview and customize the reader activation prompts', - 'newspack-plugin' - ) } - { ...props } - /> - ) } - /> - <Route - path="/newsletters" - render={ () => ( - <Newsletters - subHeaderText={ __( 'Configure your newsletter settings', 'newspack-plugin' ) } - { ...props } - /> - ) } - /> - <Route - path="/social" - exact - render={ () => ( - <Social - subHeaderText={ __( 'Share your content to social media', 'newspack-plugin' ) } - { ...props } - /> - ) } - /> - <Route - path="/recirculation" - exact - render={ () => ( - <RelatedContent - { ...props } - subHeaderText={ __( 'Engage visitors with related content', 'newspack-plugin' ) } - relatedPostsEnabled={ relatedPostsEnabled } - relatedPostsError={ relatedPostsError } - buttonAction={ () => this.updatedRelatedContentSettings() } - buttonText={ __( 'Save Settings', 'newspack-plugin' ) } - buttonDisabled={ ! relatedPostsEnabled || ! relatedPostsUpdated } - relatedPostsMaxAge={ relatedPostsMaxAge } - onChange={ value => { - this.setState( { relatedPostsMaxAge: value, relatedPostsUpdated: true } ); - } } - /> - ) } - /> - <Redirect to={ defaultPath } /> - </Switch> - </HashRouter> - </Fragment> - ); - } -} - -render( - createElement( withWizard( EngagementWizard, [ 'jetpack' ] ) ), - document.getElementById( 'newspack-engagement-wizard' ) -); diff --git a/src/wizards/engagement/views/index.js b/src/wizards/engagement/views/index.js deleted file mode 100644 index ab358064c0..0000000000 --- a/src/wizards/engagement/views/index.js +++ /dev/null @@ -1,6 +0,0 @@ -export { default as Newsletters } from './newsletters'; -export { default as ReaderActivation } from './reader-activation'; -export { default as ReaderActivationCampaign } from './reader-activation/campaign'; -export { default as ReaderActivationComplete } from './reader-activation/complete'; -export { default as Social } from './social'; -export { default as RelatedContent } from './related-content'; diff --git a/src/wizards/engagement/views/reader-activation/campaign.js b/src/wizards/engagement/views/reader-activation/campaign.js deleted file mode 100644 index 34ea029edb..0000000000 --- a/src/wizards/engagement/views/reader-activation/campaign.js +++ /dev/null @@ -1,160 +0,0 @@ -/* global newspack_engagement_wizard */ - -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import apiFetch from '@wordpress/api-fetch'; -import { useEffect, useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { - Button, - Notice, - SectionHeader, - Waiting, - withWizardScreen, - utils, -} from '../../../../components/src'; -import Prompt from '../../components/prompt'; -import Router from '../../../../components/src/proxied-imports/router'; -import './style.scss'; - -const { useHistory } = Router; - -export default withWizardScreen( () => { - const { is_skipped_campaign_setup, reader_activation_url } = newspack_engagement_wizard; - - const [ inFlight, setInFlight ] = useState( false ); - const [ error, setError ] = useState( false ); - const [ prompts, setPrompts ] = useState( null ); - const [ allReady, setAllReady ] = useState( false ); - const [ skipped, setSkipped ] = useState( { - status: '', - isSkipped: is_skipped_campaign_setup === '1', - } ); - const history = useHistory(); - - const fetchPrompts = () => { - setError( false ); - setInFlight( true ); - apiFetch( { - path: '/newspack-popups/v1/reader-activation/campaign', - } ) - .then( fetchedPrompts => { - setPrompts( fetchedPrompts ); - } ) - .catch( setError ) - .finally( () => setInFlight( false ) ); - }; - - /** - * Display prompt requiring editors to confirm skipping, on confirmation send request to - * server to store skipped option in options table and redirect back to RAS - * - * @return {void} - */ - async function onSkipCampaignSetup() { - if ( - ! utils.confirmAction( - __( - 'Are you sure you want to skip setting up a reader activation campaign?', - 'newspack-plugin' - ) - ) - ) { - return; - } - setError( false ); - setSkipped( { ...skipped, status: 'pending' } ); - try { - const request = await apiFetch( { - path: '/newspack/v1/wizard/newspack-engagement-wizard/reader-activation/skip-campaign-setup', - method: 'POST', - data: { skip: ! skipped.isSkipped }, - } ); - if ( ! request.updated ) { - setError( { message: __( 'Server not updated', 'newspack-plugin' ) } ); - setSkipped( { isSkipped: false, status: '' } ); - return; - } - setSkipped( { isSkipped: Boolean( request.skipped ), status: '' } ); - newspack_engagement_wizard.is_skipped_campaign_setup = request.skipped ? '1' : ''; - history.push( '/reader-activation/complete' ); - } catch ( err ) { - setError( err ); - setSkipped( { isSkipped: false, status: '' } ); - } - } - - useEffect( () => { - window.scrollTo( 0, 0 ); - fetchPrompts(); - }, [] ); - - useEffect( () => { - if ( Array.isArray( prompts ) && 0 < prompts.length ) { - setAllReady( prompts.every( prompt => prompt.ready ) ); - } - }, [ prompts ] ); - - return ( - <div className="newspack-ras-campaign__prompt-wizard"> - <SectionHeader - title={ __( 'Set Up Reader Activation Campaign', 'newspack-plugin' ) } - description={ __( - 'Preview and customize the prompts, or use our suggested defaults.', - 'newspack-plugin' - ) } - /> - { error && ( - <Notice - noticeText={ error?.message || __( 'Something went wrong.', 'newspack-plugin' ) } - isError - /> - ) } - { ! prompts && ! error && ( - <> - <Waiting isLeft /> - { __( 'Retrieving prompts…', 'newspack-plugin' ) } - </> - ) } - { prompts && - prompts.map( prompt => ( - <Prompt - key={ prompt.slug } - prompt={ prompt } - inFlight={ inFlight } - setInFlight={ setInFlight } - setPrompts={ setPrompts } - /> - ) ) } - <div className="newspack-buttons-card"> - <Button - isTertiary - disabled={ inFlight || skipped.isSkipped || skipped.status === 'pending' } - onClick={ onSkipCampaignSetup } - > - { /* eslint-disable-next-line no-nested-ternary */ } - { skipped.status === 'pending' - ? __( 'Skipping…', 'newspack-plugin' ) - : skipped.isSkipped - ? __( 'Skipped', 'newspack-plugin' ) - : __( 'Skip', 'newspack-plugin' ) } - </Button> - <Button - isPrimary - disabled={ inFlight || ( ! allReady && ! skipped.isSkipped ) } - href={ `${ reader_activation_url }/complete` } - > - { __( 'Continue', 'newspack-plugin' ) } - </Button> - <Button isSecondary disabled={ inFlight } href={ reader_activation_url }> - { __( 'Back', 'newspack-plugin' ) } - </Button> - </div> - </div> - ); -} ); diff --git a/src/wizards/engagement/views/reader-activation/index.js b/src/wizards/engagement/views/reader-activation/index.js deleted file mode 100644 index 3bb15cd567..0000000000 --- a/src/wizards/engagement/views/reader-activation/index.js +++ /dev/null @@ -1,520 +0,0 @@ -/* global newspack_engagement_wizard */ -/** - * WordPress dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { ExternalLink, TextareaControl, ToggleControl } from '@wordpress/components'; -import { useEffect, useState } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { - ActionCard, - Button, - Card, - Grid, - Notice, - PluginInstaller, - SectionHeader, - TextControl, - Waiting, - withWizardScreen, - utils, -} from '../../../../components/src'; -import Prerequisite from '../../components/prerequisite'; -import ActiveCampaign from '../../components/active-campaign'; -import MetadataFields from '../../components/metadata-fields'; -import Mailchimp from '../../components/mailchimp'; -import { HANDOFF_KEY } from '../../../../components/src/consts'; -import SortableNewsletterListControl from '../../../../components/src/sortable-newsletter-list-control'; - -export default withWizardScreen( ( { wizardApiFetch } ) => { - const [ inFlight, setInFlight ] = useState( false ); - const [ config, setConfig ] = useState( {} ); - const [ membershipsConfig, setMembershipsConfig ] = useState( {} ); - const [ error, setError ] = useState( false ); - const [ allReady, setAllReady ] = useState( false ); - const [ isActiveCampaign, setIsActiveCampaign ] = useState( false ); - const [ isMailchimp, setIsMailchimp ] = useState( false ); - const [ prerequisites, setPrerequisites ] = useState( null ); - const [ missingPlugins, setMissingPlugins ] = useState( [] ); - const [ showAdvanced, setShowAdvanced ] = useState( false ); - const [ espSyncErrors, setEspSyncErrors ] = useState( [] ); - const updateConfig = ( key, val ) => { - setConfig( { ...config, [ key ]: val } ); - }; - const fetchConfig = () => { - setError( false ); - setInFlight( true ); - apiFetch( { - path: '/newspack/v1/wizard/newspack-engagement-wizard/reader-activation', - } ) - .then( ( { config: fetchedConfig, prerequisites_status, memberships, can_esp_sync } ) => { - setPrerequisites( prerequisites_status ); - setConfig( fetchedConfig ); - setMembershipsConfig( memberships ); - setEspSyncErrors( can_esp_sync.errors ); - } ) - .catch( setError ) - .finally( () => setInFlight( false ) ); - }; - const saveConfig = data => { - setError( false ); - setInFlight( true ); - wizardApiFetch( { - path: '/newspack/v1/wizard/newspack-engagement-wizard/reader-activation', - method: 'post', - quiet: true, - data, - } ) - .then( ( { config: fetchedConfig, prerequisites_status, memberships, can_esp_sync } ) => { - setPrerequisites( prerequisites_status ); - setConfig( fetchedConfig ); - setMembershipsConfig( memberships ); - setEspSyncErrors( can_esp_sync.errors ); - } ) - .catch( setError ) - .finally( () => setInFlight( false ) ); - }; - const resetEmail = postId => { - setError( false ); - setInFlight( true ); - wizardApiFetch( { - path: `/newspack/v1/wizard/newspack-engagement-wizard/reader-activation/emails/${ postId }`, - method: 'DELETE', - quiet: true, - } ) - .then( emails => setConfig( { ...config, emails } ) ) - .catch( setError ) - .finally( () => setInFlight( false ) ); - }; - useEffect( () => { - window.scrollTo( 0, 0 ); - fetchConfig(); - - // Clear the handoff when the component mounts. - window.localStorage.removeItem( HANDOFF_KEY ); - }, [] ); - useEffect( () => { - apiFetch( { - path: '/newspack/v1/wizard/newspack-engagement-wizard/newsletters', - } ).then( data => { - setIsMailchimp( - data?.settings?.newspack_newsletters_service_provider?.value === 'mailchimp' - ); - setIsActiveCampaign( - data?.settings?.newspack_newsletters_service_provider?.value === 'active_campaign' - ); - } ); - }, [] ); - useEffect( () => { - const _allReady = - ! missingPlugins.length && - prerequisites && - Object.keys( prerequisites ).every( - key => prerequisites[ key ]?.active || prerequisites[ key ]?.skipped - ); - - setAllReady( _allReady ); - - if ( prerequisites ) { - setMissingPlugins( - Object.keys( prerequisites ).reduce( ( acc, slug ) => { - const prerequisite = prerequisites[ slug ]; - if ( prerequisite.plugins ) { - for ( const pluginSlug in prerequisite.plugins ) { - if ( ! prerequisite.plugins[ pluginSlug ] ) { - acc.push( pluginSlug ); - } - } - } - return acc; - }, [] ) - ); - } - }, [ prerequisites ] ); - - const getSharedProps = ( configKey, type = 'checkbox' ) => { - const props = { - onChange: val => updateConfig( configKey, val ), - }; - if ( configKey !== 'enabled' ) { - props.disabled = inFlight; - } - switch ( type ) { - case 'checkbox': - props.checked = Boolean( config[ configKey ] ); - break; - case 'text': - props.value = config[ configKey ] || ''; - break; - } - - return props; - }; - - const emails = Object.values( config.emails || {} ); - - const getContentGateDescription = () => { - let message = __( - 'Configure the gate rendered on content with restricted access.', - 'newspack-plugin' - ); - if ( 'publish' === membershipsConfig?.gate_status ) { - message += ' ' + __( 'The gate is currently published.', 'newspack-plugin' ); - } else if ( - 'draft' === membershipsConfig?.gate_status || - 'trash' === membershipsConfig?.gate_status - ) { - message += ' ' + __( 'The gate is currently a draft.', 'newspack-plugin' ); - } - return message; - }; - - return ( - <> - <SectionHeader - title={ __( 'Reader Activation', 'newspack-plugin' ) } - description={ () => ( - <> - { __( - 'Newspack’s Reader Activation system is a set of features that aim to increase reader loyalty, promote engagement, and drive revenue. ', - 'newspack-plugin' - ) } - <ExternalLink href={ 'https://help.newspack.com/engagement/reader-activation-system' }> - { __( 'Learn more', 'newspack-plugin' ) } - </ExternalLink> - </> - ) } - /> - { error && ( - <Notice - noticeText={ error?.message || __( 'Something went wrong.', 'newspack-plugin' ) } - isError - /> - ) } - { 0 < missingPlugins.length && ( - <Notice - noticeText={ __( 'The following plugins are required.', 'newspack-plugin' ) } - isWarning - /> - ) } - { 0 === missingPlugins.length && prerequisites && ! allReady && ( - <Notice - noticeText={ __( - 'Complete these settings to enable Reader Activation.', - 'newspack-plugin' - ) } - isWarning - /> - ) } - { prerequisites && allReady && config.enabled && ( - <Notice noticeText={ __( 'Reader Activation is enabled.', 'newspack-plugin' ) } isSuccess /> - ) } - { ! prerequisites && ( - <> - <Waiting isLeft /> - { __( 'Retrieving status…', 'newspack-plugin' ) } - </> - ) } - { 0 < missingPlugins.length && prerequisites && ( - <PluginInstaller - plugins={ missingPlugins } - withoutFooterButton - onStatus={ ( { complete } ) => complete && fetchConfig() } - /> - ) } - { ! missingPlugins.length && - prerequisites && - Object.keys( prerequisites ).map( key => ( - <Prerequisite - key={ key } - config={ config } - getSharedProps={ getSharedProps } - inFlight={ inFlight } - prerequisite={ prerequisites[ key ] } - fetchConfig={ fetchConfig } - saveConfig={ saveConfig } - /> - ) ) } - { config.enabled && ( - <> - <hr /> - <Button variant="secondary" onClick={ () => setShowAdvanced( ! showAdvanced ) }> - { sprintf( - // Translators: Show or Hide advanced settings. - __( '%s Advanced Settings', 'newspack-plugin' ), - showAdvanced ? __( 'Hide', 'newspack-plugin' ) : __( 'Show', 'newspack-plugin' ) - ) } - </Button> - </> - ) } - { showAdvanced && ( - <Card noBorder> - { newspack_engagement_wizard.has_memberships && membershipsConfig ? ( - <> - <SectionHeader - title={ __( 'Memberships Integration', 'newspack-plugin' ) } - description={ __( - 'Improve the reader experience on content gating.', - 'newspack-plugin' - ) } - /> - <ActionCard - title={ __( 'Content Gate', 'newspack-plugin' ) } - titleLink={ membershipsConfig.edit_gate_url } - href={ membershipsConfig.edit_gate_url } - description={ getContentGateDescription() } - actionText={ __( 'Configure', 'newspack-plugin' ) } - /> - { membershipsConfig?.plans && 1 < membershipsConfig.plans.length && ( - <ActionCard - title={ __( 'Require membership in all plans', 'newspack-plugin' ) } - description={ __( - 'When enabled, readers must belong to all membership plans that apply to a restricted content item before they are granted access. Otherwise, they will be able to unlock access to that item with membership in any single plan that applies to it.', - 'newspack-plugin' - ) } - toggleOnChange={ value => - setMembershipsConfig( { ...membershipsConfig, require_all_plans: value } ) - } - toggleChecked={ membershipsConfig.require_all_plans } - /> - ) } - <ActionCard - title={ __( 'Display memberships on the subscriptions tab', 'newspack-plugin' ) } - description={ __( - "Display memberships that don't have active subscriptions on the My Account Subscriptions tab, so readers can see information like expiration dates.", - 'newspack-plugin' - ) } - toggleOnChange={ value => - setMembershipsConfig( { ...membershipsConfig, show_on_subscription_tab: value } ) - } - toggleChecked={ membershipsConfig.show_on_subscription_tab } - /> - <hr /> - </> - ) : null } - - { emails?.length > 0 && ( - <> - <SectionHeader - title={ __( 'Transactional Email Content', 'newspack-plugin' ) } - description={ __( - 'Customize the content of transactional emails.', - 'newspack-plugin' - ) } - /> - { emails.map( email => ( - <ActionCard - key={ email.post_id } - title={ email.label } - titleLink={ email.edit_link } - href={ email.edit_link } - description={ email.description } - actionText={ __( 'Edit', 'newspack-plugin' ) } - onSecondaryActionClick={ () => { - if ( - utils.confirmAction( - __( - 'Are you sure you want to reset the contents of this email?', - 'newspack-plugin' - ) - ) - ) { - resetEmail( email.post_id ); - } - } } - secondaryActionText={ __( 'Reset', 'newspack-plugin' ) } - secondaryDestructive={ true } - isSmall - /> - ) ) } - <hr /> - </> - ) } - - <SectionHeader title={ __( 'Newsletter Subscription Lists', 'newspack-plugin' ) } /> - <ActionCard - title={ __( - 'Present newsletter signup after checkout and registration', - 'newspack-plugin' - ) } - description={ __( - 'Ask readers to sign up for newsletters after creating an account or completing a purchase.', - 'newspack-plugin' - ) } - toggleChecked={ config.use_custom_lists } - toggleOnChange={ value => updateConfig( 'use_custom_lists', value ) } - /> - { config.use_custom_lists && ( - <SortableNewsletterListControl - lists={ newspack_engagement_wizard.available_newsletter_lists } - selected={ config.newsletter_lists } - onChange={ selected => updateConfig( 'newsletter_lists', selected ) } - /> - ) } - - <hr /> - - <SectionHeader - title={ __( 'Email Service Provider (ESP) Advanced Settings', 'newspack-plugin' ) } - description={ __( - 'Settings for Newspack Newsletters integration.', - 'newspack-plugin' - ) } - /> - <TextControl - label={ __( 'Newsletter subscription text on registration', 'newspack-plugin' ) } - help={ __( - 'The text to display while subscribing to newsletters from the sign-in modal.', - 'newspack-plugin' - ) } - { ...getSharedProps( 'newsletters_label', 'text' ) } - /> - <ActionCard - description={ __( - 'Configure options for syncing reader data to the connected ESP.', - 'newspack-plugin' - ) } - hasGreyHeader={ true } - isMedium - title={ __( 'Sync contacts to ESP', 'newspack-plugin' ) } - toggleChecked={ config.sync_esp } - toggleOnChange={ value => updateConfig( 'sync_esp', value ) } - > - { config.sync_esp && ( - <> - { 0 < Object.keys(espSyncErrors).length && ( - <Notice - noticeText={ Object.values(espSyncErrors).join( ' ' ) } - isError - /> - ) } - { isMailchimp && ( - <Mailchimp - value={ { - audienceId: config.mailchimp_audience_id, - readerDefaultStatus: config.mailchimp_reader_default_status, - } } - onChange={ ( key, value ) => { - if ( key === 'audienceId' ) { - updateConfig( 'mailchimp_audience_id', value ); - } - if ( key === 'readerDefaultStatus' ) { - updateConfig( 'mailchimp_reader_default_status', value ); - } - } } - /> - ) } - { isActiveCampaign && ( - <ActiveCampaign - value={ { masterList: config.active_campaign_master_list } } - onChange={ ( key, value ) => { - if ( key === 'masterList' ) { - updateConfig( 'active_campaign_master_list', value ); - } - } } - /> - ) } - <MetadataFields - availableFields={ newspack_engagement_wizard.esp_metadata_fields || [] } - selectedFields={ config.metadata_fields } - updateConfig={ updateConfig } - getSharedProps={ getSharedProps } - /> - </> - ) } - </ActionCard> - - <hr /> - - <SectionHeader title={ __( 'Checkout Configuration', 'newspack-plugin' ) } /> - - <ToggleControl - label={ __( - 'Require sign in or create account before checkout', - 'newspack-plugin' - ) } - help={ __( - 'Prompt users who are not logged in to sign in or register a new account before proceeding to checkout. When disabled, an account will automatically be created with the email address used at checkout.', - 'newspack-plugin' - ) } - checked={ config.woocommerce_registration_required } - onChange={ value => updateConfig( 'woocommerce_registration_required', value ) } - /> - <Grid> - <TextareaControl - label={ __( 'Post-checkout success message', 'newspack-plugin' ) } - help={ __( - 'The success message to display to readers after completing checkout.', - 'newspack-plugin' - ) } - { ...getSharedProps( 'woocommerce_post_checkout_success_text', 'text' ) } - /> - { ! config.woocommerce_registration_required && ( - <TextareaControl - label={ __( 'Post-checkout registration success message', 'newspack-plugin' ) } - help={ __( - 'The success message to display to new readers that have an account automatically created after completing checkout.', - 'newspack-plugin' - ) } - { ...getSharedProps( 'woocommerce_post_checkout_registration_success_text', 'text' ) } - /> - ) } - </Grid> - <Grid> - <TextareaControl - label={ __( 'Checkout privacy policy text', 'newspack-plugin' ) } - help={ __( - 'The privacy policy text to display at time of checkout for existing users. This will not show up unless a privacy page is set.', - 'newspack-plugin' - ) } - { ...getSharedProps( 'woocommerce_checkout_privacy_policy_text', 'text' ) } - /> - </Grid> - <div className="newspack-buttons-card"> - <Button - isPrimary - onClick={ () => { - if ( config.sync_esp ) { - if (isMailchimp && config.mailchimp_audience_id === '') { - // eslint-disable-next-line no-alert - alert( __( 'Please select a Mailchimp Audience ID.', 'newspack-plugin' ) ); - return - } - if (isActiveCampaign && config.active_campaign_master_list === '') { - // eslint-disable-next-line no-alert - alert( __( 'Please select an ActiveCampaign Master List.', 'newspack-plugin' ) ); - return - } - } - saveConfig( { - newsletters_label: config.newsletters_label, // TODO: Deprecate this in favor of user input via the prompt copy wizard. - mailchimp_audience_id: config.mailchimp_audience_id, - mailchimp_reader_default_status: config.mailchimp_reader_default_status, - active_campaign_master_list: config.active_campaign_master_list, - memberships_require_all_plans: membershipsConfig.require_all_plans, - memberships_show_on_subscription_tab: membershipsConfig.show_on_subscription_tab, - use_custom_lists: config.use_custom_lists, - newsletter_lists: config.newsletter_lists, - sync_esp: config.sync_esp, - metadata_fields: config.metadata_fields, - metadata_prefix: config.metadata_prefix, - woocommerce_registration_required: config.woocommerce_registration_required, - woocommerce_checkout_privacy_policy_text: config.woocommerce_checkout_privacy_policy_text, - woocommerce_post_checkout_success_text: config.woocommerce_post_checkout_success_text, - woocommerce_post_checkout_registration_success_text: config.woocommerce_post_checkout_registration_success_text, - } ); - } } - disabled={ inFlight } - > - { __( 'Save advanced settings', 'newspack-plugin' ) } - </Button> - </div> - </Card> - ) } - </> - ); -} ); diff --git a/src/wizards/engagement/views/related-content/index.js b/src/wizards/engagement/views/related-content/index.js deleted file mode 100644 index 6f1da2bd96..0000000000 --- a/src/wizards/engagement/views/related-content/index.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Related content screen. - */ - -/** - * WordPress dependencies - */ -import { Component } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { - ActionCard, - Card, - Grid, - Notice, - TextControl, - withWizardScreen, -} from '../../../../components/src'; - -/** - * Related Content Screen - */ -class RelatedContent extends Component { - /** - * Render. - */ - render() { - const { onChange, relatedPostsEnabled, relatedPostsError, relatedPostsMaxAge } = this.props; - - return ( - <> - { relatedPostsError && <Notice noticeText={ relatedPostsError } isError /> } - - <ActionCard - title={ __( 'Related Posts', 'newspack-plugin' ) } - badge="Jetpack" - description={ __( - 'Automatically add related content at the bottom of each post.', - 'newspack-plugin' - ) } - actionText={ __( 'Configure', 'newspack-plugin' ) } - handoff="jetpack" - editLink="admin.php?page=jetpack#/traffic" - /> - - { relatedPostsEnabled && ( - <Grid> - <Card noBorder> - <TextControl - help={ __( - 'If set, posts will be shown as related content only if published within the past number of months. If 0, any published post can be shown, regardless of publish date.', - 'newspack-plugin' - ) } - label={ __( 'Maximum age of related content, in months', 'newspack-plugin' ) } - onChange={ value => onChange( value ) } - placeholder={ __( 'Maximum age of related content, in months', 'newspack-plugin' ) } - type="number" - value={ relatedPostsMaxAge || 0 } - /> - </Card> - </Grid> - ) } - </> - ); - } -} - -export default withWizardScreen( RelatedContent ); diff --git a/src/wizards/engagement/views/social/index.js b/src/wizards/engagement/views/social/index.js deleted file mode 100644 index 6f45b407d8..0000000000 --- a/src/wizards/engagement/views/social/index.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Social screen. - */ - -/** - * WordPress dependencies - */ -import { Component } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { ActionCard, withWizardScreen } from '../../../../components/src'; -import MetaPixel from './meta-pixel'; -import TwitterPixel from './twitter-pixel'; - -/** - * Social Screen - */ -class Social extends Component { - /** - * Render. - */ - render() { - return ( - <> - <ActionCard - title={ __( 'Publicize', 'newspack-plugin' ) } - badge="Jetpack" - description={ __( - 'Publicize makes it easy to share your site’s posts on several social media networks automatically when you publish a new post.', - 'newspack-plugin' - ) } - actionText={ __( 'Configure', 'newspack-plugin' ) } - handoff="jetpack" - editLink="admin.php?page=jetpack#/sharing" - /> - <MetaPixel /> - <TwitterPixel /> - </> - ); - } -} - -export default withWizardScreen( Social ); diff --git a/src/wizards/engagement/views/social/meta-pixel.js b/src/wizards/engagement/views/social/meta-pixel.js deleted file mode 100644 index ccc9da6264..0000000000 --- a/src/wizards/engagement/views/social/meta-pixel.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { createInterpolateElement } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import Pixel from './pixel'; - -const MetaPixel = () => ( - <Pixel - title={ __( 'Meta Pixel', 'newspack-plugin' ) } - pixelKey="meta" - pixelValueType="integer" - description={ __( - 'Add the Meta pixel (formely known as Facebook pixel) to your site.', - 'newspack-plugin' - ) } - fieldDescription={ __( 'Pixel ID', 'newspack-plugin' ) } - fieldHelp={ createInterpolateElement( - __( - 'The Meta Pixel ID. You only need to add the number, not the full code. Example: 123456789123456789. You can get this information <linkToFb>here</linkToFb>.', - 'newspack-plugin' - ), - { - /* eslint-disable jsx-a11y/anchor-has-content */ - linkToFb: ( - <a - href="https://www.facebook.com/ads/manager/pixel/facebook_pixel" - target="_blank" - rel="noopener noreferrer" - /> - ), - } - ) } - /> -); - -export default MetaPixel; diff --git a/src/wizards/engagement/views/social/pixel.js b/src/wizards/engagement/views/social/pixel.js deleted file mode 100644 index 0e2a300e4c..0000000000 --- a/src/wizards/engagement/views/social/pixel.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * WordPress dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { useEffect, useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { PluginSettings } from '../../../../components/src'; - -export default function Pixel( { - title, - description, - pixelKey, - fieldDescription, - fieldHelp, - pixelValueType, -} ) { - const apiEndpoint = `/newspack/v1/wizard/newspack-engagement-wizard/social/${ pixelKey }_pixel`; - const [ inFlight, setInFlight ] = useState( false ); - const [ error, setError ] = useState( null ); - const [ settings, setSettings ] = useState( null ); - - useEffect( () => { - const fetchSettings = async () => { - setInFlight( true ); - try { - const response = await apiFetch( { path: apiEndpoint } ); - setSettings( response ); - } catch ( err ) { - setSettings( null ); - } - setInFlight( false ); - }; - fetchSettings(); - }, [] ); - - const handleChange = ( key, value ) => { - setSettings( { - ...settings, - [ key ]: value, - } ); - }; - - const handleUpdate = async data => { - setError( null ); - setInFlight( true ); - try { - const result = await apiFetch( { - path: apiEndpoint, - method: 'POST', - data: { - ...settings, - ...data, - }, - } ); - setSettings( result ); - } catch ( err ) { - setError( err ); - } - setInFlight( false ); - }; - - if ( ! settings ) { - return null; - } - - const fields = [ - { - key: 'pixel_id', - type: pixelValueType, - description: fieldDescription, - help: fieldHelp, - value: settings.pixel_id, - }, - ]; - - return ( - <PluginSettings.Section - error={ error } - disabled={ inFlight } - sectionKey="pixel-settings" - title={ title } - description={ description } - active={ settings.active } - fields={ fields } - onUpdate={ handleUpdate } - onChange={ handleChange } - /> - ); -} diff --git a/src/wizards/engagement/views/social/twitter-pixel.js b/src/wizards/engagement/views/social/twitter-pixel.js deleted file mode 100644 index e0f1416968..0000000000 --- a/src/wizards/engagement/views/social/twitter-pixel.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { createInterpolateElement } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import Pixel from './pixel'; - -const TwitterPixel = () => ( - <Pixel - title={ __( 'Twitter Pixel', 'newspack-plugin' ) } - pixelKey="twitter" - description={ __( 'Add the Twitter pixel to your site.', 'newspack-plugin' ) } - pixelValueType="text" - fieldDescription={ __( 'Pixel ID', 'newspack-plugin' ) } - fieldHelp={ createInterpolateElement( - __( - 'The Twitter Pixel ID. You only need to add the ID, not the full code. Example: ny3ad. You can read more about it <link>here</link>.', - 'newspack-plugin' - ), - { - /* eslint-disable jsx-a11y/anchor-has-content */ - link: ( - <a - href="https://business.twitter.com/en/help/campaign-measurement-and-analytics/conversion-tracking-for-websites.html" - target="_blank" - rel="noopener noreferrer" - /> - ), - } - ) } - /> -); - -export default TwitterPixel; diff --git a/src/wizards/errors/class-wizard-api-error.ts b/src/wizards/errors/class-wizard-api-error.ts new file mode 100644 index 0000000000..8a188575c3 --- /dev/null +++ b/src/wizards/errors/class-wizard-api-error.ts @@ -0,0 +1,38 @@ +/** + * Internal dependencies + */ +import WizardError from './class-wizard-error'; + +/** + * Custom error class for Newspack Wizards API requests. + */ +class WizardApiError extends WizardError { + statusCode: number; + + constructor( message: string, statusCode: number, errorCode: string, details = '' ) { + super( message, errorCode, details ); + this.name = 'WizardApiError'; + this.statusCode = statusCode; + + // Set the prototype explicitly. + Object.setPrototypeOf( this, WizardApiError.prototype ); + } + + /** + * For when this class is serialized outside of the API. + * + * @return JSON representation of the error. + */ + toJSON() { + return { + name: this.name, + message: this.message, + statusCode: this.statusCode, + errorCode: this.errorCode, + details: this.details, + stackTrace: this.stack, + }; + } +} + +export default WizardApiError; diff --git a/src/wizards/errors/class-wizard-error.ts b/src/wizards/errors/class-wizard-error.ts new file mode 100644 index 0000000000..1b054a4f6f --- /dev/null +++ b/src/wizards/errors/class-wizard-error.ts @@ -0,0 +1,35 @@ +/** + * Custom error class for Newspack Wizards. + */ +class WizardError extends Error { + errorCode: string; + details: string; + + constructor( message: string, errorCode: string, details: any = '' ) { + super( message ); + this.name = 'WizardError'; + this.errorCode = errorCode; + this.details = details; + + if ( Object.setPrototypeOf ) { + Object.setPrototypeOf( this, new.target.prototype ); + } + } + + /** + * For when this class is serialized outside of the API. + * + * @return JSON representation of the error. + */ + toJSON() { + return { + name: this.name, + message: this.message, + errorCode: this.errorCode, + details: this.details, + stackTrace: this.stack, + }; + } +} + +export default WizardError; diff --git a/src/wizards/errors/index.ts b/src/wizards/errors/index.ts new file mode 100644 index 0000000000..61f77d5b14 --- /dev/null +++ b/src/wizards/errors/index.ts @@ -0,0 +1,22 @@ +import { __ } from '@wordpress/i18n'; + +export { default as WizardError } from './class-wizard-error'; +export { default as WizardApiError } from './class-wizard-api-error'; + +export const WIZARD_ERROR_MESSAGES = { + MAILCHIMP_API_KEY_INVALID: __( 'Invalid Mailchimp API Key.', 'newspack-plugin' ), + GOOGLEOAUTH_REFRESH_TOKEN_EXPIRED: __( + 'Missing Google refresh token. Please re-authenticate site.', + 'newspack-plugin' + ), + GOOGLE: { + OAUTH_POPUP_BLOCKED: __( + 'Popup blocked by browser. Disable any popup blocking settings and try again.', + 'newspack-plugin' + ), + URL_INVALID: __( + 'URL is invalid. Please try again or contact support if the issue persists.', + 'newspack-plugin' + ), + }, +}; diff --git a/src/wizards/handoff-banner/index.js b/src/wizards/handoff-banner/index.js index d104eed1b6..bcd204f955 100644 --- a/src/wizards/handoff-banner/index.js +++ b/src/wizards/handoff-banner/index.js @@ -20,7 +20,7 @@ const HandoffBanner = ( { bodyText = __( 'Return to Newspack after completing configuration', 'newspack-plugin' ), primaryButtonText = __( 'Back to Newspack', 'newspack-plugin' ), dismissButtonText = __( 'Dismiss', 'newspack-plugin' ), - primaryButtonURL = '/wp-admin/admin.php?page=newspack', + primaryButtonURL = '/wp-admin/admin.php?page=newspack-dashboard', } ) => { const [ visibility, setVisibility ] = useState( true ); return ( diff --git a/src/wizards/handoff-banner/style.scss b/src/wizards/handoff-banner/style.scss index 0cc20390b2..859ac3b1e9 100644 --- a/src/wizards/handoff-banner/style.scss +++ b/src/wizards/handoff-banner/style.scss @@ -7,10 +7,10 @@ #newspack-handoff-banner { // Color - --wp-admin-theme-color: #{colors.$primary-500}; - --wp-admin-theme-color--rgb: #{colors.$primary-500--rgb}; - --wp-admin-theme-color-darker-10: #{colors.$primary-600}; - --wp-admin-theme-color-darker-10--rgb: #{colors.$primary-600--rgb}; + --wp-admin-theme-color: var(--newspack-ui-color-primary); + --wp-admin-theme-color--rgb: var(--newspack-ui-color-primary-rgb); + --wp-admin-theme-color-darker-10: var(--newspack-ui-color-primary); + --wp-admin-theme-color-darker-10--rgb: var(--newspack-ui-color-primary-rgb); --wp-admin-theme-color-darker-20: #{colors.$primary-700}; --wp-admin-theme-color-darker-20--rgb: #{colors.$primary-700--rgb}; diff --git a/src/wizards/health-check/index.js b/src/wizards/health-check/index.js deleted file mode 100644 index ab0aa61555..0000000000 --- a/src/wizards/health-check/index.js +++ /dev/null @@ -1,125 +0,0 @@ -import '../../shared/js/public-path'; - -/** - * Health Check - */ - -/** - * WordPress dependencies. - */ -import { Component, render, Fragment, createElement } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies. - */ -import { withWizard } from '../../components/src'; -import Router from '../../components/src/proxied-imports/router'; -import { Configuration, Plugins } from './views'; - -const { HashRouter, Redirect, Route, Switch } = Router; - -class HealthCheckWizard extends Component { - constructor( props ) { - super( props ); - this.state = { - hasData: false, - healthCheckData: { - unsupported_plugins: {}, - missing_plugins: {}, - }, - }; - } - onWizardReady = () => { - this.fetchHealthData(); - }; - - fetchHealthData = () => { - const { wizardApiFetch, setError } = this.props; - wizardApiFetch( { path: '/newspack/v1/wizard/newspack-health-check-wizard/' } ) - .then( healthCheckData => this.setState( { healthCheckData, hasData: true } ) ) - .catch( error => { - setError( error ); - } ); - }; - - deactivateAllPlugins = () => { - const { wizardApiFetch, setError } = this.props; - wizardApiFetch( { - path: '/newspack/v1/wizard/newspack-health-check-wizard/unsupported_plugins', - method: 'delete', - } ) - .then( healthCheckData => this.setState( { healthCheckData } ) ) - .catch( error => { - setError( error ); - } ); - }; - - /** - * Render - */ - render() { - const { hasData, healthCheckData } = this.state; - const { - unsupported_plugins: unsupportedPlugins, - missing_plugins: missingPlugins, - configuration_status: configurationStatus, - } = healthCheckData; - const tabs = [ - { - label: __( 'Plugins', 'newspack-plugin' ), - path: '/', - exact: true, - }, - { - label: __( 'Configuration', 'newspack-plugin' ), - path: '/configuration', - }, - ]; - return ( - <Fragment> - <HashRouter hashType="slash"> - <Switch> - <Route - path="/" - exact - render={ () => ( - <Plugins - headerText={ __( 'Health Check', 'newspack-plugin' ) } - subHeaderText={ __( 'Verify and correct site health issues', 'newspack-plugin' ) } - deactivateAllPlugins={ this.deactivateAllPlugins } - tabbedNavigation={ tabs } - missingPlugins={ Object.keys( missingPlugins ) } - unsupportedPlugins={ Object.keys( unsupportedPlugins ).map( value => ( { - ...unsupportedPlugins[ value ], - Slug: value, - } ) ) } - /> - ) } - /> - <Route - path="/configuration" - exact - render={ () => ( - <Configuration - hasData={ hasData } - headerText={ __( 'Health Check', 'newspack-plugin' ) } - subHeaderText={ __( 'Verify and correct site health issues', 'newspack-plugin' ) } - tabbedNavigation={ tabs } - configurationStatus={ configurationStatus } - missingPlugins={ Object.keys( missingPlugins ) } - /> - ) } - /> - <Redirect to="/" /> - </Switch> - </HashRouter> - </Fragment> - ); - } -} - -render( - createElement( withWizard( HealthCheckWizard ) ), - document.getElementById( 'newspack-health-check-wizard' ) -); diff --git a/src/wizards/health-check/views/configuration/index.js b/src/wizards/health-check/views/configuration/index.js deleted file mode 100644 index df8499329b..0000000000 --- a/src/wizards/health-check/views/configuration/index.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Notify about site misconfigurations. - */ - -/** - * WordPress dependencies - */ -import { Component, Fragment } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { ActionCard, withWizardScreen } from '../../../../components/src'; - -/** - * SEO Intro screen. - */ -class Configuration extends Component { - /** - * Render. - */ - render() { - const { configurationStatus, hasData } = this.props; - const { jetpack, sitekit } = configurationStatus || {}; - return ( - hasData && ( - <Fragment> - <ActionCard - className={ jetpack ? 'newspack-card__is-supported' : 'newspack-card__is-unsupported' } - title={ __( 'Jetpack', 'newspack-plugin' ) } - description={ - jetpack - ? __( 'Jetpack is connected.', 'newspack-plugin' ) - : __( 'Jetpack is not connected. ', 'newspack-plugin' ) - } - actionText={ ! jetpack && __( 'Connect', 'newspack-plugin' ) } - handoff="jetpack" - /> - <ActionCard - className={ sitekit ? 'newspack-card__is-supported' : 'newspack-card__is-unsupported' } - title={ __( 'Google Site Kit', 'newspack-plugin' ) } - description={ - sitekit - ? __( 'Site Kit is connected.', 'newspack-plugin' ) - : __( 'Site Kit is not connected. ', 'newspack-plugin' ) - } - actionText={ ! sitekit && __( 'Connect', 'newspack-plugin' ) } - handoff="google-site-kit" - /> - </Fragment> - ) - ); - } -} - -export default withWizardScreen( Configuration ); diff --git a/src/wizards/health-check/views/index.js b/src/wizards/health-check/views/index.js deleted file mode 100644 index 723f378c77..0000000000 --- a/src/wizards/health-check/views/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as Plugins } from './plugins'; -export { default as Configuration } from './configuration'; diff --git a/src/wizards/health-check/views/plugins/index.js b/src/wizards/health-check/views/plugins/index.js deleted file mode 100644 index 1efb1b4042..0000000000 --- a/src/wizards/health-check/views/plugins/index.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Remove unsupported plugins. - */ - -/** - * WordPress dependencies - */ -import { Component } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { - ActionCard, - Button, - Grid, - PluginInstaller, - Notice, - withWizardScreen, -} from '../../../../components/src'; - -/** - * SEO Intro screen. - */ -class Plugins extends Component { - /** - * Render. - */ - render() { - const { unsupportedPlugins, missingPlugins, deactivateAllPlugins } = this.props; - return ( - <Grid columns={ 1 } gutter={ 64 }> - { missingPlugins.length ? ( - <Grid columns={ 1 } gutter={ 16 }> - <Notice - noticeText={ __( 'These plugins shoud be active:', 'newspack-plugin' ) } - isWarning - /> - <PluginInstaller plugins={ missingPlugins } /> - </Grid> - ) : null } - { unsupportedPlugins.length ? ( - <Grid columns={ 1 } gutter={ 16 }> - <Notice - noticeText={ __( 'Newspack does not support these plugins:', 'newspack-plugin' ) } - isError - /> - { unsupportedPlugins.map( unsupportedPlugin => ( - <ActionCard - title={ unsupportedPlugin.Name } - key={ unsupportedPlugin.Slug } - description={ unsupportedPlugin.Description } - className="newspack-card__is-unsupported" - /> - ) ) } - <div className="newspack-buttons-card"> - <Button isPrimary onClick={ deactivateAllPlugins }> - { __( 'Deactivate All', 'newspack-plugin' ) } - </Button> - </div> - </Grid> - ) : ( - <Notice - noticeText={ __( 'No unsupported plugins found.', 'newspack-plugin' ) } - isSuccess - /> - ) } - </Grid> - ); - } -} - -export default withWizardScreen( Plugins ); diff --git a/src/wizards/hooks/use-fields-validation.ts b/src/wizards/hooks/use-fields-validation.ts new file mode 100644 index 0000000000..231166dec2 --- /dev/null +++ b/src/wizards/hooks/use-fields-validation.ts @@ -0,0 +1,84 @@ +/** + * Hook for validating form fields. + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { WizardError } from '../errors'; + +/** + * Known common validation callbacks. + */ +const knownValidationCallbacks = { + /** + * Is string a valid ID? + * + * @param value ID string to test + * @param errorMessage Optional error message to display on failure + * @return Empty string if ID is valid, error message otherwise + */ + isId( value: string, errorMessage: string = __( 'Field cannot be empty!', 'newspack-plugin' ) ) { + return /^[A-Za-z0-9_-]*$/.test( value ) ? '' : errorMessage; + }, + /** + * Is string a valid url? + * + * @param value Url string to test + * @param errorMessage Optional error message to display on failure + * @return Empty string if URL is valid, error message otherwise + */ + isUrl( value: string, errorMessage: string = __( 'Invalid URL!', 'newspack-plugin' ) ) { + return '' === value || /^https?:\/\/[^\s]*$/.test( value ) ? '' : errorMessage; + }, +}; + +/** + * Array of tupils where each tupil contains: + * 1. Field name + * 2. Validation callback name or custom validation callback + * 3. (Optional) Configuration object + */ +type ValidationMap< TData, TConfig > = [ + keyof TData, + keyof typeof knownValidationCallbacks | ( ( inputValue: string ) => string ), + ( TConfig & { + dependsOn?: { [ k in keyof TData ]?: string }; + message?: string; + } )? +][]; + +/** + * React hook for validating form fields. + */ +export function useFieldsValidation< TData, TConfig = Record< string, unknown > >( + config: ValidationMap< TData, TConfig >, + data: TData +) { + const [ errorMessage, setErrorMessage ] = useState< WizardError | null >( null ); + return { + isInputsValid() { + for ( const [ key, callback, options ] of config ) { + const inputValue = data[ key ] as string; + const isFieldValid = ( + typeof callback === 'string' ? knownValidationCallbacks[ callback ] : callback + )( inputValue, options?.message ); + if ( '' !== isFieldValid ) { + setErrorMessage( new WizardError( isFieldValid, `invalid_field_${ key.toString() }` ) ); + return false; + } + } + setErrorMessage( null ); + return true; + }, + errorMessage: errorMessage instanceof WizardError ? errorMessage.message : '', + }; +} + +export default useFieldsValidation; diff --git a/src/wizards/hooks/use-wizard-api-fetch-toggle.ts b/src/wizards/hooks/use-wizard-api-fetch-toggle.ts new file mode 100644 index 0000000000..a427389016 --- /dev/null +++ b/src/wizards/hooks/use-wizard-api-fetch-toggle.ts @@ -0,0 +1,86 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; +import { useState, useEffect, createElement } from '@wordpress/element'; + +/** + * Internal dependencies. + */ +import { Waiting } from '../../components/src'; +import { useWizardApiFetch } from './use-wizard-api-fetch'; + +/** + * Hook to perform toggle operations using the Wizard API. + */ +function useWizardApiFetchToggle< T >( { + path, + apiNamespace, + refreshOn = [], + data, + description, +}: { + path: `/newspack/v${ string }`; + apiNamespace: string; + refreshOn?: ApiMethods[]; + data: T; + description: string; +} ) { + const [ apiData, setApiData ] = useState< T >( data ); + + const [ actionText, setActionText ] = useState< React.ReactNode >( null ); + + const { wizardApiFetch, isFetching, errorMessage } = useWizardApiFetch( apiNamespace ); + + /** + * Perform `GET` request on initial load. + */ + useEffect( () => { + apiFetchToggle(); + }, [] ); + + /** + * Toggle function for the Wizard API fetch. + * + * @param dataToSend Data to send to endpoint. + * @param isToggleOn If set method will default to POST, otherwise GET. + */ + function apiFetchToggle( dataToSend?: T, isToggleOn?: boolean ) { + const method = typeof isToggleOn === 'boolean' && isToggleOn ? 'POST' : 'GET'; + + const options: ApiFetchOptions = { + path, + method, + }; + if ( dataToSend ) { + options.data = dataToSend; + } + wizardApiFetch< T >( options, { + onSuccess: setApiData, + onFinally() { + if ( refreshOn.includes( method ) ) { + setActionText( + createElement( + 'span', + { className: 'gray' }, + __( 'Page reloading…', 'newspack-plugin' ) + ) + ); + if ( ! errorMessage ) { + window.location.reload(); + } + } + }, + } ); + } + return { + actionText: isFetching ? createElement( Waiting ) : actionText, + apiData, + apiFetchToggle, + description: isFetching ? __( 'Loading…', 'newspack-plugin' ) : description, + errorMessage, + isFetching, + }; +} + +export default useWizardApiFetchToggle; diff --git a/src/wizards/hooks/use-wizard-api-fetch.ts b/src/wizards/hooks/use-wizard-api-fetch.ts new file mode 100644 index 0000000000..20502c87de --- /dev/null +++ b/src/wizards/hooks/use-wizard-api-fetch.ts @@ -0,0 +1,306 @@ +/** + * Custom hook for making API fetch requests using the wizard API. + */ + +/** + * WordPress dependencies + */ +import { useDispatch, useSelect } from '@wordpress/data'; +import { decodeEntities } from '@wordpress/html-entities'; +import { useState, useCallback, useEffect, useRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { WIZARD_STORE_NAMESPACE } from '../../components/src/wizard/store'; +import { WizardApiError } from '../errors'; + +/** + * Remove query arguments from a path. Similar to `removeQueryArgs` in `@wordpress/url`, but this function + * removes all query arguments from a string and returns it. + * + * @param str String to remove query arguments from. + * @return The string without query arguments. + */ +function removeQueryArgs( str: string ) { + return str.split( '?' ).at( 0 ) ?? str; +} + +/** + * Holds in-progress promises for each fetch request. + */ +let promiseCache: Record< string, any > = {}; + +/** + * Parses the API error response into a WizardApiError object. + * + * @param error The error response from the API. + * @return Parsed error object or null if no error. + */ +const parseApiError = ( + error: WpFetchError | string +): WizardApiError | null => { + const newError = { + message: 'An unknown API error occurred.', + statusCode: 500, + errorCode: 'api_unknown_error', + details: '', + }; + + if ( ! error ) { + return null; + } else if ( typeof error === 'string' ) { + newError.message = error; + } else if ( error instanceof Error || 'message' in error ) { + newError.message = error.message ?? newError.message; + newError.statusCode = error.data?.status ?? newError.statusCode; + newError.errorCode = error.code ?? newError.errorCode; + newError.details = ''; + } + + return new WizardApiError( + newError.message, + newError.statusCode, + newError.errorCode, + newError.details + ); +}; + +/** + * Executes the provided callback function if it exists. + * + * @template T + * @param callbacks Object containing callback functions. + * @return Object with an `on` method to trigger callbacks. + */ +const onCallbacks = < T >( callbacks: ApiFetchCallbacks< T > ) => ( { + on( cb: keyof ApiFetchCallbacks< T >, d: any = null ) { + const callback = callbacks?.[ cb ]; + if ( callback && typeof callback === 'function' ) { + callback( d ); + } + }, +} ); + +/** + * Custom hook to perform API fetch requests using the wizard API. + * + * @param slug Unique identifier for the wizard data. + * @return Object containing fetch function, error handlers and state. + */ +export function useWizardApiFetch( slug: string ) { + const [ isFetching, setIsFetching ] = useState( false ); + const { wizardApiFetch, updateWizardSettings } = useDispatch( + WIZARD_STORE_NAMESPACE + ); + const wizardData: WizardData = useSelect( + ( select: ( namespace: string ) => WizardSelector ) => + select( WIZARD_STORE_NAMESPACE ).getWizardData( slug ), + [ slug ] + ); + const [ error, setError ] = useState< WizardApiError | null >( + wizardData.error ?? null + ); + + const requests = useRef< string[] >( [] ); + + useEffect( () => { + updateWizardSettings( { + slug, + path: [ 'error' ], + value: error, + } ); + }, [ error, updateWizardSettings, slug ] ); + + function resetError() { + setError( null ); + } + + /** + * Updates the wizard data at the specified path. + * + * @param cacheKeyPath The cacheKeyPath to update in the wizard data. + * @return Function to update the wizard data. + */ + function updateWizardData( cacheKeyPath: string | null ) { + /** + * Updates the wizard data prop at the specified path. + * + * @param prop The property to update in the wizard path data. i.e. 'GET' + * @param value The value to set for the property. + * @param cacheKeyPathOverride The path to update in the wizard data. + */ + return ( + prop: string | string[], + value: any, + cacheKeyPathOverride = cacheKeyPath + ) => { + // Remove query parameters from the cacheKeyPath + + const normalizedPath = cacheKeyPathOverride + ? removeQueryArgs( cacheKeyPathOverride ) + : cacheKeyPathOverride; + + updateWizardSettings( { + slug, + path: [ + normalizedPath, + ...( Array.isArray( prop ) ? prop : [ prop ] ), + ].filter( str => typeof str === 'string' ), + value, + } ); + }; + } + + /** + * Makes an API fetch request using the wizard API. + * + * @template T + * @param opts The options for the API fetch request. + * @param [callbacks] Optional callback functions for different stages of the fetch request. + * @return The result of the API fetch request. + */ + const apiFetch = useCallback( + async < T = any >( + opts: ApiFetchOptions, + callbacks?: ApiFetchCallbacks< T > + ) => { + const { on } = onCallbacks< T >( callbacks ?? {} ); + const updateSettings = updateWizardData( opts.path ); + const { path, method = 'GET' } = opts; + const cacheKeyPath = removeQueryArgs( path ?? '' ); + const { + isCached = method === 'GET', + updateCacheKey = null, + updateCacheMethods = [], + ...options + } = opts; + + const { + error: cachedError, + [ cacheKeyPath ]: { [ method ]: cachedMethod = null } = {}, + }: WizardData = wizardData; + + function thenCallback( response: T ) { + if ( isCached ) { + updateSettings( method, response ); + } + + if ( updateCacheKey && updateCacheKey.constructor === Object ) { + // Derive the key and method from the updateCacheKey object. + const [ updateCacheKeyKey, updateCacheKeyMethod ]: [ + keyof WizardData, + ApiMethods, + ] = Object.entries( updateCacheKey )[ 0 ]; + + const cachedValue = + wizardData[ updateCacheKeyKey ][ updateCacheKeyMethod ]; + + let newCache; + + if ( cachedValue && cachedValue.constructor === Object ) { + newCache = { + ...cachedValue, + ...response, + }; + } else { + newCache = response; + } + + updateSettings( + Object.entries( updateCacheKey )[ 0 ], + newCache, + null + ); + } + + for ( const replaceMethod of updateCacheMethods ) { + updateSettings( replaceMethod, response ); + } + on( 'onSuccess', response ); + return response; + } + + function catchCallback( err: WpFetchError ) { + const newError = parseApiError( err ); + setError( newError ); + on( 'onError', newError ); + throw newError; + } + + function finallyCallback() { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { [ cacheKeyPath ]: _removed, ...newData } = promiseCache; + promiseCache = newData; + requests.current = requests.current.filter( + request => request !== cacheKeyPath + ); + setIsFetching( requests.current.length > 0 ); + on( 'onFinally' ); + } + + // If the promise is already in progress, return it before making a new request. + if ( promiseCache[ cacheKeyPath ] ) { + setIsFetching( true ); + return promiseCache[ cacheKeyPath ] + .then( thenCallback ) + .catch( catchCallback ) + .finally( finallyCallback ); + } + + // Cache exists and is not empty, return it. + if ( isCached && ( cachedError || cachedMethod ) ) { + setError( cachedError ); + on( 'onSuccess', cachedMethod ); + return cachedMethod; + } + + setIsFetching( true ); + on( 'onStart' ); + requests.current.push( cacheKeyPath ); + + promiseCache[ cacheKeyPath ] = wizardApiFetch( { + isQuietFetch: true, + isLocalError: true, + ...options, + } ) + .then( thenCallback ) + .catch( catchCallback ) + .finally( finallyCallback ); + + return promiseCache[ slug ]; + }, + [ wizardApiFetch, wizardData, updateWizardSettings, isFetching, slug ] + ); + + return { + wizardApiFetch: apiFetch, + isFetching, + errorMessage: error ? decodeEntities( error.message ) : null, + error, + cache( cacheKey: string ) { + return { + get( method: ApiMethods = 'GET' ) { + return wizardData[ cacheKey ][ method ]; + }, + set( value: any, method: ApiMethods = 'GET' ) { + updateWizardSettings( { + slug, + path: [ cacheKey, method ], + value, + } ); + }, + }; + }, + setError( + value: string | WizardErrorType | null | { message: string } + ) { + if ( value === null ) { + resetError(); + } else { + setError( parseApiError( value as WpFetchError ) ); + } + }, + resetError, + }; +} diff --git a/src/wizards/index.tsx b/src/wizards/index.tsx new file mode 100644 index 0000000000..8c0afbfff2 --- /dev/null +++ b/src/wizards/index.tsx @@ -0,0 +1,124 @@ +/** + * Newspack - Dashboard + * + * WP Admin Newspack Dashboard page. + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { render, lazy, Suspense } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import * as Components from '../components/src'; + +/** + * Internal dependencies + */ +import '../shared/js/public-path'; + +const pageParam = + new URLSearchParams( window.location.search ).get( 'page' ) ?? ''; +const rootElement = document.getElementById( pageParam ); + +const components: Record< string, any > = { + /** + * `page` param with `newspack-*`. + */ + 'newspack-dashboard': { + label: __( 'Dashboard', 'newspack-plugin' ), + component: lazy( + () => + import( + /* webpackChunkName: "newspack-wizards" */ './newspack/views/dashboard' + ) + ), + }, + 'newspack-settings': { + label: __( 'Settings', 'newspack-plugin' ), + component: lazy( + () => + import( + /* webpackChunkName: "newspack-wizards" */ './newspack/views/settings' + ) + ), + }, + 'newspack-audience': { + label: __( 'Audience', 'newspack-plugin' ), + component: lazy( + () => + import( + /* webpackChunkName: "audience-wizards" */ './audience/views/setup' + ) + ), + }, + 'newspack-audience-campaigns': { + label: __( 'Audience Campaigns', 'newspack-plugin' ), + component: lazy( + () => + import( + /* webpackChunkName: "audience-wizards" */ './audience/views/campaigns' + ) + ), + }, + 'newspack-audience-donations': { + label: __( 'Audience Donations', 'newspack-plugin' ), + component: lazy( + () => + import( + /* webpackChunkName: "audience-wizards" */ './audience/views/donations' + ) + ), + }, + 'newspack-audience-subscriptions': { + label: __( 'Audience Subscriptions', 'newspack-plugin' ), + component: lazy( + () => + import( + /* webpackChunkName: "audience-wizards" */ './audience/views/subscriptions' + ) + ), + }, +} as const; + +const AdminPageLoader = ( { label }: { label: string } ) => { + return ( + <div className="newspack-wizard__loader"> + <div> + <Components.Waiting + style={ { + height: '50px', + width: '50px', + } } + isCenter + /> + <span> + { label } { __( 'loading', 'newspack-plugin' ) }… + </span> + </div> + </div> + ); +}; + +const AdminPages = () => { + const PageComponent = components[ pageParam ].component; + return ( + <Suspense + fallback={ + <AdminPageLoader label={ components[ pageParam ].label } /> + } + > + <PageComponent /> + </Suspense> + ); +}; + +if ( rootElement && pageParam in components ) { + render( <AdminPages />, rootElement ); +} else { + // eslint-disable-next-line no-console + console.error( `${ pageParam } not found!` ); +} diff --git a/src/wizards/newsletters/index.js b/src/wizards/newsletters/index.js new file mode 100644 index 0000000000..c46a9f5941 --- /dev/null +++ b/src/wizards/newsletters/index.js @@ -0,0 +1,72 @@ +import '../../shared/js/public-path'; + +/** + * Advertising + */ + +/** + * WordPress dependencies. + */ +import { Component, render, Fragment, createElement } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies. + */ +import { withWizard } from '../../components/src'; +import Router from '../../components/src/proxied-imports/router'; +import { Settings, Tracking } from './views'; + +const { HashRouter, Redirect, Route, Switch } = Router; + +class NewslettersWizard extends Component { + /** + * Render + */ + render() { + const { pluginRequirements } = this.props; + const tabs = [ + { + label: __( 'Settings', 'newspack-plugin' ), + path: '/', + }, + { + label: __( 'Tracking', 'newspack-plugin' ), + path: '/tracking', + }, + ]; + return ( + <Fragment> + <HashRouter hashType="slash"> + <Switch> + { pluginRequirements } + <Route + path="/" + exact + render={ () => ( + <Settings + headerText={ __( 'Newsletters / Settings', 'newspack-plugin' ) } + tabbedNavigation={ tabs } + /> + ) } + /> + <Route + path="/tracking" + render={ () => ( + <Tracking + headerText={ __( 'Newsletters / Tracking', 'newspack-plugin' ) } + tabbedNavigation={ tabs } + /> + ) } + /> + <Redirect to="/" /> + </Switch> + </HashRouter> + </Fragment> + ); + } +} +render( + createElement( withWizard( NewslettersWizard, [ 'newspack-newsletters' ] ) ), + document.getElementById( 'newspack-newsletters' ) +); diff --git a/src/wizards/seo/views/index.js b/src/wizards/newsletters/views/index.js similarity index 50% rename from src/wizards/seo/views/index.js rename to src/wizards/newsletters/views/index.js index ab6044375c..0bf75f1d8d 100644 --- a/src/wizards/seo/views/index.js +++ b/src/wizards/newsletters/views/index.js @@ -1 +1,2 @@ export { default as Settings } from './settings'; +export { default as Tracking } from './tracking'; diff --git a/src/wizards/engagement/views/newsletters/index.js b/src/wizards/newsletters/views/settings/index.js similarity index 76% rename from src/wizards/engagement/views/newsletters/index.js rename to src/wizards/newsletters/views/settings/index.js index 32c614b2cc..cb9c14045b 100644 --- a/src/wizards/engagement/views/newsletters/index.js +++ b/src/wizards/newsletters/views/settings/index.js @@ -1,4 +1,4 @@ -/* global newspack_engagement_wizard */ +/* global newspack_newsletters_wizard */ /** * Internal dependencies */ @@ -34,8 +34,7 @@ import { import './style.scss'; -export const NewspackNewsletters = ( { - className, +export const Settings = ( { onUpdate, initialProvider, newslettersConfig, @@ -100,7 +99,7 @@ export const NewspackNewsletters = ( { const fetchConfiguration = () => { setError( false ); apiFetch( { - path: '/newspack/v1/wizard/newspack-engagement-wizard/newsletters', + path: '/newspack/v1/wizard/newspack-newsletters/settings', } ) .then( performConfigUpdate ) .catch( setError ); @@ -124,7 +123,7 @@ export const NewspackNewsletters = ( { setError( false ); setInFlight( true ); apiFetch( { - path: '/newspack/v1/wizard/newspack-engagement-wizard/newsletters', + path: '/newspack/v1/wizard/newspack-newsletters/settings', method: 'POST', data: newslettersConfig, } ).finally( () => { @@ -242,7 +241,7 @@ export const NewspackNewsletters = ( { } return ( - <div className={ className }> + <> { config.configured === false && ( <PluginInstaller plugins={ [ 'newspack-newsletters' ] } @@ -251,7 +250,7 @@ export const NewspackNewsletters = ( { /> ) } { config.configured === true && renderProviderSettings() } - </div> + </> ); }; @@ -307,95 +306,91 @@ export const SubscriptionLists = ( { lockedLists, onUpdate, initialProvider } ) } return ( - <> - <ActionCard - isMedium - title={ __( 'Subscription Lists', 'newspack-plugin' ) } - description={ __( - 'Manage the lists available to readers for subscription.', - 'newspack-plugin' - ) } - notification={ - /* eslint-disable no-nested-ternary */ - error - ? error?.message || __( 'Something went wrong.', 'newspack-plugin' ) - : lockedLists - ? __( - 'Please save your ESP settings before changing your subscription lists.', - 'newspack-plugin' + <ActionCard + isMedium + title={ __( 'Subscription Lists', 'newspack-plugin' ) } + description={ __( + 'Manage the lists available to readers for subscription.', + 'newspack-plugin' + ) } + notification={ + /* eslint-disable no-nested-ternary */ + error + ? error?.message || __( 'Something went wrong.', 'newspack-plugin' ) + : lockedLists + ? __( + 'Please save your ESP settings before changing your subscription lists.', + 'newspack-plugin' + ) : null + } + notificationLevel={ error ? 'error' : 'warning' } + hasGreyHeader + actionContent={ + <> + { newspack_newsletters_wizard.new_subscription_lists_url && ( + <Button + variant="secondary" + disabled={ inFlight || lockedLists } + href={ newspack_newsletters_wizard.new_subscription_lists_url } + > + { __( 'Add New', 'newspack-plugin' ) } + </Button> + ) } + <Button isPrimary onClick={ saveLists } disabled={ inFlight || lockedLists }> + { __( 'Save Subscription Lists', 'newspack-plugin' ) } + </Button> + </> + } + disabled={ inFlight || lockedLists } + > + { ! lockedLists && + lists.map( ( list, index ) => ( + <ActionCard + key={ index } + isSmall + simple + hasWhiteHeader + title={ list.name } + description={ list?.type_label ? list.type_label : null } + disabled={ inFlight } + toggleOnChange={ handleChange( index, 'active' ) } + toggleChecked={ list.active } + className={ + list?.id && ( list.id.startsWith( 'group' ) || list.id.startsWith( 'tag' ) ) + ? 'newspack-newsletters-sub-list-item' + : '' + } + actionText={ + list?.edit_link ? ( + <ExternalLink href={ list.edit_link }> + { __( 'Edit', 'newspack-plugin' ) } + </ExternalLink> ) : null - } - notificationLevel={ error ? 'error' : 'warning' } - hasGreyHeader - actionContent={ - <> - { newspack_engagement_wizard.new_subscription_lists_url && ( - <Button - variant="secondary" - disabled={ inFlight || lockedLists } - href={ newspack_engagement_wizard.new_subscription_lists_url } - > - { __( 'Add New', 'newspack-plugin' ) } - </Button> + } + > + { list.active && 'local' !== list?.type && ( + <> + <TextControl + label={ __( 'List title', 'newspack-plugin' ) } + value={ list.title } + disabled={ inFlight || 'local' === list?.type } + onChange={ handleChange( index, 'title' ) } + /> + <TextareaControl + label={ __( 'List description', 'newspack-plugin' ) } + value={ list.description } + disabled={ inFlight || 'local' === list?.type } + onChange={ handleChange( index, 'description' ) } + /> + </> ) } - <Button isPrimary onClick={ saveLists } disabled={ inFlight || lockedLists }> - { __( 'Save Subscription Lists', 'newspack-plugin' ) } - </Button> - </> - } - disabled={ inFlight || lockedLists } - > - { ! lockedLists && - lists.map( ( list, index ) => ( - <ActionCard - key={ list.id } - isSmall - simple - hasWhiteHeader - title={ list.remote_name } - description={ - list?.type_label ? list.type_label : null - } - disabled={ inFlight } - toggleOnChange={ handleChange( index, 'active' ) } - toggleChecked={ list.active } - className={ - list?.id && ( list.id.startsWith( 'group' ) || list.id.startsWith( 'tag' ) ) - ? 'newspack-newsletters-sub-list-item' - : '' - } - actionText={ - list?.edit_link ? ( - <ExternalLink href={ list.edit_link }> - { __( 'Edit', 'newspack-plugin' ) } - </ExternalLink> - ) : null - } - > - { list.active && 'local' !== list?.type && ( - <> - <TextControl - label={ __( 'List title', 'newspack-plugin' ) } - value={ list.title } - disabled={ inFlight || 'local' === list?.type } - onChange={ handleChange( index, 'title' ) } - /> - <TextareaControl - label={ __( 'List description', 'newspack-plugin' ) } - value={ list.description } - disabled={ inFlight || 'local' === list?.type } - onChange={ handleChange( index, 'description' ) } - /> - </> - ) } - </ActionCard> - ) ) } - </ActionCard> - </> + </ActionCard> + ) ) } + </ActionCard> ); }; -const Newsletters = () => { +const NewslettersSettings = () => { const [ { newslettersConfig }, updateConfiguration ] = hooks.useObjectState( {} ); const [ initialProvider, setInitialProvider ] = useState( '' ); const [ lockedLists, setLockedLists ] = useState( false ); @@ -403,7 +398,8 @@ const Newsletters = () => { return ( <> - <NewspackNewsletters + <h1>{ __( 'Settings', 'newspack-plugin' ) }</h1> + <Settings isOnboarding={ false } onUpdate={ config => updateConfiguration( { newslettersConfig: config } ) } authUrl={ authUrl } @@ -420,6 +416,12 @@ const Newsletters = () => { export default withWizardScreen( () => ( <> - <Newsletters /> + <NewslettersSettings /> + <hr /> + <h2>{ __( 'WooCommerce Integration', 'newspack-plugin' ) }</h2> + <PluginInstaller + plugins={ [ 'mailchimp-for-woocommerce' ] } + withoutFooterButton + /> </> ) ); diff --git a/src/wizards/engagement/views/newsletters/style.scss b/src/wizards/newsletters/views/settings/style.scss similarity index 100% rename from src/wizards/engagement/views/newsletters/style.scss rename to src/wizards/newsletters/views/settings/style.scss diff --git a/src/wizards/newsletters/views/tracking/index.js b/src/wizards/newsletters/views/tracking/index.js new file mode 100644 index 0000000000..2069bd5990 --- /dev/null +++ b/src/wizards/newsletters/views/tracking/index.js @@ -0,0 +1,72 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useEffect, useState } from '@wordpress/element'; +import apiFetch from '@wordpress/api-fetch'; + +/** + * Internal dependencies + */ +import { ActionCard, withWizardScreen } from '../../../../components/src'; + +const apiPath = '/newspack/v1/wizard/newspack-newsletters/settings/tracking'; + +export default withWizardScreen( () => { + const [ inFlight, setInFlight ] = useState( false ); + const [ tracking, setTracking ] = useState( {} ); + + const fetchData = () => { + setInFlight( true ); + apiFetch( { path: apiPath } ) + .then( response => { + setTracking( response ); + } ) + .finally( () => { + setInFlight( false ); + } ) + }; + + const handleChange = type => async ( value ) => { + const newData = { + ...tracking, + [ type ]: value, + }; + setInFlight( true ); + apiFetch( { + path: apiPath, + method: 'POST', + data: newData + } ) + .then( () => { + setTracking( newData ); + } ) + .finally( () => { + setInFlight( false ); + } ); + }; + + useEffect( () => { + fetchData(); + }, [] ); + + return ( + <> + <h1>{ __( 'Tracking', 'newspack-plugin' ) }</h1> + <ActionCard + title={ __( 'Click-tracking', 'newspack-plugin' ) } + description={ __( 'Track the clicks on the links in your newsletter.', 'newspack-plugin' ) } + disabled={ inFlight } + toggleOnChange={ handleChange( 'click' ) } + toggleChecked={ tracking.click } + /> + <ActionCard + title={ __( 'Tracking pixel', 'newspack-plugin' ) } + description={ __( 'Track the opens of your newsletter.', 'newspack-plugin' ) } + disabled={ inFlight } + toggleOnChange={ handleChange( 'pixel' ) } + toggleChecked={ tracking.pixel } + /> + </> + ); +} ); diff --git a/src/wizards/newspack/components/brand-header.tsx b/src/wizards/newspack/components/brand-header.tsx new file mode 100644 index 0000000000..f12071f597 --- /dev/null +++ b/src/wizards/newspack/components/brand-header.tsx @@ -0,0 +1,24 @@ +/** + * Newspack Dashboard, Brand-Header + * + * Displaying stored logo and header bg color in a header + */ + +import { BoxContrast } from '../../../components/src'; + +const { settings } = window.newspackDashboard; + +const BrandHeader = () => { + return ( + <header + className="newspack-dashboard__brand-header" + style={ { backgroundColor: settings.headerBgColor } } + > + <BoxContrast className="brand-header__inner" hexColor={ settings.headerBgColor }> + <h1>{ settings.siteName }</h1> + </BoxContrast> + </header> + ); +}; + +export default BrandHeader; diff --git a/src/wizards/newspack/components/icons/gift.tsx b/src/wizards/newspack/components/icons/gift.tsx new file mode 100644 index 0000000000..24b60c4f7e --- /dev/null +++ b/src/wizards/newspack/components/icons/gift.tsx @@ -0,0 +1,21 @@ +/** + * Newspack Dashboard Icons, Gift + */ + +/** + * WordPress dependencies + */ +import SVG from './svg'; +import { Path } from '@wordpress/primitives'; + +const gift = ( + <SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <Path + fillRule="evenodd" + d="M14.75 9a2.5 2.5 0 0 0 0-5 2.5 2.5 0 0 0-2.5 2.5 2.5 2.5 0 0 0-5 0A2.5 2.5 0 0 0 9.75 9H4v11h16V9h-5.25Zm-1-2.5c0-.55.45-1 1-1s1 .45 1 1-.45 1-1 1h-1v-1Zm-5 0c0-.55.45-1 1-1s1 .45 1 1v1h-1c-.55 0-1-.45-1-1Zm-3.25 4h6v8h-6v-8Zm13 8H13v-8h5.5v8Z" + clipRule="evenodd" + /> + </SVG> +); + +export default gift; diff --git a/src/wizards/newspack/components/icons/index.ts b/src/wizards/newspack/components/icons/index.ts new file mode 100644 index 0000000000..2818322fee --- /dev/null +++ b/src/wizards/newspack/components/icons/index.ts @@ -0,0 +1,63 @@ +/** + * Newspack Dashboard, Icons + */ + +/** + * WordPress dependencies + */ +export { Icon } from '@wordpress/icons'; +import { + chartBar, + currencyDollar, + envelope, + formatListBullets, + globe, + help, + mapMarker, + megaphone, + payment, + plus, + post, + postDate, + postList, + pullquote, + rotateRight, + settings, + store, + tool, + trash, +} from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import gift from './gift'; + +/** + * Export Dashboard Icons + */ +export const icons = { + chartBar, + currencyDollar, + envelope, + formatListBullets, + globe, + help, + mapMarker, + megaphone, + payment, + plus, + post, + postDate, + postList, + pullquote, + rotateRight, + settings, + store, + tool, + trash, + // Custom + gift, +}; + +export default icons; diff --git a/src/wizards/newspack/components/icons/svg.tsx b/src/wizards/newspack/components/icons/svg.tsx new file mode 100644 index 0000000000..1a673c5197 --- /dev/null +++ b/src/wizards/newspack/components/icons/svg.tsx @@ -0,0 +1,14 @@ +/** + * Newspack Dashboard Icons, Block-Post-Date + */ + +/** + * WordPress dependencies + */ +import { SVG } from '@wordpress/primitives'; + +function wizardSvg( { children, ...props }: any ) { + return <SVG { ...props }>{ children }</SVG>; +} + +export default wizardSvg; diff --git a/src/wizards/newspack/components/quick-actions.tsx b/src/wizards/newspack/components/quick-actions.tsx new file mode 100644 index 0000000000..6d7a13db2c --- /dev/null +++ b/src/wizards/newspack/components/quick-actions.tsx @@ -0,0 +1,343 @@ +/** + * Newspack - Dashboard, Quick Actions + * + * Quick Actions component provides editors quick access to content creation and viewing data relating to their site + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState, useEffect } from '@wordpress/element'; +import { Spinner } from '@wordpress/components'; +import apiFetch from '@wordpress/api-fetch'; + +/** + * Internal dependencies + */ +import { Card, Grid, Button } from '../../../components/src'; +import { Icon, icons } from './icons'; + +interface QuickAction { + href: string; + title: string; + icon: keyof typeof icons; + id: string; +} + +interface DragData { + action: QuickAction; + isActive: boolean; +} + +const MAX_ACTIVE_ACTIONS = 3; + +const QuickActions = () => { + const [isEditing, setIsEditing] = useState<boolean>(false); + const [isSaving, setIsSaving] = useState<boolean>(false); + const [activeActions, setActiveActions] = useState<QuickAction[]>([]); + const [inactiveActions, setInactiveActions] = useState<QuickAction[]>([]); + const [isDragging, setIsDragging] = useState<boolean>(false); + const [isDragOverRemove, setIsDragOverRemove] = useState<boolean>(false); + const [isDraggingActive, setIsDraggingActive] = useState<boolean>(false); + const [isDraggingInactive, setIsDraggingInactive] = useState<boolean>(false); + + // Initialize state from window object + useEffect(() => { + const { quickActions = [], availableQuickActions = [] } = (window as any).newspackDashboard || {}; + if (Array.isArray(quickActions) && Array.isArray(availableQuickActions)) { + setActiveActions(quickActions); + setInactiveActions( + availableQuickActions.filter((action: QuickAction) => !quickActions.find((qa: QuickAction) => qa.id === action.id)) + ); + } + }, []); + + useEffect(() => { + document.addEventListener('dragend', handleDragEnd); + return () => document.removeEventListener('dragend', handleDragEnd); + }, []); + + const handleDragStart = (e: React.DragEvent, action: QuickAction, isActive: boolean) => { + e.dataTransfer.setData('text/plain', JSON.stringify({ action, isActive })); + setIsDragging(true); + setIsDraggingActive(isActive); + setIsDraggingInactive(!isActive); + }; + + const handleDragEnd = () => { + setIsDragging(false); + setIsDragOverRemove(false); + setIsDraggingActive(false); + setIsDraggingInactive(false); + document.querySelectorAll('.is-drop-target').forEach(el => el.classList.remove('is-drop-target')); + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + if (isDraggingActive) { + const rect = e.currentTarget.getBoundingClientRect(); + setIsDragOverRemove(e.clientY > rect.bottom - 80); + } + }; + + const handleRemoveAction = (draggedAction: QuickAction) => { + setActiveActions(activeActions.filter(a => a.id !== draggedAction.id)); + setInactiveActions([...inactiveActions, draggedAction]); + }; + + const handleReorderWithinList = (draggedAction: QuickAction, dropIndex: number, isDragActive: boolean) => { + const sourceList = isDragActive ? activeActions : inactiveActions; + const sourceIndex = sourceList.findIndex(a => a.id === draggedAction.id); + const newList = [...sourceList]; + newList.splice(sourceIndex, 1); + newList.splice(dropIndex, 0, draggedAction); + + if (isDragActive) { + setActiveActions(newList); + } else { + setInactiveActions(newList); + } + }; + + const handleMoveToInactive = (draggedAction: QuickAction, dropIndex: number) => { + setActiveActions(activeActions.filter(a => a.id !== draggedAction.id)); + const newInactive = [...inactiveActions]; + newInactive.splice(dropIndex, 0, draggedAction); + setInactiveActions(newInactive); + }; + + const handleMoveToActive = (draggedAction: QuickAction, dropIndex: number) => { + if (dropIndex >= MAX_ACTIVE_ACTIONS) { + return; + } + + const newActive = [...activeActions]; + if (newActive.length >= MAX_ACTIVE_ACTIONS) { + const replacedAction = newActive[dropIndex]; + newActive[dropIndex] = draggedAction; + setActiveActions(newActive); + const newInactive = [...inactiveActions.filter(a => a.id !== draggedAction.id), replacedAction]; + setInactiveActions(newInactive); + } else { + newActive.splice(dropIndex, 0, draggedAction); + setActiveActions(newActive); + setInactiveActions(inactiveActions.filter(a => a.id !== draggedAction.id)); + } + }; + + const handleDrop = (e: React.DragEvent, dropIndex?: number, isDropActive?: boolean) => { + e.preventDefault(); + const data = JSON.parse(e.dataTransfer.getData('text/plain')) as DragData; + const { action: draggedAction, isActive: isDragActive } = data; + + if (isDragOverRemove && isDragActive) { + handleRemoveAction(draggedAction); + handleDragEnd(); + return; + } + + if (typeof dropIndex === 'undefined' || typeof isDropActive === 'undefined') { + handleDragEnd(); + return; + } + + if (isDragActive === isDropActive) { + handleReorderWithinList(draggedAction, dropIndex, isDragActive); + } else if (isDragActive) { + handleMoveToInactive(draggedAction, dropIndex); + } else { + handleMoveToActive(draggedAction, dropIndex); + } + + handleDragEnd(); + }; + + const handleSave = async () => { + setIsSaving(true); + try { + await apiFetch({ + path: '/wp/v2/newspack/quick-actions', + method: 'POST', + data: activeActions.map(action => action.id), + }); + setIsEditing(false); + (window as any).newspackDashboard = { + ...(window as any).newspackDashboard, + quickActions: activeActions, + }; + } finally { + setIsSaving(false); + } + }; + + const handleCancel = () => { + const { quickActions = [], availableQuickActions = [] } = (window as any).newspackDashboard || {}; + if (Array.isArray(quickActions) && Array.isArray(availableQuickActions)) { + setActiveActions(quickActions); + setInactiveActions( + availableQuickActions.filter((action: QuickAction) => !quickActions.find((qa: QuickAction) => qa.id === action.id)) + ); + } + setIsEditing(false); + }; + + const handleReset = () => { + const { availableQuickActions = [] } = (window as any).newspackDashboard || {}; + if (Array.isArray(availableQuickActions)) { + setActiveActions(availableQuickActions.slice(0, MAX_ACTIVE_ACTIONS)); + setInactiveActions(availableQuickActions.slice(MAX_ACTIVE_ACTIONS)); + } + }; + + if (!activeActions.length && !inactiveActions.length) { + return null; + } + + const renderIcon = (iconName: keyof typeof icons) => { + const iconSvg = icons[iconName]; + return iconSvg ? <Icon icon={iconSvg} /> : null; + }; + + const getRandomDelay = () => ({ '--wobble-delay': `${Math.random() * -1.5}s` }); + + const ActionCard = ({ action }: { action: QuickAction }) => ( + <Card className="newspack-dashboard__card"> + <div className="newspack-dashboard__card-icon"> + {renderIcon(action.icon)} + </div> + <h4>{action.title}</h4> + </Card> + ); + + const PlaceholderCard = () => ( + <Card className="newspack-dashboard__card"> + <div className="newspack-dashboard__card-placeholder"> + {renderIcon('plus')} + </div> + </Card> + ); + + const RemoveZoneCard = () => ( + <Card className="newspack-dashboard__card"> + <div className="newspack-dashboard__card-placeholder"> + {renderIcon('trash')} + </div> + </Card> + ); + + return ( + <div + className={`newspack-dashboard__section ${isSaving ? 'is-saving' : ''}`} + onDragOver={handleDragOver} + > + {isSaving && ( + <div className="newspack-dashboard__section-overlay"> + <Spinner /> + </div> + )} + <div className="newspack-dashboard__section-header"> + <h3>{ __( 'Quick actions', 'newspack-plugin' ) }</h3> + {!isEditing ? ( + <Button isSecondary isSmall onClick={() => setIsEditing(true)}> + {__('Edit', 'newspack-plugin')} + </Button> + ) : ( + <div className="newspack-dashboard__section-header-actions"> + <Button isTertiary isSmall onClick={handleReset}> + {__('Reset to default', 'newspack-plugin')} + </Button> + <Button isSecondary isSmall onClick={handleCancel}> + {__('Cancel', 'newspack-plugin')} + </Button> + <Button isPrimary isSmall onClick={handleSave}> + {__('Save', 'newspack-plugin')} + </Button> + </div> + )} + </div> + <Grid style={ { '--np-dash-card-icon-size': '48px' } } columns={ 3 } gutter={ 24 }> + {activeActions.map((action, i) => + isEditing ? ( + <div + key={action.id} + draggable + onDragStart={(e) => handleDragStart(e, action, true)} + onDragEnd={handleDragEnd} + onDrop={(e) => handleDrop(e, i, true)} + className={`newspack-dashboard__card-wrapper ${isDragging ? 'is-dragging' : ''} is-editing ${isDraggingInactive ? 'is-drop-target' : ''}`} + style={getRandomDelay()} + > + <ActionCard action={action} /> + </div> + ) : ( + <a + key={action.id} + href={action.href} + className="newspack-dashboard__card-wrapper" + > + <ActionCard action={action} /> + </a> + ) + )} + {isEditing && activeActions.length < MAX_ACTIVE_ACTIONS && + Array.from({ length: MAX_ACTIVE_ACTIONS - activeActions.length }).map((_, i) => ( + <div + key={`placeholder-${i}`} + onDrop={(e) => handleDrop(e, activeActions.length + i, true)} + onDragOver={(e) => { + e.preventDefault(); + if (isDraggingInactive) { + e.currentTarget.classList.add('is-drop-target'); + } + }} + onDragLeave={(e) => e.currentTarget.classList.remove('is-drop-target')} + className="newspack-dashboard__card-wrapper is-placeholder" + style={getRandomDelay()} + > + <PlaceholderCard /> + </div> + )) + } + </Grid> + {isEditing && (inactiveActions.length > 0 || isDraggingActive) && ( + <> + <h4 className="newspack-dashboard__inactive-heading"> + {__('Available actions', 'newspack-plugin')} + </h4> + <Grid style={ { '--np-dash-card-icon-size': '48px' } } columns={ 3 } gutter={ 24 }> + {inactiveActions.map((action, i) => ( + <div + key={action.id} + draggable + onDragStart={(e) => handleDragStart(e, action, false)} + onDragEnd={handleDragEnd} + onDrop={(e) => handleDrop(e, i, false)} + className={`newspack-dashboard__card-wrapper is-inactive ${isDragging ? 'is-dragging' : ''}`} + style={getRandomDelay()} + > + <ActionCard action={action} /> + </div> + ))} + {isDraggingActive && ( + <div + className={`newspack-dashboard__remove-zone ${isDragOverRemove ? 'is-drag-over' : ''}`} + onDrop={handleDrop} + onDragOver={(e) => { + e.preventDefault(); + setIsDragOverRemove(true); + }} + onDragLeave={() => setIsDragOverRemove(false)} + onDragEnd={() => setIsDragOverRemove(false)} + > + <RemoveZoneCard /> + </div> + )} + </Grid> + </> + )} + </div> + ); +}; + +export default QuickActions; diff --git a/src/wizards/newspack/components/site-statuses/index.scss b/src/wizards/newspack/components/site-statuses/index.scss new file mode 100644 index 0000000000..2e9d55d09e --- /dev/null +++ b/src/wizards/newspack/components/site-statuses/index.scss @@ -0,0 +1,119 @@ +.newspack-site-status { + --np-sa-bg-color: #eee; + --np-sa-txt-color: #666; + --np-sa-space: 16px; + + animation: none; + background-color: var(--np-sa-bg-color); + border-radius: 26px !important; + font-weight: 600; + overflow: hidden; + padding: var(--np-sa-space) 24px; + position: relative; + line-height: 16px; + text-overflow: ellipsis; + transition: background-color 0.4s; + white-space: nowrap; + + // Dot + &::before { + content: ""; + background-color: var(--np-sa-txt-color); + border-radius: 4px; + display: inline-block; + height: 8px; + margin-right: 8px; + width: 8px; + transition: background-color 0.4s; + } + + // Status text i.e. Connect/Disconnected + span { + color: var(--np-sa-txt-color); + } + + &:has(span.hidden):hover { + span { + display: none; + + &.hidden { + display: contents; + } + } + } + + // When loading + &.newspack-site-status__pending { + overflow: hidden; + + &::after { + animation: gradient-left-to-right 2s infinite; + background-image: linear-gradient(90deg, rgba(#fff, 0) 0, rgba(#fff, 0.2) 20%, rgba(#fff, 0.6) 60%, rgba(#fff, 0)); + content: ""; + inset: 0; + position: absolute; + transform: translateX(-100%); + } + } + + &.newspack-site-status__success { + --np-sa-bg-color: #e6f2e8; + --np-sa-txt-color: #007017; + + background-color: var(--np-sa-bg-color); + + span { + color: #007017; + } + + &::before { + background-color: var(--np-sa-txt-color); + } + } + + // Errors + &[class*="newspack-site-status__error"] { + --np-sa-bg-color: #f7ebec; + --np-sa-txt-color: #b32d2e; + + background-color: var(--np-sa-bg-color); + + span { + color: #b32d2e; + } + + &::before { + background-color: var(--np-sa-txt-color); + } + } + + // Dependency Error + &.newspack-site-status__error-dependencies { + --np-sa-bg-color: #f5f1e1; + --np-sa-txt-color: #755100; + + background-color: var(--np-sa-bg-color); + border: 0; + cursor: pointer; + text-align: left; + + span { + color: var(--np-sa-txt-color); + } + + &::before { + background-color: var(--np-sa-txt-color); + } + } +} + +a.newspack-site-status { + color: inherit; + text-decoration: none; + + &:hover, + &:active, + &:focus { + color: inherit; + } +} diff --git a/src/wizards/newspack/components/site-statuses/index.tsx b/src/wizards/newspack/components/site-statuses/index.tsx new file mode 100644 index 0000000000..e182764e98 --- /dev/null +++ b/src/wizards/newspack/components/site-statuses/index.tsx @@ -0,0 +1,57 @@ +/** + * Newspack - Dashboard, Site Actions + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import SiteStatus from './site-status'; +import { Grid } from '../../../../components/src'; +import './index.scss'; + +const { + newspackDashboard: { siteStatuses }, +} = window; + +const actions: Statuses = { + readerActivation: { + ...siteStatuses.readerActivation, + then( { config } ) { + return Boolean( config?.enabled ); + }, + }, + googleAnalytics: { + ...siteStatuses.googleAnalytics, + then( { propertyID = '' } ) { + return propertyID !== ''; + }, + }, + googleAdManager: { + ...siteStatuses.googleAdManager, + then( { services: { google_ad_manager } } ) { + return ( + google_ad_manager.available && google_ad_manager.enabled === '1' + ); + }, + }, +} as const; + +const SiteStatuses = () => { + return ( + <div className="newspack-dashboard__section"> + <h3>{ __( 'Site status', 'newspack-plugin' ) }</h3> + <Grid columns={ 3 } gutter={ 24 }> + { Object.keys( actions ).map( id => { + return <SiteStatus key={ id } { ...actions[ id ] } />; + } ) } + </Grid> + </div> + ); +}; + +export default SiteStatuses; diff --git a/src/wizards/newspack/components/site-statuses/site-status-modal.tsx b/src/wizards/newspack/components/site-statuses/site-status-modal.tsx new file mode 100644 index 0000000000..cd637ee62a --- /dev/null +++ b/src/wizards/newspack/components/site-statuses/site-status-modal.tsx @@ -0,0 +1,40 @@ +/** + * Newspack - Dashboard, Site Action Modal + * + * Modal component for installing necessary dependencies + */ + +/** + * Dependencies + */ +// WordPress +import { __ } from '@wordpress/i18n'; +import { PluginInstaller, Modal } from '../../../../components/src'; + +const SiteActionModal = ( { onRequestClose, plugins, onSuccess }: SiteActionModal ) => { + return ( + <Modal + title={ __( 'Add missing dependencies', 'newspack-plugin' ) } + onRequestClose={ () => onRequestClose( false ) } + > + <PluginInstaller + plugins={ plugins } + canUninstall + onStatus={ ( { + complete, + pluginInfo, + }: { + complete: boolean; + pluginInfo: Record< string, any >; + } ) => { + if ( complete ) { + onSuccess( pluginInfo ); + onRequestClose( false ); + } + } } + /> + </Modal> + ); +}; + +export default SiteActionModal; diff --git a/src/wizards/newspack/components/site-statuses/site-status.tsx b/src/wizards/newspack/components/site-statuses/site-status.tsx new file mode 100644 index 0000000000..98d47795da --- /dev/null +++ b/src/wizards/newspack/components/site-statuses/site-status.tsx @@ -0,0 +1,207 @@ +/** + * Newspack - Dashboard, Site Status + */ + +/** + * Dependencies + */ +// WordPress +import { __, _n, sprintf } from '@wordpress/i18n'; +import apiFetch from '@wordpress/api-fetch'; +import { useState, useEffect } from '@wordpress/element'; +import { Tooltip } from '@wordpress/components'; +// Internal +import SiteActionModal from './site-status-modal'; + +const defaultStatuses = { + idle: undefined, + success: __( 'Connected', 'newspack-plugin' ), + pending: __( 'Fetching…', 'newspack-plugin' ), + 'pending-install': __( 'Installing…', 'newspack-plugin' ), + // Error types + error: __( 'Disconnected', 'newspack-plugin' ), + 'error-dependencies': undefined, + 'error-preflight': undefined, + 'error-request': undefined, +}; + +const SiteStatus = ( { + label = '', + isPreflightValid = true, + dependencies: dependenciesProp, + statuses, + endpoint, + configLink, + then, +}: Status ) => { + const parsedStatusLabels: Record< StatusLabels, string > = { + ...defaultStatuses, + ...statuses, + }; + + const [ requestCode, setRequestCode ] = useState( 200 ); + + const [ requestStatus, setRequestStatus ] = + useState< StatusLabels >( 'idle' ); + const [ failedDependencies, setFailedDependencies ] = useState< string[] >( + [] + ); + const [ isModalVisible, setIsModalVisible ] = useState( false ); + + const dependencies = structuredClone< Dependencies | undefined >( + dependenciesProp + ); + + useEffect( () => { + makeRequest(); + }, [] ); + + function makeRequest( pluginInfo = {} ) { + // When/if a dependency is activated update reference. + if ( dependencies && Object.keys( pluginInfo ).length > 0 ) { + for ( const [ pluginName ] of Object.entries( pluginInfo ) ) { + dependencies[ pluginName ].isActive = true; + } + } + return new Promise< void | boolean >( resolve => { + // Dependency check + if ( dependencies && Object.keys( dependencies ).length > 0 ) { + const failedDeps: string[] = []; + for ( const [ + dependencyName, + dependencyInfo, + ] of Object.entries( dependencies ) ) { + // Don't process active + if ( dependencyInfo.isActive ) { + continue; + } + failedDeps.push( dependencyName ); + } + setFailedDependencies( failedDeps ); + if ( failedDeps.length > 0 ) { + setRequestStatus( 'error-dependencies' ); + resolve( false ); + return; + } + } + // Preflight check + if ( ! isPreflightValid ) { + setRequestStatus( 'error-preflight' ); + resolve( false ); + return; + } + // Pending API request + setRequestStatus( 'pending' ); + apiFetch( { + path: endpoint, + parse: false, + } ) + .then( async res => { + const response = res as Response; + setRequestCode( response.status ); + const data = await response.json(); + const apiRequest = then( data ); + setRequestStatus( apiRequest ? 'success' : 'error' ); + resolve( apiRequest ); + } ) + .catch( err => { + const status = err?.status ?? 500; + setRequestStatus( + status > 399 ? 'error-request' : 'error' + ); + setRequestCode( status ); + resolve(); + } ); + } ); + } + + const classes = `newspack-site-status newspack-site-status__${ requestStatus }`; + + return ( + <> + { isModalVisible && ( + <SiteActionModal + plugins={ failedDependencies } + onSuccess={ makeRequest } + onRequestClose={ setIsModalVisible } + /> + ) } + { /* Error UI, link user to config */ } + { requestStatus === 'error' && ( + <Tooltip + text={ __( + 'Click to navigate to configuration', + 'newspack-plugin' + ) } + > + <a href={ configLink } className={ classes }> + { label }:{ ' ' } + <span>{ parsedStatusLabels[ requestStatus ] }</span> + <span className="hidden">{ __( 'Configure?' ) }</span> + </a> + </Tooltip> + ) } + { /* Error Dependencies, dependencies install modal */ } + { requestStatus === 'error-dependencies' && dependencies && ( + <Tooltip + text={ sprintf( + // translators: %s is a comma separated list of needed dependencies. + __( '%s must be installed & activated!' ), + failedDependencies + .map( dep => dependencies[ dep ].label ) + .join( ', ' ) + ) } + > + <button + onClick={ () => setIsModalVisible( true ) } + className={ classes } + > + { label }:{ ' ' } + <span> + { _n( + 'Missing dependency', + 'Missing dependencies', + failedDependencies.length, + 'newspack-plugin' + ) } + </span> + <span className="hidden"> + { _n( + 'Install dependency', + 'Install dependencies', + failedDependencies.length, + 'newspack-plugin' + ) } + </span> + </button> + </Tooltip> + ) } + { /* Display standard UI for the rest */ } + { [ + 'error-preflight', + 'success', + 'idle', + 'pending', + 'error-request', + ].includes( requestStatus ) && ( + <div className={ classes }> + { label }:{ ' ' } + <span> + { requestStatus === 'error-request' + ? sprintf( + /* translators: %d is the HTTP status code */ + __( + 'Request failed - %d', + 'newspack-plugin' + ), + requestCode + ) + : parsedStatusLabels[ requestStatus ] } + </span> + </div> + ) } + </> + ); +}; + +export default SiteStatus; diff --git a/src/wizards/newspack/types/index.d.ts b/src/wizards/newspack/types/index.d.ts new file mode 100644 index 0000000000..987810508a --- /dev/null +++ b/src/wizards/newspack/types/index.d.ts @@ -0,0 +1,123 @@ +import 'react'; +import icons from '../components/icons'; + +declare module 'react' { + interface CSSProperties { + [ key: `--${ string }` ]: string | number; + } +} + +type WizardTab = { + label: string; + path?: string; + activeTabPaths?: string[]; + sections: { + [ k: string ]: { + editLink?: string; + dependencies?: Record< string, string >; + enabled?: Record< string, boolean >; + } & Record< string, any >; + }; +}; + +declare global { + interface Window { + newspackDashboard: { + siteStatuses: { + readerActivation: Status; + googleAnalytics: Status; + googleAdManager: Status & { + isAvailable: boolean; + }; + }; + quickActions: { + href: string; + title: string; + icon: keyof typeof icons; + id: string; + }[]; + availableQuickActions: { + href: string; + title: string; + icon: keyof typeof icons; + id: string; + }[]; + sections: { + [ k: string ]: { + title: string; + desc: string; + cards: { + href: string; + title: string; + desc: string; + icon: keyof typeof icons; + }[]; + }; + }; + settings: { + siteName: string; + headerBgColor: string; + }; + }; + newspackSettings: { + social: WizardTab; + connections: WizardTab; + syndication: WizardTab; + 'theme-and-brand': WizardTab; + seo: WizardTab; + emails: WizardTab & { + sections: { + emails: { + all: { + [ str: string ]: { + label: string; + description: string; + post_id: number; + edit_link: string; + subject: string; + from_name: string; + from_email: string; + reply_to_email: string; + status: string; + type: string; + }; + }; + dependencies: Record< string, boolean >; + postType: string; + }; + }; + }; + 'additional-brands': WizardTab & { + sections: { + additionalBrands: { + themeColors: { + color: string; + label: string; + theme_mod_name?: string; + default?: string; + }[]; + menuLocations: Record< string, string >; + menus: { label: string; value: number }[]; + }; + }; + }; + 'display-settings': WizardTab; + }; + newspack_aux_data: { + is_debug_mode: boolean; + }; + newspack_urls: { + site: string; + }; + } +} + +interface Status { + label: string; + statuses: Record< string, string >; + endpoint: string; + configLink: string; + dependencies: Record< string, { label: string; isActive: boolean } >; +} + +export {}; diff --git a/src/wizards/newspack/types/site-statuses.d.ts b/src/wizards/newspack/types/site-statuses.d.ts new file mode 100644 index 0000000000..430d57dee6 --- /dev/null +++ b/src/wizards/newspack/types/site-statuses.d.ts @@ -0,0 +1,31 @@ +type Dependencies = Record< string, { isActive: boolean; label: string } >; + +type StatusLabels = + | 'success' + | 'error' + | 'error-dependencies' + | 'error-preflight' + | 'error-request' // 404, 500 + | 'pending' + | 'pending-install' + | 'idle'; + +type Status = { + label: string; + statuses?: Partial<StatusLabels, string>; + isPreflightValid?: boolean; + configLink: string; + endpoint: string; + dependencies?: Dependencies; + then: ( args: any ) => boolean; +}; + +type Statuses = { + [ k: string ]: Status; +}; + +type SiteActionModal = { + onRequestClose: ( a: boolean ) => void; + onSuccess: ( a: Record< string, any > ) => void; + plugins: string[]; +}; diff --git a/src/wizards/newspack/views/dashboard/index.tsx b/src/wizards/newspack/views/dashboard/index.tsx new file mode 100644 index 0000000000..86a0af8a23 --- /dev/null +++ b/src/wizards/newspack/views/dashboard/index.tsx @@ -0,0 +1,48 @@ +/** + * Newspack - Dashboard + * + * WP Admin Newspack Dashboard page. + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Fragment } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import './style.scss'; +import sections from './sections'; +import BrandHeader from '../../components/brand-header'; +import QuickActions from '../../components/quick-actions'; +import SiteStatuses from '../../components/site-statuses'; +import { GlobalNotices, Notice, Wizard } from '../../../../components/src'; + +const { + newspack_aux_data: { is_debug_mode: isDebugMode = false }, +} = window; + +function Dashboard() { + return ( + <Fragment> + <GlobalNotices /> + { isDebugMode && <Notice debugMode /> } + <Wizard + headerText={ __( 'Newspack / Dashboard', 'newspack' ) } + sections={ sections } + renderAboveSections={ () => ( + <> + <BrandHeader /> + <SiteStatuses /> + <hr /> + <QuickActions /> + </> + ) } + /> + </Fragment> + ); +} + +export default Dashboard; diff --git a/src/wizards/newspack/views/dashboard/sections.tsx b/src/wizards/newspack/views/dashboard/sections.tsx new file mode 100644 index 0000000000..b6321f1d44 --- /dev/null +++ b/src/wizards/newspack/views/dashboard/sections.tsx @@ -0,0 +1,87 @@ +/** + * Newspack - Dashboard, Sections + * + * Component for outputting sections with grid and cards + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Fragment } from '@wordpress/element'; + +/** + * Internal dependencies + */ +/* eslint import/namespace: ['error', { allowComputed: true }] */ +import { Icon, icons } from '../../components/icons'; +import { Grid, Card } from '../../../../components/src'; + +const { + newspackDashboard: { sections: dashSections }, +} = window; + +function getIcon( iconName: keyof typeof icons ) { + if ( iconName in icons ) { + return icons[ iconName ]; + } + return icons.help; +} + +export default [ + { + label: __( 'Dashboard', 'newspack' ), + path: '/', + render: () => { + const dashSectionsKeys = Object.keys( dashSections ); + return dashSectionsKeys.map( sectionKey => { + return ( + <Fragment key={ sectionKey }> + <hr /> + <div className="newspack-dashboard__section"> + <h3>{ dashSections[ sectionKey ].title }</h3> + <p>{ dashSections[ sectionKey ].desc }</p> + <Grid + columns={ 3 } + gutter={ 24 } + key={ `${ sectionKey }-grid` } + > + { dashSections[ sectionKey ].cards.map( + ( sectionCard, i ) => { + return ( + <a + href={ sectionCard.href } + key={ `${ sectionKey }-card-${ i }` } + > + <Card className="newspack-dashboard__card"> + <div className="newspack-dashboard__card-icon"> + <Icon + size={ 32 } + icon={ getIcon( + sectionCard.icon + ) } + /> + </div> + <div className="newspack-dashboard__card-text"> + <h4> + { + sectionCard.title + } + </h4> + <p> + { sectionCard.desc } + </p> + </div> + </Card> + </a> + ); + } + ) } + </Grid> + </div> + </Fragment> + ); + } ); + }, + }, +]; diff --git a/src/wizards/newspack/views/dashboard/style.scss b/src/wizards/newspack/views/dashboard/style.scss new file mode 100644 index 0000000000..d0cefc1af1 --- /dev/null +++ b/src/wizards/newspack/views/dashboard/style.scss @@ -0,0 +1,286 @@ +/** + * Dashboard + */ + +@use "~@wordpress/base-styles/colors" as wp-colors; +@use "../../../../shared/scss/colors"; + +.newspack-wizard__content { + box-sizing: border-box; + margin: 0; + max-width: 100%; + padding: 0 0 32px; + + * { + box-sizing: border-box; + } +} + +.newspack-dashboard__brand-header { + padding: 40px 0; + + .brand-header__inner { + margin: 0 auto; + max-width: calc(calc(var(--newspack-wizard-section-space) * 2) + var(--newspack-wizard-section-width)); + padding: 0 var(--newspack-wizard-section-space); + } + + h1 { + font-size: 32px; + font-style: normal; + font-weight: 400; + line-height: 40px; + margin: 0; + } +} + +.newspack-dashboard__section { + position: relative; + + &.is-saving { + .newspack-dashboard__section-header, + .newspack-grid, + .newspack-dashboard__inactive-heading { + opacity: 0.1; + pointer-events: none; + transition: opacity 0.15s ease-in-out; + } + } + + &-overlay { + align-items: center; + bottom: 0; + display: flex; + justify-content: center; + left: 0; + position: absolute; + right: 0; + top: 0; + z-index: 1; + + .components-spinner { + margin: 0; + } + } + + &-header { + align-items: center; + display: flex; + justify-content: space-between; + + &-actions { + display: flex; + gap: 8px; + } + } + + a { + border-radius: 2px; + + &:hover, + &:focus { + .newspack-dashboard__card { + border-color: var(--wp-admin-theme-color); + + .newspack-dashboard__card-icon { + svg { + transform: scale(1.25); + } + } + } + } + } +} + +.newspack-dashboard__remove-zone { + grid-column: 1 / -1; + margin-top: 24px; + + .newspack-dashboard__card { + align-items: center; + border-color: currentcolor; + border-style: dashed; + color: var(--np-sa-txt-color, #{wp-colors.$alert-red}); + display: flex; + justify-content: center; + min-height: 80px; + padding: 0; + + .newspack-dashboard__card-placeholder { + align-items: center; + display: flex; + height: 100%; + justify-content: center; + width: 100%; + + svg { + fill: currentcolor; + height: 24px; + opacity: 1; + transform: scale(1); + transition: all 125ms ease-in-out; + width: 24px; + } + } + } + + &.is-drag-over { + .newspack-dashboard__card { + background-color: var(--np-sa-bg-color, rgba(wp-colors.$alert-red, 0.16)); + + .newspack-dashboard__card-placeholder svg { + transform: scale(1.25); + } + } + } +} + +.newspack-dashboard__card { + align-items: center; + background-color: transparent; + border-color: wp-colors.$gray-300; + border-radius: 2px; + display: grid; + grid-gap: var(--newspack-wizard-section-child-space); + grid-template: "icon text" auto / var(--np-dash-card-icon-size) auto; + margin: 0; + overflow: hidden; + padding: var(--newspack-wizard-section-child-space); + transition: border-color 125ms, background-color 125ms; + + h4 { + font-size: 16px; + font-weight: 600; + grid-area: text; + line-height: 24px; + margin: 0 0 calc(var(--newspack-wizard-section-child-space) / 2); + } + + p { + grid-area: text; + line-height: 16px; + margin: 0; + opacity: 1; + transition: transform 125ms ease-out, opacity 125ms; + } + + &:not(:has(p)) h4 { + margin: 0; + } + + &-wrapper { + cursor: pointer; + position: relative; + + &.is-editing, + &.is-inactive { + animation: wobble 1.5s cubic-bezier(0.445, 0.05, 0.55, 0.95) infinite; + animation-delay: var(--wobble-delay, 0s); + background: white; + cursor: grab; + + &:active, + &.is-dragging { + animation: none; + cursor: grabbing; + } + + .newspack-card { + border-color: wp-colors.$gray-100; + border-style: dashed; + transition: border-color 125ms ease-in-out; + } + + &:hover .newspack-card { + border-color: wp-colors.$gray-300; + } + } + + &.is-placeholder { + animation: wobble 1.5s cubic-bezier(0.445, 0.05, 0.55, 0.95) infinite; + cursor: default; + + .newspack-dashboard__card { + align-items: center; + border-color: wp-colors.$gray-100; + border-style: dashed; + display: flex; + justify-content: center; + min-height: 80px; + padding: 0; + + .newspack-dashboard__card-placeholder { + align-items: center; + display: flex; + justify-content: center; + + svg { + fill: var(--wp-admin-theme-color); + height: 24px; + transform: scale(1); + transition: transform 125ms ease-in-out; + width: 24px; + } + } + } + + &.is-drop-target { + .newspack-dashboard__card { + background: wp-colors.$gray-100; + border-color: wp-colors.$gray-300; + + svg { + transform: scale(1.25); + } + } + } + } + } +} + +// Icon styles +.newspack-dashboard__card-icon { + background-color: #{colors.$primary-000}; + border-radius: 4px; + display: grid; + fill: var(--wp-admin-theme-color); + grid-area: icon; + height: var(--np-dash-card-icon-size); + place-items: center; + transform: translateZ(0); + width: var(--np-dash-card-icon-size); + + svg { + transition: transform 125ms ease-in-out; + } +} + +// Edit mode styles +.newspack-dashboard__card-wrapper.is-editing { + .newspack-dashboard__card-icon svg { + transform: scale(1); + } + + a:hover, + a:focus { + .newspack-dashboard__card { + border-color: wp-colors.$gray-300; + + .newspack-dashboard__card-icon svg { + transform: scale(1); + } + } + } +} + +@keyframes wobble { + 0% { + transform: translate(-1px) rotate(-0.2deg); + } + 50% { + transform: translate(1px) rotate(0.2deg); + } + 100% { + transform: translate(-1px) rotate(-0.2deg); + } +} diff --git a/src/wizards/newspack/views/settings/additional-brands/brand-upsert.tsx b/src/wizards/newspack/views/settings/additional-brands/brand-upsert.tsx new file mode 100644 index 0000000000..fcaa0f5861 --- /dev/null +++ b/src/wizards/newspack/views/settings/additional-brands/brand-upsert.tsx @@ -0,0 +1,415 @@ +/** + * Additional Brands Brand page. Used to edit and create. + */ + +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; +import { addQueryArgs, cleanForSlug } from '@wordpress/url'; +import { Fragment, useState, useEffect } from '@wordpress/element'; + +/** + * Internal dependencies. + */ +import { + Router, + Card, + Grid, + Button, + SectionHeader, + TextControl, + ImageUpload, + ColorPicker, + SelectControl, + RadioControl, + hooks, + Notice, +} from '../../../../../components/src'; + +import './style.scss'; +import { TAB_PATH } from './constants'; + +const { useParams } = Router; + +const { + themeColors: registeredThemeColors, + menuLocations, + menus: availableMenus, +} = window.newspackSettings[ 'additional-brands' ].sections.additionalBrands; + +export default function Brand( { + brands = [], + upsertBrand, + wizardApiFetch, + fetchLogoAttachment, + errorMessage, +}: { + brands: Brand[]; + editBrand?: number; + upsertBrand: ( brandId: number, brand: Brand ) => void; + wizardApiFetch: WizardApiFetch; + fetchLogoAttachment: ( brandId: number, logoId: number ) => void; + errorMessage?: string | null; +} ) { + const { brandId = '0' } = useParams(); + const selectedBrand = brands.find( ( { id } ) => id === Number( brandId ) ); + + const [ brand, updateBrand ] = hooks.useObjectState< Brand >( { + id: 0, + name: '', + slug: '', + meta: { + _show_page_on_front: 0, + _custom_url: 'yes', + _logo: 0, + _theme_colors: [], + _menus: [], + }, + count: 0, + description: '', + link: '', + taxonomy: '', + parent: 0, + } ); + const [ publicPages, setPublicPages ] = useState< PublicPage[] >( [] ); + const [ showOnFrontSelect, setShowOnFrontSelect ] = + useState< string >( 'no' ); + + useEffect( () => { + if ( selectedBrand && typeof selectedBrand.meta._logo === 'number' ) { + // Only fetch the logo if _logo is a number (ID) and not an `Attachment` object. + fetchLogoAttachment( Number( brandId ), selectedBrand.meta._logo ); + } + }, [ selectedBrand?.meta._logo ] ); + + useEffect( () => { + if ( selectedBrand ) { + updateBrand( selectedBrand ); + setShowOnFrontSelect( + selectedBrand.meta._show_page_on_front ? 'yes' : 'no' + ); + } + }, [ selectedBrand ] ); + + useEffect( () => { + wizardApiFetch( + { + path: addQueryArgs( '/wp/v2/pages', { + per_page: 100, + orderby: 'title', + order: 'asc', + } ), + }, + { + onSuccess: setPublicPages, + } + ); + }, [] ); + + const brandThemeColors = brand.meta._theme_colors; + + const isBrandValid = + brand.name?.length > 0 && + ( showOnFrontSelect === 'no' || + ( showOnFrontSelect === 'yes' && + brand.meta._show_page_on_front > 0 ) ); + + // Utility functions for brand updates + function updateThemeColor( + name: string | undefined, + color: string | undefined + ) { + if ( ! name ) { + return; + } + + const existingColor = brandThemeColors.find( c => c.name === name ); + let updatedThemeColors = [ ...( brandThemeColors || [] ) ]; + + if ( color ) { + if ( existingColor ) { + // Update existing color + updatedThemeColors = updatedThemeColors.map( _color => + _color.name === name ? { ..._color, color } : _color + ); + } else { + // Add new color + updatedThemeColors.push( { name, color } ); + } + } else { + // Reset to default + updatedThemeColors = updatedThemeColors.filter( + _color => _color.name !== name + ); + } + + updateBrand( { + meta: { ...brand.meta, _theme_colors: updatedThemeColors }, + } ); + } + + function updateSlugFromName( e: React.ChangeEvent< HTMLInputElement > ) { + if ( ! brand.slug ) { + updateBrand( { slug: cleanForSlug( e.target.value ) } ); + } + } + + function updateShowOnFront( value: string ) { + setShowOnFrontSelect( value ); + updateBrand( { + meta: { + ...brand.meta, + _show_page_on_front: value === 'yes' ? 1 : 0, + }, + } ); + } + + function updateMenus( location: string, menu: number ) { + const updatedMenus = + brand.meta._menus.map( _menu => + _menu.location === location ? { ..._menu, menu } : _menu + ) || []; + + updateBrand( { + meta: { + ...brand.meta, + _menus: [ ...updatedMenus, { location, menu } ], + }, + } ); + } + + const baseUrl = `${ window.newspack_urls.site }/${ + brand.meta._custom_url === 'no' ? 'brand/' : '' + }`; + + function findSelectedMenu( location: string ) { + return ( + brand.meta._menus.find( menu => menu.location === location ) + ?.menu || 0 + ); + } + + function isFetchingLogo() { + return typeof brand.meta._logo === 'number' && brand.meta._logo > 0; + } + + return ( + <Fragment> + <SectionHeader + title={ __( 'Brand', 'newspack-plugin' ) } + description={ __( + 'Set your brand identity', + 'newspack-plugin' + ) } + /> + <Grid gutter={ 32 }> + <Grid columns={ 1 } gutter={ 16 }> + <TextControl + label={ __( 'Name', 'newspack-plugin' ) } + value={ brand.name || '' } + onChange={ updateBrand( 'name' ) } + onBlur={ updateSlugFromName } + placeholder={ __( 'Brand Name', 'newspack-plugin' ) } + /> + </Grid> + <Grid columns={ 1 } gutter={ 16 }> + <ImageUpload + className="newspack-brand__header__logo" + buttonLabel={ + isFetchingLogo() + ? __( 'Fetching logo…', 'newspack-plugin' ) + : undefined + } + label={ __( 'Logo', 'newspack-plugin' ) } + image={ + isFetchingLogo() ? undefined : brand.meta._logo + } + onChange={ ( logoId: number ) => + updateBrand( { + meta: { ...brand.meta, _logo: logoId }, + } ) + } + /> + </Grid> + </Grid> + + { /* Theme Colors Section */ } + { registeredThemeColors && ( + <Fragment> + <SectionHeader + title={ __( 'Colors', 'newspack-plugin' ) } + description={ __( + 'These are the colors you can customize for this brand in the active theme', + 'newspack-plugin' + ) } + /> + { registeredThemeColors.map( color => ( + <Card noBorder key={ color.theme_mod_name }> + <ColorPicker + className="newspack-brand__theme-mod-color-picker" + label={ + <Fragment> + <span>{ color.label }</span> + { brandThemeColors.find( + c => c.name === color.theme_mod_name + )?.color && ( + <Button + variant="link" + onClick={ () => + updateThemeColor( + color.theme_mod_name, + '' + ) + } + > + { __( + 'Reset default color', + 'newspack-plugin' + ) } + </Button> + ) } + </Fragment> + } + color={ + brandThemeColors.find( + c => c.name === color.theme_mod_name + )?.color ?? color.default + } + onChange={ ( newColor: string ) => + updateThemeColor( + color.theme_mod_name, + newColor + ) + } + /> + </Card> + ) ) } + </Fragment> + ) } + + { /* URL Settings */ } + <SectionHeader title={ __( 'Settings', 'newspack-plugin' ) } /> + <Card noBorder> + <RadioControl + className="newspack-brand__base-url-radio-control" + label={ __( 'URL Base', 'newspack-plugin' ) } + selected={ brand.meta._custom_url || 'yes' } + options={ [ + { + label: __( 'Homepage', 'newspack-plugin' ), + value: 'yes', + }, + { + label: __( 'Default', 'newspack-plugin' ), + value: 'no', + }, + ] } + onChange={ ( _custom_url: string ) => + updateBrand( { meta: { ...brand.meta, _custom_url } } ) + } + /> + <div className="newspack-brand__base-url-component"> + <span>{ baseUrl }</span> + <TextControl + className="newspack-brand__base-url-component__text-control" + label={ __( 'Slug', 'newspack-plugin' ) } + hideLabelFromVision + withMargin={ false } + value={ brand.slug || '' } + onChange={ ( slug: string ) => updateBrand( { slug } ) } + placeholder="brand-slug" + /> + </div> + </Card> + + { /* Front Page Settings */ } + <Card noBorder> + <RadioControl + className="newspack-brand__base-url-radio-control" + label={ __( 'Show on Front', 'newspack-plugin' ) } + selected={ showOnFrontSelect } + options={ [ + { + label: __( 'Latest posts', 'newspack-plugin' ), + value: 'no', + }, + { + label: __( 'A page', 'newspack-plugin' ), + value: 'yes', + }, + ] } + onChange={ updateShowOnFront } + /> + { showOnFrontSelect === 'yes' && ( + <SelectControl + label={ __( 'Homepage URL', 'newspack-plugin' ) } + value={ brand.meta._show_page_on_front || 0 } + options={ [ + { + label: __( 'Select a Page', 'newspack-plugin' ), + value: 0, + disabled: true, + }, + ...publicPages.map( page => ( { + label: page.title.rendered, + value: Number( page.id ), + } ) ), + ] } + onChange={ ( _show_page_on_front: number ) => + updateBrand( { + meta: { + ...brand.meta, + _show_page_on_front: + Number( _show_page_on_front ), + }, + } ) + } + required + /> + ) } + </Card> + + { /* Menu Settings */ } + <SectionHeader + title={ __( 'Menus', 'newspack-plugin' ) } + description={ __( + 'Customize the menus for this brand', + 'newspack-plugin' + ) } + /> + { menuLocations && + Object.keys( menuLocations ).map( location => ( + <SelectControl + key={ location } + label={ menuLocations[ location ] } + value={ findSelectedMenu( location ) } + options={ [ + { + label: __( 'Same as site', 'newspack-plugin' ), + value: 0, + }, + ...availableMenus, + ] } + onChange={ ( menuId: number ) => + updateMenus( location, menuId ) + } + /> + ) ) } + { errorMessage && <Notice isError>{ errorMessage }</Notice> } + { /* Action Buttons */ } + <div className="newspack-buttons-card"> + <Button + disabled={ ! isBrandValid } + variant="primary" + onClick={ () => upsertBrand( Number( brandId ), brand ) } + > + { __( 'Save', 'newspack-plugin' ) } + </Button> + <Button variant="secondary" href={ `#${ TAB_PATH }` }> + { __( 'Cancel', 'newspack-plugin' ) } + </Button> + </div> + </Fragment> + ); +} diff --git a/src/wizards/newspack/views/settings/additional-brands/brand.tsx b/src/wizards/newspack/views/settings/additional-brands/brand.tsx new file mode 100644 index 0000000000..78ca0d886a --- /dev/null +++ b/src/wizards/newspack/views/settings/additional-brands/brand.tsx @@ -0,0 +1,84 @@ +/** + * Additional Brands Brand Card. + */ + +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; +import { ESCAPE } from '@wordpress/keycodes'; +import { useState } from '@wordpress/element'; +import { moreVertical } from '@wordpress/icons'; +import { MenuItem } from '@wordpress/components'; + +/** + * Internal dependencies. + */ +import WizardsActionCard from '../../../../wizards-action-card'; +import { Button, Popover, Router } from '../../../../../components/src'; +import { TAB_PATH } from './constants'; + +const { useHistory } = Router; + +export default function Brand( { + brand, + deleteBrand, +}: { + brand: Brand; + deleteBrand: ( brand: Brand ) => void; +} ) { + const [ popoverVisibility, setPopoverVisibility ] = useState( false ); + const onFocusOutside = () => setPopoverVisibility( false ); + const history = useHistory(); + + return ( + <WizardsActionCard + isSmall + title={ brand.name } + actionText={ + <> + <Button + onClick={ () => + setPopoverVisibility( ! popoverVisibility ) + } + label={ __( 'More options', 'newspack-plugin' ) } + icon={ moreVertical } + className={ popoverVisibility ? 'popover-active' : '' } + /> + { popoverVisibility && ( + <Popover + position="bottom left" + onKeyDown={ ( event: React.KeyboardEvent ) => + ESCAPE === event.keyCode && onFocusOutside + } + onFocusOutside={ onFocusOutside } + > + <MenuItem + onClick={ () => onFocusOutside() } + className="screen-reader-text" + > + { __( 'Close Popover', 'newspack-plugin' ) } + </MenuItem> + <MenuItem + onClick={ () => + history.push( + `${ TAB_PATH }/${ brand.id }` + ) + } + className="newspack-button" + > + { __( 'Edit', 'newspack-plugin' ) } + </MenuItem> + <MenuItem + onClick={ () => deleteBrand( brand ) } + className="newspack-button" + > + { __( 'Delete', 'newspack-plugin' ) } + </MenuItem> + </Popover> + ) } + </> + } + /> + ); +} diff --git a/src/wizards/newspack/views/settings/additional-brands/brands.tsx b/src/wizards/newspack/views/settings/additional-brands/brands.tsx new file mode 100644 index 0000000000..5aff29609a --- /dev/null +++ b/src/wizards/newspack/views/settings/additional-brands/brands.tsx @@ -0,0 +1,67 @@ +/** + * Additional Brands Brands page. + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Fragment } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import Brand from './brand'; +import { Card, Button, Router } from '../../../../../components/src'; +import { TAB_PATH } from './constants'; + +const { NavLink } = Router; + +export default function Brands( { + brands, + isFetching, + deleteBrand, +}: { + brands: Brand[]; + isFetching: boolean; + deleteBrand: ( brand: Brand ) => void; +} ) { + return ( + <Fragment> + <Card headerActions noBorder> + <h2> + { ! brands.length && ! isFetching + ? __( 'You have no saved brands.', 'newspack-plugin' ) + : __( 'Site brands', 'newspack-plugin' ) } + </h2> + <NavLink to={ `${ TAB_PATH }/new` }> + <Button variant="primary" disabled={ isFetching }> + { __( 'Add New Brand', 'newspack-plugin' ) } + </Button> + </NavLink> + </Card> + { brands.length ? ( + brands.map( brand => ( + <Brand + key={ brand.id } + brand={ brand } + deleteBrand={ deleteBrand } + /> + ) ) + ) : ( + <Fragment> + { isFetching ? ( + <p>{ __( 'Fetching brands…', 'newspack-plugin' ) }</p> + ) : ( + <p> + { __( + 'Create brands to enhance your readers experience.', + 'newspack-plugin' + ) } + </p> + ) } + </Fragment> + ) } + </Fragment> + ); +} diff --git a/src/wizards/newspack/views/settings/additional-brands/constants.ts b/src/wizards/newspack/views/settings/additional-brands/constants.ts new file mode 100644 index 0000000000..5c0b1371a0 --- /dev/null +++ b/src/wizards/newspack/views/settings/additional-brands/constants.ts @@ -0,0 +1,5 @@ +/** + * Additional brands tab constants. + */ + +export const TAB_PATH = '/additional-brands'; diff --git a/src/wizards/newspack/views/settings/additional-brands/index.tsx b/src/wizards/newspack/views/settings/additional-brands/index.tsx new file mode 100644 index 0000000000..b31cdf1b85 --- /dev/null +++ b/src/wizards/newspack/views/settings/additional-brands/index.tsx @@ -0,0 +1,239 @@ +/** + * Additional Brands page. + */ + +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; +import { addQueryArgs } from '@wordpress/url'; +import { useState, useEffect } from '@wordpress/element'; + +/** + * Internal dependencies. + */ +import Brands from './brands'; +import BrandUpsert from './brand-upsert'; +import WizardsTab from '../../../../wizards-tab'; +import WizardSection from '../../../../wizards-section'; +import { Router, utils } from '../../../../../components/src'; +import { useWizardApiFetch } from '../../../../hooks/use-wizard-api-fetch'; +import { TAB_PATH } from './constants'; + +const { Route, Switch, useHistory, useRouteMatch, useLocation } = Router; + +export default function AdditionalBrands() { + const { wizardApiFetch, isFetching, cache, errorMessage, resetError } = + useWizardApiFetch( 'newspack-settings/additional-brands' ); + + const brandsCache = cache( '/wp/v2/brand' ); + + const [ brands, setBrands ] = useState< Brand[] >( [] ); + const history = useHistory(); + const location = useLocation(); + const { path } = useRouteMatch(); + + useEffect( () => { + resetError(); + }, [ location.pathname ] ); + + /** + * Cache brands data. + */ + useEffect( () => { + brandsCache.set( brands ); + }, [ brands ] ); + + const wizardScreenProps = { + isFetching, + headerText: __( 'Brands', 'newspack-plugin' ), + subHeaderText: __( 'Configure brands settings', 'newspack-plugin' ), + }; + + /** + * Fetching brands data. + */ + const fetchBrands = () => { + wizardApiFetch< Brand[] >( + { + path: addQueryArgs( '/wp/v2/brand', { per_page: 100 } ), + }, + { + onSuccess( response ) { + setBrands( + response.map( ( brand: Brand ) => ( { + ...brand, + meta: { + ...brand.meta, + _theme_colors: brand.meta._theme_colors?.length + ? brand.meta._theme_colors + : [], + _menus: brand.meta._menus?.length + ? brand.meta._menus + : [], + }, + } ) ) + ); + }, + } + ); + }; + + const upsertBrand = ( brandId: number, brand: Brand ) => { + // BrandId is NaN when inserting new brand. + wizardApiFetch< Brand >( + { + path: brandId ? `/wp/v2/brand/${ brandId }` : '/wp/v2/brand', + method: 'POST', + data: { + ...brand, + meta: { + ...brand.meta, + ...( brand.meta._logo && { + _logo: + brand.meta._logo instanceof Object + ? brand.meta._logo.id + : brand.meta._logo, + } ), + }, + }, + }, + { + onSuccess( result ) { + setBrands( ( brandsList: Brand[] ) => { + // Is update + if ( 0 === brandId ) { + return [ result, ...brandsList ]; + } + return brandsList.map( b => + brandId === b.id ? result : b + ); + } ); + history.push( TAB_PATH ); + }, + } + ); + }; + + const deleteBrand = ( brand: Brand ) => { + if ( + utils.confirmAction( + __( + 'Are you sure you want to delete this brand?', + 'newspack-plugin' + ) + ) + ) { + wizardApiFetch< { deleted: boolean; previous: Brand } >( + { + path: addQueryArgs( `/wp/v2/brand/${ brand.id }`, { + force: true, + } ), + method: 'DELETE', + }, + { + onSuccess( result ) { + if ( result.deleted ) { + setBrands( oldBrands => + oldBrands.filter( + oldBrand => brand.id !== oldBrand.id + ) + ); + } + }, + } + ); + } + }; + + const fetchLogoAttachment = ( brandId: number, attachmentId: number ) => { + if ( ! attachmentId ) { + return; + } + wizardApiFetch( + { + path: `/wp/v2/media/${ attachmentId }`, + }, + { + onSuccess( attachment ) { + setBrands( brandsList => { + const brandIndex = brandsList.findIndex( + _brand => brandId === _brand.id + ); + return brandIndex > -1 + ? brandsList.map( _brand => + brandId === _brand.id + ? { + ..._brand, + meta: { + ..._brand.meta, + _logo: { + ...attachment, + url: attachment.source_url, + }, + }, + } + : _brand + ) + : brandsList; + } ); + }, + } + ); + }; + + useEffect( fetchBrands, [] ); + + return ( + <WizardsTab + isFetching={ isFetching } + title={ __( 'Additional Brands', 'newspack-plugin' ) } + > + <WizardSection> + <Switch> + <Route + exact + path={ path } + render={ () => ( + <Brands + { ...wizardScreenProps } + brands={ brands } + deleteBrand={ deleteBrand } + /> + ) } + /> + <Route + path={ `${ path }/new` } + render={ () => ( + <BrandUpsert + { ...wizardScreenProps } + brands={ brands } + upsertBrand={ upsertBrand } + fetchLogoAttachment={ fetchLogoAttachment } + wizardApiFetch={ wizardApiFetch } + /> + ) } + /> + <Route + path={ `${ path }/:brandId` } + render={ ( { + match, + }: { + match: { params: { brandId: string } }; + } ) => ( + <BrandUpsert + { ...wizardScreenProps } + brands={ brands } + editBrand={ Number( match.params.brandId ) } + upsertBrand={ upsertBrand } + fetchLogoAttachment={ fetchLogoAttachment } + wizardApiFetch={ wizardApiFetch } + errorMessage={ errorMessage } + /> + ) } + /> + </Switch> + </WizardSection> + </WizardsTab> + ); +} diff --git a/src/wizards/newspack/views/settings/additional-brands/style.scss b/src/wizards/newspack/views/settings/additional-brands/style.scss new file mode 100755 index 0000000000..5b8da6b62d --- /dev/null +++ b/src/wizards/newspack/views/settings/additional-brands/style.scss @@ -0,0 +1,39 @@ +.newspack-brand { + &__header { + &__logo { + .newspack-image-upload__image { + border-color: rgba(black, 0.54); + height: 98px; + } + } + } + + &__base-url-radio-control { + margin-bottom: 16px; + + .components-base-control__field > div { + justify-content: flex-start; + flex-direction: row; + + .components-radio-control__option { + margin-right: 32px; + + &:last-child { + margin-right: 0; + } + } + } + } + + &__base-url-component { + display: flex; + align-items: baseline; + } + + &__theme-mod-color-picker { + .newspack-color-picker__label { + display: flex; + justify-content: space-between; + } + } +} diff --git a/src/wizards/newspack/views/settings/additional-brands/types.d.ts b/src/wizards/newspack/views/settings/additional-brands/types.d.ts new file mode 100644 index 0000000000..2de6886cba --- /dev/null +++ b/src/wizards/newspack/views/settings/additional-brands/types.d.ts @@ -0,0 +1,32 @@ +interface ThemeColorsMeta { + color: string; + name: string; + theme_mod_name?: string; + default?: string; +} + +type Brand = { + id: number; + count: number; + description: string; + link: string; + name: string; + slug: string; + taxonomy: string; + parent: number; + meta: { + _custom_url: string; + _show_page_on_front: number; + _logo: number | Attachment; + _theme_colors: ThemeColorsMeta[]; + _menus: Array< { + location: string; + menu: number; + } >; + }; +}; + +interface PublicPage { + id: string; + title: { rendered: string }; +} diff --git a/src/wizards/newspack/views/settings/connections/analytics.tsx b/src/wizards/newspack/views/settings/connections/analytics.tsx new file mode 100644 index 0000000000..b617176a2e --- /dev/null +++ b/src/wizards/newspack/views/settings/connections/analytics.tsx @@ -0,0 +1,33 @@ +/** + * Settings Wizard: Connections > Analytics + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import WizardsPluginCard from '../../../../wizards-plugin-card'; + +const { analytics } = window.newspackSettings.connections.sections; + +/** + * Analytics Plugins Section + */ +function Analytics() { + return ( + <WizardsPluginCard + { ...{ + editLink: analytics.editLink, + slug: 'google-site-kit', + title: __( 'Google Analytics', 'newspack-plugin' ), + actionText: { complete: __( 'View', 'newspack-plugin' ) }, + } } + /> + ); +} + +export default Analytics; diff --git a/src/wizards/newspack/views/settings/connections/constants.ts b/src/wizards/newspack/views/settings/connections/constants.ts new file mode 100644 index 0000000000..22bd779e5b --- /dev/null +++ b/src/wizards/newspack/views/settings/connections/constants.ts @@ -0,0 +1,34 @@ +/** + * Newspack Settings Connections Constants. + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Error messages. + */ +export const ERROR_MESSAGES = { + RECAPTCHA: { + SITE_KEY_EMPTY: __( 'Site Key cannot be empty!', 'newspack-plugin' ), + SITE_SECRET_EMPTY: __( 'Site Secret cannot be empty', 'newspack-plugin' ), + THRESHOLD_INVALID_MIN: __( 'Threshold cannot be less than 0.1', 'newspack-plugin' ), + THRESHOLD_INVALID_MAX: __( 'Threshold cannot be greater than 1', 'newspack-plugin' ), + VERSION_CHANGE: __( + 'Your site key and secret must match the selected reCAPTCHA version. Please enter new credentials.', + 'newspack-plugin' + ), + }, + CUSTOM_EVENTS: { + INVALID_MEASUREMENT_ID: __( + 'You need a valid Measurement ID (e.g. "G-ABCDE12345") to activate Newspack Custom Events.', + 'newspack-plugin' + ), + INVALID_MEASUREMENT_PROTOCOL_SECRET: __( + 'You need a valid Measurement API Secret to activate Newspack Custom Events.', + 'newspack-plugin' + ), + }, +}; diff --git a/src/wizards/newspack/views/settings/connections/custom-events.tsx b/src/wizards/newspack/views/settings/connections/custom-events.tsx new file mode 100644 index 0000000000..678faa8d2c --- /dev/null +++ b/src/wizards/newspack/views/settings/connections/custom-events.tsx @@ -0,0 +1,170 @@ +/** + * Settings Wizard: Connections > Custom Events + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState, useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { WizardError } from '../../../../errors'; +import { useWizardApiFetch } from '../../../../hooks/use-wizard-api-fetch'; +import { Button, Grid, Notice, TextControl, utils } from '../../../../../components/src'; +import { ERROR_MESSAGES } from './constants'; + +/** + * Validate GA4 Measurement ID. + * + * @see https://measureschool.com/ga4-measurement-id/ + * + * @param measurementId Measurement ID to validate + * @return boolean True if the measurement ID is valid, false otherwise + */ +function isValidGA4MeasurementID( measurementId = '' ) { + const ga4Pattern = /^G-[A-Za-z0-9]{10,}$/; + return ga4Pattern.test( measurementId ); +} + +let { customEvents } = window.newspackSettings.connections.sections; + +/** + * Analytics Custom Events screen. + */ +function CustomEvents() { + const [ ga4Credentials, setGa4Credentials ] = useState< Ga4Credentials >( customEvents ); + const { wizardApiFetch, errorMessage, resetError, setError } = useWizardApiFetch( + 'newspack-settings/connections/custom-events' + ); + + function isInputsEmpty() { + return ! ga4Credentials.measurement_id && ! ga4Credentials.measurement_protocol_secret; + } + + useEffect( () => { + if ( isInputsEmpty() ) { + resetError(); + return; + } + if ( ! isValidGA4MeasurementID( ga4Credentials.measurement_id ) ) { + setError( + new WizardError( + ERROR_MESSAGES.CUSTOM_EVENTS.INVALID_MEASUREMENT_ID, + 'ga4_invalid_measurement_id' + ) + ); + return; + } + if ( ! ga4Credentials.measurement_protocol_secret ) { + setError( + new WizardError( + ERROR_MESSAGES.CUSTOM_EVENTS.INVALID_MEASUREMENT_PROTOCOL_SECRET, + 'ga4_invalid_measurement_protocol_secret' + ) + ); + return; + } + resetError(); + }, [ ga4Credentials.measurement_id, ga4Credentials.measurement_protocol_secret ] ); + + function updateGa4Credentials() { + wizardApiFetch< Ga4Credentials >( + { + path: '/newspack/v2/wizard/analytics/ga4-credentials', + method: 'POST', + data: { + measurement_id: ga4Credentials.measurement_id, + measurement_protocol_secret: ga4Credentials.measurement_protocol_secret, + }, + }, + { + onSuccess( fetchedData ) { + customEvents = fetchedData; + setGa4Credentials( fetchedData ); + }, + } + ); + } + + function resetGa4Credentials() { + if ( + ! utils.confirmAction( + __( 'Are you sure you want to reset the GA4 credentials?', 'newspack-plugin' ) + ) + ) { + return; + } + wizardApiFetch< Ga4Credentials >( + { + path: '/newspack/v2/wizard/analytics/ga4-credentials/reset', + method: 'POST', + }, + { + onSuccess( fetchedData ) { + customEvents = { + measurement_id: '', + measurement_protocol_secret: '', + }; + setGa4Credentials( fetchedData ); + }, + } + ); + } + + return ( + <div className="newspack__analytics-configuration"> + <div className="newspack__analytics-configuration__header"> + <p> + { __( + "Newspack already sends some custom event data to your GA account, but adding the credentials below enables enhanced events that are fired from your site's backend. For example, when a donation is confirmed or when a user successfully subscribes to a newsletter.", + 'newspack-plugin' + ) } + </p> + </div> + <Grid noMargin rowGap={ 16 }> + <TextControl + value={ ga4Credentials.measurement_id } + label={ __( 'Measurement ID', 'newspack-plugin' ) } + help={ __( + 'You can find this in Site Kit Settings, or in Google Analytics > Admin > Data Streams and clickng the data stream. Example: G-ABCDE12345', + 'newspack-plugin' + ) } + onChange={ ( value: string ) => + setGa4Credentials( { ...ga4Credentials, measurement_id: value } ) + } + autoComplete="off" + /> + <TextControl + type="password" + value={ ga4Credentials.measurement_protocol_secret } + label={ __( 'Measurement Protocol API Secret', 'newspack-plugin' ) } + help={ __( + 'Generate an API secret from your GA dashboard in Admin > Data Streams and opening your data stream. Select "Measurement Protocol API secrets" under the Events section. Create a new secret.', + 'newspack-plugin' + ) } + onChange={ ( value: string ) => + setGa4Credentials( { ...ga4Credentials, measurement_protocol_secret: value } ) + } + autoComplete="one-time-code" + /> + </Grid> + { errorMessage && <Notice isError noticeText={ errorMessage } /> } + <Button + className="mr2" + variant="primary" + onClick={ updateGa4Credentials } + disabled={ isInputsEmpty() || !! errorMessage } + > + { __( 'Save', 'newspack-plugin' ) } + </Button> + <Button variant="secondary" onClick={ resetGa4Credentials } disabled={ isInputsEmpty() }> + { __( 'Reset', 'newspack-plugin' ) } + </Button> + </div> + ); +} + +export default CustomEvents; diff --git a/src/wizards/newspack/views/settings/connections/google-oauth.tsx b/src/wizards/newspack/views/settings/connections/google-oauth.tsx new file mode 100644 index 0000000000..349caa2713 --- /dev/null +++ b/src/wizards/newspack/views/settings/connections/google-oauth.tsx @@ -0,0 +1,184 @@ +/** + * Settings Wizard: Connections > Google OAuth + */ + +/** + * WordPress dependencies. + */ +import { __, sprintf } from '@wordpress/i18n'; +import { useEffect, useState } from '@wordpress/element'; + +/** + * Internal dependencies. + */ +import { Button } from '../../../../../components/src'; +import WizardsActionCard from '../../../../wizards-action-card'; +import { useWizardApiFetch } from '../../../../hooks/use-wizard-api-fetch'; +import { WizardError, WIZARD_ERROR_MESSAGES } from '../../../../errors'; + +function getURLParams() { + const searchParams = new URLSearchParams( window.location.search ); + const params: { [ key: string ]: string } = {}; + for ( const [ key, value ] of searchParams.entries() ) { + params[ key ] = value; + } + return params; +} + +function GoogleOAuth( { + onSuccess, + isOnboarding, +}: { + onInit?: ( str: Error | null ) => void; + onSuccess?: ( arg: OAuthData ) => void; + isOnboarding?: ( str: string ) => void; +} ) { + const [ authState, setAuthState ] = useState< OAuthData >( {} ); + + const userBasicInfo = authState?.user_basic_info; + const isConnected = Boolean( userBasicInfo && userBasicInfo.email ); + + const { + setError, + errorMessage, + wizardApiFetch, + isFetching: inFlight, + } = useWizardApiFetch( '/newspack-settings/connections/apis/google-oauth' ); + + // We only want to autofetch the current auth state if we're not onboarding. + useEffect( () => { + if ( ! isOnboarding ) { + getCurrentAuth(); + } + }, [] ); + + useEffect( () => { + if ( isConnected && userBasicInfo && ! userBasicInfo.has_refresh_token ) { + setError( + new WizardError( + WIZARD_ERROR_MESSAGES.GOOGLEOAUTH_REFRESH_TOKEN_EXPIRED, + 'googleoauth_refresh_token_expired' + ) + ); + } + }, [ isConnected ] ); + + function getCurrentAuth() { + const params = getURLParams(); + if ( ! params.access_token ) { + wizardApiFetch< OAuthData >( + { + path: '/newspack/v1/oauth/google', + isCached: false, + }, + { + onSuccess( data ) { + setAuthState( data ); + if ( data?.user_basic_info ) { + if ( typeof onSuccess === 'function' ) { + onSuccess( data ); + } + setError( null ); + } + }, + } + ); + } + } + + function openAuth() { + let authWindow: Window | null = null; + wizardApiFetch< string >( + { + path: '/newspack/v1/oauth/google/start', + isCached: false, + }, + { + onSuccess( url ) { + if ( url === null ) { + setError( + new WizardError( + WIZARD_ERROR_MESSAGES.GOOGLE.URL_INVALID, + 'googleoauth_popup_blocked' + ) + ); + return; + } + authWindow = window.open( url, 'newspack_google_oauth', 'width=500,height=600' ); + /** authWindow can be 'null' due to browser's popup blocker. */ + if ( authWindow === null ) { + setError( + new WizardError( + WIZARD_ERROR_MESSAGES.GOOGLE.OAUTH_POPUP_BLOCKED, + 'googleoauth_popup_blocked' + ) + ); + return; + } + const interval = setInterval( () => { + if ( authWindow?.closed ) { + clearInterval( interval ); + getCurrentAuth(); + } + }, 500 ); + }, + onError() { + if ( authWindow ) { + authWindow.close(); + } + }, + } + ); + } + + // Redirect user to Google auth screen. + function disconnect() { + wizardApiFetch< void >( + { + path: '/newspack/v1/oauth/google/revoke', + method: 'DELETE', + }, + { + onSuccess: () => setAuthState( {} ), + } + ); + } + + function getDescription() { + if ( inFlight ) { + return __( 'Loading…', 'newspack-plugin' ); + } + if ( isConnected ) { + return sprintf( + // Translators: connected user's email address. + __( 'Connected as %s', 'newspack-plugin' ), + userBasicInfo?.email + ); + } + return __( 'Not connected', 'newspack-plugin' ); + } + + return ( + <WizardsActionCard + title={ __( 'Google', 'newspack-plugin' ) } + description={ getDescription() } + isChecked={ isConnected } + actionText={ + <Button + variant="link" + isDestructive={ isConnected } + onClick={ isConnected ? disconnect : openAuth } + disabled={ inFlight } + > + { isConnected + ? __( 'Disconnect', 'newspack-plugin' ) + : __( 'Connect', 'newspack-plugin' ) } + </Button> + } + error={ errorMessage } + isMedium + /> + ); +} + +export default GoogleOAuth; diff --git a/src/wizards/newspack/views/settings/connections/index.tsx b/src/wizards/newspack/views/settings/connections/index.tsx new file mode 100644 index 0000000000..8d5ab07f08 --- /dev/null +++ b/src/wizards/newspack/views/settings/connections/index.tsx @@ -0,0 +1,86 @@ +/** + * Settings Connections: Plugins, APIs, reCAPTCHA, Webhooks, Analytics, Custom Events + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import './style.scss'; +import Plugins from './plugins'; +import Webhooks from './webhooks'; +import Analytics from './analytics'; +import Recaptcha from './recaptcha'; +import JetpackSSO from './jetpack-sso'; +import Mailchimp from './mailchimp'; +import GoogleOAuth from './google-oauth'; +import CustomEvents from './custom-events'; + +import WizardsTab from '../../../../wizards-tab'; +import WizardSection from '../../../../wizards-section'; + +const { connections } = window.newspackSettings; + +function Connections() { + return ( + <WizardsTab title={ __( 'Connections', 'newspack-plugin' ) }> + { /* Plugins */ } + <WizardSection title={ __( 'Plugins', 'newspack-plugin' ) }> + <Plugins /> + </WizardSection> + + { /* APIs; google */ } + <WizardSection title={ __( 'APIs', 'newspack-plugin' ) }> + { connections.sections.apis.dependencies?.googleOAuth && ( + <GoogleOAuth /> + ) } + <Mailchimp /> + </WizardSection> + + { /* Jetpack SSO */ } + { connections.sections.jetpack_sso.dependencies?.jetpack_sso ? ( + <WizardSection title={ __( 'Jetpack SSO', 'newspack-plugin' ) }> + <JetpackSSO /> + </WizardSection> + ) : null } + + { /* reCAPTCHA */ } + <WizardSection + scrollToAnchor="newspack-settings-recaptcha" + title={ __( 'reCAPTCHA', 'newspack-plugin' ) } + > + <Recaptcha /> + </WizardSection> + + { /* Webhooks */ } + <WizardSection> + <Webhooks /> + </WizardSection> + + { /* Analytics */ } + <WizardSection title={ __( 'Analytics', 'newspack-plugin' ) }> + <Analytics /> + </WizardSection> + + { /* Custom Events */ } + <WizardSection + title={ __( + 'Activate Newspack Custom Events', + 'newspack-plugin' + ) } + description={ __( + 'Allows Newspack to send enhanced custom event data to your Google Analytics.', + 'newspack-plugin' + ) } + > + <CustomEvents /> + </WizardSection> + </WizardsTab> + ); +} + +export default Connections; diff --git a/src/wizards/connections/views/main/jetpack-sso.js b/src/wizards/newspack/views/settings/connections/jetpack-sso.tsx similarity index 72% rename from src/wizards/connections/views/main/jetpack-sso.js rename to src/wizards/newspack/views/settings/connections/jetpack-sso.tsx index b6c5a3198d..78008b1764 100644 --- a/src/wizards/connections/views/main/jetpack-sso.js +++ b/src/wizards/newspack/views/settings/connections/jetpack-sso.tsx @@ -1,4 +1,3 @@ -/* globals newspack_connections_data */ /** * WordPress dependencies */ @@ -15,25 +14,31 @@ import { Button, Grid, Notice, - SectionHeader, SelectControl, -} from '../../../../components/src'; +} from '../../../../../components/src'; + +const isValidError = ( e: unknown ): e is WpRestApiError => { + return e instanceof Error && 'message' in e; +} const JetpackSSO = () => { - const [ error, setError ] = useState( null ); - const [ isLoading, setIsLoading ] = useState( false ); - const [ settings, setSettings ] = useState( {} ); - const [ settingsToUpdate, setSettingsToUpdate ] = useState( {} ); + const [ error, setError ] = useState<string>( '' ); + const [ isLoading, setIsLoading ] = useState<boolean>( false ); + const [ settings, setSettings ] = useState<JetpackSSOSettings>( {} ); + const [ settingsToUpdate, setSettingsToUpdate ] = useState<JetpackSSOSettings>( {} ); + + const getCapLabel = ( cap: JetpackSSOCaps ): string | undefined => + settings.available_caps ? settings.available_caps[ cap ] : undefined; useEffect( () => { const fetchSettings = async () => { setIsLoading( true ); try { - const fetchedSettings = await apiFetch( { path: '/newspack-manager/v1/jetpack-sso' } ); + const fetchedSettings = await apiFetch<JetpackSSOSettings>( { path: '/newspack-manager/v1/jetpack-sso' } ); setSettings( fetchedSettings ); setSettingsToUpdate( fetchedSettings ); - } catch ( e ) { - setError( e.message || __( 'Error fetching settings.', 'newspack-plugin' ) ); + } catch ( e: unknown ) { + setError( isValidError( e ) ? e.message : __( 'Error fetching settings.', 'newspack-plugin' ) ); } finally { setIsLoading( false ); } @@ -41,29 +46,25 @@ const JetpackSSO = () => { fetchSettings(); }, [] ); - const updateSettings = async data => { - setError( null ); + const updateSettings = async ( data: JetpackSSOSettings ) => { + setError( '' ); setIsLoading( true ); try { - const newSettings = await apiFetch( { + const newSettings = await apiFetch<JetpackSSOSettings>( { path: '/newspack-manager/v1/jetpack-sso', method: 'POST', data, } ); setSettings( newSettings ); setSettingsToUpdate( newSettings ); - } catch ( e ) { - setError( e?.message || __( 'Error updating settings.', 'newspack-plugin' ) ); + } catch ( e: unknown ) { + setError( isValidError( e ) ? e.message : __( 'Error updating settings.', 'newspack-plugin' ) ); } finally { setIsLoading( false ); } }; - if ( ! newspack_connections_data.can_use_jetpack_sso ) { - return null; - } return ( <> - <SectionHeader id="jetpack-sso" title={ __( 'Jetpack SSO', 'newspack-plugin' ) } /> <ActionCard isMedium title={ __( 'Force two-factor authentication', 'newspack-plugin' ) } @@ -120,12 +121,12 @@ const JetpackSSO = () => { label={ __( 'Capability', 'newspack-plugin' ) } hideLabelFromVision value={ settingsToUpdate?.force_2fa_cap || '' } - onChange={ value => + onChange={ ( value: JetpackSSOCaps ) => setSettingsToUpdate( { ...settingsToUpdate, force_2fa_cap: value } ) } options={ - Object.keys( settings.available_caps || {} ).map( cap => ( { - label: settings.available_caps[ cap ], + Object.keys( settings.available_caps || {} ).map( ( cap: string ) => ( { + label: getCapLabel( cap as JetpackSSOCaps ), value: cap, } ) ) } diff --git a/src/wizards/newspack/views/settings/connections/mailchimp.tsx b/src/wizards/newspack/views/settings/connections/mailchimp.tsx new file mode 100644 index 0000000000..4464b96915 --- /dev/null +++ b/src/wizards/newspack/views/settings/connections/mailchimp.tsx @@ -0,0 +1,215 @@ +/** + * Settings Wizard: Connections > Mailchimp + */ + +/** + * WordPress dependencies. + */ +import { ENTER } from '@wordpress/keycodes'; +import { __, sprintf } from '@wordpress/i18n'; +import { ExternalLink } from '@wordpress/components'; +import { useEffect, useState, useRef, Fragment } from '@wordpress/element'; + +/** + * Internal dependencies. + */ +import WizardsActionCard from '../../../../wizards-action-card'; +import { useWizardApiFetch } from '../../../../hooks/use-wizard-api-fetch'; +import { WIZARD_ERROR_MESSAGES, WizardError } from '../../../../errors'; +import { + Button, + Card, + Grid, + Modal, + TextControl, +} from '../../../../../components/src'; + +function Mailchimp() { + const [ isModalOpen, setIsModalOpen ] = useState( false ); + const { wizardApiFetch, isFetching, errorMessage, setError, resetError } = + useWizardApiFetch( '/newspack-settings/connections/apis/mailchimp' ); + const [ authState, setAuthState ] = useState< OAuthData >(); + const [ apiKey, setAPIKey ] = useState< string | undefined >(); + + const modalTextRef = useRef< HTMLDivElement | null >( null ); + const isConnected = Boolean( authState && authState.username ); + + useEffect( () => { + wizardApiFetch< OAuthData >( + { + path: '/newspack/v1/oauth/mailchimp', + }, + { + onSuccess: res => setAuthState( res ), + } + ); + }, [] ); + + useEffect( () => { + if ( isModalOpen && modalTextRef.current ) { + const [ inputElement ] = + modalTextRef.current.getElementsByTagName( 'input' ); + if ( inputElement ) { + inputElement.focus(); + } + } + }, [ isModalOpen ] ); + + function openModal() { + return setIsModalOpen( true ); + } + + function closeModal() { + setIsModalOpen( false ); + setAPIKey( undefined ); + } + + function submitAPIKey() { + wizardApiFetch< OAuthData >( + { + path: '/newspack/v1/oauth/mailchimp', + method: 'POST', + data: { + api_key: apiKey, + }, + updateCacheMethods: [ 'GET' ], + }, + { + onSuccess( response ) { + setAuthState( response ); + resetError(); + }, + onFinally() { + closeModal(); + }, + } + ); + } + + function disconnect() { + wizardApiFetch< OAuthData >( + { + path: '/newspack/v1/oauth/mailchimp', + method: 'DELETE', + updateCacheMethods: [ 'GET' ], + }, + { + onSuccess( data ) { + setAuthState( data ); + setError( + new WizardError( + WIZARD_ERROR_MESSAGES.MAILCHIMP_API_KEY_INVALID, + 'MAILCHIMP_API_KEY_INVALID' + ) + ); + }, + } + ); + } + + function getDescription() { + if ( isFetching ) { + return __( 'Loading…', 'newspack-plugin' ); + } + if ( isConnected ) { + // Translators: user connection status message. + return sprintf( + /* translators: %s: username */ + __( 'Connected as %s', 'newspack-plugin' ), + authState?.username ?? {} + ); + } + return __( 'Not connected', 'newspack-plugin' ); + } + + function getModalButtonText() { + if ( ! apiKey ) { + return __( 'Invalid Mailchimp API Key.', 'newspack' ); + } + if ( isFetching ) { + return __( 'Connecting…', 'newspack-plugin' ); + } + if ( isConnected ) { + return __( 'Connected', 'newspack-plugin' ); + } + return __( 'Connect', 'newspack-plugin' ); + } + + return ( + <Fragment> + <WizardsActionCard + title="Mailchimp" + description={ getDescription() } + isChecked={ isConnected } + actionText={ + <Button + variant="link" + isDestructive={ isConnected } + onClick={ isConnected ? disconnect : openModal } + disabled={ isFetching } + > + { isConnected + ? __( 'Disconnect', 'newspack-plugin' ) + : __( 'Connect', 'newspack-plugin' ) } + </Button> + } + error={ errorMessage } + isMedium + /> + { isModalOpen && ( + <Modal + title={ __( 'Add Mailchimp API Key', 'newspack-plugin' ) } + onRequestClose={ closeModal } + > + <div ref={ modalTextRef }> + <Grid columns={ 1 } gutter={ 8 }> + <TextControl + placeholder="123457103961b1f4dc0b2b2fd59c137b-us1" + label={ __( + 'Mailchimp API Key', + 'newspack-plugin' + ) } + hideLabelFromVision={ true } + value={ apiKey ?? '' } + onChange={ ( value: string ) => + setAPIKey( value ) + } + onKeyDown={ ( event: KeyboardEvent ) => { + if ( + ENTER === event.keyCode && + '' !== apiKey + ) { + event.preventDefault(); + submitAPIKey(); + } + } } + /> + <p> + <ExternalLink href="https://mailchimp.com/help/about-api-keys/#Find_or_generate_your_API_key"> + { __( + 'Find or generate your API key', + 'newspack-plugin' + ) } + </ExternalLink> + </p> + </Grid> + </div> + <Card buttonsCard noBorder className="justify-end"> + <Button variant="secondary" onClick={ closeModal }> + { __( 'Cancel', 'newspack-plugin' ) } + </Button> + <Button + variant="primary" + disabled={ ! apiKey } + onClick={ submitAPIKey } + > + { getModalButtonText() } + </Button> + </Card> + </Modal> + ) } + </Fragment> + ); +} + +export default Mailchimp; diff --git a/src/wizards/newspack/views/settings/connections/plugins.tsx b/src/wizards/newspack/views/settings/connections/plugins.tsx new file mode 100644 index 0000000000..380e228686 --- /dev/null +++ b/src/wizards/newspack/views/settings/connections/plugins.tsx @@ -0,0 +1,94 @@ +/** + * Settings Wizard: Connections > Plugins + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Fragment } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import WizardsPluginCard from '../../../../wizards-plugin-card'; + +const { plugins: pluginsSection, analytics: analyticsSection } = + window.newspackSettings.connections.sections; + +const PLUGINS: Record< string, PluginCard > = { + jetpack: { + slug: 'jetpack', + title: __( 'Jetpack', 'newspack-plugin' ), + editLink: 'admin.php?page=jetpack#/settings', + }, + 'google-site-kit': { + slug: 'google-site-kit', + editLink: analyticsSection.editLink, + title: __( 'Site Kit by Google', 'newspack-plugin' ), + statusDescription: { + notConfigured: __( + 'Not connected for this user', + 'newspack-plugin' + ), + }, + }, + everlit: { + slug: 'everlit', + editLink: 'admin.php?page=everlit_settings', + title: __( 'Everlit', 'newspack-plugin' ), + subTitle: __( 'AI-Generated Audio Stories', 'newspack-plugin' ), + description: ( + <> + { __( + 'Complete setup and licensing agreement to unlock 5 free audio stories per month.', + 'newspack-plugin' + ) }{ ' ' } + <a + href="https://everlit.audio/" + target="_blank" + rel="noreferrer" + > + { __( 'Learn more', 'newspack-plugin' ) } + </a> + </> + ), + statusDescription: { + uninstalled: __( 'Not installed.', 'newspack-plugin' ), + inactive: __( 'Inactive.', 'newspack-plugin' ), + notConfigured: __( 'Pending.', 'newspack-plugin' ), + }, + }, +}; + +/** + * Newspack Settings Plugins section. + */ +function Plugins() { + const plugins = Object.keys( PLUGINS ).reduce( + ( acc: Record< string, PluginCard >, pluginKey ) => { + acc[ pluginKey ] = { + ...PLUGINS[ pluginKey ], + isEnabled: pluginsSection.enabled?.[ pluginKey ] ?? true, + }; + return acc; + }, + {} + ); + return ( + <Fragment> + { Object.keys( plugins ).map( pluginKey => { + return ( + plugins[ pluginKey ].isEnabled && ( + <WizardsPluginCard + key={ pluginKey } + { ...plugins[ pluginKey ] } + /> + ) + ); + } ) } + </Fragment> + ); +} + +export default Plugins; diff --git a/src/wizards/newspack/views/settings/connections/recaptcha.tsx b/src/wizards/newspack/views/settings/connections/recaptcha.tsx new file mode 100644 index 0000000000..d1cd6e66c0 --- /dev/null +++ b/src/wizards/newspack/views/settings/connections/recaptcha.tsx @@ -0,0 +1,328 @@ +/** + * Settings Wizard: Connections > reCAPTCHA + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { BaseControl, ExternalLink } from '@wordpress/components'; +import { useEffect, useState, Fragment } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { ERROR_MESSAGES } from './constants'; +import WizardsActionCard from '../../../../wizards-action-card'; +import WizardError from '../../../../errors/class-wizard-error'; +import { useWizardApiFetch } from '../../../../hooks/use-wizard-api-fetch'; +import { + Grid, + Button, + TextControl, + SelectControl, +} from '../../../../../components/src'; + +const settingsDefault: RecaptchaData = { + threshold: '', + use_captcha: false, + version: 'v3', + credentials: { + v2_invisible: { site_key: '', site_secret: '' }, + v3: { site_key: '', site_secret: '' }, + }, +}; + +type RecaptchaDependsOn = { [ k in keyof RecaptchaData ]?: string }; + +const fieldValidationMap = new Map< + keyof Omit< RecaptchaData, 'use_captcha' >, + { + callback: ( value: any, version?: RecaptchaVersions ) => string; + dependsOn?: RecaptchaDependsOn; + } +>( [ + [ + 'credentials', + { + dependsOn: { version: 'v3' }, + callback: ( + credentials: RecaptchaData[ 'credentials' ], + version = 'v3' + ) => { + if ( ! credentials[ version ].site_key ) { + return ERROR_MESSAGES.RECAPTCHA.SITE_KEY_EMPTY; + } + if ( ! credentials[ version ].site_secret ) { + return ERROR_MESSAGES.RECAPTCHA.SITE_SECRET_EMPTY; + } + return ''; + }, + }, + ], + [ + 'threshold', + { + dependsOn: { version: 'v3' }, + callback: value => { + const threshold = parseFloat( value || '0' ); + if ( threshold < 0.1 ) { + return ERROR_MESSAGES.RECAPTCHA.THRESHOLD_INVALID_MIN; + } + if ( threshold > 1 ) { + return ERROR_MESSAGES.RECAPTCHA.THRESHOLD_INVALID_MAX; + } + return ''; + }, + }, + ], +] ); + +const apiPath = '/newspack/v1/recaptcha'; + +function Recaptcha() { + const { wizardApiFetch, isFetching, errorMessage, setError, resetError } = + useWizardApiFetch( '/newspack-settings/connections/recaptcha' ); + + const [ settings, setSettings ] = useState< RecaptchaData >( { + ...settingsDefault, + } ); + const [ settingsToUpdate, setSettingsToUpdate ] = useState< RecaptchaData >( + { + ...settingsDefault, + } + ); + const credentials = settingsToUpdate.credentials || {}; + const versionCredentials = credentials[ settingsToUpdate.version ]; + + useEffect( () => { + wizardApiFetch< RecaptchaData >( + { + path: apiPath, + }, + { + onSuccess( fetchedSettings ) { + setSettings( fetchedSettings ); + setSettingsToUpdate( fetchedSettings ); + }, + } + ); + }, [] ); + + function updateSettings( data: RecaptchaData, isToggleSave = false ) { + resetError(); + + // Perform validation on non `use_captcha` updates. + if ( ! isToggleSave ) { + for ( const [ field, validate ] of fieldValidationMap ) { + if ( validate.dependsOn ) { + const [ [ key, value ] ] = Object.entries( + validate.dependsOn + ); + if ( + settingsToUpdate[ key as keyof RecaptchaDependsOn ] !== + value + ) { + continue; + } + } + const validationError = validate.callback( + settingsToUpdate[ field ], + settingsToUpdate.version + ); + if ( validationError ) { + setError( new WizardError( validationError, field ) ); + return; + } + } + } + + wizardApiFetch< RecaptchaData >( + { + path: apiPath, + method: 'POST', + data, + updateCacheMethods: [ 'GET' ], + }, + { + onSuccess( fetchedSettings ) { + setSettings( fetchedSettings ); + setSettingsToUpdate( fetchedSettings ); + }, + } + ); + } + + function onCredentialsChange( + field: 'site_key' | 'site_secret', + value: string + ) { + setSettingsToUpdate( prev => ( { + ...prev, + credentials: { + ...prev.credentials, + [ prev.version ]: { + ...prev.credentials[ prev.version ], + [ field ]: value, + }, + }, + } ) ); + } + + return ( + <WizardsActionCard + isMedium + title={ __( 'Use reCAPTCHA', 'newspack-plugin' ) } + description={ () => ( + <Fragment> + { isFetching && ! settings.use_captcha ? ( + __( 'Loading…', 'newspack-plugin' ) + ) : ( + <> + { __( + 'Enabling reCAPTCHA can help protect your site against bot attacks and credit card testing.', + 'newspack-plugin' + ) }{ ' ' } + <ExternalLink href="https://www.google.com/recaptcha/admin/create"> + { __( 'Get started', 'newspack-plugin' ) } + </ExternalLink> + </> + ) } + </Fragment> + ) } + hasGreyHeader={ !! settings.use_captcha } + toggleChecked={ !! settings.use_captcha } + toggleOnChange={ () => + updateSettings( + { + ...settings, + use_captcha: ! settings.use_captcha, + }, + true + ) + } + actionContent={ + settings.use_captcha && ( + <Button + variant="primary" + disabled={ + isFetching || + ! Object.keys( settingsToUpdate ).length + } + onClick={ () => updateSettings( settingsToUpdate ) } + > + { isFetching + ? __( 'Loading…', 'newspack-plugin' ) + : __( 'Save Settings', 'newspack-plugin' ) } + </Button> + ) + } + error={ settings.use_captcha ? errorMessage : null } + disabled={ isFetching } + > + { settings.use_captcha && ( + <Fragment> + <Grid noMargin rowGap={ 16 }> + <BaseControl + id="recaptcha-version" + label={ __( + 'reCAPTCHA Version', + 'newspack-plugin' + ) } + help={ + <ExternalLink href="https://developers.google.com/recaptcha/docs/versions"> + { __( + 'Learn more about reCAPTCHA versions', + 'newspack-plugin' + ) } + </ExternalLink> + } + > + <SelectControl + label={ __( + 'reCAPTCHA Version', + 'newspack-plugin' + ) } + hideLabelFromVision + value={ settingsToUpdate.version || 'v3' } + onChange={ ( version: RecaptchaVersions ) => + setSettingsToUpdate( { + ...settingsToUpdate, + version, + } ) + } + // Note: add 'v2_checkbox' here and in Recaptcha::SUPPORTED_VERSIONS to add support for the Checkbox flavor of reCAPTCHA v2. + options={ [ + { + value: 'v3', + label: __( + 'Score based (v3)', + 'newspack-plugin' + ), + }, + { + value: 'v2_invisible', + label: __( + 'Challenge (v2) - invisible reCAPTCHA badge', + 'newspack-plugin' + ), + }, + ] } + /> + </BaseControl> + </Grid> + <Grid noMargin rowGap={ 16 }> + <TextControl + value={ versionCredentials.site_key || '' } + label={ __( 'Site Key', 'newspack-plugin' ) } + onChange={ ( value: string ) => + onCredentialsChange( 'site_key', value ) + } + disabled={ isFetching } + autoComplete="off" + /> + <TextControl + type="password" + value={ versionCredentials.site_secret || '' } + label={ __( 'Site Secret', 'newspack-plugin' ) } + onChange={ ( value: string ) => + onCredentialsChange( 'site_secret', value ) + } + disabled={ isFetching } + autoComplete="one-time-code" + /> + { settingsToUpdate.version === 'v3' && ( + <TextControl + type="number" + step="0.05" + min="0.1" + max="1" + value={ parseFloat( + settingsToUpdate?.threshold || '0' + ) } + label={ __( 'Threshold', 'newspack-plugin' ) } + onChange={ ( value: string ) => + setSettingsToUpdate( { + ...settingsToUpdate, + threshold: value, + } ) + } + disabled={ isFetching } + help={ + <ExternalLink href="https://developers.google.com/recaptcha/docs/v3#interpreting_the_score"> + { __( + 'Learn more about the threshold value', + 'newspack-plugin' + ) } + </ExternalLink> + } + /> + ) } + </Grid> + </Fragment> + ) } + </WizardsActionCard> + ); +} + +export default Recaptcha; diff --git a/src/wizards/connections/style.scss b/src/wizards/newspack/views/settings/connections/style.scss similarity index 76% rename from src/wizards/connections/style.scss rename to src/wizards/newspack/views/settings/connections/style.scss index c2b8ccb7b8..b94d1d2037 100644 --- a/src/wizards/connections/style.scss +++ b/src/wizards/newspack/views/settings/connections/style.scss @@ -1,9 +1,10 @@ @use "sass:color"; @use "~@wordpress/base-styles/colors" as wp-colors; +/** Webhooks */ .newspack-webhooks { - &__endpoint { - &__action { + .newspack-webhooks__endpoint { + .newspack-webhooks__endpoint-action { font-weight: 700; font-family: Consolas, monaco, monospace; background: wp-colors.$gray-100; @@ -14,10 +15,11 @@ font-size: 0.8em; } - &__label { + .newspack-webhooks__label { margin-right: 8px; } - &__url { + + .newspack-webhooks__url { font-family: Consolas, monaco, monospace; font-size: 0.8em; color: wp-colors.$gray-900; @@ -28,73 +30,64 @@ align-items: center; } } - &__test-response { - display: flex; - align-items: center; - justify-content: center; - font-family: Consolas, monaco, monospace; - font-size: 12px; - &.status { - &--success { - .code { - color: wp-colors.$alert-green; - } - } - &--error { - .code { - color: wp-colors.$alert-red; - } - } - } - .code { - margin-left: 8px; - } - } - &__requests { + + .newspack-webhooks__requests { border-collapse: collapse; white-space: nowrap; width: 100%; + th { text-align: left; } + tr:nth-child(odd) td { background-color: wp-colors.$gray-100; } + th, td { border-bottom: 1px solid wp-colors.$gray-300; padding: 6px; } + td { color: wp-colors.$gray-900; font-size: 12px; + &:last-child { text-align: right; } } + .status { - &--finished { + .status--finished { fill: wp-colors.$alert-green; } - &--killed { + + .status--killed { fill: wp-colors.$alert-red; } - &--pending { + + .status--pending { fill: wp-colors.$alert-yellow; } + svg { display: block; } } + .action-name { font-weight: 700; font-family: Consolas, monaco, monospace; } + &.has-error { .error { width: 100%; white-space: normal; } + .error-count { background: rgba(black, 0.025); color: color.adjust(wp-colors.$gray-700, $lightness: -0.75%); @@ -104,6 +97,7 @@ display: inline-block; } } + &:not(.has-error) { .action-name { width: 100%; diff --git a/src/wizards/newspack/views/settings/connections/webhooks/constants.ts b/src/wizards/newspack/views/settings/connections/webhooks/constants.ts new file mode 100644 index 0000000000..e4cd3dcf30 --- /dev/null +++ b/src/wizards/newspack/views/settings/connections/webhooks/constants.ts @@ -0,0 +1,16 @@ +/** + * Settings Wizard: Connections > Webhooks > Constants. + */ + +/** Wizard API Fetch namespace used for hooks */ +export const API_NAMESPACE = '/newspack-settings/connections/webhooks'; + +/** Cache key for get requests, primary storage for endpoints */ +export const ENDPOINTS_CACHE_KEY: Record< string, ApiMethods > = { + '/newspack/v1/webhooks/endpoints': 'GET', +}; + +export default { + API_NAMESPACE, + ENDPOINTS_CACHE_KEY, +}; diff --git a/src/wizards/newspack/views/settings/connections/webhooks/endpoint-actions-card.tsx b/src/wizards/newspack/views/settings/connections/webhooks/endpoint-actions-card.tsx new file mode 100644 index 0000000000..8fc6d5a10c --- /dev/null +++ b/src/wizards/newspack/views/settings/connections/webhooks/endpoint-actions-card.tsx @@ -0,0 +1,66 @@ +/** + * Settings Wizard: Connections > Webhooks > Endpoint Actions Card + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Fragment } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { getEndpointTitle } from './utils'; +import EndpointActions from './endpoint-actions'; +import WizardsActionCard from '../../../../../wizards-action-card'; + +function EndpointActionsCard( { + endpoint, + setAction, +}: { + endpoint: Endpoint; + setAction: ( action: WebhookActions, id: number | string ) => void; +} ) { + return ( + <WizardsActionCard + isMedium + className="newspack-webhooks__endpoint mt16" + toggleChecked={ ! endpoint.disabled } + toggleOnChange={ () => setAction( 'toggle', endpoint.id ) } + key={ endpoint.id } + title={ getEndpointTitle( endpoint ) } + description={ () => { + if ( endpoint.disabled && endpoint.disabled_error ) { + return `${ __( + 'Endpoint disabled due to error', + 'newspack-plugin' + ) }: ${ endpoint.disabled_error }`; + } + return ( + <Fragment> + { __( 'Actions:', 'newspack-plugin' ) }{ ' ' } + { endpoint.actions.map( action => ( + <span + key={ action } + className="newspack-webhooks__endpoint-action" + > + { action } + </span> + ) ) + } + </Fragment> + ); + } } + actionText={ + <EndpointActions + endpoint={ endpoint } + setAction={ setAction } + isSystem={ endpoint.system } + /> + } + /> + ); +} + +export default EndpointActionsCard; diff --git a/src/wizards/newspack/views/settings/connections/webhooks/endpoint-actions-modals.tsx b/src/wizards/newspack/views/settings/connections/webhooks/endpoint-actions-modals.tsx new file mode 100644 index 0000000000..80a31f45f1 --- /dev/null +++ b/src/wizards/newspack/views/settings/connections/webhooks/endpoint-actions-modals.tsx @@ -0,0 +1,147 @@ +/** + * Settings Wizard: Connections > Webhooks > Endpoint Actions Modals + */ + +/** + * WordPress dependencies + */ +import { Fragment } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import ModalView from './modals/view'; +import { getDisplayUrl } from './utils'; +import ModalUpsert from './modals/upsert'; +import ModalConfirmation from './modals/confirmation'; +import { ENDPOINTS_CACHE_KEY } from './constants'; + +const EndpointActionsModals = ( { + endpoint, + actions, + action = null, + errorMessage = null, + inFlight = false, + setAction, + setError, + setEndpoints, + wizardApiFetch, +}: ModalComponentProps ) => { + const onSuccess = ( endpointId: string | number, response: Endpoint[] ) => { + setEndpoints( response ); + setAction( null, endpointId ); + }; + + // API + function toggleEndpoint( endpointToToggle: Endpoint ) { + wizardApiFetch< Endpoint[] >( + { + path: `/newspack/v1/webhooks/endpoints/${ endpointToToggle.id }`, + method: 'POST', + data: { disabled: ! endpointToToggle.disabled }, + updateCacheKey: ENDPOINTS_CACHE_KEY, + }, + { + onSuccess: endpoints => + onSuccess( endpointToToggle.id, endpoints ), + } + ); + } + function deleteEndpoint( endpointToDelete: Endpoint ) { + wizardApiFetch< Endpoint[] >( + { + path: `/newspack/v1/webhooks/endpoints/${ endpointToDelete.id }`, + method: 'DELETE', + updateCacheKey: ENDPOINTS_CACHE_KEY, + }, + { + onSuccess: endpoints => + onSuccess( endpointToDelete.id, endpoints ), + } + ); + } + + return ( + <Fragment> + { action === 'delete' && ( + <ModalConfirmation + title={ __( 'Remove Endpoint', 'newspack-plugin' ) } + description={ sprintf( + /* translators: %s: endpoint title */ + __( + 'Are you sure you want to remove the endpoint %s?', + 'newspack-plugin' + ), + `"${ getDisplayUrl( endpoint.url ) }"` + ) } + onClose={ () => setAction( null, endpoint.id ) } + onConfirm={ () => deleteEndpoint( endpoint ) } + disabled={ inFlight } + /> + ) } + { action === 'toggle' && ( + <ModalConfirmation + title={ + endpoint.disabled + ? __( 'Enable Endpoint', 'newspack-plugin' ) + : __( 'Disable Endpoint', 'newspack-plugin' ) + } + description={ + endpoint.disabled + ? sprintf( + /* translators: %s: endpoint title */ + __( + 'Are you sure you want to enable the endpoint %s?', + 'newspack-plugin' + ), + `"${ getDisplayUrl( endpoint.url ) }"` + ) + : sprintf( + /* translators: %s: endpoint title */ + __( + 'Are you sure you want to disable the endpoint %s?', + 'newspack-plugin' + ), + `"${ getDisplayUrl( endpoint.url ) }"` + ) + } + onClose={ () => setAction( null, endpoint.id ) } + onConfirm={ () => toggleEndpoint( endpoint ) } + disabled={ inFlight } + /> + ) } + { action === 'view' && ( + <ModalView + { ...{ + action, + actions, + endpoint, + inFlight, + setAction, + setError, + errorMessage, + setEndpoints, + wizardApiFetch, + } } + /> + ) } + { [ 'edit', 'new' ].includes( action ?? '' ) && ( + <ModalUpsert + { ...{ + endpoint, + actions, + errorMessage, + inFlight, + setError, + setAction, + setEndpoints, + wizardApiFetch, + } } + /> + ) } + </Fragment> + ); +}; + +export default EndpointActionsModals; diff --git a/src/wizards/newspack/views/settings/connections/webhooks/endpoint-actions.tsx b/src/wizards/newspack/views/settings/connections/webhooks/endpoint-actions.tsx new file mode 100644 index 0000000000..d366444058 --- /dev/null +++ b/src/wizards/newspack/views/settings/connections/webhooks/endpoint-actions.tsx @@ -0,0 +1,84 @@ +/** + * Settings Wizard: Connections > Webhooks > Endpoint Actions + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { ESCAPE } from '@wordpress/keycodes'; +import { moreVertical } from '@wordpress/icons'; +import { useState, useEffect, Fragment } from '@wordpress/element'; +import { Button, Popover, MenuItem } from '@wordpress/components'; + +function EndpointActions( { + endpoint, + disabled = undefined, + isSystem, + setAction, +}: { + endpoint: Endpoint; + disabled?: boolean; + isSystem: string; + setAction: ( action: WebhookActions, id: number | string ) => void; +} ) { + const [ popoverVisible, setPopoverVisible ] = useState( false ); + + useEffect( () => { + setPopoverVisible( false ); + }, [ disabled ] ); + + return ( + <Fragment> + <Button + className={ popoverVisible ? 'popover-active' : '' } + onClick={ () => setPopoverVisible( ! popoverVisible ) } + icon={ moreVertical } + disabled={ disabled } + label={ __( 'Endpoint Actions', 'newspack-plugin' ) } + tooltipPosition={ 'bottom left' } + /> + { popoverVisible && ( + <Popover + position={ 'bottom left' } + onFocusOutside={ () => setPopoverVisible( false ) } + onKeyDown={ ( event: KeyboardEvent ) => + ESCAPE === event.keyCode && setPopoverVisible( false ) + } + > + <MenuItem + onClick={ () => setPopoverVisible( false ) } + className="screen-reader-text" + > + { __( 'Close Endpoint Actions', 'newspack-plugin' ) } + </MenuItem> + <MenuItem + onClick={ () => setAction( 'view', endpoint.id ) } + className="newspack-button" + > + { __( 'View Requests', 'newspack-plugin' ) } + </MenuItem> + { ! isSystem && ( + <MenuItem + onClick={ () => setAction( 'edit', endpoint.id ) } + className="newspack-button" + > + { __( 'Edit', 'newspack-plugin' ) } + </MenuItem> + ) } + { ! isSystem && ( + <MenuItem + onClick={ () => setAction( 'delete', endpoint.id ) } + className="newspack-button" + isDestructive + > + { __( 'Remove', 'newspack-plugin' ) } + </MenuItem> + ) } + </Popover> + ) } + </Fragment> + ); +} + +export default EndpointActions; diff --git a/src/wizards/newspack/views/settings/connections/webhooks/index.tsx b/src/wizards/newspack/views/settings/connections/webhooks/index.tsx new file mode 100644 index 0000000000..9369a2866a --- /dev/null +++ b/src/wizards/newspack/views/settings/connections/webhooks/index.tsx @@ -0,0 +1,156 @@ +/** + * Settings Wizard: Connections > Webhooks. + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useEffect, useState, Fragment } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { API_NAMESPACE } from './constants'; +import EndpointActionsCard from './endpoint-actions-card'; +import EndpointActionsModals from './endpoint-actions-modals'; +import { useWizardApiFetch } from '../../../../../hooks/use-wizard-api-fetch'; +import { + Card, + Button, + Notice, + SectionHeader, +} from '../../../../../../components/src'; + +const defaultEndpoint: Endpoint = { + url: '', + label: '', + requests: [], + disabled: false, + disabled_error: false, + id: 0, + system: '', + actions: [], + bearer_token: '', +}; + +function Webhooks() { + const { + setError, + resetError, + errorMessage, + wizardApiFetch, + isFetching: inFlight, + } = useWizardApiFetch( API_NAMESPACE ); + + const [ action, setAction ] = useState< WebhookActions >( null ); + const [ actions, setActions ] = useState< string[] >( [] ); + const [ endpoints, setEndpoints ] = useState< Endpoint[] | null >( null ); + const [ selectedEndpoint, setSelectedEndpoint ] = + useState< Endpoint | null >( null ); + + useEffect( () => { + fetchActions(); + fetchEndpoints(); + }, [] ); + + function fetchActions() { + wizardApiFetch< never[] >( + { + path: '/newspack/v1/data-events/actions', + }, + { + onSuccess: newActions => setActions( newActions ), + } + ); + } + + function fetchEndpoints() { + wizardApiFetch< Endpoint[] >( + { path: '/newspack/v1/webhooks/endpoints' }, + { + onSuccess: newEndpoints => setEndpoints( newEndpoints ), + } + ); + } + + function setActionHandler( + newAction: WebhookActions, + id?: number | string + ) { + resetError(); + setAction( newAction ); + if ( newAction === null ) { + setSelectedEndpoint( null ); + } else if ( newAction === 'new' ) { + resetError(); + setSelectedEndpoint( { ...defaultEndpoint } ); + } else if ( + endpoints && + [ 'edit', 'delete', 'view', 'toggle' ].includes( newAction ) + ) { + setSelectedEndpoint( + endpoints.find( endpoint => endpoint.id === id ) || null + ); + } + } + + return ( + <Card noBorder className="newspack-webhooks"> + <div className="flex justify-between items-end mb4"> + <SectionHeader + title={ __( 'Webhook Endpoints', 'newspack-plugin' ) } + heading={ 3 } + description={ __( + 'Register webhook endpoints to integrate reader activity data to third-party services or private APIs', + 'newspack-plugin' + ) } + noMargin + /> + <Button + variant="primary" + onClick={ () => setActionHandler( 'new' ) } + disabled={ inFlight } + > + { inFlight + ? __( 'Loading…', 'newspack-plugin' ) + : __( 'Add New Endpoint', 'newspack-plugin' ) } + </Button> + </div> + { ! inFlight && + ( endpoints && endpoints.length > 0 ? ( + <Fragment> + { endpoints.map( endpoint => ( + <EndpointActionsCard + key={ endpoint.id } + endpoint={ endpoint } + setAction={ setActionHandler } + /> + ) ) } + </Fragment> + ) : ( + <Notice + noticeText={ __( + 'No endpoints found', + 'newspack-plugin' + ) } + /> + ) ) } + { selectedEndpoint && ( + <EndpointActionsModals + actions={ actions } + setError={ setError } + action={ action } + errorMessage={ errorMessage } + inFlight={ inFlight } + wizardApiFetch={ wizardApiFetch } + endpoint={ selectedEndpoint } + setAction={ setActionHandler } + setEndpoints={ setEndpoints } + /> + ) } + </Card> + ); +} + +export default Webhooks; diff --git a/src/wizards/newspack/views/settings/connections/webhooks/modals/confirmation.tsx b/src/wizards/newspack/views/settings/connections/webhooks/modals/confirmation.tsx new file mode 100644 index 0000000000..7b29c2fbac --- /dev/null +++ b/src/wizards/newspack/views/settings/connections/webhooks/modals/confirmation.tsx @@ -0,0 +1,43 @@ +/** + * Settings Wizard: Connections > Webhooks > Modal > Confirmation + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { Card, Button, Modal } from '../../../../../../../components/src'; + +function Confirmation( { + disabled, + onConfirm, + onClose, + title, + description, +}: { + disabled?: boolean; + onConfirm?: () => void; + onClose: () => void; + title: string; + description: string; +} ) { + return ( + <Modal title={ title } onRequestClose={ onClose }> + <p>{ description }</p> + <Card buttonsCard noBorder className="justify-end"> + <Button variant="secondary" onClick={ onClose } disabled={ disabled }> + { __( 'Cancel', 'newspack-plugin' ) } + </Button> + <Button variant="primary" onClick={ onConfirm } disabled={ disabled }> + { __( 'Confirm', 'newspack-plugin' ) } + </Button> + </Card> + </Modal> + ); +} + +export default Confirmation; diff --git a/src/wizards/newspack/views/settings/connections/webhooks/modals/upsert.tsx b/src/wizards/newspack/views/settings/connections/webhooks/modals/upsert.tsx new file mode 100644 index 0000000000..89ebc60c65 --- /dev/null +++ b/src/wizards/newspack/views/settings/connections/webhooks/modals/upsert.tsx @@ -0,0 +1,297 @@ +/** + * Settings Wizard: Connections > Webhooks > Modals > Upsert + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState, useEffect, useRef, Fragment } from '@wordpress/element'; +import { + CheckboxControl as WpCheckboxControl, + TextControl, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { ENDPOINTS_CACHE_KEY } from '../constants'; +import { WizardApiError } from '../../../../../../errors'; +import { + Card, + Button, + Notice, + Modal, + Grid, +} from '../../../../../../../components/src'; +import { validateEndpoint, validateUrl } from '../utils'; + +/** + * Checkbox control props override. + * + * @param param WP CheckboxControl Component props. + * @return JSX.Element + */ +const CheckboxControl: React.FC< + WpCheckboxControlPropsOverride< typeof WpCheckboxControl > +> = ( { ...props } ) => { + return <WpCheckboxControl { ...props } />; +}; + +const Upsert = ( { + endpoint, + actions, + errorMessage = null, + inFlight = false, + setError, + setAction, + setEndpoints, + wizardApiFetch, +}: Omit< ModalComponentProps, 'action' > ) => { + const [ editing, setEditing ] = useState< Endpoint >( endpoint ); + // Test request + const [ testResponse, setTestResponse ] = useState< { + success?: boolean; + code?: number; + message?: string; + } >( {} ); + + const modalRef = useRef( null as HTMLElement | null ); + + const onSuccess = ( endpointId: string | number, response: Endpoint[] ) => { + setEndpoints( response ); + setAction( null, endpointId ); + }; + + function upsertEndpoint( endpointToUpsert: Endpoint ) { + const errors = validateEndpoint( endpointToUpsert ); + if ( errors.length ) { + setError( errors.join( ' ' ) ); + return; + } + setError( null ); + wizardApiFetch< Endpoint[] >( + { + path: `/newspack/v1/webhooks/endpoints/${ + endpointToUpsert.id || '' + }`, + method: 'POST', + data: endpointToUpsert, + updateCacheKey: ENDPOINTS_CACHE_KEY, + }, + { + onSuccess: endpoints => + onSuccess( endpointToUpsert.id, endpoints ), + } + ); + } + + function testEndpoint( + url: string, + bearer_token: string | undefined + ) { + const urlError = validateUrl( url ); + if ( urlError ) { + setError( urlError ); + return; + } + wizardApiFetch< { success: boolean; code: number; message: string } >( + { + path: '/newspack/v1/webhooks/endpoints/test', + method: 'POST', + data: { url, bearer_token }, + }, + { + onStart() { + setError( null ); + setTestResponse( {} ); + }, + onSuccess( res ) { + if ( ! res.success ) { + setError( + new WizardApiError( + `${ res.code ? `${ res.code }: ` : '' }${ + res.message + }`, + res.code, + 'endpoint_test' + ) + ); + return; + } + setTestResponse( res ); + }, + } + ); + } + + useEffect( () => { + if ( errorMessage ) { + modalRef?.current?.querySelector('.components-modal__content')?.scrollTo( { top: 0, left: 0, behavior: 'smooth' } ); + } + }, [ errorMessage ] ); + + return ( + <Fragment> + <Modal + ref={ modalRef } + title={ __( 'Webhook Endpoint', 'newspack-plugin' ) } + onRequestClose={ () => { + setAction( null, endpoint.id ); + } } + > + { errorMessage && ( + <Notice isError noticeText={ errorMessage } /> + ) } + { true === editing.disabled && ( + <Notice + noticeText={ __( + 'This webhook endpoint is currently disabled.', + 'newspack-plugin' + ) } + /> + ) } + { editing.disabled && editing.disabled_error && ( + <Notice + isError + noticeText={ + __( 'Request Error: ', 'newspack-plugin' ) + + editing.disabled_error + } + /> + ) } + { testResponse.success && ( + <Notice + isSuccess + noticeText={ `${ testResponse.message }: ${ testResponse.code }` } + /> + ) } + <Grid columns={ 1 } gutter={ 16 } className="mt0"> + <TextControl + label={ __( 'URL', 'newspack-plugin' ) } + help={ __( + "The URL to send requests to. It's required for the URL to be under a valid TLS/SSL certificate. You can use the test button below to verify the endpoint response.", + 'newspack-plugin' + ) } + className="code" + value={ editing.url } + onChange={ ( value: string ) => + setEditing( { ...editing, url: value } ) + } + disabled={ inFlight } + /> + <TextControl + label={ __( + 'Authentication token (optional)', + 'newspack-plugin' + ) } + help={ __( + 'If your endpoint requires a token authentication, enter it here. It will be sent as a Bearer token in the Authorization header.', + 'newspack-plugin' + ) } + value={ editing.bearer_token ?? '' } + onChange={ ( value: string ) => + setEditing( { ...editing, bearer_token: value } ) + } + disabled={ inFlight } + /> + <Card buttonsCard noBorder className="justify-end"> + <Button + variant="secondary" + disabled={ inFlight || ! editing.url } + onClick={ () => + testEndpoint( + editing.url, + editing.bearer_token + ) + } + > + { __( 'Send a test request', 'newspack-plugin' ) } + </Button> + </Card> + </Grid> + <hr /> + <TextControl + label={ __( 'Label (optional)', 'newspack-plugin' ) } + help={ __( + 'A label to help you identify this endpoint. It will not be sent to the endpoint.', + 'newspack-plugin' + ) } + value={ editing.label } + onChange={ ( value: string ) => + setEditing( { ...editing, label: value } ) + } + disabled={ inFlight } + /> + <Grid columns={ 1 } gutter={ 16 }> + <h3>{ __( 'Actions', 'newspack-plugin' ) }</h3> + { actions.length > 0 && ( + <Fragment> + <p> + { __( + 'Select which actions should trigger this endpoint:', + 'newspack-plugin' + ) } + </p> + <Grid columns={ 2 } gutter={ 16 }> + { actions.map( ( actionKey, i ) => ( + <CheckboxControl + key={ i } + disabled={ inFlight } + label={ actionKey } + checked={ + ( editing.actions && + editing.actions.includes( + actionKey + ) ) || + false + } + onChange={ () => { + const currentActions = + editing.actions || []; + if ( + currentActions.includes( + actionKey + ) + ) { + currentActions.splice( + currentActions.indexOf( + actionKey + ), + 1 + ); + } else { + currentActions.push( + actionKey + ); + } + setEditing( { + ...editing, + actions: currentActions, + } ); + } } + /> + ) ) } + </Grid> + </Fragment> + ) } + <Card buttonsCard noBorder className="justify-end"> + <Button + isPrimary + onClick={ () => { + if ( null !== editing && 'url' in editing ) { + upsertEndpoint( editing ); + } + } } + disabled={ inFlight || null === editing } + > + { __( 'Save', 'newspack-plugin' ) } + </Button> + </Card> + </Grid> + </Modal> + </Fragment> + ); +}; + +export default Upsert; diff --git a/src/wizards/newspack/views/settings/connections/webhooks/modals/view.tsx b/src/wizards/newspack/views/settings/connections/webhooks/modals/view.tsx new file mode 100644 index 0000000000..6691cb6db0 --- /dev/null +++ b/src/wizards/newspack/views/settings/connections/webhooks/modals/view.tsx @@ -0,0 +1,141 @@ +/** + * Settings Wizard: Connections > Webhooks > Modal > View + */ + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { Fragment } from '@wordpress/element'; +import { Icon } from '@wordpress/components'; + +/** + * External dependencies + */ +import moment from 'moment'; + +/** + * Internal dependencies + */ +import { Notice, Modal } from '../../../../../../../components/src'; +import { + getEndpointLabel, + getRequestStatusIcon, + hasEndpointErrors, +} from '../utils'; + +const View = ( { + endpoint, + setAction, +}: { + endpoint: ModalComponentProps[ 'endpoint' ]; + setAction: ModalComponentProps[ 'setAction' ]; +} ) => { + return ( + <Modal + title={ __( 'Latest Requests', 'newspack-plugin' ) } + onRequestClose={ () => setAction( null, endpoint.id ) } + > + <p> + { sprintf( + // translators: %s is the endpoint title (shortened URL). + __( 'Most recent requests for %s', 'newspack-plugin' ), + getEndpointLabel( endpoint ) + ) } + </p> + { endpoint.requests.length > 0 ? ( + <table + className={ `newspack-webhooks__requests ${ + hasEndpointErrors( endpoint ) ? 'has-error' : '' + }` } + > + <tr> + <th /> + <th colSpan={ 2 }> + { __( 'Action', 'newspack-plugin' ) } + </th> + { hasEndpointErrors( endpoint ) && ( + <th colSpan={ 2 }> + { __( 'Error', 'newspack-plugin' ) } + </th> + ) } + </tr> + { endpoint.requests.map( request => ( + <tr key={ request.id }> + <td + className={ `status status--${ request.status }` } + > + <Icon + icon={ getRequestStatusIcon( + request.status + ) } + /> + </td> + <td className="action-name"> + { request.action_name } + </td> + <td className="scheduled"> + { 'pending' === request.status + ? sprintf( + // translators: %s is a human-readable time difference. + __( + 'sending in %s', + 'newspack-plugin' + ), + moment( + parseInt( request.scheduled ) * + 1000 + ).fromNow( true ) + ) + : sprintf( + // translators: %s is a human-readable time difference. + __( + 'processed %s', + 'newspack-plugin' + ), + moment( + parseInt( request.scheduled ) * + 1000 + ).fromNow() + ) } + </td> + { hasEndpointErrors( endpoint ) && ( + <Fragment> + <td className="error"> + { request.errors && + request.errors.length > 0 + ? request.errors[ + request.errors.length - 1 + ] + : '--' } + </td> + <td> + <span className="error-count"> + { sprintf( + // translators: %s is the number of errors. + __( + 'Attempt #%s', + 'newspack-plugin' + ), + request.errors.length + ) } + </span> + </td> + </Fragment> + ) } + </tr> + ) ) } + </table> + ) : ( + <Notice + noticeText={ __( + "This endpoint hasn't received any requests yet.", + 'newspack-plugin' + ) } + /> + ) } + </Modal> + ); +}; + +export default View; diff --git a/src/wizards/newspack/views/settings/connections/webhooks/utils.tsx b/src/wizards/newspack/views/settings/connections/webhooks/utils.tsx new file mode 100644 index 0000000000..035f35ddfe --- /dev/null +++ b/src/wizards/newspack/views/settings/connections/webhooks/utils.tsx @@ -0,0 +1,123 @@ +/** + * Settings Wizard: Connections > Webhooks > Utils + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Fragment } from '@wordpress/element'; +import { settings, check, close, reusableBlock } from '@wordpress/icons'; + +/** + * Returns a shortened version of the URL for display purposes. + * + * @param url The URL to be shortened. + * @return The shortened URL. + */ +export function getDisplayUrl( url: string ): string { + let displayUrl = url.slice( 8 ); + if ( url.length > 45 ) { + displayUrl = `${ url.slice( 8, 38 ) }...${ url.slice( -10 ) }`; + } + return displayUrl; +} + +/** + * Returns the label or a shortened version of the URL for an endpoint. + * + * @param endpoint The endpoint object. + * @return The label or shortened URL. + */ +export function getEndpointLabel( endpoint: Endpoint ): string { + const { label, url } = endpoint; + return label || getDisplayUrl( url ); +} + +/** + * Returns the title JSX for an endpoint. + * + * @param endpoint The endpoint object. + * @return The JSX for the endpoint title. + */ +export function getEndpointTitle( endpoint: Endpoint ): JSX.Element { + const { label, url } = endpoint; + return ( + <Fragment> + { label && ( + <span className="newspack-webhooks__endpoint__label"> + { label }:{ ' ' } + </span> + ) } + <span className="newspack-webhooks__endpoint__url"> + { getDisplayUrl( url ) } + </span> + </Fragment> + ); +} + +/** + * Returns the icon for the request status. + * + * @param status The status of the request. + * @return The icon component for the request status. + */ +export function getRequestStatusIcon( + status: 'pending' | 'finished' | 'killed' +) { + const icons = { + pending: reusableBlock, + finished: check, + killed: close, + }; + return icons[ status ] || settings; +} + +/** + * Checks if an endpoint has any errors in its requests. + * + * @param endpoint The endpoint object. + * @return True if there are errors, false otherwise. + */ +export function hasEndpointErrors( endpoint: Endpoint ): boolean { + return endpoint.requests.some( request => request.errors.length ); +} + +/** + * Validate endpoint URL. + * + * @param url The URL to validate. + * @return Error message if URL is invalid, false otherwise. + */ +export function validateUrl( url: string ): string | false { + if ( ! url ) { + return __( 'URL is required.', 'newspack-plugin' ); + } + try { + const urlObject = new URL( url ); + if ( urlObject.protocol !== 'https:' ) { + return __( 'HTTPS protocol is required for the endpoint URL.', 'newspack-plugin' ); + } + return false; + } catch ( error ) { + return __( 'Invalid URL format.', 'newspack-plugin' ); + } +} + +/** + * Validate an endpoint. + * + * @param endpoint The endpoint to validate. + * @return An array of error messages. + */ +export function validateEndpoint( endpoint: Endpoint ): string[] { + const errors = []; + const urlError = validateUrl( endpoint.url ); + if ( urlError ) { + errors.push( urlError ); + } + if ( ! endpoint.actions || ! endpoint.actions.length ) { + errors.push( __( 'At least one action is required.', 'newspack-plugin' ) ); + } + return errors; +} diff --git a/src/wizards/newspack/views/settings/constants.ts b/src/wizards/newspack/views/settings/constants.ts new file mode 100644 index 0000000000..bf6eb16186 --- /dev/null +++ b/src/wizards/newspack/views/settings/constants.ts @@ -0,0 +1,75 @@ +/** + * Constants for the settings page. + */ + +/** + * Settings page namespace. + */ +export const PAGE_NAMESPACE = 'newspack-settings'; + +/** + * Theme and Brand. + */ +export const THEME_BRAND_DEFAULTS = { + // Colors. + header_color: 'custom', + theme_colors: 'default', + primary_color_hex: '#003da5', + secondary_color_hex: '#666666', + // Typography. + font_header: '', + font_body: '', + accent_allcaps: true, + custom_font_import_code: undefined, + custom_font_import_code_alternate: undefined, + // Header. + header_center_logo: false, + header_simplified: false, + header_solid_background: false, + header_color_hex: '#003da5', + custom_logo: '', + logo_size: 0, + header_text: false, // No custom_logo set. + header_display_tagline: false, // No custom_logo set. + // Footer. + footer_copyright: '', + footer_color: 'default', + footer_color_hex: '', + newspack_footer_logo: '', + footer_logo_size: 'medium', + // Homepage pattern. + homepage_pattern_index: 0, +}; + +export const DISPLAY_SETTINGS_DEFAULTS = { + // Author Bio. + show_author_bio: true, + show_author_email: false, + author_bio_length: 200, + // Default Featured Image and Post Template. + featured_image_default: 'large', + post_template_default: 'default', + // Featured Image and Post Template for All Posts. + featured_image_all_posts: 'none', + post_template_all_posts: 'none', + // Media Credits. + newspack_image_credits_placeholder_url: undefined, + newspack_image_credits_class_name: 'image-credit', + newspack_image_credits_prefix_label: 'Credit:', + newspack_image_credits_placeholder: null, + newspack_image_credits_auto_populate: false, +}; + +export const DEFAULT_THEME_MODS: ThemeMods = { + ...THEME_BRAND_DEFAULTS, + ...DISPLAY_SETTINGS_DEFAULTS, + + /** + * Misc. + */ + custom_css_post_id: -1, +}; + +export default { + PAGE_NAMESPACE, +}; diff --git a/src/wizards/newspack/views/settings/display-settings/author-bio.tsx b/src/wizards/newspack/views/settings/display-settings/author-bio.tsx new file mode 100644 index 0000000000..127f7c62a3 --- /dev/null +++ b/src/wizards/newspack/views/settings/display-settings/author-bio.tsx @@ -0,0 +1,59 @@ +import { __ } from '@wordpress/i18n'; +import { ToggleControl } from '@wordpress/components'; + +import { Grid, TextControl } from '../../../../../components/src'; + +export default function AuthorBio( { + data, + isFetching, + update, +}: ThemeModComponentProps< DisplaySettings > ) { + return ( + <Grid gutter={ 32 }> + <Grid columns={ 1 } gutter={ 16 }> + <ToggleControl + label={ __( 'Author Bio', 'newspack-plugin' ) } + help={ __( + 'Display an author bio under individual posts.', + 'newspack-plugin' + ) } + disabled={ isFetching } + checked={ data.show_author_bio } + onChange={ show_author_bio => + update( { show_author_bio } ) + } + /> + { data.show_author_bio && ( + <ToggleControl + label={ __( 'Author Email', 'newspack-plugin' ) } + help={ __( + 'Display the author email with bio on individual posts.', + 'newspack-plugin' + ) } + disabled={ isFetching } + checked={ data.show_author_email } + onChange={ show_author_email => + update( { show_author_email } ) + } + /> + ) } + </Grid> + <Grid columns={ 1 } gutter={ 16 }> + { data.show_author_bio && ( + <TextControl + label={ __( 'Length', 'newspack-plugin' ) } + help={ __( + 'Truncates the author bio on single posts to this approximate character length, but without breaking a word. The full bio appears on the author archive page.', + 'newspack-plugin' + ) } + type="number" + value={ data.author_bio_length } + onChange={ ( author_bio_length: number ) => + update( { author_bio_length } ) + } + /> + ) } + </Grid> + </Grid> + ); +} diff --git a/src/wizards/newspack/views/settings/display-settings/featured-image-posts-all.tsx b/src/wizards/newspack/views/settings/display-settings/featured-image-posts-all.tsx new file mode 100644 index 0000000000..4114dfab3b --- /dev/null +++ b/src/wizards/newspack/views/settings/display-settings/featured-image-posts-all.tsx @@ -0,0 +1,156 @@ +/** + * Newspack > Settings > Display Settings > Featured Image Posts All + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Fragment } from '@wordpress/element'; +import { Notice } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { Grid, SelectControl } from '../../../../../components/src'; + +export default function FeaturedImagePostsAll( { + data, + update, + postCount, +}: ThemeModComponentProps< DisplaySettings > & { postCount: string } ) { + return ( + <Fragment> + { Number( postCount ) > 1000 && ( + <Notice + isDismissible={ false } + status="warning" + className="ma0 mb2" + > + { __( + 'You have more than 1000 posts. Applying these settings might take a moment.', + 'newspack-plugin' + ) } + </Notice> + ) } + <Grid gutter={ 32 }> + <div> + <SelectControl + label={ __( + 'Featured image position for all posts', + 'newspack-plugin' + ) } + help={ __( + 'Set a featured image position for all posts.', + 'newspack-plugin' + ) } + value={ data.featured_image_all_posts } + options={ [ + { + label: __( + 'Select to change all posts', + 'newspack-plugin' + ), + value: 'none', + }, + { + label: __( 'Large', 'newspack-plugin' ), + value: 'large', + }, + { + label: __( 'Small', 'newspack-plugin' ), + value: 'small', + }, + { + label: __( + 'Behind article title', + 'newspack-plugin' + ), + value: 'behind', + }, + { + label: __( + 'Beside article title', + 'newspack-plugin' + ), + value: 'beside', + }, + { + label: __( 'Hidden', 'newspack-plugin' ), + value: 'hidden', + }, + ] } + onChange={ ( featured_image_all_posts: string ) => + update( { featured_image_all_posts } ) + } + /> + { data.featured_image_all_posts !== 'none' && ( + <Notice + isDismissible={ false } + status="warning" + className="ma0 mt2" + > + { __( + 'After saving the settings with this option selected, all posts will be updated. This cannot be undone.', + 'newspack-plugin' + ) } + </Notice> + ) } + </div> + + <div> + <SelectControl + label={ __( + 'Template for all posts', + 'newspack-plugin' + ) } + help={ __( + 'Set a template for all posts.', + 'newspack-plugin' + ) } + value={ data.post_template_all_posts } + options={ [ + { + label: __( + 'Select to change all posts', + 'newspack-plugin' + ), + value: 'none', + }, + { + label: __( 'With sidebar', 'newspack-plugin' ), + value: 'default', + }, + { + label: __( 'One Column', 'newspack-plugin' ), + value: 'single-feature.php', + }, + { + label: __( + 'One Column Wide', + 'newspack-plugin' + ), + value: 'single-wide.php', + }, + ] } + onChange={ ( post_template_all_posts: string ) => + update( { post_template_all_posts } ) + } + /> + { data.post_template_all_posts !== 'none' && ( + <Notice + isDismissible={ false } + status="warning" + className="ma0 mt2" + > + { __( + 'After saving the settings with this option selected, all posts will be updated. This cannot be undone.', + 'newspack-plugin' + ) } + </Notice> + ) } + </div> + </Grid> + </Fragment> + ); +} diff --git a/src/wizards/newspack/views/settings/display-settings/featured-image-posts-new.tsx b/src/wizards/newspack/views/settings/display-settings/featured-image-posts-new.tsx new file mode 100644 index 0000000000..6b09042894 --- /dev/null +++ b/src/wizards/newspack/views/settings/display-settings/featured-image-posts-new.tsx @@ -0,0 +1,81 @@ +/** + * Newspack > Settings > Display Settings > Featured Image Posts New + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { Grid, SelectControl } from '../../../../../components/src'; + +export default function FeaturedImagePostsNew( { + data, + update, +}: ThemeModComponentProps< DisplaySettings > ) { + return ( + <Grid gutter={ 32 }> + <SelectControl + label={ __( + 'Default featured image position for new posts', + 'newspack-plugin' + ) } + help={ __( + 'Set a default featured image position for new posts.', + 'newspack-plugin' + ) } + value={ data.featured_image_default } + options={ [ + { label: __( 'Large', 'newspack-plugin' ), value: 'large' }, + { label: __( 'Small', 'newspack-plugin' ), value: 'small' }, + { + label: __( 'Behind article title', 'newspack-plugin' ), + value: 'behind', + }, + { + label: __( 'Beside article title', 'newspack-plugin' ), + value: 'beside', + }, + { + label: __( 'Hidden', 'newspack-plugin' ), + value: 'hidden', + }, + ] } + onChange={ ( featured_image_default: string ) => + update( { featured_image_default } ) + } + /> + <SelectControl + label={ __( + 'Default template for new posts', + 'newspack-plugin' + ) } + help={ __( + 'Set a default template for new posts.', + 'newspack-plugin' + ) } + value={ data.post_template_default } + options={ [ + { + label: __( 'With sidebar', 'newspack-plugin' ), + value: 'default', + }, + { + label: __( 'One Column', 'newspack-plugin' ), + value: 'single-feature.php', + }, + { + label: __( 'One Column Wide', 'newspack-plugin' ), + value: 'single-wide.php', + }, + ] } + onChange={ ( post_template_default: string ) => + update( { post_template_default } ) + } + /> + </Grid> + ); +} diff --git a/src/wizards/newspack/views/settings/display-settings/index.tsx b/src/wizards/newspack/views/settings/display-settings/index.tsx new file mode 100644 index 0000000000..d74811d919 --- /dev/null +++ b/src/wizards/newspack/views/settings/display-settings/index.tsx @@ -0,0 +1,182 @@ +/** + * Newspack > Settings > Emails. + */ + +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; +import { useEffect } from '@wordpress/element'; + +/** + * Internal dependencies. + */ +import { DEFAULT_THEME_MODS } from '../constants'; +import WizardsTab from '../../../../wizards-tab'; +import WizardSection from '../../../../wizards-section'; +import { Button, hooks, Notice, utils } from '../../../../../components/src'; +import { useWizardApiFetch } from '../../../../hooks/use-wizard-api-fetch'; +import Recirculation from './recirculation'; +import AuthorBio from './author-bio'; +import FeaturedImagePostsAll from './featured-image-posts-all'; +import FeaturedImagePostsNew from './featured-image-posts-new'; +import MediaCredits from './media-credits'; + +export default function DisplaySettings() { + const [ data, setData ] = hooks.useObjectState< DisplaySettings >( { + ...DEFAULT_THEME_MODS, + } ); + const [ etc, setEtc ] = hooks.useObjectState< Etc >( { + post_count: '0', + } ); + + const [ recirculationData, setRecirculationData ] = + hooks.useObjectState< Recirculation >( { + relatedPostsMaxAge: 0, + relatedPostsEnabled: false, + relatedPostsError: null, + relatedPostsUpdated: false, + } ); + + const { wizardApiFetch, isFetching, errorMessage } = useWizardApiFetch( + 'newspack-settings/theme-mods' + ); + const { + wizardApiFetch: wizardApiFetchRecirculation, + isFetching: isFetchingRecirculation, + } = useWizardApiFetch( 'newspack-settings/display-settings/recirculation' ); + + useEffect( () => { + wizardApiFetch< ThemeData >( + { + path: '/newspack/v1/wizard/newspack-setup-wizard/theme', + }, + { + onSuccess( { theme_mods, etc: newEtc } ) { + setData( theme_mods ); + setEtc( newEtc ); + }, + } + ); + wizardApiFetchRecirculation< Recirculation >( + { + path: '/newspack/v1/wizard/newspack-settings/related-content', + }, + { + onSuccess: setRecirculationData, + } + ); + }, [] ); + + function save() { + wizardApiFetchRecirculation( + { + path: '/newspack/v1/wizard/newspack-settings/related-posts-max-age', + method: 'POST', + updateCacheKey: { + '/newspack/v1/wizard/newspack-settings/related-content': + 'GET', + }, + data: recirculationData, + }, + { + onSuccess: setRecirculationData, + } + ); + if ( + data.featured_image_all_posts !== 'none' || + data.post_template_all_posts !== 'none' + ) { + if ( + ! utils.confirmAction( + __( + 'Saving will overwrite existing posts, this cannot be undone. Are you sure you want to proceed?', + 'newspack-plugin' + ) + ) + ) { + return; + } + } + wizardApiFetch( + { + path: '/newspack/v1/wizard/newspack-setup-wizard/theme', + method: 'POST', + updateCacheMethods: [ 'GET' ], + data: { theme_mods: data }, + }, + { + onSuccess: savedData => { + setData( { + ...savedData, + // Strange UX behavior: if the user saves the settings with the "all posts" options selected, the settings are reset to "none". + featured_image_all_posts: 'none', + post_template_all_posts: 'none', + } ); + }, + } + ); + } + + return ( + <WizardsTab + title={ __( 'Display Settings', 'newspack-plugin' ) } + isFetching={ isFetching || isFetchingRecirculation } + > + <WizardSection title={ __( 'Recirculation', 'newspack-plugin' ) }> + <Recirculation + isFetching={ isFetchingRecirculation } + update={ setRecirculationData } + data={ recirculationData } + /> + </WizardSection> + <WizardSection title={ __( 'Author Bio', 'newspack-plugin' ) }> + <AuthorBio + update={ setData } + data={ data } + isFetching={ isFetching } + /> + </WizardSection> + <WizardSection + title={ __( + 'Default Featured Image Position And Post Template', + 'newspack-plugin' + ) } + description={ __( + 'Modify how the featured image and post template settings are applied to new posts.', + 'newspack-plugin' + ) } + > + <FeaturedImagePostsNew data={ data } update={ setData } /> + </WizardSection> + <WizardSection + title={ __( + 'Featured Image Position And Post Template For All Posts', + 'newspack-plugin' + ) } + description={ __( + 'Modify how the featured image and post template settings are applied to existing posts. Warning: saving these options will override all posts.', + 'newspack-plugin' + ) } + > + <FeaturedImagePostsAll + data={ data } + postCount={ etc.post_count } + update={ setData } + /> + </WizardSection> + <WizardSection title={ __( 'Media Credits', 'newspack-plugin' ) }> + <MediaCredits data={ data } update={ setData } /> + </WizardSection> + { errorMessage && <Notice /> } + <div className="newspack-buttons-card"> + <Button variant="tertiary" href="/wp-admin/customize.php"> + { __( 'Advanced Settings', 'newspack-plugin' ) } + </Button> + <Button variant="primary" onClick={ save }> + { __( 'Save', 'newspack-plugin' ) } + </Button> + </div> + </WizardsTab> + ); +} diff --git a/src/wizards/newspack/views/settings/display-settings/media-credits.tsx b/src/wizards/newspack/views/settings/display-settings/media-credits.tsx new file mode 100644 index 0000000000..7eaa6a46bc --- /dev/null +++ b/src/wizards/newspack/views/settings/display-settings/media-credits.tsx @@ -0,0 +1,110 @@ +/** + * Newspack > Settings > Display Settings > Media Credits + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { ToggleControl } from '@wordpress/components'; +import { useEffect, useState, Fragment } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { Grid, ImageUpload, TextControl } from '../../../../../components/src'; + +export default function MediaCredits( { + data, + update, +}: ThemeModComponentProps< DisplaySettings > ) { + const [ imageThumbnail, setImageThumbnail ] = useState< null | string >( + null + ); + useEffect( () => { + if ( data.newspack_image_credits_placeholder_url ) { + setImageThumbnail( data.newspack_image_credits_placeholder_url ); + } + }, [ data.newspack_image_credits_placeholder_url ] ); + return ( + <Fragment> + <Grid gutter={ 32 }> + <Grid columns={ 1 } gutter={ 16 }> + <TextControl + label={ __( 'Credit Class Name', 'newspack-plugin' ) } + help={ __( + 'A CSS class name to be applied to all image credit elements. Leave blank to display no class name.', + 'newspack-plugin' + ) } + value={ data.newspack_image_credits_class_name } + onChange={ ( + newspack_image_credits_class_name: string + ) => + update( { + newspack_image_credits_class_name, + } ) + } + /> + <TextControl + label={ __( 'Credit Label', 'newspack-plugin' ) } + help={ __( + 'A label to prefix all media credits. Leave blank to display no prefix.', + 'newspack-plugin' + ) } + value={ data.newspack_image_credits_prefix_label } + onChange={ ( + newspack_image_credits_prefix_label: string + ) => + update( { + newspack_image_credits_prefix_label, + } ) + } + /> + </Grid> + <Grid columns={ 1 } gutter={ 16 }> + <ImageUpload + image={ + imageThumbnail && + data.newspack_image_credits_placeholder + ? { + url: imageThumbnail, + } + : null + } + label={ __( 'Placeholder Image', 'newspack-plugin' ) } + buttonLabel={ __( 'Select', 'newspack-plugin' ) } + onChange={ ( image: null | PlaceholderImage ) => { + setImageThumbnail( image?.url || null ); + update( { + newspack_image_credits_placeholder: + image?.id || null, + newspack_image_credits_placeholder_url: + image?.url, + } ); + } } + help={ __( + 'A placeholder image to be displayed in place of images without credits. If none is chosen, the image will be displayed normally whether or not it has a credit.', + 'newspack-plugin' + ) } + /> + <ToggleControl + label={ __( + 'Auto-populate image credits', + 'newspack-plugin' + ) } + help={ __( + 'Automatically populate image credits from EXIF or IPTC metadata when uploading new images.', + 'newspack-plugin' + ) } + checked={ data.newspack_image_credits_auto_populate } + onChange={ newspack_image_credits_auto_populate => + update( { + newspack_image_credits_auto_populate, + } ) + } + /> + </Grid> + </Grid> + </Fragment> + ); +} diff --git a/src/wizards/newspack/views/settings/display-settings/recirculation.tsx b/src/wizards/newspack/views/settings/display-settings/recirculation.tsx new file mode 100644 index 0000000000..3db46e280f --- /dev/null +++ b/src/wizards/newspack/views/settings/display-settings/recirculation.tsx @@ -0,0 +1,77 @@ +import { __ } from '@wordpress/i18n'; +import { Fragment } from '@wordpress/element'; + +import WizardsActionCard from '../../../../wizards-action-card'; +import { + Button, + Card, + Grid, + TextControl, + Waiting, +} from '../../../../../components/src'; + +export default function Recirculation( { + data, + update, + isFetching, +}: ThemeModComponentProps< Recirculation > ) { + return ( + <> + <WizardsActionCard + title={ __( 'Related Posts', 'newspack-plugin' ) } + badge="Jetpack" + description={ () => ( + <Fragment> + { isFetching + ? __( 'Loading…', 'newspack-plugin' ) + : __( + 'Automatically add related content at the bottom of each post.', + 'newspack-plugin' + ) } + </Fragment> + ) } + editLink="admin.php?page=jetpack#/traffic" + actionText={ + <Fragment> + { isFetching ? ( + <Waiting /> + ) : ( + <Button + variant="link" + href="admin.php?page=jetpack#/traffic" + > + { __( 'Configure', 'newspack-plugin' ) } + </Button> + ) } + </Fragment> + } + /> + + { data.relatedPostsEnabled && ( + <Grid> + <Card noBorder> + <TextControl + help={ __( + 'If set, posts will be shown as related content only if published within the past number of months. If 0, any published post can be shown, regardless of publish date.', + 'newspack-plugin' + ) } + label={ __( + 'Maximum age of related content, in months', + 'newspack-plugin' + ) } + onChange={ ( relatedPostsMaxAge: number ) => + update( { relatedPostsMaxAge } ) + } + placeholder={ __( + 'Maximum age of related content, in months', + 'newspack-plugin' + ) } + type="number" + value={ data.relatedPostsMaxAge || 0 } + /> + </Card> + </Grid> + ) } + </> + ); +} diff --git a/src/wizards/newspack/views/settings/display-settings/types.d.ts b/src/wizards/newspack/views/settings/display-settings/types.d.ts new file mode 100644 index 0000000000..528401234b --- /dev/null +++ b/src/wizards/newspack/views/settings/display-settings/types.d.ts @@ -0,0 +1,7 @@ +/** + * Media Credit placeholder image object. + */ +type PlaceholderImage = { + url: string; + id: number; +}; diff --git a/src/wizards/newspack/views/settings/emails/emails.tsx b/src/wizards/newspack/views/settings/emails/emails.tsx new file mode 100644 index 0000000000..a3192f6574 --- /dev/null +++ b/src/wizards/newspack/views/settings/emails/emails.tsx @@ -0,0 +1,184 @@ +/** + * Newspack > Settings > Emails > Emails section + */ + +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; +import { useState, Fragment } from '@wordpress/element'; + +/** + * Internal dependencies. + */ +import { Notice, utils } from '../../../../../components/src'; +import { useWizardApiFetch } from '../../../../hooks/use-wizard-api-fetch'; +import WizardsActionCard from '../../../../wizards-action-card'; +import WizardsPluginCard from '../../../../wizards-plugin-card'; + +const Emails = () => { + const emailSections = window.newspackSettings.emails.sections; + const postType = emailSections.emails.postType; + + const [ pluginsReady, setPluginsReady ] = useState( + emailSections.emails.dependencies.newspackNewsletters + ); + + const { wizardApiFetch, isFetching, errorMessage, resetError } = + useWizardApiFetch( 'newspack-settings/emails' ); + + const [ emails, setEmails ] = useState( + Object.values( emailSections.emails.all ) + ); + + const updateStatus = ( postId: number, status: string ) => { + wizardApiFetch( + { + path: `/wp/v2/${ postType }/${ postId }`, + method: 'POST', + data: { status }, + }, + { + onStart() { + resetError(); + }, + onSuccess() { + setEmails( + emails.map( email => { + if ( email.post_id === postId ) { + return { ...email, status }; + } + return email; + } ) + ); + }, + } + ); + }; + + const resetEmail = ( postId: number ) => { + wizardApiFetch( + { + path: `/newspack/v1/wizard/newspack-audience-donations/emails/${ postId }`, + method: 'DELETE', + }, + { + onSuccess( result ) { + window.newspackSettings.emails.sections.emails.all = result; + setEmails( Object.values( result ) ); + }, + } + ); + }; + + if ( false === pluginsReady ) { + return ( + <Fragment> + <Notice isError> + { __( + 'Newspack uses Newspack Newsletters to handle editing email-type content. Please activate this plugin to proceed.', + 'newspack-plugin' + ) } + <br /> + { __( + 'Until this feature is configured, default receipts will be used.', + 'newspack-plugin' + ) } + </Notice> + <WizardsPluginCard + slug="newspack-newsletters" + title={ __( 'Newspack Newsletters', 'newspack-plugin' ) } + description={ __( + 'Newspack Newsletters is the plugin that powers Newspack email receipts.', + 'newspack-plugin' + ) } + onStatusChange={ ( + statuses: Record< string, boolean > + ) => { + if ( ! statuses.isLoading ) { + setPluginsReady( statuses.isSetup ); + } + } } + /> + </Fragment> + ); + } + + return ( + <Fragment> + { emails.map( email => { + const isActive = email.status === 'publish'; + let notification = __( + 'This email is not active.', + 'newspack-plugin' + ); + if ( email.type === 'receipt' ) { + notification = __( + 'This email is not active. The default receipt will be used.', + 'newspack-plugin' + ); + } + + if ( email.type === 'welcome' ) { + notification = __( + 'This email is not active. The receipt template will be used if active.', + 'newspack-plugin' + ); + } + return ( + <WizardsActionCard + key={ email.post_id } + disabled={ isFetching } + title={ email.label } + titleLink={ email.edit_link } + href={ email.edit_link } + description={ email.description } + actionText={ __( 'Edit', 'newspack-plugin' ) } + secondaryActionText={ __( 'Reset', 'newspack-plugin' ) } + onSecondaryActionClick={ () => { + if ( + utils.confirmAction( + __( + 'Are you sure you want to reset the contents of this email?', + 'newspack-plugin' + ) + ) + ) { + resetEmail( email.post_id ); + } + } } + secondaryDestructive={ true } + toggleChecked={ isActive } + toggleOnChange={ value => + updateStatus( + email.post_id, + value ? 'publish' : 'draft' + ) + } + { ...( isActive + ? {} + : { + notification, + notificationLevel: 'info', + } ) } + > + { errorMessage && ( + <Notice + noticeText={ + errorMessage || + __( + 'Something went wrong.', + 'newspack-plugin' + ) + } + isError + /> + ) } + </WizardsActionCard> + ); + } ) } + </Fragment> + ); +}; + +export default Emails; diff --git a/src/wizards/newspack/views/settings/emails/index.tsx b/src/wizards/newspack/views/settings/emails/index.tsx new file mode 100644 index 0000000000..f6def61b24 --- /dev/null +++ b/src/wizards/newspack/views/settings/emails/index.tsx @@ -0,0 +1,27 @@ +/** + * Newspack > Settings > Emails + */ + +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies. + */ +import WizardsTab from '../../../../wizards-tab'; +import { default as EmailsSection } from './emails'; +import WizardSection from '../../../../wizards-section'; + +function Emails() { + return ( + <WizardsTab title={ __( 'Emails', 'newspack-plugin' ) }> + <WizardSection> + <EmailsSection /> + </WizardSection> + </WizardsTab> + ); +} + +export default Emails; diff --git a/src/wizards/newspack/views/settings/index.tsx b/src/wizards/newspack/views/settings/index.tsx new file mode 100644 index 0000000000..3b901e823b --- /dev/null +++ b/src/wizards/newspack/views/settings/index.tsx @@ -0,0 +1,40 @@ +/** + * Newspack - Dashboard + * + * WP Admin Newspack Dashboard page. + */ + +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; +import { Fragment } from '@wordpress/element'; + +/** + * Internal dependencies. + */ +import './style.scss'; +import sections from './sections'; +import Wizard from '../../../../components/src/wizard'; +import { GlobalNotices, Notice } from '../../../../components/src/'; + +const { + newspack_aux_data: { is_debug_mode: isDebugMode = false }, +} = window; + +function Settings() { + return ( + <Fragment> + { isDebugMode && <Notice debugMode /> } + <GlobalNotices /> + <Wizard + className="newspack-admin__tabs" + headerText={ __( 'Newspack / Settings', 'newspack' ) } + sections={ sections } + isInitialFetchTriggered={ false } + /> + </Fragment> + ); +} + +export default Settings; diff --git a/src/wizards/newspack/views/settings/sections.tsx b/src/wizards/newspack/views/settings/sections.tsx new file mode 100644 index 0000000000..104a07d44f --- /dev/null +++ b/src/wizards/newspack/views/settings/sections.tsx @@ -0,0 +1,60 @@ +/** + * Newspack - Dashboard, Sections + * + * Component for outputting sections with grid and cards + */ +import { __ } from '@wordpress/i18n'; +import { lazy } from '@wordpress/element'; + +const settingsTabs = window.newspackSettings; + +import Seo from './seo'; +import Social from './social'; +import Emails from './emails'; +import Connections from './connections'; +import Syndication from './syndication'; +import DisplaySettings from './display-settings'; +import ThemeAndBrand from './theme-and-brand'; + +type SectionKeys = keyof typeof settingsTabs; + +const sectionComponents: Partial< + Record< + SectionKeys | 'default', + ( props: { isPartOfSetup?: boolean } ) => React.ReactNode + > +> = { + connections: Connections, + social: Social, + emails: Emails, + syndication: Syndication, + seo: Seo, + 'theme-and-brand': ThemeAndBrand, + 'display-settings': DisplaySettings, + default: () => <h2>🚫 { __( 'Not found' ) }</h2>, +}; + +/** + * Load additional brands section if `newspack-multibranded-site` plugin is active. + */ +if ( 'additional-brands' in settingsTabs ) { + sectionComponents[ 'additional-brands' ] = lazy( + () => + import( + /* webpackChunkName: "newspack-wizards" */ './additional-brands' + ) + ); +} + +const settingsSectionKeys = Object.keys( settingsTabs ) as SectionKeys[]; + +export default settingsSectionKeys.reduce( ( acc: any[], sectionPath ) => { + acc.push( { + label: settingsTabs[ sectionPath ].label, + exact: '/' === ( settingsTabs[ sectionPath ].path ?? '' ), + path: settingsTabs[ sectionPath ].path ?? `/${ sectionPath }`, + activeTabPaths: settingsTabs[ sectionPath ].activeTabPaths ?? undefined, + render: sectionComponents[ sectionPath ] ?? sectionComponents.default, + } ); + return acc; +}, [] ); diff --git a/src/wizards/newspack/views/settings/seo/accounts.tsx b/src/wizards/newspack/views/settings/seo/accounts.tsx new file mode 100644 index 0000000000..aee3f5a286 --- /dev/null +++ b/src/wizards/newspack/views/settings/seo/accounts.tsx @@ -0,0 +1,37 @@ +/** + * Components for managing SEO accounts. + */ + +/** + * WordPress dependencies. + */ +import { ACCOUNTS } from './constants'; +import { Grid, TextControl } from '../../../../../components/src'; + +/** + * Internal dependencies. + */ + +function Accounts( { + setData, + data, +}: { + setData: ( v: SeoData[ 'urls' ] ) => void; + data: SeoData[ 'urls' ] & { [ k: string ]: string }; +} ) { + return ( + <Grid columns={ 3 } rowGap={ 16 }> + { ACCOUNTS.map( ( [ key, label, placeholder ] ) => ( + <TextControl + key={ key } + label={ label } + onChange={ ( value: string ) => setData( { ...data, [ key ]: value } ) } + value={ data[ key ] } + placeholder={ placeholder } + /> + ) ) } + </Grid> + ); +} + +export default Accounts; diff --git a/src/wizards/newspack/views/settings/seo/constants.ts b/src/wizards/newspack/views/settings/seo/constants.ts new file mode 100644 index 0000000000..306dcceadb --- /dev/null +++ b/src/wizards/newspack/views/settings/seo/constants.ts @@ -0,0 +1,60 @@ +import { __ } from '@wordpress/i18n'; + +/** + * Array of tupils where each tupil contains: + * 1. Field key. + * 2. Field label. + * 3. Field placeholder. + * 4. (Optional) Validation callback name. + * 5. (Optional) Field error message. + */ +export const ACCOUNTS = [ + [ + 'twitter', + __( 'X (formerly Twitter) Handle', 'newspack-plugin' ), + __( 'username', 'newspack-plugin' ), + ( inputValue: string ) => { + if ( inputValue.length === 0 ) { + return ''; + } + if ( inputValue.length > 15 ) { + return __( + 'X handle cannot exceed 15 characters!', + 'newspack-plugin' + ); + } + if ( ! /^[a-zA-Z0-9_]+$/.test( inputValue ) ) { + return __( + 'X handle may only contain letters, numbers, and underscores!', + 'newspack-plugin' + ); + } + return ''; + }, + ], + [ + 'facebook', + __( 'Facebook', 'newspack-plugin' ), + 'https://facebook.com/page', + ], + [ + 'instagram', + __( 'Instagram', 'newspack-plugin' ), + 'https://instagram.com/user', + ], + [ + 'youtube', + __( 'YouTube', 'newspack-plugin' ), + 'https://youtube.com/c/channel', + ], + [ + 'linkedin', + __( 'LinkedIn', 'newspack-plugin' ), + 'https://linkedin.com/user', + ], + [ + 'pinterest', + __( 'Pinterest', 'newspack-plugin' ), + 'https://pinterest.com/user', + ], +] as const; diff --git a/src/wizards/newspack/views/settings/seo/index.tsx b/src/wizards/newspack/views/settings/seo/index.tsx new file mode 100644 index 0000000000..eb68ddd9c6 --- /dev/null +++ b/src/wizards/newspack/views/settings/seo/index.tsx @@ -0,0 +1,166 @@ +/** + * Newspack > Settings > Emails + */ + +/** + * WordPress dependencies. + */ +import { __, sprintf } from '@wordpress/i18n'; +import { useState, useEffect } from '@wordpress/element'; + +/** + * Internal dependencies. + */ +import Accounts from './accounts'; +import { ACCOUNTS } from './constants'; +import WizardsTab from '../../../../wizards-tab'; +import VerificationCodes from './verification-codes'; +import WizardSection from '../../../../wizards-section'; +import { Button, Notice } from '../../../../../components/src'; +import WizardsActionCard from '../../../../wizards-action-card'; +import useFieldsValidation from '../../../../hooks/use-fields-validation'; +import { useWizardApiFetch } from '../../../../hooks/use-wizard-api-fetch'; + +const PATH = '/newspack/v1/wizard/newspack-settings/seo'; + +function Seo() { + const { wizardApiFetch, isFetching } = useWizardApiFetch( 'newspack-settings/seo' ); + + const [ data, setData ] = useState< SeoData >( { + under_construction: false, + urls: { + facebook: '', + twitter: '', + instagram: '', + youtube: '', + linkedin: '', + pinterest: '', + }, + verification: { + bing: '', + google: '', + }, + } ); + + const codesValidation = useFieldsValidation< SeoData[ 'verification' ] >( + [ + [ + 'google', + 'isId', + { message: __( 'Invalid Google verification code!', 'newspack-plugin' ) }, + ], + [ + 'bing', + /** JS version of [WPSEO PHP regex](https://github.com/Yoast/wordpress-seo/blob/trunk/inc/options/class-wpseo-option.php#L313) */ + v => + /^[A-Fa-f0-9_-]*$/.test( v ) + ? '' + : __( 'Invalid Bing verification code!', 'newspack-plugin' ), + ], + ], + data.verification + ); + + const accountsValidation = useFieldsValidation< SeoData[ 'urls' ] >( + ACCOUNTS.map( + ( [ key, label, placeholder, validation ] ) => [ + key, + validation ?? 'isUrl', + validation + ? {} + : { + message: sprintf( + /* translators: %1$s: label, %2$s: placeholder */ + __( 'Invalid URL for "%1$s", correct format is "%2$s"', 'newspack-plugin' ), + label, + placeholder + ), + }, + ], + [] + ), + data.urls + ); + + useEffect( get, [] ); + + function get() { + wizardApiFetch( + { + path: PATH, + }, + { + onSuccess: res => setData( res ), + } + ); + } + + function post() { + const isVerificationCodesValid = codesValidation.isInputsValid(); + const isAccountsValid = accountsValidation.isInputsValid(); + if ( ! isVerificationCodesValid || ! isAccountsValid ) { + return; + } + wizardApiFetch( + { + path: PATH, + method: 'POST', + updateCacheMethods: [ 'GET' ], + data, + }, + { + onSuccess: res => setData( res ), + } + ); + } + return ( + <WizardsTab + title={ __( 'SEO', 'newspack-plugin' ) } + className={ isFetching ? 'is-fetching' : '' } + > + <WizardSection + title={ __( 'Webmaster Tools', 'newspack-plugin' ) } + description={ __( 'Add verification meta tags to your site', 'newspack-plugin' ) } + > + { codesValidation.errorMessage && ( + <Notice isError noticeText={ codesValidation.errorMessage } /> + ) } + <VerificationCodes + setData={ verification => setData( { ...data, verification } ) } + data={ data.verification } + /> + </WizardSection> + <WizardSection + title={ __( 'Social Accounts', 'newspack-plugin' ) } + description={ __( + 'Let search engines know which social profiles are associated to your site', + 'newspack-plugin' + ) } + > + { accountsValidation.errorMessage && ( + <Notice isError noticeText={ accountsValidation.errorMessage } /> + ) } + <Accounts setData={ urls => setData( { ...data, urls } ) } data={ data.urls } /> + </WizardSection> + <WizardSection> + <WizardsActionCard + isMedium + disabled={ isFetching } + toggleChecked={ data.under_construction } + title={ __( 'Under construction', 'newspack' ) } + toggleOnChange={ under_construction => setData( { ...data, under_construction } ) } + description={ __( 'Discourage search engines from indexing this site.', 'newspack' ) } + /> + </WizardSection> + <div className="newspack-buttons-card"> + <Button isPrimary onClick={ post } disabled={ isFetching }> + { isFetching + ? __( 'Loading…', 'newspack-plugin' ) + : __( 'Save Settings', 'newspack-plugin' ) } + </Button> + </div> + </WizardsTab> + ); +} + +export default Seo; diff --git a/src/wizards/newspack/views/settings/seo/types.d.ts b/src/wizards/newspack/views/settings/seo/types.d.ts new file mode 100644 index 0000000000..d583033c01 --- /dev/null +++ b/src/wizards/newspack/views/settings/seo/types.d.ts @@ -0,0 +1,18 @@ +/** + * SEO data type. + */ +type SeoData = { + under_construction: boolean; + urls: { + facebook: string; + twitter: string; + instagram: string; + youtube: string; + linkedin: string; + pinterest: string; + }; + verification: { + bing: string; + google: string; + }; +}; diff --git a/src/wizards/newspack/views/settings/seo/verification-codes.tsx b/src/wizards/newspack/views/settings/seo/verification-codes.tsx new file mode 100644 index 0000000000..a3cf5bb5e8 --- /dev/null +++ b/src/wizards/newspack/views/settings/seo/verification-codes.tsx @@ -0,0 +1,66 @@ +/** + * Components for managing SEO verification codes. + */ + +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; +import { Fragment } from '@wordpress/element'; +import { ExternalLink } from '@wordpress/components'; + +/** + * Internal dependencies. + */ +import { Grid, TextControl } from '../../../../../components/src'; + +function VerificationCodes( { + data, + setData, +}: { + data: SeoData[ 'verification' ]; + setData: ( v: SeoData[ 'verification' ] ) => void; +} ) { + return ( + <Grid> + <TextControl + label="Google" + onChange={ ( google: string ) => + setData( { ...data, google } ) + } + value={ data.google } + help={ + <Fragment> + { __( 'Get your verification code in', 'newspack' ) + + ' ' } + <ExternalLink + href={ `https://search.google.com/search-console/ownership?resource_id=${ encodeURIComponent( + window.location.origin + ) }` } + > + { __( 'Google Search Console', 'newspack' ) } + </ExternalLink> + </Fragment> + } + /> + <TextControl + label="Bing" + onChange={ ( bing: string ) => setData( { ...data, bing } ) } + value={ data.bing } + help={ + <Fragment> + { `${ __( + 'Get your verification code in', + 'newspack' + ) } ` } + <ExternalLink href="https://www.bing.com/toolbox/webmaster/#/Dashboard/"> + { __( 'Bing Webmaster Tools', 'newspack' ) } + </ExternalLink> + </Fragment> + } + /> + </Grid> + ); +} + +export default VerificationCodes; diff --git a/src/wizards/newspack/views/settings/social/index.tsx b/src/wizards/newspack/views/settings/social/index.tsx new file mode 100644 index 0000000000..ddc3b00076 --- /dev/null +++ b/src/wizards/newspack/views/settings/social/index.tsx @@ -0,0 +1,36 @@ +/** + * Newspack > Settings > Social + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import XPixel from './x-pixel'; +import MetaPixel from './meta-pixel'; + +/** + * Internal dependencies + */ +import Section from '../../../../wizards-section'; +import Publicize from './publicize'; + +function Social() { + return ( + <div className="newspack-wizard__sections"> + <h1>{ __( 'Social', 'newspack-plugin' ) }</h1> + + <Section> + <Publicize /> + <MetaPixel /> + <XPixel /> + </Section> + </div> + ); +} + +export default Social; diff --git a/src/wizards/newspack/views/settings/social/meta-pixel.tsx b/src/wizards/newspack/views/settings/social/meta-pixel.tsx new file mode 100644 index 0000000000..605bfea74f --- /dev/null +++ b/src/wizards/newspack/views/settings/social/meta-pixel.tsx @@ -0,0 +1,75 @@ +/** + * Meta Pixel component. Used in Settings > Social > Meta Pixel. + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { createInterpolateElement } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { PAGE_NAMESPACE } from '../constants'; +import { TextControl } from '../../../../../components/src'; +import WizardsToggleHeaderCard from '../../../../wizards-toggle-header-card'; + +const MetaPixel = () => { + return ( + <WizardsToggleHeaderCard< PixelData > + title={ __( 'Meta Pixel', 'newspack-plugin' ) } + namespace={ `${ PAGE_NAMESPACE }/social/pixels/meta` } + description={ __( + 'Add the Meta pixel (formerly known as Facebook pixel) to your site.', + 'newspack-plugin' + ) } + path="/newspack/v1/wizard/newspack-settings/social/meta_pixel" + defaultValue={ { + active: false, + pixel_id: '', + } } + fieldValidationMap={ [ + [ + 'pixel_id', + { + callback: 'isIntegerId', + }, + ], + ] } + renderProp={ ( { + settingsUpdates, + setSettingsUpdates, + isFetching, + } ) => ( + <TextControl + value={ settingsUpdates?.pixel_id ?? '' } + label={ __( 'Pixel ID', 'newspack-plugin' ) } + onChange={ ( pixel_id: string ) => + setSettingsUpdates( { ...settingsUpdates, pixel_id } ) + } + help={ createInterpolateElement( + __( + 'The Meta Pixel ID. You only need to add the number, not the full code. Example: 123456789123456789. You can get this information <linkToFb>here</linkToFb>.', + 'newspack-plugin' + ), + { + linkToFb: ( + /* eslint-disable-next-line jsx-a11y/anchor-has-content */ + <a + href="https://www.facebook.com/ads/manager/pixel/facebook_pixel" + target="_blank" + rel="noopener noreferrer" + /> + ), + } + ) } + disabled={ isFetching } + autoComplete="one-time-code" + /> + ) } + /> + ); +}; + +export default MetaPixel; diff --git a/src/wizards/newspack/views/settings/social/publicize.tsx b/src/wizards/newspack/views/settings/social/publicize.tsx new file mode 100644 index 0000000000..813f46c070 --- /dev/null +++ b/src/wizards/newspack/views/settings/social/publicize.tsx @@ -0,0 +1,30 @@ +/** + * Newspack > Settings > Social + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import WizardsPluginCard from '../../../../wizards-plugin-card'; + +function Publicize() { + return ( + <WizardsPluginCard + title={ __( 'Publicize', 'newspack-plugin' ) } + badge={ __( 'Jetpack', 'newspack-plugin' ) } + slug="jetpack" + description={ __( + "Publicize makes it easy to share your site's posts on several social media networks automatically when you publish a new post.", + 'newspack-plugin' + ) } + actionText={ { + complete: __( 'Configure', 'newspack-plugin' ), + activate: __( 'Activate Jetpack', 'newspack-plugin' ), + } } + editLink="admin.php?page=jetpack#/sharing" + /> + ); +} + +export default Publicize; diff --git a/src/wizards/newspack/views/settings/social/x-pixel.tsx b/src/wizards/newspack/views/settings/social/x-pixel.tsx new file mode 100644 index 0000000000..1ecf59ca31 --- /dev/null +++ b/src/wizards/newspack/views/settings/social/x-pixel.tsx @@ -0,0 +1,75 @@ +/** + * X Pixel component. Used in Settings > Social > X Pixel. + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { createInterpolateElement } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { PAGE_NAMESPACE } from '../constants'; +import { TextControl } from '../../../../../components/src'; +import WizardsToggleHeaderCard from '../../../../wizards-toggle-header-card'; + +const XPixel = () => { + return ( + <WizardsToggleHeaderCard< PixelData > + title={ __( 'X Pixel', 'newspack-plugin' ) } + namespace={ `${ PAGE_NAMESPACE }/social/pixel/x` } + description={ __( + 'Add the X pixel (formerly known as Twitter pixel) to your site.', + 'newspack-plugin' + ) } + path="/newspack/v1/wizard/newspack-settings/social/x_pixel" + defaultValue={ { + active: false, + pixel_id: '', + } } + fieldValidationMap={ [ + [ + 'pixel_id', + { + callback: 'isId', + }, + ], + ] } + renderProp={ ( { + settingsUpdates, + setSettingsUpdates, + isFetching, + } ) => ( + <TextControl + value={ settingsUpdates?.pixel_id ?? '' } + label={ __( 'Pixel ID', 'newspack-plugin' ) } + onChange={ ( pixel_id: string ) => + setSettingsUpdates( { ...settingsUpdates, pixel_id } ) + } + help={ createInterpolateElement( + __( + 'The X Pixel ID. You only need to add the number, not the full code. Example: 123456789123456789. You can get this information <linkToFb>here</linkToFb>.', + 'newspack-plugin' + ), + { + linkToFb: ( + /* eslint-disable-next-line jsx-a11y/anchor-has-content */ + <a + href="https://www.facebook.com/ads/manager/pixel/facebook_pixel" + target="_blank" + rel="noopener noreferrer" + /> + ), + } + ) } + disabled={ isFetching } + autoComplete="one-time-code" + /> + ) } + /> + ); +}; + +export default XPixel; diff --git a/src/wizards/newspack/views/settings/style.scss b/src/wizards/newspack/views/settings/style.scss new file mode 100644 index 0000000000..3f56283b6c --- /dev/null +++ b/src/wizards/newspack/views/settings/style.scss @@ -0,0 +1,3 @@ +.newspack-settings { + box-sizing: border-box; +} diff --git a/src/wizards/newspack/views/settings/syndication/index.tsx b/src/wizards/newspack/views/settings/syndication/index.tsx new file mode 100644 index 0000000000..2ecfc8aba8 --- /dev/null +++ b/src/wizards/newspack/views/settings/syndication/index.tsx @@ -0,0 +1,29 @@ +/** + * Settings Syndication: RSS, Apple News, and Distributor. + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import Rss from './rss'; +import Plugins from './plugins'; +import WizardsTab from '../../../../wizards-tab'; +import WizardSection from '../../../../wizards-section'; + +function Syndication() { + return ( + <WizardsTab title={ __( 'Syndication', 'newspack-plugin' ) }> + <WizardSection> + <Rss /> + <Plugins /> + </WizardSection> + </WizardsTab> + ); +} + +export default Syndication; diff --git a/src/wizards/newspack/views/settings/syndication/plugins.tsx b/src/wizards/newspack/views/settings/syndication/plugins.tsx new file mode 100644 index 0000000000..a519094fdc --- /dev/null +++ b/src/wizards/newspack/views/settings/syndication/plugins.tsx @@ -0,0 +1,58 @@ +/** + * Settings Wizard: Connections > Plugins + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Fragment } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import WizardsPluginCard from '../../../../wizards-plugin-card'; + +const PLUGINS: Record< string, PluginCard > = { + 'publish-to-apple-news': { + slug: 'publish-to-apple-news', + title: __( 'Publish to Apple News', 'newspack-plugin' ), + editLink: 'admin.php?page=apple-news-options', + isConfigurable: true, + reloadOnActivation: false, + description: __( + 'Export and synchronize posts to Apple format.', + 'newspack-plugin' + ), + }, + distributor: { + slug: 'distributor', + title: __( 'Distributor', 'newspack-plugin' ), + editLink: 'admin.php?page=distributor', + reloadOnActivation: false, + description: __( + 'Distributor is a WordPress plugin that makes it easy to syndicate and reuse content across your websites — whether in a single multisite or across the web.', + 'newspack-plugin' + ), + }, +}; + +function Plugins() { + return ( + <Fragment> + { Object.keys( PLUGINS ).map( pluginKey => { + return ( + <WizardsPluginCard + key={ pluginKey } + isTogglable + isStatusPrepended={ false } + isMedium={ false } + { ...PLUGINS[ pluginKey ] } + /> + ); + } ) } + </Fragment> + ); +} + +export default Plugins; diff --git a/src/wizards/newspack/views/settings/syndication/rss.tsx b/src/wizards/newspack/views/settings/syndication/rss.tsx new file mode 100644 index 0000000000..8ea6fa5af1 --- /dev/null +++ b/src/wizards/newspack/views/settings/syndication/rss.tsx @@ -0,0 +1,52 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import WizardsActionCard from '../../../../wizards-action-card'; +import useWizardApiFetchToggle from '../../../../hooks/use-wizard-api-fetch-toggle'; + +function Rss() { + const { + description, + apiData, + isFetching, + actionText, + apiFetchToggle, + errorMessage, + } = useWizardApiFetchToggle< RssData >( { + path: '/newspack/v1/wizard/newspack-settings/syndication', + apiNamespace: 'newspack-settings/syndication/rss', + refreshOn: [ 'POST' ], + data: { + module_enabled_rss: false, + 'module_enabled_media-partners': false, + }, + description: __( + 'Create and manage customized RSS feeds for syndication partners', + 'newspack-plugin' + ), + } ); + + return ( + <WizardsActionCard + title={ __( 'RSS Enhancements', 'newspack' ) } + description={ description } + disabled={ isFetching } + actionText={ actionText } + error={ errorMessage } + toggleChecked={ apiData.module_enabled_rss } + toggleOnChange={ ( value: boolean ) => + apiFetchToggle( + { ...apiData, module_enabled_rss: value }, + true + ) + } + /> + ); +} + +export default Rss; diff --git a/src/wizards/newspack/views/settings/theme-and-brand/colors.tsx b/src/wizards/newspack/views/settings/theme-and-brand/colors.tsx new file mode 100644 index 0000000000..a2043d121f --- /dev/null +++ b/src/wizards/newspack/views/settings/theme-and-brand/colors.tsx @@ -0,0 +1,49 @@ +/** + * Newspack > Settings > Theme and Brand > Colors. Component for setting colors to use in your theme. + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { ColorPicker, Grid } from '../../../../../components/src'; + +export default function Colors( { + themeMods, + updateColors, +}: { + themeMods: ThemeMods; + updateColors: ( a: ThemeMods ) => void; +} ) { + return ( + <Grid gutter={ 32 }> + { /* This UI does not enable setting 'theme_colors' to 'default'. As soon as a color is picked, 'theme_colors' will be 'custom'. */ } + <ColorPicker + label={ __( 'Primary', 'newspack-plugin' ) } + color={ themeMods.primary_color_hex } + onChange={ ( primary_color_hex: string ) => + updateColors( { + ...themeMods, + primary_color_hex, + theme_colors: 'custom', + } ) + } + /> + <ColorPicker + label={ __( 'Secondary', 'newspack-plugin' ) } + color={ themeMods.secondary_color_hex } + onChange={ ( secondary_color_hex: string ) => + updateColors( { + ...themeMods, + secondary_color_hex, + theme_colors: 'custom', + } ) + } + /> + </Grid> + ); +} diff --git a/src/wizards/newspack/views/settings/theme-and-brand/footer.tsx b/src/wizards/newspack/views/settings/theme-and-brand/footer.tsx new file mode 100644 index 0000000000..6e34d1b92c --- /dev/null +++ b/src/wizards/newspack/views/settings/theme-and-brand/footer.tsx @@ -0,0 +1,105 @@ +/** + * Newspack > Settings > Theme and Brand > Footer. + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { ToggleControl } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { + ColorPicker, + Grid, + ImageUpload, + SelectControl, + TextControl, +} from '../../../../../components/src'; + +export default function Footer( { + themeMods, + onUpdate, +}: { + themeMods: ThemeMods; + onUpdate: ( a: ThemeMods ) => void; +} ) { + function updateThemeMods( themeModChanges: Partial< ThemeMods > ) { + onUpdate( { ...themeMods, ...themeModChanges } ); + } + return ( + <Grid gutter={ 32 }> + <Grid columns={ 1 } gutter={ 16 }> + <TextControl + label={ __( 'Copyright information', 'newspack' ) } + value={ themeMods.footer_copyright || '' } + onChange={ ( footer_copyright: string ) => + updateThemeMods( { footer_copyright } ) + } + /> + { /* <Card noBorder className="newspack-design__footer__copyright"> + </Card> */ } + <ToggleControl + checked={ themeMods.footer_color !== 'default' } + onChange={ checked => + updateThemeMods( { + footer_color: checked ? 'custom' : 'default', + } ) + } + label={ __( + 'Apply a background color to the footer', + 'newspack' + ) } + /> + { themeMods.footer_color === 'custom' && ( + <ColorPicker + label={ __( 'Background color' ) } + color={ themeMods.footer_color_hex } + onChange={ ( footer_color_hex: string ) => + updateThemeMods( { footer_color_hex } ) + } + /> + ) } + </Grid> + <Grid columns={ 1 } gutter={ 16 }> + <ImageUpload + className="newspack-design__footer__logo" + label={ __( 'Alternative Logo', 'newspack' ) } + help={ __( + 'Optional alternative logo to be displayed in the footer.', + 'newspack' + ) } + style={ { + backgroundColor: + themeMods.footer_color === 'custom' && + themeMods.footer_color_hex + ? themeMods.footer_color_hex + : 'transparent', + } } + image={ themeMods.newspack_footer_logo } + onChange={ ( newspack_footer_logo: string ) => + updateThemeMods( { newspack_footer_logo } ) + } + /> + { themeMods.newspack_footer_logo && ( + <SelectControl + className="icon-only" + label={ __( 'Alternative logo - Size', 'newspack' ) } + value={ themeMods.footer_logo_size } + onChange={ ( footer_logo_size: string ) => + updateThemeMods( { footer_logo_size } ) + } + buttonOptions={ [ + { value: 'small', label: 'S' }, + { value: 'medium', label: 'M' }, + { value: 'large', label: 'L' }, + { value: 'xlarge', label: 'XL' }, + ] } + /> + ) } + </Grid> + </Grid> + ); +} diff --git a/src/wizards/newspack/views/settings/theme-and-brand/header.tsx b/src/wizards/newspack/views/settings/theme-and-brand/header.tsx new file mode 100644 index 0000000000..19e8b93ad1 --- /dev/null +++ b/src/wizards/newspack/views/settings/theme-and-brand/header.tsx @@ -0,0 +1,134 @@ +/** + * Newspack > Settings > Theme and Brand > Header. + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { alignCenter, alignLeft } from '@wordpress/icons'; +import { ToggleControl } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { + ColorPicker, + Grid, + ImageUpload, + SelectControl, +} from '../../../../../components/src'; +import { LOGO_SIZE_OPTIONS, parseLogoSize } from './utils'; + +export default function Header( { + themeMods, + updateHeader, +}: { + themeMods: ThemeMods; + updateHeader: ( a: ThemeMods ) => void; +} ) { + return ( + <Grid gutter={ 32 }> + <Grid columns={ 1 } gutter={ 16 }> + <Grid + gutter={ 16 } + className="newspack-design__header__style-size" + > + <SelectControl + className="icon-only" + label={ __( 'Style', 'newspack' ) } + value={ + themeMods.header_center_logo ? 'center' : 'left' + } + onChange={ ( align: string ) => + updateHeader( { + ...themeMods, + header_center_logo: align === 'center', + } ) + } + buttonOptions={ [ + { value: 'left', icon: alignLeft }, + { value: 'center', icon: alignCenter }, + ] } + /> + <SelectControl + className="icon-only" + label={ __( 'Size', 'newspack' ) } + value={ + themeMods.header_simplified ? 'small' : 'large' + } + onChange={ ( size: string ) => + updateHeader( { + ...themeMods, + header_simplified: size === 'small', + } ) + } + buttonOptions={ [ + { value: 'small', label: 'S' }, + { value: 'large', label: 'L' }, + ] } + /> + </Grid> + <ToggleControl + checked={ Boolean( themeMods.header_solid_background ) } + onChange={ header_solid_background => + updateHeader( { + ...themeMods, + header_solid_background, + } ) + } + label={ __( + 'Apply a background color to the header', + 'newspack' + ) } + /> + { themeMods.header_solid_background && ( + <ColorPicker + label={ __( 'Background color' ) } + color={ themeMods.header_color_hex } + onChange={ ( header_color_hex: string ) => + updateHeader( { + ...themeMods, + header_color_hex, + } ) + } + /> + ) } + </Grid> + <Grid columns={ 1 } gutter={ 16 }> + <ImageUpload + className="newspack-design__header__logo" + style={ { + backgroundColor: themeMods.header_solid_background + ? themeMods.header_color_hex + : 'transparent', + } } + label={ __( 'Logo', 'newspack' ) } + image={ themeMods.custom_logo } + onChange={ ( custom_logo: string ) => + updateHeader( { + ...themeMods, + custom_logo, + header_text: ! custom_logo, + header_display_tagline: ! custom_logo, + } ) + } + /> + { themeMods.custom_logo && ( + <SelectControl + className="icon-only" + label={ __( 'Logo Size', 'newspack' ) } + value={ parseLogoSize( themeMods.logo_size ) } + onChange={ ( logo_size: number ) => + updateHeader( { + ...themeMods, + logo_size, + } ) + } + buttonOptions={ LOGO_SIZE_OPTIONS } + /> + ) } + </Grid> + </Grid> + ); +} diff --git a/src/wizards/newspack/views/settings/theme-and-brand/homepage-select.tsx b/src/wizards/newspack/views/settings/theme-and-brand/homepage-select.tsx new file mode 100644 index 0000000000..4a0ea016a0 --- /dev/null +++ b/src/wizards/newspack/views/settings/theme-and-brand/homepage-select.tsx @@ -0,0 +1,55 @@ +/** + * Newspack > Settings > Theme and Brand (Tab) > Homepage Select. Component used to select the homepage wp block pattern. + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { Grid, StyleCard } from '../../../../../components/src'; + +/** + * Temporary loading cards to show while fetching data. + */ +const LOADING_CARDS = Array.from( { length: 12 }, Object ); + +export function HomepageSelect( { + homepagePatternIndex, + updateHomepagePattern, + homepagePatterns, + isFetching, +}: { + homepagePatternIndex: number; + updateHomepagePattern: ( a: number ) => void; + homepagePatterns: HomepagePattern[]; + isFetching: boolean; +} ) { + const items = + isFetching && homepagePatterns.length === 0 + ? LOADING_CARDS + : homepagePatterns; + + return ( + <Grid columns={ 6 } gutter={ 16 }> + { items.map( ( pattern, i ) => ( + <StyleCard + key={ i } + image={ { __html: pattern.image } } + imageType="html" + isActive={ i === homepagePatternIndex } + onClick={ () => { + updateHomepagePattern( i ); + } } + ariaLabel={ `${ __( + 'Activate Layout', + 'newspack-plugin' + ) } ${ i + 1 }` } + /> + ) ) } + </Grid> + ); +} diff --git a/src/wizards/site-design/components/theme-selection/images/joseph.png b/src/wizards/newspack/views/settings/theme-and-brand/images/joseph.png similarity index 100% rename from src/wizards/site-design/components/theme-selection/images/joseph.png rename to src/wizards/newspack/views/settings/theme-and-brand/images/joseph.png diff --git a/src/wizards/site-design/components/theme-selection/images/katharine.png b/src/wizards/newspack/views/settings/theme-and-brand/images/katharine.png similarity index 100% rename from src/wizards/site-design/components/theme-selection/images/katharine.png rename to src/wizards/newspack/views/settings/theme-and-brand/images/katharine.png diff --git a/src/wizards/site-design/components/theme-selection/images/nelson.png b/src/wizards/newspack/views/settings/theme-and-brand/images/nelson.png similarity index 100% rename from src/wizards/site-design/components/theme-selection/images/nelson.png rename to src/wizards/newspack/views/settings/theme-and-brand/images/nelson.png diff --git a/src/wizards/site-design/components/theme-selection/images/newspack.png b/src/wizards/newspack/views/settings/theme-and-brand/images/newspack.png similarity index 100% rename from src/wizards/site-design/components/theme-selection/images/newspack.png rename to src/wizards/newspack/views/settings/theme-and-brand/images/newspack.png diff --git a/src/wizards/site-design/components/theme-selection/images/sacha.png b/src/wizards/newspack/views/settings/theme-and-brand/images/sacha.png similarity index 100% rename from src/wizards/site-design/components/theme-selection/images/sacha.png rename to src/wizards/newspack/views/settings/theme-and-brand/images/sacha.png diff --git a/src/wizards/site-design/components/theme-selection/images/scott.png b/src/wizards/newspack/views/settings/theme-and-brand/images/scott.png similarity index 100% rename from src/wizards/site-design/components/theme-selection/images/scott.png rename to src/wizards/newspack/views/settings/theme-and-brand/images/scott.png diff --git a/src/wizards/newspack/views/settings/theme-and-brand/index.tsx b/src/wizards/newspack/views/settings/theme-and-brand/index.tsx new file mode 100644 index 0000000000..fb0ee6397a --- /dev/null +++ b/src/wizards/newspack/views/settings/theme-and-brand/index.tsx @@ -0,0 +1,232 @@ +/** + * Newspack > Settings > Theme and Brand + */ + +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; +import { useState, useEffect, Fragment } from '@wordpress/element'; + +/** + * Internal dependencies. + */ +import ThemeSelection from './theme-select'; +import WizardsTab from '../../../../wizards-tab'; +import WizardSection from '../../../../wizards-section'; +import { HomepageSelect } from './homepage-select'; +import { Button, Router } from '../../../../../components/src'; +import { useWizardApiFetch } from '../../../../hooks/use-wizard-api-fetch'; +import Header from './header'; +import Footer from './footer'; +import Colors from './colors'; +import Typography from './typography'; +import { DEFAULT_THEME_MODS } from '../constants'; + +const { useHistory } = Router; + +const DEFAULT_DATA: ThemeData = { + etc: { post_count: '0' }, + theme: 'newspack-theme', + homepage_patterns: [], + theme_mods: { ...DEFAULT_THEME_MODS }, +}; + +const ThemeBrand = ( { isPartOfSetup = false } ) => { + const { wizardApiFetch, isFetching } = useWizardApiFetch( + 'newspack-settings/theme-mods' + ); + const [ data, setDataState ] = useState< ThemeData >( DEFAULT_DATA ); + + const history = useHistory(); + + function setData( newData: ThemeData ) { + setDataState( { ...data, ...newData } ); + } + + const finishSetup = () => { + wizardApiFetch( + { + data, + path: '/newspack/v1/wizard/newspack-setup-wizard/complete', + method: 'POST', + updateCacheMethods: [ 'GET' ], + }, + { + onSuccess: res => { + setData( res ); + history.push( '/completed' ); + }, + } + ); + }; + + async function save() { + return new Promise( resolve => + wizardApiFetch( + { + data, + path: '/newspack/v1/wizard/newspack-setup-wizard/theme', + method: 'POST', + updateCacheMethods: [ 'GET' ], + }, + { + onSuccess: res => { + setData( res ); + resolve( res ); + }, + } + ) + ); + } + + useEffect( () => { + wizardApiFetch( + { + path: '/newspack/v1/wizard/newspack-setup-wizard/theme', + }, + { + onSuccess: setData, + } + ); + }, [] ); + + return ( + <WizardsTab + title={ __( 'Theme and Brand', 'newspack-plugin' ) } + isFetching={ isFetching } + > + { ! isPartOfSetup && ( + <Fragment> + <WizardSection + title={ __( 'Theme', 'newspack-plugin' ) } + description={ __( + 'Update your sites theme.', + 'newspack-plugin' + ) } + > + <ThemeSelection + theme={ + isFetching ? '' : data.theme || 'newspack-theme' + } + updateTheme={ theme => + setData( { ...data, theme } ) + } + /> + </WizardSection> + </Fragment> + ) } + { isPartOfSetup && ( + <WizardSection + title={ __( 'Homepage', 'newspack-plugin' ) } + description={ __( + 'Select a homepage layout.', + 'newspack-plugin' + ) } + > + <HomepageSelect + isFetching={ isFetching } + homepagePatternIndex={ + data.theme_mods.homepage_pattern_index + } + homepagePatterns={ data.homepage_patterns } + updateHomepagePattern={ homepage_pattern_index => { + setData( { + ...data, + theme_mods: { + ...data.theme_mods, + homepage_pattern_index, + }, + } ); + } } + /> + </WizardSection> + ) } + <WizardSection + title={ __( 'Colors', 'newspack-plugin' ) } + description={ __( + 'Pick your primary and secondary colors.', + 'newspack-plugin' + ) } + > + <Colors + themeMods={ data.theme_mods } + updateColors={ theme_mods => { + setData( { + ...data, + theme_mods, + } ); + } } + /> + </WizardSection> + <WizardSection + title={ __( 'Typography', 'newspack-plugin' ) } + description={ __( + 'Define the font pairing to use throughout your site', + 'newspack-plugin' + ) } + > + <Typography + data={ data.theme_mods } + isFetching={ isFetching } + update={ theme_mods => { + setData( { + ...data, + theme_mods, + } ); + } } + /> + </WizardSection> + <WizardSection + title={ __( 'Header', 'newspack-plugin' ) } + description={ __( + 'Update the header and add your logo.', + 'newspack-plugin' + ) } + > + <Header + themeMods={ data.theme_mods } + updateHeader={ theme_mods => { + setData( { + ...data, + theme_mods, + } ); + } } + /> + </WizardSection> + <WizardSection + title={ __( 'Footer', 'newspack-plugin' ) } + description={ __( + 'Personalize the footer of your site.', + 'newspack-plugin' + ) } + > + <Footer + themeMods={ data.theme_mods } + onUpdate={ theme_mods => { + setData( { + ...data, + theme_mods, + } ); + } } + /> + </WizardSection> + <div className="newspack-buttons-card"> + { isPartOfSetup ? ( + <Button + variant="primary" + onClick={ () => save().then( finishSetup ) } + > + { __( 'Finish', 'newspack-plugin' ) } + </Button> + ) : ( + <Button variant="primary" onClick={ save }> + { __( 'Save', 'newspack-plugin' ) } + </Button> + ) } + </div> + </WizardsTab> + ); +}; + +export default ThemeBrand; diff --git a/src/wizards/site-design/components/theme-selection/index.js b/src/wizards/newspack/views/settings/theme-and-brand/theme-select.tsx similarity index 87% rename from src/wizards/site-design/components/theme-selection/index.js rename to src/wizards/newspack/views/settings/theme-and-brand/theme-select.tsx index b5aab1552f..3ef568c64b 100644 --- a/src/wizards/site-design/components/theme-selection/index.js +++ b/src/wizards/newspack/views/settings/theme-and-brand/theme-select.tsx @@ -1,15 +1,24 @@ +/** + * Theme Selection component + */ /** * Internal dependencies */ -import { Grid, StyleCard } from '../../../../components/src'; -import NewspackImg from './images/newspack.png'; import ScottImg from './images/scott.png'; -import NelsonImg from './images/nelson.png'; -import KatharineImg from './images/katharine.png'; import SachaImg from './images/sacha.png'; +import NelsonImg from './images/nelson.png'; import JosephImg from './images/joseph.png'; +import NewspackImg from './images/newspack.png'; +import KatharineImg from './images/katharine.png'; +import { Grid, StyleCard } from '../../../../../components/src'; -const ThemeSelection = ( { theme, updateTheme } ) => ( +const ThemeSelection = ( { + theme, + updateTheme, +}: { + theme: ThemeData[ 'theme' ]; + updateTheme: ( a: ThemeData[ 'theme' ] ) => void; +} ) => ( <Grid columns={ 3 } gutter={ 32 }> <StyleCard cardTitle="Newspack" diff --git a/src/wizards/newspack/views/settings/theme-and-brand/types.d.ts b/src/wizards/newspack/views/settings/theme-and-brand/types.d.ts new file mode 100644 index 0000000000..30273a6268 --- /dev/null +++ b/src/wizards/newspack/views/settings/theme-and-brand/types.d.ts @@ -0,0 +1,13 @@ +/** + * Typography schema. + */ +type Typography = { + font_header: string; + font_body: string; + accent_allcaps: boolean; + // Curated fonts + custom_font_import_code?: string; + custom_font_import_code_alternate?: string; + font_body_stack?: string; + font_header_stack?: string; +}; diff --git a/src/wizards/newspack/views/settings/theme-and-brand/typography.tsx b/src/wizards/newspack/views/settings/theme-and-brand/typography.tsx new file mode 100644 index 0000000000..df94f13167 --- /dev/null +++ b/src/wizards/newspack/views/settings/theme-and-brand/typography.tsx @@ -0,0 +1,241 @@ +/** + * Newspack > Settings > Theme and Brand > Typography. Component for setting typography to use in your theme. + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState, useEffect } from '@wordpress/element'; +import { TextareaControl, ToggleControl } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { + Grid, + SelectControl, + TextControl, +} from '../../../../../components/src'; +import { + getFontImportURL, + getFontsList, + isFontInOptions, + TYPOGRAPHY_OPTIONS, +} from './utils'; + +/** + * Font Group schema. + */ +type FontGroup = { + label: string; + fallback?: string; + options: Array< { + label: string; + value: string; + } >; +}; + +export default function Typography( { + data, + isFetching, + update, +}: ThemeModComponentProps & { isFetching: boolean } ) { + const [ typographyOptionsType, updateTypographyOptionsType ] = useState< + null | 'curated' | 'custom' + >( null ); + + useEffect( () => { + if ( typographyOptionsType ) { + return; + } + if ( data.font_body && data.font_header ) { + updateTypographyOptionsType( getType() ); + } + }, [ data.font_body, data.font_body ] ); + + function getType() { + const { font_header: headerFont, font_body: bodyFont } = data; + if ( + ( headerFont && ! isFontInOptions( headerFont ) ) || + ( bodyFont && ! isFontInOptions( bodyFont ) ) + ) { + return TYPOGRAPHY_OPTIONS[ 1 ].value; + } + return TYPOGRAPHY_OPTIONS[ 0 ].value; + } + + function updateTypographyState( + objectOrKey: Partial< Typography > | string, + change?: string | boolean + ) { + if ( objectOrKey instanceof Object ) { + update( { ...data, ...objectOrKey } ); + return; + } + if ( typeof change === 'undefined' ) { + return; + } + update( { ...data, [ objectOrKey ]: change } ); + } + + const renderCustomFontChoice = ( type: string ) => { + const isHeadings = type === 'headings'; + const label = isHeadings + ? __( 'Headings', 'newspack-plugin' ) + : __( 'Body', 'newspack-plugin' ); + return ( + <Grid columns={ 1 } gutter={ 16 }> + <TextareaControl + label={ + label + + ' - ' + + __( + 'Font provider import code or URL', + 'newspack-plugin' + ) + } + placeholder={ + 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,700;1,400;1,700&display=swap' + } + value={ + ( isHeadings + ? data.custom_font_import_code + : data.custom_font_import_code_alternate ) ?? '' + } + onChange={ e => { + updateTypographyState( + isHeadings + ? 'custom_font_import_code' + : 'custom_font_import_code_alternate', + e + ); + } } + rows={ 3 } + /> + <TextControl + label={ + label + ' - ' + __( 'Font name', 'newspack-plugin' ) + } + value={ isHeadings ? data.font_header : data.font_body } + onChange={ ( e: string ) => { + updateTypographyState( + isHeadings ? 'font_header' : 'font_body', + e + ); + } } + /> + <SelectControl + label={ + label + + ' - ' + + __( 'Font fallback stack', 'newspack-plugin' ) + } + options={ [ + { + value: 'serif', + label: __( 'Serif', 'newspack-plugin' ), + }, + { + value: 'sans-serif', + label: __( 'Sans Serif', 'newspack-plugin' ), + }, + { + value: 'display', + label: __( 'Display', 'newspack-plugin' ), + }, + { + value: 'monospace', + label: __( 'Monospace', 'newspack-plugin' ), + }, + ] } + value={ + isHeadings + ? data.font_header_stack + : data.font_body_stack + } + onChange={ ( e: string ) => + updateTypographyState( + isHeadings + ? 'font_header_stack' + : 'font_body_stack', + e + ) + } + /> + </Grid> + ); + }; + + return ( + <Grid columns={ 1 } gutter={ 16 }> + <SelectControl + label={ __( 'Typography Options', 'newspack-plugin' ) } + hideLabelFromVision + disabled={ true } + value={ + typographyOptionsType ? typographyOptionsType : 'curated' + } + onChange={ updateTypographyOptionsType } + buttonOptions={ + isFetching + ? [ + { + label: __( 'Loading…', 'newspack-plugin' ), + value: null, + }, + ] + : TYPOGRAPHY_OPTIONS + } + /> + <Grid gutter={ 32 }> + { typographyOptionsType === 'curated' || + null === typographyOptionsType ? ( + <> + <SelectControl + label={ __( 'Headings', 'newspack-plugin' ) } + optgroups={ getFontsList( true ) } + value={ data.font_header } + onChange={ ( value: string, group: FontGroup ) => { + updateTypographyState( { + font_header: value, + custom_font_import_code: + getFontImportURL( value ), + font_header_stack: group?.fallback, + } ); + } } + /> + <SelectControl + label={ __( 'Body', 'newspack-plugin' ) } + optgroups={ getFontsList() } + value={ data.font_body } + onChange={ ( value: string, group: FontGroup ) => { + updateTypographyState( { + font_body: value, + custom_font_import_code_alternate: + getFontImportURL( value ), + font_body_stack: group?.fallback, + } ); + } } + /> + </> + ) : ( + <> + { renderCustomFontChoice( 'headings' ) } + { renderCustomFontChoice( 'body' ) } + </> + ) } + </Grid> + <ToggleControl + checked={ data.accent_allcaps } + onChange={ checked => + updateTypographyState( 'accent_allcaps', checked ) + } + label={ __( + 'Use all-caps for accent text', + 'newspack-plugin' + ) } + /> + </Grid> + ); +} diff --git a/src/wizards/site-design/views/main/utils.js b/src/wizards/newspack/views/settings/theme-and-brand/utils.ts similarity index 69% rename from src/wizards/site-design/views/main/utils.js rename to src/wizards/newspack/views/settings/theme-and-brand/utils.ts index bf013f30d6..eb3b568761 100644 --- a/src/wizards/site-design/views/main/utils.js +++ b/src/wizards/newspack/views/settings/theme-and-brand/utils.ts @@ -3,8 +3,11 @@ */ import { __ } from '@wordpress/i18n'; -const processFontOptions = ( headingsOnly, options ) => - options.reduce( ( acc, option ) => { +const processFontOptions = ( + headingsOnly: boolean, + options: { label: string; value?: string }[] +) => + options.reduce( ( acc: { label: string; value?: string }[], option ) => { const isHeadingsOnly = option.label.indexOf( '(*)' ) > 0; const label = option.label.replace( ' (*)', '' ); const selectOption = { @@ -96,56 +99,72 @@ const MONOSPACE_FONTS = [ { label: 'Space Mono' }, { label: 'Roboto Mono' }, ]; -const ALL_FONTS = [ ...SERIF_FONTS, ...SANS_SERIF_FONTS, ...DISPLAY_FONTS, ...MONOSPACE_FONTS ]; +const ALL_FONTS = [ + ...SERIF_FONTS, + ...SANS_SERIF_FONTS, + ...DISPLAY_FONTS, + ...MONOSPACE_FONTS, +]; + +export const TYPOGRAPHY_OPTIONS: { + value: 'curated' | 'custom'; + label: string; +}[] = [ + { value: 'curated', label: __( 'Default', 'newspack-plugin' ) }, + { value: 'custom', label: __( 'Custom', 'newspack-plugin' ) }, +]; -export const getFontsList = headingsOnly => +export const getFontsList = ( headingsOnly: boolean = false ) => [ { - label: __( 'Serif', 'newspack' ), + label: __( 'Serif', 'newspack-plugin' ), fallback: 'serif', options: SERIF_FONTS, }, { - label: __( 'Sans Serif', 'newspack' ), + label: __( 'Sans Serif', 'newspack-plugin' ), fallback: 'sans_serif', options: SANS_SERIF_FONTS, }, { - label: __( 'Display', 'newspack' ), + label: __( 'Display', 'newspack-plugin' ), fallback: 'display', options: DISPLAY_FONTS, }, { - label: __( 'Monospace', 'newspack' ), + label: __( 'Monospace', 'newspack-plugin' ), fallback: 'monospace', options: MONOSPACE_FONTS, }, ] - .map( group => ( { ...group, options: processFontOptions( headingsOnly, group.options ) } ) ) + .map( group => ( { + ...group, + options: processFontOptions( headingsOnly, group.options ), + } ) ) .filter( group => group.options.length ); -export const isFontInOptions = label => - ALL_FONTS.filter( option => option.label.indexOf( label ) === 0 ).length >= 1; +export const isFontInOptions = ( label: string ) => + ALL_FONTS.filter( option => ! option.label.includes( label ) ).length >= 1; -export const getFontImportURL = value => +export const getFontImportURL = ( value: string ) => `//fonts.googleapis.com/css2?family=${ value.replace( /\s/g, '+' ) }:ital,wght@0,400;0,700;1,400;1,700&display=swap`; export const LOGO_SIZE_OPTIONS = [ - { value: 0, label: __( 'XS', 'newspack' ) }, - { value: 19, label: __( 'S', 'newspack' ) }, - { value: 48, label: __( 'M', 'newspack' ) }, - { value: 72, label: __( 'L', 'newspack' ) }, - { value: 91, label: __( 'XL', 'newspack' ) }, + { value: 0, label: __( 'XS', 'newspack-plugin' ) }, + { value: 19, label: __( 'S', 'newspack-plugin' ) }, + { value: 48, label: __( 'M', 'newspack-plugin' ) }, + { value: 72, label: __( 'L', 'newspack-plugin' ) }, + { value: 91, label: __( 'XL', 'newspack-plugin' ) }, ]; /** * Map a logo size to an option value. * The size might have been set in the Customizer, where it is a slider input. */ -export const parseLogoSize = ( size, options = LOGO_SIZE_OPTIONS ) => +export const parseLogoSize = ( size: number, options = LOGO_SIZE_OPTIONS ) => options.reduce( ( foundSize, { value } ) => ( size >= value ? value : foundSize ), options[ 0 ].value diff --git a/src/wizards/newspack/views/settings/theme-mods.d.ts b/src/wizards/newspack/views/settings/theme-mods.d.ts new file mode 100644 index 0000000000..c02aa7f904 --- /dev/null +++ b/src/wizards/newspack/views/settings/theme-mods.d.ts @@ -0,0 +1,133 @@ +/** + * Theme names without `newspack` prefix. + */ +type ThemeNames = + | 'theme' + | 'scott' + | 'nelson' + | 'katharine' + | 'sacha' + | 'joseph'; + +/** + * Theme names with `newspack` prefix. + */ +type NewspackThemes = `newspack-${ ThemeNames }`; + +/** + * Property on theme mods endpoint. + */ +interface Etc { + post_count: string; +} + +/** + * Theme and brand schema. + */ +interface ThemeData< T = {} > { + etc: Etc; + theme: '' | NewspackThemes; + theme_mods: ThemeMods< T >; + homepage_patterns: HomepagePattern[]; +} + +/** + * Homepage pattern schema. + */ +type HomepagePattern = { + content: string; + image: string; +}; + +/** + * Theme and Brand. + */ +interface ThemeAndBrand { + // Colors. + header_color: string; // Possible values from context + theme_colors: string; + primary_color_hex: string; + secondary_color_hex: string; + + // Typography. + font_body: string; + font_header: string; + font_body_stack?: string; + font_header_stack?: string; + accent_allcaps: boolean; + custom_font_import_code?: string; + custom_font_import_code_alternate?: string; + + // Header. + header_center_logo: boolean; + header_simplified: boolean; + header_solid_background: boolean; + header_color_hex: string; + custom_logo: string; + logo_size: number; + header_text: boolean; + header_display_tagline: boolean; + + // Footer. + footer_copyright: string; + footer_color: string; + footer_color_hex: string; + newspack_footer_logo: string; + footer_logo_size: string; + + // Homepage pattern. + homepage_pattern_index: number; +} + +/** + * Theme mods component. + */ +type ThemeModComponentProps< T = ThemeMods > = { + update: ( a: Partial< T > ) => void; + isFetching?: boolean; + data: T; +}; + +/** + * Recirculation schema. + */ +interface Recirculation { + relatedPostsEnabled: boolean; + relatedPostsError: WizardApiErrorType | null; + relatedPostsMaxAge: number; + relatedPostsUpdated: boolean; +} + +/** + * Display settings. + */ +interface DisplaySettings { + // Author Bio. + show_author_bio: boolean; + show_author_email: boolean; + author_bio_length: number; + + // Default Featured Image and Post Template. + featured_image_default: string; + post_template_default: string; + + // Featured Image and Post Template for All Posts. + featured_image_all_posts: string; + post_template_all_posts: string; + + // Media Credits. + newspack_image_credits_placeholder_url?: string; + newspack_image_credits_class_name: string; + newspack_image_credits_prefix_label: string; + newspack_image_credits_placeholder: number | null; + newspack_image_credits_auto_populate: boolean; +} + +interface MiscSettings { + /** + * Misc. + */ + custom_css_post_id: number; +} + +interface ThemeMods extends ThemeAndBrand, DisplaySettings, MiscSettings {} diff --git a/src/wizards/newspack/views/settings/types.d.ts b/src/wizards/newspack/views/settings/types.d.ts new file mode 100644 index 0000000000..dc2fe91389 --- /dev/null +++ b/src/wizards/newspack/views/settings/types.d.ts @@ -0,0 +1,104 @@ +/** + * reCAPTCHA state params + */ +type RecaptchaVersions = 'v2_invisible' | 'v3'; +type RecaptchaData = { + threshold: string; + use_captcha: boolean; + version: RecaptchaVersions; + credentials: Record< RecaptchaVersions, { site_key: string; site_secret: string } >; +}; + +/** + * OAuth payload + */ +type OAuthData = { + user_basic_info?: { email: string; has_refresh_token: boolean }; + username?: string; + error?: Error; +}; + +/** + * Webhook actions data type. + */ +type WebhookActions = 'edit' | 'delete' | 'view' | 'toggle' | 'new' | null; + +/** + * Endpoint data type. + */ +type Endpoint = { + url: string; + label: string; + requests: { + errors: any[]; + id: string; + status: 'pending' | 'finished' | 'killed'; + scheduled: string; + action_name: string; + }[]; + disabled: boolean; + disabled_error: boolean; + id: string | number; + system: string; + actions: string[]; + bearer_token?: string; +}; + +/** + * @wordpress/components/CheckboxControl component props override. Required to apply + * correct types to legacy version. + */ +type WpCheckboxControlPropsOverride< T > = React.ComponentProps< T > & { + indeterminate?: boolean; + key?: React.Key | null; +}; + +/** + * Modals component props. + */ +type ModalComponentProps = { + endpoint: Endpoint; + actions: string[]; + errorMessage: string | null; + inFlight: boolean; + action: WebhookActions; + setError: ( err: WizardErrorType | null | string ) => void; + setAction: ( action: WebhookActions, id: number | string ) => void; + wizardApiFetch: < T = any >( + opts: ApiFetchOptions, + callbacks?: ApiFetchCallbacks< T > + ) => void; + setEndpoints: ( endpoints: Endpoint[] ) => void; +}; + +/* + * Google Analytics 4 credentials Type. + */ +type Ga4Credentials = Record< string, string >; + +/** Social */ +type PixelData = { + active: boolean; + pixel_id: string; +}; + +/** Syndication */ +/** + * RSS API data + */ +type RssData = { + module_enabled_rss: boolean; + 'module_enabled_media-partners': boolean; +}; + +/** Jetpack SSO Caps */ +type JetpackSSOCaps = 'edit_posts' | 'publish_posts' | 'edit_others_posts' | 'manage_options'; + +/** Jetpack SSO Settings */ +type JetpackSSOSettings = Partial<{ + jetpack_sso_force_2fa: boolean; + force_2fa: boolean; + force_2fa_cap: JetpackSSOCaps; + obfuscate_account: boolean; + available_caps: { [key in JetpackSSOCaps]?: string }; +}>; diff --git a/src/wizards/popups/views/settings/index.js b/src/wizards/popups/views/settings/index.js deleted file mode 100644 index 210ee6aead..0000000000 --- a/src/wizards/popups/views/settings/index.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Internal dependencies - */ -import { withWizardScreen, PluginSettings } from '../../../../components/src'; - -const Settings = () => { - return <PluginSettings pluginSlug="newspack-popups-wizard" isWizard={ true } title={ null } />; -}; - -export default withWizardScreen( Settings ); diff --git a/src/wizards/readerRevenue/components/index.js b/src/wizards/readerRevenue/components/index.js deleted file mode 100644 index cea2a876da..0000000000 --- a/src/wizards/readerRevenue/components/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as MoneyInput } from './money-input'; diff --git a/src/wizards/readerRevenue/index.js b/src/wizards/readerRevenue/index.js deleted file mode 100644 index 6ecd543329..0000000000 --- a/src/wizards/readerRevenue/index.js +++ /dev/null @@ -1,86 +0,0 @@ -import '../../shared/js/public-path'; - -/** - * External dependencies. - */ -import values from 'lodash/values'; - -/** - * WordPress dependencies. - */ -import { render, createElement } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies. - */ -import { Wizard, Notice } from '../../components/src'; -import * as Views from './views'; -import { READER_REVENUE_WIZARD_SLUG, NEWSPACK, NRH, OTHER } from './constants'; - -const ReaderRevenueWizard = () => { - const { platform_data, plugin_status, donation_data } = Wizard.useWizardData( 'reader-revenue' ); - const usedPlatform = platform_data?.platform; - const platformSection = { - label: __( 'Platform', 'newspack' ), - path: '/', - render: Views.Platform, - }; - - let sections = [ - { - label: __( 'Donations', 'newspack' ), - path: '/donations', - render: Views.Donation, - isHidden: usedPlatform === OTHER, - }, - { - label: __( 'Payment Methods', 'newspack' ), - path: '/payment-methods', - activeTabPaths: [ '/payment-methods' ], - render: Views.StripeSetup, - isHidden: usedPlatform !== NEWSPACK, - }, - { - label: __( 'Emails', 'newspack' ), - path: '/emails', - render: Views.Emails, - isHidden: usedPlatform !== NEWSPACK, - }, - { - label: __( 'Salesforce', 'newspack' ), - path: '/salesforce', - render: Views.Salesforce, - isHidden: usedPlatform !== NEWSPACK, - }, - { - label: __( 'News Revenue Hub Settings', 'newspack' ), - path: '/settings', - render: Views.NRHSettings, - isHidden: usedPlatform !== NRH, - }, - platformSection, - ]; - if ( usedPlatform === NEWSPACK && ! plugin_status ) { - sections = [ platformSection ]; - } - return ( - <Wizard - headerText={ __( 'Reader Revenue', 'newspack' ) } - subHeaderText={ __( 'Generate revenue from your customers', 'newspack' ) } - sections={ sections } - apiSlug={ READER_REVENUE_WIZARD_SLUG } - renderAboveSections={ () => - values( donation_data?.errors ).map( ( error, i ) => ( - <Notice key={ i } isError noticeText={ error } /> - ) ) - } - requiredPlugins={ [ 'newspack-blocks' ] } - /> - ); -}; - -render( - createElement( ReaderRevenueWizard ), - document.getElementById( 'newspack-reader-revenue-wizard' ) -); diff --git a/src/wizards/readerRevenue/views/donation/index.tsx b/src/wizards/readerRevenue/views/donation/index.tsx deleted file mode 100644 index 5163fefcf5..0000000000 --- a/src/wizards/readerRevenue/views/donation/index.tsx +++ /dev/null @@ -1,449 +0,0 @@ -/** - * WordPress dependencies. - */ -import { __, sprintf } from '@wordpress/i18n'; -import { useDispatch } from '@wordpress/data'; -import { ToggleControl, CheckboxControl } from '@wordpress/components'; - -/** - * Internal dependencies. - */ -import { MoneyInput } from '../../components'; -import { - ActionCard, - Button, - Card, - Grid, - Notice, - SectionHeader, - SelectControl, - TextControl, - Wizard, -} from '../../../../components/src'; -import { READER_REVENUE_WIZARD_SLUG } from '../../constants'; - -type FrequencySlug = 'once' | 'month' | 'year'; - -const FREQUENCIES: { - [ Key in FrequencySlug as string ]: { tieredLabel: string; staticLabel: string }; -} = { - once: { - tieredLabel: __( 'One-time donations' ), - staticLabel: __( 'Suggested one-time donation amount' ), - }, - month: { - tieredLabel: __( 'Monthly donations' ), - staticLabel: __( 'Suggested donation amount per month' ), - }, - year: { - tieredLabel: __( 'Annual donations' ), - staticLabel: __( 'Suggested donation amount per year' ), - }, -}; -const FREQUENCY_SLUGS: FrequencySlug[] = Object.keys( FREQUENCIES ) as FrequencySlug[]; - -type FieldConfig = { - autocomplete: string; - class: string[]; - label: string; - priority: number; - required: boolean; - type: string; - validate: string[]; -}; - -type WizardData = { - donation_data: - | { errors: { [ key: string ]: string[] } } - | { - amounts: { - [ Key in FrequencySlug as string ]: [ number, number, number, number ]; - }; - disabledFrequencies: { - [ Key in FrequencySlug as string ]: boolean; - }; - currencySymbol: string; - tiered: boolean; - minimumDonation: string; - billingFields: string[]; - trashed: string[]; - }; - platform_data: { - platform: string; - }; - donation_page: { - editUrl: string; - status: string; - }; - available_billing_fields: { - [ key: string ]: FieldConfig; - }; - order_notes_field: FieldConfig; -}; - -export const DonationAmounts = () => { - const wizardData = Wizard.useWizardData( 'reader-revenue' ) as WizardData; - const { updateWizardSettings } = useDispatch( Wizard.STORE_NAMESPACE ); - - if ( ! wizardData.donation_data || 'errors' in wizardData.donation_data ) { - return null; - } - - const { amounts, currencySymbol, tiered, disabledFrequencies, minimumDonation, trashed } = - wizardData.donation_data; - - const changeHandler = ( path: ( string | number )[] ) => ( value: any ) => - updateWizardSettings( { - slug: 'newspack-reader-revenue-wizard', - path: [ 'donation_data', ...path ], - value, - } ); - - const availableFrequencies = FREQUENCY_SLUGS.map( slug => ( { - key: slug, - ...FREQUENCIES[ slug ], - } ) ); - - // Minimum donation is returned by the REST API as a string. - const minimumDonationFloat = parseFloat( minimumDonation ); - - // Whether we can use the Name Your Price extension. If not, layout is forced to Tiered. - const canUseNameYourPrice = window.newspack_reader_revenue?.can_use_name_your_price; - - return ( - <> - { - Array.isArray( trashed ) && 0 < trashed.length && ( - <Notice isError> - { <p - dangerouslySetInnerHTML={ - { __html: sprintf( - // Translators: %1$s is a link to the trashed products. %2$s is a comma-separated list of trashed product names. - __( - 'One or more donation products is in trash. Please <a href="%1$s">restore the product(s)</a> to continue using donation features: %2$s', - 'newspack-plugin' - ), - '/wp-admin/edit.php?post_status=trash&post_type=product', - trashed.join( ', ' ) - ) - } - } - /> - } - </Notice> - ) - } - <Card headerActions noBorder> - <SectionHeader - title={ __( 'Suggested Donations', 'newspack-plugin' ) } - description={ __( - 'Set suggested donation amounts. These will be the default settings for the Donate block.', - 'newspack-plugin' - ) } - noMargin - /> - { canUseNameYourPrice && ( - <SelectControl - label={ __( 'Donation Type', 'newspack-plugin' ) } - onChange={ () => changeHandler( [ 'tiered' ] )( ! tiered ) } - buttonOptions={ [ - { value: true, label: __( 'Tiered', 'newspack-plugin' ) }, - { value: false, label: __( 'Untiered', 'newspack-plugin' ) }, - ] } - buttonSmall - value={ tiered } - hideLabelFromVision - /> - ) } - </Card> - { tiered ? ( - <Grid columns={ 1 }> - { availableFrequencies.map( section => { - const isFrequencyDisabled = disabledFrequencies[ section.key ]; - const isOneFrequencyActive = - Object.values( disabledFrequencies ).filter( Boolean ).length === - FREQUENCY_SLUGS.length - 1; - return ( - <Card noBorder key={ section.key }> - <Grid columns={ 1 } gutter={ 8 }> - <ToggleControl - checked={ ! isFrequencyDisabled } - onChange={ () => - changeHandler( [ 'disabledFrequencies', section.key ] )( - ! isFrequencyDisabled - ) - } - label={ section.tieredLabel } - disabled={ ! isFrequencyDisabled && isOneFrequencyActive } - /> - { ! isFrequencyDisabled && ( - <Grid columns={ 3 } rowGap={ 16 }> - <MoneyInput - currencySymbol={ currencySymbol } - label={ __( 'Low-tier' ) } - error={ - amounts[ section.key ][ 0 ] < minimumDonationFloat - ? __( - 'Warning: suggested donations should be at least the minimum donation amount.', - 'newspack-plugin' - ) : null - } - value={ amounts[ section.key ][ 0 ] } - min={ minimumDonationFloat } - onChange={ changeHandler( [ 'amounts', section.key, 0 ] ) } - /> - <MoneyInput - currencySymbol={ currencySymbol } - label={ __( 'Mid-tier' ) } - error={ - amounts[ section.key ][ 1 ] < minimumDonationFloat - ? __( - 'Warning: suggested donations should be at least the minimum donation amount.', - 'newspack-plugin' - ) : null - } - value={ amounts[ section.key ][ 1 ] } - min={ minimumDonationFloat } - onChange={ changeHandler( [ 'amounts', section.key, 1 ] ) } - /> - <MoneyInput - currencySymbol={ currencySymbol } - label={ __( 'High-tier' ) } - error={ - amounts[ section.key ][ 2 ] < minimumDonationFloat - ? __( - 'Warning: suggested donations should be at least the minimum donation amount.', - 'newspack-plugin' - ) : null - } - value={ amounts[ section.key ][ 2 ] } - min={ minimumDonationFloat } - onChange={ changeHandler( [ 'amounts', section.key, 2 ] ) } - /> - </Grid> - ) } - </Grid> - </Card> - ); - } ) } - </Grid> - ) : ( - <Grid columns={ 1 }> - <Card noBorder> - <Grid columns={ 3 } rowGap={ 16 }> - { availableFrequencies.map( section => { - const isFrequencyDisabled = disabledFrequencies[ section.key ]; - const isOneFrequencyActive = - Object.values( disabledFrequencies ).filter( Boolean ).length === - FREQUENCY_SLUGS.length - 1; - return ( - <Grid columns={ 1 } gutter={ 16 } key={ section.key }> - <ToggleControl - checked={ ! isFrequencyDisabled } - onChange={ () => - changeHandler( [ 'disabledFrequencies', section.key ] )( - ! isFrequencyDisabled - ) - } - label={ section.tieredLabel } - disabled={ ! isFrequencyDisabled && isOneFrequencyActive } - /> - { ! isFrequencyDisabled && ( - <MoneyInput - currencySymbol={ currencySymbol } - label={ section.staticLabel } - value={ amounts[ section.key ][ 3 ] } - min={ minimumDonationFloat } - error={ - amounts[ section.key ][ 3 ] < minimumDonationFloat - ? __( - 'Warning: suggested donations should be at least the minimum donation amount.', - 'newspack-plugin' - ) - : null - } - onChange={ changeHandler( [ 'amounts', section.key, 3 ] ) } - key={ section.key } - /> - ) } - </Grid> - ); - } ) } - </Grid> - </Card> - </Grid> - ) } - <Grid columns={ 3 }> - <TextControl - label={ __( 'Minimum donation', 'newspack-plugin' ) } - help={ __( - 'Set minimum donation amount. Setting a reasonable minimum donation amount can help protect your site from bot attacks.', - 'newspack-plugin' - ) } - type="number" - min={ 1 } - value={ minimumDonationFloat } - onChange={ ( value: string ) => changeHandler( [ 'minimumDonation' ] )( value ) } - /> - </Grid> - </> - ); -}; - -const BillingFields = () => { - const wizardData = Wizard.useWizardData( 'reader-revenue' ) as WizardData; - const { updateWizardSettings } = useDispatch( Wizard.STORE_NAMESPACE ); - - if ( ! wizardData.donation_data || 'errors' in wizardData.donation_data ) { - return null; - } - - const changeHandler = ( path: string[] ) => ( value: any ) => - updateWizardSettings( { - slug: 'newspack-reader-revenue-wizard', - path: [ 'donation_data', ...path ], - value, - } ); - - const availableFields = wizardData.available_billing_fields; - const orderNotesField = wizardData.order_notes_field; - if ( ! availableFields || ! Object.keys( availableFields ).length ) { - return null; - } - - const billingFields = wizardData.donation_data.billingFields.length - ? wizardData.donation_data.billingFields - : Object.keys( availableFields ); - - return ( - <> - <Card noBorder headerActions> - <SectionHeader - title={ __( 'Billing Fields', 'newspack-plugin' ) } - description={ __( - 'Configure the billing fields shown in the modal checkout form. Fields marked with (*) are required if shown. Note that for shippable products, address fields will always be shown.', - 'newspack-plugin' - ) } - noMargin - /> - </Card> - <Grid columns={ 3 } rowGap={ 16 }> - { Object.keys( availableFields ).map( fieldKey => ( - <CheckboxControl - key={ fieldKey } - label={ - availableFields[ fieldKey ].label + - ( availableFields[ fieldKey ].required ? ' *' : '' ) - } - checked={ billingFields.includes( fieldKey ) } - disabled={ fieldKey === 'billing_email' } // Email is always required. - onChange={ () => { - let newFields = [ ...billingFields ]; - if ( billingFields.includes( fieldKey ) ) { - newFields = newFields.filter( field => field !== fieldKey ); - } else { - newFields = [ ...newFields, fieldKey ]; - } - changeHandler( [ 'billingFields' ] )( newFields ); - } } - /> - ) ) } - { orderNotesField && ( - <CheckboxControl - label={ orderNotesField.label } - checked={ billingFields.includes( 'order_comments' ) } - onChange={ () => { - let newFields = [ ...billingFields ]; - if ( billingFields.includes( 'order_comments' ) ) { - newFields = newFields.filter( field => field !== 'order_comments' ); - } else { - newFields = [ ...newFields, 'order_comments' ]; - } - changeHandler( [ 'billingFields' ] )( newFields ); - } } - /> - ) } - </Grid> - </> - ); -}; - -const Donation = () => { - const wizardData = Wizard.useWizardData( 'reader-revenue' ) as WizardData; - const { saveWizardSettings } = useDispatch( Wizard.STORE_NAMESPACE ); - const onSaveDonationSettings = () => - saveWizardSettings( { - slug: READER_REVENUE_WIZARD_SLUG, - section: 'donations', - payloadPath: [ 'donation_data' ], - auxData: { saveDonationProduct: true }, - } ); - const onSaveBillingFields = () => - saveWizardSettings( { - slug: READER_REVENUE_WIZARD_SLUG, - section: 'donations', - payloadPath: [ 'donation_data' ], - } ); - - return ( - <> - <ActionCard - description={ __( 'Configure options for donations.', 'newspack-plugin' ) } - hasGreyHeader={ true } - isMedium - title={ __( 'Donation Settings', 'newspack-plugin' ) } - actionContent={ - <Button variant="primary" onClick={ onSaveDonationSettings }> - { __( 'Save Donation Settings', 'newspack-plugin' ) } - </Button> - } - > - { wizardData.donation_page && ( - <> - <Card noBorder headerActions> - <SectionHeader title={ __( 'Donations Landing Page', 'newspack-plugin' ) } noMargin /> - <Button - variant="secondary" - isSmall - href={ wizardData.donation_page.editUrl } - onClick={ undefined } - > - { __( 'Edit Page' ) } - </Button> - </Card> - { 'publish' === wizardData.donation_page.status ? ( - <Notice - isSuccess - noticeText={ __( 'Your donations landing page is published.', 'newspack-plugin' ) } - /> - ) : ( - <Notice - isError - noticeText={ __( - 'Your donations landing page is not yet published.', - 'newspack-plugin' - ) } - /> - ) } - </> - ) } - <DonationAmounts /> - </ActionCard> - <ActionCard - description={ __( 'Configure options for modal checkouts.', 'newspack-plugin' ) } - hasGreyHeader={ true } - isMedium - title={ __( 'Modal Checkout Settings', 'newspack-plugin' ) } - actionContent={ - <Button variant="primary" onClick={ onSaveBillingFields }> - { __( 'Save Modal Checkout Settings', 'newspack-plugin' ) } - </Button> - } - > - <BillingFields /> - </ActionCard> - </> - ); -}; - -export default Donation; diff --git a/src/wizards/readerRevenue/views/emails/index.js b/src/wizards/readerRevenue/views/emails/index.js deleted file mode 100644 index aab4b7c33a..0000000000 --- a/src/wizards/readerRevenue/views/emails/index.js +++ /dev/null @@ -1,151 +0,0 @@ -/* globals newspack_reader_revenue*/ - -/** - * WordPress dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { useState } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * External dependencies - */ -import values from 'lodash/values'; - -/** - * Internal dependencies - */ -import { PluginInstaller, ActionCard, Notice, utils } from '../../../../components/src'; - -const EMAILS = values( newspack_reader_revenue.emails ); -const postType = newspack_reader_revenue.email_cpt; - -const Emails = () => { - const [ pluginsReady, setPluginsReady ] = useState( null ); - const [ error, setError ] = useState( false ); - const [ inFlight, setInFlight ] = useState( false ); - const [ emails, setEmails ] = useState( EMAILS ); - - const updateStatus = ( postId, status ) => { - setError( false ); - setInFlight( true ); - apiFetch( { - path: `/wp/v2/${ postType }/${ postId }`, - method: 'post', - data: { status }, - } ) - .then( () => { - setEmails( - emails.map( email => { - if ( email.post_id === postId ) { - return { ...email, status }; - } - return email; - } ) - ); - } ) - .catch( setError ) - .finally( () => setInFlight( false ) ); - }; - const resetEmail = postId => { - setError( false ); - setInFlight( true ); - apiFetch( { - path: `/newspack/v1/wizard/newspack-reader-revenue-wizard/donations/emails/${ postId }`, - method: 'DELETE', - quiet: true, - } ) - .then( result => setEmails( values( result ) ) ) - .catch( setError ) - .finally( () => setInFlight( false ) ); - }; - - if ( false === pluginsReady ) { - return ( - <> - <Notice isError> - { __( - 'Newspack uses Newspack Newsletters to handle editing email-type content. Please activate this plugin to proceed.', - 'newspack-plugin' - ) } - </Notice> - <Notice isError> - { __( 'Until this feature is configured, default receipts will be used.', 'newspack-plugin' ) } - </Notice> - <PluginInstaller - style={ pluginsReady ? { display: 'none' } : {} } - plugins={ [ 'newspack-newsletters' ] } - onStatus={ res => setPluginsReady( res.complete ) } - onInstalled={ () => window.location.reload() } - withoutFooterButton={ true } - /> - </> - ); - } - - return ( - <> - { emails.map( email => { - const isActive = email.status === 'publish'; - - let notification = __( 'This email is not active.', 'newspack-plugin' ); - if ( email.type === 'receipt' ) { - notification = __( - 'This email is not active. The default receipt will be used.', - 'newspack-plugin' - ); - } - - if ( email.type === 'welcome' ) { - notification = __( - 'This email is not active. The receipt template will be used if active.', - 'newspack-plugin' - ); - } - - return ( - <ActionCard - key={ email.post_id } - disabled={ inFlight } - title={ email.label } - titleLink={ email.edit_link } - href={ email.edit_link } - description={ email.description } - actionText={ __( 'Edit', 'newspack-plugin' ) } - secondaryActionText={ __( 'Reset', 'newspack-plugin' ) } - onSecondaryActionClick={ () => { - if ( - utils.confirmAction( - __( - 'Are you sure you want to reset the contents of this email?', - 'newspack-plugin' - ) - ) - ) { - resetEmail( email.post_id ); - } - } } - secondaryDestructive={ true } - toggleChecked={ isActive } - toggleOnChange={ value => updateStatus( email.post_id, value ? 'publish' : 'draft' ) } - { ...( isActive - ? {} - : { - notification, - notificationLevel: 'info', - } ) } - > - { error && ( - <Notice - noticeText={ error?.message || __( 'Something went wrong.', 'newspack-plugin' ) } - isError - /> - ) } - </ActionCard> - ); - } ) } - </> - ); -}; - -export default Emails; diff --git a/src/wizards/readerRevenue/views/index.js b/src/wizards/readerRevenue/views/index.js deleted file mode 100644 index c92ff44a8c..0000000000 --- a/src/wizards/readerRevenue/views/index.js +++ /dev/null @@ -1,6 +0,0 @@ -export { default as Donation } from './donation'; -export { default as NRHSettings } from './nrh-settings'; -export { default as Platform } from './platform'; -export { default as StripeSetup } from './payment-methods'; -export { default as Emails } from './emails'; -export { default as Salesforce } from './salesforce'; diff --git a/src/wizards/readerRevenue/views/payment-methods/additional-settings.js b/src/wizards/readerRevenue/views/payment-methods/additional-settings.js deleted file mode 100644 index 9884e1e84f..0000000000 --- a/src/wizards/readerRevenue/views/payment-methods/additional-settings.js +++ /dev/null @@ -1,105 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { CheckboxControl } from '@wordpress/components'; -import { useDispatch } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { - ActionCard, - Button, - Grid, - SectionHeader, - TextControl, - Wizard, -} from '../../../../components/src'; -import { READER_REVENUE_WIZARD_SLUG } from '../../constants'; -import './style.scss'; - -export const AdditionalSettings = ( { settings } ) => { - const { updateWizardSettings } = useDispatch( Wizard.STORE_NAMESPACE ); - const changeHandler = ( key, value ) => - updateWizardSettings( { - slug: READER_REVENUE_WIZARD_SLUG, - path: [ 'additional_settings', key ], - value, - } ); - - const { saveWizardSettings } = useDispatch( Wizard.STORE_NAMESPACE ); - const onSave = () => - saveWizardSettings( { - slug: READER_REVENUE_WIZARD_SLUG, - section: 'settings', - payloadPath: [ 'additional_settings' ], - } ); - - return ( - <> - <SectionHeader - title={ __( 'Additional Settings', 'newspack-plugin' ) } - description={ __( - 'Configure Newspack-exclusive settings.', - 'newspack-plugin' - ) } - /> - <ActionCard - isMedium - title={ __( 'Collect transaction fees', 'newspack-plugin' ) } - description={ __( 'Allow donors to optionally cover transaction fees imposed by payment processors.', 'newspack-plugin' ) } - notificationLevel="info" - toggleChecked={ settings.allow_covering_fees } - toggleOnChange={ () => { - changeHandler( 'allow_covering_fees', ! settings.allow_covering_fees ); - onSave(); - } } - hasGreyHeader={ settings.allow_covering_fees } - hasWhiteHeader={ ! settings.allow_covering_fees } - actionContent={ settings.allow_covering_fees && ( - <Button isPrimary onClick={ onSave }> - { __( 'Save Settings', 'newspack-plugin' ) } - </Button> - ) } - > - { settings.allow_covering_fees && ( - <Grid noMargin rowGap={ 16 }> - <TextControl - type="number" - step="0.1" - value={ settings.fee_multiplier } - label={ __( 'Fee multiplier', 'newspack-plugin' ) } - onChange={ value => changeHandler( 'fee_multiplier', value ) } - /> - <TextControl - type="number" - step="0.1" - value={ settings.fee_static } - label={ __( 'Fee static portion', 'newspack-plugin' ) } - onChange={ value => changeHandler( 'fee_static', value ) } - /> - <TextControl - value={ settings.allow_covering_fees_label } - label={ __( 'Custom message', 'newspack-plugin' ) } - placeholder={ __( - 'A message to explain the transaction fee option (optional).', - 'newspack-plugin' - ) } - onChange={ value => changeHandler( 'allow_covering_fees_label', value ) } - /> - <CheckboxControl - label={ __( 'Cover fees by default', 'newspack-plugin' ) } - checked={ settings.allow_covering_fees_default } - onChange={ () => changeHandler( 'allow_covering_fees_default', ! settings.allow_covering_fees_default ) } - help={ __( - 'If enabled, the option to cover the transaction fee will be checked by default.', - 'newspack-plugin' - ) } - /> - </Grid> - ) } - </ActionCard> - </> - ); -} \ No newline at end of file diff --git a/src/wizards/seo/index.js b/src/wizards/seo/index.js deleted file mode 100644 index 6b24e0ff5a..0000000000 --- a/src/wizards/seo/index.js +++ /dev/null @@ -1,153 +0,0 @@ -import '../../shared/js/public-path'; - -/** - * SEO - */ - -/** - * WordPress dependencies. - */ -import { Component, render, Fragment, createElement } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies. - */ -import { withWizard } from '../../components/src'; -import Router from '../../components/src/proxied-imports/router'; -import { Settings } from './views'; - -/** - * External dependencies. - */ -import camelCase from 'lodash/camelCase'; -import snakeCase from 'lodash/snakeCase'; - -const { HashRouter, Redirect, Route, Switch } = Router; - -/** - * Check whether the given object is a pure object with key/value pairs. - * - * @param {*} obj Object to check. - * @return {boolean} True if an object, otherwise false. - */ -const isObj = obj => - null !== obj && typeof obj === 'object' && Object.getPrototypeOf( obj ).isPrototypeOf( Object ); - -/** - * Recursively run the given `callback` on all keys of `obj`, and all keys of values of `obj`. - * - * @param {*} obj - * @param {Function} callback - * @return {*} Transformed obj. - */ -const deepMapKeys = ( obj, callback ) => { - if ( ! isObj( obj ) ) { - return obj; - } - const result = {}; - for ( const key in obj ) { - if ( obj.hasOwnProperty( key ) ) { - result[ callback( key ) ] = deepMapKeys( obj[ key ], callback ); - } - } - - return result; -}; - -class SEOWizard extends Component { - state = { - underConstruction: false, - urls: { - facebook: '', - twitter: '', - instagram: '', - youtube: '', - linkedin: '', - pinterest: '', - }, - verification: { - bing: '', - google: '', - }, - }; - - onWizardReady = () => this.fetch(); - - /** - * Get settings for the wizard. - */ - fetch() { - const { setError, wizardApiFetch } = this.props; - return wizardApiFetch( { - path: '/newspack/v1/wizard/newspack-seo-wizard/settings', - } ) - .then( response => this.setState( this.sanitizeResponse( response ) ) ) - .catch( error => setError( error ) ); - } - /** - * Update settings. - */ - update() { - const { setError, wizardApiFetch } = this.props; - return wizardApiFetch( { - path: '/newspack/v1/wizard/newspack-seo-wizard/settings', - method: 'POST', - data: deepMapKeys( this.state, key => snakeCase( key ) ), - quiet: true, - } ) - .then( response => this.setState( this.sanitizeResponse( response ) ) ) - .catch( error => setError( error ) ); - } - - /** - * Sanitize API response. - */ - sanitizeResponse = response => { - return deepMapKeys( response, key => camelCase( key ) ); - }; - - /** - * Render - */ - render() { - const { pluginRequirements } = this.props; - const headerText = __( 'SEO', 'newspack' ); - const subHeaderText = __( 'Configure basic SEO settings', 'newspack' ); - const buttonText = __( 'Save Settings', 'newspack' ); - const secondaryButtonText = __( 'Advanced Settings', 'newspack' ); - const screenParams = { - data: this.state, - headerText, - subHeaderText, - }; - return ( - <Fragment> - <HashRouter hashType="slash"> - <Switch> - { pluginRequirements } - <Route - exact - path="/" - render={ () => ( - <Settings - { ...screenParams } - buttonAction={ () => this.update() } - buttonText={ buttonText } - onChange={ settings => this.setState( settings ) } - secondaryButtonText={ secondaryButtonText } - /> - ) } - /> - <Redirect to="/" /> - </Switch> - </HashRouter> - </Fragment> - ); - } -} - -render( - createElement( withWizard( SEOWizard, [ 'wordpress-seo', 'jetpack' ] ) ), - document.getElementById( 'newspack-seo-wizard' ) -); diff --git a/src/wizards/seo/views/settings/index.js b/src/wizards/seo/views/settings/index.js deleted file mode 100644 index 76aae0d42d..0000000000 --- a/src/wizards/seo/views/settings/index.js +++ /dev/null @@ -1,127 +0,0 @@ -/** - * WordPress dependencies - */ -import { Component, Fragment } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { ExternalLink } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { - ActionCard, - Grid, - SectionHeader, - TextControl, - withWizardScreen, -} from '../../../../components/src'; - -/** - * SEO Settings screen. - */ -class Settings extends Component { - /** - * Render. - */ - render() { - const { data, onChange } = this.props; - const { verification, underConstruction, urls } = data; - const { google, bing } = verification; - const { facebook, linkedin, twitter, youtube, instagram, pinterest } = urls; - return ( - <> - <SectionHeader - title={ __( 'Webmaster Tools', 'newspack' ) } - description={ __( 'Add verification meta tags to your site', 'newspack' ) } - /> - <Grid> - <TextControl - label="Google" - onChange={ value => onChange( { verification: { ...verification, google: value } } ) } - value={ google } - help={ - <> - { __( 'Get your verification code in', 'newspack' ) + ' ' } - <ExternalLink href="https://www.google.com/webmasters/verification/verification?tid=alternate"> - { __( 'Google Search Console', 'newspack' ) } - </ExternalLink> - </> - } - /> - <TextControl - label="Bing" - onChange={ value => onChange( { verification: { ...verification, bing: value } } ) } - value={ bing } - help={ - <> - { __( 'Get your verification code in', 'newspack' ) + ' ' } - <ExternalLink href="https://www.bing.com/toolbox/webmaster/#/Dashboard/"> - { __( 'Bing Webmaster Tool', 'newspack' ) } - </ExternalLink> - </> - } - /> - </Grid> - <SectionHeader - title={ __( 'Social Accounts', 'newspack' ) } - description={ __( - 'Let search engines know which social profiles are associated to your site', - 'newspack' - ) } - /> - <Grid columns={ 1 } gutter={ 64 }> - <Grid columns={ 3 } rowGap={ 16 }> - <TextControl - label={ __( 'Facebook Page', 'newspack' ) } - onChange={ value => onChange( { urls: { ...urls, facebook: value } } ) } - value={ facebook } - placeholder={ __( 'https://facebook.com/page', 'newspack' ) } - /> - <TextControl - label={ __( 'Twitter', 'newspack' ) } - onChange={ value => onChange( { urls: { ...urls, twitter: value } } ) } - value={ twitter } - placeholder={ __( 'username', 'newspack' ) } - /> - <TextControl - label="Instagram" - onChange={ value => onChange( { urls: { ...urls, instagram: value } } ) } - value={ instagram } - placeholder={ __( 'https://instagram.com/user', 'newspack' ) } - /> - <TextControl - label="LinkedIn" - onChange={ value => onChange( { urls: { ...urls, linkedin: value } } ) } - value={ linkedin } - placeholder={ __( 'https://linkedin.com/user', 'newspack' ) } - /> - <TextControl - label="YouTube" - onChange={ value => onChange( { urls: { ...urls, youtube: value } } ) } - value={ youtube } - placeholder={ __( 'https://youtube.com/c/channel', 'newspack' ) } - /> - <TextControl - label="Pinterest" - onChange={ value => onChange( { urls: { ...urls, pinterest: value } } ) } - value={ pinterest } - placeholder={ __( 'https://pinterest.com/user', 'newspack' ) } - /> - </Grid> - <ActionCard - isMedium - title={ __( 'Under construction', 'newspack' ) } - description={ __( 'Discourage search engines from indexing this site.', 'newspack' ) } - toggleChecked={ underConstruction } - toggleOnChange={ value => onChange( { underConstruction: value } ) } - /> - </Grid> - </> - ); - } -} -Settings.defaultProps = { - data: {}, -}; - -export default withWizardScreen( Settings ); diff --git a/src/wizards/setup/index.js b/src/wizards/setup/index.js index 9f48131e18..434bef0592 100644 --- a/src/wizards/setup/index.js +++ b/src/wizards/setup/index.js @@ -10,7 +10,7 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies. */ import { Welcome, Settings, Services, Design, Completed } from './views/'; -import { withWizard, Notice } from '../../components/src'; +import { withWizard, withWizardScreen, Notice } from '../../components/src'; import Router from '../../components/src/proxied-imports/router'; import './style.scss'; @@ -39,7 +39,7 @@ const ROUTES = [ path: '/design', label: __( 'Design', 'newspack' ), subHeaderText: __( 'Customize the look and feel of your site', 'newspack' ), - render: Design, + render: withWizardScreen( Design, { hidePrimaryButton: true } ), }, { path: '/completed', @@ -82,6 +82,7 @@ const SetupWizard = ( { wizardApiFetch, setError } ) => { subHeaderText: route.subHeaderText, buttonText: nextRoute ? route.buttonText || __( 'Continue' ) : __( 'Finish' ), buttonAction, + isPartOfSetup: true } ) } /> diff --git a/src/wizards/setup/style.scss b/src/wizards/setup/style.scss index e1d8ca47a8..e701ab6a06 100644 --- a/src/wizards/setup/style.scss +++ b/src/wizards/setup/style.scss @@ -51,6 +51,7 @@ .newspack--error, .newspack-checkbox-icon { + line-height: 24px; margin-right: 8px; } } diff --git a/src/wizards/setup/views/index.js b/src/wizards/setup/views/index.js index e2fa5481d1..ebaf76b8cf 100644 --- a/src/wizards/setup/views/index.js +++ b/src/wizards/setup/views/index.js @@ -1,5 +1,5 @@ export { default as Welcome } from './welcome'; export { default as Settings } from './settings'; export { default as Services } from './services'; -export { default as Design } from '../../site-design/views/main'; +export { default as Design } from '../../newspack/views/settings/theme-and-brand'; export { default as Completed } from './completed'; diff --git a/src/wizards/setup/views/services/ReaderRevenue.js b/src/wizards/setup/views/services/ReaderRevenue.js index dc48df0436..a01dc67502 100644 --- a/src/wizards/setup/views/services/ReaderRevenue.js +++ b/src/wizards/setup/views/services/ReaderRevenue.js @@ -12,12 +12,13 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import Platform from '../../../readerRevenue/views/platform'; -import { DonationAmounts } from '../../../readerRevenue/views/donation'; +import Platform from '../../../audience/components/platform'; +import { DonationAmounts } from '../../../audience/views/donations/configuration'; import { Wizard } from '../../../../components/src'; +import { AUDIENCE_DONATIONS_WIZARD_SLUG } from '../../../audience/constants'; const ReaderRevenue = ( { className } ) => { - const wizardData = Wizard.useWizardData( 'reader-revenue' ); + const wizardData = Wizard.useWizardData( AUDIENCE_DONATIONS_WIZARD_SLUG ); return ( <div className={ classnames( className, { 'o-50': isEmpty( wizardData ) } ) }> <Platform /> diff --git a/src/wizards/setup/views/services/index.js b/src/wizards/setup/views/services/index.js index 5f39d8381b..76919e6d36 100644 --- a/src/wizards/setup/views/services/index.js +++ b/src/wizards/setup/views/services/index.js @@ -18,8 +18,9 @@ import apiFetch from '@wordpress/api-fetch'; */ import { withWizardScreen, Wizard, ActionCard, hooks } from '../../../../components/src'; import ReaderRevenue from './ReaderRevenue'; -import { NewspackNewsletters } from '../../../engagement/views/newsletters'; +import { Settings as NewslettersSettings } from '../../../newsletters/views/settings'; import GAMOnboarding from '../../../advertising/components/onboarding'; +import { AUDIENCE_DONATIONS_WIZARD_SLUG } from '../../../audience/constants'; import './style.scss'; const SERVICES_LIST = { @@ -38,7 +39,7 @@ const SERVICES_LIST = { 'Create email newsletters and send them to your mail lists, all without leaving your website', 'newspack' ), - Component: NewspackNewsletters, + Component: NewslettersSettings, configuration: { is_service_enabled: false }, }, 'google-ad-manager': { @@ -56,7 +57,7 @@ const Services = ( { renderPrimaryButton } ) => { const [ services, updateServices ] = hooks.useObjectState( SERVICES_LIST ); const [ isLoading, setIsLoading ] = useState( true ); const slugs = keys( services ); - const readerRevenueWizardData = Wizard.useWizardData( 'reader-revenue' ); + const wizardData = Wizard.useWizardData( AUDIENCE_DONATIONS_WIZARD_SLUG ); useEffect( () => { apiFetch( { @@ -72,7 +73,7 @@ const Services = ( { renderPrimaryButton } ) => { // Add Reader Revenue Wizard data straight from the Wizard. data[ 'reader-revenue' ] = { ...data[ 'reader-revenue' ], - ...readerRevenueWizardData, + ...wizardData, }; return apiFetch( { path: '/newspack/v1/wizard/newspack-setup-wizard/services', diff --git a/src/wizards/site-design/index.js b/src/wizards/site-design/index.js deleted file mode 100644 index 38649d5512..0000000000 --- a/src/wizards/site-design/index.js +++ /dev/null @@ -1,148 +0,0 @@ -import '../../shared/js/public-path'; - -/** - * WordPress dependencies. - */ -import { Component, render, Fragment, createElement } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies. - */ -import { withWizard, utils } from '../../components/src'; -import Router from '../../components/src/proxied-imports/router'; -import { ThemeSettings, Main } from './views'; - -const { HashRouter, Redirect, Route, Switch } = Router; - -/** - * Site Design Wizard. - */ -class SiteDesignWizard extends Component { - componentDidMount = () => { - const { setError, wizardApiFetch } = this.props; - const params = { - path: '/newspack/v1/wizard/newspack-setup-wizard/theme', - method: 'GET', - }; - wizardApiFetch( params ) - .then( response => - this.setState( { themeSettings: { ...response.theme_mods, ...response.etc } } ) - ) - .catch( setError ); - }; - - setThemeMods = themeModUpdates => - this.setState( { themeSettings: { ...this.state.themeSettings, ...themeModUpdates } } ); - - updateThemeSettings = () => { - const { setError, wizardApiFetch } = this.props; - const { themeSettings } = this.state; - - // Warn user before overwriting existing posts. - if ( - ( themeSettings.featured_image_all_posts && - themeSettings.featured_image_all_posts !== 'none' ) || - ( themeSettings.post_template_all_posts && themeSettings.post_template_all_posts !== 'none' ) - ) { - if ( - ! utils.confirmAction( - __( - 'Saving will overwrite existing posts, this cannot be undone. Are you sure you want to proceed?', - 'newspack-plugin' - ) - ) - ) { - return; - } - } - - const params = { - path: '/newspack/v1/wizard/newspack-setup-wizard/theme/', - method: 'POST', - data: { theme_mods: themeSettings }, - quiet: true, - }; - wizardApiFetch( params ) - .then( response => { - const { theme, theme_mods } = response; - this.setState( { theme, themeSettings: theme_mods } ); - } ) - .catch( error => setError( { error } ) ); - }; - - state = {}; - - /** - * Render - */ - render() { - const { pluginRequirements, wizardApiFetch, setError } = this.props; - const tabbedNavigation = [ - { - label: __( 'Design' ), - path: '/', - exact: true, - }, - { - label: __( 'Settings' ), - path: '/settings', - exact: true, - }, - ]; - return ( - <Fragment> - <HashRouter hashType="slash"> - <Switch> - { pluginRequirements } - <Route - path="/" - exact - render={ () => { - return ( - <Main - headerText={ __( 'Site Design', 'newspack-plugin' ) } - subHeaderText={ __( - 'Customize the look and feel of your site', - 'newspack-plugin' - ) } - tabbedNavigation={ tabbedNavigation } - wizardApiFetch={ wizardApiFetch } - setError={ setError } - isPartOfSetup={ false } - /> - ); - } } - /> - <Route - path="/settings" - exact - render={ () => { - const { themeSettings } = this.state; - return ( - <ThemeSettings - headerText={ __( 'Site Design', 'newspack-plugin' ) } - subHeaderText={ __( 'Configure your Newspack theme', 'newspack-plugin' ) } - tabbedNavigation={ tabbedNavigation } - themeSettings={ themeSettings } - setThemeMods={ this.setThemeMods } - buttonText={ __( 'Save', 'newspack-plugin' ) } - buttonAction={ this.updateThemeSettings } - secondaryButtonText={ __( 'Advanced Settings', 'newspack-plugin' ) } - secondaryButtonAction="/wp-admin/customize.php" - /> - ); - } } - /> - <Redirect to="/" /> - </Switch> - </HashRouter> - </Fragment> - ); - } -} - -render( - createElement( withWizard( SiteDesignWizard ) ), - document.getElementById( 'newspack-site-design-wizard' ) -); diff --git a/src/wizards/site-design/views/index.js b/src/wizards/site-design/views/index.js deleted file mode 100644 index 6027d549e9..0000000000 --- a/src/wizards/site-design/views/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as Main } from './main'; -export { default as ThemeSettings } from './theme-settings'; diff --git a/src/wizards/site-design/views/main/index.js b/src/wizards/site-design/views/main/index.js deleted file mode 100644 index 9f5701e583..0000000000 --- a/src/wizards/site-design/views/main/index.js +++ /dev/null @@ -1,405 +0,0 @@ -/** - * WordPress dependencies - */ -import { useEffect, useState } from '@wordpress/element'; -import { alignCenter, alignLeft } from '@wordpress/icons'; -import { __ } from '@wordpress/i18n'; -import { TextareaControl, ToggleControl } from '@wordpress/components'; - -/** - * External dependencies - */ -import omit from 'lodash/omit'; - -/** - * Internal dependencies - */ -import { - ButtonCard, - Card, - ColorPicker, - Grid, - ImageUpload, - SectionHeader, - SelectControl, - StyleCard, - TextControl, - WebPreview, - hooks, - withWizardScreen, -} from '../../../../components/src'; -import ThemeSelection from '../../components/theme-selection'; -import { - getFontsList, - isFontInOptions, - getFontImportURL, - LOGO_SIZE_OPTIONS, - parseLogoSize, -} from './utils'; -import './style.scss'; - -const TYPOGRAPHY_OPTIONS = [ - { value: 'curated', label: __( 'Default', 'newspack' ) }, - { value: 'custom', label: __( 'Custom', 'newspack' ) }, -]; - -const Main = ( { wizardApiFetch, setError, renderPrimaryButton, isPartOfSetup = true } ) => { - const [ themeSlug, updateThemeSlug ] = useState(); - const [ homepagePatterns, updateHomepagePatterns ] = useState( [] ); - const [ mods, updateMods ] = hooks.useObjectState(); - const [ typographyOptionsType, updateTypographyOptionsType ] = useState( - TYPOGRAPHY_OPTIONS[ 0 ].value - ); - - const finishSetup = () => { - const params = { - path: `/newspack/v1/wizard/newspack-setup-wizard/complete`, - method: 'POST', - quiet: true, - }; - wizardApiFetch( params ).catch( setError ); - }; - - const isDisplayingHomepageLayoutPicker = isPartOfSetup && homepagePatterns.length > 0; - - const updateSettings = response => { - updateMods( response.theme_mods ); - updateThemeSlug( response.theme ); - updateHomepagePatterns( response.homepage_patterns ); - const { font_header: headerFont, font_body: bodyFont } = response.theme_mods; - if ( - ( headerFont && ! isFontInOptions( headerFont ) ) || - ( bodyFont && ! isFontInOptions( bodyFont ) ) - ) { - updateTypographyOptionsType( TYPOGRAPHY_OPTIONS[ 1 ].value ); - } - }; - useEffect( () => { - wizardApiFetch( { - path: '/newspack/v1/wizard/newspack-setup-wizard/theme', - } ) - .then( updateSettings ) - .catch( setError ); - }, [] ); - - const saveSettings = () => - wizardApiFetch( { - path: '/newspack/v1/wizard/newspack-setup-wizard/theme/', - method: 'POST', - data: { - theme_mods: omit( - mods, - isDisplayingHomepageLayoutPicker ? [] : [ 'homepage_pattern_index' ] - ), - theme: themeSlug, - }, - quiet: true, - } ) - .then( updateSettings ) - .catch( setError ); - - const renderCustomFontChoice = type => { - const isHeadings = type === 'headings'; - const label = isHeadings ? __( 'Headings', 'newspack' ) : __( 'Body', 'newspack' ); - return ( - <Grid columns={ 1 } gutter={ 16 }> - <TextareaControl - label={ label + ' - ' + __( 'Font provider import code or URL', 'newspack' ) } - placeholder={ - 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,700;1,400;1,700&display=swap' - } - value={ - isHeadings ? mods.custom_font_import_code : mods.custom_font_import_code_alternate - } - onChange={ updateMods( - isHeadings ? 'custom_font_import_code' : 'custom_font_import_code_alternate' - ) } - rows={ 3 } - /> - <TextControl - label={ label + ' - ' + __( 'Font name', 'newspack' ) } - value={ isHeadings ? mods.font_header : mods.font_body } - onChange={ updateMods( isHeadings ? 'font_header' : 'font_body' ) } - /> - <SelectControl - label={ label + ' - ' + __( 'Font fallback stack', 'newspack' ) } - options={ [ - { value: 'serif', label: __( 'Serif', 'newspack' ) }, - { value: 'sans-serif', label: __( 'Sans Serif', 'newspack' ) }, - { value: 'display', label: __( 'Display', 'newspack' ) }, - { value: 'monospace', label: __( 'Monospace', 'newspack' ) }, - ] } - value={ isHeadings ? mods.font_header_stack : mods.font_body_stack } - onChange={ updateMods( isHeadings ? 'font_header_stack' : 'font_body_stack' ) } - /> - </Grid> - ); - }; - - return ( - <Card noBorder className="newspack-design"> - { ! isPartOfSetup && ( - <> - <SectionHeader - title={ __( 'Theme', 'newspack' ) } - description={ __( 'Select the theme for your site', 'newspack' ) } - /> - <ThemeSelection theme={ themeSlug } updateTheme={ updateThemeSlug } /> - </> - ) } - { isDisplayingHomepageLayoutPicker ? ( - <> - <SectionHeader - title={ __( 'Homepage', 'newspack' ) } - description={ __( 'Select a homepage layout', 'newspack' ) } - className="newspack-design__header" - /> - <Grid columns={ 6 } gutter={ 16 }> - { homepagePatterns.map( ( pattern, i ) => ( - <StyleCard - key={ i } - image={ { __html: pattern.image } } - imageType="html" - isActive={ i === mods.homepage_pattern_index } - onClick={ () => updateMods( { homepage_pattern_index: i } ) } - ariaLabel={ __( 'Activate Layout', 'newspack' ) + ' ' + ( i + 1 ) } - /> - ) ) } - </Grid> - </> - ) : null } - <SectionHeader - title={ __( 'Colors', 'newspack' ) } - description={ __( 'Pick your primary and secondary colors', 'newspack' ) } - /> - <Grid gutter={ 32 }> - { /* This UI does not enable setting 'theme_colors' to 'default'. As soon as a color is picked, 'theme_colors' will be 'custom'. */ } - <ColorPicker - label={ __( 'Primary' ) } - color={ mods.primary_color_hex } - onChange={ primary_color_hex => - updateMods( { primary_color_hex, theme_colors: 'custom' } ) - } - /> - <ColorPicker - label={ __( 'Secondary' ) } - color={ mods.secondary_color_hex } - onChange={ secondary_color_hex => - updateMods( { secondary_color_hex, theme_colors: 'custom' } ) - } - /> - </Grid> - <SectionHeader - title={ __( 'Typography', 'newspack' ) } - description={ __( 'Define the font pairing to use throughout your site', 'newspack' ) } - /> - <Grid columns={ 1 } gutter={ 16 }> - <SelectControl - label={ __( 'Typography Options', 'newspack' ) } - hideLabelFromVision - value={ typographyOptionsType ? typographyOptionsType : 'curated' } - onChange={ updateTypographyOptionsType } - buttonOptions={ TYPOGRAPHY_OPTIONS } - /> - <Grid gutter={ 32 }> - { typographyOptionsType === 'curated' ? ( - <> - <SelectControl - label={ __( 'Headings', 'newspack' ) } - optgroups={ getFontsList( true ) } - value={ mods.font_header } - onChange={ ( value, group ) => - updateMods( { - font_header: value, - custom_font_import_code: getFontImportURL( value ), - font_header_stack: group?.fallback, - } ) - } - /> - <SelectControl - label={ __( 'Body', 'newspack' ) } - optgroups={ getFontsList() } - value={ mods.font_body } - onChange={ ( value, group ) => - updateMods( { - font_body: value, - custom_font_import_code_alternate: getFontImportURL( value ), - font_body_stack: group?.fallback, - } ) - } - /> - </> - ) : ( - <> - { renderCustomFontChoice( 'headings' ) } - { renderCustomFontChoice( 'body' ) } - </> - ) } - </Grid> - <ToggleControl - checked={ mods.accent_allcaps === true } - onChange={ updateMods( 'accent_allcaps' ) } - label={ __( 'Use all-caps for accent text', 'newspack' ) } - /> - </Grid> - <SectionHeader - title={ __( 'Header', 'newspack' ) } - description={ __( 'Update the header and add your logo', 'newspack' ) } - className="newspack-design__header" - /> - <Grid gutter={ 32 }> - <Grid columns={ 1 } gutter={ 16 }> - <Grid gutter={ 16 } className="newspack-design__header__style-size"> - <SelectControl - className="icon-only" - label={ __( 'Style', 'newspack' ) } - value={ mods.header_center_logo ? 'center' : 'left' } - onChange={ value => updateMods( 'header_center_logo' )( value === 'center' ) } - buttonOptions={ [ - { value: 'left', icon: alignLeft }, - { value: 'center', icon: alignCenter }, - ] } - /> - <SelectControl - className="icon-only" - label={ __( 'Size', 'newspack' ) } - value={ mods.header_simplified ? 'small' : 'large' } - onChange={ value => updateMods( 'header_simplified' )( value === 'small' ) } - buttonOptions={ [ - { value: 'small', label: 'S' }, - { value: 'large', label: 'L' }, - ] } - /> - </Grid> - <ToggleControl - checked={ mods.header_solid_background } - onChange={ updateMods( 'header_solid_background' ) } - label={ __( 'Apply a background color to the header', 'newspack' ) } - /> - { mods.header_solid_background && ( - <ColorPicker - label={ __( 'Background color' ) } - color={ mods.header_color_hex } - onChange={ updateMods( 'header_color_hex' ) } - /> - ) } - </Grid> - <Grid columns={ 1 } gutter={ 16 }> - <ImageUpload - className="newspack-design__header__logo" - style={ { - ...( mods.header_solid_background - ? { - backgroundColor: mods.header_color_hex, - } : {} ), - } } - label={ __( 'Logo', 'newspack' ) } - image={ mods.custom_logo } - onChange={ custom_logo => - updateMods( { - custom_logo, - header_text: ! custom_logo, - header_display_tagline: ! custom_logo, - } ) - } - /> - { mods.custom_logo && ( - <SelectControl - className="icon-only" - label={ __( 'Logo Size', 'newspack' ) } - value={ parseLogoSize( mods.logo_size ) } - onChange={ updateMods( 'logo_size' ) } - buttonOptions={ LOGO_SIZE_OPTIONS } - /> - ) } - </Grid> - </Grid> - <SectionHeader - title={ __( 'Footer', 'newspack' ) } - description={ __( 'Personalize the footer of your site', 'newspack' ) } - className="newspack-design__footer" - /> - <Grid gutter={ 32 }> - <Grid columns={ 1 } gutter={ 16 }> - <Card noBorder className="newspack-design__footer__copyright"> - <TextControl - label={ __( 'Copyright information', 'newspack' ) } - value={ mods.footer_copyright || '' } - onChange={ updateMods( 'footer_copyright' ) } - /> - </Card> - <ToggleControl - checked={ mods.footer_color !== 'default' } - onChange={ checked => updateMods( 'footer_color' )( checked ? 'custom' : 'default' ) } - label={ __( 'Apply a background color to the footer', 'newspack' ) } - /> - { mods.footer_color === 'custom' && ( - <ColorPicker - label={ __( 'Background color' ) } - color={ mods.footer_color_hex } - onChange={ updateMods( 'footer_color_hex' ) } - /> - ) } - </Grid> - <Grid columns={ 1 } gutter={ 16 }> - <ImageUpload - className="newspack-design__footer__logo" - label={ __( 'Alternative Logo', 'newspack' ) } - help={ __( 'Optional alternative logo to be displayed in the footer.', 'newspack' ) } - style={ { - ...( mods.footer_color === 'custom' && mods.footer_color_hex - ? { backgroundColor: mods.footer_color_hex } - : {} ), - } } - image={ mods.newspack_footer_logo } - onChange={ updateMods( 'newspack_footer_logo' ) } - /> - { mods.newspack_footer_logo && ( - <SelectControl - className="icon-only" - label={ __( 'Alternative logo - Size', 'newspack' ) } - value={ mods.footer_logo_size } - onChange={ updateMods( 'footer_logo_size' ) } - buttonOptions={ [ - { value: 'small', label: 'S' }, - { value: 'medium', label: 'M' }, - { value: 'large', label: 'L' }, - { value: 'xlarge', label: 'XL' }, - ] } - /> - ) } - </Grid> - </Grid> - { isPartOfSetup && ( - <div className="newspack-floating-button"> - <WebPreview - url="/?newspack_design_preview" - renderButton={ ( { showPreview } ) => ( - <ButtonCard - onClick={ () => saveSettings().then( showPreview ) } - title={ __( 'Preview', 'newspack' ) } - desc={ __( 'See how your site looks like', 'newspack' ) } - chevron - isSmall - /> - ) } - /> - </div> - ) } - <div className="newspack-buttons-card"> - { renderPrimaryButton( - isPartOfSetup - ? { - onClick: () => saveSettings().then( finishSetup ), - children: __( 'Finish', 'newspack' ), - } : { - onClick: () => saveSettings(), - children: __( 'Save', 'newspack' ), - } - ) } - </div> - </Card> - ); -}; - -export default withWizardScreen( Main, { hidePrimaryButton: true } ); diff --git a/src/wizards/site-design/views/main/style.scss b/src/wizards/site-design/views/main/style.scss deleted file mode 100644 index a4c5753cd2..0000000000 --- a/src/wizards/site-design/views/main/style.scss +++ /dev/null @@ -1,45 +0,0 @@ -.newspack-floating-button { - z-index: 9997; - position: fixed; - bottom: 16px; - right: 16px; - - .newspack-button-card { - margin: 0; - position: relative; - z-index: 1; - - &::before { - border-radius: 2px; - box-shadow: 0 0 8px 4px rgba(black, 0.08); - content: ""; - display: block; - inset: 0; - position: absolute; - transition: box-shadow 125ms ease-in-out; - z-index: -1; - } - - &:hover::before { - box-shadow: 0 0 8px 4px rgba(black, 0.16); - } - } -} - -.newspack-design { - &__header, - &__footer { - &__logo { - .newspack-image-upload__image { - border-color: rgba(black, 0.54); - height: 98px; - } - } - } - - &__header { - &__style-size { - grid-template-columns: repeat(2, min-content); - } - } -} diff --git a/src/wizards/site-design/views/theme-settings/index.js b/src/wizards/site-design/views/theme-settings/index.js deleted file mode 100644 index d64f6f39b4..0000000000 --- a/src/wizards/site-design/views/theme-settings/index.js +++ /dev/null @@ -1,249 +0,0 @@ -/** - * WordPress dependencies - */ -import { Fragment, useEffect, useState } from '@wordpress/element'; -import { SelectControl, Notice, ToggleControl } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { - Grid, - ImageUpload, - SectionHeader, - TextControl, - withWizardScreen, -} from '../../../../components/src'; -import './style.scss'; - -/** - * Theme Settings Screen. - */ -const ThemeSettings = props => { - const [ imageThumbnail, setImageThumbnail ] = useState( null ); - const { themeSettings, setThemeMods } = props; - - const { - show_author_bio: authorBio = true, - show_author_email: authorEmail = false, - author_bio_length: authorBioLength = 200, - featured_image_default: featuredImageDefault = 'large', - post_template_default: postTemplateDefault = 'default', - featured_image_all_posts: featuredImageAllPosts = 'none', - post_template_all_posts: postTemplateAllPosts = 'none', - newspack_image_credits_placeholder_url: imageCreditsPlaceholderUrl, - newspack_image_credits_class_name: imageCreditsClassName = '', - newspack_image_credits_prefix_label: imageCreditsPrefix = '', - newspack_image_credits_auto_populate: imageCreditsAutoPopulate = false, - } = themeSettings; - - useEffect( () => { - if ( imageCreditsPlaceholderUrl ) { - setImageThumbnail( imageCreditsPlaceholderUrl ); - } - }, [ imageCreditsPlaceholderUrl ] ); - - return ( - <Fragment> - <SectionHeader - title={ __( 'Author Bio', 'newspack-plugin' ) } - description={ __( 'Control how author bios are displayed on posts.', 'newspack-plugin' ) } - /> - <Grid gutter={ 32 }> - <Grid columns={ 1 } gutter={ 16 }> - <ToggleControl - label={ __( 'Author Bio', 'newspack-plugin' ) } - help={ __( 'Display an author bio under individual posts.', 'newspack-plugin' ) } - checked={ authorBio } - onChange={ value => setThemeMods( { show_author_bio: value } ) } - /> - { authorBio && ( - <ToggleControl - label={ __( 'Author Email', 'newspack-plugin' ) } - help={ __( - 'Display the author email with bio on individual posts.', - 'newspack-plugin' - ) } - checked={ authorEmail } - onChange={ value => setThemeMods( { show_author_email: value } ) } - /> - ) } - </Grid> - <Grid columns={ 1 } gutter={ 16 }> - { authorBio && ( - <TextControl - label={ __( 'Length', 'newspack-plugin' ) } - help={ __( - 'Truncates the author bio on single posts to this approximate character length, but without breaking a word. The full bio appears on the author archive page.', - 'newspack-plugin' - ) } - type="number" - value={ authorBioLength } - onChange={ value => setThemeMods( { author_bio_length: value } ) } - /> - ) } - </Grid> - </Grid> - - <SectionHeader - title={ __( 'Default Featured Image Position And Post Template', 'newspack-plugin' ) } - description={ __( - 'Modify how the featured image and post template settings are applied to new posts.', - 'newspack-plugin' - ) } - /> - <Grid gutter={ 32 }> - <SelectControl - label={ __( 'Default featured image position for new posts', 'newspack-plugin' ) } - help={ __( 'Set a default featured image position for new posts.', 'newspack-plugin' ) } - value={ featuredImageDefault } - options={ [ - { label: __( 'Large', 'newspack-plugin' ), value: 'large' }, - { label: __( 'Small', 'newspack-plugin' ), value: 'small' }, - { label: __( 'Behind article title', 'newspack-plugin' ), value: 'behind' }, - { label: __( 'Beside article title', 'newspack-plugin' ), value: 'beside' }, - { label: __( 'Hidden', 'newspack-plugin' ), value: 'hidden' }, - ] } - onChange={ value => setThemeMods( { featured_image_default: value } ) } - /> - <SelectControl - label={ __( 'Default template for new posts', 'newspack-plugin' ) } - help={ __( 'Set a default template for new posts.', 'newspack-plugin' ) } - value={ postTemplateDefault } - options={ [ - { label: __( 'With sidebar', 'newspack-plugin' ), value: 'default' }, - { label: __( 'One Column', 'newspack-plugin' ), value: 'single-feature.php' }, - { label: __( 'One Column Wide', 'newspack-plugin' ), value: 'single-wide.php' }, - ] } - onChange={ value => setThemeMods( { post_template_default: value } ) } - /> - </Grid> - <SectionHeader - title={ __( 'Featured Image Position And Post Template For All Posts', 'newspack-plugin' ) } - description={ __( - 'Modify how the featured image and post template settings are applied to existing posts. Warning: saving these options will override all posts.', - 'newspack-plugin' - ) } - /> - { themeSettings.post_count > 1000 && ( - <Notice isDismissible={ false } status="warning" className="ma0 mb2"> - { __( - 'You have more than 1000 posts. Applying these settings might take a moment.', - 'newspack-plugin' - ) } - </Notice> - ) } - <Grid gutter={ 32 }> - <div> - <SelectControl - label={ __( 'Featured image position for all posts', 'newspack-plugin' ) } - help={ __( 'Set a featured image position for all posts.', 'newspack-plugin' ) } - value={ featuredImageAllPosts } - options={ [ - { label: __( 'Select to change all posts', 'newspack-plugin' ), value: 'none' }, - { label: __( 'Large', 'newspack-plugin' ), value: 'large' }, - { label: __( 'Small', 'newspack-plugin' ), value: 'small' }, - { label: __( 'Behind article title', 'newspack-plugin' ), value: 'behind' }, - { label: __( 'Beside article title', 'newspack-plugin' ), value: 'beside' }, - { label: __( 'Hidden', 'newspack-plugin' ), value: 'hidden' }, - ] } - onChange={ value => setThemeMods( { featured_image_all_posts: value } ) } - /> - { featuredImageAllPosts !== 'none' && ( - <Notice isDismissible={ false } status="warning" className="ma0 mt2"> - { __( - 'After saving the settings with this option selected, all posts will be updated. This cannot be undone.', - 'newspack-plugin' - ) } - </Notice> - ) } - </div> - - <div> - <SelectControl - label={ __( 'Template for all posts', 'newspack-plugin' ) } - help={ __( 'Set a template for all posts.', 'newspack-plugin' ) } - value={ postTemplateAllPosts } - options={ [ - { label: __( 'Select to change all posts', 'newspack-plugin' ), value: 'none' }, - { label: __( 'With sidebar', 'newspack-plugin' ), value: 'default' }, - { label: __( 'One Column', 'newspack-plugin' ), value: 'single-feature.php' }, - { label: __( 'One Column Wide', 'newspack-plugin' ), value: 'single-wide.php' }, - ] } - onChange={ value => setThemeMods( { post_template_all_posts: value } ) } - /> - { postTemplateAllPosts !== 'none' && ( - <Notice isDismissible={ false } status="warning" className="ma0 mt2"> - { __( - 'After saving the settings with this option selected, all posts will be updated. This cannot be undone.', - 'newspack-plugin' - ) } - </Notice> - ) } - </div> - </Grid> - - <SectionHeader - title={ __( 'Media Credits', 'newspack-plugin' ) } - description={ __( - 'Control how credits are displayed alongside media attachments.', - 'newspack-plugin' - ) } - /> - <Grid gutter={ 32 }> - <Grid columns={ 1 } gutter={ 16 }> - <TextControl - label={ __( 'Credit Class Name', 'newspack-plugin' ) } - help={ __( - 'A CSS class name to be applied to all image credit elements. Leave blank to display no class name.', - 'newspack-plugin' - ) } - value={ imageCreditsClassName } - onChange={ value => setThemeMods( { newspack_image_credits_class_name: value } ) } - /> - <TextControl - label={ __( 'Credit Label', 'newspack-plugin' ) } - help={ __( - 'A label to prefix all media credits. Leave blank to display no prefix.', - 'newspack-plugin' - ) } - value={ imageCreditsPrefix } - onChange={ value => setThemeMods( { newspack_image_credits_prefix_label: value } ) } - /> - </Grid> - <Grid columns={ 1 } gutter={ 16 }> - <ImageUpload - image={ imageThumbnail ? { url: imageThumbnail } : null } - label={ __( 'Placeholder Image', 'newspack-plugin' ) } - buttonLabel={ __( 'Select', 'newspack-plugin' ) } - onChange={ image => { - setImageThumbnail( image?.url || null ); - setThemeMods( { newspack_image_credits_placeholder: image?.id || null } ); - } } - help={ __( - 'A placeholder image to be displayed in place of images without credits. If none is chosen, the image will be displayed normally whether or not it has a credit.', - 'newspack-plugin' - ) } - /> - <ToggleControl - label={ __( 'Auto-populate image credits', 'newspack-plugin' ) } - help={ __( - 'Automatically populate image credits from EXIF or IPTC metadata when uploading new images.', - 'newspack-plugin' - ) } - checked={ imageCreditsAutoPopulate } - onChange={ value => setThemeMods( { newspack_image_credits_auto_populate: value } ) } - /> - </Grid> - </Grid> - </Fragment> - ); -}; - -ThemeSettings.defaultProps = { - themeSettings: {}, - setThemeMods: () => null, -}; - -export default withWizardScreen( ThemeSettings ); diff --git a/src/wizards/site-design/views/theme-settings/style.scss b/src/wizards/site-design/views/theme-settings/style.scss deleted file mode 100644 index 0599447ad7..0000000000 --- a/src/wizards/site-design/views/theme-settings/style.scss +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Theme Selection Screen - */ - -.newspack-toggle-group { - + .newspack-text-control { - margin-top: -16px; - } - - > .newspack-toggle-control { - margin: 0; - } - - .newspack-toggle-group__description { - .newspack-toggle-control, - .newspack-text-control { - margin: 8px 0 0 -44px; - } - } -} diff --git a/src/wizards/syndication/index.js b/src/wizards/syndication/index.js deleted file mode 100644 index 20dd4637c0..0000000000 --- a/src/wizards/syndication/index.js +++ /dev/null @@ -1,36 +0,0 @@ -import '../../shared/js/public-path'; - -/** - * Syndication - */ - -/** - * WordPress dependencies. - */ -import { render, createElement } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies. - */ -import { Wizard } from '../../components/src'; -import { Intro } from './views'; - -const SyndicationWizard = () => ( - <Wizard - headerText={ __( 'Syndication', 'newspack' ) } - subHeaderText={ __( 'Distribute your content across multiple websites', 'newspack' ) } - sections={ [ - { - label: __( 'Main', 'newspack' ), - path: '/', - render: Intro, - }, - ] } - /> -); - -render( - createElement( SyndicationWizard ), - document.getElementById( 'newspack-syndication-wizard' ) -); diff --git a/src/wizards/syndication/views/index.js b/src/wizards/syndication/views/index.js deleted file mode 100644 index d92bbe045a..0000000000 --- a/src/wizards/syndication/views/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as Intro } from './intro'; diff --git a/src/wizards/syndication/views/intro/index.js b/src/wizards/syndication/views/intro/index.js deleted file mode 100644 index 05120b18ba..0000000000 --- a/src/wizards/syndication/views/intro/index.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { useDispatch } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { PluginToggle, ActionCard, Wizard } from '../../../../components/src'; - -const Intro = () => { - const settingsData = Wizard.useWizardData( 'settings' ); - const { saveWizardSettings } = useDispatch( Wizard.STORE_NAMESPACE ); - return ( - <> - <ActionCard - title={ __( 'RSS Enhancements', 'newspack' ) } - description={ __( - 'Create and manage customized RSS feeds for syndication partners', - 'newspack' - ) } - toggleChecked={ Boolean( settingsData.module_enabled_rss ) } - toggleOnChange={ value => { - saveWizardSettings( { - slug: 'newspack-settings-wizard', - updatePayload: { - path: [ 'module_enabled_rss' ], - value, - }, - } ).then( () => { - window.location.reload( true ); - } ); - } } - /> - <PluginToggle - plugins={ { - 'publish-to-apple-news': { - name: __( 'Apple News', 'newspack' ), - }, - distributor: { - name: __( 'Distributor', 'newspack' ), - }, - } } - /> - </> - ); -}; - -export default Intro; diff --git a/src/wizards/types/hooks.d.ts b/src/wizards/types/hooks.d.ts new file mode 100644 index 0000000000..4b9d1a1d62 --- /dev/null +++ b/src/wizards/types/hooks.d.ts @@ -0,0 +1,100 @@ +/** + * API Method types + */ +type ApiMethods = 'GET' | 'POST' | 'PUT' | 'DELETE'; + +/** + * useWizardApiFetch hook types + */ +interface ApiFetchOptions { + path: string; + method?: ApiMethods; + /** Data to send along with request */ + data?: any; + /** Display simplified loading status during request */ + isQuietFetch?: boolean; + /** Throw errors to be caught in hooks/components */ + isLocalError?: boolean; + /** Should this request be cached. If omitted and `GET` method is used the request will cache automatically */ + isCached?: boolean; + /** Update a specific cacheKey, requires `{ [path]: method }` format */ + updateCacheKey?: { [ k: string ]: ApiMethods }; + /** Will purge and replace cache keys matching method. Well suited for endpoints where only the `method` changes */ + updateCacheMethods?: ApiMethods[]; +} + +/** + * API callback functions + */ +interface ApiFetchCallbacks< T > { + onStart?: () => void; + onSuccess?: ( data: T ) => void; + onError?: ( error: any ) => void; + onFinally?: () => void; +} + +/** + * WP API Fetch error + */ +type WpFetchError = Error & { + code: string; + data?: null | { + status: number; + }; +}; + +/** + * Wizard store schema + */ +type WizardData = { + error: WizardApiError | null; +} & { + [ key: string ]: { [ k in ApiMethods ]?: Record< string, any > | null }; +}; + +// Define the type for the selector's return value +type WizardSelector = { + getWizardData: ( slug: string ) => WizardData; + isLoading: () => boolean; +}; + +/** + * Reader Revenue Wizard Data + */ +type AudienceFieldConfig = { + autocomplete: string; + class: string[]; + label: string; + priority: number; + required: boolean; + type: string; + validate: string[]; +}; +type AudienceDonationsWizardData = { + donation_data: + | { errors: { [ key: string ]: string[] } } + | { + amounts: { + [ Key in FrequencySlug as string ]: [ number, number, number, number ]; + }; + disabledFrequencies: { + [ Key in FrequencySlug as string ]: boolean; + }; + currencySymbol: string; + tiered: boolean; + minimumDonation: string; + billingFields: string[]; + trashed: string[]; + }; + platform_data: { + platform: string; + }; + donation_page: { + editUrl: string; + status: string; + }; + available_billing_fields: { + [ key: string ]: AudienceFieldConfig; + }; + order_notes_field: AudienceFieldConfig; +}; diff --git a/src/wizards/types/index.d.ts b/src/wizards/types/index.d.ts new file mode 100644 index 0000000000..af7466d505 --- /dev/null +++ b/src/wizards/types/index.d.ts @@ -0,0 +1,75 @@ +/** + * Allow image to be imported as a modules + */ +declare module '*.png' { + const path: string; + export default path; +} + +/** + * Wizard API fetch function + */ +type WizardApiFetch< T = {} > = ( + options: ApiFetchOptions, + callbacks?: ApiFetchCallbacks< any > +) => Promise< T >; + +/** + * WP REST API Error. + */ +type WpRestApiError = { + code: string; + message: string; + data: { + status: number; + params: Record< string, string >; + }; +}; + +/** + * Attachment object interface. + */ +interface Attachment { + id: number; + date: string; + date_gmt: string; + guid: { + rendered: string; + }; + modified: string; + modified_gmt: string; + slug: string; + status: string; + type: string; + link: string; + title: { + rendered: string; + }; + author: number; + featured_media: number; + comment_status: string; + ping_status: string; + template: string; + meta: { + newspack_ads_suppress_ads: boolean; + newspack_popups_has_disabled_popups: boolean; + newspack_sponsor_sponsorship_scope: string; + newspack_sponsor_native_byline_display: string; + newspack_sponsor_native_category_display: string; + newspack_sponsor_underwriter_style: string; + newspack_sponsor_underwriter_placement: string; + _media_credit: string; + _media_credit_url: string; + _navis_media_credit_org: string; + _navis_media_can_distribute: string; + }; + class_list: string[]; + description: { + rendered: string; + }; + caption: { + rendered: string; + }; + alt_text: string; + media_type: string; +} diff --git a/src/wizards/types/window.d.ts b/src/wizards/types/window.d.ts new file mode 100644 index 0000000000..189c3b9fea --- /dev/null +++ b/src/wizards/types/window.d.ts @@ -0,0 +1,64 @@ +declare global { + interface Window { + newspackWizardsAdminHeader: { + tabs: Array< { + textContent: string; + href: string; + forceSelected: boolean; + } >; + title: string; + }; + newspackAudience: { + has_reader_activation: boolean; + has_memberships: boolean; + new_subscription_lists_url: string; + reader_activation_url: string; + preview_query_keys: { + [ K in PromptOptionsBaseKey ]: string; + }; + preview_post: string; + preview_archive: string; + }; + newspackAudienceCampaigns: { + api: string; + preview_post: string; + preview_archive: string; + frontend_url: string; + custom_placements: { + [ key: string ]: string; + }; + overlay_placements: string[]; + overlay_sizes: Array< { + value: string; + label: string; + } >; + preview_query_keys: { + [ K in PromptOptionsBaseKey ]: string; + }; + experimental: boolean; + criteria: Array< { + category: string; + description: string; + id: string; + matching_attribute: string; + matching_function: string; + name: string; + } >; + }; + newspackAudienceDonations: { + can_use_name_your_price: boolean; + }; + newspackAudienceSubscriptions: { + tabs: Array< { + title: string; + path: string; + header: string; + description: string; + href: string; + btn_text: string; + } >; + }; + } +} + +export {}; diff --git a/src/wizards/types/wizard-action-card.d.ts b/src/wizards/types/wizard-action-card.d.ts new file mode 100644 index 0000000000..0d25ad541e --- /dev/null +++ b/src/wizards/types/wizard-action-card.d.ts @@ -0,0 +1,126 @@ +/** + * Wizard Action Card Props + */ +type ActionCardProps = Partial< { + title: string | React.ReactNode; + titleLink: string; + href: string; + description: string | React.ReactNode; + actionText: React.ReactNode | string | null; + badge: string; + className: string; + indent: string; + notification: string; + notificationLevel: 'error' | 'warning' | 'info'; + isMedium: boolean; + disabled: boolean | string; + hasGreyHeader: boolean; + toggleChecked: boolean; + toggleOnChange: ( a: boolean ) => void; + actionContent: boolean | React.ReactNode | null; + error: Error | string | null; + handoff: string | null; + isErrorStatus: boolean; + isChecked: boolean; + children: boolean | React.ReactNode; + isSmall: boolean; + editLink: string; + simple: boolean; + secondaryActionText: string; + onSecondaryActionClick: () => void; + secondaryDestructive: boolean; +} >; + +/** + * Plugin callbacks for install, activate and init states + */ +type PluginCallbacks = { + init: PluginWizardApiFetchCallback; + activate: PluginWizardApiFetchCallback; + deactivate: PluginWizardApiFetchCallback; + install: PluginWizardApiFetchCallback; + configure: PluginWizardApiFetchCallback; +}; + +/** + * Plugin partial response + */ +type PluginResponse = { Status: string; Configured: boolean }; + +/** + * Plugin Wizard API fetch callback + */ +type PluginWizardApiFetchCallback = ( + callbacks?: ApiFetchCallbacks< PluginResponse > +) => Promise< PluginResponse >; + +/** + * Plugin card action texts + */ +type PluginCardActionText = { + complete?: string; + configure?: string; + activate?: string; + install?: string; +}; + +/** + * Plugin data type + */ +type PluginCard = { + slug: string; + actionText?: PluginCardActionText; + editLink?: string; + badge?: string; + description?: string | React.ReactNode; + title: string; + subTitle?: string; + statusDescription?: Partial< { + uninstalled: string; + inactive: string; + notConfigured: string; + connected: string; + } >; + isEnabled?: boolean; + isManageable?: boolean; + // Toggle card props + toggleChecked?: boolean; + toggleOnChange?: ( value?: boolean ) => void; + isStatusPrepended?: boolean; + error?: string | null; + onStatusChange?: ( statuses: Record< string, boolean > ) => void; + reloadOnActivation?: boolean; + isConfigurable?: boolean; + isTogglable?: boolean; + isMedium?: boolean; + disabled?: boolean; +}; + +/** + * Wizard Toggle Header Card Props + */ +type WizardsToggleHeaderCardProps< T > = { + title: string; + description: string; + namespace: string; + path: string; + defaultValue: T; + fieldValidationMap: Array< + [ + keyof T, + { + callback?: 'isIntegerId' | 'isId' | ( ( v: any ) => string ); + dependsOn?: { [ k in keyof T ]?: string }; + }, + ] + >; + renderProp: ( props: { + settingsUpdates: T; + setSettingsUpdates: React.Dispatch< React.SetStateAction< T > >; + isFetching: boolean; + } ) => React.ReactNode; + /** Optional prop to override conditions for toggling. Default uses `active` prop to dictate if toggled on/off */ + onToggle?: ( active: boolean, data: T ) => T; + /** Optional prop to override conditions for isToggled. Default uses `active` prop to dictate if toggled on/off */ + onChecked?: ( data: T ) => boolean; +}; diff --git a/src/wizards/types/wizard-errors.d.ts b/src/wizards/types/wizard-errors.d.ts new file mode 100644 index 0000000000..4d3159541c --- /dev/null +++ b/src/wizards/types/wizard-errors.d.ts @@ -0,0 +1,5 @@ +/** Top-level error object for Wizard failures. */ +type WizardErrorType = import('../errors/class-wizard-error').default; + +/** Errors specific to Wizard API Fetch Requests */ +type WizardApiErrorType = import('../errors/class-wizard-api-error').default; diff --git a/src/wizards/wizards-action-card.tsx b/src/wizards/wizards-action-card.tsx new file mode 100644 index 0000000000..6e8e7932b9 --- /dev/null +++ b/src/wizards/wizards-action-card.tsx @@ -0,0 +1,37 @@ +/** + * Settings Wizard: Action Card component. + */ + +/** + * Internal dependencies + */ +import { ActionCard } from '../components/src'; + +const WizardsActionCard = ( { + description, + error, + isChecked, + notificationLevel = 'error', + children, + ...props +}: ActionCardProps ) => { + let checkbox: 'checked' | 'unchecked' | undefined; + if ( typeof isChecked !== 'undefined' ) { + checkbox = isChecked ? 'checked' : 'unchecked'; + } + return ( + <ActionCard + { ...{ + description, + checkbox, + notification: error, + notificationLevel, + ...props, + } } + > + { children } + </ActionCard> + ); +}; + +export default WizardsActionCard; diff --git a/src/wizards/wizards-card.tsx b/src/wizards/wizards-card.tsx new file mode 100644 index 0000000000..543c3eb805 --- /dev/null +++ b/src/wizards/wizards-card.tsx @@ -0,0 +1,29 @@ +/** + * Wizards Card component. + */ + +/** + * Internal dependencies. + */ +import { Card } from '../components/src'; + +/** + * Wizards Card component. + * + * @param props Component props. + * @param props.children Component children. + * @param props.className Component classNames. + * + * @return Component. + */ +function WizardsCard( { + children, + ...props +}: { + children: React.ReactNode; + className?: string; +} ) { + return <Card { ...props }>{ children }</Card>; +} + +export default WizardsCard; diff --git a/src/wizards/wizards-plugin-card.tsx b/src/wizards/wizards-plugin-card.tsx new file mode 100644 index 0000000000..778d7d7fe8 --- /dev/null +++ b/src/wizards/wizards-plugin-card.tsx @@ -0,0 +1,375 @@ +/** + * Settings Wizard: Plugin Card component. + */ + +/** + * WordPress dependencies + */ +import { sprintf, __ } from '@wordpress/i18n'; +import { useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { Button, hooks, Waiting } from '../components/src'; +import WizardsActionCard from './wizards-action-card'; +import { useWizardApiFetch } from './hooks/use-wizard-api-fetch'; + +/** + * Helper for managing plugins API requests. + * + * @param slug Plugin slug to fetch + * @param action Endpoint action to request + * @param apiFetch Wizard API Fetch instance + * @param callbacks Wizard API Fetch callbacks + * @return Wizard API Fetch response + */ +function fetchHandler( + slug: string, + action = '', + apiFetch: WizardApiFetch< PluginResponse >, + callbacks?: ApiFetchCallbacks< PluginResponse > +) { + const path = action + ? `/newspack/v1/plugins/${ slug }/${ action }` + : `/newspack/v1/plugins/${ slug }`; + const method = action ? 'POST' : 'GET'; + return apiFetch( { path, method }, callbacks ); +} + +/** + * Wizard Plugin Action Card component. + * + * @param props Component props. + * @param props.isLoading Whether the plugin is performing an API request. + * @param props.isSetup Whether the plugin is install, active and configured. + * @param props.isActive Whether the plugin is active. + * @param props.isInstalled Whether the plugin is installed. + * @param props.isConfigurable Whether the plugin is configurable. + * @param props.onActivate Callback to activate the plugin. + * @param props.onInstall Callback to install the plugin. + * @param props.onConfigure Callback to configure the plugin. + * @param props.status Plugin status. + * @param props.title Plugin title. + * @param props.editLink Plugin edit link. + * @param props.actionText Plugin action texts. + */ +function WizardsPluginCardButton( { + isLoading, + isSetup, + isActive, + isInstalled, + isConfigurable, + onActivate, + onInstall, + onConfigure, + actionText = {}, + ...plugin +}: { + status: string; + title: string; + editLink?: string; + isLoading: boolean; + isSetup: boolean; + isActive: boolean; + isConfigurable?: boolean; + isInstalled: boolean; + onActivate: () => void; + onInstall: () => void; + onConfigure: () => void; + actionText?: PluginCardActionText; +} ) { + if ( plugin.status === 'page-reload' ) { + return ( + <span className="gray"> + { __( 'Page reloading…', 'newspack-plugin' ) } + </span> + ); + } + if ( plugin.status === 'page-redirect' ) { + return ( + <span className="gray"> + { __( 'Page redirecting…', 'newspack-plugin' ) } + </span> + ); + } + if ( isLoading ) { + return <Waiting />; + } + if ( ! isInstalled ) { + return ( + <Button isLink onClick={ onInstall }> + { actionText.install ?? + sprintf( + /* translators: %s: Plugin name */ + __( 'Install %s', 'newspack-plugin' ), + plugin.title + ) } + </Button> + ); + } + if ( ! isActive ) { + return ( + <Button isLink onClick={ onActivate }> + { actionText.activate ?? + sprintf( + /* translators: %s: Plugin name */ + __( 'Activate %s', 'newspack-plugin' ), + plugin.title + ) } + </Button> + ); + } + if ( ! isSetup ) { + if ( isConfigurable ) { + return ( + <Button isLink onClick={ onConfigure }> + { actionText.configure ?? + sprintf( + /* translators: %s: Plugin name */ + __( 'Configure %s', 'newspack-plugin' ), + plugin.title + ) } + </Button> + ); + } + if ( plugin.editLink ) { + return ( + <a href={ plugin.editLink }> + { actionText.complete ?? + __( 'Complete Setup', 'newspack-plugin' ) } + </a> + ); + } + } + if ( plugin.editLink ) { + return ( + <a href={ plugin.editLink }> + { actionText.configure ?? + __( 'Configure', 'newspack-plugin' ) } + </a> + ); + } + return null; +} + +/** + * Wizard Plugin Card component. + * + * @param props Component props. + * @param props.slug Plugin slug. + * @param props.title Plugin title. + * @param props.subTitle Plugin subtitle. String appended to title. + * @param props.editLink Plugin edit link. + * @param props.description Plugin description. + * @param props.onStatusChange Callback invoked when the plugin status changes. + * @param props.reloadOnActivation Should the page reload on activation status change? + * @param props.isStatusPrepended Should status be prepended to description. + * @param props.isConfigurable Whether the plugin is configurable. + * @param props.isTogglable Whether the plugin is togglable. + * @param props.actionText Action card action text. + * @param props.statusDescription Plugin status description. + */ +function WizardsPluginCard( { + slug, + title, + subTitle, + editLink, + description, + statusDescription, + onStatusChange = () => {}, + reloadOnActivation = true, + isStatusPrepended = true, + isConfigurable, + isTogglable, + actionText = {}, + ...props +}: PluginCard ) { + const { wizardApiFetch, errorMessage, isFetching } = useWizardApiFetch( + `/newspack/wizards/plugins/${ slug }` + ); + const [ pluginState, setPluginState ] = hooks.useObjectState( { + slug, + status: '', + statusDescription, + configured: false, + } ); + + const statuses = { + isSetup: pluginState.status === 'active' && pluginState.configured, + isActive: pluginState.status === 'active', + isLoading: ! pluginState.status, + isInstalled: pluginState.status !== 'uninstalled', + isConfigured: pluginState.configured, + isError: Boolean( errorMessage ), + }; + + const on: PluginCallbacks = { + init: fetchCallbacks => + fetchHandler( + pluginState.slug, + undefined, + wizardApiFetch, + fetchCallbacks + ), + activate: fetchCallbacks => + fetchHandler( + pluginState.slug, + 'activate', + wizardApiFetch, + fetchCallbacks + ), + deactivate: fetchCallbacks => + fetchHandler( + pluginState.slug, + 'deactivate', + wizardApiFetch, + fetchCallbacks + ), + install: fetchCallbacks => + fetchHandler( + pluginState.slug, + 'install', + wizardApiFetch, + fetchCallbacks + ), + configure: fetchCallbacks => + fetchHandler( + pluginState.slug, + 'configure', + wizardApiFetch, + fetchCallbacks + ), + }; + + /** + * Set plugin state. + * + * @param callbacksKey Callback key to dictate action to perform. + */ + function setPluginAction( callbacksKey: keyof PluginCallbacks ) { + // If action is activating or deactivating. + const actions = reloadOnActivation ? [ 'activate', 'deactivate' ] : [ 'deactivate' ]; + const isPluginStateUpdate = actions.includes( + callbacksKey + ); + setPluginState( { status: '' } ); + on[ callbacksKey ]( { + onSuccess( update ) { + let statusUpdate = update.Status; + if ( isPluginStateUpdate ) { + statusUpdate = 'page-reload'; + } + setPluginState( { + status: statusUpdate, + configured: update.Configured, + } ); + }, + onFinally() { + if ( isPluginStateUpdate && ! errorMessage ) { + window.location.reload(); + } + if ( editLink && callbacksKey === 'configure' ) { + window.location.href = editLink; + setPluginState( { status: 'page-redirect' } ); + } + }, + } ); + } + + useEffect( () => { + setPluginAction( 'init' ); + }, [] ); + + useEffect( () => { + onStatusChange( statuses ); + }, [ statuses ] ); + + function onActivate() { + setPluginAction( 'activate' ); + } + + function onDeactivate() { + setPluginAction( 'deactivate' ); + } + + function getDescription() { + if ( statuses.isError ) { + return __( 'Status: Error!', 'newspack-plugin' ); + } + if ( statuses.isLoading ) { + return __( 'Loading…', 'newspack-plugin' ); + } + const descriptionAppend = description ?? ''; + let newDescription = ''; + if ( ! statuses.isInstalled ) { + newDescription = + pluginState.statusDescription?.uninstalled ?? + __( 'Uninstalled.', 'newspack-plugin' ); + } else if ( ! statuses.isActive ) { + newDescription = + pluginState.statusDescription?.inactive ?? + __( 'Inactive.', 'newspack-plugin' ); + } else if ( ! statuses.isConfigured ) { + newDescription = + pluginState.statusDescription?.notConfigured ?? + __( 'Not connected.', 'newspack-plugin' ); + } else { + newDescription = + pluginState.statusDescription?.connected ?? + __( 'Connected.', 'newspack-plugin' ); + } + return ( + <> + { isStatusPrepended && + sprintf( + // Translators: %s: Plugin status + __( 'Status: %s', 'newspack-plugin' ), + newDescription + ) }{ ' ' } + { descriptionAppend } + </> + ); + } + + const conditionalProps: Partial< PluginCard > = {}; + + // Add toggle specific props if the card is togglable. + if ( isTogglable ) { + conditionalProps.toggleChecked = statuses.isActive; + conditionalProps.toggleOnChange = () => + ! statuses.isActive ? onActivate() : onDeactivate(); + conditionalProps.disabled = isFetching; + } + + return ( + <WizardsActionCard + title={ `${ title }${ subTitle ? `: ${ subTitle }` : '' }` } + description={ getDescription } + className={ `wizards-plugin-card ${ slug }` } + actionText={ + ! statuses.isError ? ( + <WizardsPluginCardButton + { ...{ + title, + editLink, + onActivate, + actionText, + isConfigurable, + onInstall: () => setPluginAction( 'activate' ), + onConfigure: () => setPluginAction( 'configure' ), + ...statuses, + ...pluginState, + } } + /> + ) : null + } + isChecked={ statuses.isSetup } + error={ props.error ?? errorMessage } + { ...props } + { ...conditionalProps } + /> + ); +} + +export default WizardsPluginCard; diff --git a/src/wizards/wizards-section.tsx b/src/wizards/wizards-section.tsx new file mode 100644 index 0000000000..81bde686c7 --- /dev/null +++ b/src/wizards/wizards-section.tsx @@ -0,0 +1,51 @@ +/** + * Wizards Section component. + */ + +/** + * Internal dependencies. + */ +import { SectionHeader } from '../components/src'; + +/** + * Section component. + * + * @param props Component props. + * @param props.title Section title. + * @param props.description Section description. + * @param props.children Section children. + * @param props.scrollToAnchor Scroll to anchor. + * @param props.className Optional class name. + * + * @return Component. + */ +export default function WizardSection( { + title, + description, + children = null, + scrollToAnchor = null, + className, +}: { + title?: string; + description?: string; + children: React.ReactNode; + scrollToAnchor?: string | null; + className?: string; +} ) { + const classNames = `newspack-wizard__section${ + className ? ` ${ className }` : '' + }`; + return ( + <div className={ classNames }> + { title && ( + <SectionHeader + id={ scrollToAnchor } + heading={ 3 } + title={ title } + description={ description } + /> + ) } + { children } + </div> + ); +} diff --git a/src/wizards/wizards-tab.tsx b/src/wizards/wizards-tab.tsx new file mode 100644 index 0000000000..0e9e299991 --- /dev/null +++ b/src/wizards/wizards-tab.tsx @@ -0,0 +1,47 @@ +/** + * WordPress dependencies. + */ +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies. + */ +import { WIZARD_STORE_NAMESPACE } from '../components/src/wizard/store'; + +/** + * Wizards Tab component. + */ + +function WizardsTab( { + title, + children, + isFetching, + description, + ...props +}: { + title: string; + children: React.ReactNode; + isFetching?: boolean; + className?: string; + description?: React.ReactNode; +} ) { + const isWizardLoading = useSelect( + ( select: ( namespace: string ) => WizardSelector ) => + select( WIZARD_STORE_NAMESPACE ).isLoading(), + [] + ); + const className = props.className || ''; + return ( + <div + className={ `${ + isWizardLoading || isFetching ? 'is-fetching ' : '' + }${ className } newspack-wizard__sections` } + > + <h1>{ title }</h1> + { description && <p className="newspack-wizard__sections__description">{ description }</p> } + { children } + </div> + ); +} + +export default WizardsTab; diff --git a/src/wizards/wizards-toggle-header-card.tsx b/src/wizards/wizards-toggle-header-card.tsx new file mode 100644 index 0000000000..ea4d387773 --- /dev/null +++ b/src/wizards/wizards-toggle-header-card.tsx @@ -0,0 +1,208 @@ +/** + * Wizards Toggle Header Card Component. Uses render props pattern to allow for custom card content that can update state in card. + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Fragment, useEffect, useState, useCallback } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { Grid, Button } from '../components/src'; +import WizardsActionCard from './wizards-action-card'; +import WizardError from './errors/class-wizard-error'; +import { useWizardApiFetch } from './hooks/use-wizard-api-fetch'; + +/** + * A few helper validation callbacks. Allows consuming components to define validation callbacks by string i.e. 'isNonEmptyString' + */ +function validationErrorHandler( { setError }: { setError: ( value: WizardErrorType ) => void } ) { + return { + /** + * Check if the value is a non-empty number + * + * @param value String value to validate + * @return true if string is all numbers, false otherwise + */ + isIntegerId( value: string ) { + const trimmedValue = value.trim(); + let errorMessage = ''; + if ( trimmedValue === '' ) { + errorMessage = __( 'Value cannot be empty!', 'newspack-plugin' ); + } else if ( ! /^[0-9]+$/.test( trimmedValue ) ) { + errorMessage = __( 'Value may only contain numbers!', 'newspack-plugin' ); + } else if ( trimmedValue === '0' ) { + errorMessage = __( 'Value cannot be zero!', 'newspack-plugin' ); + } + if ( errorMessage ) { + setError( new WizardError( errorMessage, 'invalid_input_int_id' ) ); + return false; + } + return Number( trimmedValue ) > 0; + }, + /** + * Check if the value is a non-empty string + * + * @param value String value to validate + * @return true if string is non-empty, false otherwise + */ + isId( value: string ) { + const trimmedValue = value.trim(); + let errorMessage = ''; + if ( trimmedValue === '' ) { + errorMessage = __( 'Value cannot be empty!', 'newspack-plugin' ); + } else if ( ! /^[a-zA-Z0-9]+$/.test( value.trim() ) ) { + errorMessage = __( 'Value may only contain numbers and letters.', 'newspack-plugin' ); + } + if ( errorMessage ) { + setError( new WizardError( errorMessage, 'invalid_input_id' ) ); + return false; + } + return true; + }, + }; +} + +/** + * Wizard Toggle Header Card Component + * + * @param props Wizard Toggle Header Card Component props + * @param props.title Title of the card + * @param props.description Card description + * @param props.namespace Namespace for the wizard API + * @param props.path Path for the wizard API requests + * @param props.defaultValue Default value and schema for storage + * @param props.fieldValidationMap Array of field id and validation callback + * @param props.renderProp Render prop for the card children + * @param props.onToggle Callback for the toggle + * @param props.onChecked Callback for the checked state + */ +const WizardsToggleHeaderCard = < T extends Record< string, any > >( { + title, + description, + namespace, + path, + defaultValue, + fieldValidationMap, + renderProp, + onToggle = ( active, data ) => ( { ...data, active } ), + onChecked = ( data: T ) => data.active, +}: WizardsToggleHeaderCardProps< T > ) => { + const { wizardApiFetch, isFetching, errorMessage, setError, resetError } = + useWizardApiFetch( namespace ); + const [ settings, setSettings ] = useState< T >( { ...defaultValue } ); + const [ settingsUpdates, setSettingsUpdates ] = useState< T >( { ...defaultValue } ); + + const fieldValidations = validationErrorHandler( { setError } ); + + useEffect( () => { + wizardApiFetch< T >( + { path }, + { + onSuccess: ( res: T ) => { + setSettings( res ); + setSettingsUpdates( res ); + }, + } + ); + }, [] ); + + const updateSettings = useCallback( + ( data: T, isToggleSave = false ) => { + resetError(); + + if ( ! isToggleSave ) { + for ( const [ field, validate ] of fieldValidationMap ) { + if ( validate.dependsOn ) { + const [ [ key, value ] ] = Object.entries( validate.dependsOn ); + if ( settingsUpdates[ key as keyof T ] !== value ) { + continue; + } + } + if ( typeof validate.callback === 'string' ) { + if ( ! fieldValidations[ validate.callback ]( settingsUpdates[ field ] ) ) { + return; + } + } else if ( typeof validate.callback === 'function' ) { + const validationError = validate.callback( settingsUpdates[ field ] ); + if ( validationError ) { + setError( new WizardError( validationError, `invalid_${ field.toString() }` ) ); + return; + } + } + } + } + + wizardApiFetch< T >( + { + path, + method: 'POST', + data, + updateCacheMethods: [ 'GET' ], + }, + { + onSuccess: ( res: T ) => { + setSettings( res ); + setSettingsUpdates( res ); + }, + } + ); + }, + [ fieldValidationMap, fieldValidations, resetError, settingsUpdates, wizardApiFetch ] + ); + + const renderCard = ( + renderCallback: ( a: { + updates: T; + settings: T; + isFetching?: boolean; + setSettingsUpdates: React.Dispatch< React.SetStateAction< T > >; + } ) => React.ReactNode + ) => { + const isChecked = onChecked( settingsUpdates ); + return ( + <WizardsActionCard + title={ title } + description={ + isFetching && ! isChecked ? __( 'Loading…', 'newspack-plugin' ) : description + } + hasGreyHeader={ isChecked } + actionContent={ + isChecked && ( + <Button + variant="primary" + disabled={ isFetching } + onClick={ () => updateSettings( settingsUpdates ) } + > + { isFetching + ? __( 'Loading…', 'newspack-plugin' ) + : __( 'Save Settings', 'newspack-plugin' ) } + </Button> + ) + } + error={ errorMessage } + disabled={ isFetching } + toggleOnChange={ active => { + updateSettings( onToggle( active, settingsUpdates ), true ); + } } + toggleChecked={ isChecked } + > + { isChecked && + renderCallback( { updates: settingsUpdates, setSettingsUpdates, settings } ) } + </WizardsActionCard> + ); + }; + + return renderCard( () => ( + <Fragment> + <Grid noMargin rowGap={ 16 } columns={ 1 }> + { renderProp( { settingsUpdates, setSettingsUpdates, isFetching } ) } + </Grid> + </Fragment> + ) ); +}; + +export default WizardsToggleHeaderCard; diff --git a/tests/unit-tests/api-wizards-controller.php b/tests/unit-tests/api-wizards-controller.php index e5126feb76..ed4aacf80e 100644 --- a/tests/unit-tests/api-wizards-controller.php +++ b/tests/unit-tests/api-wizards-controller.php @@ -17,7 +17,7 @@ class Newspack_Test_Wizards_Controller extends WP_UnitTestCase { * * @var string */ - protected $api_route = '/newspack/v1/wizards/reader-revenue'; + protected $api_route = '/newspack/v1/wizards/newspack-dashboard'; /** * Set up stuff for testing API requests. @@ -61,7 +61,7 @@ public function test_get_wizard_authorized() { $this->assertEquals( 200, $response->get_status() ); $data = $response->get_data(); - $this->assertEquals( 'Reader Revenue', $data['name'] ); + $this->assertEquals( 'Newspack', $data['name'] ); } /** diff --git a/tests/unit-tests/oauth.php b/tests/unit-tests/oauth.php index 07ff314249..341e320ef1 100644 --- a/tests/unit-tests/oauth.php +++ b/tests/unit-tests/oauth.php @@ -66,7 +66,7 @@ public function test_oauth_google() { $consent_page_params, [ 'scope' => 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/dfp https://www.googleapis.com/auth/analytics https://www.googleapis.com/auth/analytics.edit', - 'redirect_after' => 'http://example.org/wp-admin/admin.php?page=newspack-connections-wizard', + 'redirect_after' => 'http://example.org/wp-admin/admin.php?page=newspack-settings', 'csrf_token' => $csrf_token, ], 'The consent page request params are as expected.' @@ -129,24 +129,4 @@ public function test_oauth_google() { 'OAuth2 object getter return false after credentials are removed.' ); } - - /** - * Fivetran OAuth flow. - */ - public function test_oauth_fivetran() { - self::expectException( Exception::class ); - self::assertFalse( - OAuth::authenticate_proxy_url( 'fivetran', '/wp-json/newspack-fivetran' ), - 'Proxy URL getting throws until configured.' - ); - self::set_api_key(); - if ( ! defined( 'NEWSPACK_FIVETRAN_PROXY' ) ) { - define( 'NEWSPACK_FIVETRAN_PROXY', 'http://dummy.proxy' ); - } - self::assertEquals( - 'http://dummy.proxy/wp-json/newspack-fivetran?api_key=123abc', - OAuth::authenticate_proxy_url( 'fivetran', '/wp-json/newspack-fivetran' ), - 'Proxy URL is as expected after proxy is configured.' - ); - } } diff --git a/tests/unit-tests/settings.php b/tests/unit-tests/syndication.php similarity index 75% rename from tests/unit-tests/settings.php rename to tests/unit-tests/syndication.php index 8730f011ce..04fb110a64 100644 --- a/tests/unit-tests/settings.php +++ b/tests/unit-tests/syndication.php @@ -5,7 +5,7 @@ * @package Newspack\Tests */ -use Newspack\Settings; +use Newspack\Syndication; /** * Tests the Settings. @@ -15,7 +15,7 @@ class Newspack_Test_Settings extends WP_UnitTestCase { * Setup for the tests. */ public function set_up() { - delete_option( Settings::SETTINGS_OPTION_NAME ); + delete_option( Syndication::OPTION_NAME ); } /** @@ -23,7 +23,7 @@ public function set_up() { */ public function test_settings_defaults() { self::assertEquals( - Settings::api_get_settings(), + Syndication::get_settings(), [ 'module_enabled_rss' => false, 'module_enabled_media-partners' => false, @@ -38,9 +38,9 @@ public function test_settings_defaults() { public function test_settings_update() { $request = new WP_REST_Request(); $request->set_param( 'module_enabled_rss', true ); - Settings::api_update_settings( $request ); + Syndication::api_update_settings( $request ); self::assertEquals( - Settings::api_get_settings(), + Syndication::get_settings(), [ 'module_enabled_rss' => true, 'module_enabled_media-partners' => false, @@ -49,9 +49,9 @@ public function test_settings_update() { ); $request->set_param( 'non_existent_setting', true ); - Settings::api_update_settings( $request ); + Syndication::api_update_settings( $request ); self::assertEquals( - Settings::api_get_settings(), + Syndication::get_settings(), [ 'module_enabled_rss' => true, 'module_enabled_media-partners' => false, @@ -65,15 +65,15 @@ public function test_settings_update() { */ public function test_settings_optional_modules() { self::assertEquals( - Settings::is_optional_module_active( 'rss' ), + Syndication::is_optional_module_active( 'rss' ), false, 'RSS module is not active by default.' ); - Settings::activate_optional_module( 'rss' ); + Syndication::activate_optional_module( 'rss' ); self::assertEquals( - Settings::is_optional_module_active( 'rss' ), + Syndication::is_optional_module_active( 'rss' ), true, 'RSS module is active after being activated.' ); diff --git a/tsconfig.json b/tsconfig.json index 4b15dce028..1a89f1d250 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,17 @@ { - "extends": "newspack-scripts/config/tsconfig.json", - "compilerOptions": { - "rootDir": "src", - "jsx": "react-jsx" - }, - "include": [ - "src", - "src/**/*.json" - ] + "extends": "newspack-scripts/config/tsconfig.json", + "compilerOptions": { + "esModuleInterop": true, + "rootDir": "src", + "jsx": "react-jsx", + "paths": { + "react": [ + "./node_modules/@types/react" + ] + } + }, + "include": [ + "src", + "src/**/*.json" + ] } \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index f7cc11e180..4d73dde7ec 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -15,13 +15,24 @@ const wizardsDir = path.join( __dirname, 'src', 'wizards' ); // Get files for wizards scripts. const wizardsScripts = fs .readdirSync( wizardsDir ) - .filter( ( wizard ) => - fs.existsSync( path.join( __dirname, 'src', 'wizards', wizard, 'index.js' ) ) + .filter( + wizard => + fs.existsSync( + path.join( __dirname, 'src', 'wizards', wizard, 'index.js' ) + ) || + fs.existsSync( + path.join( __dirname, 'src', 'wizards', wizard, 'index.tsx' ) + ) ); const wizardsScriptFiles = { - 'plugins-screen': path.join( __dirname, 'src', 'plugins-screen', 'plugins-screen.js' ), + 'plugins-screen': path.join( + __dirname, + 'src', + 'plugins-screen', + 'plugins-screen.js' + ), }; -wizardsScripts.forEach( function( wizard ) { +wizardsScripts.forEach( function ( wizard ) { let wizardFileName = wizard; if ( wizard === 'advertising' ) { // "advertising.js" might be blocked by ad-blocking extensions. @@ -32,7 +43,11 @@ wizardsScripts.forEach( function( wizard ) { 'src', 'wizards', wizard, - 'index.js' + fs.existsSync( + path.join( __dirname, 'src', 'wizards', wizard, 'index.tsx' ) + ) + ? 'index.tsx' + : 'index.js' ); } ); @@ -52,21 +67,43 @@ const entry = { 'reader-registration', 'view.js' ), - 'my-account': path.join( __dirname, 'includes', 'reader-revenue', 'my-account', 'index.js' ), + 'my-account': path.join( + __dirname, + 'includes', + 'reader-revenue', + 'my-account', + 'index.js' + ), admin: path.join( __dirname, 'src', 'admin', 'index.js' ), - 'memberships-gate': path.join( __dirname, 'src', 'memberships-gate', 'gate.js' ), - 'memberships-gate-metering': path.join( __dirname, 'src', 'memberships-gate', 'metering.js' ), + 'memberships-gate': path.join( + __dirname, + 'src', + 'memberships-gate', + 'gate.js' + ), + 'memberships-gate-metering': path.join( + __dirname, + 'src', + 'memberships-gate', + 'metering.js' + ), // Newspack wizard assets. ...wizardsScriptFiles, blocks: path.join( __dirname, 'src', 'blocks', 'index.js' ), - 'memberships-gate-editor': path.join( __dirname, 'src', 'memberships-gate', 'editor.js' ), + 'memberships-gate-editor': path.join( + __dirname, + 'src', + 'memberships-gate', + 'editor.js' + ), 'memberships-gate-block-patterns': path.join( __dirname, 'src', 'memberships-gate', 'block-patterns.js' ), + wizards: path.join( __dirname, 'src', 'wizards', 'index.tsx' ), 'newspack-ui': path.join( __dirname, 'src', 'newspack-ui', 'index.js' ), 'bylines': path.join( __dirname, 'src', 'bylines', 'index.js' ), }; @@ -75,7 +112,9 @@ const entry = { const otherScripts = fs .readdirSync( path.join( __dirname, 'src', 'other-scripts' ) ) .filter( script => - fs.existsSync( path.join( __dirname, 'src', 'other-scripts', script, 'index.js' ) ) + fs.existsSync( + path.join( __dirname, 'src', 'other-scripts', script, 'index.js' ) + ) ); otherScripts.forEach( function ( script ) { entry[ `other-scripts/${ script }` ] = path.join( @@ -87,11 +126,9 @@ otherScripts.forEach( function ( script ) { ); } ); -const webpackConfig = getBaseWebpackConfig( - { - entry, - } -); +const webpackConfig = getBaseWebpackConfig( { + entry, +} ); // Overwrite default optimisation. webpackConfig.optimization.splitChunks.cacheGroups.commons = {