My subscriptions error notices (#41124)

* Marketplace: Populate the table rows with components

* Marketplace: add links to dropdown menu and fix fallback product icons

* Marketplace: fix popover width

* Add My subscriptions install (#40630)

* Marketplace: add plugin install from the subscriptions page

Co-authored-by: berislav grgičak <berislav.grgicak@gmail.com>

* Marketplace: Use the activation function to show install button

---------

Co-authored-by: raicem <unalancem@gmail.com>

* Add WP updates script to the extensions page

* Add update button

* Add update data to subscriptions

* Update plugins

* Prevent update if license unavailable

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

* Remove all data from API

* Linter fixes

* Linter fixes

* Remove merge string

* Update link style

* Add comment for updates.js

* Prevent updates if required data is missing

* Return removed slug code

* Add renew modal

* Update install to run until new data loaded

* Add activate modal

* Add connect modal

* Add renew button

* Renewal button

* Rename activate to connect

* Add subscribe button

* Add action buttons

* Remove unused const

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

* Switch to WP installer

* Use WP installer

* Remove install endpoint

* Fix php warning

* Add download_link if subscription exists

* My subscriptions action modals (#40934)

* Add renew modal

* Update install to run until new data loaded

* Add activate modal

* Add connect modal

* Add renew button

* Renewal button

* Rename activate to connect

* Add subscribe button

* Add action buttons

* Remove unused const

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

* Update plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.scss

Co-authored-by: And Finally <andfinally@users.noreply.github.com>

* Update import path

---------

Co-authored-by: And Finally <andfinally@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>

* Use product slugs for installing

* Add store for installing state

* Add theme install support

* Product activate endpoint

* Activate after install

* PHP warning

* Update context

* Debugging

* Install context

* Linter

* Simplify context

* Use Redux instead of context

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

* Replace ~ with relative paths

* Add error notices

* Move update to functions

* Add notice store

* Fix linter errors

* Remove temp file

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

* Don't autoremove notices

* Add status to notices

* Send just required path field

* Subvscribe hover color

* Css linter fix

* Fix error notice style

* Update manage button text

* Linter fixes

---------

Co-authored-by: raicem <unalancem@gmail.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: And Finally <andfinally@users.noreply.github.com>
This commit is contained in:
berislav grgičak 2023-11-07 09:21:12 +01:00 committed by GitHub
parent 19df04921e
commit 154c69c2d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 358 additions and 71 deletions

View File

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

View File

@ -110,10 +110,13 @@
padding: 0 12px;
}
.woocommerce-marketplace__my-subscriptions__table .components-button.is-link {
.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;
@ -157,3 +160,50 @@
margin-right: $grid-unit-10;
}
}
.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;
}
}

View File

@ -3,9 +3,9 @@
*/
import { getNewPath } from '@woocommerce/navigation';
import { Button, Tooltip } from '@wordpress/components';
import { help } from '@wordpress/icons';
import { useContext } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { help } from '@wordpress/icons';
/**
* Internal dependencies
@ -22,6 +22,7 @@ import {
installedSubscriptionRow,
} from './table/table-rows';
import { Subscription } from './types';
import Notices from './notices';
export default function MySubscriptions(): JSX.Element {
const { subscriptions, isLoading } = useContext( SubscriptionsContext );
@ -78,6 +79,9 @@ export default function MySubscriptions(): JSX.Element {
return (
<div className="woocommerce-marketplace__my-subscriptions">
<section className="woocommerce-marketplace__my-subscriptions__notices">
<Notices />
</section>
<section>
<h2 className="woocommerce-marketplace__my-subscriptions__header">
{ __( 'Installed on this store', 'woocommerce' ) }

View File

@ -0,0 +1,49 @@
/**
* 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
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 }</>;
}

View File

@ -2,7 +2,6 @@
* External dependencies
*/
import { Button, Icon } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { useContext, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
@ -10,8 +9,13 @@ import { __, sprintf } from '@wordpress/i18n';
* Internal dependencies
*/
import { SubscriptionsContext } from '../../../../contexts/subscriptions-context';
import { connectProduct } from '../../../../utils/functions';
import {
addNotice,
connectProduct,
removeNotice,
} from '../../../../utils/functions';
import { Subscription } from '../../types';
import { NoticeStatus } from '../../../../contexts/types';
interface ConnectProps {
subscription: Subscription;
@ -21,21 +25,22 @@ interface ConnectProps {
export default function ConnectButton( props: ConnectProps ) {
const [ isConnecting, setIsConnecting ] = useState( false );
const { createWarningNotice, createSuccessNotice } =
useDispatch( 'core/notices' );
const { loadSubscriptions } = useContext( SubscriptionsContext );
const connect = () => {
setIsConnecting( true );
removeNotice( props.subscription.product_key );
connectProduct( props.subscription )
.then( () => {
loadSubscriptions( false ).then( () => {
createSuccessNotice(
addNotice(
props.subscription.product_key,
sprintf(
// translators: %s is the product name.
__( '%s successfully connected.', 'woocommerce' ),
props.subscription.product_name
),
NoticeStatus.Success,
{
icon: <Icon icon="yes" />,
}
@ -47,12 +52,14 @@ export default function ConnectButton( props: ConnectProps ) {
} );
} )
.catch( () => {
createWarningNotice(
addNotice(
props.subscription.product_key,
sprintf(
// translators: %s is the product name.
__( '%s couldnt be connected.', 'woocommerce' ),
props.subscription.product_name
),
NoticeStatus.Error,
{
actions: [
{

View File

@ -2,7 +2,7 @@
* External dependencies
*/
import { Button, Icon } from '@wordpress/components';
import { dispatch, useDispatch, useSelect } from '@wordpress/data';
import { dispatch, useSelect } from '@wordpress/data';
import { useContext } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
@ -10,17 +10,20 @@ import { __, sprintf } from '@wordpress/i18n';
* Internal dependencies
*/
import { SubscriptionsContext } from '../../../../contexts/subscriptions-context';
import { installProduct } from '../../../../utils/functions';
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 { createWarningNotice, createSuccessNotice } =
useDispatch( 'core/notices' );
const { loadSubscriptions } = useContext( SubscriptionsContext );
const loading: boolean = useSelect(
@ -45,15 +48,18 @@ export default function Install( props: InstallProps ) {
const install = () => {
startInstall();
removeNotice( props.subscription.product_key );
installProduct( props.subscription )
.then( () => {
loadSubscriptions( false ).then( () => {
createSuccessNotice(
addNotice(
props.subscription.product_key,
sprintf(
// translators: %s is the product name.
__( '%s successfully installed.', 'woocommerce' ),
props.subscription.product_name
),
NoticeStatus.Success,
{
icon: <Icon icon="yes" />,
}
@ -71,14 +77,19 @@ export default function Install( props: InstallProps ) {
if ( error?.success === false && error?.data.message ) {
errorMessage += ' ' + error.data.message;
}
createWarningNotice( errorMessage, {
actions: [
{
label: __( 'Try again', 'woocommerce' ),
onClick: install,
},
],
} );
addNotice(
props.subscription.product_key,
errorMessage,
NoticeStatus.Error,
{
actions: [
{
label: __( 'Try again', 'woocommerce' ),
onClick: install,
},
],
}
);
stopInstall();
} );
} );

View File

@ -2,7 +2,6 @@
* External dependencies
*/
import { Button, Icon } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { useContext, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
@ -14,6 +13,12 @@ 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;
@ -22,8 +27,6 @@ interface UpdateProps {
export default function Update( props: UpdateProps ) {
const [ showModal, setShowModal ] = useState( false );
const [ isUpdating, setIsUpdating ] = useState( false );
const { createWarningNotice, createSuccessNotice } =
useDispatch( 'core/notices' );
const { loadSubscriptions } = useContext( SubscriptionsContext );
const canUpdate =
@ -37,13 +40,16 @@ export default function Update( props: UpdateProps ) {
setShowModal( true );
return;
}
removeNotice( props.subscription.product_key );
if ( ! window.wp.updates ) {
createWarningNotice(
addNotice(
props.subscription.product_key,
sprintf(
// translators: %s is the product name.
__( '%s couldnt be updated.', 'woocommerce' ),
props.subscription.product_name
),
NoticeStatus.Error,
{
actions: [
{
@ -63,36 +69,33 @@ export default function Update( props: UpdateProps ) {
setIsUpdating( true );
const action =
props.subscription.local.type === 'plugin'
? 'update-plugin'
: 'update-theme';
window.wp.updates
.ajax( action, {
slug: props.subscription.local.slug,
plugin: props.subscription.local.path,
theme: props.subscription.local.path,
} )
updateProduct( props.subscription )
.then( () => {
loadSubscriptions( false );
createSuccessNotice(
sprintf(
// translators: %s is the product name.
__( '%s updated successfully.', 'woocommerce' ),
props.subscription.product_name
),
{
icon: <Icon icon="yes" />,
}
);
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,
{
icon: <Icon icon="yes" />,
}
);
setIsUpdating( false );
} );
} )
.catch( () => {
createWarningNotice(
addNotice(
props.subscription.product_key,
sprintf(
// translators: %s is the product name.
__( '%s couldnt be updated.', 'woocommerce' ),
props.subscription.product_name
),
NoticeStatus.Error,
{
actions: [
{
@ -102,8 +105,6 @@ export default function Update( props: UpdateProps ) {
],
}
);
} )
.always( () => {
setIsUpdating( false );
} );
}

View File

@ -15,7 +15,7 @@ export default function ActionsDropdownMenu() {
) }
controls={ [
{
title: __( 'Manage in Woo.com', 'woocommerce' ),
title: __( 'Manage on Woo.com', 'woocommerce' ),
icon: external,
onClick: () => {
window.location.href =

View File

@ -3,31 +3,22 @@
*/
import { createReduxStore, register } from '@wordpress/data';
/**
* Internal dependencies
*/
import { InstallingState } from './types';
const INSTALLING_STORE_NAME = 'woocommerce-admin/installing';
interface InstallingState {
installingProducts: string[];
export interface InstallingStateErrorAction {
label: string;
onClick: () => void;
}
const DEFAULT_STATE: InstallingState = {
installingProducts: [],
};
const actions = {
startInstalling( productKey: string ) {
return {
type: 'START_INSTALLING',
productKey,
};
},
stopInstalling( productKey: string ) {
return {
type: 'STOP_INSTALLING',
productKey,
};
},
};
const store = createReduxStore( INSTALLING_STORE_NAME, {
reducer( state: InstallingState | undefined = DEFAULT_STATE, action ) {
switch ( action.type ) {
@ -52,9 +43,20 @@ const store = createReduxStore( INSTALLING_STORE_NAME, {
return state;
},
actions,
actions: {
startInstalling( productKey: string ) {
return {
type: 'START_INSTALLING',
productKey,
};
},
stopInstalling( productKey: string ) {
return {
type: 'STOP_INSTALLING',
productKey,
};
},
},
selectors: {
isInstalling(
state: InstallingState | undefined,

View File

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

View File

@ -1,3 +1,8 @@
/**
* External dependencies
*/
import { Options } from '@wordpress/notices';
/**
* Internal dependencies
*/
@ -17,3 +22,25 @@ export type SubscriptionsContextType = {
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[];
}

View File

@ -3,7 +3,8 @@
*/
import apiFetch from '@wordpress/api-fetch';
import { __, sprintf } from '@wordpress/i18n';
import { dispatch } from '@wordpress/data';
import { Options } from '@wordpress/notices';
/**
* Internal dependencies
*/
@ -21,6 +22,8 @@ import {
SearchAPIJSONType,
SearchAPIProductType,
} from '../components/product-list/types';
import { NoticeStatus } from '../contexts/types';
import { noticeStore } from '../contexts/notice-store';
interface ProductGroup {
id: string;
@ -314,6 +317,35 @@ function installProduct( subscription: Subscription ): Promise< void > {
} );
}
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 {
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,
@ -342,4 +374,7 @@ export {
fetchSearchResults,
fetchSubscriptions,
installProduct,
updateProduct,
addNotice,
removeNotice,
};

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Comment: Add My subscriptions error notices and move existing notices to helper functions.