diff --git a/plugins/woocommerce/changelog/52661-feature-51567-order-attribution-install-and-connect b/plugins/woocommerce/changelog/52661-feature-51567-order-attribution-install-and-connect
new file mode 100644
index 00000000000..8a96eefea70
--- /dev/null
+++ b/plugins/woocommerce/changelog/52661-feature-51567-order-attribution-install-and-connect
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+Add banners for promoting order attribution plugin (woocommerce-analytics).
\ No newline at end of file
diff --git a/plugins/woocommerce/client/admin/client/dashboard/index.js b/plugins/woocommerce/client/admin/client/dashboard/index.js
index caf81663f73..3428c00df03 100644
--- a/plugins/woocommerce/client/admin/client/dashboard/index.js
+++ b/plugins/woocommerce/client/admin/client/dashboard/index.js
@@ -2,11 +2,16 @@
* External dependencies
*/
import { Component, Suspense, lazy } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
import { Spinner } from '@woocommerce/components';
/**
* Internal dependencies
*/
+import {
+ OrderAttributionInstallBanner,
+ OrderAttributionInstallBannerImage,
+} from '~/order-attribution-install-banner';
import './style.scss';
const CustomizableDashboard = lazy( () =>
@@ -19,6 +24,20 @@ class Dashboard extends Component {
return (
}>
+ }
+ dismissable
+ />
);
diff --git a/plugins/woocommerce/client/admin/client/header/index.js b/plugins/woocommerce/client/admin/client/header/index.js
index d0cfcb088db..74b731ced31 100644
--- a/plugins/woocommerce/client/admin/client/header/index.js
+++ b/plugins/woocommerce/client/admin/client/header/index.js
@@ -14,7 +14,7 @@ import {
} from '@woocommerce/admin-layout';
import { getSetting } from '@woocommerce/settings';
import { Text, useSlot } from '@woocommerce/experimental';
-import { getScreenFromPath, isWCAdmin } from '@woocommerce/navigation';
+import { getScreenFromPath, isWCAdmin, getPath } from '@woocommerce/navigation';
/**
* Internal dependencies
@@ -26,6 +26,10 @@ import {
LaunchYourStoreStatus,
useLaunchYourStore,
} from '../launch-your-store';
+import {
+ OrderAttributionInstallBanner,
+ BANNER_TYPE_HEADER as ORDER_ATTRIBUTION_INSTALL_BANNER_TYPE_HEADER,
+} from '~/order-attribution-install-banner';
export const PAGE_TITLE_FILTER = 'woocommerce_admin_header_page_title';
@@ -128,6 +132,9 @@ export const Header = ( { sections, isEmbedded = false, query } ) => {
const showLaunchYourStoreStatus =
isHomescreen && launchYourStoreEnabled && ! isLoading;
+ const isAnalyticsOverviewScreen =
+ isWCAdmin() && getPath() === '/analytics/overview';
+
return (
{ activeSetupList && (
@@ -161,6 +168,14 @@ export const Header = ( { sections, isEmbedded = false, query } ) => {
{ showLaunchYourStoreStatus && }
+ { isAnalyticsOverviewScreen && (
+
+ ) }
diff --git a/plugins/woocommerce/client/admin/client/index.js b/plugins/woocommerce/client/admin/client/index.js
index 45b109edf09..e635b2a2dd5 100644
--- a/plugins/woocommerce/client/admin/client/index.js
+++ b/plugins/woocommerce/client/admin/client/index.js
@@ -35,6 +35,10 @@ import {
} from './settings-payments';
import { ErrorBoundary } from './error-boundary';
import { registerBlueprintSlotfill } from './blueprint';
+import {
+ possiblyRenderOrderAttributionSlot,
+ registerOrderAttributionSlotFill,
+} from './order-attribution-install-banner/order-editor/slot';
const appRoot = document.getElementById( 'root' );
const embeddedRoot = document.getElementById( 'woocommerce-embedded-root' );
@@ -116,6 +120,9 @@ if ( appRoot ) {
if ( window.wcAdminFeatures && window.wcAdminFeatures.blueprint === true ) {
registerBlueprintSlotfill();
}
+
+ possiblyRenderOrderAttributionSlot();
+ registerOrderAttributionSlotFill();
}
// Render the CustomerEffortScoreTracksContainer only if
diff --git a/plugins/woocommerce/client/admin/client/marketing/data/action-types.js b/plugins/woocommerce/client/admin/client/marketing/data/action-types.js
index edc1e20fee9..437eea83908 100644
--- a/plugins/woocommerce/client/admin/client/marketing/data/action-types.js
+++ b/plugins/woocommerce/client/admin/client/marketing/data/action-types.js
@@ -6,6 +6,7 @@ const TYPES = {
INSTALL_AND_ACTIVATE_RECOMMENDED_PLUGIN:
'INSTALL_AND_ACTIVATE_RECOMMENDED_PLUGIN',
SET_BLOG_POSTS: 'SET_BLOG_POSTS',
+ SET_MISC_RECOMMENDATIONS: 'SET_MISC_RECOMMENDATIONS',
SET_ERROR: 'SET_ERROR',
};
diff --git a/plugins/woocommerce/client/admin/client/marketing/data/actions.js b/plugins/woocommerce/client/admin/client/marketing/data/actions.js
index e3bfd36af41..f60045518f4 100644
--- a/plugins/woocommerce/client/admin/client/marketing/data/actions.js
+++ b/plugins/woocommerce/client/admin/client/marketing/data/actions.js
@@ -39,6 +39,13 @@ export function receiveRecommendedPlugins( plugins, category ) {
};
}
+export function receiveMiscRecommendations( miscRecommendations ) {
+ return {
+ type: TYPES.SET_MISC_RECOMMENDATIONS,
+ data: { miscRecommendations },
+ };
+}
+
export function receiveBlogPosts( posts, category ) {
return {
type: TYPES.SET_BLOG_POSTS,
diff --git a/plugins/woocommerce/client/admin/client/marketing/data/reducer.js b/plugins/woocommerce/client/admin/client/marketing/data/reducer.js
index f27d6b0a302..6fea2ad2c38 100644
--- a/plugins/woocommerce/client/admin/client/marketing/data/reducer.js
+++ b/plugins/woocommerce/client/admin/client/marketing/data/reducer.js
@@ -15,6 +15,7 @@ const DEFAULT_STATE = {
installedPlugins: installedExtensions,
activatingPlugins: [],
recommendedPlugins: {},
+ miscRecommendations: [],
blogPosts: {},
errors: {},
};
@@ -72,6 +73,11 @@ const reducer = ( state = DEFAULT_STATE, action ) => {
[ action.data.category ]: action.data.posts,
},
};
+ case TYPES.SET_MISC_RECOMMENDATIONS:
+ return {
+ ...state,
+ miscRecommendations: action.data.miscRecommendations,
+ };
case TYPES.SET_ERROR:
return {
...state,
diff --git a/plugins/woocommerce/client/admin/client/marketing/data/resolvers.js b/plugins/woocommerce/client/admin/client/marketing/data/resolvers.js
index f4ee2e8578f..2532cbb91d4 100644
--- a/plugins/woocommerce/client/admin/client/marketing/data/resolvers.js
+++ b/plugins/woocommerce/client/admin/client/marketing/data/resolvers.js
@@ -10,6 +10,7 @@ import { apiFetch } from '@wordpress/data-controls';
import {
handleFetchError,
receiveRecommendedPlugins,
+ receiveMiscRecommendations,
receiveBlogPosts,
setError,
} from './actions';
@@ -38,6 +39,28 @@ export function* getRecommendedPlugins( category ) {
}
}
+export function* getMiscRecommendations() {
+ try {
+ const response = yield apiFetch( {
+ path: `${ API_NAMESPACE }/misc-recommendations`,
+ } );
+
+ if ( response ) {
+ yield receiveMiscRecommendations( response );
+ } else {
+ throw new Error();
+ }
+ } catch ( error ) {
+ yield handleFetchError(
+ error,
+ __(
+ 'There was an error loading misc recommendations',
+ 'woocommerce'
+ )
+ );
+ }
+}
+
export function* getBlogPosts( category ) {
try {
const categoryParam = yield category ? `?category=${ category }` : '';
diff --git a/plugins/woocommerce/client/admin/client/marketing/data/selectors.js b/plugins/woocommerce/client/admin/client/marketing/data/selectors.js
index dacf0aa6414..f8e3dca9df7 100644
--- a/plugins/woocommerce/client/admin/client/marketing/data/selectors.js
+++ b/plugins/woocommerce/client/admin/client/marketing/data/selectors.js
@@ -10,6 +10,10 @@ export function getRecommendedPlugins( state, category ) {
return state.recommendedPlugins[ category ] || [];
}
+export function getMiscRecommendations( state ) {
+ return state.miscRecommendations;
+}
+
export function getBlogPosts( state, category ) {
return state.blogPosts[ category ] || [];
}
diff --git a/plugins/woocommerce/client/admin/client/order-attribution-install-banner/constants.js b/plugins/woocommerce/client/admin/client/order-attribution-install-banner/constants.js
new file mode 100644
index 00000000000..ac4edfe027a
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/order-attribution-install-banner/constants.js
@@ -0,0 +1,9 @@
+export const ORDER_ATTRIBUTION_SLOT_FILL_CONSTANT =
+ '__EXPERIMENTAL__WcAdminOrderAttributionSlots';
+export const ORDER_ATTRIBUTION_INSTALL_BANNER_DOM_ID =
+ 'order-attribution-install-banner-slotfill';
+export const ORDER_ATTRIBUTION_INSTALL_BANNER_SLOT_SCOPE =
+ 'order-attribution-install-banner';
+export const BANNER_TYPE_BIG = 'order-attribution-install-banner-big';
+export const BANNER_TYPE_SMALL = 'order-attribution-install-banner-small';
+export const BANNER_TYPE_HEADER = 'order-attribution-install-banner-header';
diff --git a/plugins/woocommerce/client/admin/client/order-attribution-install-banner/index.js b/plugins/woocommerce/client/admin/client/order-attribution-install-banner/index.js
new file mode 100644
index 00000000000..21d1e16cc03
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/order-attribution-install-banner/index.js
@@ -0,0 +1,4 @@
+export * from './order-attribution-install-banner';
+export * from './order-attribution-install-banner-image';
+export * from './use-order-attribution-install-banner';
+export * from './constants';
diff --git a/plugins/woocommerce/client/admin/client/order-attribution-install-banner/order-attribution-install-banner-image.js b/plugins/woocommerce/client/admin/client/order-attribution-install-banner/order-attribution-install-banner-image.js
new file mode 100644
index 00000000000..021c43e363a
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/order-attribution-install-banner/order-attribution-install-banner-image.js
@@ -0,0 +1,60 @@
+export const OrderAttributionInstallBannerImage = () => (
+
+);
diff --git a/plugins/woocommerce/client/admin/client/order-attribution-install-banner/order-attribution-install-banner.js b/plugins/woocommerce/client/admin/client/order-attribution-install-banner/order-attribution-install-banner.js
new file mode 100644
index 00000000000..9af9b745b9a
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/order-attribution-install-banner/order-attribution-install-banner.js
@@ -0,0 +1,192 @@
+/**
+ * External dependencies
+ */
+import { Button, Card, CardBody } from '@wordpress/components';
+import { useEffect, useCallback } from '@wordpress/element';
+import { plugins, external } from '@wordpress/icons';
+import { __ } from '@wordpress/i18n';
+import { Text } from '@woocommerce/experimental';
+import { recordEvent } from '@woocommerce/tracks';
+import { getPath } from '@woocommerce/navigation';
+
+/**
+ * Internal dependencies
+ */
+import { useOrderAttributionInstallBanner } from './use-order-attribution-install-banner';
+import {
+ BANNER_TYPE_BIG,
+ BANNER_TYPE_SMALL,
+ BANNER_TYPE_HEADER,
+} from './constants';
+import './style.scss';
+
+const WC_ANALYTICS_PRODUCT_URL =
+ 'https://woocommerce.com/products/woocommerce-analytics';
+
+/**
+ * The banner to prompt users to install the Order Attribution extension.
+ *
+ * This banner will only appear when the Order Attribution extension is not installed. It can appear in three different ways:
+ *
+ * - As a big banner in the analytics overview page. (dismissable)
+ * - As a header banner in the analytics overview page. (non-dismissable, appear only when the big banner is dismissed)
+ * - As a small banner in the order editor. (non-dismissable)
+ *
+ * @param {Object} props Component props.
+ * @param {Object} props.bannerImage The banner image component.
+ * @param {string} props.bannerType The type of the banner. Can be BANNER_TYPE_BIG, BANNER_TYPE_SMALL, or BANNER_TYPE_HEADER.
+ * @param {string} props.eventContext The context for the event tracking.
+ * @param {boolean} props.dismissable Whether the banner is dismissable.
+ * @param {string} props.badgeText The badge text to display on the banner.
+ * @param {string} props.title The title of the banner.
+ * @param {string} props.description The description of the banner.
+ * @param {string} props.buttonText The text for the button.
+ *
+ * @return {JSX.Element} The rendered component.
+ *
+ */
+export const OrderAttributionInstallBanner = ( {
+ bannerImage = null,
+ bannerType = BANNER_TYPE_BIG,
+ eventContext = 'analytics-overview',
+ dismissable = false,
+ badgeText = '',
+ title = '',
+ description = '',
+ buttonText = '',
+} ) => {
+ const { isDismissed, dismiss, shouldShowBanner } =
+ useOrderAttributionInstallBanner();
+
+ const onButtonClick = () => {
+ recordEvent( 'order_attribution_install_banner_clicked', {
+ path: getPath(),
+ context: eventContext,
+ } );
+ };
+
+ const getShouldRender = useCallback( () => {
+ // The header banner should be shown if shouldShowBanner is true and the big banner is dismissed
+ if ( bannerType === BANNER_TYPE_HEADER ) {
+ return shouldShowBanner && isDismissed;
+ }
+
+ // The small banner should always be shown if shouldShowBanner is true.
+ if ( ! dismissable ) {
+ return shouldShowBanner;
+ }
+
+ // The big banner should be shown if shouldShowBanner is true and the banner is not dismissed.
+ return shouldShowBanner && ! isDismissed;
+ }, [ bannerType, shouldShowBanner, isDismissed, dismissable ] );
+
+ const shouldRender = getShouldRender();
+
+ useEffect( () => {
+ if ( ! shouldRender ) {
+ return;
+ }
+ recordEvent( 'order_attribution_install_banner_viewed', {
+ path: getPath(),
+ context: eventContext,
+ } );
+ }, [ eventContext, shouldRender ] );
+
+ if ( ! shouldRender ) {
+ return null;
+ }
+
+ if ( bannerType === BANNER_TYPE_HEADER ) {
+ return (
+
+ );
+ }
+
+ const isSmallBanner = bannerType === BANNER_TYPE_SMALL;
+
+ return (
+
+
+
+ { bannerImage }
+
+
+ { badgeText && (
+
+
+ { badgeText }
+
+
+ ) }
+ { title && (
+
+ { title }
+
+ ) }
+ { description && (
+
+ { description }
+
+ ) }
+
+
+ { dismissable && (
+
+ ) }
+
+
+
+
+ );
+};
diff --git a/plugins/woocommerce/client/admin/client/order-attribution-install-banner/order-editor/slot.js b/plugins/woocommerce/client/admin/client/order-attribution-install-banner/order-editor/slot.js
new file mode 100644
index 00000000000..9c3d551ddaf
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/order-attribution-install-banner/order-editor/slot.js
@@ -0,0 +1,65 @@
+/**
+ * External dependencies
+ */
+import { createRoot } from '@wordpress/element';
+import { createSlotFill, SlotFillProvider } from '@wordpress/components';
+import { PluginArea, registerPlugin } from '@wordpress/plugins';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import { OrderAttributionInstallBanner } from '../order-attribution-install-banner';
+import {
+ ORDER_ATTRIBUTION_INSTALL_BANNER_DOM_ID,
+ ORDER_ATTRIBUTION_INSTALL_BANNER_SLOT_SCOPE,
+ ORDER_ATTRIBUTION_SLOT_FILL_CONSTANT,
+ BANNER_TYPE_SMALL,
+} from '../constants';
+
+const { Slot, Fill } = createSlotFill( ORDER_ATTRIBUTION_SLOT_FILL_CONSTANT );
+
+export const possiblyRenderOrderAttributionSlot = () => {
+ const slotDomElement = document.getElementById(
+ ORDER_ATTRIBUTION_INSTALL_BANNER_DOM_ID
+ );
+
+ if ( slotDomElement ) {
+ createRoot( slotDomElement ).render(
+ <>
+
+
+
+
+ >
+ );
+ }
+};
+
+const OrderAttributionInstallBannerSlotFill = () => {
+ return (
+
+
+
+ );
+};
+
+export const registerOrderAttributionSlotFill = () => {
+ registerPlugin(
+ 'woocommerce-admin-order-editor-order-attribution-install-banner-slotfill',
+ {
+ scope: ORDER_ATTRIBUTION_INSTALL_BANNER_SLOT_SCOPE,
+ render: OrderAttributionInstallBannerSlotFill,
+ }
+ );
+};
diff --git a/plugins/woocommerce/client/admin/client/order-attribution-install-banner/style.scss b/plugins/woocommerce/client/admin/client/order-attribution-install-banner/style.scss
new file mode 100644
index 00000000000..841e19afd7f
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/order-attribution-install-banner/style.scss
@@ -0,0 +1,106 @@
+.woocommerce-order-attribution-install-banner {
+
+ margin: 0 15px 10px 0;
+ animation: isLoaded;
+ animation-duration: 250ms;
+
+ &.components-card {
+ box-shadow: none;
+ border: 1px solid $table-border;
+ border-radius: 8px;
+
+ &.small {
+ border: 0;
+ border-top: 1px solid #c3c4c7;
+ border-radius: 0;
+ margin: 20px -12px -12px;
+ }
+ }
+
+ .woocommerce-order-attribution-install-banner__body {
+ display: flex;
+ align-items: center;
+
+ &.small {
+ padding: 0;
+ padding-top: 16px;
+
+ @media (max-width: $break-small) {
+ margin-left: 12px;
+ }
+ }
+
+ @media (max-width: $break-small) {
+ flex-direction: column;
+ }
+ }
+
+ .woocommerce-order-attribution-install-banner__image_container {
+ display: flex;
+ padding-left: 15px;
+ }
+
+ .woocommerce-order-attribution-install-banner__text_container {
+ margin-inline: 24px;
+
+ > * {
+ margin-block: 1rem;
+ }
+
+ p {
+ margin-block: 0.1rem;
+ }
+
+ &.small {
+ margin-inline: 0;
+ margin-inline-end: 16px;
+ }
+ }
+
+ .woocommerce-order-attribution-install-banner__text-badge {
+ display: inline-flex;
+ justify-content: center;
+ background: $studio-gray-0;
+ border-radius: 2px;
+ padding: 2px 5px;
+
+ p {
+ color: $studio-gray-80;
+ }
+ }
+
+ .woocommerce-order-attribution-install-banner__text-title {
+ color: $studio-gray-90;
+ line-height: 24px;
+ font-weight: 600;
+ }
+
+ .woocommerce-order-attribution-install-banner__text-description {
+ color: $studio-gray-90;
+ line-height: 16px;
+ max-width: 580px;
+ }
+}
+
+.components-button.woocommerce-order-attribution-install-header-banner {
+ margin-right: 12px;
+
+ @media (max-width: $break-small) {
+ text-wrap: wrap;
+ font-size: 11px;
+
+ svg {
+ display: none;
+ }
+ }
+}
+
+.components-button {
+ &.small {
+ width: 100%;
+ }
+
+ &.has-icon.has-text {
+ justify-content: center;
+ }
+}
diff --git a/plugins/woocommerce/client/admin/client/order-attribution-install-banner/use-order-attribution-install-banner.js b/plugins/woocommerce/client/admin/client/order-attribution-install-banner/use-order-attribution-install-banner.js
new file mode 100644
index 00000000000..a3128c904a1
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/order-attribution-install-banner/use-order-attribution-install-banner.js
@@ -0,0 +1,189 @@
+/**
+ * External dependencies
+ */
+import { useDispatch, useSelect } from '@wordpress/data';
+import { useCallback, useMemo } from '@wordpress/element';
+import {
+ OPTIONS_STORE_NAME,
+ PLUGINS_STORE_NAME,
+ useUser,
+} from '@woocommerce/data';
+import { recordEvent } from '@woocommerce/tracks';
+import { getPath } from '@woocommerce/navigation';
+import { isWcVersion } from '@woocommerce/settings';
+
+/**
+ * Internal dependencies
+ */
+import { STORE_KEY } from '~/marketing/data/constants';
+import '~/marketing/data';
+
+const OPTION_NAME_BANNER_DISMISSED =
+ 'woocommerce_order_attribution_install_banner_dismissed';
+const OPTION_VALUE_YES = 'yes';
+const OPTION_NAME_REMOTE_VARIANT_ASSIGNMENT =
+ 'woocommerce_remote_variant_assignment';
+
+const getThreshold = ( percentages ) => {
+ const defaultPercentages = [
+ [ '9.7', 100 ], // 100%
+ [ '9.6', 60 ], // 60%
+ [ '9.5', 10 ], // 10%
+ ];
+
+ if ( ! Array.isArray( percentages ) || percentages.length === 0 ) {
+ percentages = defaultPercentages;
+ }
+
+ // Sort the percentages in descending order by version, to ensure we get the highest version first so the isWcVersion() check works correctly.
+ // E.g. if we are on 9.7 but the percentages are in version ascending order, we would get 10% instead of 100%.
+ percentages.sort( ( a, b ) => parseFloat( b[ 0 ] ) - parseFloat( a[ 0 ] ) );
+
+ for ( let [ version, percentage ] of percentages ) {
+ if ( isWcVersion( version, '>=' ) ) {
+ percentage = parseInt( percentage, 10 );
+ if ( isNaN( percentage ) ) {
+ return 12; // Default to 10% if the percentage is not a number.
+ }
+ // Since remoteVariantAssignment ranges from 1 to 120, we need to convert the percentage to a number between 1 and 120.
+ return ( percentage / 100 ) * 120;
+ }
+ }
+
+ return 12; // Default to 10% if version is lower than 9.5
+};
+
+const shouldPromoteOrderAttribution = (
+ remoteVariantAssignment,
+ percentages
+) => {
+ remoteVariantAssignment = parseInt( remoteVariantAssignment, 10 );
+
+ if ( isNaN( remoteVariantAssignment ) ) {
+ return false;
+ }
+
+ const threshold = getThreshold( percentages );
+
+ return remoteVariantAssignment <= threshold;
+};
+
+/**
+ * A utility hook designed specifically for the order attribution install banner,
+ * which determines if the banner should be displayed, checks if it has been dismissed, and provides a function to dismiss it.
+ */
+export const useOrderAttributionInstallBanner = () => {
+ const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
+ const { currentUserCan } = useUser();
+
+ const dismiss = ( eventContext = 'analytics-overview' ) => {
+ updateOptions( {
+ [ OPTION_NAME_BANNER_DISMISSED ]: OPTION_VALUE_YES,
+ } );
+ recordEvent( 'order_attribution_install_banner_dismissed', {
+ path: getPath(),
+ context: eventContext,
+ } );
+ };
+
+ const { canUserInstallPlugins, orderAttributionInstallState } = useSelect(
+ ( select ) => {
+ const { getPluginInstallState } = select( PLUGINS_STORE_NAME );
+ const installState = getPluginInstallState(
+ 'woocommerce-analytics'
+ );
+
+ return {
+ orderAttributionInstallState: installState,
+ canUserInstallPlugins: currentUserCan( 'install_plugins' ),
+ };
+ },
+ [ currentUserCan ]
+ );
+
+ const { loading, isBannerDismissed, remoteVariantAssignment } = useSelect(
+ ( select ) => {
+ const { getOption, hasFinishedResolution } =
+ select( OPTIONS_STORE_NAME );
+
+ return {
+ loading: ! hasFinishedResolution( 'getOption', [
+ OPTION_NAME_BANNER_DISMISSED,
+ ] ),
+ isBannerDismissed: getOption( OPTION_NAME_BANNER_DISMISSED ),
+ remoteVariantAssignment: getOption(
+ OPTION_NAME_REMOTE_VARIANT_ASSIGNMENT
+ ),
+ };
+ },
+ []
+ );
+
+ const { loadingRecommendations, recommendations } = useSelect(
+ ( select ) => {
+ const { getMiscRecommendations, hasFinishedResolution } =
+ select( STORE_KEY );
+
+ return {
+ loadingRecommendations: ! hasFinishedResolution(
+ 'getMiscRecommendations'
+ ),
+ recommendations: getMiscRecommendations(),
+ };
+ },
+ []
+ );
+
+ const percentages = useMemo( () => {
+ if (
+ loadingRecommendations ||
+ ! Array.isArray( recommendations ) ||
+ recommendations.length === 0
+ ) {
+ return null;
+ }
+
+ for ( const recommendation of recommendations ) {
+ if ( recommendation.id === 'woocommerce-analytics' ) {
+ return (
+ recommendation?.order_attribution_promotion_percentage ||
+ null
+ );
+ }
+ }
+
+ return null;
+ }, [ loadingRecommendations, recommendations ] );
+
+ const getShouldShowBanner = useCallback( () => {
+ if ( ! canUserInstallPlugins || loading ) {
+ return false;
+ }
+
+ const isPluginInstalled = [ 'installed', 'activated' ].includes(
+ orderAttributionInstallState
+ );
+
+ if ( isPluginInstalled ) {
+ return false;
+ }
+
+ return shouldPromoteOrderAttribution(
+ remoteVariantAssignment,
+ percentages
+ );
+ }, [
+ loading,
+ canUserInstallPlugins,
+ orderAttributionInstallState,
+ remoteVariantAssignment,
+ percentages,
+ ] );
+
+ return {
+ loading,
+ isDismissed: isBannerDismissed === OPTION_VALUE_YES,
+ dismiss,
+ shouldShowBanner: getShouldShowBanner(),
+ };
+};
diff --git a/plugins/woocommerce/src/Admin/API/Marketing.php b/plugins/woocommerce/src/Admin/API/Marketing.php
index dcca5702b42..4a8cacf2fa5 100644
--- a/plugins/woocommerce/src/Admin/API/Marketing.php
+++ b/plugins/woocommerce/src/Admin/API/Marketing.php
@@ -79,6 +79,19 @@ class Marketing extends \WC_REST_Data_Controller {
'schema' => array( $this, 'get_public_item_schema' ),
)
);
+
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/misc-recommendations',
+ array(
+ array(
+ 'methods' => \WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_misc_recommendations' ),
+ 'permission_callback' => array( $this, 'get_recommended_plugins_permissions_check' ),
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
}
/**
@@ -141,4 +154,19 @@ class Marketing extends \WC_REST_Data_Controller {
$category = $request->get_param( 'category' );
return rest_ensure_response( $marketing_specs->get_knowledge_base_posts( $category ) );
}
+
+ /**
+ * Return misc recommendations.
+ *
+ * @param \WP_REST_Request $request Request data.
+ *
+ * @since 9.5.0
+ *
+ * @return \WP_Error|\WP_REST_Response
+ */
+ public function get_misc_recommendations( $request ) {
+ $misc_recommendations = MarketingRecommendationsInit::get_misc_recommendations();
+
+ return rest_ensure_response( $misc_recommendations );
+ }
}
diff --git a/plugins/woocommerce/src/Admin/API/Options.php b/plugins/woocommerce/src/Admin/API/Options.php
index 836886fec81..4f69eba25b3 100644
--- a/plugins/woocommerce/src/Admin/API/Options.php
+++ b/plugins/woocommerce/src/Admin/API/Options.php
@@ -223,6 +223,8 @@ class Options extends \WC_REST_Data_Controller {
'woocommerce_private_link',
'woocommerce_share_key',
'woocommerce_show_lys_tour',
+ 'woocommerce_order_attribution_install_banner_dismissed',
+ 'woocommerce_remote_variant_assignment',
// WC Test helper options.
'wc-admin-test-helper-rest-api-filters',
'wc_admin_helper_feature_values',
diff --git a/plugins/woocommerce/src/Admin/Features/MarketingRecommendations/Init.php b/plugins/woocommerce/src/Admin/Features/MarketingRecommendations/Init.php
index 8c265023eff..7933f07c9e7 100644
--- a/plugins/woocommerce/src/Admin/Features/MarketingRecommendations/Init.php
+++ b/plugins/woocommerce/src/Admin/Features/MarketingRecommendations/Init.php
@@ -37,6 +37,7 @@ class Init extends RemoteSpecsEngine {
*/
public static function delete_specs_transient() {
MarketingRecommendationsDataSourcePoller::get_instance()->delete_specs_transient();
+ MiscRecommendationsDataSourcePoller::get_instance()->delete_specs_transient();
}
/**
@@ -56,6 +57,25 @@ class Init extends RemoteSpecsEngine {
return $specs;
}
+ /**
+ * Get misc recommendations specs or fetch remotely if they don't exist.
+ *
+ * @since 9.5.0
+ */
+ public static function get_misc_recommendations_specs() {
+ if ( 'no' === get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) ) {
+ return array();
+ }
+ $specs = MiscRecommendationsDataSourcePoller::get_instance()->get_specs_from_data_sources();
+
+ // Return empty specs if they don't yet exist.
+ if ( ! is_array( $specs ) ) {
+ return array();
+ }
+
+ return $specs;
+ }
+
/**
* Process specs.
*
@@ -140,6 +160,38 @@ class Init extends RemoteSpecsEngine {
);
}
+ /**
+ * Load misc recommendations from WooCommerce.com
+ *
+ * @since 9.5.0
+ * @return array
+ */
+ public static function get_misc_recommendations(): array {
+ $specs = self::get_misc_recommendations_specs();
+ $results = self::evaluate_specs( $specs );
+
+ $specs_to_return = $results['suggestions'];
+ $specs_to_save = null;
+
+ if ( empty( $specs_to_return ) ) {
+ // When misc_recommendations is empty, replace it with defaults and save for 3 hours.
+ $specs_to_save = array();
+ } elseif ( count( $results['errors'] ) > 0 ) {
+ // When misc_recommendations is not empty but has errors, save it for 3 hours.
+ $specs_to_save = $specs;
+ }
+
+ if ( $specs_to_save ) {
+ MiscRecommendationsDataSourcePoller::get_instance()->set_specs_transient( $specs_to_save, 3 * HOUR_IN_SECONDS );
+ }
+ $errors = $results['errors'];
+ if ( ! empty( $errors ) ) {
+ self::log_errors( $errors );
+ }
+
+ return $specs_to_return;
+ }
+
/**
* Returns whether a plugin is a marketing extension.
*
diff --git a/plugins/woocommerce/src/Admin/Features/MarketingRecommendations/MiscRecommendationsDataSourcePoller.php b/plugins/woocommerce/src/Admin/Features/MarketingRecommendations/MiscRecommendationsDataSourcePoller.php
new file mode 100644
index 00000000000..d360c992072
--- /dev/null
+++ b/plugins/woocommerce/src/Admin/Features/MarketingRecommendations/MiscRecommendationsDataSourcePoller.php
@@ -0,0 +1,66 @@
+ DAY_IN_SECONDS,
+ )
+ );
+ }
+ return self::$instance;
+ }
+
+ /**
+ * Get data sources.
+ *
+ * @return array
+ */
+ public static function get_data_sources() {
+ return array(
+ WC_Helper::get_woocommerce_com_base_url() . 'wp-json/wccom/marketing-tab/misc/recommendations.json',
+ );
+ }
+}
diff --git a/plugins/woocommerce/templates/order/attribution-details.php b/plugins/woocommerce/templates/order/attribution-details.php
index 57dd4e8e601..7b5231f2e8d 100644
--- a/plugins/woocommerce/templates/order/attribution-details.php
+++ b/plugins/woocommerce/templates/order/attribution-details.php
@@ -6,7 +6,7 @@
*
* @see Automattic\WooCommerce\Internal\Orders\OrderAttributionController
* @package WooCommerce\Templates
- * @version 9.0.0
+ * @version 9.5.0
*/
declare( strict_types=1 );
@@ -135,4 +135,6 @@ defined( 'ABSPATH' ) || exit;
+
+