Marketplace: My Subscriptions feature branch (#40249)
This commit is contained in:
commit
99d063095f
|
@ -0,0 +1,46 @@
|
|||
# Marketplace
|
||||
|
||||
This folder contains the components used in the Marketplace page found in `WooCommerce > Extensions`.
|
||||
|
||||
The page contains two parts, the Woo.com marketplace and a list of products the user purchased.
|
||||
|
||||
## Marketplace Tabs
|
||||
|
||||
- **Discover**: A curated list of extensions and themes.
|
||||
- **Browse**: All extensions.
|
||||
- **Themes**: All themes.
|
||||
- **Search**: Search results.
|
||||
|
||||
### Marketplace API
|
||||
|
||||
The data for the Discover section is fetched from the `/wc/v3/marketplace/featured` endpoint. This behaves as a proxy to fetch and cache the content from the `woocommerce.com/wp-json/wccom-extensions` endpoint.
|
||||
|
||||
Themes, extensions, and search results are fetched directly from Woo.com.
|
||||
|
||||
## My Subscriptions
|
||||
|
||||
This tab contains the list of all the extensions and themes the WooCommerce merchant has purchased from the Woo.com Marketplace.
|
||||
|
||||
The merchant needs to connect the site to their Woo.com account to view this list and install, update, and enable the products.
|
||||
|
||||
If a subscription is expired, the merchant will be prompted to renew it.
|
||||
|
||||
### My Subscriptions API
|
||||
|
||||
My Subscriptions data uses `/wc/v3/marketplace/subscriptions` API endpoints to list, install, connect, and update products.
|
||||
|
||||
You can find a full list of endpoints in the [subscriptions API source code](/plugins/woocommerce/includes/admin/helper/class-wc-helper-subscriptions-api.php).
|
||||
|
||||
## Project Structure
|
||||
|
||||
The project is structured as follows:
|
||||
|
||||
- **components**: The React components used in the Marketplace page.
|
||||
- **contexts**: React contexts.
|
||||
- **utils**: Functions used to interact with APIs.
|
||||
- **stylesheets**: Shared stylesheets.
|
||||
- **assets**: Images.
|
||||
|
||||
## Development
|
||||
|
||||
This feature is part of WooCommerce Admin and uses the [same development environment.](/plugins/woocommerce-admin/README.md)
|
|
@ -0,0 +1,7 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="alert">
|
||||
<path id="Vector" 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="#CC1818" stroke-width="1.5"/>
|
||||
<path id="Vector_2" d="M13 7H11V13H13V7Z" fill="#CC1818"/>
|
||||
<path id="Vector_3" d="M13 15H11V17H13V15Z" fill="#CC1818"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 417 B |
|
@ -1,6 +0,0 @@
|
|||
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M70.8227 17.1683C73.8744 17.1683 77.2598 18.623 80 20.1745V13.4403C80 9.31419 77.664 7 73.5407 7H6.45562C2.33604 6.99628 0 9.31047 0 13.4366V47.3717C0 51.4978 2.33604 53.812 6.45933 53.812H29.5824C28.1214 56.4871 26.8014 59.7091 26.8014 62.626C26.8014 66.6553 28.1214 69.3676 30.7207 71.3916C32.9455 73.1254 35.9898 73.9662 39.9981 73.9662C44.0065 73.9662 47.0508 73.1254 49.2756 71.3916C51.8749 69.3676 53.1949 66.659 53.1949 62.626C53.1949 59.7053 51.8749 56.4871 50.4139 53.812H73.537C77.6603 53.812 79.9963 51.4978 79.9963 47.3717V40.645C77.2561 42.1964 73.8744 43.6512 70.819 43.6512C66.8032 43.6512 64.1001 42.3266 62.083 39.7185C60.355 37.4862 59.517 34.4316 59.517 30.4097C59.517 26.3878 60.355 23.3332 62.083 21.1009C64.1001 18.4928 66.7995 17.1683 70.819 17.1683H70.8227Z" fill="#F0F0F0"/>
|
||||
<path d="M39.9865 20.4834H6.6145V23.8321H39.9865V20.4834Z" fill="#DDDDDD"/>
|
||||
<path d="M39.9865 13.7158H6.6145V17.0645H39.9865V13.7158Z" fill="#DDDDDD"/>
|
||||
<path d="M26.6377 27.1804H6.6145V30.5291H26.6377V27.1804Z" fill="#DDDDDD"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.1 KiB |
|
@ -1,17 +0,0 @@
|
|||
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2293_37324)">
|
||||
<path d="M6.45466 7C2.33301 7 0 9.31262 0 13.4396V73.9767H59.8473L59.9917 13.4396C59.9917 9.31262 57.6587 7 53.537 7L6.45466 7Z" fill="#F0F0F0"/>
|
||||
<path d="M80.0001 27.0774H43.3274V63.8897H80.0001V27.0774Z" fill="#DDDDDD"/>
|
||||
<path d="M36.6616 27.0774H6.66577V43.8086H36.6616V27.0774Z" fill="#DDDDDD"/>
|
||||
<path d="M36.6616 47.5229H6.66943V50.4974H36.6616V47.5229Z" fill="#DDDDDD"/>
|
||||
<path d="M23.3301 54.219H6.66577V57.1934H23.3301V54.219Z" fill="#DDDDDD"/>
|
||||
<path d="M60.1989 51.7429L59.3768 49.5232H53.785L52.9629 51.7429H49.8152L54.8404 38.6479H58.3214L63.3466 51.7429H60.1989ZM56.5809 41.4365L54.5479 47.073H58.614L56.5809 41.4365Z" fill="white"/>
|
||||
<path d="M70.179 51.7429V50.7427C69.5347 51.5272 68.42 51.9771 67.1869 51.9771C65.6834 51.9771 63.9207 50.9584 63.9207 48.8354C63.9207 46.5971 65.6834 45.7754 67.1869 45.7754C68.4608 45.7754 69.5532 46.1881 70.179 46.9317V45.7345C70.179 44.7716 69.3569 44.1432 68.1053 44.1432C67.1091 44.1432 66.1722 44.5373 65.3871 45.2623L64.4095 43.5149C65.5649 42.4738 67.0499 42.0239 68.5348 42.0239C70.7049 42.0239 72.6787 42.8865 72.6787 45.6156V51.7429H70.1753H70.179ZM70.179 49.4637V48.2888C69.768 47.7386 68.9866 47.4448 68.1867 47.4448C67.2091 47.4448 66.4055 47.9765 66.4055 48.88C66.4055 49.7835 67.2091 50.2928 68.1867 50.2928C68.9866 50.2928 69.768 50.0177 70.179 49.4674V49.4637Z" fill="white"/>
|
||||
<path d="M0 13.4396V16.9829H59.9917V13.4396C59.9917 9.31262 57.6587 7 53.537 7H6.45466C2.33301 7 0 9.31262 0 13.4396Z" fill="#DDDDDD"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2293_37324">
|
||||
<rect width="80" height="66.9767" fill="white" transform="translate(0 7)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.7 KiB |
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.0002 2.25V4C16.4185 4 20.0003 7.58172 20.0003 12C20.0003 15.7277 17.4507 18.8599 14.0003 19.748V19.7479C13.8301 19.7917 13.6577 19.8301 13.4833 19.8628C13.3371 19.8905 13.1906 19.914 13.0442 19.9334L13.0441 19.9325C12.7024 19.977 12.3539 20 12 20C11.646 20 11.2975 19.977 10.9556 19.9324L10.9556 19.9327C10.9148 19.9273 10.874 19.9216 10.8333 19.9155C9.61036 19.7369 8.47722 19.2815 7.49949 18.615C7.37365 18.5295 7.25011 18.4404 7.12904 18.3477L7.12954 18.347C6.57546 17.9212 6.07872 17.4245 5.65291 16.8704L5.65171 16.8713C5.48551 16.6545 5.32941 16.4276 5.18438 16.1911C4.69453 15.3962 4.33985 14.5091 4.15239 13.5619C4.11788 13.3895 4.08925 13.2168 4.06641 13.0441L4.06749 13.0439C4.02297 12.7022 4 12.3538 4 12C4 11.646 4.02299 11.2974 4.06756 10.9555L4.06708 10.9555C4.07694 10.8811 4.08784 10.807 4.09976 10.733C4.281 9.594 4.70266 8.53507 5.31109 7.60992C5.41864 7.44572 5.53236 7.28525 5.6521 7.12891L5.65294 7.12955C5.65535 7.12641 5.65777 7.12327 5.66018 7.12014L6.86037 8.02028C6.74015 8.17531 6.62679 8.33593 6.52074 8.50168C6.06043 9.22439 5.7454 10.0333 5.59639 10.8785C5.53303 11.2428 5.5 11.6176 5.5 12C5.5 12.4751 5.55096 12.9382 5.64772 13.3843C5.72894 13.7544 5.84397 14.1233 5.99475 14.4873C6.11754 14.7837 6.25938 15.0657 6.41813 15.3325C6.91672 16.1658 7.59663 16.8782 8.40335 17.4151C9.17682 17.9275 10.054 18.271 10.9714 18.4191C11.3064 18.4723 11.65 18.5 12 18.5C12.4506 18.5 12.8904 18.4542 13.3151 18.3669C13.5263 18.323 13.7372 18.2681 13.947 18.2019L13.9474 18.2032C16.5859 17.3758 18.5 14.9114 18.5 12C18.5 8.41023 15.59 5.50013 12.0002 5.5V7.25L9.00024 4.75L12.0002 2.25Z" fill="#007CBA"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -6,11 +6,13 @@ import { chevronDown, chevronUp, Icon } from '@wordpress/icons';
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { navigateTo, getNewPath } from '@woocommerce/navigation';
|
||||
import classNames from 'classnames';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Category } from './types';
|
||||
import { ProductType } from '../product-list/types';
|
||||
|
||||
function DropdownContent( props: {
|
||||
readonly categories: Category[];
|
||||
|
@ -71,16 +73,28 @@ type CategoryDropdownProps = {
|
|||
contentClassName?: string;
|
||||
arrowIconSize?: number;
|
||||
selected?: Category;
|
||||
type?: ProductType;
|
||||
};
|
||||
|
||||
export default function CategoryDropdown(
|
||||
props: CategoryDropdownProps
|
||||
): JSX.Element {
|
||||
function dropDownTracksEvent() {
|
||||
recordEvent( 'marketplace_category_dropdown_opened', {
|
||||
type: props.type,
|
||||
} );
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
renderToggle={ ( { isOpen, onToggle } ) => (
|
||||
<button
|
||||
onClick={ onToggle }
|
||||
onClick={ () => {
|
||||
if ( ! isOpen ) {
|
||||
dropDownTracksEvent();
|
||||
}
|
||||
onToggle();
|
||||
} }
|
||||
className={ props.buttonClassName }
|
||||
aria-label={ __(
|
||||
'Toggle category dropdown',
|
||||
|
|
|
@ -145,6 +145,7 @@ export default function CategorySelector(
|
|||
<li className="woocommerce-marketplace__category-item">
|
||||
{ dropdownItems.length > 0 && (
|
||||
<CategoryDropdown
|
||||
type={ props.type }
|
||||
label={ __( 'More', 'woocommerce' ) }
|
||||
categories={ dropdownItems }
|
||||
buttonClassName={ classNames(
|
||||
|
@ -164,6 +165,7 @@ export default function CategorySelector(
|
|||
|
||||
<div className="woocommerce-marketplace__category-selector--full-width">
|
||||
<CategoryDropdown
|
||||
type={ props.type }
|
||||
label={ mobileCategoryDropdownLabel() }
|
||||
categories={ visibleItems.concat( dropdownItems ) }
|
||||
buttonClassName="woocommerce-marketplace__category-dropdown-button"
|
||||
|
|
|
@ -7,3 +7,7 @@ export const MARKETPLACE_CATEGORY_API_PATH =
|
|||
'/wp-json/wccom-extensions/1.0/categories';
|
||||
export const MARKETPLACE_ITEMS_PER_PAGE = 60;
|
||||
export const MARKETPLACE_SEARCH_RESULTS_PER_PAGE = 8;
|
||||
export const MARKETPLACE_CART_PATH = MARKETPLACE_HOST + '/cart/';
|
||||
export const MARKETPLACE_COLLABORATION_PATH =
|
||||
MARKETPLACE_HOST +
|
||||
'/document/managing-woocommerce-com-subscriptions/#transfer-a-woocommerce-com-subscription';
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
margin: auto;
|
||||
max-width: $content-max-width;
|
||||
padding: $grid-unit-30 $grid-unit-20;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $breakpoint-medium) {
|
||||
|
|
|
@ -13,8 +13,10 @@ import { getAdminSetting } from '../../../utils/admin-settings';
|
|||
import Discover from '../discover/discover';
|
||||
import Products from '../products/products';
|
||||
import SearchResults from '../search-results/search-results';
|
||||
import MySubscriptions from '../my-subscriptions/my-subscriptions';
|
||||
import { MarketplaceContext } from '../../contexts/marketplace-context';
|
||||
import { fetchSearchResults } from '../../utils/functions';
|
||||
import { SubscriptionsContextProvider } from '../../contexts/subscriptions-context';
|
||||
import {
|
||||
recordMarketplaceView,
|
||||
recordLegacyTabView,
|
||||
|
@ -118,6 +120,12 @@ export default function Content(): JSX.Element {
|
|||
);
|
||||
case 'discover':
|
||||
return <Discover />;
|
||||
case 'my-subscriptions':
|
||||
return (
|
||||
<SubscriptionsContextProvider>
|
||||
<MySubscriptions />
|
||||
</SubscriptionsContextProvider>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { useContext, useEffect, useState } from '@wordpress/element';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -20,6 +21,19 @@ export default function Discover(): JSX.Element | null {
|
|||
const marketplaceContextValue = useContext( MarketplaceContext );
|
||||
const { isLoading, setIsLoading } = marketplaceContextValue;
|
||||
|
||||
function recordTracksEvent( products: ProductGroup[] ) {
|
||||
const product_ids = products
|
||||
.flatMap( ( group ) => group.items )
|
||||
.map( ( product ) => {
|
||||
return product.id;
|
||||
} );
|
||||
|
||||
recordEvent( 'marketplace_discover_viewed', {
|
||||
view: 'discover',
|
||||
product_ids,
|
||||
} );
|
||||
}
|
||||
|
||||
// Get the content for this screen
|
||||
useEffect( () => {
|
||||
setIsLoading( true );
|
||||
|
@ -35,6 +49,7 @@ export default function Discover(): JSX.Element | null {
|
|||
)
|
||||
.then( ( products: Array< ProductGroup > ) => {
|
||||
setProductGroups( products );
|
||||
recordTracksEvent( products );
|
||||
} )
|
||||
.finally( () => {
|
||||
setIsLoading( false );
|
||||
|
|
|
@ -31,6 +31,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketplace--my-subscriptions {
|
||||
@media (width <= $breakpoint-medium) {
|
||||
.woocommerce-marketplace__search {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__header-title {
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import classnames from 'classnames';
|
||||
import classNames from 'classnames';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -52,7 +52,7 @@ export default function LikertScale( props: LikertScaleProps ): JSX.Element {
|
|||
},
|
||||
];
|
||||
|
||||
const classes = classnames( 'woocommerce-marketplace__likert-scale', {
|
||||
const classes = classNames( 'woocommerce-marketplace__likert-scale', {
|
||||
'validation-failed': validationFailed,
|
||||
} );
|
||||
|
||||
|
|
|
@ -0,0 +1,340 @@
|
|||
@import '@wordpress/base-styles/_colors.native.scss';
|
||||
@import '../../stylesheets/_variables.scss';
|
||||
|
||||
@mixin content-width {
|
||||
max-width: calc(100vw - (2 * #{$grid-unit-20}));
|
||||
@media screen and (min-width: 783px) {
|
||||
max-width: calc(100vw - (2 * #{$grid-unit-40}) - $admin-menu-width-collapsed);
|
||||
}
|
||||
@media screen and (min-width: 960px) {
|
||||
max-width: calc(100vw - (2 * #{$grid-unit-40}) - $admin-menu-width);
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: stretch;
|
||||
@include content-width;
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions__header-content,
|
||||
.woocommerce-marketplace__my-subscriptions__header-refresh {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions__header-content {
|
||||
order: 2;
|
||||
}
|
||||
.woocommerce-marketplace__my-subscriptions__header-refresh {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $breakpoint-medium) {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions__header-content,
|
||||
.woocommerce-marketplace__my-subscriptions__header-refresh {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions__header-content {
|
||||
order: 1;
|
||||
}
|
||||
.woocommerce-marketplace__my-subscriptions__header-refresh {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__refresh-subscriptions {
|
||||
text-decoration: none;
|
||||
color: #007cba;
|
||||
.woocommerce-marketplace__refresh-subscriptions-icon {
|
||||
margin-right: $grid-unit-05;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions__available {
|
||||
margin-top: $grid-unit-50;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions__heading {
|
||||
font-size: 20px;
|
||||
color: $gray-900;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__notice--error {
|
||||
&:last-child {
|
||||
margin-bottom: $grid-unit-50;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions__table-description {
|
||||
font-size: 13px;
|
||||
margin: 1em 0;
|
||||
line-height: 20px;
|
||||
color: $gray-700;
|
||||
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: #007cba;
|
||||
margin-left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions__table-wrapper {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions__table {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
margin-top: $grid-unit-30;
|
||||
color: $gray-900;
|
||||
@include content-width;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions__product {
|
||||
$product-icon-size: 40px;
|
||||
min-width: 400px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&-name {
|
||||
margin-left: $grid-unit-15;
|
||||
line-height: 18px;
|
||||
font-weight: 600;
|
||||
color: $gray-900;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&-icon img {
|
||||
border-radius: 4px;
|
||||
width: $product-icon-size;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
width: $product-icon-size;
|
||||
height: $product-icon-size;
|
||||
|
||||
svg {
|
||||
border-radius: 4px;
|
||||
padding: $grid-unit-10;
|
||||
fill: $gray-600;
|
||||
background-color: $gray-200;
|
||||
width: $product-icon-size;
|
||||
height: $product-icon-size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-table__item {
|
||||
.woocommerce-marketplace__my-subscriptions__product-name {
|
||||
&:active,
|
||||
&:hover,
|
||||
&:visited {
|
||||
color: $gray-900;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions__product-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 2px;
|
||||
border: none;
|
||||
padding: 2px $grid-unit-10;
|
||||
margin-left: $grid-unit-15;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
|
||||
&--error {
|
||||
color: var(--wp-red-red-70, #8a2424);
|
||||
background: var(--wp-red-red-0, #fcf0f1);
|
||||
|
||||
& > svg {
|
||||
margin-right: 2px;
|
||||
fill: var(--wp-red-red-70, #8a2424);
|
||||
}
|
||||
}
|
||||
|
||||
&--warning {
|
||||
color: var(--wp-yellow-yellow-70, #614200);
|
||||
background: var(--wp-yellow-yellow-0, #fcf9e8);
|
||||
|
||||
& > svg {
|
||||
margin-right: 2px;
|
||||
color: var(--wp-yellow-yellow-70, #614200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions__popover {
|
||||
top: -8px !important;
|
||||
.components-popover__content {
|
||||
border: none;
|
||||
width: 300px;
|
||||
border-radius: 2px;
|
||||
padding: $grid-unit-15;
|
||||
color: $gray-900;
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.components-base-control.woocommerce-marketplace__my-subscriptions__activation {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions-version {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
.woocommerce-marketplace__my-subscriptions__table__header--version > span {
|
||||
display: inline-block;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions {
|
||||
.woocommerce-table__empty-item,
|
||||
.woocommerce-table__header,
|
||||
.woocommerce-table__item {
|
||||
padding: $grid-unit-10 $grid-unit-30;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.woocommerce-table__table tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.woocommerce-table.is-empty {
|
||||
background: none;
|
||||
border: 1px solid var(--gutenberg-gray-100, #f0f0f0);
|
||||
flex-direction: column;
|
||||
gap: $grid-unit-15;
|
||||
margin-top: $grid-unit-30;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions .components-button.is-link {
|
||||
text-decoration: none;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions
|
||||
.components-button.is-secondary:hover:not(:disabled) {
|
||||
color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9));
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions--connect {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 495px;
|
||||
padding: $grid-unit-80 $grid-unit-40;
|
||||
margin: 0 auto;
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions__icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAiIGhlaWdodD0iODAiIHZpZXdCb3g9IjAgMCA4MCA4MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzI5NzBfMTA0MzcpIj4KPHJlY3Qgd2lkdGg9IjgwIiBoZWlnaHQ9IjgwIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNzQuMTMyNSAwTDY4LjQ0ODEgMy4yOTQ4NEw2Mi43NTc5IDBMNTcuMDczNCAzLjI5NDg0TDUxLjM4MzIgMEw0NS42OTMxIDMuMjk0ODRMNDAuMDA4NiAwTDM0LjMxODQgMy4yOTQ4NEwyOC42MzM5IDBMMjIuOTQzNyAzLjI5NDg0TDE3LjI1MzYgMEwxMS41NjM0IDMuMjk0ODRMNS44NzMxOCAwTDAgMy40MDIwNlY3Ni41OTc5TDUuODY3NDcgODBMMTEuNTU3NiA3Ni43MDFMMTcuMjQyMSA4MEwyMi45MzIzIDc2LjcwMUwyOC42MTY4IDgwTDM0LjMwNjkgNzYuNzAxTDM5Ljk5NzEgODBMNDUuNjgxNiA3Ni43MDFMNTEuMzcxOCA4MEw1Ny4wNTYzIDc2LjcwMUw2Mi43NDY1IDgwTDY4LjQzNjYgNzYuNzAxTDc0LjEzMjUgODBMODAgNzYuNTk3OVYzLjQwMjA2TDc0LjEzMjUgMFoiIGZpbGw9IiNGMEYwRjAiLz4KPHBhdGggZD0iTTcxLjE5OTIgMTQuNzk5Nkg4LjM5OTE3VjE4Ljc5OTZINzEuMTk5MlYxNC43OTk2WiIgZmlsbD0iI0RERERERCIvPgo8cGF0aCBkPSJNNzEuMTk5MiAyNS4xOTk1SDguMzk5MTdWMjkuMTk5NUg3MS4xOTkyVjI1LjE5OTVaIiBmaWxsPSIjREREREREIi8+CjxwYXRoIGQ9Ik03MS4xOTkyIDM1LjU5OTZIOC4zOTkxN1YzOS41OTk2SDcxLjE5OTJWMzUuNTk5NloiIGZpbGw9IiNEREREREQiLz4KPHBhdGggZD0iTTY4IDUzLjYwMDFINDkuMkM0Ny40MzI3IDUzLjYwMDEgNDYgNTUuMDMyOCA0NiA1Ni44MDAxVjYwLjgwMDFDNDYgNjIuNTY3NCA0Ny40MzI3IDY0LjAwMDEgNDkuMiA2NC4wMDAxSDY4QzY5Ljc2NzMgNjQuMDAwMSA3MS4yIDYyLjU2NzQgNzEuMiA2MC44MDAxVjU2LjgwMDFDNzEuMiA1NS4wMzI4IDY5Ljc2NzMgNTMuNjAwMSA2OCA1My42MDAxWiIgZmlsbD0iI0RERERERCIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzI5NzBfMTA0MzciPgo8cmVjdCB3aWR0aD0iODAiIGhlaWdodD0iODAiIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==);
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions__header {
|
||||
margin-top: $grid-unit-20;
|
||||
margin-bottom: $grid-unit-10;
|
||||
font-size: $default-font-size;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions__description {
|
||||
text-align: center;
|
||||
font-size: $default-font-size;
|
||||
line-height: 20px;
|
||||
font-weight: 400;
|
||||
color: var(--gutenberg-gray-700);
|
||||
margin-top: 0;
|
||||
margin-bottom: $grid-unit-30;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions__table__header--actions {
|
||||
text-align: right;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions__actions {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
|
||||
.components-button {
|
||||
margin-right: $grid-unit-10;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #007cba;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__my-subscriptions__notices {
|
||||
.components-notice {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 6px 0 rgba($gray-100, 0.05);
|
||||
border: 1px solid var(--gutenberg-gray-100, #f0f0f0);
|
||||
padding-right: $grid-unit-15;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background-color: var(--wp-admin-theme-color, #007cba);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&.is-error::before {
|
||||
background-color: $alert-red;
|
||||
}
|
||||
|
||||
.components-notice__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $grid-unit-15;
|
||||
}
|
||||
.components-notice__dismiss.has-icon {
|
||||
width: 24px;
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
align-self: center;
|
||||
padding: $grid-unit-05;
|
||||
> svg {
|
||||
fill: $gray-900;
|
||||
}
|
||||
}
|
||||
}
|
||||
.components-notice__action.components-button.is-link {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Button } from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { createInterpolateElement, useContext } from '@wordpress/element';
|
||||
import { Icon, external } from '@wordpress/icons';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getAdminSetting } from '../../../utils/admin-settings';
|
||||
import { SubscriptionsContext } from '../../contexts/subscriptions-context';
|
||||
import './my-subscriptions.scss';
|
||||
import {
|
||||
AvailableSubscriptionsTable,
|
||||
InstalledSubscriptionsTable,
|
||||
} from './table/table';
|
||||
import {
|
||||
availableSubscriptionRow,
|
||||
installedSubscriptionRow,
|
||||
} from './table/table-rows';
|
||||
import { Subscription } from './types';
|
||||
import { RefreshButton } from './table/actions/refresh-button';
|
||||
import Notices from './notices';
|
||||
|
||||
export default function MySubscriptions(): JSX.Element {
|
||||
const { subscriptions, isLoading } = useContext( SubscriptionsContext );
|
||||
const wccomSettings = getAdminSetting( 'wccomHelper', {} );
|
||||
|
||||
const installedTableDescription = createInterpolateElement(
|
||||
__(
|
||||
'Woo.com extensions and themes installed on this store. To see all your subscriptions go to <a>your account<custom_icon /></a> on Woo.com.',
|
||||
'woocommerce'
|
||||
),
|
||||
{
|
||||
a: (
|
||||
<a
|
||||
href="https://woo.com/my-account/my-subscriptions"
|
||||
target="_blank"
|
||||
rel="nofollow noopener noreferrer"
|
||||
>
|
||||
your account
|
||||
</a>
|
||||
),
|
||||
custom_icon: <Icon icon={ external } size={ 12 } />,
|
||||
}
|
||||
);
|
||||
|
||||
const subscriptionsInstalled: Array< Subscription > = subscriptions.filter(
|
||||
( subscription: Subscription ) => subscription.subscription_installed
|
||||
);
|
||||
|
||||
const subscriptionsAvailable: Array< Subscription > = subscriptions.filter(
|
||||
( subscription: Subscription ) => ! subscription.subscription_installed
|
||||
);
|
||||
|
||||
if ( ! wccomSettings?.isConnected ) {
|
||||
return (
|
||||
<div className="woocommerce-marketplace__my-subscriptions--connect">
|
||||
<div className="woocommerce-marketplace__my-subscriptions__icon" />
|
||||
<h2 className="woocommerce-marketplace__my-subscriptions__header">
|
||||
{ __( 'Manage your subscriptions', 'woocommerce' ) }
|
||||
</h2>
|
||||
<p className="woocommerce-marketplace__my-subscriptions__description">
|
||||
{ __(
|
||||
'Connect your account to get updates, manage your subscriptions, and get seamless support. Once connected, your Woo.com subscriptions will appear here.',
|
||||
'woocommerce'
|
||||
) }
|
||||
</p>
|
||||
<Button href={ wccomSettings?.connectURL } variant="primary">
|
||||
{ __( 'Connect Account', 'woocommerce' ) }
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="woocommerce-marketplace__my-subscriptions">
|
||||
<section className="woocommerce-marketplace__my-subscriptions__notices">
|
||||
<Notices />
|
||||
</section>
|
||||
<section className="woocommerce-marketplace__my-subscriptions-section woocommerce-marketplace__my-subscriptions__installed">
|
||||
<header className="woocommerce-marketplace__my-subscriptions__header">
|
||||
<div className="woocommerce-marketplace__my-subscriptions__header-content">
|
||||
<h2 className="woocommerce-marketplace__my-subscriptions__heading">
|
||||
{ __( 'Installed on this store', 'woocommerce' ) }
|
||||
</h2>
|
||||
<p className="woocommerce-marketplace__my-subscriptions__table-description">
|
||||
{ installedTableDescription }
|
||||
</p>
|
||||
</div>
|
||||
<div className="woocommerce-marketplace__my-subscriptions__header-refresh">
|
||||
<RefreshButton />
|
||||
</div>
|
||||
</header>
|
||||
<div className="woocommerce-marketplace__my-subscriptions__table-wrapper">
|
||||
<InstalledSubscriptionsTable
|
||||
isLoading={ isLoading }
|
||||
rows={ subscriptionsInstalled.map( ( item ) => {
|
||||
return installedSubscriptionRow( item );
|
||||
} ) }
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
{ subscriptionsAvailable.length > 0 && (
|
||||
<section className="woocommerce-marketplace__my-subscriptions-section woocommerce-marketplace__my-subscriptions__available">
|
||||
<h2 className="woocommerce-marketplace__my-subscriptions__heading">
|
||||
{ __( 'Available to use', 'woocommerce' ) }
|
||||
</h2>
|
||||
<p className="woocommerce-marketplace__my-subscriptions__table-description">
|
||||
{ __(
|
||||
"Woo.com subscriptions you haven't used yet.",
|
||||
'woocommerce'
|
||||
) }
|
||||
</p>
|
||||
<div className="woocommerce-marketplace__my-subscriptions__table-wrapper">
|
||||
<AvailableSubscriptionsTable
|
||||
isLoading={ isLoading }
|
||||
rows={ subscriptionsAvailable.map( ( item ) => {
|
||||
return availableSubscriptionRow( item );
|
||||
} ) }
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Notice } from '@wordpress/components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Alert from '../../assets/images/alert.svg';
|
||||
import { Notice as NoticeType } from '../../contexts/types';
|
||||
import { noticeStore } from '../../contexts/notice-store';
|
||||
import { removeNotice } from '../../utils/functions';
|
||||
|
||||
export default function Notices() {
|
||||
const notices: NoticeType[] = useSelect(
|
||||
( select ) => select( noticeStore ).notices(),
|
||||
[]
|
||||
);
|
||||
|
||||
const actions = ( notice: NoticeType ) => {
|
||||
if ( ! notice.options?.actions ) {
|
||||
return [];
|
||||
}
|
||||
return notice.options?.actions.map( ( action ) => {
|
||||
return {
|
||||
...action,
|
||||
variant: 'link',
|
||||
className: 'is-link',
|
||||
};
|
||||
} );
|
||||
};
|
||||
|
||||
const errorNotices = [];
|
||||
for ( const notice of notices ) {
|
||||
errorNotices.push(
|
||||
<Notice
|
||||
className="woocommerce-marketplace__notice--error"
|
||||
status={ notice.status }
|
||||
onRemove={ () => removeNotice( notice.productKey ) }
|
||||
key={ notice.productKey }
|
||||
actions={ actions( notice ) }
|
||||
>
|
||||
<img src={ Alert } alt="" width={ 24 } height={ 24 } />
|
||||
{ notice.message }
|
||||
</Notice>
|
||||
);
|
||||
}
|
||||
return <>{ errorNotices }</>;
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Button } from '@wordpress/components';
|
||||
import { useContext, useState } from '@wordpress/element';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { SubscriptionsContext } from '../../../../contexts/subscriptions-context';
|
||||
import {
|
||||
addNotice,
|
||||
connectProduct,
|
||||
removeNotice,
|
||||
} from '../../../../utils/functions';
|
||||
import { Subscription } from '../../types';
|
||||
import { NoticeStatus } from '../../../../contexts/types';
|
||||
|
||||
interface ConnectProps {
|
||||
subscription: Subscription;
|
||||
onClose?: () => void;
|
||||
variant?: Button.ButtonVariant;
|
||||
}
|
||||
|
||||
export default function ConnectButton( props: ConnectProps ) {
|
||||
const [ isConnecting, setIsConnecting ] = useState( false );
|
||||
const { loadSubscriptions } = useContext( SubscriptionsContext );
|
||||
|
||||
const connect = () => {
|
||||
recordEvent( 'marketplace_product_connect_button_clicked', {
|
||||
product_zip_slug: props.subscription.zip_slug,
|
||||
product_id: props.subscription.product_id,
|
||||
} );
|
||||
|
||||
setIsConnecting( true );
|
||||
removeNotice( props.subscription.product_key );
|
||||
connectProduct( props.subscription )
|
||||
.then( () => {
|
||||
loadSubscriptions( false ).then( () => {
|
||||
addNotice(
|
||||
props.subscription.product_key,
|
||||
sprintf(
|
||||
// translators: %s is the product name.
|
||||
__( '%s successfully connected.', 'woocommerce' ),
|
||||
props.subscription.product_name
|
||||
),
|
||||
NoticeStatus.Success
|
||||
);
|
||||
setIsConnecting( false );
|
||||
if ( props.onClose ) {
|
||||
props.onClose();
|
||||
}
|
||||
} );
|
||||
} )
|
||||
.catch( () => {
|
||||
addNotice(
|
||||
props.subscription.product_key,
|
||||
sprintf(
|
||||
// translators: %s is the product name.
|
||||
__( '%s couldn’t be connected.', 'woocommerce' ),
|
||||
props.subscription.product_name
|
||||
),
|
||||
NoticeStatus.Error,
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: __( 'Try again', 'woocommerce' ),
|
||||
onClick: connect,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
setIsConnecting( false );
|
||||
if ( props.onClose ) {
|
||||
props.onClose();
|
||||
}
|
||||
} );
|
||||
};
|
||||
return (
|
||||
<Button
|
||||
onClick={ connect }
|
||||
variant={ props.variant ?? 'secondary' }
|
||||
isBusy={ isConnecting }
|
||||
disabled={ isConnecting }
|
||||
>
|
||||
{ __( 'Connect', 'woocommerce' ) }
|
||||
</Button>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Button, ButtonGroup, Modal } from '@wordpress/components';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Subscription } from '../../types';
|
||||
import ConnectButton from './connect-button';
|
||||
|
||||
interface ConnectProps {
|
||||
subscription: Subscription;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function ConnectModal( props: ConnectProps ) {
|
||||
return (
|
||||
<Modal
|
||||
title={ __( 'Connect to update', 'woocommerce' ) }
|
||||
onRequestClose={ props.onClose }
|
||||
focusOnMount={ true }
|
||||
className="woocommerce-marketplace__header-account-modal"
|
||||
style={ { borderRadius: 4 } }
|
||||
overlayClassName="woocommerce-marketplace__header-account-modal-overlay"
|
||||
>
|
||||
<p className="woocommerce-marketplace__header-account-modal-text">
|
||||
{ sprintf(
|
||||
// translators: %s is the product version number (e.g. 1.0.2).
|
||||
__(
|
||||
'Version %s is available. To enable this update you need to connect your subscription to this store.',
|
||||
'woocommerce'
|
||||
),
|
||||
props.subscription.version
|
||||
) }
|
||||
</p>
|
||||
<ButtonGroup className="woocommerce-marketplace__header-account-modal-button-group">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={ props.onClose }
|
||||
className="woocommerce-marketplace__header-account-modal-button"
|
||||
>
|
||||
{ __( 'Cancel', 'woocommerce' ) }
|
||||
</Button>
|
||||
<ConnectButton
|
||||
subscription={ props.subscription }
|
||||
onClose={ props.onClose }
|
||||
variant="primary"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Button } from '@wordpress/components';
|
||||
import { dispatch, useSelect } from '@wordpress/data';
|
||||
import { useContext } from '@wordpress/element';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { SubscriptionsContext } from '../../../../contexts/subscriptions-context';
|
||||
import {
|
||||
addNotice,
|
||||
installProduct,
|
||||
removeNotice,
|
||||
} from '../../../../utils/functions';
|
||||
import { Subscription } from '../../types';
|
||||
import { installingStore } from '../../../../contexts/install-store';
|
||||
import { NoticeStatus } from '../../../../contexts/types';
|
||||
|
||||
interface InstallProps {
|
||||
subscription: Subscription;
|
||||
}
|
||||
|
||||
export default function Install( props: InstallProps ) {
|
||||
const { loadSubscriptions } = useContext( SubscriptionsContext );
|
||||
|
||||
const loading: boolean = useSelect(
|
||||
( select ) => {
|
||||
return select( installingStore ).isInstalling(
|
||||
props.subscription.product_key
|
||||
);
|
||||
},
|
||||
[ props.subscription.product_key ]
|
||||
);
|
||||
|
||||
const startInstall = () => {
|
||||
dispatch( installingStore ).startInstalling(
|
||||
props.subscription.product_key
|
||||
);
|
||||
};
|
||||
const stopInstall = () => {
|
||||
dispatch( installingStore ).stopInstalling(
|
||||
props.subscription.product_key
|
||||
);
|
||||
};
|
||||
|
||||
const install = () => {
|
||||
recordEvent( 'marketplace_product_install_button_clicked', {
|
||||
product_zip_slug: props.subscription.zip_slug,
|
||||
product_id: props.subscription.product_id,
|
||||
product_current_version: props.subscription.version,
|
||||
} );
|
||||
|
||||
startInstall();
|
||||
removeNotice( props.subscription.product_key );
|
||||
installProduct( props.subscription )
|
||||
.then( () => {
|
||||
loadSubscriptions( false ).then( () => {
|
||||
addNotice(
|
||||
props.subscription.product_key,
|
||||
sprintf(
|
||||
// translators: %s is the product name.
|
||||
__( '%s successfully installed.', 'woocommerce' ),
|
||||
props.subscription.product_name
|
||||
),
|
||||
NoticeStatus.Success
|
||||
);
|
||||
stopInstall();
|
||||
} );
|
||||
|
||||
recordEvent( 'marketplace_product_installed', {
|
||||
product_zip_slug: props.subscription.zip_slug,
|
||||
product_id: props.subscription.product_id,
|
||||
product_current_version: props.subscription.version,
|
||||
} );
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
loadSubscriptions( false ).then( () => {
|
||||
let errorMessage = sprintf(
|
||||
// translators: %s is the product name.
|
||||
__( '%s couldn’t be installed.', 'woocommerce' ),
|
||||
props.subscription.product_name
|
||||
);
|
||||
if ( error?.success === false && error?.data.message ) {
|
||||
errorMessage += ' ' + error.data.message;
|
||||
}
|
||||
addNotice(
|
||||
props.subscription.product_key,
|
||||
errorMessage,
|
||||
NoticeStatus.Error,
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: __( 'Try again', 'woocommerce' ),
|
||||
onClick: install,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
stopInstall();
|
||||
} );
|
||||
|
||||
recordEvent( 'marketplace_product_install_failed', {
|
||||
product_zip_slug: props.subscription.zip_slug,
|
||||
product_id: props.subscription.product_id,
|
||||
product_current_version: props.subscription.version,
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="link"
|
||||
isBusy={ loading }
|
||||
disabled={ loading }
|
||||
onClick={ install }
|
||||
>
|
||||
{ __( 'Install', 'woocommerce' ) }
|
||||
</Button>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Button } from '@wordpress/components';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { useContext, useState } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import RefreshIcon from '../../../../assets/images/refresh.svg';
|
||||
import { SubscriptionsContext } from '../../../../contexts/subscriptions-context';
|
||||
import { addNotice, removeNotice } from '../../../../utils/functions';
|
||||
import { NoticeStatus } from '../../../../contexts/types';
|
||||
|
||||
const NOTICE_ID = 'woocommerce-marketplace-refresh-subscriptions';
|
||||
|
||||
export function RefreshButton() {
|
||||
const { refreshSubscriptions } = useContext( SubscriptionsContext );
|
||||
const [ isLoading, setIsLoading ] = useState( false );
|
||||
|
||||
const refresh = () => {
|
||||
if ( isLoading ) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeNotice( NOTICE_ID );
|
||||
setIsLoading( true );
|
||||
|
||||
refreshSubscriptions()
|
||||
.then( () => {
|
||||
addNotice(
|
||||
NOTICE_ID,
|
||||
__( 'Subscriptions refreshed.', 'woocommerce' ),
|
||||
NoticeStatus.Success
|
||||
);
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
addNotice(
|
||||
NOTICE_ID,
|
||||
sprintf(
|
||||
// translators: %s is the error message.
|
||||
__(
|
||||
'Error refreshing subscriptions: %s',
|
||||
'woocommerce'
|
||||
),
|
||||
error.message
|
||||
),
|
||||
NoticeStatus.Error,
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: __( 'Try again', 'woocommerce' ),
|
||||
onClick: refresh,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
} )
|
||||
.finally( () => {
|
||||
setIsLoading( false );
|
||||
} );
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="woocommerce-marketplace__refresh-subscriptions"
|
||||
onClick={ refresh }
|
||||
isBusy={ isLoading }
|
||||
>
|
||||
<img
|
||||
src={ RefreshIcon }
|
||||
alt={ __( 'Refresh subscriptions', 'woocommerce' ) }
|
||||
className="woocommerce-marketplace__refresh-subscriptions-icon"
|
||||
/>
|
||||
{ __( 'Refresh', 'woocommerce' ) }
|
||||
</Button>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Button } from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { queueRecordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { renewUrl } from '../../../../utils/functions';
|
||||
import { Subscription } from '../../types';
|
||||
|
||||
interface RenewProps {
|
||||
subscription: Subscription;
|
||||
variant?: Button.ButtonVariant;
|
||||
}
|
||||
|
||||
export default function RenewButton( props: RenewProps ) {
|
||||
function recordTracksEvent() {
|
||||
queueRecordEvent( 'marketplace_renew_button_clicked', {
|
||||
product_zip_slug: props.subscription.zip_slug,
|
||||
product_id: props.subscription.product_id,
|
||||
} );
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
href={ renewUrl( props.subscription ) }
|
||||
variant={ props.variant ?? 'secondary' }
|
||||
onClick={ recordTracksEvent }
|
||||
>
|
||||
{ __( 'Renew', 'woocommerce' ) }
|
||||
</Button>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Button, ButtonGroup, Modal } from '@wordpress/components';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Subscription } from '../../types';
|
||||
import RenewButton from './renew-button';
|
||||
|
||||
interface RenewProps {
|
||||
subscription: Subscription;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function RenewModal( props: RenewProps ) {
|
||||
return (
|
||||
<Modal
|
||||
title={ __( 'Renew to update', 'woocommerce' ) }
|
||||
onRequestClose={ props.onClose }
|
||||
focusOnMount={ true }
|
||||
className="woocommerce-marketplace__header-account-modal"
|
||||
style={ { borderRadius: 4 } }
|
||||
overlayClassName="woocommerce-marketplace__header-account-modal-overlay"
|
||||
>
|
||||
<p className="woocommerce-marketplace__header-account-modal-text">
|
||||
{ sprintf(
|
||||
// translators: %s is the product version number (e.g. 1.0.2).
|
||||
__(
|
||||
'Version %s is available. To enable this update you need to renew your subscription.',
|
||||
'woocommerce'
|
||||
),
|
||||
props.subscription.version
|
||||
) }
|
||||
</p>
|
||||
<ButtonGroup className="woocommerce-marketplace__header-account-modal-button-group">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={ props.onClose }
|
||||
className="woocommerce-marketplace__header-account-modal-button"
|
||||
>
|
||||
{ __( 'Cancel', 'woocommerce' ) }
|
||||
</Button>
|
||||
<RenewButton
|
||||
subscription={ props.subscription }
|
||||
variant="primary"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Button } from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { queueRecordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { subscribeUrl } from '../../../../utils/functions';
|
||||
import { Subscription } from '../../types';
|
||||
|
||||
interface SubscribeProps {
|
||||
subscription: Subscription;
|
||||
variant?: Button.ButtonVariant;
|
||||
}
|
||||
|
||||
export default function SubscribeButton( props: SubscribeProps ) {
|
||||
function recordTracksEvent() {
|
||||
queueRecordEvent( 'marketplace_subscribe_button_clicked', {
|
||||
product_zip_slug: props.subscription.zip_slug,
|
||||
product_id: props.subscription.product_id,
|
||||
} );
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
href={ subscribeUrl( props.subscription ) }
|
||||
variant={ props.variant ?? 'secondary' }
|
||||
onClick={ recordTracksEvent }
|
||||
>
|
||||
{ __( 'Subscribe', 'woocommerce' ) }
|
||||
</Button>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Button, ButtonGroup, Modal } from '@wordpress/components';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Subscription } from '../../types';
|
||||
import SubscribeButton from './subscribe-button';
|
||||
|
||||
interface SubscribeProps {
|
||||
subscription: Subscription;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function SubscribeModal( props: SubscribeProps ) {
|
||||
return (
|
||||
<Modal
|
||||
title={ __( 'Subscribe to update', 'woocommerce' ) }
|
||||
onRequestClose={ props.onClose }
|
||||
focusOnMount={ true }
|
||||
className="woocommerce-marketplace__header-account-modal"
|
||||
style={ { borderRadius: 4 } }
|
||||
overlayClassName="woocommerce-marketplace__header-account-modal-overlay"
|
||||
>
|
||||
<p className="woocommerce-marketplace__header-account-modal-text">
|
||||
{ sprintf(
|
||||
// translators: %s is the product version number (e.g. 1.0.2).
|
||||
__(
|
||||
'Version %s is available. To enable this update you need to purchase a subscription.',
|
||||
'woocommerce'
|
||||
),
|
||||
props.subscription.version
|
||||
) }
|
||||
</p>
|
||||
<ButtonGroup className="woocommerce-marketplace__header-account-modal-button-group">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={ props.onClose }
|
||||
className="woocommerce-marketplace__header-account-modal-button"
|
||||
>
|
||||
{ __( 'Cancel', 'woocommerce' ) }
|
||||
</Button>
|
||||
<SubscribeButton
|
||||
subscription={ props.subscription }
|
||||
variant="primary"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Button } from '@wordpress/components';
|
||||
import { useContext, useState } from '@wordpress/element';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { SubscriptionsContext } from '../../../../contexts/subscriptions-context';
|
||||
import { Subscription } from '../../types';
|
||||
import ConnectModal from './connect-modal';
|
||||
import RenewModal from './renew-modal';
|
||||
import SubscribeModal from './subscribe-modal';
|
||||
import {
|
||||
addNotice,
|
||||
removeNotice,
|
||||
updateProduct,
|
||||
} from '../../../../utils/functions';
|
||||
import { NoticeStatus } from '../../../../contexts/types';
|
||||
|
||||
interface UpdateProps {
|
||||
subscription: Subscription;
|
||||
}
|
||||
|
||||
export default function Update( props: UpdateProps ) {
|
||||
const [ showModal, setShowModal ] = useState( false );
|
||||
const [ isUpdating, setIsUpdating ] = useState( false );
|
||||
const { loadSubscriptions } = useContext( SubscriptionsContext );
|
||||
|
||||
const canUpdate =
|
||||
props.subscription.active &&
|
||||
props.subscription.local &&
|
||||
props.subscription.local.slug &&
|
||||
props.subscription.local.path;
|
||||
|
||||
function update() {
|
||||
recordEvent( 'marketplace_product_update_button_clicked', {
|
||||
product_zip_slug: props.subscription.zip_slug,
|
||||
product_id: props.subscription.product_id,
|
||||
product_installed_version: props.subscription.local.installed,
|
||||
product_current_version: props.subscription.version,
|
||||
} );
|
||||
|
||||
if ( ! canUpdate ) {
|
||||
setShowModal( true );
|
||||
return;
|
||||
}
|
||||
removeNotice( props.subscription.product_key );
|
||||
if ( ! window.wp.updates ) {
|
||||
addNotice(
|
||||
props.subscription.product_key,
|
||||
sprintf(
|
||||
// translators: %s is the product name.
|
||||
__( '%s couldn’t be updated.', 'woocommerce' ),
|
||||
props.subscription.product_name
|
||||
),
|
||||
NoticeStatus.Error,
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: __(
|
||||
'Reload page and try again',
|
||||
'woocommerce'
|
||||
),
|
||||
onClick: () => {
|
||||
window.location.reload();
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUpdating( true );
|
||||
|
||||
updateProduct( props.subscription )
|
||||
.then( () => {
|
||||
loadSubscriptions( false ).then( () => {
|
||||
addNotice(
|
||||
props.subscription.product_key,
|
||||
sprintf(
|
||||
// translators: %s is the product name.
|
||||
__( '%s updated successfully.', 'woocommerce' ),
|
||||
props.subscription.product_name
|
||||
),
|
||||
NoticeStatus.Success
|
||||
);
|
||||
setIsUpdating( false );
|
||||
} );
|
||||
|
||||
recordEvent( 'marketplace_product_updated', {
|
||||
product_zip_slug: props.subscription.zip_slug,
|
||||
product_id: props.subscription.product_id,
|
||||
product_installed_version:
|
||||
props.subscription.local.installed,
|
||||
product_current_version: props.subscription.version,
|
||||
} );
|
||||
} )
|
||||
.catch( () => {
|
||||
addNotice(
|
||||
props.subscription.product_key,
|
||||
sprintf(
|
||||
// translators: %s is the product name.
|
||||
__( '%s couldn’t be updated.', 'woocommerce' ),
|
||||
props.subscription.product_name
|
||||
),
|
||||
NoticeStatus.Error,
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: __( 'Try again', 'woocommerce' ),
|
||||
onClick: update,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
setIsUpdating( false );
|
||||
|
||||
recordEvent( 'marketplace_product_update_failed', {
|
||||
product_zip_slug: props.subscription.zip_slug,
|
||||
product_id: props.subscription.product_id,
|
||||
product_installed_version:
|
||||
props.subscription.local.installed,
|
||||
product_current_version: props.subscription.version,
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
const modal = () => {
|
||||
if ( ! showModal ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( props.subscription.product_key === '' ) {
|
||||
return (
|
||||
<SubscribeModal
|
||||
onClose={ () => setShowModal( false ) }
|
||||
subscription={ props.subscription }
|
||||
/>
|
||||
);
|
||||
} else if ( props.subscription.expired ) {
|
||||
return (
|
||||
<RenewModal
|
||||
subscription={ props.subscription }
|
||||
onClose={ () => setShowModal( false ) }
|
||||
/>
|
||||
);
|
||||
} else if ( ! props.subscription.active ) {
|
||||
return (
|
||||
<ConnectModal
|
||||
subscription={ props.subscription }
|
||||
onClose={ () => setShowModal( false ) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{ modal() }
|
||||
<Button
|
||||
variant="link"
|
||||
className="woocommerce-marketplace__my-subscriptions-update"
|
||||
onClick={ update }
|
||||
isBusy={ isUpdating }
|
||||
disabled={ isUpdating }
|
||||
label={ sprintf(
|
||||
// translators: %s is the product version.
|
||||
__( 'Update to %s', 'woocommerce' ),
|
||||
props.subscription.version
|
||||
) }
|
||||
showTooltip={ true }
|
||||
tooltipPosition="top center"
|
||||
>
|
||||
{ isUpdating
|
||||
? __( 'Updating', 'woocommerce' )
|
||||
: __( 'Update', 'woocommerce' ) }
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { DropdownMenu } from '@wordpress/components';
|
||||
import { moreVertical } from '@wordpress/icons';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Subscription } from '../../types';
|
||||
import { ADMIN_URL } from '../../../../../utils/admin-settings';
|
||||
|
||||
export default function ActionsDropdownMenu( props: {
|
||||
subscription: Subscription;
|
||||
} ) {
|
||||
const controls = [
|
||||
{
|
||||
title: __( 'Manage on Woo.com', 'woocommerce' ),
|
||||
icon: <></>,
|
||||
onClick: () => {
|
||||
window.open(
|
||||
'https://woo.com/my-account/my-subscriptions',
|
||||
'_blank'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: __( 'Manage in Plugins', 'woocommerce' ),
|
||||
icon: <></>,
|
||||
onClick: () => {
|
||||
window.location.href = ADMIN_URL + 'plugins.php';
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if ( props.subscription.documentation_url ) {
|
||||
controls.unshift( {
|
||||
title: __( 'View documentation', 'woocommerce' ),
|
||||
icon: <></>,
|
||||
onClick: () => {
|
||||
window.open( props.subscription.documentation_url, '_blank' );
|
||||
},
|
||||
} );
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
icon={ moreVertical }
|
||||
label={ __( 'Actions', 'woocommerce' ) }
|
||||
controls={ controls }
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,291 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { TableRow } from '@woocommerce/components/build-types/table/types';
|
||||
import { gmdateI18n } from '@wordpress/date';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Icon, plugins } from '@wordpress/icons';
|
||||
import { createInterpolateElement } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { StatusLevel, Subscription } from '../../types';
|
||||
import ConnectButton from '../actions/connect-button';
|
||||
import Install from '../actions/install';
|
||||
import RenewButton from '../actions/renew-button';
|
||||
import SubscribeButton from '../actions/subscribe-button';
|
||||
import Update from '../actions/update';
|
||||
import StatusPopover from './status-popover';
|
||||
import ActionsDropdownMenu from './actions-dropdown-menu';
|
||||
import Version from './version';
|
||||
import {
|
||||
appendURLParams,
|
||||
renewUrl,
|
||||
subscribeUrl,
|
||||
} from '../../../../utils/functions';
|
||||
import { MARKETPLACE_COLLABORATION_PATH } from '../../../constants';
|
||||
|
||||
type StatusBadge = {
|
||||
text: string;
|
||||
level: StatusLevel;
|
||||
explanation?: string | JSX.Element;
|
||||
};
|
||||
|
||||
function getStatusBadge( subscription: Subscription ): StatusBadge | false {
|
||||
if ( subscription.product_key === '' ) {
|
||||
/**
|
||||
* If there is no subscription, we don't need to check for the expiry.
|
||||
*/
|
||||
return {
|
||||
text: __( 'No subscription', 'woocommerce' ),
|
||||
level: StatusLevel.Error,
|
||||
explanation: createInterpolateElement(
|
||||
__(
|
||||
'To receive updates and support, please <purchase>purchase</purchase> a subscription or use a subscription from another account by <sharing>sharing</sharing> or <transferring>transferring</transferring>.',
|
||||
'woocommerce'
|
||||
),
|
||||
{
|
||||
purchase: (
|
||||
<a
|
||||
href={ subscribeUrl( subscription ) }
|
||||
rel="nofollow noopener noreferrer"
|
||||
>
|
||||
renew
|
||||
</a>
|
||||
),
|
||||
sharing: (
|
||||
<a
|
||||
href={ MARKETPLACE_COLLABORATION_PATH }
|
||||
rel="nofollow noopener noreferrer"
|
||||
>
|
||||
sharing
|
||||
</a>
|
||||
),
|
||||
transferring: (
|
||||
<a
|
||||
href={ MARKETPLACE_COLLABORATION_PATH }
|
||||
rel="nofollow noopener noreferrer"
|
||||
>
|
||||
sharing
|
||||
</a>
|
||||
),
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
if ( subscription.local.installed && ! subscription.active ) {
|
||||
return {
|
||||
text: __( 'Not connected', 'woocommerce' ),
|
||||
level: StatusLevel.Warning,
|
||||
explanation: __(
|
||||
'To receive updates and support, please connect your subscription to this store.',
|
||||
'woocommerce'
|
||||
),
|
||||
};
|
||||
}
|
||||
if ( subscription.expired ) {
|
||||
return {
|
||||
text: __( 'Expired', 'woocommerce' ),
|
||||
level: StatusLevel.Error,
|
||||
explanation: createInterpolateElement(
|
||||
__(
|
||||
'To receive updates and support, please <renew>renew</renew> this subscription or use a subscription from another account by <sharing>sharing</sharing> or <transferring>transferring</transferring>.',
|
||||
'woocommerce'
|
||||
),
|
||||
{
|
||||
renew: (
|
||||
<a
|
||||
href={ renewUrl( subscription ) }
|
||||
rel="nofollow noopener noreferrer"
|
||||
>
|
||||
renew
|
||||
</a>
|
||||
),
|
||||
sharing: (
|
||||
<a
|
||||
href={ MARKETPLACE_COLLABORATION_PATH }
|
||||
rel="nofollow noopener noreferrer"
|
||||
>
|
||||
sharing
|
||||
</a>
|
||||
),
|
||||
transferring: (
|
||||
<a
|
||||
href={ MARKETPLACE_COLLABORATION_PATH }
|
||||
rel="nofollow noopener noreferrer"
|
||||
>
|
||||
sharing
|
||||
</a>
|
||||
),
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getVersion( subscription: Subscription ): string | JSX.Element {
|
||||
if ( subscription.local.version === subscription.version ) {
|
||||
return <Version span={ subscription.local.version } />;
|
||||
}
|
||||
|
||||
if ( subscription.local.version && subscription.version ) {
|
||||
return <Update subscription={ subscription } />;
|
||||
}
|
||||
|
||||
if ( subscription.version ) {
|
||||
return <Version span={ subscription.version } />;
|
||||
}
|
||||
|
||||
if ( subscription.local.version ) {
|
||||
return <Version span={ subscription.local.version } />;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function appendUTMParams( url: string ) {
|
||||
return appendURLParams( url, [
|
||||
[ 'utm_source', 'subscriptionsscreen' ],
|
||||
[ 'utm_medium', 'product' ],
|
||||
[ 'utm_campaign', 'wcaddons' ],
|
||||
[ 'utm_content', 'product-name' ],
|
||||
] );
|
||||
}
|
||||
|
||||
export function nameAndStatus( subscription: Subscription ): TableRow {
|
||||
// This is the fallback icon element with products without
|
||||
let iconElement = <Icon icon={ plugins } size={ 40 } />;
|
||||
|
||||
// If the product has an icon, use that instead.
|
||||
if ( subscription.product_icon ) {
|
||||
iconElement = (
|
||||
<img
|
||||
src={ subscription.product_icon }
|
||||
alt={ sprintf(
|
||||
/* translators: %s is the product name. */
|
||||
__( '%s icon', 'woocommerce' ),
|
||||
subscription.product_name
|
||||
) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const statusBadge = getStatusBadge( subscription );
|
||||
|
||||
const displayElement = (
|
||||
<div className="woocommerce-marketplace__my-subscriptions__product">
|
||||
<a
|
||||
href={ appendUTMParams( subscription.product_url ) }
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span className="woocommerce-marketplace__my-subscriptions__product-icon">
|
||||
{ iconElement }
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href={ appendUTMParams( subscription.product_url ) }
|
||||
className="woocommerce-marketplace__my-subscriptions__product-name"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{ subscription.product_name }
|
||||
</a>
|
||||
<span className="woocommerce-marketplace__my-subscriptions__product-statuses">
|
||||
{ statusBadge && (
|
||||
<StatusPopover
|
||||
text={ statusBadge.text }
|
||||
level={ statusBadge.level }
|
||||
explanation={ statusBadge.explanation ?? '' }
|
||||
/>
|
||||
) }
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return {
|
||||
display: displayElement,
|
||||
value: subscription.product_name,
|
||||
};
|
||||
}
|
||||
|
||||
export function expiry( subscription: Subscription ): TableRow {
|
||||
const expiryDate = subscription.expires;
|
||||
|
||||
if (
|
||||
subscription.local.installed === true &&
|
||||
subscription.product_key === ''
|
||||
) {
|
||||
return {
|
||||
display: '',
|
||||
value: '',
|
||||
};
|
||||
}
|
||||
|
||||
let expiryDateElement = __( 'Never expires', 'woocommerce' );
|
||||
|
||||
if ( expiryDate ) {
|
||||
expiryDateElement = gmdateI18n(
|
||||
'j M, Y',
|
||||
new Date( expiryDate * 1000 )
|
||||
);
|
||||
}
|
||||
|
||||
const displayElement = (
|
||||
<span className="woocommerce-marketplace__my-subscriptions__expiry-date">
|
||||
{ expiryDateElement }
|
||||
</span>
|
||||
);
|
||||
|
||||
return {
|
||||
display: displayElement,
|
||||
value: expiryDate,
|
||||
};
|
||||
}
|
||||
|
||||
export function autoRenew( subscription: Subscription ): TableRow {
|
||||
return {
|
||||
display: subscription.autorenew
|
||||
? __( 'On', 'woocommerce' )
|
||||
: __( 'Off', 'woocommerce' ),
|
||||
value: subscription.autorenew,
|
||||
};
|
||||
}
|
||||
|
||||
export function version( subscription: Subscription ): TableRow {
|
||||
return {
|
||||
display: getVersion( subscription ),
|
||||
};
|
||||
}
|
||||
|
||||
export function actions( subscription: Subscription ): TableRow {
|
||||
let actionButton = null;
|
||||
if ( subscription.product_key === '' ) {
|
||||
actionButton = <SubscribeButton subscription={ subscription } />;
|
||||
} else if ( subscription.expired ) {
|
||||
actionButton = <RenewButton subscription={ subscription } />;
|
||||
} else if (
|
||||
subscription.local.installed === false &&
|
||||
subscription.subscription_installed === false
|
||||
) {
|
||||
actionButton = <Install subscription={ subscription } />;
|
||||
} else if (
|
||||
subscription.active === false &&
|
||||
subscription.subscription_available === true
|
||||
) {
|
||||
actionButton = (
|
||||
<ConnectButton subscription={ subscription } variant="link" />
|
||||
);
|
||||
}
|
||||
return {
|
||||
display: (
|
||||
<div className="woocommerce-marketplace__my-subscriptions__actions">
|
||||
{ actionButton }
|
||||
|
||||
<ActionsDropdownMenu subscription={ subscription } />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Popover } from '@wordpress/components';
|
||||
import { Icon, info } from '@wordpress/icons';
|
||||
import { useState } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { StatusLevel } from '../../types';
|
||||
|
||||
export default function StatusPopover( props: {
|
||||
text: string;
|
||||
level: StatusLevel;
|
||||
explanation: string | JSX.Element;
|
||||
} ) {
|
||||
const [ isVisible, setIsVisible ] = useState( false );
|
||||
|
||||
function shouldShowExplanation() {
|
||||
if ( props.explanation === '' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isVisible;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={ () => {
|
||||
setIsVisible( ! isVisible );
|
||||
} }
|
||||
className={
|
||||
'woocommerce-marketplace__my-subscriptions__product-status' +
|
||||
' ' +
|
||||
`woocommerce-marketplace__my-subscriptions__product-status--${ props.level }`
|
||||
}
|
||||
>
|
||||
<Icon icon={ info } size={ 16 } />
|
||||
{ props.text }
|
||||
{ shouldShowExplanation() && (
|
||||
<Popover
|
||||
className="woocommerce-marketplace__my-subscriptions__popover"
|
||||
position="top center"
|
||||
>
|
||||
{ props.explanation }
|
||||
</Popover>
|
||||
) }
|
||||
</button>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
interface VersionProps {
|
||||
span: string;
|
||||
}
|
||||
|
||||
export default function Version( props: VersionProps ) {
|
||||
return (
|
||||
<span className="woocommerce-marketplace__my-subscriptions-version">
|
||||
{ props.span }
|
||||
</span>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { TableRow } from '@woocommerce/components/build-types/table/types';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Subscription } from '../types';
|
||||
import {
|
||||
actions,
|
||||
autoRenew,
|
||||
expiry,
|
||||
nameAndStatus,
|
||||
version,
|
||||
} from './rows/functions';
|
||||
|
||||
export function availableSubscriptionRow( item: Subscription ): TableRow[] {
|
||||
return [
|
||||
nameAndStatus( item ),
|
||||
expiry( item ),
|
||||
autoRenew( item ),
|
||||
version( item ),
|
||||
actions( item ),
|
||||
];
|
||||
}
|
||||
|
||||
export function installedSubscriptionRow( item: Subscription ): TableRow[] {
|
||||
return [
|
||||
nameAndStatus( item ),
|
||||
expiry( item ),
|
||||
autoRenew( item ),
|
||||
version( item ),
|
||||
actions( item ),
|
||||
];
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { EmptyTable, Table, TablePlaceholder } from '@woocommerce/components';
|
||||
import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@woocommerce/components/build-types/table/types';
|
||||
import { getNewPath } from '@woocommerce/navigation';
|
||||
import { createInterpolateElement } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { MARKETPLACE_PATH } from '../../constants';
|
||||
|
||||
const tableHeadersDefault = [
|
||||
{
|
||||
key: 'name',
|
||||
label: __( 'Name', 'woocommerce' ),
|
||||
},
|
||||
{
|
||||
key: 'expiry',
|
||||
label: __( 'Expiry/Renewal date', 'woocommerce' ),
|
||||
},
|
||||
{
|
||||
key: 'autoRenew',
|
||||
label: __( 'Auto-renew', 'woocommerce' ),
|
||||
},
|
||||
{
|
||||
key: 'version',
|
||||
label: __( 'Version', 'woocommerce' ),
|
||||
},
|
||||
];
|
||||
|
||||
function SubscriptionsTable( props: {
|
||||
rows?: TableRow[][];
|
||||
headers: TableHeader[];
|
||||
isLoading: boolean;
|
||||
} ) {
|
||||
if ( props.isLoading ) {
|
||||
return (
|
||||
<TablePlaceholder
|
||||
caption={ __( 'Loading your subscriptions', 'woocommerce' ) }
|
||||
headers={ props.headers }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const headersWithClasses = props.headers.map( ( header ) => {
|
||||
return {
|
||||
...header,
|
||||
cellClassName:
|
||||
'woocommerce-marketplace__my-subscriptions__table__header--' +
|
||||
header.key,
|
||||
};
|
||||
} );
|
||||
|
||||
return (
|
||||
<Table
|
||||
className="woocommerce-marketplace__my-subscriptions__table"
|
||||
headers={ headersWithClasses }
|
||||
rows={ props.rows }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function InstalledSubscriptionsTable( props: {
|
||||
rows?: TableRow[][];
|
||||
isLoading: boolean;
|
||||
} ) {
|
||||
const headers = [
|
||||
...tableHeadersDefault,
|
||||
{
|
||||
key: 'actions',
|
||||
label: __( 'Actions', 'woocommerce' ),
|
||||
},
|
||||
];
|
||||
|
||||
if ( ! props.isLoading && ( ! props.rows || props.rows.length === 0 ) ) {
|
||||
const marketplaceBrowseURL = getNewPath( {}, MARKETPLACE_PATH, {} );
|
||||
const noInstalledSubscriptionsHTML = createInterpolateElement(
|
||||
__(
|
||||
'No extensions or themes installed. <a>Browse the Marketplace</a>',
|
||||
'woocommerce'
|
||||
),
|
||||
{
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
a: <a href={ marketplaceBrowseURL } />,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<EmptyTable numberOfRows={ 4 }>
|
||||
{ noInstalledSubscriptionsHTML }
|
||||
</EmptyTable>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SubscriptionsTable
|
||||
rows={ props.rows }
|
||||
isLoading={ props.isLoading }
|
||||
headers={ headers }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function AvailableSubscriptionsTable( props: {
|
||||
rows?: TableRow[][];
|
||||
isLoading: boolean;
|
||||
} ) {
|
||||
const headers = [
|
||||
...tableHeadersDefault,
|
||||
{
|
||||
key: 'actions',
|
||||
label: __( 'Actions', 'woocommerce' ),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<SubscriptionsTable
|
||||
rows={ props.rows }
|
||||
isLoading={ props.isLoading }
|
||||
headers={ headers }
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
export type Subscription = {
|
||||
product_key: string;
|
||||
product_id: number;
|
||||
product_name: string;
|
||||
product_url: string;
|
||||
product_icon: string;
|
||||
product_slug: string;
|
||||
product_type: string;
|
||||
documentation_url: string;
|
||||
zip_slug: string;
|
||||
key_type: string;
|
||||
key_type_label: string;
|
||||
autorenew: boolean;
|
||||
connections: string[];
|
||||
legacy_connections: string[];
|
||||
shares: SubscriptionShare[];
|
||||
lifetime: boolean;
|
||||
expires: number;
|
||||
expired: boolean;
|
||||
expiring: boolean;
|
||||
sites_max: number;
|
||||
sites_active: number;
|
||||
maxed: boolean;
|
||||
order_id: number;
|
||||
product_keys_all: string[];
|
||||
product_status: string;
|
||||
active: boolean;
|
||||
local: SubscriptionLocal;
|
||||
has_updates: boolean;
|
||||
version: string;
|
||||
subscription_installed: boolean;
|
||||
subscription_available: boolean;
|
||||
};
|
||||
|
||||
export interface SubscriptionLocal {
|
||||
installed: boolean;
|
||||
installable: boolean;
|
||||
active: boolean;
|
||||
version: string;
|
||||
type: string;
|
||||
slug: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface SubscriptionShare {
|
||||
share_id: string;
|
||||
product_key: string;
|
||||
user_id: string;
|
||||
subscription_item_id: string;
|
||||
status: string;
|
||||
created: string;
|
||||
}
|
||||
|
||||
export enum StatusLevel {
|
||||
Warning = 'warning',
|
||||
Error = 'error',
|
||||
}
|
|
@ -4,21 +4,26 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { Card } from '@wordpress/components';
|
||||
import classnames from 'classnames';
|
||||
import { ExtraProperties, queueRecordEvent } from '@woocommerce/tracks';
|
||||
import { useQuery } from '@woocommerce/navigation';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './product-card.scss';
|
||||
import { Product, ProductType } from '../product-list/types';
|
||||
import { Product, ProductTracksData, ProductType } from '../product-list/types';
|
||||
import { appendURLParams } from '../../utils/functions';
|
||||
|
||||
export interface ProductCardProps {
|
||||
type?: string;
|
||||
product?: Product;
|
||||
isLoading?: boolean;
|
||||
tracksData: ProductTracksData;
|
||||
}
|
||||
|
||||
function ProductCard( props: ProductCardProps ): JSX.Element {
|
||||
const { isLoading, type } = props;
|
||||
const query = useQuery();
|
||||
// Get the product if provided; if not provided, render a skeleton loader
|
||||
const product = props.product ?? {
|
||||
title: '',
|
||||
|
@ -34,16 +39,64 @@ function ProductCard( props: ProductCardProps ): JSX.Element {
|
|||
// We hardcode this for now while we only display prices in USD.
|
||||
const currencySymbol = '$';
|
||||
|
||||
function recordTracksEvent( event: string, data: ExtraProperties ) {
|
||||
const tracksData = props.tracksData;
|
||||
|
||||
if ( tracksData.position ) {
|
||||
data.position = tracksData.position;
|
||||
}
|
||||
|
||||
if ( tracksData.label ) {
|
||||
data.label = tracksData.label;
|
||||
}
|
||||
|
||||
if ( tracksData.group ) {
|
||||
data.group = tracksData.group;
|
||||
}
|
||||
|
||||
if ( tracksData.searchTerm ) {
|
||||
data.search_term = tracksData.searchTerm;
|
||||
}
|
||||
|
||||
if ( tracksData.category ) {
|
||||
data.category = tracksData.category;
|
||||
}
|
||||
|
||||
queueRecordEvent( event, data );
|
||||
}
|
||||
|
||||
const isTheme = type === ProductType.theme;
|
||||
let productVendor: string | JSX.Element | null = product?.vendorName;
|
||||
if ( product?.vendorName && product?.vendorUrl ) {
|
||||
productVendor = (
|
||||
<a href={ product.vendorUrl } rel="noopener noreferrer">
|
||||
<a
|
||||
href={ product.vendorUrl }
|
||||
rel="noopener noreferrer"
|
||||
onClick={ () => {
|
||||
recordTracksEvent(
|
||||
'marketplace_product_card_vendor_clicked',
|
||||
{
|
||||
product: product.title,
|
||||
vendor: product.vendorName,
|
||||
product_type: type,
|
||||
}
|
||||
);
|
||||
} }
|
||||
>
|
||||
{ product.vendorName }
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const productUrl = () => {
|
||||
if ( query.ref ) {
|
||||
return appendURLParams( product.url, [
|
||||
[ 'utm_content', query.ref ],
|
||||
] );
|
||||
}
|
||||
return product.url;
|
||||
};
|
||||
|
||||
const classNames = classnames(
|
||||
'woocommerce-marketplace__product-card',
|
||||
`woocommerce-marketplace__product-card--${ type }`,
|
||||
|
@ -86,8 +139,18 @@ function ProductCard( props: ProductCardProps ): JSX.Element {
|
|||
<h2 className="woocommerce-marketplace__product-card__title">
|
||||
<a
|
||||
className="woocommerce-marketplace__product-card__link"
|
||||
href={ product.url }
|
||||
href={ productUrl() }
|
||||
rel="noopener noreferrer"
|
||||
onClick={ () => {
|
||||
recordTracksEvent(
|
||||
'marketplace_product_card_clicked',
|
||||
{
|
||||
product: product.title,
|
||||
vendor: product.vendorName,
|
||||
product_type: type,
|
||||
}
|
||||
);
|
||||
} }
|
||||
>
|
||||
{ isLoading ? ' ' : product.title }
|
||||
</a>
|
||||
|
|
|
@ -1,38 +1,23 @@
|
|||
@import '../../stylesheets/_variables.scss';
|
||||
|
||||
.woocommerce-marketplace__no-results__content {
|
||||
border: 1px solid $gutenberg-gray-100;
|
||||
padding: $grid-unit-80 $grid-unit-40;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: $grid-unit-30;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__no-results__product-group {
|
||||
margin-top: $grid-unit-60;
|
||||
|
||||
.woocommerce-marketplace__product-list-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__no-results__icon {
|
||||
height: 80px;
|
||||
.woocommerce-marketplace__no-results__heading {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__no-results__description {
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
max-width: 52ch;
|
||||
|
||||
p {
|
||||
color: $gutenberg-gray-700;
|
||||
}
|
||||
margin: $grid-unit-10 0 0 0;
|
||||
color: $gray-700;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__no-results__description--bold {
|
||||
font-weight: 600;
|
||||
.woocommerce-marketplace__no-results__product-groups {
|
||||
margin-top: $grid-unit-20;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__no-results .woocommerce-marketplace__product-list-title {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
margin: $grid-unit-40 0 0 0;
|
||||
}
|
||||
|
|
|
@ -1,159 +1,150 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useEffect, useState } from '@wordpress/element';
|
||||
import { useQuery } from '@woocommerce/navigation';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import NoResultsExtensionsIcon from '../../assets/images/no-results-extensions.svg';
|
||||
import NoResultsThemesIcon from '../../assets/images/no-results-themes.svg';
|
||||
import { fetchDiscoverPageData, ProductGroup } from '../../utils/functions';
|
||||
import ProductLoader from '../product-loader/product-loader';
|
||||
import ProductList from '../product-list/product-list';
|
||||
import './no-results.scss';
|
||||
import { ProductType } from '../product-list/types';
|
||||
import { ProductType, SearchResultType } from '../product-list/types';
|
||||
import CategorySelector from '../category-selector/category-selector';
|
||||
import './no-results.scss';
|
||||
|
||||
interface NoResultsProps {
|
||||
type: ProductType;
|
||||
}
|
||||
|
||||
export default function NoResults( props: NoResultsProps ): JSX.Element {
|
||||
const [ productGroup, setProductGroup ] = useState< ProductGroup >();
|
||||
const [ isLoadingProductGroup, setisLoadingProductGroup ] =
|
||||
useState( false );
|
||||
const [ noResultsTerm, setNoResultsTerm ] = useState< string >( '' );
|
||||
const typeLabel =
|
||||
props.type === ProductType.theme ? 'themes' : 'extensions';
|
||||
export default function NoResults( props: {
|
||||
type: SearchResultType;
|
||||
showHeading?: boolean;
|
||||
heading?: string;
|
||||
} ): JSX.Element {
|
||||
const [ productGroups, setProductGroups ] = useState< ProductGroup[] >();
|
||||
const [ isLoading, setIsLoading ] = useState( false );
|
||||
const query = useQuery();
|
||||
const showCategorySelector = query.tab === 'search' && query.section;
|
||||
const productGroupsForSearchType = {
|
||||
[ SearchResultType.all ]: [ 'most-popular', 'popular-themes' ],
|
||||
[ SearchResultType.theme ]: [ 'popular-themes' ],
|
||||
[ SearchResultType.extension ]: [ 'most-popular' ],
|
||||
};
|
||||
|
||||
useEffect( () => {
|
||||
if ( query.term ) {
|
||||
setNoResultsTerm( query.term );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( query.category ) {
|
||||
/**
|
||||
* Trim understore from start and end of a category. Some categories have underscores at the start and end
|
||||
* and we don't want to show them for the no results term
|
||||
*/
|
||||
const categoryTerm = query.category.replace( /^_+|_+$/g, '' );
|
||||
|
||||
setNoResultsTerm( categoryTerm );
|
||||
}
|
||||
}, [ query ] );
|
||||
|
||||
useEffect( () => {
|
||||
setisLoadingProductGroup( true );
|
||||
setIsLoading( true );
|
||||
|
||||
fetchDiscoverPageData()
|
||||
.then( ( products: ProductGroup[] ) => {
|
||||
const productGroupId =
|
||||
props.type === ProductType.theme
|
||||
? 'popular-themes'
|
||||
: 'most-popular';
|
||||
const mostPopularGroup = products.find(
|
||||
( group ) => group.id === productGroupId
|
||||
);
|
||||
const productGroupIds =
|
||||
productGroupsForSearchType[ props.type ];
|
||||
|
||||
if ( ! mostPopularGroup ) {
|
||||
if ( ! productGroupIds ) {
|
||||
return;
|
||||
}
|
||||
|
||||
mostPopularGroup.items = mostPopularGroup.items.slice( 0, 9 );
|
||||
const productGroupsToDisplay = products.filter( ( group ) => {
|
||||
return productGroupIds.includes( group.id );
|
||||
} );
|
||||
|
||||
setProductGroup( mostPopularGroup );
|
||||
if ( ! productGroupsToDisplay ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Limit productGroup.items to 4 items.
|
||||
productGroupsToDisplay.forEach( ( group ) => {
|
||||
group.items = group.items.slice( 0, 4 );
|
||||
} );
|
||||
|
||||
setProductGroups( productGroupsToDisplay );
|
||||
} )
|
||||
.catch( () => {
|
||||
setProductGroup( undefined );
|
||||
setProductGroups( undefined );
|
||||
} )
|
||||
.finally( () => {
|
||||
setisLoadingProductGroup( false );
|
||||
setIsLoading( false );
|
||||
} );
|
||||
}, [] );
|
||||
|
||||
function productListTitle( type: ProductType ) {
|
||||
if ( type === ProductType.theme ) {
|
||||
function productListTitle( groupId: string ) {
|
||||
if ( groupId === 'popular-themes' ) {
|
||||
return __( 'Our favorite themes', 'woocommerce' );
|
||||
}
|
||||
|
||||
return __( 'Most popular extensions', 'woocommerce' );
|
||||
}
|
||||
|
||||
function renderProductGroup() {
|
||||
if ( isLoadingProductGroup ) {
|
||||
function renderProductGroups() {
|
||||
if ( isLoading ) {
|
||||
return (
|
||||
<ProductLoader
|
||||
type={ productGroup?.itemType || ProductType.extension }
|
||||
/>
|
||||
<>
|
||||
<ProductLoader
|
||||
type={ ProductType.extension }
|
||||
placeholderCount={ 4 }
|
||||
/>
|
||||
<ProductLoader
|
||||
type={ ProductType.theme }
|
||||
placeholderCount={ 4 }
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! productGroup ) {
|
||||
if ( ! productGroups || productGroups.length === 0 ) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ProductList
|
||||
title={ productListTitle( props.type ) }
|
||||
products={ productGroup.items }
|
||||
groupURL={ productGroup.url }
|
||||
type={ productGroup.itemType }
|
||||
/>
|
||||
<>
|
||||
{ productGroups.map( ( productGroup ) => {
|
||||
return (
|
||||
<ProductList
|
||||
title={ productListTitle( productGroup.id ) }
|
||||
products={ productGroup.items }
|
||||
groupURL={ productGroup.url }
|
||||
type={ productGroup.itemType }
|
||||
key={ productGroup.id }
|
||||
/>
|
||||
);
|
||||
} ) }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getNoResultsIcon( type: ProductType ) {
|
||||
if ( type === ProductType.theme ) {
|
||||
return NoResultsThemesIcon;
|
||||
function categorySelector() {
|
||||
if ( ! showCategorySelector ) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return NoResultsExtensionsIcon;
|
||||
if ( props.type === SearchResultType.all ) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
let categorySelectorType = ProductType.extension;
|
||||
|
||||
if ( props.type === SearchResultType.theme ) {
|
||||
categorySelectorType = ProductType.theme;
|
||||
}
|
||||
|
||||
return <CategorySelector type={ categorySelectorType } />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="woocommerce-marketplace__no-results">
|
||||
{ showCategorySelector && <CategorySelector type={ props.type } /> }
|
||||
{ categorySelector() }
|
||||
<div className="woocommerce-marketplace__no-results__content">
|
||||
<img
|
||||
className="woocommerce-marketplace__no-results__icon"
|
||||
src={ getNoResultsIcon( props.type ) }
|
||||
alt={ __( 'No results.', 'woocommerce' ) }
|
||||
width="80"
|
||||
height="80"
|
||||
/>
|
||||
<div className="woocommerce-marketplace__no-results__description">
|
||||
<h3 className="woocommerce-marketplace__no-results__description--bold">
|
||||
{ sprintf(
|
||||
// translators: %1$s product type (themes or extensions), %2$s: search term
|
||||
__(
|
||||
"We didn't find %1$s for “%2$s”",
|
||||
'woocommerce'
|
||||
),
|
||||
typeLabel,
|
||||
noResultsTerm
|
||||
) }
|
||||
</h3>
|
||||
<p>
|
||||
{ sprintf(
|
||||
// translators: %s product type (themes or extensions)
|
||||
__(
|
||||
'Try searching again using a different term, or take a look at some of our most popular %s below.',
|
||||
'woocommerce'
|
||||
),
|
||||
typeLabel
|
||||
) }
|
||||
</p>
|
||||
</div>
|
||||
<h2 className="woocommerce-marketplace__no-results__heading">
|
||||
{ props.showHeading ? props.heading : '' }
|
||||
</h2>
|
||||
<p className="woocommerce-marketplace__no-results__description">
|
||||
{ __(
|
||||
'Try searching again using a different term, or take a look at' +
|
||||
' our recommendations below.',
|
||||
'woocommerce'
|
||||
) }
|
||||
</p>
|
||||
</div>
|
||||
<div className="woocommerce-marketplace__no-results__product-group">
|
||||
{ renderProductGroup() }
|
||||
<div className="woocommerce-marketplace__no-results__product-groups">
|
||||
{ renderProductGroups() }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -20,9 +20,11 @@
|
|||
gap: $large-gap;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
// Hide third and above product cards on Discover page due to API result count
|
||||
// These are progressively displayed at larger screen sizes.
|
||||
&__discover .woocommerce-marketplace__product-card:nth-child(n+3) {
|
||||
|
||||
// Hide third and above product cards on Discover and suggestions on "no results" search results page
|
||||
// due to item count. We display progressively more on larger screen sizes.
|
||||
&__discover .woocommerce-marketplace__product-card:nth-child(n+3),
|
||||
&__no-results .woocommerce-marketplace__product-card:nth-child(n+3) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +36,9 @@
|
|||
gap: $large-gap;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
&__discover .woocommerce-marketplace__product-card:nth-child(3) {
|
||||
|
||||
&__discover .woocommerce-marketplace__product-card:nth-child(3),
|
||||
&__no-results .woocommerce-marketplace__product-card:nth-child(3) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +49,9 @@
|
|||
&__product-list-content {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
&__discover .woocommerce-marketplace__product-card:nth-child(4) {
|
||||
|
||||
&__discover .woocommerce-marketplace__product-card:nth-child(4),
|
||||
&__no-results .woocommerce-marketplace__product-card:nth-child(4) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,11 @@ import { getAdminSetting } from '../../../utils/admin-settings';
|
|||
|
||||
export default function ProductListContent( props: {
|
||||
products: Product[];
|
||||
group?: string;
|
||||
type: ProductType;
|
||||
className?: string;
|
||||
searchTerm?: string;
|
||||
category?: string;
|
||||
} ): JSX.Element {
|
||||
const wccomHelperSettings = getAdminSetting( 'wccomHelper', {} );
|
||||
|
||||
|
@ -26,7 +29,7 @@ export default function ProductListContent( props: {
|
|||
|
||||
return (
|
||||
<div className={ classes }>
|
||||
{ props.products.map( ( product ) => (
|
||||
{ props.products.map( ( product, index ) => (
|
||||
<ProductCard
|
||||
key={ product.id }
|
||||
type={ props.type }
|
||||
|
@ -53,6 +56,15 @@ export default function ProductListContent( props: {
|
|||
),
|
||||
description: product.description,
|
||||
} }
|
||||
tracksData={ {
|
||||
position: index + 1,
|
||||
...( product.label && { label: product.label } ),
|
||||
...( props.group && { group: props.group } ),
|
||||
...( props.searchTerm && {
|
||||
searchTerm: props.searchTerm,
|
||||
} ),
|
||||
...( props.category && { category: props.category } ),
|
||||
} }
|
||||
/>
|
||||
) ) }
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
flex: 1 0 0;
|
||||
font-size: 20px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
margin-bottom: $medium-gap;
|
||||
margin-top: $small-gap;
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { Link } from '@woocommerce/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import classnames from 'classnames';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -35,7 +36,16 @@ export default function ProductListHeader(
|
|||
</h2>
|
||||
{ groupURL !== null && (
|
||||
<span className="woocommerce-marketplace__product-list-link">
|
||||
<Link href={ groupURL } target="_blank">
|
||||
<Link
|
||||
href={ groupURL }
|
||||
target="_blank"
|
||||
onClick={ () => {
|
||||
recordEvent( 'marketplace_see_more_clicked', {
|
||||
group_title: title,
|
||||
group_url: groupURL,
|
||||
} );
|
||||
} }
|
||||
>
|
||||
{ __( 'See more', 'woocommerce' ) }
|
||||
</Link>
|
||||
</span>
|
||||
|
|
|
@ -18,7 +18,11 @@ export default function ProductList( props: ProductListProps ): JSX.Element {
|
|||
return (
|
||||
<div className="woocommerce-marketplace__product-list">
|
||||
<ProductListHeader title={ title } groupURL={ groupURL } />
|
||||
<ProductListContent products={ products } type={ type } />
|
||||
<ProductListContent
|
||||
group={ title }
|
||||
products={ products }
|
||||
type={ type }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ export type SearchAPIProductType = {
|
|||
|
||||
export interface Product {
|
||||
id?: number;
|
||||
position?: number;
|
||||
title: string;
|
||||
image: string;
|
||||
type: ProductType;
|
||||
|
@ -35,6 +36,18 @@ export interface Product {
|
|||
productType?: string;
|
||||
averageRating?: number | null;
|
||||
reviewsCount?: number | null;
|
||||
label?: string;
|
||||
group?: string;
|
||||
searchTerm?: string;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
export interface ProductTracksData {
|
||||
position?: number;
|
||||
label?: string;
|
||||
group?: string;
|
||||
searchTerm?: string;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
export enum ProductType {
|
||||
|
|
|
@ -27,7 +27,12 @@ export default function ProductLoader(
|
|||
) }
|
||||
<div className="woocommerce-marketplace__product-list-content">
|
||||
{ [ ...Array( placeholderCount ) ].map( ( element, i ) => (
|
||||
<ProductCard key={ i } isLoading={ true } type={ type } />
|
||||
<ProductCard
|
||||
key={ i }
|
||||
isLoading={ true }
|
||||
type={ type }
|
||||
tracksData={ {} }
|
||||
/>
|
||||
) ) }
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -16,17 +16,16 @@ import CategorySelector from '../category-selector/category-selector';
|
|||
import ProductListContent from '../product-list-content/product-list-content';
|
||||
import ProductLoader from '../product-loader/product-loader';
|
||||
import NoResults from '../product-list-content/no-results';
|
||||
import { Product, ProductType } from '../product-list/types';
|
||||
import {
|
||||
MARKETPLACE_ITEMS_PER_PAGE,
|
||||
MARKETPLACE_SEARCH_RESULTS_PER_PAGE,
|
||||
} from '../constants';
|
||||
import { Product, ProductType, SearchResultType } from '../product-list/types';
|
||||
import { MARKETPLACE_ITEMS_PER_PAGE } from '../constants';
|
||||
|
||||
interface ProductsProps {
|
||||
categorySelector?: boolean;
|
||||
products?: Product[];
|
||||
perPage?: number;
|
||||
type: ProductType;
|
||||
searchTerm?: string;
|
||||
showAllButton?: boolean;
|
||||
}
|
||||
|
||||
const LABELS = {
|
||||
|
@ -46,15 +45,12 @@ export default function Products( props: ProductsProps ): JSX.Element {
|
|||
const label = LABELS[ props.type ].label;
|
||||
const singularLabel = LABELS[ props.type ].singularLabel;
|
||||
const query = useQuery();
|
||||
const category = query?.category;
|
||||
|
||||
const perPage =
|
||||
// Limit results when on search tab, and not showing only one section.
|
||||
query?.tab === 'search' && ! query?.section
|
||||
? MARKETPLACE_SEARCH_RESULTS_PER_PAGE
|
||||
: MARKETPLACE_ITEMS_PER_PAGE;
|
||||
const perPage = props.perPage ?? MARKETPLACE_ITEMS_PER_PAGE;
|
||||
|
||||
// Only show the "View all" button when on search but not showing a specific section of results.
|
||||
const showAllButton = query.tab === 'search' && ! query.section;
|
||||
const showAllButton = props.showAllButton ?? false;
|
||||
|
||||
function showSection( section: ProductType ) {
|
||||
navigateTo( {
|
||||
|
@ -109,7 +105,12 @@ export default function Products( props: ProductsProps ): JSX.Element {
|
|||
}
|
||||
|
||||
if ( products.length === 0 ) {
|
||||
return <NoResults type={ props.type } />;
|
||||
const type =
|
||||
props.type === ProductType.extension
|
||||
? SearchResultType.extension
|
||||
: SearchResultType.theme;
|
||||
|
||||
return <NoResults type={ type } showHeading={ false } />;
|
||||
}
|
||||
|
||||
const productListClass = classnames(
|
||||
|
@ -127,6 +128,8 @@ export default function Products( props: ProductsProps ): JSX.Element {
|
|||
products={ products }
|
||||
type={ props.type }
|
||||
className={ productListClass }
|
||||
searchTerm={ props.searchTerm }
|
||||
category={ category }
|
||||
/>
|
||||
{ showAllButton && (
|
||||
<Button
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { useQuery } from '@woocommerce/navigation';
|
||||
import { useContext } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -9,6 +11,12 @@ import { useQuery } from '@woocommerce/navigation';
|
|||
import './search-results.scss';
|
||||
import { Product, ProductType, SearchResultType } from '../product-list/types';
|
||||
import Products from '../products/products';
|
||||
import NoResults from '../product-list-content/no-results';
|
||||
import { MarketplaceContext } from '../../contexts/marketplace-context';
|
||||
import {
|
||||
MARKETPLACE_ITEMS_PER_PAGE,
|
||||
MARKETPLACE_SEARCH_RESULTS_PER_PAGE,
|
||||
} from '../../../marketplace/components/constants';
|
||||
|
||||
export interface SearchResultProps {
|
||||
products: Product[];
|
||||
|
@ -22,47 +30,112 @@ export default function SearchResults( props: SearchResultProps ): JSX.Element {
|
|||
const themeList = props.products.filter(
|
||||
( product ) => product.type === ProductType.theme
|
||||
);
|
||||
const marketplaceContextValue = useContext( MarketplaceContext );
|
||||
const { isLoading } = marketplaceContextValue;
|
||||
|
||||
const query = useQuery();
|
||||
const showCategorySelector = query.section ? true : false;
|
||||
const searchTerm = query.term ? query.term : '';
|
||||
|
||||
const extensionComponent = (
|
||||
<Products
|
||||
products={ extensionList }
|
||||
type={ ProductType.extension }
|
||||
categorySelector={ showCategorySelector }
|
||||
/>
|
||||
);
|
||||
type Overrides = {
|
||||
categorySelector?: boolean;
|
||||
showAllButton?: boolean;
|
||||
perPage?: number;
|
||||
};
|
||||
|
||||
const themeComponent = (
|
||||
<Products
|
||||
products={ themeList }
|
||||
type={ ProductType.theme }
|
||||
categorySelector={ showCategorySelector }
|
||||
/>
|
||||
);
|
||||
function productsComponent(
|
||||
products: Product[],
|
||||
type: ProductType,
|
||||
overrides: Overrides = {}
|
||||
) {
|
||||
return (
|
||||
<Products
|
||||
products={ products }
|
||||
type={ type }
|
||||
categorySelector={
|
||||
overrides.categorySelector ?? showCategorySelector
|
||||
}
|
||||
searchTerm={ searchTerm }
|
||||
showAllButton={ overrides.showAllButton ?? true }
|
||||
perPage={ overrides.perPage ?? MARKETPLACE_ITEMS_PER_PAGE }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function extensionsComponent( overrides: Overrides = {} ) {
|
||||
return productsComponent(
|
||||
extensionList,
|
||||
ProductType.extension,
|
||||
overrides
|
||||
);
|
||||
}
|
||||
|
||||
function themesComponent( overrides: Overrides = {} ) {
|
||||
return productsComponent( themeList, ProductType.theme, overrides );
|
||||
}
|
||||
|
||||
const content = () => {
|
||||
if ( query?.section === SearchResultType.theme ) {
|
||||
return themeComponent;
|
||||
}
|
||||
if ( query?.section === SearchResultType.extension ) {
|
||||
return extensionComponent;
|
||||
return extensionsComponent( { showAllButton: false } );
|
||||
}
|
||||
|
||||
if ( extensionList.length === 0 && themeList.length > 0 ) {
|
||||
if ( query?.section === SearchResultType.theme ) {
|
||||
return themesComponent( { showAllButton: false } );
|
||||
}
|
||||
|
||||
// Components can handle their isLoading state. So we can put them both on the page.
|
||||
if ( isLoading ) {
|
||||
return (
|
||||
<>
|
||||
{ themeComponent }
|
||||
{ extensionComponent }
|
||||
{ extensionsComponent() }
|
||||
{ themesComponent() }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// If we did finish loading items, and there are no results, show the no results component.
|
||||
if (
|
||||
! isLoading &&
|
||||
extensionList.length === 0 &&
|
||||
themeList.length === 0
|
||||
) {
|
||||
return (
|
||||
<NoResults
|
||||
type={ SearchResultType.all }
|
||||
showHeading={ true }
|
||||
heading={ __(
|
||||
'No extensions or themes found…',
|
||||
'woocommerce'
|
||||
) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if ( themeList.length === 0 && extensionList.length > 0 ) {
|
||||
return extensionsComponent( {
|
||||
categorySelector: true,
|
||||
showAllButton: false,
|
||||
perPage: MARKETPLACE_ITEMS_PER_PAGE,
|
||||
} );
|
||||
}
|
||||
|
||||
if ( extensionList.length === 0 && themeList.length > 0 ) {
|
||||
return themesComponent( {
|
||||
categorySelector: true,
|
||||
showAllButton: false,
|
||||
perPage: MARKETPLACE_ITEMS_PER_PAGE,
|
||||
} );
|
||||
}
|
||||
|
||||
// If we're done loading, we can put these components on the page.
|
||||
return (
|
||||
<>
|
||||
{ extensionComponent }
|
||||
{ themeComponent }
|
||||
{ extensionsComponent( {
|
||||
perPage: MARKETPLACE_SEARCH_RESULTS_PER_PAGE,
|
||||
} ) }
|
||||
{ themesComponent( {
|
||||
perPage: MARKETPLACE_SEARCH_RESULTS_PER_PAGE,
|
||||
} ) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -48,14 +48,7 @@ const tabs: Tabs = {
|
|||
},
|
||||
'my-subscriptions': {
|
||||
name: 'my-subscriptions',
|
||||
title: __( 'My Subscriptions', 'woocommerce' ),
|
||||
href: getNewPath(
|
||||
{
|
||||
page: 'wc-addons',
|
||||
section: 'helper',
|
||||
},
|
||||
''
|
||||
),
|
||||
title: __( 'My subscriptions', 'woocommerce' ),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createReduxStore, register } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { InstallingState } from './types';
|
||||
|
||||
const INSTALLING_STORE_NAME = 'woocommerce-admin/installing';
|
||||
|
||||
export interface InstallingStateErrorAction {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_STATE: InstallingState = {
|
||||
installingProducts: [],
|
||||
};
|
||||
|
||||
const store = createReduxStore( INSTALLING_STORE_NAME, {
|
||||
reducer( state: InstallingState | undefined = DEFAULT_STATE, action ) {
|
||||
switch ( action.type ) {
|
||||
case 'START_INSTALLING':
|
||||
return {
|
||||
...state,
|
||||
installingProducts: [
|
||||
...state.installingProducts,
|
||||
action.productKey,
|
||||
],
|
||||
};
|
||||
case 'STOP_INSTALLING':
|
||||
return {
|
||||
...state,
|
||||
installingProducts: [
|
||||
...state.installingProducts.filter(
|
||||
( productKey ) => productKey !== action.productKey
|
||||
),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
actions: {
|
||||
startInstalling( productKey: string ) {
|
||||
return {
|
||||
type: 'START_INSTALLING',
|
||||
productKey,
|
||||
};
|
||||
},
|
||||
stopInstalling( productKey: string ) {
|
||||
return {
|
||||
type: 'STOP_INSTALLING',
|
||||
productKey,
|
||||
};
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
isInstalling(
|
||||
state: InstallingState | undefined,
|
||||
productKey: string
|
||||
): boolean {
|
||||
if ( ! state ) {
|
||||
return false;
|
||||
}
|
||||
return state.installingProducts.includes( productKey );
|
||||
},
|
||||
},
|
||||
} );
|
||||
|
||||
register( store );
|
||||
|
||||
export { store as installingStore, INSTALLING_STORE_NAME };
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createReduxStore, register } from '@wordpress/data';
|
||||
import { Options } from '@wordpress/notices';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { NoticeState, Notice, NoticeStatus } from './types';
|
||||
|
||||
const NOTICE_STORE_NAME = 'woocommerce-admin/subscription-notices';
|
||||
|
||||
const DEFAULT_STATE: NoticeState = {
|
||||
notices: {},
|
||||
};
|
||||
|
||||
const store = createReduxStore( NOTICE_STORE_NAME, {
|
||||
reducer( state: NoticeState | undefined = DEFAULT_STATE, action ) {
|
||||
switch ( action.type ) {
|
||||
case 'ADD_NOTICE':
|
||||
return {
|
||||
...state,
|
||||
notices: {
|
||||
...state.notices,
|
||||
[ action.productKey ]: {
|
||||
productKey: action.productKey,
|
||||
message: action.message,
|
||||
status: action.status,
|
||||
options: action.options,
|
||||
},
|
||||
},
|
||||
};
|
||||
case 'REMOVE_NOTICE':
|
||||
const notices = { ...state.notices };
|
||||
if ( notices[ action.productKey ] ) {
|
||||
delete notices[ action.productKey ];
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
notices,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
actions: {
|
||||
addNotice(
|
||||
productKey: string,
|
||||
message: string,
|
||||
status: NoticeStatus,
|
||||
options?: Partial< Options >
|
||||
) {
|
||||
return {
|
||||
type: 'ADD_NOTICE',
|
||||
productKey,
|
||||
message,
|
||||
status,
|
||||
options,
|
||||
};
|
||||
},
|
||||
removeNotice( productKey: string ) {
|
||||
return {
|
||||
type: 'REMOVE_NOTICE',
|
||||
productKey,
|
||||
};
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
notices( state: NoticeState | undefined ): Notice[] {
|
||||
if ( ! state ) {
|
||||
return [];
|
||||
}
|
||||
return Object.values( state.notices );
|
||||
},
|
||||
getNotice(
|
||||
state: NoticeState | undefined,
|
||||
productKey: string
|
||||
): Notice | undefined {
|
||||
if ( ! state ) {
|
||||
return;
|
||||
}
|
||||
return state.notices[ productKey ];
|
||||
},
|
||||
},
|
||||
} );
|
||||
|
||||
register( store );
|
||||
|
||||
export { store as noticeStore, NOTICE_STORE_NAME };
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useState, createContext, useEffect } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { SubscriptionsContextType } from './types';
|
||||
import { Subscription } from '../components/my-subscriptions/types';
|
||||
import {
|
||||
fetchSubscriptions,
|
||||
refreshSubscriptions as fetchSubscriptionsFromWooCom,
|
||||
} from '../utils/functions';
|
||||
|
||||
export const SubscriptionsContext = createContext< SubscriptionsContextType >( {
|
||||
subscriptions: [],
|
||||
setSubscriptions: () => {},
|
||||
loadSubscriptions: () => new Promise( () => {} ),
|
||||
refreshSubscriptions: () => new Promise( () => {} ),
|
||||
isLoading: true,
|
||||
setIsLoading: () => {},
|
||||
} );
|
||||
|
||||
export function SubscriptionsContextProvider( props: {
|
||||
children: JSX.Element;
|
||||
} ): JSX.Element {
|
||||
const [ subscriptions, setSubscriptions ] = useState<
|
||||
Array< Subscription >
|
||||
>( [] );
|
||||
const [ isLoading, setIsLoading ] = useState( true );
|
||||
|
||||
const loadSubscriptions = ( toggleLoading?: boolean ) => {
|
||||
if ( toggleLoading === true ) {
|
||||
setIsLoading( true );
|
||||
}
|
||||
|
||||
return fetchSubscriptions()
|
||||
.then( ( subscriptionResponse ) => {
|
||||
setSubscriptions( subscriptionResponse );
|
||||
} )
|
||||
.finally( () => {
|
||||
if ( toggleLoading ) {
|
||||
setIsLoading( false );
|
||||
}
|
||||
} );
|
||||
};
|
||||
|
||||
const refreshSubscriptions = () => {
|
||||
return fetchSubscriptionsFromWooCom().then(
|
||||
( subscriptionResponse ) => {
|
||||
setSubscriptions( subscriptionResponse );
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
useEffect( () => {
|
||||
loadSubscriptions( true );
|
||||
}, [] );
|
||||
|
||||
const contextValue = {
|
||||
subscriptions,
|
||||
setSubscriptions,
|
||||
loadSubscriptions,
|
||||
refreshSubscriptions,
|
||||
isLoading,
|
||||
setIsLoading,
|
||||
};
|
||||
|
||||
return (
|
||||
<SubscriptionsContext.Provider value={ contextValue }>
|
||||
{ props.children }
|
||||
</SubscriptionsContext.Provider>
|
||||
);
|
||||
}
|
|
@ -1,6 +1,47 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Options } from '@wordpress/notices';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Subscription } from '../components/my-subscriptions/types';
|
||||
|
||||
export type MarketplaceContextType = {
|
||||
isLoading: boolean;
|
||||
setIsLoading: ( isLoading: boolean ) => void;
|
||||
selectedTab: string;
|
||||
setSelectedTab: ( tab: string ) => void;
|
||||
};
|
||||
|
||||
export type SubscriptionsContextType = {
|
||||
subscriptions: Subscription[];
|
||||
setSubscriptions: ( subscriptions: Subscription[] ) => void;
|
||||
loadSubscriptions: ( toggleLoading?: boolean ) => Promise< void >;
|
||||
refreshSubscriptions: () => Promise< void >;
|
||||
isLoading: boolean;
|
||||
setIsLoading: ( isLoading: boolean ) => void;
|
||||
};
|
||||
|
||||
export enum NoticeStatus {
|
||||
Success = 'success',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
export interface Notice {
|
||||
productKey: string;
|
||||
message: string;
|
||||
status: NoticeStatus;
|
||||
options?: Partial< Options > | undefined;
|
||||
}
|
||||
|
||||
export interface NoticeState {
|
||||
notices: {
|
||||
[ key: string ]: Notice;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InstallingState {
|
||||
installingProducts: string[];
|
||||
}
|
||||
|
|
|
@ -1,22 +1,42 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useContext } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './marketplace.scss';
|
||||
import { MarketplaceContextProvider } from './contexts/marketplace-context';
|
||||
import {
|
||||
MarketplaceContextProvider,
|
||||
MarketplaceContext,
|
||||
} from './contexts/marketplace-context';
|
||||
import Header from './components/header/header';
|
||||
import Content from './components/content/content';
|
||||
import Footer from './components/footer/footer';
|
||||
import FeedbackModal from './components/feedback-modal/feedback-modal';
|
||||
|
||||
function MarketplaceComponents() {
|
||||
const { selectedTab } = useContext( MarketplaceContext );
|
||||
|
||||
const classNames =
|
||||
'woocommerce-marketplace' +
|
||||
( selectedTab ? ' woocommerce-marketplace--' + selectedTab : '' );
|
||||
|
||||
return (
|
||||
<div className={ classNames }>
|
||||
<Header />
|
||||
<Content />
|
||||
<FeedbackModal />
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Marketplace() {
|
||||
return (
|
||||
<MarketplaceContextProvider>
|
||||
<div className="woocommerce-marketplace">
|
||||
<Header />
|
||||
<Content />
|
||||
<FeedbackModal />
|
||||
<Footer />
|
||||
</div>
|
||||
<MarketplaceComponents />
|
||||
</MarketplaceContextProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,23 +2,31 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { dispatch } from '@wordpress/data';
|
||||
import { Options } from '@wordpress/notices';
|
||||
import { Icon } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { LOCALE } from '../../utils/admin-settings';
|
||||
import { CategoryAPIItem } from '../components/category-selector/types';
|
||||
import {
|
||||
MARKETPLACE_CART_PATH,
|
||||
MARKETPLACE_CATEGORY_API_PATH,
|
||||
MARKETPLACE_HOST,
|
||||
MARKETPLACE_SEARCH_API_PATH,
|
||||
} from '../components/constants';
|
||||
import { Subscription } from '../components/my-subscriptions/types';
|
||||
import {
|
||||
Product,
|
||||
ProductType,
|
||||
SearchAPIProductType,
|
||||
SearchAPIJSONType,
|
||||
SearchAPIProductType,
|
||||
} from '../components/product-list/types';
|
||||
import {
|
||||
MARKETPLACE_HOST,
|
||||
MARKETPLACE_CATEGORY_API_PATH,
|
||||
MARKETPLACE_SEARCH_API_PATH,
|
||||
} from '../components/constants';
|
||||
import { CategoryAPIItem } from '../components/category-selector/types';
|
||||
import { LOCALE } from '../../utils/admin-settings';
|
||||
import { NoticeStatus } from '../contexts/types';
|
||||
import { noticeStore } from '../contexts/notice-store';
|
||||
|
||||
interface ProductGroup {
|
||||
id: string;
|
||||
|
@ -179,6 +187,183 @@ function fetchCategories( type: ProductType ): Promise< CategoryAPIItem[] > {
|
|||
} );
|
||||
}
|
||||
|
||||
async function fetchSubscriptions(): Promise< Array< Subscription > > {
|
||||
const url = '/wc/v3/marketplace/subscriptions';
|
||||
return await apiFetch( { path: url.toString() } );
|
||||
}
|
||||
|
||||
async function refreshSubscriptions(): Promise< Array< Subscription > > {
|
||||
const url = '/wc/v3/marketplace/refresh';
|
||||
return await apiFetch( {
|
||||
path: url.toString(),
|
||||
method: 'POST',
|
||||
} );
|
||||
}
|
||||
|
||||
function connectProduct( subscription: Subscription ): Promise< void > {
|
||||
if ( subscription.active === true ) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const url = '/wc/v3/marketplace/subscriptions/connect';
|
||||
const data = new URLSearchParams();
|
||||
data.append( 'product_key', subscription.product_key );
|
||||
return apiFetch( {
|
||||
path: url.toString(),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: data,
|
||||
} );
|
||||
}
|
||||
|
||||
function disconnectProduct( subscription: Subscription ): Promise< void > {
|
||||
if ( subscription.active === false ) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const url = '/wc/v3/marketplace/subscriptions/disconnect';
|
||||
const data = new URLSearchParams();
|
||||
data.append( 'product_key', subscription.product_key );
|
||||
return apiFetch( {
|
||||
path: url.toString(),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: data,
|
||||
} );
|
||||
}
|
||||
|
||||
function wpAjax(
|
||||
action: string,
|
||||
data: {
|
||||
slug?: string;
|
||||
plugin?: string;
|
||||
theme?: string;
|
||||
success?: boolean;
|
||||
}
|
||||
): Promise< void > {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
if ( ! window.wp.updates ) {
|
||||
reject( __( 'Please reload and try again', 'woocommerce' ) );
|
||||
return;
|
||||
}
|
||||
|
||||
window.wp.updates.ajax( action, {
|
||||
...data,
|
||||
success: ( response: {
|
||||
success?: boolean;
|
||||
errorMessage?: string;
|
||||
} ) => {
|
||||
if ( response.success === false ) {
|
||||
reject( {
|
||||
success: false,
|
||||
data: {
|
||||
message: response.errorMessage,
|
||||
},
|
||||
} );
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
error: ( error: { errorMessage: string } ) => {
|
||||
reject( {
|
||||
success: false,
|
||||
data: {
|
||||
message: error.errorMessage,
|
||||
},
|
||||
} );
|
||||
},
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
function activateProduct( subscription: Subscription ): Promise< void > {
|
||||
if ( subscription.local.active === true ) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const url = '/wc/v3/marketplace/subscriptions/activate';
|
||||
const data = new URLSearchParams();
|
||||
data.append( 'product_key', subscription.product_key );
|
||||
return apiFetch( {
|
||||
path: url.toString(),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: data,
|
||||
} )
|
||||
.then( () => Promise.resolve() )
|
||||
.catch( () =>
|
||||
Promise.reject( {
|
||||
success: false,
|
||||
data: {
|
||||
message: sprintf(
|
||||
// translators: %s is the product name.
|
||||
__(
|
||||
'%s could not be activated. Please activate it manually.',
|
||||
'woocommerce'
|
||||
),
|
||||
subscription.product_name
|
||||
),
|
||||
},
|
||||
} )
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
} )
|
||||
.then( () => {
|
||||
return activateProduct( subscription );
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
// If install fails disconnect the product
|
||||
return disconnectProduct( subscription ).finally( () =>
|
||||
Promise.reject( error )
|
||||
);
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
function updateProduct( subscription: Subscription ): Promise< void > {
|
||||
return wpAjax( 'update-' + subscription.product_type, {
|
||||
slug: subscription.local.slug,
|
||||
[ subscription.product_type ]: subscription.local.path,
|
||||
} );
|
||||
}
|
||||
|
||||
function addNotice(
|
||||
productKey: string,
|
||||
message: string,
|
||||
status?: NoticeStatus,
|
||||
options?: Partial< Options >
|
||||
) {
|
||||
if ( status === NoticeStatus.Error ) {
|
||||
dispatch( noticeStore ).addNotice(
|
||||
productKey,
|
||||
message,
|
||||
status,
|
||||
options
|
||||
);
|
||||
} else {
|
||||
if ( ! options?.icon ) {
|
||||
options = {
|
||||
...options,
|
||||
icon: <Icon icon="saved" />,
|
||||
};
|
||||
}
|
||||
|
||||
dispatch( 'core/notices' ).createSuccessNotice( message, options );
|
||||
}
|
||||
}
|
||||
|
||||
const removeNotice = ( productKey: string ) => {
|
||||
dispatch( noticeStore ).removeNotice( productKey );
|
||||
};
|
||||
|
||||
// Append UTM parameters to a URL, being aware of existing query parameters
|
||||
const appendURLParams = (
|
||||
url: string,
|
||||
|
@ -198,10 +383,33 @@ const appendURLParams = (
|
|||
return urlObject.toString();
|
||||
};
|
||||
|
||||
const renewUrl = ( subscription: Subscription ): string => {
|
||||
return appendURLParams( MARKETPLACE_CART_PATH, [
|
||||
[ 'renew_product', subscription.product_id.toString() ],
|
||||
[ 'product_key', subscription.product_key ],
|
||||
[ 'order_id', subscription.order_id.toString() ],
|
||||
] );
|
||||
};
|
||||
|
||||
const subscribeUrl = ( subscription: Subscription ): string => {
|
||||
return appendURLParams( MARKETPLACE_CART_PATH, [
|
||||
[ 'add-to-cart', subscription.product_id.toString() ],
|
||||
] );
|
||||
};
|
||||
|
||||
export {
|
||||
fetchSearchResults,
|
||||
fetchDiscoverPageData,
|
||||
fetchCategories,
|
||||
ProductGroup,
|
||||
appendURLParams,
|
||||
connectProduct,
|
||||
fetchCategories,
|
||||
fetchDiscoverPageData,
|
||||
fetchSearchResults,
|
||||
fetchSubscriptions,
|
||||
refreshSubscriptions,
|
||||
installProduct,
|
||||
updateProduct,
|
||||
addNotice,
|
||||
removeNotice,
|
||||
renewUrl,
|
||||
subscribeUrl,
|
||||
};
|
||||
|
|
|
@ -37,6 +37,15 @@ declare global {
|
|||
'shipping-setting-tour': boolean;
|
||||
};
|
||||
wp: {
|
||||
updates?: {
|
||||
ajax: ( action, data: {
|
||||
slug?: string;
|
||||
plugin?: string;
|
||||
theme?: string;
|
||||
success?: function;
|
||||
error?: function;
|
||||
} ) => JQuery.Promise;
|
||||
};
|
||||
autosave?: {
|
||||
server: {
|
||||
postChanged: () => boolean;
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: major
|
||||
Type: update
|
||||
|
||||
The WooCommerce > Extensions section now includes a rebuilt "My Subscriptions" page with improvements to how subscriptions can be managed and installed.
|
|
@ -58,32 +58,30 @@ class WC_Helper_Admin {
|
|||
* @return string
|
||||
*/
|
||||
public static function get_connection_url() {
|
||||
// No active connection.
|
||||
if ( ! WC_Helper::is_site_connected() ) {
|
||||
$connect_url = add_query_arg(
|
||||
array(
|
||||
'page' => 'wc-addons',
|
||||
'section' => 'helper',
|
||||
'wc-helper-connect' => 1,
|
||||
'wc-helper-nonce' => wp_create_nonce( 'connect' ),
|
||||
),
|
||||
admin_url( 'admin.php' )
|
||||
);
|
||||
global $current_screen;
|
||||
|
||||
return $connect_url;
|
||||
}
|
||||
|
||||
$connect_url = add_query_arg(
|
||||
array(
|
||||
'page' => 'wc-addons',
|
||||
'section' => 'helper',
|
||||
'wc-helper-disconnect' => 1,
|
||||
'wc-helper-nonce' => wp_create_nonce( 'disconnect' ),
|
||||
),
|
||||
admin_url( 'admin.php' )
|
||||
$connect_url_args = array(
|
||||
'page' => 'wc-addons',
|
||||
'section' => 'helper',
|
||||
);
|
||||
|
||||
return $connect_url;
|
||||
// No active connection.
|
||||
if ( WC_Helper::is_site_connected() ) {
|
||||
$connect_url_args['wc-helper-disconnect'] = 1;
|
||||
$connect_url_args['wc-helper-nonce'] = wp_create_nonce( 'disconnect' );
|
||||
} else {
|
||||
$connect_url_args['wc-helper-connect'] = 1;
|
||||
$connect_url_args['wc-helper-nonce'] = wp_create_nonce( 'connect' );
|
||||
}
|
||||
|
||||
if ( isset( $current_screen->id ) && 'woocommerce_page_wc-admin' === $current_screen->id ) {
|
||||
$connect_url_args['redirect-to-wc-admin'] = 1;
|
||||
}
|
||||
|
||||
return add_query_arg(
|
||||
$connect_url_args,
|
||||
admin_url( 'admin.php' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -112,7 +110,7 @@ class WC_Helper_Admin {
|
|||
}
|
||||
|
||||
/**
|
||||
* Fetch featured procucts from Woo.com and serve them
|
||||
* Fetch featured products from Woo.com and serve them
|
||||
* as JSON.
|
||||
*/
|
||||
public static function get_featured() {
|
||||
|
@ -124,7 +122,6 @@ class WC_Helper_Admin {
|
|||
|
||||
wp_send_json( $featured );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
WC_Helper_Admin::load();
|
||||
|
|
|
@ -22,6 +22,7 @@ class WC_Helper_Plugin_Info {
|
|||
*/
|
||||
public static function load() {
|
||||
add_filter( 'plugins_api', array( __CLASS__, 'plugins_api' ), 20, 3 );
|
||||
add_filter( 'themes_api', array( __CLASS__, 'themes_api' ), 20, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -37,7 +38,31 @@ class WC_Helper_Plugin_Info {
|
|||
if ( 'plugin_information' !== $action ) {
|
||||
return $response;
|
||||
}
|
||||
return self::maybe_override_products_api( $response, $action, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme information callback for Woo themes.
|
||||
*
|
||||
* @param object $response The response core needs to display the modal.
|
||||
* @param string $action The requested themes_api() action.
|
||||
* @param object $args Arguments passed to themes_api().
|
||||
*/
|
||||
public static function themes_api( $response, $action, $args ) {
|
||||
if ( 'theme_information' !== $action ) {
|
||||
return $response;
|
||||
}
|
||||
return self::maybe_override_products_api( $response, $action, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the products API to fetch data from the Helper API if it's a Woo product.
|
||||
*
|
||||
* @param object $response The response core needs to display the modal.
|
||||
* @param string $action The requested action.
|
||||
* @param object $args Arguments passed to the API.
|
||||
*/
|
||||
public static function maybe_override_products_api( $response, $action, $args ) {
|
||||
if ( empty( $args->slug ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
@ -74,6 +99,11 @@ class WC_Helper_Plugin_Info {
|
|||
$results = json_decode( wp_remote_retrieve_body( $request ), true );
|
||||
if ( ! empty( $results ) ) {
|
||||
$response = (object) $results;
|
||||
|
||||
$product = array_shift( $products );
|
||||
if ( isset( $product['package'] ) ) {
|
||||
$response->download_link = $product['package'];
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
|
|
|
@ -0,0 +1,262 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce Admin Helper - React admin interface
|
||||
*
|
||||
* @package WooCommerce\Admin\Helper
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* WC_Helper_Subscriptions_API
|
||||
*
|
||||
* The main entry-point for all things related to the Marketplace Subscriptions API.
|
||||
* The Subscriptions API manages WooCommerce.com Subscriptions.
|
||||
*/
|
||||
class WC_Helper_Subscriptions_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 Subscriptions API.
|
||||
* These endpoints are used by the Marketplace Subscriptions React UI.
|
||||
*/
|
||||
public static function register_rest_routes() {
|
||||
register_rest_route(
|
||||
'wc/v3',
|
||||
'/marketplace/refresh',
|
||||
array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( __CLASS__, 'refresh' ),
|
||||
'permission_callback' => array( __CLASS__, 'get_permission' ),
|
||||
)
|
||||
);
|
||||
register_rest_route(
|
||||
'wc/v3',
|
||||
'/marketplace/subscriptions',
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( __CLASS__, 'get_subscriptions' ),
|
||||
'permission_callback' => array( __CLASS__, 'get_permission' ),
|
||||
)
|
||||
);
|
||||
register_rest_route(
|
||||
'wc/v3',
|
||||
'/marketplace/subscriptions/connect',
|
||||
array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( __CLASS__, 'connect' ),
|
||||
'permission_callback' => array( __CLASS__, 'get_permission' ),
|
||||
'args' => array(
|
||||
'product_key' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
register_rest_route(
|
||||
'wc/v3',
|
||||
'/marketplace/subscriptions/disconnect',
|
||||
array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( __CLASS__, 'disconnect' ),
|
||||
'permission_callback' => array( __CLASS__, 'get_permission' ),
|
||||
'args' => array(
|
||||
'product_key' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
register_rest_route(
|
||||
'wc/v3',
|
||||
'/marketplace/subscriptions/activate',
|
||||
array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( __CLASS__, 'activate' ),
|
||||
'permission_callback' => array( __CLASS__, 'get_permission' ),
|
||||
'args' => array(
|
||||
'product_key' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Extensions page can only be accessed by users with the manage_woocommerce
|
||||
* capability. So the API mimics that behavior.
|
||||
*/
|
||||
public static function get_permission() {
|
||||
return current_user_can( 'manage_woocommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch subscriptions from WooCommerce.com and serve them
|
||||
* as JSON.
|
||||
*/
|
||||
public static function get_subscriptions() {
|
||||
$subscriptions = WC_Helper::get_subscription_list_data();
|
||||
wp_send_json(
|
||||
array_values(
|
||||
$subscriptions
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh account and subscriptions from WooCommerce.com and serve subscriptions
|
||||
* as JSON.
|
||||
*/
|
||||
public static function refresh() {
|
||||
WC_Helper::refresh_helper_subscriptions();
|
||||
self::get_subscriptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect a WooCommerce.com subscription.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
*/
|
||||
public static function connect( $request ) {
|
||||
$product_key = $request->get_param( 'product_key' );
|
||||
try {
|
||||
$success = WC_Helper::activate_helper_subscription( $product_key );
|
||||
} catch ( Exception $e ) {
|
||||
wp_send_json_error(
|
||||
array(
|
||||
'message' => $e->getMessage(),
|
||||
),
|
||||
400
|
||||
);
|
||||
}
|
||||
if ( $success ) {
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'message' => __( 'Your subscription has been connected.', 'woocommerce' ),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
wp_send_json_error(
|
||||
array(
|
||||
'message' => __( 'There was an error connecting your subscription. Please try again.', 'woocommerce' ),
|
||||
),
|
||||
400
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect a WooCommerce.com subscription.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
*/
|
||||
public static function disconnect( $request ) {
|
||||
$product_key = $request->get_param( 'product_key' );
|
||||
try {
|
||||
$success = WC_Helper::deactivate_helper_subscription( $product_key );
|
||||
} catch ( Exception $e ) {
|
||||
wp_send_json_error(
|
||||
array(
|
||||
'message' => $e->getMessage(),
|
||||
),
|
||||
400
|
||||
);
|
||||
}
|
||||
if ( $success ) {
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'message' => __( 'Your subscription has been disconnected.', 'woocommerce' ),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
wp_send_json_error(
|
||||
array(
|
||||
'message' => __( 'There was an error disconnecting your subscription. Please try again.', 'woocommerce' ),
|
||||
),
|
||||
400
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate a WooCommerce.com product.
|
||||
* This activates the plugin/theme on the site.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
*/
|
||||
public static function activate( $request ) {
|
||||
$product_key = $request->get_param( 'product_key' );
|
||||
$subscription = WC_Helper::get_subscription( $product_key );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
wp_send_json_error(
|
||||
array(
|
||||
'message' => __( 'We couldn\'t find a subscription for this product.', 'woocommerce' ),
|
||||
),
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
if ( true !== $subscription['local']['installed'] || ! isset( $subscription['local']['active'] ) ) {
|
||||
wp_send_json_error(
|
||||
array(
|
||||
'message' => __( 'This product is not installed.', 'woocommerce' ),
|
||||
),
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
if ( true === $subscription['local']['active'] ) {
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'message' => __( 'This product is already active.', 'woocommerce' ),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if ( 'plugin' === $subscription['product_type'] ) {
|
||||
$success = activate_plugin( $subscription['local']['path'] );
|
||||
if ( is_wp_error( $success ) ) {
|
||||
wp_send_json_error(
|
||||
array(
|
||||
'message' => __( 'There was an error activating this plugin.', 'woocommerce' ),
|
||||
),
|
||||
400
|
||||
);
|
||||
}
|
||||
} elseif ( 'theme' === $subscription['product_type'] ) {
|
||||
switch_theme( $subscription['local']['slug'] );
|
||||
$theme = wp_get_theme();
|
||||
if ( $subscription['local']['slug'] !== $theme->get_stylesheet() ) {
|
||||
wp_send_json_error(
|
||||
array(
|
||||
'message' => __( 'There was an error activating this theme.', 'woocommerce' ),
|
||||
),
|
||||
400
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'message' => __( 'This product has been activated.', 'woocommerce' ),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
WC_Helper_Subscriptions_API::load();
|
|
@ -59,6 +59,7 @@ class WC_Helper {
|
|||
include_once dirname( __FILE__ ) . '/class-wc-helper-plugin-info.php';
|
||||
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';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -112,14 +113,17 @@ class WC_Helper {
|
|||
$woo_plugins = self::get_local_woo_plugins();
|
||||
$woo_themes = self::get_local_woo_themes();
|
||||
|
||||
$site_id = absint( $auth['site_id'] );
|
||||
$subscriptions = self::get_subscriptions();
|
||||
$subscriptions_list_data = self::get_subscription_list_data();
|
||||
$subscriptions = array_filter(
|
||||
$subscriptions_list_data,
|
||||
function( $subscription ) {
|
||||
return ! empty( $subscription['product_key'] );
|
||||
}
|
||||
);
|
||||
$updates = WC_Helper_Updater::get_update_data();
|
||||
$subscriptions_product_ids = wp_list_pluck( $subscriptions, 'product_id' );
|
||||
|
||||
foreach ( $subscriptions as &$subscription ) {
|
||||
$subscription['active'] = in_array( $site_id, $subscription['connections'] );
|
||||
|
||||
$subscription['activate_url'] = add_query_arg(
|
||||
array(
|
||||
'page' => 'wc-addons',
|
||||
|
@ -146,46 +150,22 @@ class WC_Helper {
|
|||
admin_url( 'admin.php' )
|
||||
);
|
||||
|
||||
$subscription['local'] = array(
|
||||
'installed' => false,
|
||||
'active' => false,
|
||||
'version' => null,
|
||||
);
|
||||
|
||||
$subscription['update_url'] = admin_url( 'update-core.php' );
|
||||
|
||||
$local = wp_list_filter( array_merge( $woo_plugins, $woo_themes ), array( '_product_id' => $subscription['product_id'] ) );
|
||||
|
||||
if ( ! empty( $local ) ) {
|
||||
$local = array_shift( $local );
|
||||
$subscription['local']['installed'] = true;
|
||||
$subscription['local']['version'] = $local['Version'];
|
||||
|
||||
if ( 'plugin' == $local['_type'] ) {
|
||||
if ( is_plugin_active( $local['_filename'] ) ) {
|
||||
$subscription['local']['active'] = true;
|
||||
} elseif ( is_multisite() && is_plugin_active_for_network( $local['_filename'] ) ) {
|
||||
$subscription['local']['active'] = true;
|
||||
}
|
||||
|
||||
$local = array_shift( $local );
|
||||
if ( 'plugin' === $local['_type'] ) {
|
||||
// A magic update_url.
|
||||
$subscription['update_url'] = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' ) . $local['_filename'], 'upgrade-plugin_' . $local['_filename'] );
|
||||
|
||||
} elseif ( 'theme' == $local['_type'] ) {
|
||||
if ( in_array( $local['_stylesheet'], array( get_stylesheet(), get_template() ) ) ) {
|
||||
$subscription['local']['active'] = true;
|
||||
}
|
||||
|
||||
} elseif ( 'theme' === $local['_type'] ) {
|
||||
// Another magic update_url.
|
||||
$subscription['update_url'] = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-theme&theme=' . $local['_stylesheet'] ), 'upgrade-theme_' . $local['_stylesheet'] );
|
||||
}
|
||||
}
|
||||
|
||||
$subscription['has_update'] = false;
|
||||
if ( $subscription['local']['installed'] && ! empty( $updates[ $subscription['product_id'] ] ) ) {
|
||||
$subscription['has_update'] = version_compare( $updates[ $subscription['product_id'] ]['version'], $subscription['local']['version'], '>' );
|
||||
}
|
||||
|
||||
$subscription['download_primary'] = true;
|
||||
$subscription['download_url'] = 'https://woo.com/my-account/downloads/';
|
||||
if ( ! $subscription['local']['installed'] && ! empty( $updates[ $subscription['product_id'] ] ) ) {
|
||||
|
@ -693,7 +673,7 @@ class WC_Helper {
|
|||
}
|
||||
|
||||
if ( ! empty( $_GET['wc-helper-deactivate'] ) ) {
|
||||
return self::_helper_subscription_deactivate();
|
||||
return self::helper_subscription_deactivate();
|
||||
}
|
||||
|
||||
if ( ! empty( $_GET['wc-helper-deactivate-plugin'] ) ) {
|
||||
|
@ -701,6 +681,32 @@ class WC_Helper {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get helper redirect URL.
|
||||
*
|
||||
* @param array $args Query args.
|
||||
* @param bool $redirect_to_wc_admin Whether to redirect to WC Admin.
|
||||
* @return string
|
||||
*/
|
||||
private static function get_helper_redirect_url( $args = array(), $redirect_to_wc_admin = false ) {
|
||||
global $current_screen;
|
||||
if ( true === $redirect_to_wc_admin && 'woocommerce_page_wc-addons' === $current_screen->id ) {
|
||||
return add_query_arg(
|
||||
array(
|
||||
'page' => 'wc-admin',
|
||||
'tab' => 'my-subscriptions',
|
||||
'path' => rawurlencode( '/extensions' ),
|
||||
),
|
||||
admin_url( 'admin.php' )
|
||||
);
|
||||
}
|
||||
|
||||
return add_query_arg(
|
||||
$args,
|
||||
admin_url( 'admin.php' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate a new OAuth connection.
|
||||
*/
|
||||
|
@ -710,13 +716,19 @@ class WC_Helper {
|
|||
wp_die( 'Could not verify nonce' );
|
||||
}
|
||||
|
||||
$redirect_url_args = array(
|
||||
'page' => 'wc-addons',
|
||||
'section' => 'helper',
|
||||
'wc-helper-return' => 1,
|
||||
'wc-helper-nonce' => wp_create_nonce( 'connect' ),
|
||||
);
|
||||
|
||||
if ( isset( $_GET['redirect-to-wc-admin'] ) ) {
|
||||
$redirect_url_args['redirect-to-wc-admin'] = 1;
|
||||
}
|
||||
|
||||
$redirect_uri = add_query_arg(
|
||||
array(
|
||||
'page' => 'wc-addons',
|
||||
'section' => 'helper',
|
||||
'wc-helper-return' => 1,
|
||||
'wc-helper-nonce' => wp_create_nonce( 'connect' ),
|
||||
),
|
||||
$redirect_url_args,
|
||||
admin_url( 'admin.php' )
|
||||
);
|
||||
|
||||
|
@ -776,7 +788,16 @@ class WC_Helper {
|
|||
* Fires when the Helper connection process is denied/cancelled.
|
||||
*/
|
||||
do_action( 'woocommerce_helper_denied' );
|
||||
wp_safe_redirect( admin_url( 'admin.php?page=wc-addons§ion=helper' ) );
|
||||
|
||||
wp_safe_redirect(
|
||||
self::get_helper_redirect_url(
|
||||
array(
|
||||
'page' => 'wc-addons',
|
||||
'section' => 'helper',
|
||||
),
|
||||
isset( $_GET['redirect-to-wc-admin'] )
|
||||
)
|
||||
);
|
||||
die();
|
||||
}
|
||||
|
||||
|
@ -831,13 +852,13 @@ class WC_Helper {
|
|||
}
|
||||
|
||||
wp_safe_redirect(
|
||||
add_query_arg(
|
||||
self::get_helper_redirect_url(
|
||||
array(
|
||||
'page' => 'wc-addons',
|
||||
'section' => 'helper',
|
||||
'wc-helper-status' => 'helper-connected',
|
||||
),
|
||||
admin_url( 'admin.php' )
|
||||
isset( $_GET['redirect-to-wc-admin'] )
|
||||
)
|
||||
);
|
||||
die();
|
||||
|
@ -857,13 +878,13 @@ class WC_Helper {
|
|||
*/
|
||||
do_action( 'woocommerce_helper_disconnected' );
|
||||
|
||||
$redirect_uri = add_query_arg(
|
||||
$redirect_uri = self::get_helper_redirect_url(
|
||||
array(
|
||||
'page' => 'wc-addons',
|
||||
'section' => 'helper',
|
||||
'wc-helper-status' => 'helper-disconnected',
|
||||
),
|
||||
admin_url( 'admin.php' )
|
||||
isset( $_GET['redirect-to-wc-admin'] )
|
||||
);
|
||||
|
||||
self::disconnect();
|
||||
|
@ -881,27 +902,36 @@ class WC_Helper {
|
|||
wp_die( 'Could not verify nonce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when Helper subscriptions are refreshed.
|
||||
*/
|
||||
do_action( 'woocommerce_helper_subscriptions_refresh' );
|
||||
self::refresh_helper_subscriptions();
|
||||
|
||||
$redirect_uri = add_query_arg(
|
||||
$redirect_uri = self::get_helper_redirect_url(
|
||||
array(
|
||||
'page' => 'wc-addons',
|
||||
'section' => 'helper',
|
||||
'filter' => self::get_current_filter(),
|
||||
'wc-helper-status' => 'helper-refreshed',
|
||||
),
|
||||
admin_url( 'admin.php' )
|
||||
isset( $_GET['redirect-to-wc-admin'] )
|
||||
);
|
||||
|
||||
wp_safe_redirect( $redirect_uri );
|
||||
die();
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush helper authentication cache.
|
||||
*/
|
||||
public static function refresh_helper_subscriptions() {
|
||||
/**
|
||||
* Fires when Helper subscriptions are refreshed.
|
||||
*
|
||||
* @since 8.3.0
|
||||
*/
|
||||
do_action( 'woocommerce_helper_subscriptions_refresh' );
|
||||
|
||||
self::_flush_authentication_cache();
|
||||
self::_flush_subscriptions_cache();
|
||||
self::_flush_updates_cache();
|
||||
|
||||
wp_safe_redirect( $redirect_uri );
|
||||
die();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -916,6 +946,41 @@ class WC_Helper {
|
|||
wp_die( 'Could not verify nonce' );
|
||||
}
|
||||
|
||||
try {
|
||||
$activated = self::activate_helper_subscription( $product_key, $product_id );
|
||||
} catch ( Exception $e ) {
|
||||
$activated = false;
|
||||
}
|
||||
|
||||
$redirect_uri = add_query_arg(
|
||||
array(
|
||||
'page' => 'wc-addons',
|
||||
'section' => 'helper',
|
||||
'filter' => self::get_current_filter(),
|
||||
'wc-helper-status' => $activated ? 'activate-success' : 'activate-error',
|
||||
'wc-helper-product-id' => $product_id,
|
||||
),
|
||||
admin_url( 'admin.php' )
|
||||
);
|
||||
|
||||
wp_safe_redirect( $redirect_uri );
|
||||
die();
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate helper subscription.
|
||||
*
|
||||
* @throws Exception If the subscription could not be activated or found.
|
||||
* @param string $product_key Subscription product key.
|
||||
* @return bool True if activated, false otherwise.
|
||||
*/
|
||||
public static function activate_helper_subscription( $product_key ) {
|
||||
$subscription = self::get_subscription( $product_key );
|
||||
if ( ! $subscription ) {
|
||||
throw new Exception( __( 'Subscription not found', 'woocommerce' ) );
|
||||
}
|
||||
$product_id = $subscription['product_id'];
|
||||
|
||||
// Activate subscription.
|
||||
$activation_response = WC_Helper_API::post(
|
||||
'activate',
|
||||
|
@ -954,6 +1019,7 @@ class WC_Helper {
|
|||
* @param array $activation_response The response object from wp_safe_remote_request().
|
||||
*/
|
||||
do_action( 'woocommerce_helper_subscription_activate_error', $product_id, $product_key, $activation_response );
|
||||
throw new Exception( $body['message'] ?? __( 'Unknown error', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
// Attempt to activate this plugin.
|
||||
|
@ -965,12 +1031,33 @@ class WC_Helper {
|
|||
self::_flush_subscriptions_cache();
|
||||
self::_flush_updates_cache();
|
||||
|
||||
return $activated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate a product subscription.
|
||||
*/
|
||||
private static function helper_subscription_deactivate() {
|
||||
$product_key = isset( $_GET['wc-helper-product-key'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-product-key'] ) ) : '';
|
||||
$product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0;
|
||||
|
||||
if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'deactivate:' . $product_key ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
self::log( 'Could not verify nonce in helper_subscription_deactivate' );
|
||||
wp_die( 'Could not verify nonce' );
|
||||
}
|
||||
|
||||
try {
|
||||
$deactivated = self::deactivate_helper_subscription( $product_key );
|
||||
} catch ( Exception $e ) {
|
||||
$deactivated = false;
|
||||
}
|
||||
|
||||
$redirect_uri = add_query_arg(
|
||||
array(
|
||||
'page' => 'wc-addons',
|
||||
'section' => 'helper',
|
||||
'filter' => self::get_current_filter(),
|
||||
'wc-helper-status' => $activated ? 'activate-success' : 'activate-error',
|
||||
'wc-helper-status' => $deactivated ? 'deactivate-success' : 'deactivate-error',
|
||||
'wc-helper-product-id' => $product_id,
|
||||
),
|
||||
admin_url( 'admin.php' )
|
||||
|
@ -982,15 +1069,17 @@ class WC_Helper {
|
|||
|
||||
/**
|
||||
* Deactivate a product subscription.
|
||||
*
|
||||
* @throws Exception If the subscription could not be deactivated or found.
|
||||
* @param string $product_key Subscription product key.
|
||||
* @return bool True if deactivated, false otherwise.
|
||||
*/
|
||||
private static function _helper_subscription_deactivate() {
|
||||
$product_key = isset( $_GET['wc-helper-product-key'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-product-key'] ) ) : '';
|
||||
$product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0;
|
||||
|
||||
if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'deactivate:' . $product_key ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
self::log( 'Could not verify nonce in _helper_subscription_deactivate' );
|
||||
wp_die( 'Could not verify nonce' );
|
||||
public static function deactivate_helper_subscription( $product_key ) {
|
||||
$subscription = self::get_subscription( $product_key );
|
||||
if ( ! $subscription ) {
|
||||
throw new Exception( __( 'Subscription not found', 'woocommerce' ) );
|
||||
}
|
||||
$product_id = $subscription['product_id'];
|
||||
|
||||
$deactivation_response = WC_Helper_API::post(
|
||||
'deactivate',
|
||||
|
@ -1027,23 +1116,14 @@ class WC_Helper {
|
|||
* @param array $deactivation_response The response object from wp_safe_remote_request().
|
||||
*/
|
||||
do_action( 'woocommerce_helper_subscription_deactivate_error', $product_id, $product_key, $deactivation_response );
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $deactivation_response ), true );
|
||||
throw new Exception( $body['message'] ?? __( 'Unknown error', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
self::_flush_subscriptions_cache();
|
||||
|
||||
$redirect_uri = add_query_arg(
|
||||
array(
|
||||
'page' => 'wc-addons',
|
||||
'section' => 'helper',
|
||||
'filter' => self::get_current_filter(),
|
||||
'wc-helper-status' => $deactivated ? 'deactivate-success' : 'deactivate-error',
|
||||
'wc-helper-product-id' => $product_id,
|
||||
),
|
||||
admin_url( 'admin.php' )
|
||||
);
|
||||
|
||||
wp_safe_redirect( $redirect_uri );
|
||||
die();
|
||||
return $deactivated;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1148,10 +1228,63 @@ class WC_Helper {
|
|||
if ( ! empty( $subscriptions ) ) {
|
||||
return $single ? array_shift( $subscriptions ) : $subscriptions;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get locally installed plugins
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_local_plugins() {
|
||||
if ( ! function_exists( 'get_plugins' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
}
|
||||
$plugins = get_plugins();
|
||||
|
||||
$output_plugins = array();
|
||||
foreach ( $plugins as $filename => $data ) {
|
||||
array_push(
|
||||
$output_plugins,
|
||||
array(
|
||||
'_filename' => $filename,
|
||||
'_type' => 'plugin',
|
||||
'slug' => dirname( $filename ),
|
||||
'Version' => $data['Version'],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $output_plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get locally installed themes.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_local_themes() {
|
||||
if ( ! function_exists( 'wp_get_themes' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/theme.php';
|
||||
}
|
||||
$themes = wp_get_themes();
|
||||
|
||||
$output_themes = array();
|
||||
foreach ( $themes as $theme ) {
|
||||
array_push(
|
||||
$output_themes,
|
||||
array(
|
||||
'_filename' => $theme->get_stylesheet() . '/style.css',
|
||||
'_stylesheet' => $theme->get_stylesheet(),
|
||||
'_type' => 'theme',
|
||||
'slug' => $theme->get_stylesheet(),
|
||||
'Version' => $theme->get( 'Version' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
return $output_themes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a list of data about locally installed Woo extensions.
|
||||
*/
|
||||
|
@ -1256,6 +1389,7 @@ class WC_Helper {
|
|||
'_product_id' => absint( $product_id ),
|
||||
'_file_id' => $file_id,
|
||||
'_type' => 'theme',
|
||||
'slug' => dirname( $theme->get_stylesheet() ),
|
||||
);
|
||||
|
||||
$woo_themes[ $data['_filename'] ] = $data;
|
||||
|
@ -1313,6 +1447,256 @@ class WC_Helper {
|
|||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subscription data for a given product key.
|
||||
*
|
||||
* @param string $product_key Subscription product key.
|
||||
* @return array|bool The array containing sub data or false.
|
||||
*/
|
||||
public static function get_subscription( $product_key ) {
|
||||
$subscriptions = wp_list_filter(
|
||||
self::get_subscriptions(),
|
||||
array( 'product_key' => $product_key )
|
||||
);
|
||||
|
||||
if ( empty( $subscriptions ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$subscription = array_shift( $subscriptions );
|
||||
$subscription['local'] = self::get_subscription_local_data( $subscription );
|
||||
return $subscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the connected user's subscription list data.
|
||||
* This is used by the My Subscriptions page.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_subscription_list_data() {
|
||||
$subscriptions = self::get_subscriptions();
|
||||
|
||||
// Installed plugins and themes, with or without an active subscription.
|
||||
$woo_plugins = self::get_local_woo_plugins();
|
||||
$woo_themes = self::get_local_woo_themes();
|
||||
|
||||
$subscriptions_product_ids = wp_list_pluck( $subscriptions, 'product_id' );
|
||||
|
||||
$auth = WC_Helper_Options::get( 'auth' );
|
||||
$site_id = isset( $auth['site_id'] ) ? absint( $auth['site_id'] ) : 0;
|
||||
|
||||
// Installed products without a subscription.
|
||||
foreach ( array_merge( $woo_plugins, $woo_themes ) as $filename => $data ) {
|
||||
if ( in_array( $data['_product_id'], $subscriptions_product_ids, true ) ) {
|
||||
continue;
|
||||
}
|
||||
$subscriptions[] = array(
|
||||
'product_key' => '',
|
||||
'product_id' => $data['_product_id'],
|
||||
'product_name' => $data['Name'],
|
||||
'product_url' => $data['PluginURI'] ?? '',
|
||||
'zip_slug' => $data['slug'],
|
||||
'documentation_url' => '',
|
||||
'key_type' => '',
|
||||
'key_type_label' => '',
|
||||
'lifetime' => false,
|
||||
'product_status' => 'publish',
|
||||
'connections' => array(),
|
||||
'expires' => 0,
|
||||
'expired' => true,
|
||||
'expiring' => false,
|
||||
'sites_max' => 0,
|
||||
'sites_active' => 0,
|
||||
'autorenew' => false,
|
||||
'maxed' => false,
|
||||
);
|
||||
}
|
||||
|
||||
foreach ( $subscriptions as &$subscription ) {
|
||||
$subscription['active'] = in_array( $site_id, $subscription['connections'], true );
|
||||
|
||||
$updates = WC_Helper_Updater::get_update_data();
|
||||
|
||||
$subscription['local'] = self::get_subscription_local_data( $subscription );
|
||||
|
||||
$subscription['has_update'] = false;
|
||||
if ( $subscription['local']['installed'] && ! empty( $updates[ $subscription['product_id'] ] ) ) {
|
||||
$subscription['has_update'] = version_compare( $updates[ $subscription['product_id'] ]['version'], $subscription['local']['version'], '>' );
|
||||
}
|
||||
|
||||
if ( ! empty( $updates[ $subscription['product_id'] ] ) ) {
|
||||
$subscription['version'] = $updates[ $subscription['product_id'] ]['version'];
|
||||
}
|
||||
}
|
||||
|
||||
// Sort subscriptions by name and expiration date.
|
||||
usort(
|
||||
$subscriptions,
|
||||
function( $a, $b ) {
|
||||
$compare_value = strcasecmp( $a['product_name'], $b['product_name'] );
|
||||
if ( 0 === $compare_value ) {
|
||||
return strcasecmp( $a['expires'], $b['expires'] );
|
||||
}
|
||||
return $compare_value;
|
||||
}
|
||||
);
|
||||
|
||||
// Add subscription install flags after the active and local data is set.
|
||||
foreach ( $subscriptions as &$subscription ) {
|
||||
$subscription['subscription_available'] = self::is_subscription_available( $subscription, $subscriptions );
|
||||
$subscription['subscription_installed'] = self::is_subscription_installed( $subscription, $subscriptions );
|
||||
}
|
||||
|
||||
// Break the by-ref.
|
||||
unset( $subscription );
|
||||
|
||||
return $subscriptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a subscription is available to use.
|
||||
* That is, is not already active and hasn't expired, and there are no other subscriptions
|
||||
* for this product already active on this site.
|
||||
*
|
||||
* @param array $subscription The subscription we're checking.
|
||||
* @param array $subscriptions The list of all the user's subscriptions.
|
||||
* @return bool True if multiple licenses exist, false otherwise.
|
||||
*/
|
||||
public static function is_subscription_available( $subscription, $subscriptions ) {
|
||||
if ( true === $subscription['active'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( true === $subscription['expired'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$product_subscriptions = wp_list_filter(
|
||||
$subscriptions,
|
||||
array(
|
||||
'product_id' => $subscription['product_id'],
|
||||
'active' => true,
|
||||
)
|
||||
);
|
||||
|
||||
// If there are no subscriptions for this product already active on this site, then it's available.
|
||||
if ( empty( $product_subscriptions ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if product relating to a subscription is installed.
|
||||
* This method will return true if the product is installed, but will exclude subscriptions for the same product that are not in use.
|
||||
* If a product is installed and inactive, this will ensure that one subscription is marked as installed.
|
||||
*
|
||||
* @param array $subscription The subscription we're checking.
|
||||
* @param array $subscriptions The list of all the user's subscriptions.
|
||||
* @return bool True if installed, false otherwise.
|
||||
*/
|
||||
public static function is_subscription_installed( $subscription, $subscriptions ) {
|
||||
if ( false === $subscription['local']['installed'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the subscription is active, then it's installed.
|
||||
if ( true === $subscription['active'] ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$product_subscriptions = wp_list_filter(
|
||||
$subscriptions,
|
||||
array(
|
||||
'product_id' => $subscription['product_id'],
|
||||
)
|
||||
);
|
||||
if ( empty( $product_subscriptions ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there are no other subscriptions for this product, then it's installed.
|
||||
if ( 1 === count( $product_subscriptions ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$active_subscription = wp_list_filter(
|
||||
$product_subscriptions,
|
||||
array(
|
||||
'active' => true,
|
||||
)
|
||||
);
|
||||
|
||||
// If there is another active subscription, this subscription is not installed.
|
||||
// If the current subscription is active, it would already return true above.
|
||||
if ( ! empty( $active_subscription ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there are multiple subscriptions, but no active subscriptions, then mark the first one as installed.
|
||||
$product_subscription = array_shift( $product_subscriptions );
|
||||
if ( $product_subscription['product_key'] === $subscription['product_key'] ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add local data to a subscription.
|
||||
*
|
||||
* @param array $subscription The subscription data.
|
||||
* @return array The subscription data with local data added.
|
||||
*/
|
||||
public static function get_subscription_local_data( array $subscription ) {
|
||||
$local_plugins = self::get_local_plugins();
|
||||
$local_themes = self::get_local_themes();
|
||||
|
||||
$installed_product = wp_list_filter(
|
||||
array_merge( $local_plugins, $local_themes ),
|
||||
array( 'slug' => $subscription['zip_slug'] )
|
||||
);
|
||||
$installed_product = array_shift( $installed_product );
|
||||
|
||||
if ( empty( $installed_product ) ) {
|
||||
return array(
|
||||
'installed' => false,
|
||||
'active' => false,
|
||||
'version' => null,
|
||||
'type' => null,
|
||||
'slug' => null,
|
||||
'path' => null,
|
||||
);
|
||||
}
|
||||
|
||||
$local_data = array(
|
||||
'installed' => true,
|
||||
'active' => false,
|
||||
'version' => $installed_product['Version'],
|
||||
'type' => $installed_product['_type'],
|
||||
'slug' => null,
|
||||
'path' => $installed_product['_filename'],
|
||||
);
|
||||
|
||||
if ( 'plugin' === $installed_product['_type'] ) {
|
||||
$local_data['slug'] = $installed_product['slug'];
|
||||
if ( is_plugin_active( $installed_product['_filename'] ) ) {
|
||||
$local_data['active'] = true;
|
||||
} elseif ( is_multisite() && is_plugin_active_for_network( $installed_product['_filename'] ) ) {
|
||||
$local_data['active'] = true;
|
||||
}
|
||||
} elseif ( 'theme' === $installed_product['_type'] ) {
|
||||
$local_data['slug'] = $installed_product['_stylesheet'];
|
||||
if ( in_array( $installed_product['_stylesheet'], array( get_stylesheet(), get_template() ), true ) ) {
|
||||
$local_data['active'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $local_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs when any plugin is activated.
|
||||
*
|
||||
|
|
|
@ -12,12 +12,20 @@ use Automattic\WooCommerce\Utilities\FeaturesUtil;
|
|||
*/
|
||||
class Marketplace {
|
||||
|
||||
const MARKETPLACE_TAB_SLUG = 'woo';
|
||||
|
||||
/**
|
||||
* Class initialization, to be executed when the class is resolved by the container.
|
||||
*/
|
||||
final public function init() {
|
||||
if ( FeaturesUtil::feature_is_enabled( 'marketplace' ) ) {
|
||||
add_action( 'admin_menu', array( $this, 'register_pages' ), 70 );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
|
||||
|
||||
// Add a Woo Marketplace link to the plugin install action links.
|
||||
add_filter( 'install_plugins_tabs', array( $this, 'add_woo_plugin_install_action_link' ) );
|
||||
add_action( 'install_plugins_pre_woo', array( $this, 'maybe_open_woo_tab' ) );
|
||||
add_action( 'admin_print_styles-plugin-install.php', array( $this, 'add_plugins_page_styles' ) );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,4 +65,82 @@ class Marketplace {
|
|||
*/
|
||||
return apply_filters( 'woocommerce_marketplace_menu_items', $marketplace_pages );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue update script.
|
||||
*
|
||||
* @param string $hook_suffix The current admin page.
|
||||
*/
|
||||
public function enqueue_scripts( $hook_suffix ) {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Recommended
|
||||
if ( 'woocommerce_page_wc-admin' !== $hook_suffix ) {
|
||||
return;
|
||||
};
|
||||
|
||||
if ( ! isset( $_GET['path'] ) || '/extensions' !== $_GET['path'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enqueue WordPress updates script to enable plugin and theme installs and updates.
|
||||
wp_enqueue_script( 'updates' );
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Recommended
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Woo Marketplace link to the plugin install action links.
|
||||
*
|
||||
* @param array $tabs Plugins list tabs.
|
||||
* @return array
|
||||
*/
|
||||
public function add_woo_plugin_install_action_link( $tabs ) {
|
||||
$tabs[ self::MARKETPLACE_TAB_SLUG ] = 'Woo';
|
||||
return $tabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the Woo tab when the user clicks on the Woo link in the plugin installer.
|
||||
*/
|
||||
public function maybe_open_woo_tab() {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Recommended
|
||||
if ( ! isset( $_GET['tab'] ) || self::MARKETPLACE_TAB_SLUG !== $_GET['tab'] ) {
|
||||
return;
|
||||
}
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
$woo_url = add_query_arg(
|
||||
array(
|
||||
'page' => 'wc-admin',
|
||||
'path' => '/extensions',
|
||||
'tab' => 'extensions',
|
||||
'ref' => 'plugins',
|
||||
),
|
||||
admin_url( 'admin.php' )
|
||||
);
|
||||
|
||||
wp_safe_redirect( $woo_url );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add styles to the plugin install page.
|
||||
*/
|
||||
public function add_plugins_page_styles() {
|
||||
?>
|
||||
<style>
|
||||
.plugin-install-woo > a::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M8.33321 3H12.9999V7.66667H11.9999V4.70711L8.02009 8.68689L7.31299 7.97978L11.2928 4H8.33321V3Z' fill='%23646970'/%3E%3Cpath d='M6.33333 4.1665H4.33333C3.8731 4.1665 3.5 4.5396 3.5 4.99984V11.6665C3.5 12.1267 3.8731 12.4998 4.33333 12.4998H11C11.4602 12.4998 11.8333 12.1267 11.8333 11.6665V9.6665' stroke='%23646970'/%3E%3C/svg%3E%0A");
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-repeat: no-repeat;
|
||||
vertical-align: text-top;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.plugin-install-woo:hover > a::after {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M8.33321 3H12.9999V7.66667H11.9999V4.70711L8.02009 8.68689L7.31299 7.97978L11.2928 4H8.33321V3Z' fill='%23135E96'/%3E%3Cpath d='M6.33333 4.1665H4.33333C3.8731 4.1665 3.5 4.5396 3.5 4.99984V11.6665C3.5 12.1267 3.8731 12.4998 4.33333 12.4998H11C11.4602 12.4998 11.8333 12.1267 11.8333 11.6665V9.6665' stroke='%23135E96'/%3E%3C/svg%3E%0A");
|
||||
}
|
||||
</style>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
|
|
@ -232,6 +232,9 @@ class Settings {
|
|||
$settings['allowMarketplaceSuggestions'] = WC_Marketplace_Suggestions::allow_suggestions();
|
||||
$settings['connectNonce'] = wp_create_nonce( 'connect' );
|
||||
$settings['wcpay_welcome_page_connect_nonce'] = wp_create_nonce( 'wcpay-connect' );
|
||||
$settings['wc_helper_nonces'] = array(
|
||||
'refresh' => wp_create_nonce( 'refresh' ),
|
||||
);
|
||||
|
||||
$settings['features'] = $this->get_features();
|
||||
|
||||
|
|
Loading…
Reference in New Issue