diff --git a/.circleci/config.yml b/.circleci/config.yml index ad5404cf7..d4990dc99 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -39,7 +39,7 @@ jobs: steps: - checkout - node/install: - node-version: lts + node-version: '14.18.1' - nodegit-workaround - set-up-packages - run: npm run lint @@ -67,7 +67,7 @@ jobs: sudo apt-get update && sudo apt-get install libpng-dev sudo docker-php-ext-install mysqli gd - node/install: - node-version: lts + node-version: '14.18.1' - nodegit-workaround - run: name: Installing WordPress and setting up tests @@ -94,7 +94,7 @@ jobs: steps: - checkout - node/install: - node-version: lts + node-version: '14.18.1' - nodegit-workaround - run: HUSKY_SKIP_INSTALL=1 npm ci && npm run test:js -- --maxWorkers=2 @@ -106,7 +106,7 @@ jobs: - run: sudo apt-get update && sudo apt-get install php php-xml - install-composer - node/install: - node-version: lts + node-version: '14.18.1' - nodegit-workaround - set-up-packages - run: npm run wp-env start && npm run test:e2e diff --git a/.nvmrc b/.nvmrc index b009dfb9d..8351c1939 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -lts/* +14 diff --git a/js/src/block-editor/index.js b/js/src/block-editor/index.js index 611f10f6c..e1691cebf 100644 --- a/js/src/block-editor/index.js +++ b/js/src/block-editor/index.js @@ -3,15 +3,23 @@ /** * WordPress dependencies */ -import { setLocaleData } from '@wordpress/i18n'; import { addFilter } from '@wordpress/hooks'; +import { setLocaleData } from '@wordpress/i18n'; /** * Internal dependencies */ import { addControls, registerBlocks } from './helpers'; import { Edit } from './components'; +import { GAClient } from '../common/classes'; setLocaleData( { '': {} }, 'genesis-custom-blocks' ); addFilter( 'genesisCustomBlocks.controls', 'genesisCustomBlocks/addControls', addControls ); + +// @ts-ignore registerBlocks( genesisCustomBlocks, gcbBlocks, Edit ); + +// @ts-ignore +window.GcbAnalytics = { + GAClient: new GAClient(), +}; diff --git a/js/src/common/classes/GAClient.js b/js/src/common/classes/GAClient.js new file mode 100644 index 000000000..44b0e8f30 --- /dev/null +++ b/js/src/common/classes/GAClient.js @@ -0,0 +1,103 @@ +/** + * Internal dependencies + */ +import { debounce } from '../helpers'; + +// @ts-ignore +window.dataLayer = window.dataLayer || []; + +/** + * Genesis Analytics Client + * + * Forked from BMO's work in Genesis Blocks. + * + * Follows the singleton pattern to prevent multiple instances of the GA Client from being used. + * https://developers.google.com/analytics/devguides/collection/gtagjs + */ +export default class GAClient { + /** + * Is Google Analytics enabled. + * + * @type {boolean} + */ + enabled = false; + + /** + * Google Analytics Client + * + * @type {Object} + */ + client; + + /** + * Google Analytics Measurment ID. + * + * Todo: update this for GCB. + * + * @type {string} + */ + GA_ID = 'UA-12345'; + + /** + * Class constructor. + */ + constructor() { + this.client = function() { + // @ts-ignore + window.dataLayer.push( arguments ); + }; + + // @ts-ignore + this.config = window.gcbAnalyticsConfig || {}; + if ( this.config.ga_opt_in ) { + this.enableAnalytics( this.config.ga_opt_in ); + this.initClient(); + } + } + + /** + * Enables Google Analytics. + * Setting this value allows the GA Client to respect any opt out configuration. + * + * https://developers.google.com/analytics/devguides/collection/gtagjs/user-opt-out + * + * @param {boolean | number | string} enable The value to be set. + */ + enableAnalytics( enable ) { + enable = !! +enable; + + if ( enable ) { + // Remove ga-disable-GA_MEASUREMENT_ID property to enable GA. + delete window[ `ga-disable-${ this.GA_ID }` ]; + } else { + // Set ga-disable-GA_MEASUREMENT_ID property to disable GA. + window[ `ga-disable-${ this.GA_ID }` ] = '1'; + } + this.enabled = enable; + } + + /** + * Sets up the initial values of the Google Analytics client. + */ + initClient() { + this.client( 'js', new Date() ); + this.client( 'config', this.GA_ID, { send_page_view: false } ); + } + + /** + * Sends an event to Google Analytics. + * + * @param {string} action + * @param {{event_category: string; event_label?: string;}} params + */ + send( action, params ) { + if ( this.enabled ) { + this.client( 'event', action, params ); + } + } + + /** + * Creates a debounced copy of send method. + */ + sendDebounce = debounce( this.send.bind( this ), 500 ); +} diff --git a/js/src/common/classes/index.js b/js/src/common/classes/index.js new file mode 100644 index 000000000..3e3542dc4 --- /dev/null +++ b/js/src/common/classes/index.js @@ -0,0 +1 @@ +export { default as GAClient } from './GAClient'; diff --git a/js/src/common/helpers/debounce.js b/js/src/common/helpers/debounce.js new file mode 100644 index 000000000..ee6872527 --- /dev/null +++ b/js/src/common/helpers/debounce.js @@ -0,0 +1,24 @@ +/** + * Ensures that the provided function isn't called multiple times in succession. + * + * Forked from BMO's work in Genesis Blocks. + * + * @param {() => any} func + * @param {number} wait + * + * @return {() => void} A debounced function. + */ +const debounce = ( func, wait ) => { + let timeout; + return function executedFunction( ...args ) { + const later = () => { + clearTimeout( timeout ); + func( ...args ); + }; + + clearTimeout( timeout ); + timeout = setTimeout( later, wait ); + }; +}; + +export default debounce; diff --git a/js/src/common/helpers/index.js b/js/src/common/helpers/index.js index d5217dfc8..3e504f88f 100644 --- a/js/src/common/helpers/index.js +++ b/js/src/common/helpers/index.js @@ -1,3 +1,4 @@ +export { default as debounce } from './debounce'; export { default as getFieldsAsArray } from './getFieldsAsArray'; export { default as getFieldsAsObject } from './getFieldsAsObject'; export { default as getIconComponent } from './getIconComponent'; diff --git a/js/src/edit-block/index.js b/js/src/edit-block/index.js index 01a9602da..07cb1ef42 100644 --- a/js/src/edit-block/index.js +++ b/js/src/edit-block/index.js @@ -11,6 +11,9 @@ import { addFilter } from '@wordpress/hooks'; */ import { initializeEditor } from './helpers'; import { addControls } from '../block-editor/helpers'; +import { GAClient } from '../common/classes'; + +addFilter( 'genesisCustomBlocks.controls', 'genesisCustomBlocks/addControls', addControls ); // Renders the app in the container. domReady( () => { @@ -23,4 +26,7 @@ domReady( () => { initializeEditor( gcbEditor, container ); } ); -addFilter( 'genesisCustomBlocks.controls', 'genesisCustomBlocks/addControls', addControls ); +// @ts-ignore +window.GcbAnalytics = { + GAClient: new GAClient(), +}; diff --git a/package.json b/package.json index 4da3405e2..971f57c5a 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,9 @@ "bugs": { "url": "https://github.com/studiopress/genesis-custom-blocks/issues" }, + "engines": { + "node": "14" + }, "devDependencies": { "@material-ui/core": "4.11.2", "@material-ui/icons": "4.11.2", diff --git a/php/Admin/Admin.php b/php/Admin/Admin.php index 879a70c19..97e355ba4 100644 --- a/php/Admin/Admin.php +++ b/php/Admin/Admin.php @@ -16,6 +16,13 @@ */ class Admin extends ComponentAbstract { + /** + * Plugin settings. + * + * @var Settings + */ + public $settings; + /** * Plugin documentation. * @@ -55,18 +62,21 @@ class Admin extends ComponentAbstract { * Initialise the Admin component. */ public function init() { + $this->settings = new Settings(); + genesis_custom_blocks()->register_component( $this->settings ); + $this->documentation = new Documentation(); genesis_custom_blocks()->register_component( $this->documentation ); + $this->edit_block = new EditBlock(); + genesis_custom_blocks()->register_component( $this->edit_block ); + $this->onboarding = new Onboarding(); genesis_custom_blocks()->register_component( $this->onboarding ); $this->upgrade = new Upgrade(); genesis_custom_blocks()->register_component( $this->upgrade ); - $this->edit_block = new EditBlock(); - genesis_custom_blocks()->register_component( $this->edit_block ); - if ( defined( 'WP_LOAD_IMPORTERS' ) && WP_LOAD_IMPORTERS ) { $this->import = new Import(); genesis_custom_blocks()->register_component( $this->import ); diff --git a/php/Admin/Settings.php b/php/Admin/Settings.php new file mode 100644 index 000000000..b21a7d0a0 --- /dev/null +++ b/php/Admin/Settings.php @@ -0,0 +1,89 @@ +get_post_type_slug(), + __( 'Genesis Custom Blocks Settings', 'genesis-custom-blocks' ), + __( 'Settings', 'genesis-custom-blocks' ), + 'manage_options', + self::PAGE_SLUG, + [ $this, 'render_page' ] + ); + } + + /** + * Renders the Settings page. + */ + public function render_page() { + include genesis_custom_blocks()->get_path() . 'php/Views/Settings.php'; + } + + /** + * Register Genesis Custom Blocks settings. + */ + public function register_settings() { + register_setting( self::SETTINGS_GROUP, self::ANALYTICS_OPTION_NAME ); + } +} diff --git a/php/Blocks/Loader.php b/php/Blocks/Loader.php index 2c4c6d919..20c0a29f0 100644 --- a/php/Blocks/Loader.php +++ b/php/Blocks/Loader.php @@ -10,12 +10,20 @@ use WP_REST_Server; use WP_Query; use Genesis\CustomBlocks\ComponentAbstract; +use Genesis\CustomBlocks\Admin\Settings; /** * Class Loader */ class Loader extends ComponentAbstract { + /** + * The script slug for analytics. + * + * @var string + */ + const ANALYTICS_SCRIPT_SLUG = 'genesis-custom-blocks-analytics#async'; + /** * Asset paths and urls for blocks. * @@ -158,6 +166,22 @@ public function editor_assets() { ] ); + if ( Settings::ANALYTICS_OPTED_IN_VALUE === get_option( Settings::ANALYTICS_OPTION_NAME ) ) { + wp_enqueue_script( + self::ANALYTICS_SCRIPT_SLUG, + 'https://www.googletagmanager.com/gtag/js?id=UA-12345', // Todo: update this for GCB. + [], + genesis_custom_blocks()->get_version(), + true + ); + + wp_localize_script( + self::ANALYTICS_SCRIPT_SLUG, + 'gcbAnalyticsConfig', + [ 'ga_opt_in' => 1 ] + ); + } + // Enqueue optional editor only styles. wp_enqueue_style( $css_handle, diff --git a/php/Views/Settings.php b/php/Views/Settings.php new file mode 100644 index 000000000..6272bd0f6 --- /dev/null +++ b/php/Views/Settings.php @@ -0,0 +1,40 @@ + +
+

+
+ + + + + + +
+ + + name="" id="" class="regular-text" /> +
+ Custom Blocks > Settings. + */ + do_action( 'genesis_custom_blocks_render_settings_page' ); + + submit_button(); + ?> +
+
diff --git a/tests/php/Unit/Admin/TestSettings.php b/tests/php/Unit/Admin/TestSettings.php new file mode 100644 index 000000000..aede83d55 --- /dev/null +++ b/tests/php/Unit/Admin/TestSettings.php @@ -0,0 +1,95 @@ +instance = new Settings(); + $this->instance->set_plugin( genesis_custom_blocks() ); + + } + + /** + * Teardown. + * + * @inheritdoc + */ + public function tearDown() { + global $submenu; + + unset( $submenu[ self::SUBMENU_PARENT_SLUG ] ); + tearDown(); + parent::tearDown(); + } + + /** + * Test register_hooks. + * + * @covers \Genesis\CustomBlocksPro\Admin\Settings::register_hooks() + */ + public function test_register_hooks() { + $this->instance->register_hooks(); + $this->assertEquals( 10, has_action( 'admin_menu', [ $this->instance, 'add_submenu_pages' ] ) ); + } + + /** + * Test add_submenu_pages. + * + * @covers \Genesis\CustomBlocksPro\Admin\Settings::add_submenu_pages() + */ + public function test_add_submenu_pages() { + global $submenu; + + $expected_submenu_settings = [ + 'Settings', + 'manage_options', + Settings::PAGE_SLUG, + 'Genesis Custom Blocks Settings', + ]; + + wp_set_current_user( $this->factory()->user->create( [ 'role' => 'author' ] ) ); + $this->instance->add_submenu_pages(); + + // Because the current user doesn't have 'manage_options' permissions, this shouldn't add the submenu. + $this->assertFalse( isset( $submenu ) && array_key_exists( self::SUBMENU_PARENT_SLUG, $submenu ) ); + + wp_set_current_user( $this->factory()->user->create( [ 'role' => 'administrator' ] ) ); + $this->instance->add_submenu_pages(); + + // Now that the user has 'manage_options' permissions, this should add the submenu. + $this->assertEquals( [ $expected_submenu_settings ], $submenu[ self::SUBMENU_PARENT_SLUG ] ); + } +} diff --git a/tests/php/Unit/Blocks/TestLoader.php b/tests/php/Unit/Blocks/TestLoader.php index 2fbc37862..c2a0fd260 100644 --- a/tests/php/Unit/Blocks/TestLoader.php +++ b/tests/php/Unit/Blocks/TestLoader.php @@ -8,6 +8,7 @@ use Genesis\CustomBlocks\Blocks\Block; use Genesis\CustomBlocks\Blocks\Field; use Genesis\CustomBlocks\Blocks\Loader; +use Genesis\CustomBlocks\Admin\Settings; /** * Tests for class Loader. @@ -177,9 +178,27 @@ public function test_editor_assets() { wp_scripts()->registered[ $script_handle ]->extra['before'][1] ); + $this->assertFalse( wp_script_is( Loader::ANALYTICS_SCRIPT_SLUG ) ); $this->assertTrue( wp_style_is( $style_handle ) ); } + /** + * Test editor_assets when opted into analytics. + * + * @covers \Genesis\CustomBlocks\Blocks\Loader::editor_assets() + */ + public function test_editor_assets_analytics_opted_in() { + update_option( Settings::ANALYTICS_OPTION_NAME, Settings::ANALYTICS_OPTED_IN_VALUE ); + $this->instance->init(); + $this->instance->editor_assets(); + + $this->assertTrue( wp_script_is( Loader::ANALYTICS_SCRIPT_SLUG ) ); + $this->assertContains( + 'gcbAnalyticsConfig', + wp_scripts()->registered[ Loader::ANALYTICS_SCRIPT_SLUG ]->extra['data'] + ); + } + /** * Test render_block_template. *