+ { __(
+ 'Improve your conversion tracking accuracy and unlock more powerful bidding. This feature works alongside your existing conversion tags, sending secure, privacy-friendly conversion data from your website to Google.',
+ 'google-listings-and-ads'
+ ) }
+
+ { createInterpolateElement(
+ __(
+ 'Enhanced Conversion Tracking will be enabled once you’ve agreed to the terms of service on Google Ads, which can be found in your Google Ads settings screen.',
+ 'google-listings-and-ads'
+ ),
+ {
+ link: (
+
+ ),
+ }
+ ) }
+
+ );
+};
+
+export default PendingNotice;
diff --git a/js/src/components/enhanced-conversion-tracking-settings/toggle.js b/js/src/components/enhanced-conversion-tracking-settings/toggle.js
new file mode 100644
index 0000000000..dd2d6e38a4
--- /dev/null
+++ b/js/src/components/enhanced-conversion-tracking-settings/toggle.js
@@ -0,0 +1,84 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useCallback, useEffect } from '@wordpress/element';
+import { Spinner } from '@woocommerce/components';
+
+/**
+ * Internal dependencies
+ */
+import { ENHANCED_ADS_CONVERSION_STATUS } from '.~/constants';
+import { useAppDispatch } from '.~/data';
+import AppStandaloneToggleControl from '.~/components/app-standalone-toggle-control';
+import useAllowEnhancedConversions from '.~/hooks/useAllowEnhancedConversions';
+import useAcceptedCustomerDataTerms from '.~/hooks/useAcceptedCustomerDataTerms';
+
+const TOGGLE_LABEL_MAP = {
+ [ ENHANCED_ADS_CONVERSION_STATUS.DISABLED ]: __(
+ 'Enable',
+ 'google-listings-and-ads'
+ ),
+ [ ENHANCED_ADS_CONVERSION_STATUS.ENABLED ]: __(
+ 'Disable',
+ 'google-listings-and-ads'
+ ),
+};
+
+const Toggle = () => {
+ const { updateEnhancedAdsConversionStatus, invalidateResolution } =
+ useAppDispatch();
+ const { allowEnhancedConversions, hasFinishedResolution } =
+ useAllowEnhancedConversions();
+ const {
+ acceptedCustomerDataTerms,
+ hasFinishedResolution: hasResolvedAcceptedCustomerDataTerms,
+ } = useAcceptedCustomerDataTerms();
+
+ useEffect( () => {
+ if (
+ allowEnhancedConversions === ENHANCED_ADS_CONVERSION_STATUS.PENDING
+ ) {
+ invalidateResolution( 'getAcceptedCustomerDataTerms', [] );
+ }
+ }, [ allowEnhancedConversions, invalidateResolution ] );
+
+ const handleOnChange = useCallback(
+ ( value ) => {
+ if ( ! acceptedCustomerDataTerms ) {
+ return;
+ }
+
+ updateEnhancedAdsConversionStatus(
+ value
+ ? ENHANCED_ADS_CONVERSION_STATUS.ENABLED
+ : ENHANCED_ADS_CONVERSION_STATUS.DISABLED
+ );
+ },
+ [ updateEnhancedAdsConversionStatus, acceptedCustomerDataTerms ]
+ );
+
+ if ( ! hasFinishedResolution ) {
+ return ;
+ }
+
+ return (
+
+ );
+};
+
+export default Toggle;
diff --git a/js/src/components/enhanced-conversion-tracking-settings/useTermsPolling.js b/js/src/components/enhanced-conversion-tracking-settings/useTermsPolling.js
new file mode 100644
index 0000000000..268154ea26
--- /dev/null
+++ b/js/src/components/enhanced-conversion-tracking-settings/useTermsPolling.js
@@ -0,0 +1,53 @@
+/**
+ * External dependencies
+ */
+import { useEffect } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { ENHANCED_ADS_CONVERSION_STATUS } from '.~/constants';
+import { useAppDispatch } from '.~/data';
+import useAcceptedCustomerDataTerms from '.~/hooks/useAcceptedCustomerDataTerms';
+import useAllowEnhancedConversions from '.~/hooks/useAllowEnhancedConversions';
+import useAutoCheckEnhancedConversionTOS from '.~/hooks/useAutoCheckEnhancedConversionTOS';
+
+const useTermsPolling = ( startBackgroundPoll = false ) => {
+ const { invalidateResolution } = useAppDispatch();
+ const {
+ startEnhancedConversionTOSPolling,
+ stopEnhancedConversionTOSPolling,
+ } = useAutoCheckEnhancedConversionTOS();
+ const { allowEnhancedConversions } = useAllowEnhancedConversions();
+ const { acceptedCustomerDataTerms } = useAcceptedCustomerDataTerms();
+
+ useEffect( () => {
+ if (
+ ! acceptedCustomerDataTerms &&
+ allowEnhancedConversions ===
+ ENHANCED_ADS_CONVERSION_STATUS.PENDING &&
+ startBackgroundPoll
+ ) {
+ startEnhancedConversionTOSPolling();
+ return;
+ }
+
+ stopEnhancedConversionTOSPolling();
+ }, [
+ acceptedCustomerDataTerms,
+ allowEnhancedConversions,
+ startEnhancedConversionTOSPolling,
+ stopEnhancedConversionTOSPolling,
+ startBackgroundPoll,
+ ] );
+
+ useEffect( () => {
+ if (
+ allowEnhancedConversions === ENHANCED_ADS_CONVERSION_STATUS.PENDING
+ ) {
+ invalidateResolution( 'getAcceptedCustomerDataTerms', [] );
+ }
+ }, [ allowEnhancedConversions, invalidateResolution ] );
+};
+
+export default useTermsPolling;
diff --git a/js/src/constants.js b/js/src/constants.js
index 0cce0f2c3d..46aa330a12 100644
--- a/js/src/constants.js
+++ b/js/src/constants.js
@@ -111,3 +111,9 @@ export const ASSET_FORM_KEY = {
...ASSET_KEY,
...ASSET_GROUP_KEY,
};
+
+export const ENHANCED_ADS_CONVERSION_STATUS = {
+ PENDING: 'pending',
+ ENABLED: 'enabled',
+ DISABLED: 'disabled',
+};
diff --git a/js/src/data/action-types.js b/js/src/data/action-types.js
index 86556c5fda..408b8d91ef 100644
--- a/js/src/data/action-types.js
+++ b/js/src/data/action-types.js
@@ -49,6 +49,9 @@ const TYPES = {
RECEIVE_TOUR: 'RECEIVE_TOUR',
UPSERT_TOUR: 'UPSERT_TOUR',
HYDRATE_PREFETCHED_DATA: 'HYDRATE_PREFETCHED_DATA',
+ RECEIVE_ACCEPTED_CUSTOMER_DATA_TERMS:
+ 'RECEIVE_ACCEPTED_CUSTOMER_DATA_TERMS',
+ RECEIVE_ALLOW_ENHANCED_CONVERSIONS: 'RECEIVE_ALLOW_ENHANCED_CONVERSIONS',
};
export default TYPES;
diff --git a/js/src/data/actions.js b/js/src/data/actions.js
index 8aba535605..3c23d168e7 100644
--- a/js/src/data/actions.js
+++ b/js/src/data/actions.js
@@ -1247,3 +1247,63 @@ export function* upsertTour( tour, upsertingClientStoreFirst = false ) {
);
}
}
+
+export function receiveAcceptedTerms( data ) {
+ return {
+ type: TYPES.RECEIVE_ACCEPTED_CUSTOMER_DATA_TERMS,
+ data,
+ };
+}
+
+/**
+ * Updates the enhanced ads conversion status.
+ *
+ * @param {string} status to enable/disable enhanced ads conversion. Possible values are pending, enabled and disabled.
+ * @return {boolean} String The updated status.
+ */
+export function* updateEnhancedAdsConversionStatus( status ) {
+ try {
+ const response = yield apiFetch( {
+ path: `${ API_NAMESPACE }/ads/enhanced-conversion-status`,
+ method: REQUEST_ACTIONS.POST,
+ data: {
+ status,
+ },
+ } );
+
+ return receiveAllowEnhancedConversions( response );
+ } catch ( error ) {
+ handleApiError(
+ error,
+ __(
+ 'There was an error updating the enhanced ads conversion status.',
+ 'google-listings-and-ads'
+ )
+ );
+ }
+}
+
+export function receiveAllowEnhancedConversions( data ) {
+ return {
+ type: TYPES.RECEIVE_ALLOW_ENHANCED_CONVERSIONS,
+ data,
+ };
+}
+
+export function* fetchAcceptedCustomerDataTerms() {
+ try {
+ const data = yield apiFetch( {
+ path: `${ API_NAMESPACE }/ads/accepted-customer-data-terms`,
+ } );
+
+ return receiveAcceptedTerms( data );
+ } catch ( error ) {
+ handleApiError(
+ error,
+ __(
+ 'Unable to complete request. Please try again later.',
+ 'google-listings-and-ads'
+ )
+ );
+ }
+}
diff --git a/js/src/data/reducer.js b/js/src/data/reducer.js
index daf7b223e2..3660047c9e 100644
--- a/js/src/data/reducer.js
+++ b/js/src/data/reducer.js
@@ -64,6 +64,12 @@ const DEFAULT_STATE = {
report: {},
store_categories: [],
tours: {},
+ ads: {
+ conversion_tracking_setting: {
+ accepted_customer_data_terms: null,
+ allow_enhanced_conversions: null,
+ },
+ },
};
/**
@@ -491,6 +497,34 @@ const reducer = ( state = DEFAULT_STATE, action ) => {
return stateSetter.end();
}
+ case TYPES.RECEIVE_ACCEPTED_CUSTOMER_DATA_TERMS: {
+ const {
+ data: { status },
+ } = action;
+
+ return setIn(
+ state,
+ 'ads.conversion_tracking_setting.accepted_customer_data_terms',
+ status
+ );
+ }
+
+ case TYPES.RECEIVE_ALLOW_ENHANCED_CONVERSIONS: {
+ const {
+ data: { status },
+ } = action;
+
+ if ( status === null || status === undefined ) {
+ return state;
+ }
+
+ return setIn(
+ state,
+ 'ads.conversion_tracking_setting.allow_enhanced_conversions',
+ status
+ );
+ }
+
// Page will be reloaded after all accounts have been disconnected, so no need to mutate state.
case TYPES.DISCONNECT_ACCOUNTS_ALL:
default:
diff --git a/js/src/data/resolvers.js b/js/src/data/resolvers.js
index 37f4a564d5..dae5577a60 100644
--- a/js/src/data/resolvers.js
+++ b/js/src/data/resolvers.js
@@ -34,6 +34,7 @@ import {
receiveGoogleMCContactInformation,
fetchTargetAudience,
fetchMCSetup,
+ fetchAcceptedCustomerDataTerms,
receiveGoogleAccountAccess,
receiveReport,
receiveMCProductStatistics,
@@ -45,6 +46,7 @@ import {
receiveMappingRules,
receiveStoreCategories,
receiveTour,
+ receiveAllowEnhancedConversions,
} from './actions';
export function* getShippingRates() {
@@ -519,3 +521,41 @@ export function* getTour( tourId ) {
);
}
}
+
+/**
+ * Resolver for getting the accepted customer data terms.
+ */
+export function* getAcceptedCustomerDataTerms() {
+ try {
+ yield fetchAcceptedCustomerDataTerms();
+ } catch ( error ) {
+ handleApiError(
+ error,
+ __(
+ 'There was an error getting the accepted customer data terms.',
+ 'google-listings-and-ads'
+ )
+ );
+ }
+}
+
+/**
+ * Resolver for getting the enhanced conversion status.
+ */
+export function* getAllowEnhancedConversions() {
+ try {
+ const response = yield apiFetch( {
+ path: `${ API_NAMESPACE }/ads/enhanced-conversion-status`,
+ } );
+
+ yield receiveAllowEnhancedConversions( response );
+ } catch ( error ) {
+ handleApiError(
+ error,
+ __(
+ 'There was an error getting the enhance conversions status.',
+ 'google-listings-and-ads'
+ )
+ );
+ }
+}
diff --git a/js/src/data/selectors.js b/js/src/data/selectors.js
index 181fb6c427..ed17eb5d72 100644
--- a/js/src/data/selectors.js
+++ b/js/src/data/selectors.js
@@ -400,3 +400,23 @@ export const getStoreCategories = ( state ) => {
export const getTour = ( state, tourId ) => {
return state.tours[ tourId ] || null;
};
+
+/**
+ * Return the customer accepted data terms.
+ *
+ * @param {Object} state The state
+ * @return {boolean|null} TRUE if the user signed the TOS. It will be `null` if not yet fetched or fetched but doesn't exist.
+ */
+export const getAcceptedCustomerDataTerms = ( state ) => {
+ return state.ads.conversion_tracking_setting.accepted_customer_data_terms;
+};
+
+/**
+ * Return whether the user allowed enhanced conversion tracking.
+ *
+ * @param {Object} state The state
+ * @return {string|null} Possible values are 'pending' | 'enabled' | 'disabled'. It will be `null` if not yet fetched or fetched but doesn't exist.
+ */
+export const getAllowEnhancedConversions = ( state ) => {
+ return state.ads.conversion_tracking_setting.allow_enhanced_conversions;
+};
diff --git a/js/src/data/test/reducer.test.js b/js/src/data/test/reducer.test.js
index 37eb44ba8a..aad87910cd 100644
--- a/js/src/data/test/reducer.test.js
+++ b/js/src/data/test/reducer.test.js
@@ -67,6 +67,12 @@ describe( 'reducer', () => {
report: {},
store_categories: [],
tours: {},
+ ads: {
+ conversion_tracking_setting: {
+ accepted_customer_data_terms: null,
+ allow_enhanced_conversions: null,
+ },
+ },
} );
prepareState = prepareImmutableStateWithRefCheck.bind(
diff --git a/js/src/external-components/wordpress/guide/index.js b/js/src/external-components/wordpress/guide/index.js
index de07985dfd..673a313eb9 100644
--- a/js/src/external-components/wordpress/guide/index.js
+++ b/js/src/external-components/wordpress/guide/index.js
@@ -20,12 +20,6 @@ import PageControl from './page-control';
import FinishButton from './finish-button';
import './index.scss';
-/**
- * @callback renderFinishCallback
- * @param {JSX.Element} finishButton The built-in finish button of this Guide component.
- * @return {JSX.Element} React element for rendering.
- */
-
/**
* `Guide` is a React component that renders a user guide in a modal.
* The guide consists of several pages which the user can step through one by one.
@@ -37,7 +31,6 @@ import './index.scss';
* It is required for accessibility reasons.
* @param {string} [props.backButtonText] Use this to customize the label of the *Previous* button shown at the end of the guide.
* @param {string} [props.finishButtonText] Use this to customize the label of the *Finish* button shown at the end of the guide.
- * @param {renderFinishCallback} [props.renderFinish] A function for rendering custom finish block shown at the end of the guide.
* @param {Function} props.onFinish A function which is called when the guide is finished.
* The guide is finished when the modal is closed
* or when the user clicks *Finish* on the last page of the guide.
@@ -49,7 +42,6 @@ export default function Guide( {
contentLabel,
backButtonText,
finishButtonText,
- renderFinish = ( finishButton ) => finishButton,
onFinish,
pages,
} ) {
@@ -76,8 +68,8 @@ export default function Guide( {
let finishBlock = null;
- if ( ! canGoForward ) {
- const finishButton = (
+ if ( ! canGoForward && ! pages[ currentPage ].actions ) {
+ finishBlock = (
);
-
- finishBlock = renderFinish( finishButton );
}
const guideClassName = classnames(
@@ -135,6 +125,7 @@ export default function Guide( {
{ backButtonText || __( 'Previous' ) }
) }
+
{ canGoForward && (