From 4084f0b537075bd75fb815bbe613c5d70b40bb00 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Mon, 6 May 2024 16:12:35 +0300 Subject: [PATCH 001/134] Add the mailchimp provider. --- .../Conversion_Event_Providers/Mailchimp.php | 70 +++++++++++++++++++ .../Conversion_Tracking.php | 2 + 2 files changed, 72 insertions(+) create mode 100644 includes/Core/Conversion_Tracking/Conversion_Event_Providers/Mailchimp.php diff --git a/includes/Core/Conversion_Tracking/Conversion_Event_Providers/Mailchimp.php b/includes/Core/Conversion_Tracking/Conversion_Event_Providers/Mailchimp.php new file mode 100644 index 00000000000..9c4cdcb2a3a --- /dev/null +++ b/includes/Core/Conversion_Tracking/Conversion_Event_Providers/Mailchimp.php @@ -0,0 +1,70 @@ + $this->context->url( 'dist/assets/js/mailchimp.js' ), + 'execution' => 'async', + ) + ); + + $script->register( $this->context ); + + return $script; + } + +} diff --git a/includes/Core/Conversion_Tracking/Conversion_Tracking.php b/includes/Core/Conversion_Tracking/Conversion_Tracking.php index ae4e53abd37..28e8c9e5f7c 100644 --- a/includes/Core/Conversion_Tracking/Conversion_Tracking.php +++ b/includes/Core/Conversion_Tracking/Conversion_Tracking.php @@ -11,6 +11,7 @@ namespace Google\Site_Kit\Core\Conversion_Tracking; use Google\Site_Kit\Context; +use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers\Mailchimp; use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers\OptinMonster; use LogicException; @@ -37,6 +38,7 @@ class Conversion_Tracking { * @var array */ public static $providers = array( + Mailchimp::CONVERSION_EVENT_PROVIDER_SLUG => Mailchimp::class, OptinMonster::CONVERSION_EVENT_PROVIDER_SLUG => OptinMonster::class, ); From de45bbb7df259b9f7d390a062d7197ad05c3d57d Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Mon, 6 May 2024 16:15:08 +0300 Subject: [PATCH 002/134] Add phpunit test. --- .../MailchimpTest.php | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/phpunit/integration/Core/Conversion_Tracking/Conversion_Event_Providers/MailchimpTest.php diff --git a/tests/phpunit/integration/Core/Conversion_Tracking/Conversion_Event_Providers/MailchimpTest.php b/tests/phpunit/integration/Core/Conversion_Tracking/Conversion_Event_Providers/MailchimpTest.php new file mode 100644 index 00000000000..2e4f72dc09a --- /dev/null +++ b/tests/phpunit/integration/Core/Conversion_Tracking/Conversion_Event_Providers/MailchimpTest.php @@ -0,0 +1,48 @@ +mailchimp = new Mailchimp( new Context( GOOGLESITEKIT_PLUGIN_MAIN_FILE ) ); + } + + public function test_is_active() { + $this->assertFalse( $this->mailchimp->is_active() ); + define( 'MC4WP_VERSION', 1 ); + $this->assertTrue( $this->mailchimp->is_active() ); + } + + public function test_get_event_names() { + $events = $this->mailchimp->get_event_names(); + $this->assertCount( 1, $events ); + $this->assertEquals( 'submit_lead_form', $events[0] ); + } + + public function test_register_script() { + $script = $this->mailchimp->register_script(); + $this->assertInstanceOf( Script::class, $script ); + $this->assertTrue( wp_script_is( 'gsk-cep-' . Mailchimp::CONVERSION_EVENT_PROVIDER_SLUG, 'registered' ) ); + } + +} From c7e2c8201c530d4fe69429c2298259be1dff841c Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Mon, 6 May 2024 16:43:31 +0300 Subject: [PATCH 003/134] Add the script. --- assets/js/event-providers/mailchimp.js | 29 ++++++++++++++++++++++ webpack/conversionEventProviders.config.js | 1 + 2 files changed, 30 insertions(+) create mode 100644 assets/js/event-providers/mailchimp.js diff --git a/assets/js/event-providers/mailchimp.js b/assets/js/event-providers/mailchimp.js new file mode 100644 index 00000000000..d0291e461ea --- /dev/null +++ b/assets/js/event-providers/mailchimp.js @@ -0,0 +1,29 @@ +/** + * Site Kit by Google, Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +( ( mc4wp ) => { + if ( ! mc4wp ) { + return; + } + + mc4wp.forms.on( 'subscribed', function ( form ) { + global.gtag( 'event', 'submit_lead_form', { + event_category: 'mailchimp', + formID: form.id, + formName: form.name, + } ); + } ); +} )( global.mc4wp ); diff --git a/webpack/conversionEventProviders.config.js b/webpack/conversionEventProviders.config.js index a2112e797a5..093b650fa85 100644 --- a/webpack/conversionEventProviders.config.js +++ b/webpack/conversionEventProviders.config.js @@ -35,6 +35,7 @@ const { module.exports = ( mode ) => ( { entry: { + mailchimp: './assets/js/event-providers/mailchimp.js', 'optin-monster': './assets/js/event-providers/optin-monster.js', }, externals, From 7173dfd181a3c0854c0fac70f8c34ccaf3be194a Mon Sep 17 00:00:00 2001 From: Aleksej Date: Mon, 6 May 2024 16:12:40 +0200 Subject: [PATCH 004/134] Update prop type. --- assets/js/modules/ads/components/setup/SetupForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/modules/ads/components/setup/SetupForm.js b/assets/js/modules/ads/components/setup/SetupForm.js index 2742dce0aea..9119ecfbaff 100644 --- a/assets/js/modules/ads/components/setup/SetupForm.js +++ b/assets/js/modules/ads/components/setup/SetupForm.js @@ -96,7 +96,7 @@ export default function SetupForm( { SetupForm.propTypes = { finishSetup: PropTypes.func, - createAccountCTA: PropTypes.oneOf( [ PropTypes.node, null ] ), + createAccountCTA: PropTypes.oneOf( [ PropTypes.element, null ] ), }; SetupForm.defaultProps = { From 921e25da26dc56b03e36f0275b56a9d9b5f68674 Mon Sep 17 00:00:00 2001 From: Aleksej Date: Mon, 6 May 2024 16:13:22 +0200 Subject: [PATCH 005/134] Add onCampaignCreated callback. --- .../ads/components/setup/SetupMainPAX.js | 111 ++++++++++-------- 1 file changed, 60 insertions(+), 51 deletions(-) diff --git a/assets/js/modules/ads/components/setup/SetupMainPAX.js b/assets/js/modules/ads/components/setup/SetupMainPAX.js index 51aca7d04e6..a2a20828dcf 100644 --- a/assets/js/modules/ads/components/setup/SetupMainPAX.js +++ b/assets/js/modules/ads/components/setup/SetupMainPAX.js @@ -28,6 +28,7 @@ import { createInterpolateElement, Fragment, useCallback, + useState, } from '@wordpress/element'; import { __, _x } from '@wordpress/i18n'; @@ -51,6 +52,7 @@ const { useSelect, useDispatch, useRegistry } = Data; export default function SetupMainPAX( { finishSetup } ) { const [ pax, setPax ] = useQueryArg( 'pax' ); + const [ paxApp, setPaxApp ] = useState( null ); const registry = useRegistry(); @@ -84,60 +86,63 @@ export default function SetupMainPAX( { finishSetup } ) { const { setPaxConversionID, setExtCustomerID, submitChanges } = useDispatch( MODULES_ADS ); + const onCampaignCreated = useCallback( async () => { + if ( ! paxApp ) { + return; + } + + /* eslint-disable sitekit/acronym-case */ + // Disabling rule because function and property names + // are expected in current format by PAX API. + const customerData = await paxApp + .getServices() + ?.accountService?.getAccountId( {} ); + const conversionTrackingData = await paxApp + .getServices() + ?.conversionTrackingIdService?.getConversionTrackingId( {} ); + + if ( + customerData.externalCustomerId && + conversionTrackingData.conversionTrackingId + ) { + setExtCustomerID( customerData.externalCustomerId ); + setPaxConversionID( conversionTrackingData.conversionTrackingId ); + /* eslint-enable sitekit/acronym-case */ + } + + const { error } = await submitChanges(); + + // Since callback is mostly invoked pretty early, site info migth not be resolved yet. + // We need to ensure data is resolved otherwise admin url will be undefined and finishSetup + // will trigger console error instead of redirecting to the dashboard. + await registry.__experimentalResolveSelect( CORE_SITE ).getSiteInfo(); + + const adminURL = registry + .select( CORE_SITE ) + .getAdminURL( 'googlesitekit-dashboard', { + notification: 'authentication_success', + slug: 'ads', + } ); + + if ( ! error ) { + finishSetup( adminURL ); + } + }, [ + paxApp, + setExtCustomerID, + setPaxConversionID, + submitChanges, + registry, + finishSetup, + ] ); + const onLaunch = useCallback( - async ( app ) => { + ( app ) => { if ( app && typeof app?.getServices === 'function' ) { - /* eslint-disable sitekit/acronym-case */ - // Disabling rule because function and property names - // are expected in current format by PAX API. - const customerData = await app - .getServices() - ?.accountService?.getAccountId( {} ); - const conversionTrackingData = await app - .getServices() - ?.conversionTrackingIdService?.getConversionTrackingId( - {} - ); - - if ( - customerData.externalCustomerId && - conversionTrackingData.conversionTrackingId - ) { - setExtCustomerID( customerData.externalCustomerId ); - setPaxConversionID( - conversionTrackingData.conversionTrackingId - ); - /* eslint-enable sitekit/acronym-case */ - - const { error } = await submitChanges(); - - // Since callback is mostly invoked pretty early, site info is not resolved yet. - // We need to ensure data is resolved otherwise admin url will be undefined and finishSetup - // will trigger console error instead of redirecting to the dashboard. - await registry - .__experimentalResolveSelect( CORE_SITE ) - .getSiteInfo(); - - const adminURL = registry - .select( CORE_SITE ) - .getAdminURL( 'googlesitekit-dashboard', { - notification: 'authentication_success', - slug: 'ads', - } ); - - if ( ! error ) { - finishSetup( adminURL ); - } - } + setPaxApp( app ); } }, - [ - setExtCustomerID, - setPaxConversionID, - submitChanges, - registry, - finishSetup, - ] + [ setPaxApp ] ); const clickCallback = useCallback( () => { @@ -164,7 +169,11 @@ export default function SetupMainPAX( { finishSetup } ) { { ! isAdBlockerActive && ( pax && hasAdwordsScope ? ( - + ) : (
From 70b82b4b262d58ca5d0b929c79e7a80c785fe11c Mon Sep 17 00:00:00 2001 From: Aleksej Date: Mon, 6 May 2024 16:13:40 +0200 Subject: [PATCH 006/134] Update main PAX component. --- assets/js/modules/ads/components/PAXEmbeddedApp.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/assets/js/modules/ads/components/PAXEmbeddedApp.js b/assets/js/modules/ads/components/PAXEmbeddedApp.js index ab17ba7ccb3..25123f16b6b 100644 --- a/assets/js/modules/ads/components/PAXEmbeddedApp.js +++ b/assets/js/modules/ads/components/PAXEmbeddedApp.js @@ -49,6 +49,7 @@ export default function PAXEmbeddedApp( { // eslint-disable-next-line no-unused-vars displayMode = 'default', onLaunch, + onCampaignCreated, } ) { const [ launchGoogleAdsAvailable, setLaunchGoogleAdsAvailable ] = useState( typeof global?.google?.ads?.integration?.integrator?.launchGoogleAds === @@ -62,8 +63,8 @@ export default function PAXEmbeddedApp( { const registry = useRegistry(); const paxServices = useMemo( () => { - return createPaxServices( registry ); - }, [ registry ] ); + return createPaxServices( registry, onCampaignCreated ); + }, [ registry, onCampaignCreated ] ); const isAdBlockerActive = useSelect( ( select ) => select( CORE_USER ).isAdBlockerActive() From 98c7c42be21472aa53fadbc175bddeef7ec4879a Mon Sep 17 00:00:00 2001 From: Aleksej Date: Mon, 6 May 2024 16:14:46 +0200 Subject: [PATCH 007/134] Update services to include onCampaignCreated. --- assets/js/modules/ads/pax/services.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/assets/js/modules/ads/pax/services.js b/assets/js/modules/ads/pax/services.js index 4bc8914c1d8..e45149ab904 100644 --- a/assets/js/modules/ads/pax/services.js +++ b/assets/js/modules/ads/pax/services.js @@ -46,12 +46,14 @@ const restFetchWpPages = async () => { * Returns PAX services. * * @since 1.126.0 + * @since n.e.x.t Added onCampaignCreated parameter. * - * @param {Object} registry Registry object to dispatch to. + * @param {Object} registry Registry object to dispatch to. + * @param {Object} onCampaignCreated Optional. Callback function that will be called when campaign is created. * @return {Object} An object containing various service interfaces. */ -export function createPaxServices( registry ) { - return { +export function createPaxServices( registry, onCampaignCreated ) { + const services = { businessService: { getBusinessInfo: async () => { await registry @@ -89,4 +91,12 @@ export function createPaxServices( registry ) { notify: async () => {}, }, }; + + if ( onCampaignCreated ) { + services.campaignService = { + notifyNewCampaignCreated: onCampaignCreated, + }; + } + + return services; } From 08b958800a8e8cb000fcef0d763843e6cac8fb5c Mon Sep 17 00:00:00 2001 From: Aleksej Date: Mon, 6 May 2024 16:15:00 +0200 Subject: [PATCH 008/134] Update tests. --- assets/js/modules/ads/pax/services.test.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/assets/js/modules/ads/pax/services.test.js b/assets/js/modules/ads/pax/services.test.js index b561f5cc7bf..82dd7ba31e8 100644 --- a/assets/js/modules/ads/pax/services.test.js +++ b/assets/js/modules/ads/pax/services.test.js @@ -152,6 +152,28 @@ describe( 'PAX partner services', () => { ] ); } ); } ); + describe( 'campaignService', () => { + describe( 'notifyNewCampaignCreated', () => { + it( 'should return a callback function', async () => { + const mockOnCampaignCreated = jest.fn(); + const servicesWithCampaign = createPaxServices( + registry, + mockOnCampaignCreated + ); + + await servicesWithCampaign.campaignService.notifyNewCampaignCreated(); + + expect( servicesWithCampaign ).toEqual( + expect.objectContaining( { + campaignService: expect.objectContaining( { + notifyNewCampaignCreated: + mockOnCampaignCreated, + } ), + } ) + ); + } ); + } ); + } ); } ); } ); } ); From d7fc0b65afc00f665fbb283e6800e68c70edb59f Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Mon, 6 May 2024 19:03:14 +0300 Subject: [PATCH 009/134] Update the script to add mc4wp api as dependency. --- assets/js/event-providers/mailchimp.js | 4 +--- .../Conversion_Event_Providers/Mailchimp.php | 5 +++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/assets/js/event-providers/mailchimp.js b/assets/js/event-providers/mailchimp.js index d0291e461ea..7fdf32afd92 100644 --- a/assets/js/event-providers/mailchimp.js +++ b/assets/js/event-providers/mailchimp.js @@ -19,11 +19,9 @@ return; } - mc4wp.forms.on( 'subscribed', function ( form ) { + mc4wp.forms.on( 'subscribed', () => { global.gtag( 'event', 'submit_lead_form', { event_category: 'mailchimp', - formID: form.id, - formName: form.name, } ); } ); } )( global.mc4wp ); diff --git a/includes/Core/Conversion_Tracking/Conversion_Event_Providers/Mailchimp.php b/includes/Core/Conversion_Tracking/Conversion_Event_Providers/Mailchimp.php index 9c4cdcb2a3a..402115bee9a 100644 --- a/includes/Core/Conversion_Tracking/Conversion_Event_Providers/Mailchimp.php +++ b/includes/Core/Conversion_Tracking/Conversion_Event_Providers/Mailchimp.php @@ -57,8 +57,9 @@ public function register_script() { $script = new Script( 'gsk-cep-' . self::CONVERSION_EVENT_PROVIDER_SLUG, array( - 'src' => $this->context->url( 'dist/assets/js/mailchimp.js' ), - 'execution' => 'async', + 'src' => $this->context->url( 'dist/assets/js/mailchimp.js' ), + 'execution' => 'async', + 'dependencies' => array( 'mc4wp-forms-api' ), ) ); From b3262b368d13581699eceae30ff9041e371fa96f Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Tue, 7 May 2024 14:58:37 +0300 Subject: [PATCH 010/134] Update tests. --- .../Conversion_Event_Providers/MailchimpTest.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/integration/Core/Conversion_Tracking/Conversion_Event_Providers/MailchimpTest.php b/tests/phpunit/integration/Core/Conversion_Tracking/Conversion_Event_Providers/MailchimpTest.php index 2e4f72dc09a..189b06f1f6d 100644 --- a/tests/phpunit/integration/Core/Conversion_Tracking/Conversion_Event_Providers/MailchimpTest.php +++ b/tests/phpunit/integration/Core/Conversion_Tracking/Conversion_Event_Providers/MailchimpTest.php @@ -27,6 +27,16 @@ public function set_up() { $this->mailchimp = new Mailchimp( new Context( GOOGLESITEKIT_PLUGIN_MAIN_FILE ) ); } + public static function tear_down_after_class() { + parent::tear_down_after_class(); + + if ( function_exists( 'runkit7_constant_remove' ) ) { + runkit7_constant_remove( 'MC4WP_VERSION' ); + } elseif ( function_exists( 'runkit_constant_remove' ) ) { + runkit_constant_remove( 'MC4WP_VERSION' ); + } + } + public function test_is_active() { $this->assertFalse( $this->mailchimp->is_active() ); define( 'MC4WP_VERSION', 1 ); @@ -40,9 +50,12 @@ public function test_get_event_names() { } public function test_register_script() { + $handle = 'gsk-cep-' . Mailchimp::CONVERSION_EVENT_PROVIDER_SLUG; + $this->assertTrue( wp_script_is( $handle, 'registered' ) ); + $script = $this->mailchimp->register_script(); $this->assertInstanceOf( Script::class, $script ); - $this->assertTrue( wp_script_is( 'gsk-cep-' . Mailchimp::CONVERSION_EVENT_PROVIDER_SLUG, 'registered' ) ); + $this->assertTrue( wp_script_is( $handle, 'registered' ) ); } } From f8d3bbb9ec748192cb2125c6c6ecc7311f65e38e Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Tue, 7 May 2024 16:49:11 +0300 Subject: [PATCH 011/134] Fix broken test. --- .../Conversion_Event_Providers/MailchimpTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/integration/Core/Conversion_Tracking/Conversion_Event_Providers/MailchimpTest.php b/tests/phpunit/integration/Core/Conversion_Tracking/Conversion_Event_Providers/MailchimpTest.php index 189b06f1f6d..7403a0cfdd5 100644 --- a/tests/phpunit/integration/Core/Conversion_Tracking/Conversion_Event_Providers/MailchimpTest.php +++ b/tests/phpunit/integration/Core/Conversion_Tracking/Conversion_Event_Providers/MailchimpTest.php @@ -51,7 +51,7 @@ public function test_get_event_names() { public function test_register_script() { $handle = 'gsk-cep-' . Mailchimp::CONVERSION_EVENT_PROVIDER_SLUG; - $this->assertTrue( wp_script_is( $handle, 'registered' ) ); + $this->assertFalse( wp_script_is( $handle, 'registered' ) ); $script = $this->mailchimp->register_script(); $this->assertInstanceOf( Script::class, $script ); From 8484e2b57645ce6dcde21e01e403ed5aeda19594 Mon Sep 17 00:00:00 2001 From: Aleksej Date: Tue, 7 May 2024 18:36:20 +0200 Subject: [PATCH 012/134] Add popup-maker.js. --- assets/js/event-providers/popup-maker.js | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 assets/js/event-providers/popup-maker.js diff --git a/assets/js/event-providers/popup-maker.js b/assets/js/event-providers/popup-maker.js new file mode 100644 index 00000000000..b6d4b4f399a --- /dev/null +++ b/assets/js/event-providers/popup-maker.js @@ -0,0 +1,28 @@ +/** + * Site Kit by Google, Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +document.addEventListener( 'DOMContentLoaded', () => { + const { jQuery } = global; + // eslint-disable-next-line no-undef + if ( ! jQuery || ! PUM ) { + return; + } + + // eslint-disable-next-line no-undef + PUM.hooks.addAction( 'pum.integration.form.success', function () { + global.gtag( 'event', 'submit_lead_form' ); + } ); +} ); From 406f6993c5386a2f1066f2d324b80f92f92e251b Mon Sep 17 00:00:00 2001 From: Aleksej Date: Tue, 7 May 2024 18:37:16 +0200 Subject: [PATCH 013/134] Add popup maker JS file to the webpack partial. --- webpack/conversionEventProviders.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/webpack/conversionEventProviders.config.js b/webpack/conversionEventProviders.config.js index a2112e797a5..a03164071fc 100644 --- a/webpack/conversionEventProviders.config.js +++ b/webpack/conversionEventProviders.config.js @@ -36,6 +36,7 @@ const { module.exports = ( mode ) => ( { entry: { 'optin-monster': './assets/js/event-providers/optin-monster.js', + 'popup-maker': './assets/js/event-providers/popup-maker.js', }, externals, output: { From 64045fad0619af1604794cf73e24cedf58ab5ced Mon Sep 17 00:00:00 2001 From: Aleksej Date: Tue, 7 May 2024 18:37:49 +0200 Subject: [PATCH 014/134] Add popup maker to the Conversion_Tracking class. --- includes/Core/Conversion_Tracking/Conversion_Tracking.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/includes/Core/Conversion_Tracking/Conversion_Tracking.php b/includes/Core/Conversion_Tracking/Conversion_Tracking.php index ae4e53abd37..118583bbe6a 100644 --- a/includes/Core/Conversion_Tracking/Conversion_Tracking.php +++ b/includes/Core/Conversion_Tracking/Conversion_Tracking.php @@ -12,6 +12,7 @@ use Google\Site_Kit\Context; use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers\OptinMonster; +use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Event_Providers\PopupMaker; use LogicException; /** @@ -38,6 +39,7 @@ class Conversion_Tracking { */ public static $providers = array( OptinMonster::CONVERSION_EVENT_PROVIDER_SLUG => OptinMonster::class, + PopupMaker::CONVERSION_EVENT_PROVIDER_SLUG => PopupMaker::class, ); /** From 537c928f09a81dc58b77fa9056e230262d78ae09 Mon Sep 17 00:00:00 2001 From: Aleksej Date: Tue, 7 May 2024 18:38:31 +0200 Subject: [PATCH 015/134] Add PopupMaker class. --- .../Conversion_Event_Providers/PopupMaker.php | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 includes/Core/Conversion_Tracking/Conversion_Event_Providers/PopupMaker.php diff --git a/includes/Core/Conversion_Tracking/Conversion_Event_Providers/PopupMaker.php b/includes/Core/Conversion_Tracking/Conversion_Event_Providers/PopupMaker.php new file mode 100644 index 00000000000..b9e7a2aa8d4 --- /dev/null +++ b/includes/Core/Conversion_Tracking/Conversion_Event_Providers/PopupMaker.php @@ -0,0 +1,71 @@ + $this->context->url( 'dist/assets/js/popup-maker.js' ), + 'dependencies' => array( 'popup-maker-site' ), + 'execution' => 'async', + ) + ); + + $script->register( $this->context ); + + return $script; + } + +} From ac9a81b84314c551d43b119b3f60b73479f0b231 Mon Sep 17 00:00:00 2001 From: Aleksej Date: Wed, 8 May 2024 13:29:54 +0200 Subject: [PATCH 016/134] Add tests. --- .../PopupMakerTest.php | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/phpunit/integration/Core/Conversion_Tracking/Conversion_Event_Providers/PopupMakerTest.php diff --git a/tests/phpunit/integration/Core/Conversion_Tracking/Conversion_Event_Providers/PopupMakerTest.php b/tests/phpunit/integration/Core/Conversion_Tracking/Conversion_Event_Providers/PopupMakerTest.php new file mode 100644 index 00000000000..5d03158868f --- /dev/null +++ b/tests/phpunit/integration/Core/Conversion_Tracking/Conversion_Event_Providers/PopupMakerTest.php @@ -0,0 +1,49 @@ +popupmaker = new PopupMaker( new Context( GOOGLESITEKIT_PLUGIN_MAIN_FILE ) ); + } + + public function test_is_active() { + $this->assertFalse( $this->popupmaker->is_active() ); + define( 'POPMAKE_VERSION', 1 ); + $this->assertTrue( $this->popupmaker->is_active() ); + } + + public function test_get_event_names() { + $events = $this->popupmaker->get_event_names(); + $this->assertCount( 1, $events ); + $this->assertEquals( 'submit_lead_form', $events[0] ); + } + + public function test_register_script() { + $script = $this->popupmaker->register_script(); + $this->assertInstanceOf( Script::class, $script ); + $this->assertTrue( wp_script_is( 'gsk-cep-' . PopupMaker::CONVERSION_EVENT_PROVIDER_SLUG, 'registered' ) ); + } + +} From 78139e25ed9e9aeae2592fb4bd1dfbfc1c338fa8 Mon Sep 17 00:00:00 2001 From: Tom Rees-Herdman Date: Wed, 8 May 2024 12:33:25 +0100 Subject: [PATCH 017/134] Initial pass extracting a reusable set of Selection Panel components. --- .../MetricsSelectionPanel/MetricItem.js | 78 +++--- .../{Metrics.js => MetricItems.js} | 82 ++----- .../{Footer.js => MetricsFooter.js} | 222 +++++------------- .../{Header.js => MetricsHeader.js} | 28 +-- .../KeyMetrics/MetricsSelectionPanel/index.js | 25 +- .../SelectionPanel/SelectionPanel.js | 23 ++ .../SelectionPanel/SelectionPanelFooter.js | 160 +++++++++++++ .../SelectionPanel/SelectionPanelHeader.js | 24 ++ .../SelectionPanel/SelectionPanelItem.js | 28 +++ .../SelectionPanel/SelectionPanelItems.js | 59 +++++ assets/js/components/SelectionPanel/index.js | 7 + 11 files changed, 443 insertions(+), 293 deletions(-) rename assets/js/components/KeyMetrics/MetricsSelectionPanel/{Metrics.js => MetricItems.js} (56%) rename assets/js/components/KeyMetrics/MetricsSelectionPanel/{Footer.js => MetricsFooter.js} (52%) rename assets/js/components/KeyMetrics/MetricsSelectionPanel/{Header.js => MetricsHeader.js} (72%) create mode 100644 assets/js/components/SelectionPanel/SelectionPanel.js create mode 100644 assets/js/components/SelectionPanel/SelectionPanelFooter.js create mode 100644 assets/js/components/SelectionPanel/SelectionPanelHeader.js create mode 100644 assets/js/components/SelectionPanel/SelectionPanelItem.js create mode 100644 assets/js/components/SelectionPanel/SelectionPanelItems.js create mode 100644 assets/js/components/SelectionPanel/index.js diff --git a/assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricItem.js b/assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricItem.js index fb27efb353d..adaaaff3651 100644 --- a/assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricItem.js +++ b/assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricItem.js @@ -32,8 +32,10 @@ import { __, _n, sprintf } from '@wordpress/i18n'; */ import Data from 'googlesitekit-data'; import { CORE_FORMS } from '../../../googlesitekit/datastore/forms/constants'; +import { CORE_WIDGETS } from '../../../googlesitekit/widgets/datastore/constants'; +import { CORE_MODULES } from '../../../googlesitekit/modules/datastore/constants'; import { KEY_METRICS_SELECTED, KEY_METRICS_SELECTION_FORM } from '../constants'; -import SelectionBox from '../../SelectionBox'; +import { SelectionPanelItem } from '../../SelectionPanel'; const { useSelect, useDispatch } = Data; export default function MetricItem( { @@ -41,9 +43,25 @@ export default function MetricItem( { slug, title, description, - disconnectedModules, savedMetrics = [], } ) { + const { getModule } = useSelect( ( select ) => select( CORE_MODULES ) ); + const widget = useSelect( ( select ) => select( CORE_WIDGETS ) ).getWidget( + slug + ); + + const disconnectedModules = widget.modules.reduce( + ( modulesAcc, widgetSlug ) => { + const module = getModule( widgetSlug ); + if ( module?.connected || ! module?.name ) { + return modulesAcc; + } + + return [ ...modulesAcc, module.name ]; + }, + [] + ); + const selectedMetrics = useSelect( ( select ) => select( CORE_FORMS ).getValue( KEY_METRICS_SELECTION_FORM, @@ -77,34 +95,33 @@ export default function MetricItem( { ! savedMetrics.includes( slug ) && disconnectedModules.length > 0; return ( -
- - { description } - { disconnectedModules.length > 0 && ( -
- { sprintf( - /* translators: %s: module names. */ - _n( - '%s is disconnected, no data to show', - '%s are disconnected, no data to show', - disconnectedModules.length, - 'google-site-kit' - ), - disconnectedModules.join( - __( ' and ', 'google-site-kit' ) - ) - ) } -
- ) } -
-
+ + { disconnectedModules.length > 0 && ( +
+ { sprintf( + /* translators: %s: module names. */ + _n( + '%s is disconnected, no data to show', + '%s are disconnected, no data to show', + disconnectedModules.length, + 'google-site-kit' + ), + disconnectedModules.join( + __( ' and ', 'google-site-kit' ) + ) + ) } +
+ ) } +
); } @@ -113,6 +130,5 @@ MetricItem.propTypes = { slug: PropTypes.string.isRequired, title: PropTypes.string.isRequired, description: PropTypes.string.isRequired, - disconnectedModules: PropTypes.array, savedMetrics: PropTypes.array, }; diff --git a/assets/js/components/KeyMetrics/MetricsSelectionPanel/Metrics.js b/assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricItems.js similarity index 56% rename from assets/js/components/KeyMetrics/MetricsSelectionPanel/Metrics.js rename to assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricItems.js index 439d06799ff..7c23baafc99 100644 --- a/assets/js/components/KeyMetrics/MetricsSelectionPanel/Metrics.js +++ b/assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricItems.js @@ -33,22 +33,19 @@ import Data from 'googlesitekit-data'; import { AREA_MAIN_DASHBOARD_KEY_METRICS_PRIMARY } from '../../../googlesitekit/widgets/default-areas'; import { CORE_USER } from '../../../googlesitekit/datastore/user/constants'; import { CORE_WIDGETS } from '../../../googlesitekit/widgets/datastore/constants'; -import { CORE_MODULES } from '../../../googlesitekit/modules/datastore/constants'; import { KEY_METRICS_WIDGETS } from '../key-metrics-widgets'; import MetricItem from './MetricItem'; import useViewOnly from '../../../hooks/useViewOnly'; -import { Fragment } from '@wordpress/element'; +import { SelectionPanelItems } from '../../SelectionPanel'; const { useSelect } = Data; -export default function Metrics( { savedMetrics } ) { +export default function MetricItems( { savedMetrics } ) { const isViewOnlyDashboard = useViewOnly(); const { isKeyMetricAvailable } = useSelect( ( select ) => select( CORE_USER ) ); - const { getModule } = useSelect( ( select ) => select( CORE_MODULES ) ); - const displayInList = useSelect( ( select ) => ( metric ) => KEY_METRICS_WIDGETS[ metric ].displayInList( @@ -57,10 +54,6 @@ export default function Metrics( { savedMetrics } ) { ) ); - const getWidget = useSelect( - ( select ) => ( metric ) => select( CORE_WIDGETS ).getWidget( metric ) - ); - const metricsListReducer = ( acc, metric ) => { if ( ! isKeyMetricAvailable( metric ) ) { return acc; @@ -73,25 +66,10 @@ export default function Metrics( { savedMetrics } ) { return acc; } - const widget = getWidget( metric ); - - const disconnectedModules = widget.modules.reduce( - ( modulesAcc, slug ) => { - const module = getModule( slug ); - if ( module?.connected || ! module?.name ) { - return modulesAcc; - } - - return [ ...modulesAcc, module.name ]; - }, - [] - ); - return { ...acc, [ metric ]: { ...KEY_METRICS_WIDGETS[ metric ], - disconnectedModules, }, }; }; @@ -119,53 +97,21 @@ export default function Metrics( { savedMetrics } ) { } ) .reduce( metricsListReducer, {} ); - const renderMetricItems = ( metricSlugs ) => { - return Object.keys( metricSlugs ).map( ( slug ) => { - const { title, description, disconnectedModules } = - metricSlugs[ slug ]; - - const id = `key-metric-selection-checkbox-${ slug }`; - - return ( - - ); - } ); - }; - return ( -
- { - // Split list into two sections with sub-headings for current selection and - // additional metrics if there are already saved metrics. - savedMetrics.length !== 0 && ( - -

- { __( 'Current selection', 'google-site-kit' ) } -

-
- { renderMetricItems( availableSavedMetrics ) } -
-

- { __( 'Additional metrics', 'google-site-kit' ) } -

-
- ) - } -
- { renderMetricItems( availableUnsavedMetrics ) } -
-
+ ); } -Metrics.propTypes = { +MetricItems.propTypes = { savedMetrics: PropTypes.array, }; diff --git a/assets/js/components/KeyMetrics/MetricsSelectionPanel/Footer.js b/assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricsFooter.js similarity index 52% rename from assets/js/components/KeyMetrics/MetricsSelectionPanel/Footer.js rename to assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricsFooter.js index 9b61d7f82b0..9921c9f9ca4 100644 --- a/assets/js/components/KeyMetrics/MetricsSelectionPanel/Footer.js +++ b/assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricsFooter.js @@ -19,26 +19,18 @@ /** * External dependencies */ -import { isEqual } from 'lodash'; import PropTypes from 'prop-types'; /** * WordPress dependencies */ -import { - useCallback, - useEffect, - useState, - useMemo, - createInterpolateElement, -} from '@wordpress/element'; +import { useCallback } from '@wordpress/element'; import { addQueryArgs } from '@wordpress/url'; import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies */ -import { Button, SpinnerButton } from 'googlesitekit-components'; import Data from 'googlesitekit-data'; import { CORE_USER } from '../../../googlesitekit/datastore/user/constants'; import { CORE_FORMS } from '../../../googlesitekit/datastore/forms/constants'; @@ -59,13 +51,14 @@ import { } from '../../../modules/analytics-4/datastore/constants'; import { KEY_METRICS_WIDGETS } from '../key-metrics-widgets'; import { ERROR_CODE_MISSING_REQUIRED_SCOPE } from '../../../util/errors'; -import ErrorNotice from '../../ErrorNotice'; -import { safelySort } from './utils'; import useViewContext from '../../../hooks/useViewContext'; import { trackEvent } from '../../../util'; +import { SelectionPanelFooter } from '../../SelectionPanel'; const { useSelect, useDispatch } = Data; -export default function Footer( { +export default function MetricsFooter( { + isOpen, + closeFn, savedMetrics, onNavigationToOAuthURL = () => {}, } ) { @@ -85,14 +78,6 @@ export default function Footer( { ); const trackingCategory = `${ viewContext }_kmw-sidebar`; - const haveSettingsChanged = useMemo( () => { - // Arrays need to be sorted to match in `isEqual`. - return ! isEqual( - safelySort( selectedMetrics ), - safelySort( savedMetrics ) - ); - }, [ savedMetrics, selectedMetrics ] ); - const requiredCustomDimensions = selectedMetrics?.flatMap( ( tileName ) => { const tile = KEY_METRICS_WIDGETS[ tileName ]; return tile?.requiredCustomDimensions || []; @@ -147,78 +132,60 @@ export default function Footer( { return select( CORE_LOCATION ).isNavigatingTo( OAuthURL ); } ); - const isOpen = useSelect( ( select ) => - select( CORE_UI ).getValue( KEY_METRICS_SELECTION_PANEL_OPENED_KEY ) - ); - const { saveKeyMetricsSettings, setPermissionScopeError } = useDispatch( CORE_USER ); const { setValue } = useDispatch( CORE_UI ); const { setValues } = useDispatch( CORE_FORMS ); - const [ finalButtonText, setFinalButtonText ] = useState( null ); - const [ wasSaved, setWasSaved ] = useState( false ); - - const currentButtonText = - savedMetrics?.length > 0 && haveSettingsChanged - ? __( 'Apply changes', 'google-site-kit' ) - : __( 'Save selection', 'google-site-kit' ); - - const onSaveClick = useCallback( async () => { - const { error } = await saveKeyMetricsSettings( { - widgetSlugs: selectedMetrics, - } ); - - if ( ! error ) { - trackEvent( trackingCategory, 'metrics_sidebar_save' ); + const saveSettings = useCallback( + async ( widgetSlugs ) => { + // We could simply return the value of `saveKeyMetricsSettings()` here, + // but this makes the expected return value more explicit. + const { error } = await saveKeyMetricsSettings( { + widgetSlugs, + } ); + + return { error }; + }, + [ saveKeyMetricsSettings ] + ); - if ( isGA4Connected && hasMissingCustomDimensions ) { - setValues( FORM_CUSTOM_DIMENSIONS_CREATE, { - autoSubmit: true, + const onSaveSuccess = useCallback( () => { + trackEvent( trackingCategory, 'metrics_sidebar_save' ); + + if ( isGA4Connected && hasMissingCustomDimensions ) { + setValues( FORM_CUSTOM_DIMENSIONS_CREATE, { + autoSubmit: true, + } ); + + if ( ! hasAnalytics4EditScope ) { + // Let parent component know that the user is navigating to OAuth URL + // so that the panel is kept open. + onNavigationToOAuthURL(); + + // Ensure the state is set, just in case the user navigates to the + // OAuth URL before the function is fully executed. + setValue( KEY_METRICS_SELECTION_PANEL_OPENED_KEY, false ); // TODO: Can call `closeFn()` instead. + + setPermissionScopeError( { + code: ERROR_CODE_MISSING_REQUIRED_SCOPE, + message: __( + 'Additional permissions are required to create new Analytics custom dimensions', + 'google-site-kit' + ), + data: { + status: 403, + scopes: [ EDIT_SCOPE ], + skipModal: true, + redirectURL, + }, } ); - - if ( ! hasAnalytics4EditScope ) { - // Let parent component know that the user is navigating to OAuth URL - // so that the panel is kept open. - onNavigationToOAuthURL(); - - // Ensure the state is set, just in case the user navigates to the - // OAuth URL before the function is fully executed. - setValue( KEY_METRICS_SELECTION_PANEL_OPENED_KEY, false ); - - setPermissionScopeError( { - code: ERROR_CODE_MISSING_REQUIRED_SCOPE, - message: __( - 'Additional permissions are required to create new Analytics custom dimensions', - 'google-site-kit' - ), - data: { - status: 403, - scopes: [ EDIT_SCOPE ], - skipModal: true, - redirectURL, - }, - } ); - } } - - // If the state has not been set to `false` yet, set it now. - if ( isOpen ) { - setValue( KEY_METRICS_SELECTION_PANEL_OPENED_KEY, false ); - } - - // lock the button label while panel is closing - setFinalButtonText( currentButtonText ); - setWasSaved( true ); } }, [ - saveKeyMetricsSettings, - selectedMetrics, trackingCategory, isGA4Connected, hasMissingCustomDimensions, - isOpen, - currentButtonText, setValues, hasAnalytics4EditScope, onNavigationToOAuthURL, @@ -227,29 +194,9 @@ export default function Footer( { redirectURL, ] ); - const onCancelClick = useCallback( () => { - setValue( KEY_METRICS_SELECTION_PANEL_OPENED_KEY, false ); + const onCancel = useCallback( () => { trackEvent( trackingCategory, 'metrics_sidebar_cancel' ); - }, [ setValue, trackingCategory ] ); - - const [ prevIsOpen, setPrevIsOpen ] = useState( null ); - - useEffect( () => { - if ( prevIsOpen !== null ) { - // if current isOpen is true, and different from prevIsOpen - // meaning it transitioned from false to true and it is not - // in closing transition, we should reset the button label - // locked when save button was clicked - if ( prevIsOpen !== isOpen ) { - if ( isOpen ) { - setFinalButtonText( null ); - setWasSaved( false ); - } - } - } - - setPrevIsOpen( isOpen ); - }, [ isOpen, prevIsOpen ] ); + }, [ trackingCategory ] ); const selectedMetricsCount = selectedMetrics?.length || 0; let metricsLimitError; @@ -277,67 +224,24 @@ export default function Footer( { } return ( -
- { saveError && } -
- { haveSettingsChanged && metricsLimitError ? ( - MAX_SELECTED_METRICS_COUNT - } - /> - ) : ( -

- { createInterpolateElement( - sprintf( - /* translators: 1: Number of selected metrics. 2: Maximum number of metrics that can be selected. */ - __( - '%1$d selected (up to %2$d)', - 'google-site-kit' - ), - selectedMetricsCount, - MAX_SELECTED_METRICS_COUNT - ), - { - MaxCount: ( - - ), - } - ) } -

- ) } -
- - MAX_SELECTED_METRICS_COUNT || - isSavingSettings || - ( ! isOpen && wasSaved ) || - isNavigatingToOAuthURL - } - > - { finalButtonText || currentButtonText } - -
-
-
+ ); } -Footer.propTypes = { +MetricsFooter.propTypes = { savedMetrics: PropTypes.array, onNavigationToOAuthURL: PropTypes.func, }; diff --git a/assets/js/components/KeyMetrics/MetricsSelectionPanel/Header.js b/assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricsHeader.js similarity index 72% rename from assets/js/components/KeyMetrics/MetricsSelectionPanel/Header.js rename to assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricsHeader.js index 3229cc4aecc..74d2c8f0765 100644 --- a/assets/js/components/KeyMetrics/MetricsSelectionPanel/Header.js +++ b/assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricsHeader.js @@ -28,15 +28,13 @@ import { __ } from '@wordpress/i18n'; import Data from 'googlesitekit-data'; import { CORE_LOCATION } from '../../../googlesitekit/datastore/location/constants'; import { CORE_SITE } from '../../../googlesitekit/datastore/site/constants'; -import { CORE_UI } from '../../../googlesitekit/datastore/ui/constants'; import { CORE_USER } from '../../../googlesitekit/datastore/user/constants'; -import { KEY_METRICS_SELECTION_PANEL_OPENED_KEY } from '../constants'; import Link from '../../Link'; -import CloseIcon from '../../../../svg/icons/close.svg'; import useViewOnly from '../../../hooks/useViewOnly'; +import { SelectionPanelHeader } from '../../SelectionPanel'; const { useSelect, useDispatch } = Data; -export default function Header() { +export default function MetricsHeader( { closeFn } ) { const isViewOnly = useViewOnly(); const settingsURL = useSelect( ( select ) => @@ -46,30 +44,18 @@ export default function Header() { select( CORE_USER ).isSavingKeyMetricsSettings() ); - const { setValue } = useDispatch( CORE_UI ); const { navigateTo } = useDispatch( CORE_LOCATION ); - const onCloseClick = useCallback( () => { - setValue( KEY_METRICS_SELECTION_PANEL_OPENED_KEY, false ); - }, [ setValue ] ); - const onSettingsClick = useCallback( () => navigateTo( `${ settingsURL }#/admin-settings` ), [ navigateTo, settingsURL ] ); return ( -
-
-

{ __( 'Select your metrics', 'google-site-kit' ) }

- - - -
+ { ! isViewOnly && (

{ createInterpolateElement( @@ -90,6 +76,6 @@ export default function Header() { ) }

) } -
+ ); } diff --git a/assets/js/components/KeyMetrics/MetricsSelectionPanel/index.js b/assets/js/components/KeyMetrics/MetricsSelectionPanel/index.js index 49ce12c9bd6..d1cda8da41f 100644 --- a/assets/js/components/KeyMetrics/MetricsSelectionPanel/index.js +++ b/assets/js/components/KeyMetrics/MetricsSelectionPanel/index.js @@ -33,13 +33,13 @@ import { KEY_METRICS_SELECTION_FORM, KEY_METRICS_SELECTION_PANEL_OPENED_KEY, } from '../constants'; -import SideSheet from '../../SideSheet'; -import Header from './Header'; -import Footer from './Footer'; -import Metrics from './Metrics'; +import MetricsHeader from './MetricsHeader'; +import MetricsFooter from './MetricsFooter'; +import MetricItems from './MetricItems'; import CustomDimensionsNotice from './CustomDimensionsNotice'; import useViewContext from '../../../hooks/useViewContext'; import { trackEvent } from '../../../util'; +import SelectionPanel from '../../SelectionPanel'; const { useSelect, useDispatch } = Data; export default function MetricsSelectionPanel() { @@ -79,25 +79,22 @@ export default function MetricsSelectionPanel() { useState( false ); return ( - -
- + + -
- { ! adsModuleEnabled && useSnippet && ( + { useSnippet && (
@@ -94,7 +91,7 @@ export default function OptionalSettingsView() {
) } - { adsModuleEnabled && } + ); } diff --git a/assets/js/modules/analytics-4/components/settings/SettingsForm.js b/assets/js/modules/analytics-4/components/settings/SettingsForm.js index 57e4e6a0e5d..34b0f776c11 100644 --- a/assets/js/modules/analytics-4/components/settings/SettingsForm.js +++ b/assets/js/modules/analytics-4/components/settings/SettingsForm.js @@ -30,18 +30,15 @@ import { Fragment } from '@wordpress/element'; * Internal dependencies */ import Data from 'googlesitekit-data'; -import { AdsConversionIDTextField, TrackingExclusionSwitches } from '../common'; +import { TrackingExclusionSwitches } from '../common'; import { MODULES_ANALYTICS_4 } from '../../datastore/constants'; import SettingsControls from './SettingsControls'; import AdsConversionIDSettingsNotice from './AdsConversionIDSettingsNotice'; import EntityOwnershipChangeNotice from '../../../../components/settings/EntityOwnershipChangeNotice'; import { isValidAccountID } from '../../utils/validation'; -import { useFeature } from '../../../../hooks/useFeature'; const { useSelect } = Data; export default function SettingsForm( { hasModuleAccess } ) { - const adsModuleEnabled = useFeature( 'adsModule' ); - const accountID = useSelect( ( select ) => select( MODULES_ANALYTICS_4 ).getAccountID() ); @@ -53,8 +50,7 @@ export default function SettingsForm( { hasModuleAccess } ) { { isValidAccountID( accountID ) && ( - { ! adsModuleEnabled && } - { adsModuleEnabled && } + ) } diff --git a/assets/js/modules/analytics-4/hooks/useMigrateAdsConversionID.js b/assets/js/modules/analytics-4/hooks/useMigrateAdsConversionID.js index 2e03422d7ba..925f42026f1 100644 --- a/assets/js/modules/analytics-4/hooks/useMigrateAdsConversionID.js +++ b/assets/js/modules/analytics-4/hooks/useMigrateAdsConversionID.js @@ -28,7 +28,6 @@ import Data from 'googlesitekit-data'; import { CORE_MODULES } from '../../../googlesitekit/modules/datastore/constants'; import { MODULES_ANALYTICS_4 } from '../datastore/constants'; import { MODULES_ADS } from '../../ads/datastore/constants'; -import { useFeature } from '../../../hooks/useFeature'; const { useSelect, useDispatch } = Data; @@ -42,8 +41,6 @@ const { useSelect, useDispatch } = Data; export default function useMigrateAdsConversionID() { const [ loading, setLoading ] = useState( false ); - const adsModuleEnabled = useFeature( 'adsModule' ); - const legacyAdsConversionID = useSelect( ( select ) => select( MODULES_ANALYTICS_4 ).getAdsConversionID() ); @@ -60,7 +57,7 @@ export default function useMigrateAdsConversionID() { select( CORE_MODULES ).isModuleConnected( 'ads' ) ); const adsConversionID = useSelect( ( select ) => { - if ( ! adsModuleEnabled || ! adsModuleAvailable ) { + if ( ! adsModuleAvailable ) { return null; } @@ -74,12 +71,10 @@ export default function useMigrateAdsConversionID() { submitChanges: submitAnalyticsChanges, } = useDispatch( MODULES_ANALYTICS_4 ); - // TODO: Destructure actions here when the `adsModule` feature flag is removed. - const dispatch = useDispatch( MODULES_ADS ); + const { setConversionID, submitChanges } = useDispatch( MODULES_ADS ); useEffect( () => { if ( - ! adsModuleEnabled || isDoingSubmitChanges || loading || ! adsModuleAvailable || @@ -93,8 +88,8 @@ export default function useMigrateAdsConversionID() { const migrate = async () => { setLoading( true ); - await dispatch.setConversionID( legacyAdsConversionID ); - await dispatch.submitChanges(); + await setConversionID( legacyAdsConversionID ); + await submitChanges(); await setLegacyAdsConversionID( '' ); await setAdsConversionIDMigratedAtMs( Date.now() ); @@ -120,15 +115,15 @@ export default function useMigrateAdsConversionID() { adsModuleActive, adsModuleAvailable, adsModuleConnected, - adsModuleEnabled, - dispatch, fetchGetModules, isDoingSubmitChanges, legacyAdsConversionID, loading, setAdsConversionIDMigratedAtMs, + setConversionID, setLegacyAdsConversionID, submitAnalyticsChanges, + submitChanges, ] ); return loading; diff --git a/assets/js/modules/analytics-4/hooks/useMigrateAdsConversionID.test.js b/assets/js/modules/analytics-4/hooks/useMigrateAdsConversionID.test.js index 59ccc0fd66f..a7a8ef1ad4c 100644 --- a/assets/js/modules/analytics-4/hooks/useMigrateAdsConversionID.test.js +++ b/assets/js/modules/analytics-4/hooks/useMigrateAdsConversionID.test.js @@ -61,7 +61,6 @@ describe( 'useMigrateAdsConversionID', () => { renderHook( () => useMigrateAdsConversionID(), { registry, - features: [ 'adsModule' ], } ); expect( @@ -83,7 +82,6 @@ describe( 'useMigrateAdsConversionID', () => { renderHook( () => useMigrateAdsConversionID(), { registry, - features: [ 'adsModule' ], } ); // Verify that the value has not changed. @@ -104,7 +102,6 @@ describe( 'useMigrateAdsConversionID', () => { renderHook( () => useMigrateAdsConversionID(), { registry, - features: [ 'adsModule' ], } ); expect( fetchMock ).not.toHaveFetched(); @@ -124,7 +121,6 @@ describe( 'useMigrateAdsConversionID', () => { renderHook( () => useMigrateAdsConversionID(), { registry, - features: [ 'adsModule' ], } ); expect( fetchMock ).not.toHaveFetched(); @@ -144,7 +140,6 @@ describe( 'useMigrateAdsConversionID', () => { () => useMigrateAdsConversionID(), { registry, - features: [ 'adsModule' ], } ); @@ -178,7 +173,6 @@ describe( 'useMigrateAdsConversionID', () => { () => useMigrateAdsConversionID(), { registry, - features: [ 'adsModule' ], } ); @@ -237,7 +231,6 @@ describe( 'useMigrateAdsConversionID', () => { () => useMigrateAdsConversionID(), { registry, - features: [ 'adsModule' ], } ); diff --git a/feature-flags.json b/feature-flags.json index 612d3760696..1c98da9dac6 100644 --- a/feature-flags.json +++ b/feature-flags.json @@ -1 +1 @@ -[ "adsModule", "adsPax", "audienceSegmentation", "conversionInfra", "gm3Components" ] +[ "adsPax", "audienceSegmentation", "conversionInfra", "gm3Components" ] diff --git a/includes/Core/Modules/Modules.php b/includes/Core/Modules/Modules.php index b9d911a2101..70104440b4d 100644 --- a/includes/Core/Modules/Modules.php +++ b/includes/Core/Modules/Modules.php @@ -25,7 +25,6 @@ use Google\Site_Kit\Modules\Site_Verification; use Google\Site_Kit\Modules\Tag_Manager; use Google\Site_Kit\Modules\Ads; -use Google\Site_Kit\Core\Util\Feature_Flags; use Exception; /** @@ -177,9 +176,7 @@ public function __construct( $this->authentication = $authentication ?: new Authentication( $this->context, $this->options, $this->user_options ); $this->assets = $assets ?: new Assets( $this->context ); - if ( Feature_Flags::enabled( 'adsModule' ) ) { - $this->core_modules[ Ads::MODULE_SLUG ] = Ads::class; - } + $this->core_modules[ Ads::MODULE_SLUG ] = Ads::class; $this->rest_controller = new REST_Modules_Controller( $this ); $this->dashboard_sharing_controller = new REST_Dashboard_Sharing_Controller( $this ); diff --git a/tests/e2e/specs/modules/ads/setup-no-previous-ads-conversion-id.test.js b/tests/e2e/specs/modules/ads/setup-no-previous-ads-conversion-id.test.js index 21b9e83c28b..1a542e78ee2 100644 --- a/tests/e2e/specs/modules/ads/setup-no-previous-ads-conversion-id.test.js +++ b/tests/e2e/specs/modules/ads/setup-no-previous-ads-conversion-id.test.js @@ -26,7 +26,6 @@ import { visitAdminPage } from '@wordpress/e2e-test-utils'; */ import { deactivateUtilityPlugins, - enableFeature, resetSiteKit, setupSiteKit, step, @@ -77,7 +76,6 @@ describe( 'Ads setup (with no Conversion Tracking ID present)', () => { } ); beforeEach( async () => { - await enableFeature( 'adsModule' ); await setupSiteKit(); } ); diff --git a/tests/e2e/specs/modules/ads/setup-with-previous-ads-conversion-id.test.js b/tests/e2e/specs/modules/ads/setup-with-previous-ads-conversion-id.test.js index 1b0554b94b3..ae67ae2ddfb 100644 --- a/tests/e2e/specs/modules/ads/setup-with-previous-ads-conversion-id.test.js +++ b/tests/e2e/specs/modules/ads/setup-with-previous-ads-conversion-id.test.js @@ -29,7 +29,6 @@ import { resetSiteKit, setupSiteKit, setupAnalytics4, - enableFeature, useRequestInterception, step, } from '../../../utils'; @@ -72,7 +71,6 @@ describe( 'Ads setup with Conversion Tracking ID present', () => { } ); beforeEach( async () => { - await enableFeature( 'adsModule' ); await setupSiteKit(); await setupAnalytics4( { adsConversionID: 'AW-12345', From 0a0f848b717bbb03ab2cbe35d607c58a6f0be067 Mon Sep 17 00:00:00 2001 From: Ben Bowler Date: Thu, 9 May 2024 16:25:16 +0100 Subject: [PATCH 026/134] Remove the now unused AdsConversionIDTextField component. --- .../common/AdsConversionIDTextField.js | 111 ------------------ .../analytics-4/components/common/index.js | 1 - 2 files changed, 112 deletions(-) delete mode 100644 assets/js/modules/analytics-4/components/common/AdsConversionIDTextField.js diff --git a/assets/js/modules/analytics-4/components/common/AdsConversionIDTextField.js b/assets/js/modules/analytics-4/components/common/AdsConversionIDTextField.js deleted file mode 100644 index f37d7262554..00000000000 --- a/assets/js/modules/analytics-4/components/common/AdsConversionIDTextField.js +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Analytics 4 Ads Conversion ID component. - * - * Site Kit by Google, Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { useCallback } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import Data from 'googlesitekit-data'; -import AccessibleWarningIcon from '../../../../components/AccessibleWarningIcon'; -import { TextField } from 'googlesitekit-components'; -import { MODULES_ANALYTICS_4 } from '../../datastore/constants'; -import { isValidConversionID } from '../../../ads/utils/validation'; -const { useSelect, useDispatch } = Data; - -export default function AdsConversionIDTextField() { - const adsConversionID = useSelect( ( select ) => - select( MODULES_ANALYTICS_4 ).getAdsConversionID() - ); - const snippetEnabled = useSelect( ( select ) => { - return select( MODULES_ANALYTICS_4 ).getUseSnippet(); - } ); - - const { setAdsConversionID } = useDispatch( MODULES_ANALYTICS_4 ); - const onChange = useCallback( - ( { currentTarget } ) => { - let newValue = currentTarget.value.trim().toUpperCase(); - // Automatically add the AW- prefix if not provided. - if ( 'AW-'.length < newValue.length && ! /^AW-/.test( newValue ) ) { - newValue = `AW-${ newValue }`; - } - - if ( newValue !== adsConversionID ) { - setAdsConversionID( newValue ); - } - }, - [ adsConversionID, setAdsConversionID ] - ); - - const isValidValue = Boolean( - ! adsConversionID || isValidConversionID( adsConversionID ) - ); - - // Only show the field if the snippet is enabled for output, - // but only hide it if the value is valid otherwise the user will be blocked. - if ( isValidValue && ! snippetEnabled ) { - return null; - } - - return ( -
-

- { __( 'Google Ads', 'google-site-kit' ) } -

- - - - ) - } - outlined - value={ adsConversionID } - onChange={ onChange } - /> - -

- { __( - 'If you’re using Google Ads, insert your Ads conversion ID if you’d like Site Kit to place the snippet on your site', - 'google-site-kit' - ) } -

-
- ); -} diff --git a/assets/js/modules/analytics-4/components/common/index.js b/assets/js/modules/analytics-4/components/common/index.js index 087e278bd7b..545d6493a85 100644 --- a/assets/js/modules/analytics-4/components/common/index.js +++ b/assets/js/modules/analytics-4/components/common/index.js @@ -19,7 +19,6 @@ export { default as AccountCreate } from './AccountCreate'; export { default as AccountSelect } from './AccountSelect'; export { default as AccountCreateLegacy } from './AccountCreateLegacy'; -export { default as AdsConversionIDTextField } from './AdsConversionIDTextField'; export { default as EnhancedMeasurementSwitch } from './EnhancedMeasurementSwitch'; export { default as WebDataStreamSelect } from './WebDataStreamSelect'; export { default as PropertySelect } from './PropertySelect'; From 197d552816a6e2ba38d2533d2fe1de10c76bcc06 Mon Sep 17 00:00:00 2001 From: Ben Bowler Date: Thu, 9 May 2024 16:38:26 +0100 Subject: [PATCH 027/134] Fix phpunit tests adding Ads as an default available module. --- tests/phpunit/integration/Core/Modules/ModulesTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/phpunit/integration/Core/Modules/ModulesTest.php b/tests/phpunit/integration/Core/Modules/ModulesTest.php index 5aa8fa355b2..6ff6cf50a81 100644 --- a/tests/phpunit/integration/Core/Modules/ModulesTest.php +++ b/tests/phpunit/integration/Core/Modules/ModulesTest.php @@ -16,6 +16,7 @@ use Google\Site_Kit\Core\Modules\Modules; use Google\Site_Kit\Core\Storage\Options; use Google\Site_Kit\Core\Storage\User_Options; +use Google\Site_Kit\Modules\Ads; use Google\Site_Kit\Modules\AdSense; use Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Modules\PageSpeed_Insights; @@ -42,6 +43,7 @@ function ( $instance ) { $this->assertEqualSetsWithIndex( array( + 'ads' => 'Google\\Site_Kit\\Modules\\Ads', 'adsense' => 'Google\\Site_Kit\\Modules\\AdSense', 'analytics-4' => 'Google\\Site_Kit\\Modules\\Analytics_4', 'pagespeed-insights' => 'Google\\Site_Kit\\Modules\\PageSpeed_Insights', @@ -356,6 +358,7 @@ public function provider_googlesitekit_available_modules_filter() { $default_modules = array( Site_Verification::MODULE_SLUG, Search_Console::MODULE_SLUG, + Ads::MODULE_SLUG, AdSense::MODULE_SLUG, Analytics_4::MODULE_SLUG, PageSpeed_Insights::MODULE_SLUG, From 33938c7737c68e4e4676b4b0aa5c46cf6e7adc80 Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Fri, 10 May 2024 11:00:42 +0530 Subject: [PATCH 028/134] Initial file and folder structure. --- .../datastore/user/expirable-items.js | 17 +++++++++++++ includes/Core/Expirables/Expirable_Items.php | 24 +++++++++++++++++++ includes/Core/Expirables/Expirables.php | 22 +++++++++++++++++ .../REST_Expirable_Items_Controller.php | 22 +++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 assets/js/googlesitekit/datastore/user/expirable-items.js create mode 100644 includes/Core/Expirables/Expirable_Items.php create mode 100644 includes/Core/Expirables/Expirables.php create mode 100644 includes/Core/Expirables/REST_Expirable_Items_Controller.php diff --git a/assets/js/googlesitekit/datastore/user/expirable-items.js b/assets/js/googlesitekit/datastore/user/expirable-items.js new file mode 100644 index 00000000000..3f05ebd2ff6 --- /dev/null +++ b/assets/js/googlesitekit/datastore/user/expirable-items.js @@ -0,0 +1,17 @@ +/** + * `core/user` data store: expirable items + * + * Site Kit by Google, Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ diff --git a/includes/Core/Expirables/Expirable_Items.php b/includes/Core/Expirables/Expirable_Items.php new file mode 100644 index 00000000000..38c5e4a0068 --- /dev/null +++ b/includes/Core/Expirables/Expirable_Items.php @@ -0,0 +1,24 @@ + Date: Fri, 10 May 2024 13:44:04 +0530 Subject: [PATCH 029/134] Add the datastore for expirable items. --- .../datastore/user/expirable-items.js | 172 ++++++++++++++++++ .../js/googlesitekit/datastore/user/index.js | 2 + 2 files changed, 174 insertions(+) diff --git a/assets/js/googlesitekit/datastore/user/expirable-items.js b/assets/js/googlesitekit/datastore/user/expirable-items.js index 3f05ebd2ff6..c977e556f89 100644 --- a/assets/js/googlesitekit/datastore/user/expirable-items.js +++ b/assets/js/googlesitekit/datastore/user/expirable-items.js @@ -15,3 +15,175 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +/** + * External dependencies + */ +import invariant from 'invariant'; + +/** + * Internal dependencies + */ +import API from 'googlesitekit-api'; +import Data from 'googlesitekit-data'; +import { CORE_USER } from './constants'; +import { createFetchStore } from '../../data/create-fetch-store'; +import { createValidatedAction } from '../../data/utils'; +const { createRegistrySelector, commonActions } = Data; +const { getRegistry } = commonActions; + +function reducerCallback( state, expirableItems ) { + return { + ...state, + expirableItems: Array.isArray( expirableItems ) ? expirableItems : [], + }; +} + +const fetchGetExpirableItemsStore = createFetchStore( { + baseName: 'getExpirableItems', + controlCallback: () => + API.get( 'core', 'user', 'expirable-items', {}, { useCache: false } ), + reducerCallback, +} ); + +const fetchSetExpirableItemTimersStore = createFetchStore( { + baseName: 'expirableItems', + controlCallback: ( { slug, expiresInSeconds } ) => + API.set( 'core', 'user', 'set-expirable-item-timers', { + slug, + expiration: expiresInSeconds, + } ), + reducerCallback, + argsToParams: ( items ) => { + return items; + }, + validateParams: ( items = [] ) => { + items.forEach( ( item ) => { + const { slug, expiresInSeconds = 0 } = item; + invariant( slug, 'slug is required.' ); + invariant( + Number.isInteger( expiresInSeconds ), + 'expiresInSeconds must be an integer.' + ); + } ); + }, +} ); + +const baseInitialState = { + expirableItems: undefined, +}; + +const baseActions = { + setExpirableItemTimers: createValidatedAction( + ( items = [] ) => { + items.forEach( ( item ) => { + const { slug, expiresInSeconds } = item; + + invariant( slug, 'An item slug is required.' ); + invariant( + Number.isInteger( expiresInSeconds ), + 'expiresInSeconds must be an integer.' + ); + } ); + }, + function* ( items ) { + return yield fetchSetExpirableItemTimersStore.actions.fetchExpirableItems( + items + ); + } + ), +}; + +const baseResolvers = { + *getExpirableItems() { + const { select } = yield getRegistry(); + const expirableItems = select( CORE_USER ).getExpirableItems(); + if ( expirableItems === undefined ) { + yield fetchGetExpirableItemsStore.actions.fetchGetExpirableItems(); + } + }, +}; + +const baseSelectors = { + /** + * Returns the expirable items. + * + * @since n.e.x.t + * + * @param {Object} state Data store's state. + * @return {Array|undefined} Items if exists, `undefined` if not resolved yet. + */ + getExpirableItems( state ) { + return state.expirableItems; + }, + + /** + * Determines whether the item exists in expirable items. + * + * @since n.e.x.t + * + * @param {Object} state Data store's state. + * @param {string} slug Item slug. + * @return {(boolean|undefined)} TRUE if exists, otherwise FALSE, `undefined` if not resolved yet. + */ + hasExpirableItem: createRegistrySelector( ( select ) => ( state, slug ) => { + return select( CORE_USER ).getExpirableItems()?.includes( slug ); + } ), + + /** + * Determines whether the item is active and not expired. + * + * @since n.e.x.t + * + * @param {Object} state Data store's state. + * @param {string} slug Item slug. + * @return {(boolean|undefined)} TRUE if exists, otherwise FALSE, `undefined` if not resolved yet. + */ + isExpirableItemActive: createRegistrySelector( + ( select ) => ( state, slug ) => { + let active = false; + const itemExists = select( CORE_USER ).hasExpirableItem( slug ); + + if ( itemExists ) { + const expirableItems = select( CORE_USER ).getExpirableItems(); + expirableItems.forEach( ( item ) => { + const { expiresInSeconds = 0 } = item; + const timeStamp = Math.floor( Date.now() / 1000 ); + + if ( expiresInSeconds < timeStamp ) { + active = true; + } + } ); + } + + return active; + } + ), +}; + +export const { + actions, + controls, + initialState, + reducer, + resolvers, + selectors, +} = Data.combineStores( + { + initialState: baseInitialState, + actions: baseActions, + resolvers: baseResolvers, + selectors: baseSelectors, + }, + fetchGetExpirableItemsStore, + fetchSetExpirableItemTimersStore +); + +export default { + actions, + controls, + initialState, + reducer, + resolvers, + selectors, +}; diff --git a/assets/js/googlesitekit/datastore/user/index.js b/assets/js/googlesitekit/datastore/user/index.js index a91459ba7e7..8c164a301c4 100644 --- a/assets/js/googlesitekit/datastore/user/index.js +++ b/assets/js/googlesitekit/datastore/user/index.js @@ -27,6 +27,7 @@ import { CORE_USER } from './constants'; import dateRange from './date-range'; import disconnect from './disconnect'; import dismissedItems from './dismissed-items'; +import expirableItems from './expirable-items'; import featureTours from './feature-tours'; import keyMetrics from './key-metrics'; import notifications from './notifications'; @@ -46,6 +47,7 @@ const store = Data.combineStores( dateRange, disconnect, dismissedItems, + expirableItems, featureTours, keyMetrics, notifications, From a83c194caae25b344c20f2e240e78c86aaf13a61 Mon Sep 17 00:00:00 2001 From: "Matthew Riley MacPherson (tofumatt)" Date: Fri, 10 May 2024 10:57:49 +0100 Subject: [PATCH 030/134] Add PAX widget to Ads module. --- assets/js/googlesitekit-modules-ads.js | 4 +- .../modules/ads/components/PAXEmbeddedApp.js | 20 ++- .../components/dashboard/PartnerAdsWidget.js | 97 +++++++++++ assets/js/modules/ads/datastore/constants.js | 2 + assets/js/modules/ads/index.js | 18 ++ assets/js/util/whenScopesGranted.js | 96 +++++++++++ assets/js/util/whenScopesGranted.test.js | 158 ++++++++++++++++++ 7 files changed, 387 insertions(+), 8 deletions(-) create mode 100644 assets/js/modules/ads/components/dashboard/PartnerAdsWidget.js create mode 100644 assets/js/util/whenScopesGranted.js create mode 100644 assets/js/util/whenScopesGranted.test.js diff --git a/assets/js/googlesitekit-modules-ads.js b/assets/js/googlesitekit-modules-ads.js index 97a69181f92..26e3d77d775 100644 --- a/assets/js/googlesitekit-modules-ads.js +++ b/assets/js/googlesitekit-modules-ads.js @@ -21,7 +21,9 @@ */ import Data from 'googlesitekit-data'; import Modules from 'googlesitekit-modules'; -import { registerStore, registerModule } from './modules/ads'; +import Widgets from 'googlesitekit-widgets'; +import { registerStore, registerModule, registerWidgets } from './modules/ads'; registerStore( Data ); registerModule( Modules ); +registerWidgets( Widgets ); diff --git a/assets/js/modules/ads/components/PAXEmbeddedApp.js b/assets/js/modules/ads/components/PAXEmbeddedApp.js index c10e681287f..fbc2a006ec4 100644 --- a/assets/js/modules/ads/components/PAXEmbeddedApp.js +++ b/assets/js/modules/ads/components/PAXEmbeddedApp.js @@ -43,8 +43,8 @@ import { CORE_USER } from '../../../googlesitekit/datastore/user/constants'; import CTA from '../../../components/notifications/CTA'; import PreviewBlock from '../../../components/PreviewBlock'; import { createPaxServices } from '../pax/services'; +import { useMemoOne } from 'use-memo-one'; const { useRegistry, useSelect } = Data; - export default function PAXEmbeddedApp( { displayMode = 'default', onLaunch, @@ -72,9 +72,11 @@ export default function PAXEmbeddedApp( { const paxAppRef = useRef(); - const elementID = `googlesitekit-pax-embedded-app-${ instanceID }`; + const elementID = useMemoOne( () => { + return `googlesitekit-pax-embedded-app-${ instanceID }`; + }, [ instanceID ] ); - const paxConfig = useMemo( () => { + const paxConfig = useMemoOne( () => { return { ...( global?._googlesitekitPAXConfig || {} ), clientConfig: { @@ -92,6 +94,12 @@ export default function PAXEmbeddedApp( { }, [ elementID, displayMode ] ); const launchPAXApp = useCallback( async () => { + if ( hasLaunchedPAXApp || paxAppRef.current ) { + return; + } + + setHasLaunchedPAXApp( true ); + try { paxAppRef.current = await global.google.ads.integration.integrator.launchGoogleAds( @@ -109,7 +117,7 @@ export default function PAXEmbeddedApp( { } setIsLoading( false ); - }, [ paxConfig, paxServices, onLaunch ] ); + }, [ hasLaunchedPAXApp, paxConfig, paxServices, onLaunch ] ); useInterval( () => { @@ -131,9 +139,7 @@ export default function PAXEmbeddedApp( { ); useEffect( () => { - if ( launchGoogleAdsAvailable && ! hasLaunchedPAXApp ) { - setHasLaunchedPAXApp( true ); - + if ( launchGoogleAdsAvailable ) { launchPAXApp(); } }, [ diff --git a/assets/js/modules/ads/components/dashboard/PartnerAdsWidget.js b/assets/js/modules/ads/components/dashboard/PartnerAdsWidget.js new file mode 100644 index 00000000000..c8e9b4fb9cb --- /dev/null +++ b/assets/js/modules/ads/components/dashboard/PartnerAdsWidget.js @@ -0,0 +1,97 @@ +/** + * PartnerAdsWidget component. + * + * Site Kit by Google, Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +/** + * WordPress dependencies + */ +import { compose } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import Data from 'googlesitekit-data'; +import whenActive from '../../../../util/when-active'; +import whenScopesGranted from '../../../../util/whenScopesGranted'; +import { ADWORDS_SCOPE, MODULES_ADS } from '../../datastore/constants'; +import PAXEmbeddedApp from '../PAXEmbeddedApp'; +import { AdBlockerWarning } from '../common'; +import { CORE_USER } from '../../../../googlesitekit/datastore/user/constants'; +import { CORE_WIDGETS } from '../../../../googlesitekit/widgets/datastore/constants'; +const { useSelect } = Data; + +function PartnerAdsWidget( { WidgetNull, Widget } ) { + const isAdblockerActive = useSelect( ( select ) => + select( CORE_USER ).isAdBlockerActive() + ); + + const paxConversionID = useSelect( ( select ) => + select( MODULES_ADS ).getPaxConversionID() + ); + + const widgetRendered = useSelect( ( select ) => + select( CORE_WIDGETS ).isWidgetActive( 'partnerAds' ) + ); + + // If the user doesn't have a PAX Conversion ID, then they haven't set up + // Google Ads Partner experience yet, so we shouldn't render the widget. + // + // If the widget is rendered but the user doesn't have a PAX Conversion ID, + // the setup flow will be triggered and we don't want to show that in the + // "reporting" widget. + if ( ! paxConversionID?.length ) { + return ; + } + + if ( isAdblockerActive ) { + return ( + + + + ); + } + + // If the widget hasn't been rendered in the actual DOM yet, + // don't load the PAX app. + // + // This is done to prevent the PAX app from launching before it's actually + // inserted into the DOM. + if ( ! widgetRendered ) { + return ; + } + + return ( + + + + ); +} + +PartnerAdsWidget.propTypes = { + Widget: PropTypes.elementType.isRequired, + WidgetNull: PropTypes.elementType.isRequired, +}; + +export default compose( + whenActive( { moduleName: 'ads' } ), + whenScopesGranted( { scopes: [ ADWORDS_SCOPE ] } ) +)( PartnerAdsWidget ); diff --git a/assets/js/modules/ads/datastore/constants.js b/assets/js/modules/ads/datastore/constants.js index 646739101bd..a601dcc29b6 100644 --- a/assets/js/modules/ads/datastore/constants.js +++ b/assets/js/modules/ads/datastore/constants.js @@ -17,3 +17,5 @@ */ export const MODULES_ADS = 'modules/ads'; + +export const ADWORDS_SCOPE = 'https://www.googleapis.com/auth/adwords'; diff --git a/assets/js/modules/ads/index.js b/assets/js/modules/ads/index.js index 6eb1f1e3096..05e74a96810 100644 --- a/assets/js/modules/ads/index.js +++ b/assets/js/modules/ads/index.js @@ -33,6 +33,8 @@ import { CORE_USER, ERROR_CODE_ADBLOCKER_ACTIVE, } from '../../googlesitekit/datastore/user/constants'; +import PartnerAdsWidget from './components/dashboard/PartnerAdsWidget'; +import { AREA_MAIN_DASHBOARD_TRAFFIC_PRIMARY } from '../../googlesitekit/widgets/default-areas'; export { registerStore } from './datastore'; @@ -76,3 +78,19 @@ export const registerModule = ( modules ) => { } ); } }; + +export const registerWidgets = ( widgets ) => { + if ( isFeatureEnabled( 'adsPax' ) ) { + widgets.registerWidget( + 'partnerAds', + { + Component: PartnerAdsWidget, + width: widgets.WIDGET_WIDTHS.FULL, + priority: 20, + wrapWidget: false, + modules: [ 'ads' ], + }, + [ AREA_MAIN_DASHBOARD_TRAFFIC_PRIMARY ] + ); + } +}; diff --git a/assets/js/util/whenScopesGranted.js b/assets/js/util/whenScopesGranted.js new file mode 100644 index 00000000000..ae17a2cb11c --- /dev/null +++ b/assets/js/util/whenScopesGranted.js @@ -0,0 +1,96 @@ +/** + * `whenScopesGranted` HOC. + * + * Site Kit by Google, Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Internal dependencies + */ +import Data from 'googlesitekit-data'; +import { CORE_USER } from '../googlesitekit/datastore/user/constants'; +const { useSelect } = Data; + +/** + * Higher-Order Component to render wrapped components when specified scopes + * are available to this user. + * + * If the scopes are not available, a fallback component is rendered instead. + * + * A higher-order component is used here instead of hooks because there is + * potential for related selectors in components this HOC wraps to call out to + * resolvers that call endpoints for modules that aren't active. This would + * cause 404s at best and possibly errors, so it's better to wrap them in HOCs + * and "return early". + * + * @since n.e.x.t + * + * @param {Object} options Options for enhancing function. + * @param {string} options.scopes Array of scopes to check. + * @param {WPComponent} [options.FallbackComponent] Optional. Fallback component to render when the module is not active. + * @return {Function} Enhancing function. + */ +export default function whenScopesGranted( { + scopes = [], + FallbackComponent, +} ) { + return ( WrappedComponent ) => { + function WhenScopesGranted( props ) { + const allScopeResults = useSelect( + ( select ) => { + return scopes.map( ( scope ) => { + return select( CORE_USER ).hasScope( scope ); + } ); + }, + [ scopes ] + ); + + // Return null if any scopes aren't yet loaded. + if ( + allScopeResults.some( ( hasScope ) => { + return hasScope === undefined; + } ) + ) { + return null; + } + + // This component isn't widget-specific but widgets need to use + // `WidgetNull` from props when rendering "null" output. + const DefaultFallbackComponent = + FallbackComponent || props.WidgetNull || null; + + // Return a fallback (by default, ``) if any scopes are + // not present for this user. + if ( + allScopeResults.some( ( hasScope ) => { + return hasScope === false; + } ) + ) { + return ( + DefaultFallbackComponent && ( + + ) + ); + } + + // Return the component if the user has all scopes specified. + return ; + } + + WhenScopesGranted.displayName = 'WhenScopesGranted'; + + return WhenScopesGranted; + }; +} diff --git a/assets/js/util/whenScopesGranted.test.js b/assets/js/util/whenScopesGranted.test.js new file mode 100644 index 00000000000..bfa22955b8d --- /dev/null +++ b/assets/js/util/whenScopesGranted.test.js @@ -0,0 +1,158 @@ +/** + * HOC whenScopesGranted tests. + * + * Site Kit by Google, Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Internal dependencies + */ +import { render } from '../../../tests/js/test-utils'; +import { createTestRegistry, subscribeUntil } from '../../../tests/js/utils'; +import { CORE_USER } from '../googlesitekit/datastore/user/constants'; +import whenScopesGranted from './whenScopesGranted'; + +describe( 'whenScopesGranted', () => { + let registry; + + function TestComponent() { + return
; + } + function TestFallbackComponent() { + return
; + } + function FakeWidgetNull() { + return
; + } + + async function loadScopes( registryReference, scopes = [] ) { + const coreUserDataEndpointRegExp = new RegExp( + '^/google-site-kit/v1/core/user/data/authentication' + ); + + fetchMock.getOnce( coreUserDataEndpointRegExp, { + body: { + authenticated: true, + requiredScopes: [], + grantedScopes: scopes, + unsatisfiedScopes: [], + }, + status: 200, + } ); + + registryReference.select( CORE_USER ).hasScope( scopes ); + await subscribeUntil( registryReference, () => + registryReference + .select( CORE_USER ) + .hasFinishedResolution( 'getAuthentication' ) + ); + } + + beforeEach( () => { + registry = createTestRegistry(); + } ); + + it( 'renders nothing (`null`) when scopes are loading', async () => { + await loadScopes( registry ); + + const WhenScopesGrantedComponent = whenScopesGranted( { + scopes: [ 'loading' ], + } )( TestComponent ); + + const { container, queryByTestID } = render( + , + { + registry, + } + ); + + expect( queryByTestID( 'component' ) ).not.toBeInTheDocument(); + expect( container ).toBeEmptyDOMElement(); + } ); + + it( 'renders the given component when all scopes are present', async () => { + await loadScopes( registry, [ 'https://test.net/testing' ] ); + + const WhenScopesGrantedComponent = whenScopesGranted( { + scopes: [ 'https://test.net/testing' ], + FallbackComponent: TestFallbackComponent, + } )( TestComponent ); + + const { queryByTestID } = render( , { + registry, + } ); + + expect( queryByTestID( 'component' ) ).toBeInTheDocument(); + } ); + + it( 'renders the fallback component when some, but not all, scopes are present', async () => { + await loadScopes( registry, [ 'https://test.net/testing' ] ); + + const WhenScopesGrantedComponent = whenScopesGranted( { + scopes: [ + 'https://test.net/testing', + 'https://otherscope.com/i-am-not-here', + ], + FallbackComponent: TestFallbackComponent, + } )( TestComponent ); + + const { queryByTestID } = render( , { + registry, + } ); + + expect( queryByTestID( 'component' ) ).not.toBeInTheDocument(); + expect( queryByTestID( 'fallback-component' ) ).toBeInTheDocument(); + } ); + + it( 'renders the fallback component when none of the scopes are present', async () => { + await loadScopes( registry, [] ); + + const WhenScopesGrantedComponent = whenScopesGranted( { + scopes: [ + 'https://test.net/testing', + 'https://otherscope.com/i-am-not-here', + ], + FallbackComponent: TestFallbackComponent, + } )( TestComponent ); + + const { queryByTestID } = render( , { + registry, + } ); + + expect( queryByTestID( 'component' ) ).not.toBeInTheDocument(); + expect( queryByTestID( 'fallback-component' ) ).toBeInTheDocument(); + } ); + + it( 'renders `WidgetNull` from the components ownProps for the default `FallbackComponent`', async () => { + await loadScopes( registry, [] ); + + const WhenScopesGrantedComponent = whenScopesGranted( { + scopes: [ + 'https://test.net/testing', + 'https://otherscope.com/i-am-not-here', + ], + } )( TestComponent ); + + const { queryByTestID } = render( + , + { + registry, + } + ); + + expect( queryByTestID( 'component' ) ).not.toBeInTheDocument(); + expect( queryByTestID( 'widget-null' ) ).toBeInTheDocument(); + } ); +} ); From c5a1b8217554847742258abfdd49e9b88b96da74 Mon Sep 17 00:00:00 2001 From: Aleksej Date: Fri, 10 May 2024 12:00:22 +0200 Subject: [PATCH 031/134] Update complete setup button disabled checks. --- .../js/modules/ads/components/setup/SetupMainPAX.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/assets/js/modules/ads/components/setup/SetupMainPAX.js b/assets/js/modules/ads/components/setup/SetupMainPAX.js index 6f4f7ac29ce..7649cff450f 100644 --- a/assets/js/modules/ads/components/setup/SetupMainPAX.js +++ b/assets/js/modules/ads/components/setup/SetupMainPAX.js @@ -56,6 +56,11 @@ export default function SetupMainPAX( { finishSetup } ) { const hasAdwordsScope = useSelect( ( select ) => select( CORE_USER ).hasScope( ADWORDS_SCOPE ) ); + const hasPaxSettings = useSelect( ( select ) => { + const { getPaxConversionID, getExtCustomerID } = select( MODULES_ADS ); + + return getPaxConversionID() && getExtCustomerID(); + } ); const redirectURL = addQueryArgs( global.location.href, { [ PARAM_SHOW_PAX ]: 1, @@ -146,7 +151,11 @@ export default function SetupMainPAX( { finishSetup } ) {
{ __( 'Complete setup', 'google-site-kit' ) } From 31949153dba1bb096f3b7579129383e7abcae443 Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Fri, 10 May 2024 15:36:01 +0530 Subject: [PATCH 032/134] Add expirable items classes and REST routes classes. --- includes/Core/Expirables/Expirable_Items.php | 124 ++++++++++++++++ .../REST_Expirable_Items_Controller.php | 133 ++++++++++++++++++ 2 files changed, 257 insertions(+) diff --git a/includes/Core/Expirables/Expirable_Items.php b/includes/Core/Expirables/Expirable_Items.php index 38c5e4a0068..7edebb43e20 100644 --- a/includes/Core/Expirables/Expirable_Items.php +++ b/includes/Core/Expirables/Expirable_Items.php @@ -20,5 +20,129 @@ * @ignore */ class Expirable_Items extends User_Setting { + /** + * The user option name for this setting. + * + * @note This option is prefixed differently so that it will persist across disconnect/reset. + */ + const OPTION = 'googlesitekitpersistent_expirable_items'; + + /** + * Adds one or more items to the list of expired items. + * + * @since n.e.x.t + * + * @param string $item Item to set expiration for. + * @param int $expires_in_seconds TTL for the item. + */ + public function add( $item, $expires_in_seconds ) { + $items = $this->get(); + $items[ $item ] = $expires_in_seconds ? time() + $expires_in_seconds : 0; + + $this->set( $items ); + } + + /** + * Removes one or more items from the list of expirable items. + * + * @since n.e.x.t + * + * @param string $item Item to remove. + */ + public function remove( $item ) { + $items = $this->get(); + + // If the item is not in expirable items, there's nothing to do. + if ( ! array_key_exists( $item, $items ) ) { + return; + } + + unset( $items[ $item ] ); + + $this->set( $items ); + } + + /** + * Gets the value of the setting. + * + * @since n.e.x.t + * + * @return array Value set for the option, or default if not set. + */ + public function get() { + $value = parent::get(); + return is_array( $value ) ? $value : $this->get_default(); + } + + /** + * Gets the expected value type. + * + * @since n.e.x.t + * + * @return string The type name. + */ + protected function get_type() { + return 'array'; + } + + /** + * Gets the default value. + * + * @since n.e.x.t + * + * @return array The default value. + */ + protected function get_default() { + return array(); + } + + /** + * Gets the callback for sanitizing the setting's value before saving. + * + * @since n.e.x.t + * + * @return callable Sanitize callback. + */ + protected function get_sanitize_callback() { + return function ( $items ) { + return $this->filter_expirable_items( $items ); + }; + } + + /** + * Gets expirable items. + * + * @since n.e.x.t + * + * @return array Expirable items array. + */ + public function get_expirable_items() { + $expirable_items = $this->get(); + $expirable_items = $this->filter_expirable_items( $expirable_items ); + + return array_keys( $expirable_items ); + } + + /** + * Filters expirable items. + * + * @since n.e.x.t + * + * @param array $items Expirable items list. + * @return array Filtered expirable items. + */ + private function filter_expirable_items( $items ) { + $expirables = array(); + + if ( is_array( $items ) ) { + foreach ( $items as $item => $ttl ) { + if ( is_integer( $ttl ) ) { + $expirables[ $item ] = $ttl; + } + } + } + + return $expirables; + } } diff --git a/includes/Core/Expirables/REST_Expirable_Items_Controller.php b/includes/Core/Expirables/REST_Expirable_Items_Controller.php index c491cb9d383..5ead2519b1b 100644 --- a/includes/Core/Expirables/REST_Expirable_Items_Controller.php +++ b/includes/Core/Expirables/REST_Expirable_Items_Controller.php @@ -10,6 +10,15 @@ namespace Google\Site_Kit\Core\Expirables; +use Google\Site_Kit\Core\Expirables\Expirable_Items; +use Google\Site_Kit\Core\Permissions\Permissions; +use Google\Site_Kit\Core\REST_API\REST_Route; +use Google\Site_Kit\Core\REST_API\REST_Routes; +use WP_Error; +use WP_REST_Request; +use WP_REST_Response; +use WP_REST_Server; + /** * Class for handling expirable items rest routes. * @@ -19,4 +28,128 @@ */ class REST_Expirable_Items_Controller { + /** + * Expirable_Items instance. + * + * @since n.e.x.t + * @var Expirable_Items + */ + protected $expirable_items; + + /** + * Constructor. + * + * @since n.e.x.t + * + * @param Expirable_Items $expirable_items Expirable items instance. + */ + public function __construct( Expirable_Items $expirable_items ) { + $this->expirable_items = $expirable_items; + } + + /** + * Registers functionality through WordPress hooks. + * + * @since n.e.x.t + */ + public function register() { + add_filter( + 'googlesitekit_rest_routes', + function ( $routes ) { + return array_merge( $routes, $this->get_rest_routes() ); + } + ); + + add_filter( + 'googlesitekit_apifetch_preload_paths', + function ( $paths ) { + return array_merge( + $paths, + array( + '/' . REST_Routes::REST_ROOT . '/core/user/data/expirable-items', + ) + ); + } + ); + } + + /** + * Gets REST route instances. + * + * @since n.e.x.t + * + * @return REST_Route[] List of REST_Route objects. + */ + protected function get_rest_routes() { + $can_manage_expirable_item = function() { + return current_user_can( Permissions::VIEW_SPLASH ) || current_user_can( Permissions::VIEW_DASHBOARD ); + }; + + return array( + new REST_Route( + 'core/user/data/expirable-items', + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => function () { + return new WP_REST_Response( $this->expirable_items->get_expirable_items() ); + }, + 'permission_callback' => $can_manage_expirable_item, + ) + ), + new REST_Route( + 'core/user/data/set-expirable-item-timers', + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => function ( WP_REST_Request $request ) { + $data = $request['data']; + + if ( empty( $data ) || ! is_array( $data ) ) { + return new WP_Error( + 'missing_required_param', + /* translators: %s: Missing parameter name */ + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'items' ), + array( 'status' => 400 ) + ); + } + + foreach ( $data as $datum ) { + if ( empty( $datum['slug'] ) ) { + return new WP_Error( + 'missing_required_param', + /* translators: %s: Missing parameter name */ + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'slug' ), + array( 'status' => 400 ) + ); + } + + $expiration = null; + if ( isset( $datum['expiration'] ) && intval( $datum['expiration'] ) > 0 ) { + $expiration = $data['expiration']; + } + + if ( ! $expiration ) { + return new WP_Error( + 'missing_required_param', + /* translators: %s: Missing parameter name */ + sprintf( __( 'Request parameter is invalid: %s.', 'google-site-kit' ), 'expiration' ), + array( 'status' => 400 ) + ); + } + + $this->expirable_items->add( $datum['slug'], $expiration ); + } + + return new WP_REST_Response( $this->expirable_items->get_expirable_items() ); + }, + 'permission_callback' => $can_manage_expirable_item, + 'args' => array( + 'data' => array( + 'type' => 'object', + 'required' => true, + ), + ), + ) + ), + ); + } } From 82572e3b3c2e45142ad708d7653513693f89bae3 Mon Sep 17 00:00:00 2001 From: "Matthew Riley MacPherson (tofumatt)" Date: Fri, 10 May 2024 11:46:14 +0100 Subject: [PATCH 033/134] Move PAX component and export it properly. --- .../modules/ads/components/{ => common}/PAXEmbeddedApp.js | 8 ++++---- .../modules/ads/components/dashboard/PartnerAdsWidget.js | 2 +- assets/js/modules/ads/components/setup/SetupMainPAX.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) rename assets/js/modules/ads/components/{ => common}/PAXEmbeddedApp.js (94%) diff --git a/assets/js/modules/ads/components/PAXEmbeddedApp.js b/assets/js/modules/ads/components/common/PAXEmbeddedApp.js similarity index 94% rename from assets/js/modules/ads/components/PAXEmbeddedApp.js rename to assets/js/modules/ads/components/common/PAXEmbeddedApp.js index fbc2a006ec4..275e71e19f5 100644 --- a/assets/js/modules/ads/components/PAXEmbeddedApp.js +++ b/assets/js/modules/ads/components/common/PAXEmbeddedApp.js @@ -39,10 +39,10 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import Data from 'googlesitekit-data'; -import { CORE_USER } from '../../../googlesitekit/datastore/user/constants'; -import CTA from '../../../components/notifications/CTA'; -import PreviewBlock from '../../../components/PreviewBlock'; -import { createPaxServices } from '../pax/services'; +import { CORE_USER } from '../../../../googlesitekit/datastore/user/constants'; +import CTA from '../../../../components/notifications/CTA'; +import PreviewBlock from '../../../../components/PreviewBlock'; +import { createPaxServices } from '../../pax/services'; import { useMemoOne } from 'use-memo-one'; const { useRegistry, useSelect } = Data; export default function PAXEmbeddedApp( { diff --git a/assets/js/modules/ads/components/dashboard/PartnerAdsWidget.js b/assets/js/modules/ads/components/dashboard/PartnerAdsWidget.js index c8e9b4fb9cb..85fb7552834 100644 --- a/assets/js/modules/ads/components/dashboard/PartnerAdsWidget.js +++ b/assets/js/modules/ads/components/dashboard/PartnerAdsWidget.js @@ -33,7 +33,7 @@ import Data from 'googlesitekit-data'; import whenActive from '../../../../util/when-active'; import whenScopesGranted from '../../../../util/whenScopesGranted'; import { ADWORDS_SCOPE, MODULES_ADS } from '../../datastore/constants'; -import PAXEmbeddedApp from '../PAXEmbeddedApp'; +import PAXEmbeddedApp from '../common/PAXEmbeddedApp'; import { AdBlockerWarning } from '../common'; import { CORE_USER } from '../../../../googlesitekit/datastore/user/constants'; import { CORE_WIDGETS } from '../../../../googlesitekit/widgets/datastore/constants'; diff --git a/assets/js/modules/ads/components/setup/SetupMainPAX.js b/assets/js/modules/ads/components/setup/SetupMainPAX.js index 1564be0478f..99367041c7e 100644 --- a/assets/js/modules/ads/components/setup/SetupMainPAX.js +++ b/assets/js/modules/ads/components/setup/SetupMainPAX.js @@ -41,7 +41,7 @@ import { CORE_USER } from '../../../../googlesitekit/datastore/user/constants'; import { CORE_LOCATION } from '../../../../googlesitekit/datastore/location/constants'; import { ADWORDS_SCOPE, MODULES_ADS } from '../../datastore/constants'; import useQueryArg from '../../../../hooks/useQueryArg'; -import PAXEmbeddedApp from '../PAXEmbeddedApp'; +import PAXEmbeddedApp from '../common/PAXEmbeddedApp'; const { useSelect, useDispatch } = Data; const PARAM_SHOW_PAX = 'pax'; From ca9b73f8c76610a01dfb617f4b4a50d673ef0fce Mon Sep 17 00:00:00 2001 From: "Matthew Riley MacPherson (tofumatt)" Date: Fri, 10 May 2024 12:00:18 +0100 Subject: [PATCH 034/134] Add dashboard index file. --- .../js/modules/ads/components/common/index.js | 1 + .../modules/ads/components/dashboard/index.js | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 assets/js/modules/ads/components/dashboard/index.js diff --git a/assets/js/modules/ads/components/common/index.js b/assets/js/modules/ads/components/common/index.js index 86bb1b98622..598411eea09 100644 --- a/assets/js/modules/ads/components/common/index.js +++ b/assets/js/modules/ads/components/common/index.js @@ -18,3 +18,4 @@ export { default as AdBlockerWarning } from './AdBlockerWarning'; export { default as ConversionIDTextField } from './ConversionIDTextField'; +export { default as PAXEmbeddedApp } from './PAXEmbeddedApp'; diff --git a/assets/js/modules/ads/components/dashboard/index.js b/assets/js/modules/ads/components/dashboard/index.js new file mode 100644 index 00000000000..c9f40a178ab --- /dev/null +++ b/assets/js/modules/ads/components/dashboard/index.js @@ -0,0 +1,19 @@ +/** + * Ads Dashboard components. + * + * Site Kit by Google, Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { default as PartnerAdsWidget } from './PartnerAdsWidget'; From 1592f9bbbc185a7c7fd534e00f682b01f950efc9 Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Fri, 10 May 2024 17:34:35 +0530 Subject: [PATCH 035/134] Register the expirables instance in plugin. --- includes/Core/Expirables/Expirables.php | 54 +++++++++++++++++++++++++ includes/Plugin.php | 3 ++ 2 files changed, 57 insertions(+) diff --git a/includes/Core/Expirables/Expirables.php b/includes/Core/Expirables/Expirables.php index 05e3b0697b4..0f70e4ebd4b 100644 --- a/includes/Core/Expirables/Expirables.php +++ b/includes/Core/Expirables/Expirables.php @@ -10,6 +10,11 @@ namespace Google\Site_Kit\Core\Expirables; +use Google\Site_Kit\Core\Expirables\Expirable_Items; +use Google\Site_Kit\Core\Expirables\REST_Expirable_Items_Controller; +use Google\Site_Kit\Context; +use Google\Site_Kit\Core\Storage\User_Options; + /** * Class for handling expirables. * @@ -19,4 +24,53 @@ */ class Expirables { + /** + * Expirable_Items instance. + * + * @since n.e.x.t + * @var Expirable_Items + */ + protected $expirable_items; + + /** + * REST_Expirable_Items_Controller instance. + * + * @since n.e.x.t + * @var REST_Expirable_Items_Controller + */ + protected $rest_controller; + + /** + * Constructor. + * + * @since n.e.x.t + * + * @param Context $context Plugin context. + * @param User_Options $user_options Optional. User option API. Default is a new instance. + */ + public function __construct( Context $context, User_Options $user_options = null ) { + $this->expirable_items = new Expirable_Items( $user_options ?: new User_Options( $context ) ); + $this->rest_controller = new REST_Expirable_Items_Controller( $this->expirable_items ); + } + + /** + * Gets the reference to the Dismissed_Items instance. + * + * @since n.e.x.t + * + * @return Expirable_Items An instance of the Expirable_Items class. + */ + public function get_expirable_items() { + return $this->expirable_items; + } + + /** + * Registers functionality through WordPress hooks. + * + * @since n.e.x.t + */ + public function register() { + $this->expirable_items->register(); + $this->rest_controller->register(); + } } diff --git a/includes/Plugin.php b/includes/Plugin.php index 989986b3afa..9cd8f32b313 100644 --- a/includes/Plugin.php +++ b/includes/Plugin.php @@ -174,6 +174,9 @@ function() use ( $options, $activation_flag ) { $dismissed_items = $dismissals->get_dismissed_items(); + $expirables = new Core\Expirables\Expirables( $this->context, $user_options ); + $expirables->register(); + $permissions = new Core\Permissions\Permissions( $this->context, $authentication, $modules, $user_options, $dismissed_items ); $permissions->register(); From 75d7ca662162559b35d17a37a83386d681a685cb Mon Sep 17 00:00:00 2001 From: Simon Dowdles Date: Fri, 10 May 2024 15:55:25 +0200 Subject: [PATCH 036/134] Instantiate and register REST_Conversion_Tracking_Controller class in Conversion_Tracking. --- .../Conversion_Tracking/Conversion_Tracking.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/includes/Core/Conversion_Tracking/Conversion_Tracking.php b/includes/Core/Conversion_Tracking/Conversion_Tracking.php index 38704efecb2..31e2ef11c8f 100644 --- a/includes/Core/Conversion_Tracking/Conversion_Tracking.php +++ b/includes/Core/Conversion_Tracking/Conversion_Tracking.php @@ -42,6 +42,14 @@ class Conversion_Tracking { */ protected $conversion_tracking_settings; + /** + * REST_Conversion_Tracking_Controller instance. + * + * @since n.e.x.t + * @var REST_Conversion_Tracking_Controller + */ + protected $rest_conversion_tracking_controller; + /** * Supported conversion event providers. * @@ -64,9 +72,10 @@ class Conversion_Tracking { * @param Options $options Optional. Option API instance. Default is a new instance. */ public function __construct( Context $context, Options $options = null ) { - $this->context = $context; - $options = $options ?: new Options( $context ); - $this->conversion_tracking_settings = new Conversion_Tracking_Settings( $options ); + $this->context = $context; + $options = $options ?: new Options( $context ); + $this->conversion_tracking_settings = new Conversion_Tracking_Settings( $options ); + $this->rest_conversion_tracking_controller = new REST_Conversion_Tracking_Controller( $this->conversion_tracking_settings ); } /** @@ -76,6 +85,7 @@ public function __construct( Context $context, Options $options = null ) { */ public function register() { $this->conversion_tracking_settings->register(); + $this->rest_conversion_tracking_controller->register(); add_action( 'wp_enqueue_scripts', From e731ebbd0bd62b9e384af2924e967cf5d90c46b0 Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Fri, 10 May 2024 21:12:28 +0530 Subject: [PATCH 037/134] Working prototype for get and set operations. --- .../datastore/user/expirable-items.js | 32 +++++++++++++------ includes/Core/Expirables/Expirable_Items.php | 2 +- .../REST_Expirable_Items_Controller.php | 19 +++++++++-- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/assets/js/googlesitekit/datastore/user/expirable-items.js b/assets/js/googlesitekit/datastore/user/expirable-items.js index c977e556f89..45c7ceef5d8 100644 --- a/assets/js/googlesitekit/datastore/user/expirable-items.js +++ b/assets/js/googlesitekit/datastore/user/expirable-items.js @@ -35,7 +35,7 @@ const { getRegistry } = commonActions; function reducerCallback( state, expirableItems ) { return { ...state, - expirableItems: Array.isArray( expirableItems ) ? expirableItems : [], + expirableItems, }; } @@ -48,16 +48,22 @@ const fetchGetExpirableItemsStore = createFetchStore( { const fetchSetExpirableItemTimersStore = createFetchStore( { baseName: 'expirableItems', - controlCallback: ( { slug, expiresInSeconds } ) => - API.set( 'core', 'user', 'set-expirable-item-timers', { - slug, - expiration: expiresInSeconds, - } ), + controlCallback: ( items ) => + API.set( 'core', 'user', 'set-expirable-item-timers', items ), reducerCallback, argsToParams: ( items ) => { - return items; + return items.map( ( item ) => { + const { slug, expiresInSeconds } = item; + + return { + slug, + expiration: expiresInSeconds, + }; + } ); }, validateParams: ( items = [] ) => { + invariant( items.length, 'Items are required.' ); + items.forEach( ( item ) => { const { slug, expiresInSeconds = 0 } = item; invariant( slug, 'slug is required.' ); @@ -127,7 +133,13 @@ const baseSelectors = { * @return {(boolean|undefined)} TRUE if exists, otherwise FALSE, `undefined` if not resolved yet. */ hasExpirableItem: createRegistrySelector( ( select ) => ( state, slug ) => { - return select( CORE_USER ).getExpirableItems()?.includes( slug ); + const items = select( CORE_USER ).getExpirableItems(); + + if ( items ) { + return Object.keys( items )?.includes( slug ); + } + + return false; } ), /** @@ -148,9 +160,9 @@ const baseSelectors = { const expirableItems = select( CORE_USER ).getExpirableItems(); expirableItems.forEach( ( item ) => { const { expiresInSeconds = 0 } = item; - const timeStamp = Math.floor( Date.now() / 1000 ); - if ( expiresInSeconds < timeStamp ) { + // Compare with the current timestamp. + if ( expiresInSeconds < Math.floor( Date.now() / 1000 ) ) { active = true; } } ); diff --git a/includes/Core/Expirables/Expirable_Items.php b/includes/Core/Expirables/Expirable_Items.php index 7edebb43e20..d4a5da97b75 100644 --- a/includes/Core/Expirables/Expirable_Items.php +++ b/includes/Core/Expirables/Expirable_Items.php @@ -120,7 +120,7 @@ public function get_expirable_items() { $expirable_items = $this->get(); $expirable_items = $this->filter_expirable_items( $expirable_items ); - return array_keys( $expirable_items ); + return (array) $expirable_items; } /** diff --git a/includes/Core/Expirables/REST_Expirable_Items_Controller.php b/includes/Core/Expirables/REST_Expirable_Items_Controller.php index 5ead2519b1b..7aeca1e77d7 100644 --- a/includes/Core/Expirables/REST_Expirable_Items_Controller.php +++ b/includes/Core/Expirables/REST_Expirable_Items_Controller.php @@ -124,7 +124,7 @@ protected function get_rest_routes() { $expiration = null; if ( isset( $datum['expiration'] ) && intval( $datum['expiration'] ) > 0 ) { - $expiration = $data['expiration']; + $expiration = $datum['expiration']; } if ( ! $expiration ) { @@ -144,8 +144,21 @@ protected function get_rest_routes() { 'permission_callback' => $can_manage_expirable_item, 'args' => array( 'data' => array( - 'type' => 'object', - 'required' => true, + 'type' => 'array', + 'required' => true, + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'type' => 'string', + ), + 'expiration' => array( + 'type' => 'integer', + ), + ), + ), + ), ), ), ) From 60873e0c24e708b2d86626368897f5e4687cc64d Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 10 May 2024 14:14:10 -0400 Subject: [PATCH 038/134] Update conversionTrackingService. --- assets/js/modules/ads/pax/services.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/assets/js/modules/ads/pax/services.js b/assets/js/modules/ads/pax/services.js index 875c5874cb7..fc4593c0640 100644 --- a/assets/js/modules/ads/pax/services.js +++ b/assets/js/modules/ads/pax/services.js @@ -101,10 +101,18 @@ export function createPaxServices( registry ) { getPageViewConversionSetting: async () => { const websitePages = await restFetchWpPages(); return { - enablePageViewConversion: true, websitePages, }; }, + getSupportedConversionTrackingTypes: () => { + return { + conversionTrackingTypes: [ + // @TODO: Include TYPE_CONVERSION_EVENT in a future update. + // 'TYPE_CONVERSION_EVENT', + 'TYPE_PAGE_VIEW', + ], + }; + }, }, termsAndConditionsService: { notify: async () => {}, From 7f207740faddaa1ba69d75d1b6cb27d980285576 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 10 May 2024 14:26:20 -0400 Subject: [PATCH 039/134] Simplify and refactor createPaxServices. --- assets/js/modules/ads/pax/services.js | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/assets/js/modules/ads/pax/services.js b/assets/js/modules/ads/pax/services.js index fc4593c0640..39e643bd890 100644 --- a/assets/js/modules/ads/pax/services.js +++ b/assets/js/modules/ads/pax/services.js @@ -49,11 +49,13 @@ const restFetchWpPages = async () => { * @since 1.126.0 * * @param {Object} registry Registry object to dispatch to. + * @param {Object} _global The global window object. * @return {Object} An object containing various service interfaces. */ -export function createPaxServices( registry ) { +export function createPaxServices( registry, _global = global ) { + const { select, __experimentalResolveSelect: resolveSelect } = registry; const accessToken = - global?._googlesitekitPAXConfig?.authAccess?.oauthTokenAccess?.token; + _global?._googlesitekitPAXConfig?.authAccess?.oauthTokenAccess?.token; return { authenticationService: { @@ -68,14 +70,12 @@ export function createPaxServices( registry ) { }, businessService: { getBusinessInfo: async () => { - await registry - .__experimentalResolveSelect( CORE_SITE ) - .getSiteInfo(); + await resolveSelect( CORE_SITE ).getSiteInfo(); /* eslint-disable sitekit/acronym-case */ // Disabling rule because businessName and businessUrl are expected by PAX API. - const businessName = registry.select( CORE_SITE ).getSiteName(); - const businessUrl = registry.select( CORE_SITE ).getHomeURL(); + const businessName = select( CORE_SITE ).getSiteName(); + const businessUrl = select( CORE_SITE ).getHomeURL(); return { businessName, businessUrl }; /* eslint-enable sitekit/acronym-case */ @@ -86,18 +86,13 @@ export function createPaxServices( registry ) { }, }, conversionTrackingService: { - // eslint-disable-next-line require-await getSupportedConversionLabels: async () => { - await registry - .__experimentalResolveSelect( MODULES_ADS ) - .getModuleData(); + await resolveSelect( MODULES_ADS ).getModuleData(); const conversionEvents = - registry - .select( MODULES_ADS ) - .getSupportedConversionEvents() || []; + select( MODULES_ADS ).getSupportedConversionEvents() || []; + return { conversionLabels: conversionEvents }; }, - // eslint-disable-next-line require-await getPageViewConversionSetting: async () => { const websitePages = await restFetchWpPages(); return { From 89e765b9d38a7c5f03397abc3d8c601dfb7773fe Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 10 May 2024 14:27:49 -0400 Subject: [PATCH 040/134] Update test to provide data instead of global. --- assets/js/modules/ads/pax/services.test.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/assets/js/modules/ads/pax/services.test.js b/assets/js/modules/ads/pax/services.test.js index bd14646208f..be0b0b0c091 100644 --- a/assets/js/modules/ads/pax/services.test.js +++ b/assets/js/modules/ads/pax/services.test.js @@ -23,6 +23,7 @@ import { createTestRegistry, provideSiteInfo, } from '../../../../../tests/js/utils'; +import { MODULES_ADS } from '../datastore/constants'; import { createPaxServices } from './services'; describe( 'PAX partner services', () => { @@ -132,14 +133,9 @@ describe( 'PAX partner services', () => { it( 'should hold correct value for conversionLabels property when data is present', async () => { const mockSupportedEvents = [ 'mock-event' ]; - const adsModuleDataVar = '_googlesitekitModulesData'; - const adsModuleDataVarValue = { - ads: { - supportedConversionEvents: mockSupportedEvents, - }, - }; - - global[ adsModuleDataVar ] = adsModuleDataVarValue; + registry.dispatch( MODULES_ADS ).receiveModuleData( { + supportedConversionEvents: mockSupportedEvents, + } ); const supportedConversionLabels = await services.conversionTrackingService.getSupportedConversionLabels(); @@ -147,8 +143,6 @@ describe( 'PAX partner services', () => { expect( supportedConversionLabels.conversionLabels ).toEqual( mockSupportedEvents ); - - delete global[ adsModuleDataVar ]; } ); } ); From 0bfba99cb4ab2ea37809cefad0e3d492dd190243 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 10 May 2024 14:28:09 -0400 Subject: [PATCH 041/134] Remove test for removed property. --- assets/js/modules/ads/pax/services.test.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/assets/js/modules/ads/pax/services.test.js b/assets/js/modules/ads/pax/services.test.js index be0b0b0c091..fdb07e53773 100644 --- a/assets/js/modules/ads/pax/services.test.js +++ b/assets/js/modules/ads/pax/services.test.js @@ -147,15 +147,6 @@ describe( 'PAX partner services', () => { } ); describe( 'getPageViewConversionSetting', () => { - it( 'should hold correct value for enablePageViewConversion property', async () => { - const pageViewConversionSetting = - await services.conversionTrackingService.getPageViewConversionSetting(); - - expect( - pageViewConversionSetting.enablePageViewConversion - ).toBe( true ); - } ); - it( 'should hold correct value for websitePages property', async () => { const wpPagesEndpoint = new RegExp( '^/wp/v2/pages' ); From b0e6f03d161b9f77cfa4ad462a7cc7fac358a3e2 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 10 May 2024 14:36:36 -0400 Subject: [PATCH 042/134] Refactor accessToken test. --- assets/js/modules/ads/pax/services.test.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/assets/js/modules/ads/pax/services.test.js b/assets/js/modules/ads/pax/services.test.js index fdb07e53773..981d8641486 100644 --- a/assets/js/modules/ads/pax/services.test.js +++ b/assets/js/modules/ads/pax/services.test.js @@ -34,17 +34,6 @@ describe( 'PAX partner services', () => { beforeEach( () => { registry = createTestRegistry(); services = createPaxServices( registry ); - global._googlesitekitPAXConfig = { - authAccess: { - oauthTokenAccess: { - token: 'test-auth-token', - }, - }, - }; - } ); - - afterAll( () => { - global._googlesitekitPAXConfig = undefined; } ); it( 'should return object with correct services', () => { @@ -78,6 +67,17 @@ describe( 'PAX partner services', () => { expect( authAccess ).toHaveProperty( 'accessToken' ); } ); it( 'should contain correct accessToken', async () => { + const _googlesitekitPAXConfig = { + authAccess: { + oauthTokenAccess: { + token: 'test-auth-token', + }, + }, + }; + services = createPaxServices( registry, { + _googlesitekitPAXConfig, + } ); + const authAccess = await services.authenticationService.get(); From a4122f6e8e696bd94b2994067df7a4507c4642e0 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 10 May 2024 14:37:30 -0400 Subject: [PATCH 043/134] Add test for getSupportedConversionTrackingTypes. --- assets/js/modules/ads/pax/services.test.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/assets/js/modules/ads/pax/services.test.js b/assets/js/modules/ads/pax/services.test.js index 981d8641486..249728cf551 100644 --- a/assets/js/modules/ads/pax/services.test.js +++ b/assets/js/modules/ads/pax/services.test.js @@ -195,5 +195,18 @@ describe( 'PAX partner services', () => { } ); } ); } ); + + describe( 'getSupportedConversionTrackingTypes', () => { + it( 'should return the expected supported types', () => { + const supportedTypes = + services.conversionTrackingService.getSupportedConversionTrackingTypes( + {} + ); + + expect( supportedTypes ).toMatchObject( { + conversionTrackingTypes: [ 'TYPE_PAGE_VIEW' ], + } ); + } ); + } ); } ); } ); From e4c976e8723c322f35983e848f6e77e8cb5661aa Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 10 May 2024 14:40:07 -0400 Subject: [PATCH 044/134] Ensure getSupportedConversionTrackingTypes always returns Promise. --- assets/js/modules/ads/pax/services.js | 3 ++- assets/js/modules/ads/pax/services.test.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/assets/js/modules/ads/pax/services.js b/assets/js/modules/ads/pax/services.js index 39e643bd890..7e921463ff7 100644 --- a/assets/js/modules/ads/pax/services.js +++ b/assets/js/modules/ads/pax/services.js @@ -99,7 +99,8 @@ export function createPaxServices( registry, _global = global ) { websitePages, }; }, - getSupportedConversionTrackingTypes: () => { + // eslint-disable-next-line require-await + getSupportedConversionTrackingTypes: async () => { return { conversionTrackingTypes: [ // @TODO: Include TYPE_CONVERSION_EVENT in a future update. diff --git a/assets/js/modules/ads/pax/services.test.js b/assets/js/modules/ads/pax/services.test.js index 249728cf551..15f0a0b78ec 100644 --- a/assets/js/modules/ads/pax/services.test.js +++ b/assets/js/modules/ads/pax/services.test.js @@ -197,9 +197,9 @@ describe( 'PAX partner services', () => { } ); describe( 'getSupportedConversionTrackingTypes', () => { - it( 'should return the expected supported types', () => { + it( 'should return the expected supported types', async () => { const supportedTypes = - services.conversionTrackingService.getSupportedConversionTrackingTypes( + await services.conversionTrackingService.getSupportedConversionTrackingTypes( {} ); From 4c82fcea3eccd80ba22a1c53effbe6e2820ecc3f Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 10 May 2024 14:48:39 -0400 Subject: [PATCH 045/134] Update main assertion to fail on missing service method. --- assets/js/modules/ads/pax/services.test.js | 37 +++++++++++----------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/assets/js/modules/ads/pax/services.test.js b/assets/js/modules/ads/pax/services.test.js index 15f0a0b78ec..668476a073c 100644 --- a/assets/js/modules/ads/pax/services.test.js +++ b/assets/js/modules/ads/pax/services.test.js @@ -37,25 +37,24 @@ describe( 'PAX partner services', () => { } ); it( 'should return object with correct services', () => { - expect( services ).toEqual( - expect.objectContaining( { - authenticationService: expect.objectContaining( { - get: expect.any( Function ), - fix: expect.any( Function ), - } ), - businessService: expect.objectContaining( { - getBusinessInfo: expect.any( Function ), - fixBusinessInfo: expect.any( Function ), - } ), - conversionTrackingService: expect.objectContaining( { - getSupportedConversionLabels: expect.any( Function ), - getPageViewConversionSetting: expect.any( Function ), - } ), - termsAndConditionsService: expect.objectContaining( { - notify: expect.any( Function ), - } ), - } ) - ); + expect( services ).toEqual( { + authenticationService: { + get: expect.any( Function ), + fix: expect.any( Function ), + }, + businessService: { + getBusinessInfo: expect.any( Function ), + fixBusinessInfo: expect.any( Function ), + }, + conversionTrackingService: { + getSupportedConversionLabels: expect.any( Function ), + getPageViewConversionSetting: expect.any( Function ), + getSupportedConversionTrackingTypes: expect.any( Function ), + }, + termsAndConditionsService: { + notify: expect.any( Function ), + }, + } ); } ); describe( 'authenticationService', () => { From 91a042956ee707c81eb25d2c3a781cfb78f51fbe Mon Sep 17 00:00:00 2001 From: nfmohit Date: Mon, 13 May 2024 02:30:41 +0600 Subject: [PATCH 046/134] Move `safelySort` function to common location. --- assets/js/components/SelectionPanel/SelectionPanelFooter.js | 2 +- assets/js/util/index.js | 1 + .../MetricsSelectionPanel/utils.js => util/safely-sort.js} | 5 +++-- 3 files changed, 5 insertions(+), 3 deletions(-) rename assets/js/{components/KeyMetrics/MetricsSelectionPanel/utils.js => util/safely-sort.js} (84%) diff --git a/assets/js/components/SelectionPanel/SelectionPanelFooter.js b/assets/js/components/SelectionPanel/SelectionPanelFooter.js index f128d0d5b85..f375795e315 100644 --- a/assets/js/components/SelectionPanel/SelectionPanelFooter.js +++ b/assets/js/components/SelectionPanel/SelectionPanelFooter.js @@ -20,7 +20,7 @@ import { __, sprintf } from '@wordpress/i18n'; */ import { Button, SpinnerButton } from 'googlesitekit-components'; import ErrorNotice from '../ErrorNotice'; -import { safelySort } from '../KeyMetrics/MetricsSelectionPanel/utils'; // FIXME, extract this to a common location. +import { safelySort } from '../../util'; export default function SelectionPanelFooter( { savedItemSlugs, diff --git a/assets/js/util/index.js b/assets/js/util/index.js index ce04d48950f..cbee2aac599 100644 --- a/assets/js/util/index.js +++ b/assets/js/util/index.js @@ -37,6 +37,7 @@ export * from './chart'; export * from './urls'; export * from './is-valid-numeric-id'; export * from './isnumeric'; +export * from './safely-sort'; global._gsktag = trackEvent; /** diff --git a/assets/js/components/KeyMetrics/MetricsSelectionPanel/utils.js b/assets/js/util/safely-sort.js similarity index 84% rename from assets/js/components/KeyMetrics/MetricsSelectionPanel/utils.js rename to assets/js/util/safely-sort.js index 2d1d872fff0..912ceb9639d 100644 --- a/assets/js/components/KeyMetrics/MetricsSelectionPanel/utils.js +++ b/assets/js/util/safely-sort.js @@ -1,7 +1,7 @@ /** - * Metric Selection Panel utils. + * `safelySort` utility function. * - * Site Kit by Google, Copyright 2023 Google LLC + * Site Kit by Google, Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ * If the parameter is not an array, it returns the parameter as is. * * @since 1.110.0 + * @since n.e.x.t Moved to the common utility directory from the key metrics directory. * * @param {Array|*} arr Param to be sorted. * @return {Array|*} Safely sorted array without mutation. From 3c34e18ab15969db942d6f4b20d1a3652596880e Mon Sep 17 00:00:00 2001 From: nfmohit Date: Mon, 13 May 2024 02:34:11 +0600 Subject: [PATCH 047/134] Apply improvements over POC. --- .../MetricsSelectionPanel/MetricItem.js | 5 +- .../MetricsSelectionPanel/MetricItems.js | 5 +- .../MetricsSelectionPanel/MetricsFooter.js | 14 ++-- .../MetricsSelectionPanel/MetricsHeader.js | 11 ++- .../KeyMetrics/MetricsSelectionPanel/index.js | 4 +- .../SelectionPanel/SelectionPanel.js | 33 +++++++++ .../SelectionPanel/SelectionPanelFooter.js | 38 +++++++++- .../SelectionPanel/SelectionPanelHeader.js | 32 ++++++++ .../SelectionPanel/SelectionPanelItem.js | 37 ++++++++++ .../SelectionPanel/SelectionPanelItems.js | 73 ++++++++++++------- assets/js/components/SelectionPanel/index.js | 22 ++++++ 11 files changed, 231 insertions(+), 43 deletions(-) diff --git a/assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricItem.js b/assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricItem.js index e04aec85176..c6d368b27de 100644 --- a/assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricItem.js +++ b/assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricItem.js @@ -39,7 +39,6 @@ import { SelectionPanelItem } from '../../SelectionPanel'; const { useSelect, useDispatch } = Data; export default function MetricItem( { - id, slug, title, description, @@ -94,13 +93,14 @@ export default function MetricItem( { const isMetricDisabled = ! savedItemSlugs.includes( slug ) && disconnectedModules.length > 0; + const id = `key-metric-selection-checkbox-${ slug }`; + return ( ); } + +MetricsHeader.propTypes = { + closeFn: PropTypes.func, +}; diff --git a/assets/js/components/KeyMetrics/MetricsSelectionPanel/index.js b/assets/js/components/KeyMetrics/MetricsSelectionPanel/index.js index d1cda8da41f..2999840c121 100644 --- a/assets/js/components/KeyMetrics/MetricsSelectionPanel/index.js +++ b/assets/js/components/KeyMetrics/MetricsSelectionPanel/index.js @@ -33,13 +33,13 @@ import { KEY_METRICS_SELECTION_FORM, KEY_METRICS_SELECTION_PANEL_OPENED_KEY, } from '../constants'; +import CustomDimensionsNotice from './CustomDimensionsNotice'; import MetricsHeader from './MetricsHeader'; import MetricsFooter from './MetricsFooter'; import MetricItems from './MetricItems'; -import CustomDimensionsNotice from './CustomDimensionsNotice'; +import SelectionPanel from '../../SelectionPanel'; import useViewContext from '../../../hooks/useViewContext'; import { trackEvent } from '../../../util'; -import SelectionPanel from '../../SelectionPanel'; const { useSelect, useDispatch } = Data; export default function MetricsSelectionPanel() { diff --git a/assets/js/components/SelectionPanel/SelectionPanel.js b/assets/js/components/SelectionPanel/SelectionPanel.js index 906ff5667d3..09185557547 100644 --- a/assets/js/components/SelectionPanel/SelectionPanel.js +++ b/assets/js/components/SelectionPanel/SelectionPanel.js @@ -1,3 +1,29 @@ +/** + * Selection Panel component. + * + * Site Kit by Google, Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +/** + * Internal dependencies + */ import SideSheet from '../SideSheet'; export default function SelectionPanel( { @@ -21,3 +47,10 @@ export default function SelectionPanel( { ); } + +SelectionPanel.propTypes = { + children: PropTypes.node, + isOpen: PropTypes.bool, + onOpen: PropTypes.func, + closeFn: PropTypes.func, +}; diff --git a/assets/js/components/SelectionPanel/SelectionPanelFooter.js b/assets/js/components/SelectionPanel/SelectionPanelFooter.js index f375795e315..203746a2f8f 100644 --- a/assets/js/components/SelectionPanel/SelectionPanelFooter.js +++ b/assets/js/components/SelectionPanel/SelectionPanelFooter.js @@ -1,7 +1,26 @@ +/** + * Selection Panel Footer component. + * + * Site Kit by Google, Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * External dependencies */ import { isEqual } from 'lodash'; +import PropTypes from 'prop-types'; /** * WordPress dependencies @@ -23,8 +42,8 @@ import ErrorNotice from '../ErrorNotice'; import { safelySort } from '../../util'; export default function SelectionPanelFooter( { - savedItemSlugs, - selectedItemSlugs, + savedItemSlugs = [], + selectedItemSlugs = [], saveSettings, saveError, itemLimitError, @@ -158,3 +177,18 @@ export default function SelectionPanelFooter( { ); } + +SelectionPanelFooter.propTypes = { + savedItemSlugs: PropTypes.array, + selectedItemSlugs: PropTypes.array, + saveSettings: PropTypes.func, + saveError: PropTypes.object, + itemLimitError: PropTypes.string, + minSelectedItemCount: PropTypes.number, + maxSelectedItemCount: PropTypes.number, + isBusy: PropTypes.bool, + onSaveSuccess: PropTypes.func, + onCancel: PropTypes.func, + isOpen: PropTypes.bool, + closeFn: PropTypes.func, +}; diff --git a/assets/js/components/SelectionPanel/SelectionPanelHeader.js b/assets/js/components/SelectionPanel/SelectionPanelHeader.js index 73c160f0b37..0cb2296f623 100644 --- a/assets/js/components/SelectionPanel/SelectionPanelHeader.js +++ b/assets/js/components/SelectionPanel/SelectionPanelHeader.js @@ -1,3 +1,29 @@ +/** + * Selection Panel Header component. + * + * Site Kit by Google, Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +/** + * Internal dependencies + */ import Link from '../Link'; import CloseIcon from '../../../svg/icons/close.svg'; @@ -22,3 +48,9 @@ export default function SelectionPanelHeader( { ); } + +SelectionPanelHeader.propTypes = { + children: PropTypes.node, + title: PropTypes.string, + onCloseClick: PropTypes.func, +}; diff --git a/assets/js/components/SelectionPanel/SelectionPanelItem.js b/assets/js/components/SelectionPanel/SelectionPanelItem.js index 049b70b7741..60eb52749ac 100644 --- a/assets/js/components/SelectionPanel/SelectionPanelItem.js +++ b/assets/js/components/SelectionPanel/SelectionPanelItem.js @@ -1,3 +1,29 @@ +/** + * Selection Panel Item component. + * + * Site Kit by Google, Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +/** + * Internal dependencies + */ import SelectionBox from '../SelectionBox'; export default function SelectionPanelItem( { @@ -26,3 +52,14 @@ export default function SelectionPanelItem( {
); } + +SelectionPanelItem.propTypes = { + children: PropTypes.node, + id: PropTypes.string, + slug: PropTypes.string, + title: PropTypes.string, + description: PropTypes.string, + isItemSelected: PropTypes.bool, + isItemDisabled: PropTypes.bool, + onCheckboxChange: PropTypes.func, +}; diff --git a/assets/js/components/SelectionPanel/SelectionPanelItems.js b/assets/js/components/SelectionPanel/SelectionPanelItems.js index 566ba5eb683..af1d2ca9dd4 100644 --- a/assets/js/components/SelectionPanel/SelectionPanelItems.js +++ b/assets/js/components/SelectionPanel/SelectionPanelItems.js @@ -1,49 +1,63 @@ +/** + * Selection Panel Items component. + * + * Site Kit by Google, Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + /** * WordPress dependencies */ import { Fragment } from '@wordpress/element'; - -// import MetricItem from './MetricItem'; +import { __ } from '@wordpress/i18n'; export default function SelectionPanelItems( { - currentSelectionTitle, - availableItemsTitle, - savedItemSlugs, + currentSelectionTitle = __( 'Current selection', 'google-site-kit' ), + availableItemsTitle = __( 'Additional items', 'google-site-kit' ), + savedItemSlugs = [], availableSavedItems, availableUnsavedItems, ItemComponent, } ) { - const renderMetricItems = ( metrics ) => { - return Object.keys( metrics ).map( ( slug ) => { - const { title, description } = metrics[ slug ]; - - const id = `key-metric-selection-checkbox-${ slug }`; - - return ( - - ); - } ); + const renderItems = ( items ) => { + return Object.keys( items ).map( ( slug ) => ( + + ) ); }; return (
{ // Split list into two sections with sub-headings for current selection and - // additional metrics if there are already saved metrics. + // additional items if there are already saved items. savedItemSlugs.length !== 0 && (

{ currentSelectionTitle }

- { renderMetricItems( availableSavedItems ) } + { renderItems( availableSavedItems ) }

{ availableItemsTitle } @@ -52,8 +66,17 @@ export default function SelectionPanelItems( { ) }

- { renderMetricItems( availableUnsavedItems ) } + { renderItems( availableUnsavedItems ) }
); } + +SelectionPanelItems.propTypes = { + currentSelectionTitle: PropTypes.string, + availableItemsTitle: PropTypes.string, + savedItemSlugs: PropTypes.array, + availableSavedItems: PropTypes.object, + availableUnsavedItems: PropTypes.object, + ItemComponent: PropTypes.elementType, +}; diff --git a/assets/js/components/SelectionPanel/index.js b/assets/js/components/SelectionPanel/index.js index b6c820f7855..3c1eb55e3c7 100644 --- a/assets/js/components/SelectionPanel/index.js +++ b/assets/js/components/SelectionPanel/index.js @@ -1,4 +1,26 @@ +/** + * Selection Panel components. + * + * Site Kit by Google, Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Internal dependencies + */ import SelectionPanel from './SelectionPanel'; + export { default as SelectionPanelHeader } from './SelectionPanelHeader'; export { default as SelectionPanelItem } from './SelectionPanelItem'; export { default as SelectionPanelItems } from './SelectionPanelItems'; From 7ea3bb35dd70112f679f6336b6d0f5d0b327aa3e Mon Sep 17 00:00:00 2001 From: nfmohit Date: Mon, 13 May 2024 03:39:47 +0600 Subject: [PATCH 048/134] Apply refactor to CSS. --- .../CustomDimensionsNotice.js | 7 ++-- .../MetricsSelectionPanel/MetricItem.js | 2 +- .../SelectionPanel/SelectionPanel.js | 4 +-- .../SelectionPanel/SelectionPanelFooter.js | 10 +++--- .../SelectionPanel/SelectionPanelHeader.js | 6 ++-- .../SelectionPanel/SelectionPanelItem.js | 2 +- .../SelectionPanel/SelectionPanelItems.js | 10 +++--- assets/sass/admin.scss | 2 +- .../_googlesitekit-selection-panel.scss} | 34 +++++++++---------- 9 files changed, 37 insertions(+), 40 deletions(-) rename assets/sass/components/{key-metrics/_googlesitekit-km-selection-panel.scss => global/_googlesitekit-selection-panel.scss} (75%) diff --git a/assets/js/components/KeyMetrics/MetricsSelectionPanel/CustomDimensionsNotice.js b/assets/js/components/KeyMetrics/MetricsSelectionPanel/CustomDimensionsNotice.js index 9a6dac10642..1fbb590fc2d 100644 --- a/assets/js/components/KeyMetrics/MetricsSelectionPanel/CustomDimensionsNotice.js +++ b/assets/js/components/KeyMetrics/MetricsSelectionPanel/CustomDimensionsNotice.js @@ -89,7 +89,7 @@ function CustomDimensionsNotice() { if ( currentFocusedElement && currentFocusedElement.closest( - '.googlesitekit-km-selection-panel-metrics__metric-item' + '.googlesitekit-selection-panel-item' ) && elementsOverlap( noticeRef.current, currentFocusedElement ) ) { @@ -113,10 +113,7 @@ function CustomDimensionsNotice() { ); return ( -
+

{ customDimensionMessage }

); diff --git a/assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricItem.js b/assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricItem.js index c6d368b27de..9e2830960bf 100644 --- a/assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricItem.js +++ b/assets/js/components/KeyMetrics/MetricsSelectionPanel/MetricItem.js @@ -106,7 +106,7 @@ export default function MetricItem( { onCheckboxChange={ onCheckboxChange } > { disconnectedModules.length > 0 && ( -
+
{ sprintf( /* translators: %s: module names. */ _n( diff --git a/assets/js/components/SelectionPanel/SelectionPanel.js b/assets/js/components/SelectionPanel/SelectionPanel.js index 09185557547..6c222c8a3f1 100644 --- a/assets/js/components/SelectionPanel/SelectionPanel.js +++ b/assets/js/components/SelectionPanel/SelectionPanel.js @@ -34,13 +34,13 @@ export default function SelectionPanel( { } ) { return ( { children } diff --git a/assets/js/components/SelectionPanel/SelectionPanelFooter.js b/assets/js/components/SelectionPanel/SelectionPanelFooter.js index 203746a2f8f..c8de3e718ed 100644 --- a/assets/js/components/SelectionPanel/SelectionPanelFooter.js +++ b/assets/js/components/SelectionPanel/SelectionPanelFooter.js @@ -119,9 +119,9 @@ export default function SelectionPanelFooter( { const selectedItemCount = selectedItemSlugs?.length || 0; return ( -