diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index b44ef573a7..f1c36a3598 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -1,14 +1,14 @@
# Contributing
-Thanks for your interest in contributing to Google Listings and Ads!
+Thanks for your interest in contributing to Google for WooCommerce!
## Guidelines
Like the WooCommerce project, we want to ensure a welcoming environment for everyone. With that in mind, all contributors are expected to follow our [Code of Conduct](./CODE_OF_CONDUCT.md).
-If you wish to contribute code, please read the information in the sections below. Then [fork](https://help.github.com/articles/fork-a-repo/) Google Listings and Ads, commit your changes, and [submit a pull request](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) 🎉
+If you wish to contribute code, please read the information in the sections below. Then [fork](https://help.github.com/articles/fork-a-repo/) Google for WooCommerce, commit your changes, and [submit a pull request](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) 🎉
-Google Listings and Ads is licensed under the GPLv3+, and all contributions to the project will be released under the same license. You maintain copyright over any contribution you make, and by submitting a pull request, you are agreeing to release that contribution under the GPLv3+ license.
+Google for WooCommerce is licensed under the GPLv3+, and all contributions to the project will be released under the same license. You maintain copyright over any contribution you make, and by submitting a pull request, you are agreeing to release that contribution under the GPLv3+ license.
## Reporting Security Issues
diff --git a/README.md b/README.md
index 8d7a7a9590..b3c2d10244 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Google Listings & Ads
+# Google for WooCommerce
[![PHP Unit Tests](https://github.com/woocommerce/google-listings-and-ads/actions/workflows/php-unit-tests.yml/badge.svg)](https://github.com/woocommerce/google-listings-and-ads/actions/workflows/php-unit-tests.yml)
[![JavaScript Unit Tests](https://github.com/woocommerce/google-listings-and-ads/actions/workflows/js-unit-tests.yml/badge.svg)](https://github.com/woocommerce/google-listings-and-ads/actions/workflows/js-unit-tests.yml)
@@ -19,7 +19,7 @@ This repository is not suitable for support. Please don't use our issue tracker
For self help, start with our [user documentation](https://woocommerce.com/document/google-listings-and-ads/).
-The best place to get support is the [WordPress.org Google Listings and Ads forum](https://wordpress.org/support/plugin/google-listings-and-ads/).
+The best place to get support is the [WordPress.org Google for WooCommerce forum](https://wordpress.org/support/plugin/google-listings-and-ads/).
If you have a WooCommerce.com account, you can [start a chat or open a ticket on WooCommerce.com](https://woocommerce.com/my-account/contact-support/).
diff --git a/changelog.txt b/changelog.txt
index 034a016c13..24e282317f 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,4 +1,8 @@
-*** WooCommerce Google Listings and Ads Changelog ***
+*** Google for WooCommerce Changelog ***
+
+= 2.8.0 - 2024-07-31 =
+* Add Google API Pull method.
+* Rebranding Google Listings and Ads with Google for WooCommerce.
= 2.7.7 - 2024-07-24 =
* Dev - Fix E2E tests failed with WC 9.1.
@@ -350,7 +354,7 @@
* Add - Declare compatibility for High Performance Order Storage.
* Dev - Selectively externalize bundled packages.
* Fix - E2E Testing: Reduce the false positive rate and adjust the running events on GitHub Actions.
-* Fix - Move the order of Google Listings & Ads below the Coupons in the Marketing menu of WooCommerce admin page.
+* Fix - Move the order of Google Listings and Ads below the Coupons in the Marketing menu of WooCommerce admin page.
* Fix - WC 6.9 compatibility: Shipping time settings should not appear after selecting the "complex" shipping option.
* Fix - WC 6.9 compatibility: The free shipping threshold should be cleared after selecting the "No" free shipping option.
* Fix - WC 6.9 compatibility: The selected free shipping option should be reset after setting all shipping rates to 0.
@@ -560,7 +564,7 @@
= 1.12.0 - 2022-03-29 =
* Add - Additional data points for tracker snapshot.
* Add - Enables merchants to select multiple countries as their audience when creating a Google Ads campaign (Smart Shopping Campaign).
-* Add - Google Listings And Ads product attributes icon.
+* Add - Google Listings and Ads product attributes icon.
* Add - Integration with WooCommerce Shipping Zone to automatically sync shipping settings to Merchant Center.
* FIx - Show right link and message in Paid Campaigns report when there is no data available.
* Fix - Cleanup synced products locally when disconnecting Merchant Center account.
diff --git a/docs/gtag-consent-mode.md b/docs/gtag-consent-mode.md
index 6c8edaec52..e5b78c6221 100644
--- a/docs/gtag-consent-mode.md
+++ b/docs/gtag-consent-mode.md
@@ -1,6 +1,6 @@
## Google Analytics (gtag) Consent Mode
-Unless you're running the [Google Analytics for WooCommerce](https://woocommerce.com/products/woocommerce-google-analytics/) extension for a more sophisticated configuration, Google Listings and Ads will add Google's `gtag` to help you track some customer behavior.
+Unless you're running the [Google Analytics for WooCommerce](https://woocommerce.com/products/woocommerce-google-analytics/) extension for a more sophisticated configuration, Google for WooCommerce will add Google's `gtag` to help you track some customer behavior.
To respect your customers' privacy, we set up the default state of [consent mode](https://support.google.com/analytics/answer/9976101). We set it to deny all the parameters for visitors from the EEA region.
@@ -12,4 +12,4 @@ After the page loads, the consent for particular parameters can be updated by ot
The extension does not provide any UI, like a cookie banner, to let your visitors grant consent for tracking. However, it's integrated with [WP Consent API](https://wordpress.org/plugins/wp-consent-api/), so you can pick another extension that provides a banner that meets your needs.
-Each of those extensions may require additional setup or registration. Usually, the basic default setup works out of the box, but there may be some integration caveats.
\ No newline at end of file
+Each of those extensions may require additional setup or registration. Usually, the basic default setup works out of the box, but there may be some integration caveats.
diff --git a/google-listings-and-ads.php b/google-listings-and-ads.php
index a151561b15..10857a31bd 100644
--- a/google-listings-and-ads.php
+++ b/google-listings-and-ads.php
@@ -1,9 +1,9 @@
{
return (
+
}
diff --git a/js/src/attribute-mapping/index.test.js b/js/src/attribute-mapping/index.test.js
index d3dac77321..abdbe92501 100644
--- a/js/src/attribute-mapping/index.test.js
+++ b/js/src/attribute-mapping/index.test.js
@@ -132,10 +132,14 @@ jest.mock( '.~/hooks/usePolling', () => ( {
} ),
} ) );
+jest.mock( '.~/components/tours/rebranding-tour', () =>
+ jest.fn().mockReturnValue( null ).mockName( 'RebrandingTour' )
+);
+
/**
* External dependencies
*/
-import { render, fireEvent } from '@testing-library/react';
+import { render, fireEvent, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';
@@ -153,6 +157,7 @@ import {
} from '.~/data/actions';
import AttributeMappingSync from '.~/attribute-mapping/attribute-mapping-sync';
import usePolling from '.~/hooks/usePolling';
+import RebrandingTour from '.~/components/tours/rebranding-tour';
describe( 'Attribute Mapping', () => {
test( 'Renders table', () => {
@@ -378,4 +383,26 @@ describe( 'Attribute Mapping', () => {
const { queryByText } = render(
);
expect( queryByText( 'Scheduled for sync' ) ).toBeTruthy();
} );
+
+ describe( 'Rebranding Tour', () => {
+ test( 'Not rendered in UI', () => {
+ RebrandingTour.mockImplementation( () => {
+ return null;
+ } );
+
+ render(
);
+ const tour = screen.queryByRole( 'dialog', { name: 'tour' } );
+ expect( tour ).not.toBeInTheDocument();
+ } );
+
+ test( 'Rendered in UI', () => {
+ RebrandingTour.mockImplementation( () => {
+ return
;
+ } );
+
+ render(
);
+ const tour = screen.queryByRole( 'dialog', { name: 'tour' } );
+ expect( tour ).toBeInTheDocument();
+ } );
+ } );
} );
diff --git a/js/src/components/app-notice/index.js b/js/src/components/app-notice/index.js
new file mode 100644
index 0000000000..841b089562
--- /dev/null
+++ b/js/src/components/app-notice/index.js
@@ -0,0 +1,33 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+import { Notice } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import './index.scss';
+
+/**
+ * Renders a Notice component with extra props.
+ *
+ * It supports all the props from @wordpress/components - Notice Component
+ *
+ * ## Usage
+ *
+ * ```jsx
+ *
+ * My Notice
+ *
+ * ```
+ *
+ * @param {*} props Props to be forwarded to {@link Notice}.
+ */
+const AppNotice = ( props ) => {
+ const { className, ...rest } = props;
+ const classes = [ 'app-notice', className ];
+ return ;
+};
+
+export default AppNotice;
diff --git a/js/src/components/app-notice/index.scss b/js/src/components/app-notice/index.scss
new file mode 100644
index 0000000000..8c38d351e0
--- /dev/null
+++ b/js/src/components/app-notice/index.scss
@@ -0,0 +1,6 @@
+.app-notice {
+ border: 0;
+ font-size: $helptext-font-size;
+ margin: 0 var(--large-gap) var(--main-gap);
+ padding: $grid-unit-20;
+}
diff --git a/js/src/components/enable-new-product-sync-button.js b/js/src/components/enable-new-product-sync-button.js
new file mode 100644
index 0000000000..129423b856
--- /dev/null
+++ b/js/src/components/enable-new-product-sync-button.js
@@ -0,0 +1,58 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { addQueryArgs } from '@wordpress/url';
+import { useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import AppButton from '.~/components/app-button';
+import { glaData } from '.~/constants';
+import { API_NAMESPACE } from '.~/data/constants';
+import useApiFetchCallback from '.~/hooks/useApiFetchCallback';
+import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices';
+
+/**
+ * Button to initiate auth process for WP Rest API
+ *
+ * @param {Object} params The component params
+ * @return {JSX.Element} The button.
+ */
+const EnableNewProductSyncButton = ( params ) => {
+ const { createNotice } = useDispatchCoreNotices();
+ const [ loading, setLoading ] = useState( false );
+ const nextPageName = glaData.mcSetupComplete ? 'settings' : 'setup-mc';
+ const query = { next_page_name: nextPageName };
+ const path = addQueryArgs( `${ API_NAMESPACE }/rest-api/authorize`, query );
+ const [ fetchRestAPIAuthorize ] = useApiFetchCallback( { path } );
+ const handleEnableClick = async () => {
+ try {
+ setLoading( true );
+ const d = await fetchRestAPIAuthorize();
+ setLoading( false );
+ window.location.href = d.auth_url;
+ } catch ( error ) {
+ setLoading( false );
+ createNotice(
+ 'error',
+ __(
+ 'Unable to enable new product sync. Please try again later.',
+ 'google-listings-and-ads'
+ )
+ );
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default EnableNewProductSyncButton;
diff --git a/js/src/components/enable-new-product-sync-notice.js b/js/src/components/enable-new-product-sync-notice.js
new file mode 100644
index 0000000000..01f4657774
--- /dev/null
+++ b/js/src/components/enable-new-product-sync-notice.js
@@ -0,0 +1,56 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Notice } from '@wordpress/components';
+import { createInterpolateElement } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import useGoogleMCAccount from '.~/hooks/useGoogleMCAccount';
+import EnableNewProductSyncButton from '.~/components/enable-new-product-sync-button';
+
+/**
+ * Shows info {@link Notice}
+ * about enabling new product sync when the feature flag is turned on and hasn't switched to new product sync.
+ *
+ * @return {JSX.Element} {@link Notice} element with the info message and the button to enable new product sync.
+ */
+const EnableNewProductSyncNotice = () => {
+ const {
+ hasFinishedResolution: hasGoogleMCAccountFinishedResolution,
+ googleMCAccount,
+ } = useGoogleMCAccount();
+
+ // Do not render if already switch to new product sync.
+ if (
+ ! hasGoogleMCAccountFinishedResolution ||
+ ! googleMCAccount.notification_service_enabled ||
+ googleMCAccount.wpcom_rest_api_status
+ ) {
+ return null;
+ }
+
+ return (
+
+ { createInterpolateElement(
+ __(
+ 'We will soon transition to a new and improved method for synchronizing product data with Google.
Get early access',
+ 'google-listings-and-ads'
+ ),
+ {
+ enableButton: (
+
+ ),
+ p: ,
+ }
+ ) }
+
+ );
+};
+
+export default EnableNewProductSyncNotice;
diff --git a/js/src/components/google-mc-account-card/connected-google-mc-account-card.js b/js/src/components/google-mc-account-card/connected-google-mc-account-card.js
index 6976d0f846..d17820903f 100644
--- a/js/src/components/google-mc-account-card/connected-google-mc-account-card.js
+++ b/js/src/components/google-mc-account-card/connected-google-mc-account-card.js
@@ -2,6 +2,7 @@
* External dependencies
*/
import { sprintf, __ } from '@wordpress/i18n';
+import { useState } from '@wordpress/element';
import { getSetting } from '@woocommerce/settings'; // eslint-disable-line import/no-unresolved
// The above is an unpublished package, delivered with WC, we use Dependency Extraction Webpack Plugin to import it.
// See https://github.com/woocommerce/woocommerce-admin/issues/7781
@@ -13,10 +14,17 @@ import AccountCard, { APPEARANCE } from '.~/components/account-card';
import AppButton from '.~/components/app-button';
import ConnectedIconLabel from '.~/components/connected-icon-label';
import Section from '.~/wcdl/section';
+import { GOOGLE_WPCOM_APP_CONNECTED_STATUS } from '.~/constants';
import { API_NAMESPACE } from '.~/data/constants';
import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices';
import useApiFetchCallback from '.~/hooks/useApiFetchCallback';
import { useAppDispatch } from '.~/data';
+import EnableNewProductSyncButton from '.~/components/enable-new-product-sync-button';
+import AppNotice from '.~/components/app-notice';
+import DisconnectModal, {
+ API_DATA_FETCH_FEATURE,
+} from '.~/settings/disconnect-modal';
+import { getSettingsUrl } from '.~/utils/urls';
/**
* Clicking on the "connect to a different Google Merchant Center account" button.
@@ -32,10 +40,12 @@ import { useAppDispatch } from '.~/data';
* @param {Object} props React props.
* @param {{ id: number }} props.googleMCAccount A data payload object containing the user's Google Merchant Center account ID.
* @param {boolean} [props.hideAccountSwitch=false] Indicate whether hide the account switch block at the card footer.
+ * @param {boolean} [props.hideNotificationService=true] Indicate whether hide the enable Notification service block at the card footer.
*/
const ConnectedGoogleMCAccountCard = ( {
googleMCAccount,
hideAccountSwitch = false,
+ hideNotificationService = false,
} ) => {
const { createNotice, removeNotice } = useDispatchCoreNotices();
const { invalidateResolution } = useAppDispatch();
@@ -46,6 +56,22 @@ const ConnectedGoogleMCAccountCard = ( {
method: 'DELETE',
} );
+ const [
+ fetchDisableNotifications,
+ { loading: loadingDisableNotifications },
+ ] = useApiFetchCallback( {
+ path: `${ API_NAMESPACE }/rest-api/authorize`,
+ method: 'DELETE',
+ } );
+
+ /**
+ * Temporary code for disabling the API PULL Beta Feature from the GMC Card
+ */
+ const [ openedModal, setOpenedModal ] = useState( null );
+ const dismissModal = () => setOpenedModal( null );
+ const openDisableDataFetchModal = () =>
+ setOpenedModal( API_DATA_FETCH_FEATURE );
+
const domain = new URL( getSetting( 'homeUrl' ) ).host;
/**
@@ -85,6 +111,47 @@ const ConnectedGoogleMCAccountCard = ( {
removeNotice( notice.id );
};
+ const disableNotifications = async () => {
+ const { notice } = await createNotice(
+ 'info',
+ __(
+ 'Disabling the new Product Sync feature, please wait…',
+ 'google-listings-and-ads'
+ )
+ );
+
+ try {
+ await fetchDisableNotifications();
+ invalidateResolution( 'getGoogleMCAccount', [] );
+ } catch ( error ) {
+ createNotice(
+ 'error',
+ __(
+ 'Unable to disable new product sync. Please try again later.',
+ 'google-listings-and-ads'
+ )
+ );
+ }
+
+ removeNotice( notice.id );
+ };
+
+ // Show the button if the status is "approved" and the Notification Service is not hidden.
+ const showDisconnectNotificationsButton =
+ ! hideNotificationService &&
+ googleMCAccount.wpcom_rest_api_status ===
+ GOOGLE_WPCOM_APP_CONNECTED_STATUS.APPROVED;
+
+ // Show the error if the status is set but is not "approved" and the Notification Service is not hidden.
+ const showErrorNotificationsNotice =
+ ! hideNotificationService &&
+ googleMCAccount.wpcom_rest_api_status &&
+ googleMCAccount.notification_service_enabled &&
+ googleMCAccount.wpcom_rest_api_status !==
+ GOOGLE_WPCOM_APP_CONNECTED_STATUS.APPROVED;
+
+ const showFooter = ! hideAccountSwitch || showDisconnectNotificationsButton;
+
return (
}
+ indicator={
+ showErrorNotificationsNotice ? (
+
+ ) : (
+
+ )
+ }
>
- { ! hideAccountSwitch && (
+ { showDisconnectNotificationsButton && (
+
+ { __(
+ 'Google has been granted access to fetch your product data.',
+ 'google-listings-and-ads'
+ ) }
+
+ ) }
+
+ { showErrorNotificationsNotice && (
+
+ { __(
+ 'There was an issue granting access to Google for fetching your products.',
+ 'google-listings-and-ads'
+ ) }
+
+ ) }
+
+ { openedModal && (
+ {
+ window.location.href = getSettingsUrl();
+ } }
+ disconnectTarget={ openedModal }
+ disconnectAction={ disableNotifications }
+ />
+ ) }
+
+ { showFooter && (
-
+ { ! hideAccountSwitch && (
+
+ ) }
+ { showDisconnectNotificationsButton && (
+
+ ) }
) }
diff --git a/js/src/components/google-mc-account-card/google-mc-account-card.js b/js/src/components/google-mc-account-card/google-mc-account-card.js
index 227d8e7306..69429a4db0 100644
--- a/js/src/components/google-mc-account-card/google-mc-account-card.js
+++ b/js/src/components/google-mc-account-card/google-mc-account-card.js
@@ -27,7 +27,12 @@ const GoogleMCAccountCard = () => {
return ;
}
- return ;
+ return (
+
+ );
};
export default GoogleMCAccountCard;
diff --git a/js/src/dashboard/all-programs-table-card/campaign-assets-tour.js b/js/src/components/tours/campaign-assets-tour.js
similarity index 100%
rename from js/src/dashboard/all-programs-table-card/campaign-assets-tour.js
rename to js/src/components/tours/campaign-assets-tour.js
diff --git a/js/src/dashboard/all-programs-table-card/campaign-assets-tour.scss b/js/src/components/tours/campaign-assets-tour.scss
similarity index 100%
rename from js/src/dashboard/all-programs-table-card/campaign-assets-tour.scss
rename to js/src/components/tours/campaign-assets-tour.scss
diff --git a/js/src/components/tours/rebrading-tour.scss b/js/src/components/tours/rebrading-tour.scss
new file mode 100644
index 0000000000..f19bd4072f
--- /dev/null
+++ b/js/src/components/tours/rebrading-tour.scss
@@ -0,0 +1,18 @@
+.gla-rebranding-tour {
+ &__heading {
+ display: flex;
+ align-items: center;
+ gap: $grid-unit-10;
+ fill: var(--wp-admin-theme-color);
+ }
+
+ // Hide the footer content but leave the padding-bottom because
+ // TourKit doesn't offer a way not to use the primary button.
+ .components-card__footer {
+ padding-top: 0;
+
+ > * {
+ display: none;
+ }
+ }
+}
diff --git a/js/src/components/tours/rebranding-tour.js b/js/src/components/tours/rebranding-tour.js
new file mode 100644
index 0000000000..c67bdca96e
--- /dev/null
+++ b/js/src/components/tours/rebranding-tour.js
@@ -0,0 +1,67 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { createInterpolateElement } from '@wordpress/element';
+import { TourKit } from '@woocommerce/components';
+
+/**
+ * Internal dependencies
+ */
+import useTour from '.~/hooks/useTour';
+import './rebrading-tour.scss';
+
+const TOUR_ID = 'rebranding-tour';
+
+/**
+ * Renders the tour for notifying about the new extension rebranding
+ */
+export default function RebrandingTour() {
+ const { tourChecked, setTourChecked } = useTour( TOUR_ID );
+
+ if ( tourChecked ) {
+ return null;
+ }
+
+ const config = {
+ steps: [
+ {
+ referenceElements: {
+ desktop:
+ '.toplevel_page_woocommerce-marketing a[href*="google"]',
+ },
+ meta: {
+ heading: (
+
+ { __(
+ 'New name, same great solution',
+ 'google-listings-and-ads'
+ ) }
+
+ ),
+ descriptions: {
+ desktop: (
+ <>
+ { createInterpolateElement(
+ __(
+ 'Google Listings & Ads is now Google for WooCommerce.',
+ 'google-listings-and-ads'
+ ),
+ { strong: }
+ ) }
+ >
+ ),
+ },
+ },
+ },
+ ],
+ options: {
+ classNames: 'gla-admin-page,gla-rebranding-tour',
+ effects: { overlay: false },
+ },
+ placement: 'top',
+ closeHandler: () => setTourChecked( true ),
+ };
+
+ return ;
+}
diff --git a/js/src/constants.js b/js/src/constants.js
index 0cce0f2c3d..de28ac0544 100644
--- a/js/src/constants.js
+++ b/js/src/constants.js
@@ -111,3 +111,10 @@ export const ASSET_FORM_KEY = {
...ASSET_KEY,
...ASSET_GROUP_KEY,
};
+
+export const GOOGLE_WPCOM_APP_CONNECTED_STATUS = {
+ APPROVED: 'approved',
+ DISAPPROVED: 'disapproved',
+ ERROR: 'error',
+ DISABLED: 'disabled',
+};
diff --git a/js/src/dashboard/all-programs-table-card/index.js b/js/src/dashboard/all-programs-table-card/index.js
index 786c32f078..52f1f17eb5 100644
--- a/js/src/dashboard/all-programs-table-card/index.js
+++ b/js/src/dashboard/all-programs-table-card/index.js
@@ -21,7 +21,7 @@ import { FREE_LISTINGS_PROGRAM_ID, CAMPAIGN_TYPE_PMAX } from '.~/constants';
import AddPaidCampaignButton from '.~/components/paid-ads/add-paid-campaign-button';
import ProgramToggle from './program-toggle';
import FreeListingsDisabledToggle from './free-listings-disabled-toggle';
-import CampaignAssetsTour from './campaign-assets-tour';
+import CampaignAssetsTour from '.~/components/tours/campaign-assets-tour';
const PROGRAMS_TABLE_CARD_CLASS_NAME = 'gla-all-programs-table-card';
const CAMPAIGN_EDIT_BUTTON_CLASS_NAME = 'gla-campaign-edit-button';
diff --git a/js/src/dashboard/all-programs-table-card/index.test.js b/js/src/dashboard/all-programs-table-card/index.test.js
index 4030990271..ac57aed8f8 100644
--- a/js/src/dashboard/all-programs-table-card/index.test.js
+++ b/js/src/dashboard/all-programs-table-card/index.test.js
@@ -8,11 +8,11 @@ import { screen, within, render } from '@testing-library/react';
* Internal dependencies
*/
import AllProgramsTableCard from './';
-import CampaignAssetsTour from './campaign-assets-tour';
+import CampaignAssetsTour from '.~/components/tours/campaign-assets-tour';
import useAdsCampaigns from '.~/hooks/useAdsCampaigns';
import useAdsCurrency from '.~/hooks/useAdsCurrency';
-jest.mock( './campaign-assets-tour', () =>
+jest.mock( '.~/components/tours/campaign-assets-tour', () =>
jest
.fn()
.mockReturnValue( )
diff --git a/js/src/dashboard/index.js b/js/src/dashboard/index.js
index e7a18cd2fd..67ac1bca35 100644
--- a/js/src/dashboard/index.js
+++ b/js/src/dashboard/index.js
@@ -25,6 +25,7 @@ import EditPaidAdsCampaign from '.~/pages/edit-paid-ads-campaign';
import CreatePaidAdsCampaign from '.~/pages/create-paid-ads-campaign';
import { CTA_CREATE_ANOTHER_CAMPAIGN, CTA_CONFIRM } from './constants';
import { recordGlaEvent } from '.~/utils/tracks';
+import RebrandingTour from '.~/components/tours/rebranding-tour';
import './index.scss';
/**
@@ -88,6 +89,7 @@ const Dashboard = () => {
+
+ jest.fn().mockReturnValue( null ).mockName( 'RebrandingTour' )
+);
+
/**
* External dependencies
*/
@@ -11,6 +15,7 @@ import { getQuery } from '@woocommerce/navigation';
*/
import Dashboard from './index';
import isWCTracksEnabled from '.~/utils/isWCTracksEnabled';
+import RebrandingTour from '.~/components/tours/rebranding-tour';
import { GUIDE_NAMES } from '.~/constants';
jest.mock( '@woocommerce/settings', () => {
@@ -137,4 +142,36 @@ describe( 'Dashboard', () => {
).not.toBeInTheDocument();
} );
} );
+
+ describe( 'Rebranding Tour', () => {
+ beforeAll( () => {
+ getQuery.mockImplementation( () => {
+ return {};
+ } );
+ } );
+
+ afterAll( () => {
+ getQuery.mockReset();
+ } );
+
+ test( 'Not rendered in UI', () => {
+ RebrandingTour.mockImplementation( () => {
+ return null;
+ } );
+
+ render( );
+ const tour = screen.queryByRole( 'dialog', { name: 'tour' } );
+ expect( tour ).not.toBeInTheDocument();
+ } );
+
+ test( 'Rendered in UI', () => {
+ RebrandingTour.mockImplementation( () => {
+ return ;
+ } );
+
+ render( );
+ const tour = screen.queryByRole( 'dialog', { name: 'tour' } );
+ expect( tour ).toBeInTheDocument();
+ } );
+ } );
} );
diff --git a/js/src/get-started-page/benefits-card/index.js b/js/src/get-started-page/benefits-card/index.js
index b3fc6df2c9..0d80858dd8 100644
--- a/js/src/get-started-page/benefits-card/index.js
+++ b/js/src/get-started-page/benefits-card/index.js
@@ -19,7 +19,7 @@ const BenefitsCard = () => {
{ __(
- 'The Google Listings and Ads plugin allows you to upload your store and product data to Google. Your products will sync automatically to make relevant information available for free listings, Google Ads, and other Google services. You can create a new Merchant Center account or link an existing account to connect your store and list products across Google.',
+ 'The Google for WooCommerce plugin allows you to upload your store and product data to Google. Your products will sync automatically to make relevant information available for free listings, Google Ads, and other Google services. You can create a new Merchant Center account or link an existing account to connect your store and list products across Google.',
'google-listings-and-ads'
) }
@@ -220,7 +220,7 @@ const faqItems = [
<>
{ __(
- 'Create a new Google Ads account through Google Listings & Ads and a promotional code will be automatically applied to your account. You’ll have 60 days to spend $500 to qualify for the $500 ads credit.',
+ 'Create a new Google Ads account through Google for WooCommerce and a promotional code will be automatically applied to your account. You’ll have 60 days to spend $500 to qualify for the $500 ads credit.',
'google-listings-and-ads'
) }
diff --git a/js/src/get-started-page/features-card/index.js b/js/src/get-started-page/features-card/index.js
index 0e21a3035c..00c5a6cd71 100644
--- a/js/src/get-started-page/features-card/index.js
+++ b/js/src/get-started-page/features-card/index.js
@@ -60,7 +60,7 @@ const FeaturesCard = () => {
variant="body"
>
{ __(
- 'With Google Listings & Ads, connect with the right shoppers at the right moment when they’re looking to buy products like yours.',
+ 'With Google for WooCommerce, connect with the right shoppers at the right moment when they’re looking to buy products like yours.',
'google-listings-and-ads'
) }
diff --git a/js/src/get-started-page/get-started-card/index.js b/js/src/get-started-page/get-started-card/index.js
index 0bf79bdf2c..3dcc5e976a 100644
--- a/js/src/get-started-page/get-started-card/index.js
+++ b/js/src/get-started-page/get-started-card/index.js
@@ -42,7 +42,7 @@ const GetStartedCard = () => {
className="gla-get-started-card__title"
>
{ __(
- 'Get your products in front of more shoppers with Google Listings & Ads',
+ 'Get your products in front of more shoppers with Google for WooCommerce',
'google-listings-and-ads'
) }
diff --git a/js/src/get-started-page/unsupported-notices/index.js b/js/src/get-started-page/unsupported-notices/index.js
index ede58e5141..ea8ce75125 100644
--- a/js/src/get-started-page/unsupported-notices/index.js
+++ b/js/src/get-started-page/unsupported-notices/index.js
@@ -42,7 +42,7 @@ const UnsupportedLanguage = () => {
>
{ createInterpolateElement(
__(
- 'Your site language is . This language is currently not supported by Google Listings & Ads. You can change your site language here. Read more about supported languages',
+ 'Your site language is . This language is currently not supported by Google for WooCommerce. You can change your site language here. Read more about supported languages',
'google-listings-and-ads'
),
{
@@ -85,7 +85,7 @@ const UnsupportedCountry = () => {
>
{ createInterpolateElement(
__(
- 'Your store’s country is . This country is currently not supported by Google Listings & Ads. However, you can still choose to list your products in another supported country, if you are able to sell your products to customers there. Change your store’s country here. Read more about supported countries',
+ 'Your store’s country is . This country is currently not supported by Google for WooCommerce. However, you can still choose to list your products in another supported country, if you are able to sell your products to customers there. Change your store’s country here. Read more about supported countries',
'google-listings-and-ads'
),
{
diff --git a/js/src/hooks/useUpdateRestAPIAuthorizeStatusByUrlQuery.js b/js/src/hooks/useUpdateRestAPIAuthorizeStatusByUrlQuery.js
new file mode 100644
index 0000000000..f9aa20aef9
--- /dev/null
+++ b/js/src/hooks/useUpdateRestAPIAuthorizeStatusByUrlQuery.js
@@ -0,0 +1,71 @@
+/**
+ * External dependencies
+ */
+import { useCallback, useEffect } from '@wordpress/element';
+import { getQuery } from '@woocommerce/navigation';
+
+/**
+ * Internal dependencies
+ */
+import { GOOGLE_WPCOM_APP_CONNECTED_STATUS } from '.~/constants';
+import { useAppDispatch } from '.~/data';
+import { API_NAMESPACE } from '.~/data/constants';
+import useApiFetchCallback from '.~/hooks/useApiFetchCallback';
+
+/**
+ * A hook that calls an API to update Google WPCOM app connected status.
+ *
+ * At the end of the authorization for granting access to Google WPCOM app, Google will
+ * redirect back to the merchant site, either the settings page or onboarding setup account page.
+ * They will add a query param `google_wpcom_app_status` to the URL, we will store this status to
+ * the DB by calling an API `PUT /wc/gla/rest-api/authorize`.
+ */
+const useUpdateRestAPIAuthorizeStatusByUrlQuery = () => {
+ const { google_wpcom_app_status: googleWPCOMAppStatus, nonce } = getQuery();
+ const { invalidateResolution } = useAppDispatch();
+
+ const path = `${ API_NAMESPACE }/rest-api/authorize`;
+ const [ fetchUpdateRestAPIAuthorize ] = useApiFetchCallback( {
+ path,
+ method: 'PUT',
+ } );
+
+ const handleUpdateRestAPIAuthorize = useCallback( async () => {
+ try {
+ await fetchUpdateRestAPIAuthorize( {
+ data: {
+ status: googleWPCOMAppStatus,
+ nonce,
+ },
+ } );
+
+ // Refetch Google MC account so we can get the latest gla_wpcom_rest_api_status.
+ invalidateResolution( 'getGoogleMCAccount', [] );
+ } catch ( e ) {
+ // Only show in the console when failed to update rest API authorize status
+ // since the user doesn't need to know about it.
+ // eslint-disable-next-line no-console
+ console.error( e.message );
+ }
+ }, [
+ fetchUpdateRestAPIAuthorize,
+ googleWPCOMAppStatus,
+ invalidateResolution,
+ nonce,
+ ] );
+
+ useEffect( () => {
+ async function updateStatus() {
+ await handleUpdateRestAPIAuthorize( googleWPCOMAppStatus );
+ }
+ if (
+ Object.values( GOOGLE_WPCOM_APP_CONNECTED_STATUS ).includes(
+ googleWPCOMAppStatus
+ )
+ ) {
+ updateStatus();
+ }
+ }, [ googleWPCOMAppStatus, handleUpdateRestAPIAuthorize ] );
+};
+
+export default useUpdateRestAPIAuthorizeStatusByUrlQuery;
diff --git a/js/src/hooks/useUpdateRestAPIAuthorizeStatusByUrlQuery.test.js b/js/src/hooks/useUpdateRestAPIAuthorizeStatusByUrlQuery.test.js
new file mode 100644
index 0000000000..a73ebbb810
--- /dev/null
+++ b/js/src/hooks/useUpdateRestAPIAuthorizeStatusByUrlQuery.test.js
@@ -0,0 +1,121 @@
+/**
+ * External dependencies
+ */
+import { renderHook } from '@testing-library/react-hooks';
+import { getQuery } from '@woocommerce/navigation';
+
+/**
+ * Internal dependencies
+ */
+import useUpdateRestAPIAuthorizeStatusByUrlQuery from './useUpdateRestAPIAuthorizeStatusByUrlQuery';
+import useApiFetchCallback from '.~/hooks/useApiFetchCallback';
+
+jest.mock( '@woocommerce/navigation' );
+jest.mock( '.~/hooks/useApiFetchCallback' );
+
+describe( 'useUpdateRestAPIAuthorizeStatusByUrlQuery', () => {
+ let fetchUpdateRestAPIAuthorize;
+ let consoleErrorSpy;
+
+ beforeEach( () => {
+ fetchUpdateRestAPIAuthorize = jest.fn();
+ useApiFetchCallback.mockImplementation( () => [
+ fetchUpdateRestAPIAuthorize,
+ ] );
+ consoleErrorSpy = jest.spyOn( global.console, 'error' );
+ consoleErrorSpy.mockImplementation( jest.fn() );
+ } );
+
+ afterEach( () => {
+ consoleErrorSpy.mockRestore();
+ fetchUpdateRestAPIAuthorize.mockRestore();
+ } );
+
+ it( 'should call fetchUpdateRestAPIAuthorize', () => {
+ getQuery.mockReturnValue( {
+ google_wpcom_app_status: 'approved',
+ nonce: 'nonce-123',
+ } );
+ renderHook( () => useUpdateRestAPIAuthorizeStatusByUrlQuery() );
+ expect( fetchUpdateRestAPIAuthorize ).toHaveBeenCalledTimes( 1 );
+ expect( fetchUpdateRestAPIAuthorize ).toHaveBeenCalledWith( {
+ data: {
+ status: 'approved',
+ nonce: 'nonce-123',
+ },
+ } );
+ } );
+
+ it( 'should print console error if fetchUpdateRestAPIAuthorize throws error', () => {
+ fetchUpdateRestAPIAuthorize.mockImplementation( () => {
+ throw new Error( 'Nonces mismatch' );
+ } );
+ getQuery.mockReturnValue( {
+ google_wpcom_app_status: 'approved',
+ nonce: 'nonce-123',
+ } );
+ renderHook( () => useUpdateRestAPIAuthorizeStatusByUrlQuery() );
+ expect( consoleErrorSpy ).toHaveBeenCalledWith( 'Nonces mismatch' );
+ expect( fetchUpdateRestAPIAuthorize ).toHaveBeenCalledTimes( 1 );
+ expect( fetchUpdateRestAPIAuthorize ).toHaveBeenCalledWith( {
+ data: {
+ status: 'approved',
+ nonce: 'nonce-123',
+ },
+ } );
+ } );
+
+ it( 'should call fetchUpdateRestAPIAuthorize if query param is approved', () => {
+ getQuery.mockReturnValue( {
+ google_wpcom_app_status: 'approved',
+ nonce: 'nonce-123',
+ } );
+ renderHook( () => useUpdateRestAPIAuthorizeStatusByUrlQuery() );
+ expect( fetchUpdateRestAPIAuthorize ).toHaveBeenCalledTimes( 1 );
+ expect( fetchUpdateRestAPIAuthorize ).toHaveBeenCalledWith( {
+ data: {
+ status: 'approved',
+ nonce: 'nonce-123',
+ },
+ } );
+ } );
+
+ it( 'should call fetchUpdateRestAPIAuthorize if query param is disapproved', () => {
+ getQuery.mockReturnValue( {
+ google_wpcom_app_status: 'disapproved',
+ nonce: 'nonce-123',
+ } );
+ renderHook( () => useUpdateRestAPIAuthorizeStatusByUrlQuery() );
+ expect( fetchUpdateRestAPIAuthorize ).toHaveBeenCalledTimes( 1 );
+ expect( fetchUpdateRestAPIAuthorize ).toHaveBeenCalledWith( {
+ data: {
+ status: 'disapproved',
+ nonce: 'nonce-123',
+ },
+ } );
+ } );
+
+ it( 'should call fetchUpdateRestAPIAuthorize if query param is error', () => {
+ getQuery.mockReturnValue( {
+ google_wpcom_app_status: 'error',
+ nonce: 'nonce-123',
+ } );
+ renderHook( () => useUpdateRestAPIAuthorizeStatusByUrlQuery() );
+ expect( fetchUpdateRestAPIAuthorize ).toHaveBeenCalledTimes( 1 );
+ expect( fetchUpdateRestAPIAuthorize ).toHaveBeenCalledWith( {
+ data: {
+ status: 'error',
+ nonce: 'nonce-123',
+ },
+ } );
+ } );
+
+ it( 'should not call fetchUpdateRestAPIAuthorize if query param is unknown', () => {
+ getQuery.mockReturnValue( {
+ google_wpcom_app_status: 'does-not-exist',
+ nonce: 'nonce-123',
+ } );
+ renderHook( () => useUpdateRestAPIAuthorizeStatusByUrlQuery() );
+ expect( fetchUpdateRestAPIAuthorize ).toHaveBeenCalledTimes( 0 );
+ } );
+} );
diff --git a/js/src/index.js b/js/src/index.js
index f8aef20c48..60a74bf5c2 100644
--- a/js/src/index.js
+++ b/js/src/index.js
@@ -75,7 +75,7 @@ addFilter(
}
initialBreadcrumbs.push(
- __( 'Google Listings & Ads', 'google-listings-and-ads' )
+ __( 'Google for WooCommerce', 'google-listings-and-ads' )
);
const pluginAdminPages = [
diff --git a/js/src/index.test.js b/js/src/index.test.js
index b76f6c928d..dcc5480364 100644
--- a/js/src/index.test.js
+++ b/js/src/index.test.js
@@ -50,7 +50,7 @@ describe( 'index', () => {
breadcrumbs: expect.arrayContaining( [
[ '', 'WooCommerce' ],
[ '/marketing', 'Marketing' ],
- 'Google Listings & Ads',
+ 'Google for WooCommerce',
expect.any( String ),
] ),
container: expect.any( Function ),
diff --git a/js/src/product-feed/index.js b/js/src/product-feed/index.js
index cd0af7c0e7..142c29ad9a 100644
--- a/js/src/product-feed/index.js
+++ b/js/src/product-feed/index.js
@@ -18,6 +18,7 @@ import './index.scss';
import { GUIDE_NAMES, LOCAL_STORAGE_KEYS } from '.~/constants';
import localStorage from '.~/utils/localStorage';
import isWCTracksEnabled from '.~/utils/isWCTracksEnabled';
+import RebrandingTour from '.~/components/tours/rebranding-tour';
const ProductFeed = () => {
const [ canCESPromptOpen, setCESPromptOpen ] = useState( false );
@@ -47,15 +48,16 @@ const ProductFeed = () => {
return (
<>
+
{ isSubmissionSuccessOpen && }
{ canCESPromptOpen && (
+ jest.fn().mockReturnValue( null ).mockName( 'RebrandingTour' )
+);
+
/**
* External dependencies
*/
import '@testing-library/jest-dom';
-import { render } from '@testing-library/react';
+import { render, screen } from '@testing-library/react';
import { getQuery } from '@woocommerce/navigation';
/**
@@ -12,6 +16,7 @@ import ProductFeed from './index';
import localStorage from '.~/utils/localStorage';
import isWCTracksEnabled from '.~/utils/isWCTracksEnabled';
import { GUIDE_NAMES } from '.~/constants';
+import RebrandingTour from '.~/components/tours/rebranding-tour';
jest.mock( '@woocommerce/navigation', () => {
return {
@@ -30,8 +35,8 @@ jest.mock( '.~/utils/localStorage', () => {
jest.mock( '.~/utils/isWCTracksEnabled', () => jest.fn() );
const SUBMISSION_SUCCESS_GUIDE_TEXT =
- 'You’ve successfully set up Google Listings & Ads! 🎉';
-const CES_PROMPT_TEXT = 'How easy was it to set up Google Listings & Ads?';
+ 'You’ve successfully set up Google for WooCommerce! 🎉';
+const CES_PROMPT_TEXT = 'How easy was it to set up Google for WooCommerce?';
jest.mock( '.~/components/customer-effort-score-prompt', () => () => (
{ CES_PROMPT_TEXT }
@@ -177,4 +182,26 @@ describe( 'ProductFeed', () => {
} );
} );
} );
+
+ describe( 'Rebranding Tour', () => {
+ test( 'Not rendered in UI', () => {
+ RebrandingTour.mockImplementation( () => {
+ return null;
+ } );
+
+ render( );
+ const tour = screen.queryByRole( 'dialog', { name: 'tour' } );
+ expect( tour ).not.toBeInTheDocument();
+ } );
+
+ test( 'Rendered in UI', () => {
+ RebrandingTour.mockImplementation( () => {
+ return ;
+ } );
+
+ render( );
+ const tour = screen.queryByRole( 'dialog', { name: 'tour' } );
+ expect( tour ).toBeInTheDocument();
+ } );
+ } );
} );
diff --git a/js/src/product-feed/submission-success-guide/index.js b/js/src/product-feed/submission-success-guide/index.js
index b462cc9941..968124ecec 100644
--- a/js/src/product-feed/submission-success-guide/index.js
+++ b/js/src/product-feed/submission-success-guide/index.js
@@ -56,7 +56,7 @@ const pages = [
content: (
diff --git a/js/src/reports/products/index.js b/js/src/reports/products/index.js
index 537ec97906..c29fd7c549 100644
--- a/js/src/reports/products/index.js
+++ b/js/src/reports/products/index.js
@@ -24,6 +24,7 @@ import SummarySection from '../summary-section';
import ChartSection from '../chart-section';
import CompareProductsTableCard from './compare-products-table-card';
import ReportsNavigation from '../reports-navigation';
+import RebrandingTour from '.~/components/tours/rebranding-tour';
/**
* Available metrics and their human-readable labels.
@@ -123,6 +124,7 @@ const ProductsReportPage = () => {
<>
+
{ loaded ? (
diff --git a/js/src/reports/programs/index.js b/js/src/reports/programs/index.js
index 77a362d0f5..f21e4f1825 100644
--- a/js/src/reports/programs/index.js
+++ b/js/src/reports/programs/index.js
@@ -17,6 +17,7 @@ import SummarySection from '../summary-section';
import ChartSection from '../chart-section';
import CompareProgramsTableCard from './compare-programs-table-card';
import ReportsNavigation from '../reports-navigation';
+import RebrandingTour from '.~/components/tours/rebranding-tour';
/**
* Available metrics and their human-readable labels.
@@ -103,6 +104,7 @@ const ProgramsReport = () => {
<>
+
{
- const disconnect =
+ let disconnect =
disconnectTarget === ALL_ACCOUNTS
? dispatcher.disconnectAllAccounts
: dispatcher.disconnectGoogleAdsAccount;
+ if ( disconnectAction ) {
+ disconnect = disconnectAction;
+ }
+
setDisconnecting( true );
disconnect()
.then( () => {
diff --git a/js/src/settings/disconnect-modal/constants.js b/js/src/settings/disconnect-modal/constants.js
index 31c4b3bad1..c6003b98ad 100644
--- a/js/src/settings/disconnect-modal/constants.js
+++ b/js/src/settings/disconnect-modal/constants.js
@@ -1,2 +1,3 @@
export const ALL_ACCOUNTS = 'all-accounts';
export const ADS_ACCOUNT = 'ads-account';
+export const API_DATA_FETCH_FEATURE = 'api-data-fetch-feature';
diff --git a/js/src/settings/index.js b/js/src/settings/index.js
index 4189ca0059..3986b238bd 100644
--- a/js/src/settings/index.js
+++ b/js/src/settings/index.js
@@ -10,6 +10,7 @@ import { getQuery, getHistory } from '@woocommerce/navigation';
import { API_RESPONSE_CODES } from '.~/constants';
import useLegacyMenuEffect from '.~/hooks/useLegacyMenuEffect';
import useGoogleAccount from '.~/hooks/useGoogleAccount';
+import useUpdateRestAPIAuthorizeStatusByUrlQuery from '.~/hooks/useUpdateRestAPIAuthorizeStatusByUrlQuery';
import { subpaths, getReconnectAccountUrl } from '.~/utils/urls';
import { ContactInformationPreview } from '.~/components/contact-information';
import LinkedAccounts from './linked-accounts';
@@ -17,7 +18,9 @@ import ReconnectWPComAccount from './reconnect-wpcom-account';
import ReconnectGoogleAccount from './reconnect-google-account';
import EditStoreAddress from './edit-store-address';
import EditPhoneNumber from './edit-phone-number';
+import EnableNewProductSyncNotice from '.~/components/enable-new-product-sync-notice';
import NavigationClassic from '.~/components/navigation-classic';
+import RebrandingTour from '.~/components/tours/rebranding-tour';
import './index.scss';
const pageClassName = 'gla-settings';
@@ -27,6 +30,8 @@ const Settings = () => {
// Make the component highlight GLA entry in the WC legacy menu.
useLegacyMenuEffect();
+ useUpdateRestAPIAuthorizeStatusByUrlQuery();
+
const { google } = useGoogleAccount();
const isReconnectGooglePage = subpath === subpaths.reconnectGoogleAccount;
@@ -40,7 +45,7 @@ const Settings = () => {
}
}, [ isReconnectGooglePage, google ] );
- // Navigate to subpath is any.
+ // Navigate to subpath if any.
switch ( subpath ) {
case subpaths.reconnectWPComAccount:
return (
@@ -59,7 +64,9 @@ const Settings = () => {
return (
+
+
diff --git a/js/src/setup-mc/setup-stepper/setup-accounts/index.js b/js/src/setup-mc/setup-stepper/setup-accounts/index.js
index 9802c4c51f..a318189cbb 100644
--- a/js/src/setup-mc/setup-stepper/setup-accounts/index.js
+++ b/js/src/setup-mc/setup-stepper/setup-accounts/index.js
@@ -142,7 +142,7 @@ const SetupAccounts = ( props ) => {
'google-listings-and-ads'
) }
description={ __(
- 'Connect the accounts required to use Google Listings & Ads.',
+ 'Connect the accounts required to use Google for WooCommerce.',
'google-listings-and-ads'
) }
/>
@@ -150,7 +150,7 @@ const SetupAccounts = ( props ) => {
className="gla-wp-google-accounts-section"
title={ __( 'Connect accounts', 'google-listings-and-ads' ) }
description={ __(
- 'The following accounts are required to use the Google Listings & Ads plugin.',
+ 'The following accounts are required to use the Google for WooCommerce plugin.',
'google-listings-and-ads'
) }
>
diff --git a/js/src/setup-mc/top-bar/index.js b/js/src/setup-mc/top-bar/index.js
index 767f2873df..e30b4c6c07 100644
--- a/js/src/setup-mc/top-bar/index.js
+++ b/js/src/setup-mc/top-bar/index.js
@@ -25,7 +25,7 @@ const SetupMCTopBar = () => {
return (
}
diff --git a/package-lock.json b/package-lock.json
index 8da0547500..afc2dd60fb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "google-listings-and-ads",
- "version": "2.7.7",
+ "version": "2.8.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "google-listings-and-ads",
- "version": "2.7.2",
+ "version": "2.7.7",
"license": "GPL-3.0-or-later",
"dependencies": {
"@woocommerce/components": "^10.3.0",
@@ -25584,9 +25584,9 @@
}
},
"node_modules/socket.io-parser": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.3.tgz",
- "integrity": "sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg==",
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.4.tgz",
+ "integrity": "sha512-z/pFQB3x+EZldRRzORYW1vwVO8m/3ILkswtnpoeU6Ve3cbMWkmHEWDAVJn4QJtchiiFTo5j7UG2QvwxvaA9vow==",
"dependencies": {
"component-emitter": "~1.3.0",
"debug": "~3.1.0",
@@ -47630,9 +47630,9 @@
}
},
"socket.io-parser": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.3.tgz",
- "integrity": "sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg==",
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.4.tgz",
+ "integrity": "sha512-z/pFQB3x+EZldRRzORYW1vwVO8m/3ILkswtnpoeU6Ve3cbMWkmHEWDAVJn4QJtchiiFTo5j7UG2QvwxvaA9vow==",
"requires": {
"component-emitter": "~1.3.0",
"debug": "4.3.1",
diff --git a/package.json b/package.json
index d5b358e09c..47d2e1b0d7 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
{
"name": "google-listings-and-ads",
- "title": "Google Listings and Ads",
- "version": "2.7.7",
- "description": "google-listings-and-ads",
+ "title": "Google for WooCommerce",
+ "version": "2.8.0",
+ "description": "Google for WooCommerce",
"author": "Automattic",
"license": "GPL-3.0-or-later",
"keywords": [
diff --git a/readme.txt b/readme.txt
index e3adba01b8..445dcdd876 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,11 +1,11 @@
-=== Google Listings & Ads ===
+=== Google for WooCommerce ===
Contributors: automattic, google, woocommerce
Tags: woocommerce, google, product feed, ads, listings
Requires at least: 5.9
Tested up to: 6.6
Requires PHP: 7.4
Requires PHP Architecture: 64 Bits
-Stable tag: 2.7.7
+Stable tag: 2.8.0
License: GPLv3
License URI: https://www.gnu.org/licenses/gpl-3.0.html
@@ -15,11 +15,11 @@ Native integration with Google that allows merchants to easily display their pro
https://www.youtube.com/watch?v=lYCx7ZqA1uo
-Google Listings & Ads makes it simple to showcase your products to shoppers across Google. Whether you’re brand new to digital advertising or a marketing expert, you can expand your reach and grow your business, for free and with ads.
+Google for WooCommerce makes it simple to showcase your products to shoppers across Google. Whether you’re brand new to digital advertising or a marketing expert, you can expand your reach and grow your business, for free and with ads.
Sync your store with Google to list products for free, run paid ads, and track performance straight from your store dashboard.
-With Google Listings & Ads:
+With Google for WooCommerce:
- **Connect your store seamlessly** with Google Merchant Center.
- **Reach online shoppers** with free listings.
- **Boost store traffic and sales** with Performance Max Campaigns.
@@ -48,7 +48,7 @@ Connect your Google Ads account, choose a budget, and launch your campaign strai
= Get $500 in Google Ads credit when you spend your first $500! =
-Create a new Google Ads account through Google Listings & Ads and a promotional code will be automatically applied to your account. You’ll have 60 days to spend $500 to qualify for the $500 ads credit. See full terms and conditions [here](https://www.google.com/ads/coupons/terms/).
+Create a new Google Ads account through Google for WooCommerce and a promotional code will be automatically applied to your account. You’ll have 60 days to spend $500 to qualify for the $500 ads credit. See full terms and conditions [here](https://www.google.com/ads/coupons/terms/).
== Installation ==
@@ -66,7 +66,7 @@ Visit the [WooCommerce server requirements documentation](https://woocommerce.co
Automatic installation is the easiest option as WordPress handles the file transfers itself and you don’t need to leave your web browser. To do an automatic install of this plugin, log in to your WordPress dashboard, navigate to the Plugins menu and click Add New.
-In the search field type “Google Listings and Ads” and click Search Plugins. Once you’ve found this plugin you can view details about it such as the point release, rating and description. Most importantly of course, you can install it by simply clicking “Install Now”.
+In the search field type “Google for WooCommerce” and click Search Plugins. Once you’ve found this plugin you can view details about it such as the point release, rating and description. Most importantly of course, you can install it by simply clicking “Install Now”.
= Manual installation =
@@ -74,7 +74,7 @@ The manual installation method involves downloading the plugin and uploading it
= Where can I report bugs or contribute to the project? =
-Bugs should be reported in the [Google Listings and Ads GitHub repository](https://github.com/woocommerce/google-listings-and-ads/).
+Bugs should be reported in the [Google for WooCommerce GitHub repository](https://github.com/woocommerce/google-listings-and-ads/).
= This is awesome! Can I contribute? =
@@ -87,7 +87,7 @@ Release and roadmap notes available on the [WooCommerce Developers Blog](https:/
= What is Google Merchant Center? =
The Google Merchant Center helps you sync your store and product data with Google and makes the information available for both free listings on the Shopping tab and Google Shopping Ads. That means everything about your stores and products is available to shoppers when they search on a Google property.
-= Which countries are available for Google Listings & Ads? =
+= Which countries are available for Google for WooCommerce? =
Learn more about supported countries for Google free listings [here](https://support.google.com/merchants/answer/10033607?hl=en).
Learn more about supported countries and currencies for Performance Max campaigns [here](https://support.google.com/merchants/answer/160637#countrytable).
@@ -98,19 +98,23 @@ If you’re selling in the US, then eligible free listings can appear in search
If you’re running a Performance Max campaign, your approved products can appear on Google Search, the Shopping tab, Gmail, Youtube and the Google Display Network.
= Will my deals and promotions display on Google? =
-To show your coupons and promotions on Google Shopping listings, make sure you’re using the latest version of Google Listings & Ads. When you create or update a coupon in your WordPress dashboard under Marketing > Coupons, you’ll see a Channel Visibility settings box on the right: select “Show coupon on Google” to enable. This is currently available in the US only.
+To show your coupons and promotions on Google Shopping listings, make sure you’re using the latest version of Google for WooCommerce. When you create or update a coupon in your WordPress dashboard under Marketing > Coupons, you’ll see a Channel Visibility settings box on the right: select “Show coupon on Google” to enable. This is currently available in the US only.
= What are Performance Max campaigns? =
Performance Max campaigns are Google Ads that combine Google’s machine learning with automated bidding and ad placements to maximize conversion value and strategically display your ads to people searching for products like yours, at your given budget. The best part? You only pay when people click on your ad.
= How much do Performance Max campaigns cost? =
-Performance Max campaigns are pay-per-click, meaning you only pay when someone clicks on your ads. You can customize your daily budget in Google Listings & Ads but we recommend starting off with the suggested minimum budget, and you can change this budget at any time.
+Performance Max campaigns are pay-per-click, meaning you only pay when someone clicks on your ads. You can customize your daily budget in Google for WooCommerce but we recommend starting off with the suggested minimum budget, and you can change this budget at any time.
= Can I run both free listings and Performance Max campaigns at the same time? =
Yes, you can run both at the same time, and we recommend it! In the US, advertisers running free listings and ads together have seen an average of over 50% increase in clicks and over 100% increase in impressions on both free listings and ads on the Shopping tab. Your store is automatically opted into free listings automatically and can choose to run a paid Performance Max campaign.
== Changelog ==
+= 2.8.0 - 2024-07-31 =
+* Add Google API Pull method.
+* Rebranding Google Listings and Ads with Google for WooCommerce.
+
= 2.7.7 - 2024-07-24 =
* Dev - Fix E2E tests failed with WC 9.1.
* Tweak - Make campaign preview card responsive.
@@ -120,7 +124,4 @@ Yes, you can run both at the same time, and we recommend it! In the US, advertis
* Tweak - WC 9.1 compatibility.
* Tweak - WP 6.6 compatibility.
-= 2.7.5 - 2024-06-26 =
-* Add - Add an query parameter `campaign=saved` to the dashboard URL after the campaign was created.
-
[See changelog for all versions](https://raw.githubusercontent.com/woocommerce/google-listings-and-ads/trunk/changelog.txt).
diff --git a/src/API/Google/AdsConversionAction.php b/src/API/Google/AdsConversionAction.php
index 1f85957c13..f822db5057 100644
--- a/src/API/Google/AdsConversionAction.php
+++ b/src/API/Google/AdsConversionAction.php
@@ -50,7 +50,7 @@ public function __construct( GoogleAdsClient $client ) {
}
/**
- * Create the 'Google Listings and Ads purchase action' conversion action.
+ * Create the 'Google for WooCommerce purchase action' conversion action.
*
* @return array An array with some conversion action details.
* @throws Exception If the conversion action can't be created or retrieved.
@@ -67,7 +67,7 @@ public function create_conversion_action(): array {
'woocommerce_gla_conversion_action_name',
sprintf(
/* translators: %1 is a random 4-digit string */
- __( '[%1$s] Google Listings and Ads purchase action', 'google-listings-and-ads' ),
+ __( '[%1$s] Google for WooCommerce purchase action', 'google-listings-and-ads' ),
$unique
)
),
diff --git a/src/API/Google/Middleware.php b/src/API/Google/Middleware.php
index 7a13625004..8c153e0a63 100644
--- a/src/API/Google/Middleware.php
+++ b/src/API/Google/Middleware.php
@@ -14,7 +14,9 @@
use Automattic\WooCommerce\GoogleListingsAndAds\Utility\DateTimeUtility;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\TosAccepted;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\GuzzleHttp\Client;
+use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Psr\Container\ContainerExceptionInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Psr\Container\ContainerInterface;
+use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Psr\Container\NotFoundExceptionInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Psr\Http\Client\ClientExceptionInterface;
use DateTime;
use Exception;
@@ -482,6 +484,19 @@ protected function get_manager_url( string $name = '' ): string {
return $name ? trailingslashit( $url ) . $name : $url;
}
+ /**
+ * Get the Google Shopping Data Integration auth endpoint URL
+ *
+ * @return string
+ */
+ public function get_sdi_auth_endpoint(): string {
+ return $this->container->get( 'connect_server_root' )
+ . 'google/google-sdi/v1/credentials/partners/WOO_COMMERCE/merchants/'
+ . $this->strip_url_protocol( $this->get_site_url() )
+ . '/oauth/redirect:generate'
+ . '?merchant_id=' . $this->options->get_merchant_id();
+ }
+
/**
* Generate a descriptive name for a new account.
* Use site name if available.
@@ -552,4 +567,45 @@ public function is_subaccount(): bool {
// since transients don't support booleans, we save them as 0/1 and do the conversion here
return boolval( $is_subaccount );
}
+
+ /**
+ * Performs a request to Google Shopping Data Integration (SDI) to get required information in order to form an auth URL.
+ *
+ * @return array An array with the JSON response from the WCS server.
+ * @throws NotFoundExceptionInterface When the container was not found.
+ * @throws ContainerExceptionInterface When an error happens while retrieving the container.
+ * @throws Exception When the response status is not successful.
+ * @see google-sdi in google/services inside WCS
+ */
+ public function get_sdi_auth_params() {
+ try {
+ /** @var Client $client */
+ $client = $this->container->get( Client::class );
+ $result = $client->get( $this->get_sdi_auth_endpoint() );
+ $response = json_decode( $result->getBody()->getContents(), true );
+
+ if ( 200 !== $result->getStatusCode() ) {
+ do_action(
+ 'woocommerce_gla_partner_app_auth_failure',
+ [
+ 'error' => 'response',
+ 'response' => $response,
+ ]
+ );
+ do_action( 'woocommerce_gla_guzzle_invalid_response', $response, __METHOD__ );
+ $error = $response['message'] ?? __( 'Invalid response authenticating partner app.', 'google-listings-and-ads' );
+ throw new Exception( $error, $result->getStatusCode() );
+ }
+
+ return $response;
+
+ } catch ( ClientExceptionInterface $e ) {
+ do_action( 'woocommerce_gla_guzzle_client_exception', $e, __METHOD__ );
+
+ throw new Exception(
+ $this->client_exception_message( $e, __( 'Error authenticating Google Partner APP.', 'google-listings-and-ads' ) ),
+ $e->getCode()
+ );
+ }
+ }
}
diff --git a/src/API/Site/Controllers/DisconnectController.php b/src/API/Site/Controllers/DisconnectController.php
index 8eb5d06658..c0d48efc60 100644
--- a/src/API/Site/Controllers/DisconnectController.php
+++ b/src/API/Site/Controllers/DisconnectController.php
@@ -46,6 +46,7 @@ protected function get_disconnect_callback(): callable {
'mc/connection',
'google/connect',
'jetpack/connect',
+ 'rest-api/authorize',
];
$errors = [];
diff --git a/src/API/Site/Controllers/RestAPI/AuthController.php b/src/API/Site/Controllers/RestAPI/AuthController.php
new file mode 100644
index 0000000000..38bb9eecec
--- /dev/null
+++ b/src/API/Site/Controllers/RestAPI/AuthController.php
@@ -0,0 +1,215 @@
+ '/google/setup-mc',
+ 'settings' => '/google/settings',
+ ];
+
+ /**
+ * AuthController constructor.
+ *
+ * @param RESTServer $server
+ * @param OAuthService $oauth_service
+ * @param AccountService $account_service
+ */
+ public function __construct( RESTServer $server, OAuthService $oauth_service, AccountService $account_service ) {
+ parent::__construct( $server );
+ $this->oauth_service = $oauth_service;
+ $this->account_service = $account_service;
+ }
+
+ /**
+ * Registers the routes for the objects of the controller.
+ */
+ public function register_routes() {
+ $this->register_route(
+ 'rest-api/authorize',
+ [
+ [
+ 'methods' => TransportMethods::READABLE,
+ 'callback' => $this->get_authorize_callback(),
+ 'permission_callback' => $this->get_permission_callback(),
+ 'args' => $this->get_auth_params(),
+ ],
+ [
+ 'methods' => TransportMethods::DELETABLE,
+ 'callback' => $this->delete_authorize_callback(),
+ 'permission_callback' => $this->get_permission_callback(),
+ ],
+ [
+ 'methods' => TransportMethods::EDITABLE,
+ 'callback' => $this->get_update_authorize_callback(),
+ 'permission_callback' => $this->get_permission_callback(),
+ 'args' => $this->get_update_authorize_params(),
+ ],
+ 'schema' => $this->get_api_response_schema_callback(),
+ ]
+ );
+ }
+
+ /**
+ * Get the callback function for the authorization request.
+ *
+ * @return callable
+ */
+ protected function get_authorize_callback(): callable {
+ return function ( Request $request ) {
+ try {
+ $next = $request->get_param( 'next_page_name' );
+ $path = self::NEXT_PATH_MAPPING[ $next ];
+ $auth_url = $this->oauth_service->get_auth_url( $path );
+
+ $response = [
+ 'auth_url' => $auth_url,
+ ];
+
+ return $this->prepare_item_for_response( $response, $request );
+ } catch ( Exception $e ) {
+ return $this->response_from_exception( $e );
+ }
+ };
+ }
+
+ /**
+ * Get the callback function for the delete authorization request.
+ *
+ * @return callable
+ */
+ protected function delete_authorize_callback(): callable {
+ return function ( Request $request ) {
+ try {
+ $this->oauth_service->revoke_wpcom_api_auth();
+ return $this->prepare_item_for_response( [], $request );
+ } catch ( Exception $e ) {
+ return $this->response_from_exception( $e );
+ }
+ };
+ }
+
+ /**
+ * Get the callback function for the update authorize request.
+ *
+ * @return callable
+ */
+ protected function get_update_authorize_callback(): callable {
+ return function ( Request $request ) {
+ try {
+ $this->account_service->update_wpcom_api_authorization( $request['status'], $request['nonce'] );
+ return [ 'status' => $request['status'] ];
+ } catch ( Exception $e ) {
+ return $this->response_from_exception( $e );
+ }
+ };
+ }
+
+ /**
+ * Get the query params for the authorize request.
+ *
+ * @return array
+ */
+ protected function get_auth_params(): array {
+ return [
+ 'next_page_name' => [
+ 'description' => __( 'Indicates the next page name mapped to the redirect URL when redirected back from Google WPCOM App authorization.', 'google-listings-and-ads' ),
+ 'type' => 'string',
+ 'default' => array_key_first( self::NEXT_PATH_MAPPING ),
+ 'enum' => array_keys( self::NEXT_PATH_MAPPING ),
+ 'validate_callback' => 'rest_validate_request_arg',
+ ],
+ ];
+ }
+
+ /**
+ * Get the query params for the update authorize request.
+ *
+ * @return array
+ */
+ protected function get_update_authorize_params(): array {
+ return [
+ 'status' => [
+ 'description' => __( 'The status of the merchant granting access to Google\'s WPCOM app', 'google-listings-and-ads' ),
+ 'type' => 'string',
+ 'enum' => OAuthService::ALLOWED_STATUSES,
+ 'validate_callback' => 'rest_validate_request_arg',
+ 'required' => true,
+ ],
+ 'nonce' => [
+ 'description' => __( 'The nonce provided by Google in the URL query parameter when Google redirects back to merchant\'s site', 'google-listings-and-ads' ),
+ 'type' => 'string',
+ 'validate_callback' => 'rest_validate_request_arg',
+ 'required' => true,
+ ],
+ ];
+ }
+
+ /**
+ * Get the item schema properties for the controller.
+ *
+ * @return array
+ */
+ protected function get_schema_properties(): array {
+ return [
+ 'auth_url' => [
+ 'type' => 'string',
+ 'description' => __( 'The authorization URL for granting access to Google WPCOM App.', 'google-listings-and-ads' ),
+ 'context' => [ 'view' ],
+ ],
+ 'status' => [
+ 'type' => 'string',
+ 'description' => __( 'The status of the merchant granting access to Google\'s WPCOM app', 'google-listings-and-ads' ),
+ 'enum' => OAuthService::ALLOWED_STATUSES,
+ 'context' => [ 'view' ],
+ ],
+ ];
+ }
+
+ /**
+ * Get the item schema name for the controller.
+ *
+ * Used for building the API response schema.
+ *
+ * @return string
+ */
+ protected function get_schema_title(): string {
+ return 'rest_api_authorize';
+ }
+}
diff --git a/src/API/WP/NotificationsService.php b/src/API/WP/NotificationsService.php
new file mode 100644
index 0000000000..b1892e7de2
--- /dev/null
+++ b/src/API/WP/NotificationsService.php
@@ -0,0 +1,178 @@
+merchant_center = $merchant_center;
+ $this->notification_url = "https://public-api.wordpress.com/wpcom/v2/sites/{$blog_id}/partners/google/notifications";
+ }
+
+ /**
+ * Calls the Notification endpoint in WPCOM.
+ * https://public-api.wordpress.com/wpcom/v2/sites/{site}/partners/google/notifications
+ *
+ * @param string $topic The topic to use in the notification.
+ * @param int|null $item_id The item ID to notify. It can be null for topics that doesn't need Item ID
+ * @param array $data Optional data to send in the request.
+ * @return bool True is the notification is successful. False otherwise.
+ */
+ public function notify( string $topic, $item_id = null, $data = [] ): bool {
+ /**
+ * Allow users to disable the notification request.
+ *
+ * @since 2.8.0
+ *
+ * @param bool $value The current filter value. True by default.
+ * @param int $item_id The item_id for the notification.
+ * @param string $topic The topic for the notification.
+ */
+ if ( ! apply_filters( 'woocommerce_gla_notify', $this->is_ready() && in_array( $topic, self::ALLOWED_TOPICS, true ), $item_id, $topic ) ) {
+ $this->notification_error( $topic, 'Notification was not sent because the Notification Service is not ready or the topic is not valid.', $item_id );
+ return false;
+ }
+
+ $remote_args = [
+ 'method' => 'POST',
+ 'timeout' => 30,
+ 'headers' => [
+ 'x-woocommerce-topic' => $topic,
+ 'Content-Type' => 'application/json',
+ ],
+ 'body' => array_merge( $data, [ 'item_id' => $item_id ] ),
+ 'url' => $this->get_notification_url(),
+ ];
+
+ $response = $this->do_request( $remote_args );
+
+ if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) >= 400 ) {
+ $error = is_wp_error( $response ) ? $response->get_error_message() : wp_remote_retrieve_body( $response );
+ $this->notification_error( $topic, $error, $item_id );
+ return false;
+ }
+
+ do_action(
+ 'woocommerce_gla_debug_message',
+ sprintf( 'Notification - Item ID: %s - Topic: %s - Data %s', $item_id, $topic, json_encode( $data ) ),
+ __METHOD__
+ );
+
+ return true;
+ }
+
+ /**
+ * Logs an error.
+ *
+ * @param string $topic
+ * @param string $error
+ * @param int|null $item_id
+ */
+ private function notification_error( string $topic, string $error, $item_id = null ): void {
+ do_action(
+ 'woocommerce_gla_error',
+ sprintf( 'Error sending notification for Item ID %s with topic %s. %s', $item_id, $topic, $error ),
+ __METHOD__
+ );
+ }
+
+ /**
+ * Performs a Remote Request
+ *
+ * @param array $args
+ * @return array|\WP_Error
+ */
+ protected function do_request( array $args ) {
+ return Client::remote_request( $args, wp_json_encode( $args['body'] ) );
+ }
+
+ /**
+ * Get the route
+ *
+ * @return string The route.
+ */
+ public function get_notification_url(): string {
+ return $this->notification_url;
+ }
+
+ /**
+ * If the Notifications are ready
+ * This happens when the WPCOM API is Authorized and the feature is enabled.
+ *
+ * @return bool
+ */
+ public function is_ready(): bool {
+ return $this->options->is_wpcom_api_authorized() && $this->is_enabled() && $this->merchant_center->is_ready_for_syncing();
+ }
+
+ /**
+ * If the Notifications are enabled
+ *
+ * @return bool
+ */
+ public function is_enabled(): bool {
+ return apply_filters( 'woocommerce_gla_notifications_enabled', true );
+ }
+}
diff --git a/src/API/WP/OAuthService.php b/src/API/WP/OAuthService.php
new file mode 100644
index 0000000000..768fb477c0
--- /dev/null
+++ b/src/API/WP/OAuthService.php
@@ -0,0 +1,250 @@
+get_data_from_google();
+
+ $store_url = urlencode_deep( admin_url( "admin.php?page=wc-admin&path={$path}" ) );
+
+ $state = $this->base64url_encode(
+ build_query(
+ [
+ 'nonce' => $google_data['nonce'],
+ 'store_url' => $store_url,
+ ]
+ )
+ );
+
+ $auth_url = esc_url_raw(
+ add_query_arg(
+ [
+ 'blog' => Jetpack_Options::get_option( 'id' ),
+ 'client_id' => $google_data['client_id'],
+ 'redirect_uri' => $google_data['redirect_uri'],
+ 'response_type' => self::RESPONSE_TYPE,
+ 'scope' => self::SCOPE,
+ 'state' => $state,
+ ],
+ $this->get_wpcom_api_url( self::AUTH_URL )
+ )
+ );
+
+ return $auth_url;
+ }
+
+ /**
+ * Get a WPCOM REST API URl concatenating the endpoint with the API Domain
+ *
+ * @param string $endpoint The endpoint to get the URL for
+ *
+ * @return string The WPCOM endpoint with the domain.
+ */
+ protected function get_wpcom_api_url( string $endpoint ): string {
+ return self::WPCOM_API_URL . $endpoint;
+ }
+
+ /**
+ * Calls an API by Google via WCS to get required information in order to form an auth URL.
+ *
+ * @return array{client_id: string, redirect_uri: string, nonce: string} An associative array contains required information that is retrived from Google.
+ * client_id: Google's WPCOM app client ID, will be used to form the authorization URL.
+ * redirect_uri: A Google's URL that will be redirected to when the merchant approve the app access. Note that it needs to be matched with the Google WPCOM app client settings.
+ * nonce: A string returned by Google that we will put it in the auth URL and the redirect_uri. Google will use it to verify the call.
+ * @throws ContainerExceptionInterface When get_sdi_auth_params throws an exception.
+ */
+ protected function get_data_from_google(): array {
+ /** @var Middleware $middleware */
+ $middleware = $this->container->get( Middleware::class );
+ $response = $middleware->get_sdi_auth_params();
+ $nonce = $response['nonce'];
+ $this->options->update( OptionsInterface::GOOGLE_WPCOM_AUTH_NONCE, $nonce );
+ return [
+ 'client_id' => $response['clientId'],
+ 'redirect_uri' => $response['redirectUri'],
+ 'nonce' => $nonce,
+ ];
+ }
+
+ /**
+ * Perform a remote request for revoking OAuth access for the current user.
+ *
+ * @return string The body of the response
+ * @throws Exception If the remote request fails.
+ */
+ public function revoke_wpcom_api_auth(): string {
+ $args = [
+ 'method' => 'DELETE',
+ 'timeout' => 30,
+ 'url' => $this->get_wpcom_api_url( '/wpcom/v2/sites/' . Jetpack_Options::get_option( 'id' ) . '/wc/partners/google/revoke-token' ),
+ 'user_id' => get_current_user_id(),
+ ];
+
+ $request = $this->container->get( Jetpack::class )->remote_request( $args );
+
+ if ( is_wp_error( $request ) ) {
+
+ /**
+ * When the WPCOM token has been revoked with errors.
+ *
+ * @event revoke_wpcom_api_authorization
+ * @property int status The status of the request.
+ * @property string error The error message.
+ * @property int|null blog_id The blog ID.
+ */
+ do_action(
+ 'woocommerce_gla_track_event',
+ 'revoke_wpcom_api_authorization',
+ [
+ 'status' => 400,
+ 'error' => $request->get_error_message(),
+ 'blog_id' => Jetpack_Options::get_option( 'id' ),
+ ]
+ );
+
+ throw new Exception( $request->get_error_message(), 400 );
+ } else {
+ $body = wp_remote_retrieve_body( $request );
+ $status = wp_remote_retrieve_response_code( $request );
+
+ if ( ! $status || $status !== 200 ) {
+ $data = json_decode( $body, true );
+ $message = $data['message'] ?? 'Error revoking access to WPCOM.';
+
+ /**
+ *
+ * When the WPCOM token has been revoked with errors.
+ *
+ * @event revoke_wpcom_api_authorization
+ * @property int status The status of the request.
+ * @property string error The error message.
+ * @property int|null blog_id The blog ID.
+ */
+ do_action(
+ 'woocommerce_gla_track_event',
+ 'revoke_wpcom_api_authorization',
+ [
+ 'status' => $status,
+ 'error' => $message,
+ 'blog_id' => Jetpack_Options::get_option( 'id' ),
+ ]
+ );
+
+ throw new Exception( $message, $status );
+ }
+
+ /**
+ * When the WPCOM token has been revoked successfully.
+ *
+ * @event revoke_wpcom_api_authorization
+ * @property int status The status of the request.
+ * @property int|null blog_id The blog ID.
+ */
+ do_action(
+ 'woocommerce_gla_track_event',
+ 'revoke_wpcom_api_authorization',
+ [
+ 'status' => 200,
+ 'blog_id' => Jetpack_Options::get_option( 'id' ),
+ ]
+ );
+
+ $this->container->get( AccountService::class )->reset_wpcom_api_authorization_data();
+ return $body;
+ }
+ }
+
+ /**
+ * Deactivate the service.
+ *
+ * Revoke token on deactivation.
+ */
+ public function deactivate(): void {
+ // Try to revoke the token on deactivation. If no token is available, it will throw an exception which we can ignore.
+ try {
+ $this->revoke_wpcom_api_auth();
+ } catch ( Exception $e ) {
+ do_action(
+ 'woocommerce_gla_error',
+ sprintf( 'Error revoking the WPCOM token: %s', $e->getMessage() ),
+ __METHOD__
+ );
+ }
+ }
+}
diff --git a/src/Admin/Admin.php b/src/Admin/Admin.php
index 07b6c31f27..ca3825dc22 100644
--- a/src/Admin/Admin.php
+++ b/src/Admin/Admin.php
@@ -289,7 +289,7 @@ public function privacy_policy() {
' . $policy_text . '
';
- wp_add_privacy_policy_content( 'Google Listings & Ads', wpautop( $content, false ) );
+ wp_add_privacy_policy_content( 'Google for WooCommerce', wpautop( $content, false ) );
}
/**
diff --git a/src/Admin/Product/Attributes/AttributesTab.php b/src/Admin/Product/Attributes/AttributesTab.php
index 85f12c5888..682765a88f 100644
--- a/src/Admin/Product/Attributes/AttributesTab.php
+++ b/src/Admin/Product/Attributes/AttributesTab.php
@@ -94,7 +94,7 @@ function () {
}
/**
- * Adds the Google Listing & Ads tab to the WooCommerce product data box.
+ * Adds the Google for WooCommerce tab to the WooCommerce product data box.
*
* @param array $tabs The current product data tabs.
*
@@ -102,7 +102,7 @@ function () {
*/
private function add_tab( array $tabs ): array {
$tabs['gla_attributes'] = [
- 'label' => 'Google Listings and Ads',
+ 'label' => 'Google for WooCommerce',
'class' => 'gla',
'target' => 'gla_attributes',
];
diff --git a/src/Admin/Product/Attributes/AttributesTrait.php b/src/Admin/Product/Attributes/AttributesTrait.php
index 4d80abe30c..cb177345f1 100644
--- a/src/Admin/Product/Attributes/AttributesTrait.php
+++ b/src/Admin/Product/Attributes/AttributesTrait.php
@@ -10,7 +10,7 @@
*/
trait AttributesTrait {
/**
- * Return an array of WooCommerce product types that the Google Listings and Ads tab can be displayed for.
+ * Return an array of WooCommerce product types that the Google for WooCommerce tab can be displayed for.
*
* @return array of WooCommerce product types (e.g. 'simple', 'variable', etc.)
*/
diff --git a/src/Admin/ProductBlocksService.php b/src/Admin/ProductBlocksService.php
index 7286ce006d..a3843a6a81 100644
--- a/src/Admin/ProductBlocksService.php
+++ b/src/Admin/ProductBlocksService.php
@@ -145,7 +145,7 @@ public function hook_block_template( BlockInterface $block ): void {
'id' => 'google-listings-and-ads-group',
'order' => 100,
'attributes' => [
- 'title' => __( 'Google Listings & Ads', 'google-listings-and-ads' ),
+ 'title' => __( 'Google for WooCommerce', 'google-listings-and-ads' ),
],
]
);
diff --git a/src/Autoloader.php b/src/Autoloader.php
index 8f72cea100..61c2f0f7cd 100644
--- a/src/Autoloader.php
+++ b/src/Autoloader.php
@@ -48,7 +48,7 @@ public static function init() {
protected static function missing_autoloader() {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions
- esc_html__( 'Your installation of Google Listings and Ads is incomplete. If you installed from GitHub, please refer to this document to set up your development environment: https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment', 'google-listings-and-ads' )
+ esc_html__( 'Your installation of Google for WooCommerce is incomplete. If you installed from GitHub, please refer to this document to set up your development environment: https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment', 'google-listings-and-ads' )
);
}
add_action(
@@ -60,7 +60,7 @@ function () {
',
''
);
diff --git a/src/ConnectionTest.php b/src/ConnectionTest.php
index 4172a20b7a..578025e257 100644
--- a/src/ConnectionTest.php
+++ b/src/ConnectionTest.php
@@ -9,18 +9,21 @@
namespace Automattic\WooCommerce\GoogleListingsAndAds;
+use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Ads;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AdsCampaign;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Connection;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Merchant;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Middleware;
+use Automattic\WooCommerce\GoogleListingsAndAds\API\WP\NotificationsService;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Registerable;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\CleanupProductsJob;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\DeleteAllProducts;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\UpdateAllProducts;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\UpdateProducts;
+use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantStatuses;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\AdsAccountState;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\MerchantAccountState;
@@ -80,6 +83,13 @@ function() {
*/
protected $response = '';
+ /**
+ * Store response from the integration status API request.
+ *
+ * @var string
+ */
+ protected $integration_status_response = [];
+
/**
* Add menu entries
*/
@@ -88,9 +98,9 @@ protected function register_admin_menu() {
add_menu_page(
'Connection Test',
'Connection Test',
- 'manage_options',
+ 'manage_woocommerce',
'connection-test-admin-page',
- function() {
+ function () {
$this->render_admin_page();
}
);
@@ -99,9 +109,9 @@ function() {
'',
'Connection Test',
'Connection Test',
- 'manage_options',
+ 'manage_woocommerce',
'connection-test-admin-page',
- function() {
+ function () {
$this->render_admin_page();
}
);
@@ -137,7 +147,7 @@ protected function render_admin_page() {
Connection Test
-
Google Listings & Ads connection testing page used for debugging purposes. Debug responses are output at the top of the page.
+
Google for WooCommerce connection testing page used for debugging purposes. Debug responses are output at the top of the page.
@@ -636,6 +646,134 @@ protected function render_admin_page() {
+
+
+
+ container->get( OptionsInterface::class );
+ $wp_api_status = $options->get( OptionsInterface::WPCOM_REST_API_STATUS );
+ $notification_service = new NotificationsService( $this->container->get( MerchantCenterService::class ) );
+ $notification_service->set_options_object( $options );
+ ?>
+
Partner API Pull Integration
+
+
+
response .= wp_remote_retrieve_body( $response );
}
+ if ( 'partner-notification' === $_GET['action'] && check_admin_referer( 'partner-notification' ) ) {
+ if ( ! isset( $_GET['topic'] ) ) {
+ $this->response .= "\n Topic is required.";
+ return;
+ }
+
+ $item = $_GET['item_id'] ?? null;
+ $topic = $_GET['topic'];
+ $mc = $this->container->get( MerchantCenterService::class );
+ /** @var OptionsInterface $options */
+ $options = $this->container->get( OptionsInterface::class );
+ $service = new NotificationsService( $mc );
+ $service->set_options_object( $options );
+
+ if ( $service->notify( $topic, $item ) ) {
+ $this->response .= "\n Notification success. Item: " . $item . " - Topic: " . $topic;
+ } else {
+ $this->response .= "\n Notification failed. Item: " . $item . " - Topic: " . $topic;
+ }
+
+ return;
+ }
+
+ if ( 'partner-integration-status' === $_GET['action'] && check_admin_referer( 'partner-integration-status' ) ) {
+
+ $integration_status_args = [
+ 'method' => 'GET',
+ 'timeout' => 30,
+ 'url' => 'https://public-api.wordpress.com/wpcom/v2/sites/' . Jetpack_Options::get_option( 'id' ) . '/wc/partners/google/remote-site-status',
+ 'user_id' => get_current_user_id(),
+ ];
+
+ $integration_remote_request_response = Client::remote_request( $integration_status_args, null );
+
+ if ( is_wp_error( $integration_remote_request_response ) ) {
+ $this->response .= $integration_remote_request_response->get_error_message();
+ } else {
+ $this->integration_status_response = json_decode( wp_remote_retrieve_body( $integration_remote_request_response ), true ) ?? [];
+
+ if ( json_last_error() || ! isset( $this->integration_status_response['site'] ) ) {
+ $this->response .= wp_remote_retrieve_body( $integration_remote_request_response );
+ }
+ }
+
+ }
+
+ if ( 'disconnect-wp-api' === $_GET['action'] && check_admin_referer( 'disconnect-wp-api' ) ) {
+ $request = new Request( 'DELETE', '/wc/gla/rest-api/authorize' );
+ $this->send_rest_request( $request );
+ }
+
if ( 'wcs-auth-test' === $_GET['action'] && check_admin_referer( 'wcs-auth-test' ) ) {
$url = trailingslashit( $this->get_connect_server_url() ) . 'connection/test';
$args = [
diff --git a/src/Container.php b/src/Container.php
index 32c853762d..c39c3855a5 100644
--- a/src/Container.php
+++ b/src/Container.php
@@ -20,7 +20,7 @@
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Psr\Container\NotFoundExceptionInterface;
/**
- * PSR11 compliant dependency injection container for Google Listings and Ads.
+ * PSR11 compliant dependency injection container for Google for WooCommerce.
*
* Classes in the `src` directory should specify dependencies from that directory via constructor arguments
* with type hints. If an instance of the container itself is needed, the type hint to use is
diff --git a/src/Coupon/CouponHelper.php b/src/Coupon/CouponHelper.php
index 3c46644d82..6ea601eb00 100644
--- a/src/Coupon/CouponHelper.php
+++ b/src/Coupon/CouponHelper.php
@@ -8,7 +8,9 @@
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\ChannelVisibility;
+use Automattic\WooCommerce\GoogleListingsAndAds\Value\NotificationStatus;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\SyncStatus;
+use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications\HelperNotificationInterface;
use WC_Coupon;
defined( 'ABSPATH' ) || exit();
@@ -17,7 +19,7 @@
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Coupon
*/
-class CouponHelper implements Service {
+class CouponHelper implements Service, HelperNotificationInterface {
use PluginHelper;
@@ -56,6 +58,19 @@ public function __construct(
$this->merchant_center = $merchant_center;
}
+ /**
+ * Mark the item as notified.
+ *
+ * @param WC_Coupon $coupon
+ *
+ * @return void
+ */
+ public function mark_as_notified( $coupon ): void {
+ $this->meta_handler->update_synced_at( $coupon, time() );
+ $this->meta_handler->update_sync_status( $coupon, SyncStatus::SYNCED );
+ $this->update_empty_visibility( $coupon );
+ }
+
/**
* Mark a coupon as synced. This function accepts nullable $google_id,
* which guarantees version compatibility for Alpha, Beta and stable verison promtoion APIs.
@@ -91,7 +106,7 @@ public function mark_as_synced(
*
* @param WC_Coupon $coupon
*/
- public function mark_as_unsynced( WC_Coupon $coupon ) {
+ public function mark_as_unsynced( $coupon ): void {
$this->meta_handler->delete_synced_at( $coupon );
$this->meta_handler->update_sync_status( $coupon, SyncStatus::NOT_SYNCED );
$this->meta_handler->delete_google_ids( $coupon );
@@ -321,4 +336,96 @@ public function get_validation_errors( WC_Coupon $coupon ): array {
return $errors;
}
+
+ /**
+ * Indicates if a coupon is ready for sending Notifications.
+ * A coupon is ready to send notifications if its sync ready and the post status is publish.
+ *
+ * @param WC_Coupon $coupon
+ *
+ * @return bool
+ */
+ public function is_ready_to_notify( WC_Coupon $coupon ): bool {
+ $is_ready = $this->is_sync_ready( $coupon ) && $coupon->get_status() === 'publish';
+
+ /**
+ * Allow users to filter if a coupon is ready to notify.
+ *
+ * @since 2.8.0
+ *
+ * @param bool $value The current filter value.
+ * @param WC_Coupon $coupon The coupon for the notification.
+ */
+ return apply_filters( 'woocommerce_gla_coupon_is_ready_to_notify', $is_ready, $coupon );
+ }
+
+
+ /**
+ * Indicates if a coupon was already notified about its creation.
+ * Notice we consider synced coupons in MC as notified for creation.
+ *
+ * @param WC_Coupon $coupon
+ *
+ * @return bool
+ */
+ public function has_notified_creation( WC_Coupon $coupon ): bool {
+ $valid_has_notified_creation_statuses = [
+ NotificationStatus::NOTIFICATION_CREATED,
+ NotificationStatus::NOTIFICATION_UPDATED,
+ NotificationStatus::NOTIFICATION_PENDING_UPDATE,
+ NotificationStatus::NOTIFICATION_PENDING_DELETE,
+ ];
+
+ return in_array(
+ $this->meta_handler->get_notification_status( $coupon ),
+ $valid_has_notified_creation_statuses,
+ true
+ ) || $this->is_coupon_synced( $coupon );
+ }
+
+ /**
+ * Set the notification status for a WooCommerce coupon.
+ *
+ * @param WC_Coupon $coupon
+ * @param string $status
+ */
+ public function set_notification_status( $coupon, $status ): void {
+ $this->meta_handler->update_notification_status( $coupon, $status );
+ }
+
+ /**
+ * Indicates if a coupon is ready for sending a create Notification.
+ * A coupon is ready to send create notifications if is ready to notify and has not sent create notification yet.
+ *
+ * @param WC_Coupon $coupon
+ *
+ * @return bool
+ */
+ public function should_trigger_create_notification( $coupon ): bool {
+ return $this->is_ready_to_notify( $coupon ) && ! $this->has_notified_creation( $coupon );
+ }
+
+ /**
+ * Indicates if a coupon is ready for sending an update Notification.
+ * A coupon is ready to send update notifications if is ready to notify and has sent create notification already.
+ *
+ * @param WC_Coupon $coupon
+ *
+ * @return bool
+ */
+ public function should_trigger_update_notification( $coupon ): bool {
+ return $this->is_ready_to_notify( $coupon ) && $this->has_notified_creation( $coupon );
+ }
+
+ /**
+ * Indicates if a coupon is ready for sending a delete Notification.
+ * A coupon is ready to send delete notifications if it is not ready to notify and has sent create notification already.
+ *
+ * @param WC_Coupon $coupon
+ *
+ * @return bool
+ */
+ public function should_trigger_delete_notification( $coupon ): bool {
+ return ! $this->is_ready_to_notify( $coupon ) && $this->has_notified_creation( $coupon );
+ }
}
diff --git a/src/Coupon/CouponMetaHandler.php b/src/Coupon/CouponMetaHandler.php
index 81bbf405a9..a296f2e632 100644
--- a/src/Coupon/CouponMetaHandler.php
+++ b/src/Coupon/CouponMetaHandler.php
@@ -38,6 +38,9 @@
* @method update_mc_status( WC_Coupon $coupon, string $value )
* @method delete_mc_status( WC_Coupon $coupon )
* @method get_mc_status( WC_Coupon $coupon ): string|null
+ * @method update_notification_status( WC_Coupon $coupon, string $value )
+ * @method delete_notification_status( WC_Coupon $coupon )
+ * @method get_notification_status( WC_Coupon $coupon ): string|null
*/
class CouponMetaHandler implements Service {
@@ -59,6 +62,9 @@ class CouponMetaHandler implements Service {
public const KEY_MC_STATUS = 'mc_status';
+ public const KEY_NOTIFICATION_STATUS = 'notification_status';
+
+
protected const TYPES = [
self::KEY_SYNCED_AT => 'int',
self::KEY_GOOGLE_IDS => 'array',
@@ -68,6 +74,7 @@ class CouponMetaHandler implements Service {
self::KEY_SYNC_FAILED_AT => 'int',
self::KEY_SYNC_STATUS => 'string',
self::KEY_MC_STATUS => 'string',
+ self::KEY_NOTIFICATION_STATUS => 'string',
];
/**
diff --git a/src/Coupon/CouponSyncer.php b/src/Coupon/CouponSyncer.php
index 0fa2567229..7b88ae48f8 100644
--- a/src/Coupon/CouponSyncer.php
+++ b/src/Coupon/CouponSyncer.php
@@ -456,9 +456,9 @@ protected function handle_delete_errors( array $invalid_coupons ) {
}
/**
- * Validates whether Merchant Center is set up and connected.
+ * Validates whether Merchant Center is connected and ready for pushing data.
*
- * @throws CouponSyncerException If Google Merchant Center is not set up and connected.
+ * @throws CouponSyncerException If Google Merchant Center is not set up and connected or is not ready for pushing data.
*/
protected function validate_merchant_center_setup(): void {
if ( ! $this->merchant_center->is_ready_for_syncing() ) {
@@ -475,5 +475,20 @@ protected function validate_merchant_center_setup(): void {
)
);
}
+
+ if ( ! $this->merchant_center->should_push() ) {
+ do_action(
+ 'woocommerce_gla_error',
+ 'Cannot push any coupons because they are being fetched automatically.',
+ __METHOD__
+ );
+
+ throw new CouponSyncerException(
+ __(
+ 'Pushing Coupons will not run if the automatic data fetching is enabled. Please review your configuration in Google Listing and Ads settings.',
+ 'google-listings-and-ads'
+ )
+ );
+ }
}
}
diff --git a/src/Coupon/SyncerHooks.php b/src/Coupon/SyncerHooks.php
index 4eb1855a07..3c165ea8ef 100644
--- a/src/Coupon/SyncerHooks.php
+++ b/src/Coupon/SyncerHooks.php
@@ -3,14 +3,17 @@
namespace Automattic\WooCommerce\GoogleListingsAndAds\Coupon;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\DeleteCouponEntry;
+use Automattic\WooCommerce\GoogleListingsAndAds\API\WP\NotificationsService;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Registerable;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\JobRepository;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\DeleteCoupon;
+use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications\CouponNotificationJob;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\UpdateCoupon;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
+use Automattic\WooCommerce\GoogleListingsAndAds\Value\NotificationStatus;
use WC_Coupon;
defined( 'ABSPATH' ) || exit();
@@ -63,12 +66,23 @@ class SyncerHooks implements Service, Registerable {
*/
protected $delete_coupon_job;
+ /**
+ * @var CouponNotificationJob
+ */
+ protected $coupon_notification_job;
+
/**
*
* @var MerchantCenterService
*/
protected $merchant_center;
+ /**
+ * @var NotificationsService
+ */
+ protected $notifications_service;
+
+
/**
*
* @var WC
@@ -81,65 +95,79 @@ class SyncerHooks implements Service, Registerable {
* @param CouponHelper $coupon_helper
* @param JobRepository $job_repository
* @param MerchantCenterService $merchant_center
+ * @param NotificationsService $notifications_service
* @param WC $wc
*/
public function __construct(
CouponHelper $coupon_helper,
JobRepository $job_repository,
MerchantCenterService $merchant_center,
+ NotificationsService $notifications_service,
WC $wc
) {
- $this->update_coupon_job = $job_repository->get( UpdateCoupon::class );
- $this->delete_coupon_job = $job_repository->get( DeleteCoupon::class );
- $this->coupon_helper = $coupon_helper;
- $this->merchant_center = $merchant_center;
- $this->wc = $wc;
+ $this->update_coupon_job = $job_repository->get( UpdateCoupon::class );
+ $this->delete_coupon_job = $job_repository->get( DeleteCoupon::class );
+ $this->coupon_notification_job = $job_repository->get( CouponNotificationJob::class );
+ $this->coupon_helper = $coupon_helper;
+ $this->merchant_center = $merchant_center;
+ $this->notifications_service = $notifications_service;
+ $this->wc = $wc;
}
/**
* Register a service.
*/
public function register(): void {
- // only register the hooks if Merchant Center is set up and ready for syncing data.
+ // only register the hooks if Merchant Center is set up correctly.
if ( ! $this->merchant_center->is_ready_for_syncing() ) {
return;
}
- $update_by_id = function ( int $coupon_id ) {
- $coupon = $this->wc->maybe_get_coupon( $coupon_id );
- if ( $coupon instanceof WC_Coupon ) {
- $this->handle_update_coupon( $coupon );
- }
- };
-
- $pre_delete = function ( int $coupon_id ) {
- $this->handle_pre_delete_coupon( $coupon_id );
- };
-
- $delete_by_id = function ( int $coupon_id ) {
- $this->handle_delete_coupon( $coupon_id );
- };
-
// when a coupon is added / updated, schedule a update job.
- add_action( 'woocommerce_new_coupon', $update_by_id, 90, 2 );
- add_action( 'woocommerce_update_coupon', $update_by_id, 90, 2 );
- add_action( 'woocommerce_gla_bulk_update_coupon', $update_by_id, 90 );
+ add_action( 'woocommerce_new_coupon', [ $this, 'update_by_id' ], 90, 2 );
+ add_action( 'woocommerce_update_coupon', [ $this, 'update_by_id' ], 90, 2 );
+ add_action( 'woocommerce_gla_bulk_update_coupon', [ $this, 'update_by_id' ], 90 );
// when a coupon is trashed or removed, schedule a delete job.
- add_action( 'wp_trash_post', $pre_delete, 90 );
- add_action( 'before_delete_post', $pre_delete, 90 );
- add_action(
- 'woocommerce_before_delete_product_variation',
- $pre_delete,
- 90
- );
- add_action( 'trashed_post', $delete_by_id, 90 );
- add_action( 'deleted_post', $delete_by_id, 90 );
- add_action( 'woocommerce_delete_coupon', $delete_by_id, 90, 2 );
- add_action( 'woocommerce_trash_coupon', $delete_by_id, 90, 2 );
+ add_action( 'wp_trash_post', [ $this, 'pre_delete' ], 90 );
+ add_action( 'before_delete_post', [ $this, 'pre_delete' ], 90 );
+ add_action( 'trashed_post', [ $this, 'delete_by_id' ], 90 );
+ add_action( 'deleted_post', [ $this, 'delete_by_id' ], 90 );
+ add_action( 'woocommerce_delete_coupon', [ $this, 'delete_by_id' ], 90, 2 );
+ add_action( 'woocommerce_trash_coupon', [ $this, 'delete_by_id' ], 90, 2 );
// when a coupon is restored from trash, schedule a update job.
- add_action( 'untrashed_post', $update_by_id, 90 );
+ add_action( 'untrashed_post', [ $this, 'update_by_id' ], 90 );
+ }
+
+ /**
+ * Update a coupon by the ID
+ *
+ * @param int $coupon_id
+ */
+ public function update_by_id( int $coupon_id ) {
+ $coupon = $this->wc->maybe_get_coupon( $coupon_id );
+ if ( $coupon instanceof WC_Coupon ) {
+ $this->handle_update_coupon( $coupon );
+ }
+ }
+
+ /**
+ * Delete a coupon by the ID
+ *
+ * @param int $coupon_id
+ */
+ public function delete_by_id( int $coupon_id ) {
+ $this->handle_delete_coupon( $coupon_id );
+ }
+
+ /**
+ * Pre Delete a coupon by the ID
+ *
+ * @param int $coupon_id
+ */
+ public function pre_delete( int $coupon_id ) {
+ $this->handle_pre_delete_coupon( $coupon_id );
}
/**
@@ -153,6 +181,10 @@ public function register(): void {
protected function handle_update_coupon( WC_Coupon $coupon ) {
$coupon_id = $coupon->get_id();
+ if ( $this->notifications_service->is_ready() ) {
+ $this->handle_update_coupon_notification( $coupon );
+ }
+
// Schedule an update job if product sync is enabled.
if ( $this->coupon_helper->is_sync_ready( $coupon ) ) {
$this->coupon_helper->mark_as_pending( $coupon );
@@ -232,6 +264,10 @@ protected function get_coupon_to_delete( WC_Coupon $coupon ): WCCouponAdapter {
* @param int $coupon_id
*/
protected function handle_delete_coupon( int $coupon_id ) {
+ if ( $this->notifications_service->is_ready() ) {
+ $this->maybe_send_delete_notification( $coupon_id );
+ }
+
if ( ! isset( $this->delete_requests_map[ $coupon_id ] ) ) {
return;
}
@@ -248,6 +284,26 @@ protected function handle_delete_coupon( int $coupon_id ) {
}
}
+ /**
+ * Send the notification for coupon deletion
+ *
+ * @since 2.8.0
+ * @param int $coupon_id
+ */
+ protected function maybe_send_delete_notification( int $coupon_id ): void {
+ $coupon = $this->wc->maybe_get_coupon( $coupon_id );
+
+ if ( $coupon instanceof WC_Coupon && $this->coupon_helper->should_trigger_delete_notification( $coupon ) ) {
+ $this->coupon_helper->set_notification_status( $coupon, NotificationStatus::NOTIFICATION_PENDING_DELETE );
+ $this->coupon_notification_job->schedule(
+ [
+ 'item_id' => $coupon->get_id(),
+ 'topic' => NotificationsService::TOPIC_COUPON_DELETED,
+ ]
+ );
+ }
+ }
+
/**
*
* @param int $coupon_id
@@ -322,4 +378,37 @@ protected function set_already_scheduled_to_update( int $coupon_id ): void {
protected function set_already_scheduled_to_delete( int $coupon_id ): void {
$this->set_already_scheduled( $coupon_id, self::SCHEDULE_TYPE_DELETE );
}
+
+ /**
+ * Schedules notifications for an updated coupon
+ *
+ * @param WC_Coupon $coupon
+ */
+ protected function handle_update_coupon_notification( WC_Coupon $coupon ) {
+ if ( $this->coupon_helper->should_trigger_create_notification( $coupon ) ) {
+ $this->coupon_helper->set_notification_status( $coupon, NotificationStatus::NOTIFICATION_PENDING_CREATE );
+ $this->coupon_notification_job->schedule(
+ [
+ 'item_id' => $coupon->get_id(),
+ 'topic' => NotificationsService::TOPIC_COUPON_CREATED,
+ ]
+ );
+ } elseif ( $this->coupon_helper->should_trigger_update_notification( $coupon ) ) {
+ $this->coupon_helper->set_notification_status( $coupon, NotificationStatus::NOTIFICATION_PENDING_UPDATE );
+ $this->coupon_notification_job->schedule(
+ [
+ 'item_id' => $coupon->get_id(),
+ 'topic' => NotificationsService::TOPIC_COUPON_UPDATED,
+ ]
+ );
+ } elseif ( $this->coupon_helper->should_trigger_delete_notification( $coupon ) ) {
+ $this->coupon_helper->set_notification_status( $coupon, NotificationStatus::NOTIFICATION_PENDING_DELETE );
+ $this->coupon_notification_job->schedule(
+ [
+ 'item_id' => $coupon->get_id(),
+ 'topic' => NotificationsService::TOPIC_COUPON_DELETED,
+ ]
+ );
+ }
+ }
}
diff --git a/src/DB/Query/ShippingTimeQuery.php b/src/DB/Query/ShippingTimeQuery.php
index 659b68c7ad..49ce9ef485 100644
--- a/src/DB/Query/ShippingTimeQuery.php
+++ b/src/DB/Query/ShippingTimeQuery.php
@@ -43,4 +43,26 @@ protected function sanitize_value( string $column, $value ) {
return $value;
}
+
+ /**
+ * Get all shipping times.
+ *
+ * @since 2.8.0
+ *
+ * @return array
+ */
+ public function get_all_shipping_times() {
+ $times = $this->get_results();
+ $items = [];
+ foreach ( $times as $time ) {
+ $data = [
+ 'country_code' => $time['country'],
+ 'time' => $time['time'],
+ ];
+
+ $items[ $time['country'] ] = $data;
+ }
+
+ return $items;
+ }
}
diff --git a/src/Exception/ExtensionRequirementException.php b/src/Exception/ExtensionRequirementException.php
index 065139a8cd..deca35b5e9 100644
--- a/src/Exception/ExtensionRequirementException.php
+++ b/src/Exception/ExtensionRequirementException.php
@@ -28,7 +28,7 @@ public static function missing_required_plugin( string $plugin_name ): Extension
return new static(
sprintf(
/* translators: 1 the missing plugin name */
- __( 'Google Listings and Ads requires %1$s to be enabled.', 'google-listings-and-ads' ),
+ __( 'Google for WooCommerce requires %1$s to be enabled.', 'google-listings-and-ads' ),
$plugin_name
)
);
@@ -45,7 +45,7 @@ public static function incompatible_plugin( string $plugin_name ): ExtensionRequ
return new static(
sprintf(
/* translators: 1 the incompatible plugin name */
- __( 'Google Listings and Ads is incompatible with %1$s.', 'google-listings-and-ads' ),
+ __( 'Google for WooCommerce is incompatible with %1$s.', 'google-listings-and-ads' ),
$plugin_name
)
);
diff --git a/src/Exception/InvalidVersion.php b/src/Exception/InvalidVersion.php
index 7f5590fac7..875def3ea3 100644
--- a/src/Exception/InvalidVersion.php
+++ b/src/Exception/InvalidVersion.php
@@ -30,7 +30,7 @@ public static function from_requirement( string $requirement, string $found_vers
return new static(
sprintf(
/* translators: 1 is the required component, 2 is the minimum required version, 3 is the version in use on the site */
- __( 'Google Listings and Ads requires %1$s version %2$s or higher. You are using version %3$s.', 'google-listings-and-ads' ),
+ __( 'Google for WooCommerce requires %1$s version %2$s or higher. You are using version %3$s.', 'google-listings-and-ads' ),
$requirement,
$minimum_version,
$found_version
@@ -50,7 +50,7 @@ public static function requirement_missing( string $requirement, string $minimum
return new static(
sprintf(
/* translators: 1 is the required component, 2 is the minimum required version */
- __( 'Google Listings and Ads requires %1$s version %2$s or higher.', 'google-listings-and-ads' ),
+ __( 'Google for WooCommerce requires %1$s version %2$s or higher.', 'google-listings-and-ads' ),
$requirement,
$minimum_version
)
@@ -65,7 +65,7 @@ public static function requirement_missing( string $requirement, string $minimum
*/
public static function invalid_architecture(): InvalidVersion {
return new static(
- __( 'Google Listings and Ads requires a 64 bit version of PHP.', 'google-listings-and-ads' )
+ __( 'Google for WooCommerce requires a 64 bit version of PHP.', 'google-listings-and-ads' )
);
}
}
diff --git a/src/Google/GlobalSiteTag.php b/src/Google/GlobalSiteTag.php
index d00d1a84bd..d8b8159ea8 100644
--- a/src/Google/GlobalSiteTag.php
+++ b/src/Google/GlobalSiteTag.php
@@ -267,7 +267,7 @@ protected function display_global_site_tag( string $ads_conversion_id ) {
// phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript
?>
-
+