Marketplace: Add "Add to Store" button for free and WordPress.org products #43616 (#43983)

* Marketplace: Add "Add to Store" button for free and WordPress.org products (#43616)

* Marketplace: Install free .org plugins with Add to Store button

* Marketplace: addressed feedback for the the new install free products flow

- Moved notices to the top of the modal
- Updated notice styles slightly
- Updated the CreateOrderSuccessResponse to reflect API changes

* Marketplace: Require the Helper orders API file

* Marketplace: fix linter errors

* Marketplace: form encode when submitting the request

The body is encoded anyways by the WordPress core. However, if I don't
do it here, I can't create a valid signature to be verified by
Woo.com.

I could have just submitted a JSON too, but this seamed easier since
the body is parsed on Woo.com automatically when it's in this form.

* Add changefile(s) from automation for the following project(s): woocommerce

* Marketplace: remove "~" character in imports and use relative paths

* Marketplace: fix margins in the product with the Add to Store button

* Marketplace: Add conditions to hide the button

We hide it if:

- the product is already installed
- user doesn't have the right capability
- if the product is just installed using our flow and there is
no page refresh

* Marketplace: don't show Add to Store button on Themes and on Discover

* Marketplace: fix linting

* Marketplace: hide ratings from the product if "is-small" class exists

* Marketplace: fix linting errors

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Cem Ünalan 2024-01-24 23:02:20 +03:00 committed by GitHub
parent 27f079e9b1
commit 48ebe7b84c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 752 additions and 60 deletions

View File

@ -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 (
<div className="woocommerce-marketplace__content">
<InstallNewProductModal products={ products } />
{ renderContent() }
</div>
);

View File

@ -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,
};

View File

@ -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 (
<Button
variant="tertiary"
href={ documentationUrl }
className="woocommerce-marketplace__header-account-modal-button"
key={ 'docs' }
>
{ __( 'View Docs', 'woocommerce' ) }
</Button>
);
}
return <></>;
}
return (
<Button
variant="tertiary"
onClick={ onClose }
className="woocommerce-marketplace__header-account-modal-button"
key={ 'cancel' }
>
{ __( 'Cancel', 'woocommerce' ) }
</Button>
);
}
function primaryButton(): React.ReactElement {
if ( installStatus === InstallFlowStatus.notConnected ) {
return <ConnectAccountButton variant="primary" key={ 'connect' } />;
}
if (
installStatus === InstallFlowStatus.installedCanActivate ||
installStatus === InstallFlowStatus.activating
) {
return (
<Button
variant="primary"
onClick={ activateClick }
key={ 'activate' }
isBusy={ installStatus === InstallFlowStatus.activating }
disabled={ installStatus === InstallFlowStatus.activating }
>
{ __( 'Activate', 'woocommerce' ) }
</Button>
);
}
if (
installStatus === InstallFlowStatus.activated ||
installStatus === InstallFlowStatus.installedCannotActivate ||
installStatus === InstallFlowStatus.activationFailed
) {
return (
<Button
variant="primary"
href={ WP_ADMIN_PLUGIN_LIST_URL }
className="woocommerce-marketplace__header-account-modal-button"
key={ 'plugin-list' }
>
{ __( 'View in Plugins', 'woocommerce' ) }
</Button>
);
}
return (
<Button
variant="primary"
onClick={ orderAndInstall }
key={ 'install' }
isBusy={ installStatus === InstallFlowStatus.installing }
disabled={
installStatus === InstallFlowStatus.installing ||
installStatus === InstallFlowStatus.installFailed
}
>
{ __( 'Install', 'woocommerce' ) }
</Button>
);
}
/**
* 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 (
<Modal
title={ getTitle() }
onRequestClose={ onClose }
focusOnMount={ true }
className="woocommerce-marketplace__header-account-modal has-size-medium"
style={ { borderRadius: 4 } }
overlayClassName="woocommerce-marketplace__header-account-modal-overlay"
>
{ notice && (
<Notice status={ notice.status } isDismissible={ false }>
{ notice.message }
</Notice>
) }
<p className="woocommerce-marketplace__header-account-modal-text">
{ getDescription() }
</p>
{ product && (
<ProductCard
product={ product }
small={ true }
tracksData={ {
position: 1,
group: 'install-flow',
label: 'install',
} }
/>
) }
<ButtonGroup className="woocommerce-marketplace__header-account-modal-button-group">
{ secondaryButton() }
{ primaryButton() }
</ButtonGroup>
</Modal>
);
}
export default InstallNewProductModal;

View File

@ -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;
}
}
}

View File

@ -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 (
<>
<span className="woocommerce-marketplace__product-card__add-to-store">
<Button variant="secondary" onClick={ openInstallModal }>
{ __( 'Add to Store', 'woocommerce' ) }
</Button>
</span>
</>
);
}
function ProductCardFooter( props: ProductCardFooterProps ) {
const { price, currencySymbol, averageRating, reviewsCount } = props;
return (
<>
<div className="woocommerce-marketplace__product-card__price">
<span className="woocommerce-marketplace__product-card__price-label">
{
// '0' is a free product
price === 0
product.price === 0
? __( 'Free download', 'woocommerce' )
: currencySymbol + price
: currencySymbol + product.price
}
</span>
<span className="woocommerce-marketplace__product-card__price-billing">
{ props.price === 0
{ product.price === 0
? ''
: __( ' annually', 'woocommerce' ) }
</span>
</div>
<div className="woocommerce-marketplace__product-card__rating">
{ averageRating !== null && (
{ product.averageRating !== null && (
<>
<span className="woocommerce-marketplace__product-card__rating-icon">
<Icon icon={ 'star-filled' } size={ 16 } />
</span>
<span className="woocommerce-marketplace__product-card__rating-average">
{ averageRating }
{ product.averageRating }
</span>
<span className="woocommerce-marketplace__product-card__rating-count">
({ reviewsCount })
({ product.reviewsCount })
</span>
</>
) }

View File

@ -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;

View File

@ -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 && (
<div className="woocommerce-marketplace__product-card__price" />
) }
{ ! isLoading && (
<ProductCardFooter
currencySymbol={ currencySymbol }
price={ product.price }
averageRating={ product.averageRating }
reviewsCount={ product.reviewsCount }
/>
{ ! isLoading && props.product && (
<ProductCardFooter product={ props.product } />
) }
</footer>
</div>

View File

@ -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,

View File

@ -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 {

View File

@ -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 (

View File

@ -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 = {

View File

@ -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,

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Adds a faster way to install products from the In-App Marketplace

View File

@ -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;

View File

@ -0,0 +1,107 @@
<?php
/**
* WooCommerce Admin Helper - React admin interface
*
* @package WooCommerce\Admin\Helper
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC_Helper_Orders_API
*
* Pings Woo.com to create an order and pull in the necessary data to start the installation process.
*/
class WC_Helper_Orders_API {
/**
* Loads the class, runs on init
*
* @return void
*/
public static function load() {
add_filter( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
}
/**
* Registers the REST routes for the Marketplace Orders API.
* These endpoints are used by the Marketplace Subscriptions React UI.
*/
public static function register_rest_routes() {
register_rest_route(
'wc/v3',
'/marketplace/create-order',
array(
'methods' => '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();

View File

@ -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';
}
/**