diff --git a/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx b/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx
index fa9d010be15..720327cf2e9 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx
@@ -21,6 +21,7 @@ import {
recordMarketplaceView,
recordLegacyTabView,
} from '../../utils/tracking';
+import InstallNewProductModal from '../install-flow/install-new-product-modal';
export default function Content(): JSX.Element {
const marketplaceContextValue = useContext( MarketplaceContext );
@@ -133,6 +134,7 @@ export default function Content(): JSX.Element {
return (
+
{ renderContent() }
);
diff --git a/plugins/woocommerce-admin/client/marketplace/components/install-flow/create-order.tsx b/plugins/woocommerce-admin/client/marketplace/components/install-flow/create-order.tsx
new file mode 100644
index 00000000000..ec52148e66b
--- /dev/null
+++ b/plugins/woocommerce-admin/client/marketplace/components/install-flow/create-order.tsx
@@ -0,0 +1,45 @@
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+
+type CreateOrderSuccessResponse = {
+ success: true;
+ data: {
+ product_id: number;
+ zip_slug: string;
+ product_type: string;
+ documentation_url: string;
+ };
+};
+
+type CreateOrderErrorResponse = {
+ success: false;
+ data: {
+ code: string;
+ message: string;
+ redirect_location?: string;
+ };
+};
+
+type CreateOrderResponse =
+ | CreateOrderSuccessResponse
+ | CreateOrderErrorResponse;
+
+function createOrder( productId: number ): Promise< CreateOrderResponse > {
+ return apiFetch( {
+ path: '/wc/v3/marketplace/create-order',
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify( { product_id: productId } ),
+ } );
+}
+
+export {
+ createOrder,
+ CreateOrderResponse,
+ CreateOrderSuccessResponse,
+ CreateOrderErrorResponse,
+};
diff --git a/plugins/woocommerce-admin/client/marketplace/components/install-flow/install-new-product-modal.tsx b/plugins/woocommerce-admin/client/marketplace/components/install-flow/install-new-product-modal.tsx
new file mode 100644
index 00000000000..7887f90942a
--- /dev/null
+++ b/plugins/woocommerce-admin/client/marketplace/components/install-flow/install-new-product-modal.tsx
@@ -0,0 +1,404 @@
+/**
+ * External dependencies
+ */
+import { ButtonGroup, Button, Modal, Notice } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { dispatch } from '@wordpress/data';
+import { useState, useEffect, useContext } from '@wordpress/element';
+import { navigateTo, getNewPath, useQuery } from '@woocommerce/navigation';
+import { recordEvent } from '@woocommerce/tracks';
+import { Status } from '@wordpress/notices';
+
+/**
+ * Internal dependencies
+ */
+import ProductCard from '../product-card/product-card';
+import { Product } from '../product-list/types';
+import ConnectAccountButton from '../my-subscriptions/table/actions/connect-account-button';
+import { installingStore } from '../../contexts/install-store';
+import { downloadProduct } from '../../utils/functions';
+import { createOrder } from './create-order';
+import { MARKETPLACE_PATH, WP_ADMIN_PLUGIN_LIST_URL } from '../constants';
+import { getAdminSetting } from '../../../utils/admin-settings';
+import { MarketplaceContext } from '../../contexts/marketplace-context';
+
+enum InstallFlowStatus {
+ 'notConnected',
+ 'notInstalled',
+ 'installing',
+ 'installedCanActivate',
+ 'installedCannotActivate',
+ 'installFailed',
+ 'activating',
+ 'activated',
+ 'activationFailed',
+}
+
+function InstallNewProductModal( props: { products: Product[] } ) {
+ const [ installStatus, setInstallStatus ] = useState< InstallFlowStatus >(
+ InstallFlowStatus.notInstalled
+ );
+ const [ product, setProduct ] = useState< Product >();
+ const [ installedProducts, setInstalledProducts ] = useState< string[] >();
+ const [ activateUrl, setActivateUrl ] = useState< string >();
+ const [ documentationUrl, setDocumentationUrl ] = useState< string >();
+ const [ showModal, setShowModal ] = useState< boolean >( false );
+ const [ notice, setNotice ] = useState< {
+ message: string;
+ status: Status;
+ } >();
+ const { addInstalledProduct } = useContext( MarketplaceContext );
+
+ const query = useQuery();
+
+ // Check if the store is connected to Woo.com. This is run once, when the component is mounted.
+ useEffect( () => {
+ const wccomSettings = getAdminSetting( 'wccomHelper', {} );
+ const isStoreConnected = wccomSettings?.isConnected;
+
+ setInstalledProducts( wccomSettings?.installedProducts );
+
+ if ( isStoreConnected === false ) {
+ setInstallStatus( InstallFlowStatus.notConnected );
+ setNotice( {
+ status: 'warning',
+ message: __(
+ 'In order to install a product, you need to first connect your account.',
+ 'woocommerce'
+ ),
+ } );
+ } else {
+ setInstallStatus( InstallFlowStatus.notInstalled );
+ }
+ }, [] );
+
+ /**
+ * Listen for changes in the query, and show the modal if the installProduct query param is set.
+ * If it's set, try to find the product in the products prop. We need it to be able to
+ * display title, icon and send product ID to Woo.com to create an order.
+ */
+ useEffect( () => {
+ setShowModal( false );
+ if ( ! query.installProduct ) {
+ return;
+ }
+
+ const productId = parseInt( query.installProduct, 10 );
+
+ /**
+ * Try to find the product in the search results. We need to product to be able to
+ * show the title and the icon.
+ */
+ const productToInstall = props.products.find(
+ ( item ) => item.id === productId
+ );
+
+ if ( ! productToInstall ) {
+ return;
+ }
+
+ if ( installedProducts ) {
+ const isInstalled = !! installedProducts.find(
+ ( item ) => item === productToInstall.slug
+ );
+
+ if ( isInstalled ) {
+ return;
+ }
+ }
+
+ setShowModal( true );
+ setProduct( productToInstall );
+ }, [ query, props.products, installedProducts ] );
+
+ function activateClick() {
+ if ( ! activateUrl ) {
+ return;
+ }
+
+ setInstallStatus( InstallFlowStatus.activating );
+
+ recordEvent( 'marketplace_activate_new_product_clicked', {
+ product_id: product ? product.id : 0,
+ } );
+
+ fetch( activateUrl )
+ .then( () => {
+ setInstallStatus( InstallFlowStatus.activated );
+ } )
+ .catch( () => {
+ setInstallStatus( InstallFlowStatus.activationFailed );
+ setNotice( {
+ status: 'error',
+ message: __(
+ 'Activation failed. Please try again from the plugins page.',
+ 'woocommerce'
+ ),
+ } );
+ } );
+ }
+
+ function orderAndInstall() {
+ if ( ! product || ! product.id ) {
+ return;
+ }
+
+ recordEvent( 'marketplace_install_new_product_clicked', {
+ product_id: product.id,
+ } );
+
+ setInstallStatus( InstallFlowStatus.installing );
+
+ createOrder( product.id )
+ .then( ( response ) => {
+ // This narrows the CreateOrderResponse type to CreateOrderSuccessResponse
+ if ( ! response.success ) {
+ throw response;
+ }
+
+ dispatch( installingStore ).startInstalling( product.id );
+ setDocumentationUrl( response.data.documentation_url );
+
+ if ( product.slug ) {
+ addInstalledProduct( product.slug ?? '' );
+ }
+
+ return downloadProduct(
+ response.data.product_type,
+ response.data.zip_slug
+ ).then( ( downloadResponse ) => {
+ dispatch( installingStore ).stopInstalling( product.id );
+
+ if ( downloadResponse.data.activateUrl ) {
+ setActivateUrl( downloadResponse.data.activateUrl );
+
+ setInstallStatus(
+ InstallFlowStatus.installedCanActivate
+ );
+ } else {
+ setInstallStatus(
+ InstallFlowStatus.installedCannotActivate
+ );
+ }
+ } );
+ } )
+ .catch( ( error ) => {
+ /**
+ * apiFetch doesn't return the error code in the error condition.
+ * We'll rely on the data returned by the server.
+ */
+ if ( error.data.redirect_location ) {
+ setNotice( {
+ status: 'warning',
+ message: __(
+ 'We need your address to complete installing this product. We will redirect you to Woo.com checkout. Afterwards, you will be able to install the product.',
+ 'woocommerce'
+ ),
+ } );
+
+ setTimeout( () => {
+ window.location.href = error.data.redirect_location;
+ }, 5000 );
+ } else {
+ setInstallStatus( InstallFlowStatus.installFailed );
+ setNotice( {
+ status: 'error',
+ message:
+ error.data.message ??
+ __(
+ 'An error ocurred. Please try again later.',
+ 'woocommerce'
+ ),
+ } );
+ }
+ } );
+ }
+
+ function onClose() {
+ setInstallStatus( InstallFlowStatus.notInstalled );
+ setNotice( undefined );
+
+ navigateTo( {
+ url: getNewPath(
+ {
+ ...query,
+ install: undefined,
+ installProduct: undefined,
+ },
+ MARKETPLACE_PATH,
+ {}
+ ),
+ } );
+ }
+
+ function getTitle(): string {
+ if ( installStatus === InstallFlowStatus.activated ) {
+ return __( 'You are ready to go!', 'woocommerce' );
+ }
+
+ return __( 'Add to Store', 'woocommerce' );
+ }
+
+ function getDescription(): string {
+ if ( installStatus === InstallFlowStatus.notConnected ) {
+ return '';
+ }
+
+ if (
+ installStatus === InstallFlowStatus.installedCanActivate ||
+ installStatus === InstallFlowStatus.activating
+ ) {
+ return __(
+ 'Extension successfully installed. Would you like to activate it?',
+ 'woocommerce'
+ );
+ }
+
+ if ( installStatus === InstallFlowStatus.installedCannotActivate ) {
+ return __(
+ "Extension successfully installed but we can't activate it at the moment. Please visit the plugins page to see more.",
+ 'woocommerce'
+ );
+ }
+
+ if ( installStatus === InstallFlowStatus.activated ) {
+ return __(
+ 'Keep the momentum going and start setting up your extension.',
+ 'woocommerce'
+ );
+ }
+
+ return __( 'Would you like to install this extension?', 'woocommerce' );
+ }
+
+ function secondaryButton(): React.ReactElement {
+ if ( installStatus === InstallFlowStatus.activated ) {
+ if ( documentationUrl ) {
+ return (
+
+ );
+ }
+
+ return <>>;
+ }
+
+ return (
+
+ );
+ }
+
+ function primaryButton(): React.ReactElement {
+ if ( installStatus === InstallFlowStatus.notConnected ) {
+ return ;
+ }
+
+ if (
+ installStatus === InstallFlowStatus.installedCanActivate ||
+ installStatus === InstallFlowStatus.activating
+ ) {
+ return (
+
+ );
+ }
+
+ if (
+ installStatus === InstallFlowStatus.activated ||
+ installStatus === InstallFlowStatus.installedCannotActivate ||
+ installStatus === InstallFlowStatus.activationFailed
+ ) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ }
+
+ /**
+ * Actually, just checking showModal is enough here. However, checking
+ * for the product narrows the type from "Product | undefined"
+ * to "Product".
+ */
+ if ( ! product || ! showModal ) {
+ return <>>;
+ }
+
+ return (
+
+ { notice && (
+
+ { notice.message }
+
+ ) }
+
+ { getDescription() }
+
+ { product && (
+
+ ) }
+
+ { secondaryButton() }
+ { primaryButton() }
+
+
+ );
+}
+
+export default InstallNewProductModal;
diff --git a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.scss b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.scss
index 5119c8c6d14..48ec92b6574 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.scss
+++ b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.scss
@@ -354,26 +354,36 @@
}
}
.components-notice {
+ color: $gray-900;
padding: $grid-unit-15 $grid-unit-20;
border-left: none;
margin: $grid-unit-20 0;
+ &.is-error {
+ background-color: var(--wp-red-red-0, #fcf0f1);
+ }
+
&.is-warning {
background-color: var(--wp-yellow-yellow-0, #fcf9e8);
+ }
+
+ &.is-error,
+ &.is-warning, {
align-items: start;
+ .components-notice__content {
+ margin: 0;
+ }
+
&::before {
content: '';
/* stylelint-disable-next-line function-url-quotes */
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20Z' stroke='%23614200' stroke-width='1.5'/%3E%3Cpath d='M13 7H11V13H13V7Z' fill='%23614200'/%3E%3Cpath d='M13 15H11V17H13V15Z' fill='%23614200'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
margin-right: $grid-unit-15;
- width: 24px;
+ min-width: 24px;
height: 24px;
}
-
- .components-notice__content {
- margin: 0;
- }
}
}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card-footer.tsx b/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card-footer.tsx
index 7210315051c..6701a458e2e 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card-footer.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card-footer.tsx
@@ -1,46 +1,111 @@
/**
* External dependencies
*/
-import { Icon } from '@wordpress/components';
+import { Button, Icon } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
+import { useContext } from '@wordpress/element';
+import { recordEvent } from '@woocommerce/tracks';
+import { navigateTo, getNewPath } from '@woocommerce/navigation';
+import { useUser } from '@woocommerce/data';
-export interface ProductCardFooterProps {
- price: number | null | undefined;
- currencySymbol: string;
- averageRating: number | null | undefined;
- reviewsCount: number | null | undefined;
-}
+/**
+ * Internal dependencies
+ */
+import { Product } from '../product-list/types';
+import { MarketplaceContext } from '../../contexts/marketplace-context';
+
+function ProductCardFooter( props: { product: Product } ) {
+ const { product } = props;
+ const { user, currentUserCan } = useUser();
+ const { selectedTab, isProductInstalled } =
+ useContext( MarketplaceContext );
+
+ function openInstallModal() {
+ recordEvent( 'marketplace_add_to_store_clicked', {
+ product_id: product.id,
+ } );
+
+ navigateTo( {
+ url: getNewPath( {
+ installProduct: product.id,
+ } ),
+ } );
+ }
+
+ function shouldShowAddToStore( productToCheck: Product ) {
+ if ( ! user || ! productToCheck ) {
+ return false;
+ }
+
+ if ( ! currentUserCan( 'install_plugins' ) ) {
+ return false;
+ }
+
+ if ( ! productToCheck.isInstallable ) {
+ return false;
+ }
+
+ if ( productToCheck.type === 'theme' ) {
+ return false;
+ }
+
+ if ( selectedTab === 'discover' ) {
+ return false;
+ }
+
+ if (
+ productToCheck.slug &&
+ isProductInstalled( productToCheck.slug )
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ // We hardcode this for now while we only display prices in USD.
+ const currencySymbol = '$';
+
+ if ( shouldShowAddToStore( product ) ) {
+ return (
+ <>
+
+
+
+ >
+ );
+ }
-function ProductCardFooter( props: ProductCardFooterProps ) {
- const { price, currencySymbol, averageRating, reviewsCount } = props;
return (
<>
{
// '0' is a free product
- price === 0
+ product.price === 0
? __( 'Free download', 'woocommerce' )
- : currencySymbol + price
+ : currencySymbol + product.price
}
- { props.price === 0
+ { product.price === 0
? ''
: __( ' annually', 'woocommerce' ) }
- { averageRating !== null && (
+ { product.averageRating !== null && (
<>
- { averageRating }
+ { product.averageRating }
- ({ reviewsCount })
+ ({ product.reviewsCount })
>
) }
diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.scss b/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.scss
index 1805c052b19..9b4e5dd520b 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.scss
+++ b/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.scss
@@ -63,6 +63,8 @@
padding: $medium-gap;
.woocommerce-marketplace__product-card__description,
+ .woocommerce-marketplace__product-card__add-to-store,
+ .woocommerce-marketplace__product-card__rating,
.woocommerce-marketplace__product-card__price {
display: none;
}
@@ -95,7 +97,7 @@
display: grid;
gap: $medium-gap;
height: 100%;
- grid-template-rows: auto 1fr 20px;
+ grid-template-rows: auto 1fr 36px;
}
&__header {
@@ -158,6 +160,11 @@
}
}
+ /* Allow the "add to store" button to "punch through" the "whole card clickable" trick: */
+ &__add-to-store {
+ position: relative;
+ }
+
&__vendor {
display: flex;
gap: $grid-unit-05;
@@ -187,7 +194,6 @@
display: flex;
align-items: flex-end;
gap: $grid-unit-05;
- align-self: stretch;
text-decoration: none !important;
color: $gray-900 !important;
font-style: normal;
diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.tsx b/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.tsx
index 3ed5b37d1ba..3d740c1375f 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.tsx
@@ -40,9 +40,6 @@ function ProductCard( props: ProductCardProps ): JSX.Element {
reviewsCount: null,
};
- // We hardcode this for now while we only display prices in USD.
- const currencySymbol = '$';
-
function recordTracksEvent( event: string, data: ExtraProperties ) {
const tracksData = props.tracksData;
@@ -181,13 +178,8 @@ function ProductCard( props: ProductCardProps ): JSX.Element {
{ isLoading && (
) }
- { ! isLoading && (
-
+ { ! isLoading && props.product && (
+
) }
diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-list-content/product-list-content.tsx b/plugins/woocommerce-admin/client/marketplace/components/product-list-content/product-list-content.tsx
index 17ebb2d9211..aef9831625a 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/product-list-content/product-list-content.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/components/product-list-content/product-list-content.tsx
@@ -35,6 +35,8 @@ export default function ProductListContent( props: {
key={ product.id }
type={ props.type }
product={ {
+ id: product.id,
+ slug: product.slug,
title: product.title,
image: product.image,
type: product.type,
@@ -61,6 +63,7 @@ export default function ProductListContent( props: {
averageRating: product.averageRating,
reviewsCount: product.reviewsCount,
description: product.description,
+ isInstallable: product.isInstallable,
} }
tracksData={ {
position: index + 1,
diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts b/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts
index 6cc5463f245..dcf675cd49f 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts
+++ b/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts
@@ -19,10 +19,12 @@ export type SearchAPIProductType = {
vendor_name: string;
vendor_url: string;
icon: string;
+ is_installable: boolean;
};
export interface Product {
id?: number;
+ slug?: string;
position?: number;
title: string;
image: string;
@@ -40,6 +42,7 @@ export interface Product {
group?: string;
searchTerm?: string;
category?: string;
+ isInstallable: boolean;
}
export interface ProductTracksData {
diff --git a/plugins/woocommerce-admin/client/marketplace/contexts/marketplace-context.tsx b/plugins/woocommerce-admin/client/marketplace/contexts/marketplace-context.tsx
index f3d262e3726..c28c3a5c218 100644
--- a/plugins/woocommerce-admin/client/marketplace/contexts/marketplace-context.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/contexts/marketplace-context.tsx
@@ -1,18 +1,21 @@
/**
* External dependencies
*/
-import { useState, createContext } from '@wordpress/element';
+import { useState, useEffect, createContext } from '@wordpress/element';
/**
* Internal dependencies
*/
import { MarketplaceContextType } from './types';
+import { getAdminSetting } from '../../utils/admin-settings';
export const MarketplaceContext = createContext< MarketplaceContextType >( {
isLoading: false,
setIsLoading: () => {},
selectedTab: '',
setSelectedTab: () => {},
+ isProductInstalled: () => false,
+ addInstalledProduct: () => {},
} );
export function MarketplaceContextProvider( props: {
@@ -20,12 +23,33 @@ export function MarketplaceContextProvider( props: {
} ): JSX.Element {
const [ isLoading, setIsLoading ] = useState( true );
const [ selectedTab, setSelectedTab ] = useState( '' );
+ const [ installedPlugins, setInstalledPlugins ] = useState< string[] >(
+ []
+ );
+
+ useEffect( () => {
+ const wccomSettings = getAdminSetting( 'wccomHelper', {} );
+ const installedProductSlugs: string[] =
+ wccomSettings?.installedProducts;
+
+ setInstalledPlugins( installedProductSlugs );
+ }, [] );
+
+ function isProductInstalled( slug: string ): boolean {
+ return installedPlugins.includes( slug );
+ }
+
+ function addInstalledProduct( slug: string ) {
+ setInstalledPlugins( [ ...installedPlugins, slug ] );
+ }
const contextValue = {
isLoading,
setIsLoading,
selectedTab,
setSelectedTab,
+ isProductInstalled,
+ addInstalledProduct,
};
return (
diff --git a/plugins/woocommerce-admin/client/marketplace/contexts/types.ts b/plugins/woocommerce-admin/client/marketplace/contexts/types.ts
index 14930e5edd5..42f06fd32ca 100644
--- a/plugins/woocommerce-admin/client/marketplace/contexts/types.ts
+++ b/plugins/woocommerce-admin/client/marketplace/contexts/types.ts
@@ -13,6 +13,8 @@ export type MarketplaceContextType = {
setIsLoading: ( isLoading: boolean ) => void;
selectedTab: string;
setSelectedTab: ( tab: string ) => void;
+ isProductInstalled: ( slug: string ) => boolean;
+ addInstalledProduct: ( slug: string ) => void;
};
export type SubscriptionsContextType = {
diff --git a/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx b/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx
index 711e514ab86..5233303c2a7 100644
--- a/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx
@@ -125,6 +125,7 @@ async function fetchSearchResults(
( product: SearchAPIProductType ): Product => {
return {
id: product.id,
+ slug: product.slug,
title: product.title,
image: product.image,
type: product.type,
@@ -137,6 +138,7 @@ async function fetchSearchResults(
price: product.raw_price ?? product.price,
averageRating: product.rating ?? null,
reviewsCount: product.reviews_count ?? null,
+ isInstallable: product.is_installable,
};
}
);
@@ -234,6 +236,16 @@ function disconnectProduct( subscription: Subscription ): Promise< void > {
} );
}
+type WpAjaxReponse = {
+ success: boolean;
+ data: WpAjaxResponseData;
+};
+
+type WpAjaxResponseData = {
+ errorMessage?: string;
+ activateUrl?: string;
+};
+
function wpAjax(
action: string,
data: {
@@ -242,7 +254,7 @@ function wpAjax(
theme?: string;
success?: boolean;
}
-): Promise< void > {
+): Promise< WpAjaxReponse > {
return new Promise( ( resolve, reject ) => {
if ( ! window.wp.updates ) {
reject( __( 'Please reload and try again', 'woocommerce' ) );
@@ -251,21 +263,13 @@ function wpAjax(
window.wp.updates.ajax( action, {
...data,
- success: ( response: {
- success?: boolean;
- errorMessage?: string;
- } ) => {
- if ( response.success === false ) {
- reject( {
- success: false,
- data: {
- message: response.errorMessage,
- },
- } );
- }
- resolve();
+ success: ( response: WpAjaxResponseData ) => {
+ resolve( {
+ success: true,
+ data: response,
+ } );
},
- error: ( error: { errorMessage: string } ) => {
+ error: ( error: WpAjaxResponseData ) => {
reject( {
success: false,
data: {
@@ -320,12 +324,19 @@ function getInstallUrl( subscription: Subscription ): Promise< string > {
} );
}
+function downloadProduct( productType: string, zipSlug: string ) {
+ return wpAjax( 'install-' + productType, {
+ // The slug prefix is required for the install to use WCCOM install filters.
+ slug: zipSlug,
+ } );
+}
+
function installProduct( subscription: Subscription ): Promise< void > {
return connectProduct( subscription ).then( () => {
- return wpAjax( 'install-' + subscription.product_type, {
- // The slug prefix is required for the install to use WCCOM install filters.
- slug: 'woocommerce-com-' + subscription.product_slug,
- } )
+ return downloadProduct(
+ subscription.product_type,
+ subscription.zip_slug
+ )
.then( () => {
return activateProduct( subscription );
} )
@@ -338,7 +349,7 @@ function installProduct( subscription: Subscription ): Promise< void > {
} );
}
-function updateProduct( subscription: Subscription ): Promise< void > {
+function updateProduct( subscription: Subscription ): Promise< WpAjaxReponse > {
return wpAjax( 'update-' + subscription.product_type, {
slug: subscription.local.slug,
[ subscription.product_type ]: subscription.local.path,
@@ -386,8 +397,9 @@ const subscriptionToProduct = ( subscription: Subscription ): Product => {
icon: subscription.product_icon,
url: subscription.product_url,
price: -1,
- averageRating: 0,
- reviewsCount: 0,
+ averageRating: null,
+ reviewsCount: null,
+ isInstallable: false,
};
};
@@ -446,6 +458,8 @@ export {
fetchSubscriptions,
refreshSubscriptions,
getInstallUrl,
+ downloadProduct,
+ activateProduct,
installProduct,
updateProduct,
addNotice,
diff --git a/plugins/woocommerce/changelog/43983-feature-marketplace-faster-installs b/plugins/woocommerce/changelog/43983-feature-marketplace-faster-installs
new file mode 100644
index 00000000000..976f81bd8e3
--- /dev/null
+++ b/plugins/woocommerce/changelog/43983-feature-marketplace-faster-installs
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Adds a faster way to install products from the In-App Marketplace
\ No newline at end of file
diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php
index 153beeb88c1..6d58b6eea51 100644
--- a/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php
+++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php
@@ -39,13 +39,23 @@ class WC_Helper_Admin {
$auth_user_data = WC_Helper_Options::get( 'auth_user_data', array() );
$auth_user_email = isset( $auth_user_data['email'] ) ? $auth_user_data['email'] : '';
+ // Get the all installed themes and plugins. Knowing this will help us decide to show Add to Store button on product cards.
+ $installed_products = array_merge( WC_Helper::get_local_plugins(), WC_Helper::get_local_themes() );
+ $installed_products = array_map(
+ function ( $product ) {
+ return $product['slug'];
+ },
+ $installed_products
+ );
+
$settings['wccomHelper'] = array(
- 'isConnected' => WC_Helper::is_site_connected(),
- 'connectURL' => self::get_connection_url(),
- 'userEmail' => $auth_user_email,
- 'userAvatar' => get_avatar_url( $auth_user_email, array( 'size' => '48' ) ),
- 'storeCountry' => wc_get_base_location()['country'],
+ 'isConnected' => WC_Helper::is_site_connected(),
+ 'connectURL' => self::get_connection_url(),
+ 'userEmail' => $auth_user_email,
+ 'userAvatar' => get_avatar_url( $auth_user_email, array( 'size' => '48' ) ),
+ 'storeCountry' => wc_get_base_location()['country'],
'inAppPurchaseURLParams' => WC_Admin_Addons::get_in_app_purchase_url_params(),
+ 'installedProducts' => $installed_products,
);
return $settings;
diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper-orders-api.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-orders-api.php
new file mode 100644
index 00000000000..71097adde77
--- /dev/null
+++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper-orders-api.php
@@ -0,0 +1,107 @@
+ 'POST',
+ 'callback' => array( __CLASS__, 'create_order' ),
+ 'permission_callback' => array( __CLASS__, 'get_permission' ),
+ 'args' => array(
+ 'product_id' => array(
+ 'required' => true,
+ 'validate_callback' => function( $argument ) {
+ return is_int( $argument );
+ },
+ ),
+ ),
+ )
+ );
+ }
+
+ /**
+ * The Extensions page can only be accessed by users with the manage_woocommerce
+ * capability. So the API mimics that behavior.
+ *
+ * @return bool
+ */
+ public static function get_permission() {
+ return WC_Helper_Subscriptions_API::get_permission();
+ }
+
+ /**
+ * Core function to create an order on Woo.com. Pings the API and catches the exceptions if any.
+ *
+ * @param WP_REST_Request $request Request object.
+ *
+ * @return WP_REST_Response
+ */
+ public static function create_order( $request ) {
+ if ( ! current_user_can( 'install_plugins' ) ) {
+ return new \WP_REST_Response(
+ array(
+ 'message' => __( 'You do not have permission to install plugins.', 'woocommerce' ),
+ ),
+ 403
+ );
+ }
+
+ try {
+ $response = WC_Helper_API::post(
+ 'create-order',
+ array(
+ 'authenticated' => true,
+ 'body' => http_build_query(
+ array(
+ 'product_id' => $request['product_id'],
+ ),
+ ),
+ )
+ );
+
+ return new \WP_REST_Response(
+ json_decode( wp_remote_retrieve_body( $response ), true ),
+ wp_remote_retrieve_response_code( $response )
+ );
+ } catch ( Exception $e ) {
+ return new \WP_REST_Response(
+ array(
+ 'message' => __( 'Could not start the installation process. Reason: ', 'woocommerce' ) . $e->getMessage(),
+ 'code' => 'could-not-install',
+ ),
+ 500
+ );
+ }
+ }
+}
+
+WC_Helper_Orders_API::load();
diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper.php
index 36395a9b662..f93ab209cd7 100644
--- a/plugins/woocommerce/includes/admin/helper/class-wc-helper.php
+++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper.php
@@ -61,6 +61,7 @@ class WC_Helper {
include_once dirname( __FILE__ ) . '/class-wc-helper-compat.php';
include_once dirname( __FILE__ ) . '/class-wc-helper-admin.php';
include_once dirname( __FILE__ ) . '/class-wc-helper-subscriptions-api.php';
+ include_once dirname( __FILE__ ) . '/class-wc-helper-orders-api.php';
}
/**