Add subscription install modal (#42009)

* Install modal

* Add connect button

* Small product card

* Add install functionality

* Add no subscription error

* Fix error notice loading

* Connect style

* Add success state

* Fix admin urls

* Add error message to failed install events

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

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
berislav grgičak 2023-12-14 12:45:40 +01:00 committed by GitHub
parent 0903a53664
commit 2964800f27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 417 additions and 13 deletions

View File

@ -1,3 +1,8 @@
/**
* Internal dependencies
*/
import { ADMIN_URL } from '../../utils/admin-settings';
export const DEFAULT_TAB_KEY = 'discover'; export const DEFAULT_TAB_KEY = 'discover';
export const MARKETPLACE_HOST = 'https://woo.com'; export const MARKETPLACE_HOST = 'https://woo.com';
export const MARKETPLACE_PATH = '/extensions'; export const MARKETPLACE_PATH = '/extensions';
@ -11,3 +16,4 @@ export const MARKETPLACE_CART_PATH = MARKETPLACE_HOST + '/cart/';
export const MARKETPLACE_COLLABORATION_PATH = export const MARKETPLACE_COLLABORATION_PATH =
MARKETPLACE_HOST + MARKETPLACE_HOST +
'/document/managing-woocommerce-com-subscriptions/#transfer-a-woocommerce-com-subscription'; '/document/managing-woocommerce-com-subscriptions/#transfer-a-woocommerce-com-subscription';
export const WP_ADMIN_PLUGIN_LIST_URL = ADMIN_URL + '/plugins.php';

View File

@ -56,7 +56,12 @@
@media screen and (min-width: $break-small) { @media screen and (min-width: $break-small) {
.woocommerce-marketplace { .woocommerce-marketplace {
&__header-account-modal { &__header-account-modal {
max-width: 350px; max-width: $modal-min-width;
&.has-size-medium {
max-width: $modal-width-medium;
width: 100%;
}
} }
} }
} }

View File

@ -233,9 +233,11 @@
} }
} }
.woocommerce-marketplace__my-subscriptions .components-button.is-link { .woocommerce-marketplace__my-subscriptions .components-button {
&.is-link {
text-decoration: none; text-decoration: none;
padding: 6px 12px; padding: 6px 12px;
}
} }
.woocommerce-marketplace__my-subscriptions .woocommerce-marketplace__my-subscriptions
@ -300,6 +302,7 @@
box-shadow: 0 2px 6px 0 rgba($gray-100, 0.05); box-shadow: 0 2px 6px 0 rgba($gray-100, 0.05);
border: 1px solid var(--gutenberg-gray-100, #f0f0f0); border: 1px solid var(--gutenberg-gray-100, #f0f0f0);
padding-right: $grid-unit-15; padding-right: $grid-unit-15;
position: relative;
&::before { &::before {
content: ''; content: '';
@ -338,3 +341,52 @@
padding: 0; padding: 0;
} }
} }
.woocommerce-marketplace__header-account-modal-overlay {
.components-modal__header {
padding-bottom: $grid-unit-20;
.components-modal__header-heading {
font-weight: 400;
font-size: 20px;
line-height: 28px;
}
}
.components-notice {
padding: $grid-unit-15 $grid-unit-20;
border-left: none;
margin: $grid-unit-20 0;
&.is-warning {
background-color: var(--wp-yellow-yellow-0, #fcf9e8);
align-items: start;
&::before {
content: '';
/* stylelint-disable-next-line function-url-quotes */
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20Z' stroke='%23614200' stroke-width='1.5'/%3E%3Cpath d='M13 7H11V13H13V7Z' fill='%23614200'/%3E%3Cpath d='M13 15H11V17H13V15Z' fill='%23614200'/%3E%3C/svg%3E");
margin-right: $grid-unit-15;
width: 24px;
height: 24px;
}
.components-notice__content {
margin: 0;
}
}
}
.components-button-group .components-button {
&.is-primary {
box-shadow: none;
}
&.is-secondary {
box-shadow: inset 0 0 0 1px var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9));
}
}
.woocommerce-marketplace__product-card {
margin: $grid-unit-20 0;
}
}

View File

@ -23,6 +23,7 @@ import {
import { Subscription } from './types'; import { Subscription } from './types';
import { RefreshButton } from './table/actions/refresh-button'; import { RefreshButton } from './table/actions/refresh-button';
import Notices from './notices'; import Notices from './notices';
import InstallModal from './table/actions/install-modal';
export default function MySubscriptions(): JSX.Element { export default function MySubscriptions(): JSX.Element {
const { subscriptions, isLoading } = useContext( SubscriptionsContext ); const { subscriptions, isLoading } = useContext( SubscriptionsContext );
@ -58,6 +59,7 @@ export default function MySubscriptions(): JSX.Element {
if ( ! wccomSettings?.isConnected ) { if ( ! wccomSettings?.isConnected ) {
return ( return (
<div className="woocommerce-marketplace__my-subscriptions--connect"> <div className="woocommerce-marketplace__my-subscriptions--connect">
<InstallModal />
<div className="woocommerce-marketplace__my-subscriptions__icon" /> <div className="woocommerce-marketplace__my-subscriptions__icon" />
<h2 className="woocommerce-marketplace__my-subscriptions__header"> <h2 className="woocommerce-marketplace__my-subscriptions__header">
{ __( 'Manage your subscriptions', 'woocommerce' ) } { __( 'Manage your subscriptions', 'woocommerce' ) }
@ -77,6 +79,7 @@ export default function MySubscriptions(): JSX.Element {
return ( return (
<div className="woocommerce-marketplace__my-subscriptions"> <div className="woocommerce-marketplace__my-subscriptions">
<InstallModal />
<section className="woocommerce-marketplace__my-subscriptions__notices"> <section className="woocommerce-marketplace__my-subscriptions__notices">
<Notices /> <Notices />
</section> </section>

View File

@ -0,0 +1,29 @@
/**
* External dependencies
*/
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { getAdminSetting } from '../../../../../utils/admin-settings';
interface RenewProps {
variant?: Button.ButtonVariant;
install?: string;
}
export default function ConnectAccountButton( props: RenewProps ) {
const wccomSettings = getAdminSetting( 'wccomHelper', {} );
const url = new URL( wccomSettings?.connectURL ?? '' );
if ( props.install ) {
url.searchParams.set( 'install', props.install );
}
return (
<Button href={ url.href } variant={ props.variant ?? 'secondary' }>
{ __( 'Connect Account', 'woocommerce' ) }
</Button>
);
}

View File

@ -0,0 +1,228 @@
/**
* External dependencies
*/
import { Button, ButtonGroup, Modal, Notice } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { getNewPath, navigateTo, useQuery } from '@woocommerce/navigation';
import {
useCallback,
useContext,
useEffect,
useState,
} from '@wordpress/element';
/**
* Internal dependencies
*/
import { Subscription } from '../../types';
import { getAdminSetting } from '../../../../../utils/admin-settings';
import Install from './install';
import { SubscriptionsContext } from '../../../../contexts/subscriptions-context';
import { MARKETPLACE_PATH, WP_ADMIN_PLUGIN_LIST_URL } from '../../../constants';
import ConnectAccountButton from './connect-account-button';
import ProductCard from '../../../product-card/product-card';
import { addNotice, subscriptionToProduct } from '../../../../utils/functions';
import { NoticeStatus } from '../../../../contexts/types';
export default function InstallModal() {
const query = useQuery();
const installingProductKey = query?.install;
const wccomSettings = getAdminSetting( 'wccomHelper', {} );
const isConnected = !! wccomSettings?.isConnected;
const [ showModal, setShowModal ] = useState< boolean >( false );
const [ isInstalled, setIsInstalled ] = useState< boolean >( false );
const { subscriptions, isLoading } = useContext( SubscriptionsContext );
const subscription: Subscription | undefined = subscriptions.find(
( s: Subscription ) => s.product_key === installingProductKey
);
const removeInstallQuery = useCallback( () => {
navigateTo( {
url: getNewPath(
{
...query,
install: undefined,
},
MARKETPLACE_PATH,
{}
),
} );
}, [ query ] );
useEffect( () => {
if ( isLoading ) {
return;
}
// If subscriptions loaded, but we don't have a subscription for the product key, show an error.
if (
installingProductKey &&
isConnected &&
! isLoading &&
! subscription
) {
addNotice(
installingProductKey,
sprintf(
/* translators: %s is the product key */
__(
'Could not find subscription with product key %s.',
'woocommerce'
),
installingProductKey
),
NoticeStatus.Error
);
removeInstallQuery();
} else {
setShowModal( !! installingProductKey );
}
}, [
isConnected,
isLoading,
installingProductKey,
removeInstallQuery,
subscription,
] );
useEffect( () => {
if ( subscription && subscription.local.installed ) {
setIsInstalled( true );
}
}, [ subscription ] );
const onClose = () => {
removeInstallQuery();
setShowModal( false );
};
const modalTitle = () => {
if ( isInstalled ) {
return __( 'You are ready to go!', 'woocommerce' );
}
return __( 'Add to store', 'woocommerce' );
};
const modalContent = () => {
if ( ! isConnected ) {
return (
<Notice status="warning" isDismissible={ false }>
{ __(
'In order to install a product, you need to first connect your account.',
'woocommerce'
) }
</Notice>
);
} else if ( subscription ) {
const installContent = isInstalled
? __(
'Keep the momentum going and start setting up your extension.',
'woocommerce'
)
: __(
'Would you like to install this extension?',
'woocommerce'
);
return (
<>
<p className="woocommerce-marketplace__header-account-modal-text">
{ installContent }
</p>
<ProductCard
product={ subscriptionToProduct( subscription ) }
small={ true }
tracksData={ {
position: 1,
group: 'subscriptions',
label: 'install',
} }
/>
</>
);
}
};
const modalButtons = () => {
const buttons = [];
if ( isInstalled ) {
buttons.push(
<Button
variant="secondary"
href={ subscription?.documentation_url }
target="_blank"
className="woocommerce-marketplace__header-account-modal-button"
key={ 'docs' }
>
{ __( 'View docs', 'woocommerce' ) }
</Button>
);
buttons.push(
<Button
variant="primary"
href={ WP_ADMIN_PLUGIN_LIST_URL }
className="woocommerce-marketplace__header-account-modal-button"
key={ 'plugin-list' }
>
{ __( 'View in Plugins', 'woocommerce' ) }
</Button>
);
} else {
buttons.push(
<Button
variant="tertiary"
onClick={ onClose }
className="woocommerce-marketplace__header-account-modal-button"
key={ 'cancel' }
>
{ __( 'Cancel', 'woocommerce' ) }
</Button>
);
if ( ! isConnected ) {
buttons.push(
<ConnectAccountButton
variant="primary"
install={ installingProductKey }
key={ 'connect' }
/>
);
} else if ( subscription ) {
buttons.push(
<Install
subscription={ subscription }
variant="primary"
onError={ onClose }
key={ 'install' }
/>
);
}
}
return (
<ButtonGroup className="woocommerce-marketplace__header-account-modal-button-group">
{ buttons }
</ButtonGroup>
);
};
if ( ! showModal ) {
return null;
}
return (
<Modal
title={ modalTitle() }
onRequestClose={ onClose }
focusOnMount={ true }
className="woocommerce-marketplace__header-account-modal has-size-medium"
style={ { borderRadius: 4 } }
overlayClassName="woocommerce-marketplace__header-account-modal-overlay"
>
{ modalContent() }
{ modalButtons() }
</Modal>
);
}

View File

@ -22,6 +22,9 @@ import { NoticeStatus } from '../../../../contexts/types';
interface InstallProps { interface InstallProps {
subscription: Subscription; subscription: Subscription;
variant?: Button.ButtonVariant;
onSuccess?: () => void;
onError?: () => void;
} }
export default function Install( props: InstallProps ) { export default function Install( props: InstallProps ) {
@ -76,6 +79,10 @@ export default function Install( props: InstallProps ) {
product_id: props.subscription.product_id, product_id: props.subscription.product_id,
product_current_version: props.subscription.version, product_current_version: props.subscription.version,
} ); } );
if ( props.onSuccess ) {
props.onSuccess();
}
} ) } )
.catch( ( error ) => { .catch( ( error ) => {
loadSubscriptions( false ).then( () => { loadSubscriptions( false ).then( () => {
@ -101,19 +108,24 @@ export default function Install( props: InstallProps ) {
} }
); );
stopInstall(); stopInstall();
if ( props.onError ) {
props.onError();
}
} ); } );
recordEvent( 'marketplace_product_install_failed', { recordEvent( 'marketplace_product_install_failed', {
product_zip_slug: props.subscription.zip_slug, product_zip_slug: props.subscription.zip_slug,
product_id: props.subscription.product_id, product_id: props.subscription.product_id,
product_current_version: props.subscription.version, product_current_version: props.subscription.version,
error_message: error?.data?.message,
} ); } );
} ); } );
}; };
return ( return (
<Button <Button
variant="link" variant={ props.variant ?? 'link' }
isBusy={ loading } isBusy={ loading }
disabled={ loading } disabled={ loading }
onClick={ install } onClick={ install }

View File

@ -59,6 +59,28 @@
} }
} }
&.is-small {
padding: $medium-gap;
.woocommerce-marketplace__product-card__description,
.woocommerce-marketplace__product-card__price {
display: none;
}
.woocommerce-marketplace__product-card__content {
display: block;
}
.woocommerce-marketplace__product-card__details {
align-items: center;
}
.woocommerce-marketplace__product-card__title {
font-size: 14px;
line-height: 20px;
}
}
&:hover { &:hover {
outline: 1.5px solid var(--wp-admin-theme-color); outline: 1.5px solid var(--wp-admin-theme-color);
} }

View File

@ -19,6 +19,7 @@ export interface ProductCardProps {
product?: Product; product?: Product;
isLoading?: boolean; isLoading?: boolean;
tracksData: ProductTracksData; tracksData: ProductTracksData;
small?: boolean;
} }
function ProductCard( props: ProductCardProps ): JSX.Element { function ProductCard( props: ProductCardProps ): JSX.Element {
@ -102,6 +103,7 @@ function ProductCard( props: ProductCardProps ): JSX.Element {
`woocommerce-marketplace__product-card--${ type }`, `woocommerce-marketplace__product-card--${ type }`,
{ {
'is-loading': isLoading, 'is-loading': isLoading,
'is-small': props.small,
} }
); );

View File

@ -33,3 +33,8 @@ $wp-gray-0: $gray-0;
$wp-gray-50: $gray-50; $wp-gray-50: $gray-50;
$wp-gray-60: $gray-60; $wp-gray-60: $gray-60;
$skeleton-loader-color: #f0f0f0; $skeleton-loader-color: #f0f0f0;
// Modal
$modal-min-width: 350px;
$modal-width-small: 384px;
$modal-width-medium: 512px;

View File

@ -364,6 +364,23 @@ const removeNotice = ( productKey: string ) => {
dispatch( noticeStore ).removeNotice( productKey ); dispatch( noticeStore ).removeNotice( productKey );
}; };
const subscriptionToProduct = ( subscription: Subscription ): Product => {
return {
id: subscription.product_id,
title: subscription.product_name,
image: '',
type: subscription.product_type as ProductType,
description: '',
vendorName: '',
vendorUrl: '',
icon: subscription.product_icon,
url: subscription.product_url,
price: -1,
averageRating: 0,
reviewsCount: 0,
};
};
// Append UTM parameters to a URL, being aware of existing query parameters // Append UTM parameters to a URL, being aware of existing query parameters
const appendURLParams = ( const appendURLParams = (
url: string, url: string,
@ -412,4 +429,5 @@ export {
removeNotice, removeNotice,
renewUrl, renewUrl,
subscribeUrl, subscribeUrl,
subscriptionToProduct,
}; };

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add subscription install modal.

View File

@ -686,12 +686,13 @@ class WC_Helper {
* *
* @param array $args Query args. * @param array $args Query args.
* @param bool $redirect_to_wc_admin Whether to redirect to WC Admin. * @param bool $redirect_to_wc_admin Whether to redirect to WC Admin.
* @param string $install_product_key Optional Product key to install.
* @return string * @return string
*/ */
private static function get_helper_redirect_url( $args = array(), $redirect_to_wc_admin = false ) { private static function get_helper_redirect_url( $args = array(), $redirect_to_wc_admin = false, $install_product_key = '' ) {
global $current_screen; global $current_screen;
if ( true === $redirect_to_wc_admin && 'woocommerce_page_wc-addons' === $current_screen->id ) { if ( true === $redirect_to_wc_admin && 'woocommerce_page_wc-addons' === $current_screen->id ) {
return add_query_arg( $new_url = add_query_arg(
array( array(
'page' => 'wc-admin', 'page' => 'wc-admin',
'tab' => 'my-subscriptions', 'tab' => 'my-subscriptions',
@ -699,6 +700,15 @@ class WC_Helper {
), ),
admin_url( 'admin.php' ) admin_url( 'admin.php' )
); );
if ( ! empty( $install_product_key ) ) {
$new_url = add_query_arg(
array(
'install' => $install_product_key,
),
$new_url
);
}
return $new_url;
} }
return add_query_arg( return add_query_arg(
@ -727,6 +737,10 @@ class WC_Helper {
$redirect_url_args['redirect-to-wc-admin'] = 1; $redirect_url_args['redirect-to-wc-admin'] = 1;
} }
if ( isset( $_GET['install'] ) ) {
$redirect_url_args['install'] = sanitize_text_field( wp_unslash( $_GET['install'] ) );
}
$redirect_uri = add_query_arg( $redirect_uri = add_query_arg(
$redirect_url_args, $redirect_url_args,
admin_url( 'admin.php' ) admin_url( 'admin.php' )
@ -795,7 +809,8 @@ class WC_Helper {
'page' => 'wc-addons', 'page' => 'wc-addons',
'section' => 'helper', 'section' => 'helper',
), ),
isset( $_GET['redirect-to-wc-admin'] ) isset( $_GET['redirect-to-wc-admin'] ),
isset( $_GET['install'] ) ? sanitize_text_field( wp_unslash( $_GET['install'] ) ) : ''
) )
); );
die(); die();
@ -858,7 +873,8 @@ class WC_Helper {
'section' => 'helper', 'section' => 'helper',
'wc-helper-status' => 'helper-connected', 'wc-helper-status' => 'helper-connected',
), ),
isset( $_GET['redirect-to-wc-admin'] ) isset( $_GET['redirect-to-wc-admin'] ),
isset( $_GET['install'] ) ? sanitize_text_field( wp_unslash( $_GET['install'] ) ) : ''
) )
); );
die(); die();
@ -884,7 +900,8 @@ class WC_Helper {
'section' => 'helper', 'section' => 'helper',
'wc-helper-status' => 'helper-disconnected', 'wc-helper-status' => 'helper-disconnected',
), ),
isset( $_GET['redirect-to-wc-admin'] ) isset( $_GET['redirect-to-wc-admin'] ),
isset( $_GET['install'] ) ? sanitize_text_field( wp_unslash( $_GET['install'] ) ) : ''
); );
self::disconnect(); self::disconnect();
@ -911,7 +928,8 @@ class WC_Helper {
'filter' => self::get_current_filter(), 'filter' => self::get_current_filter(),
'wc-helper-status' => 'helper-refreshed', 'wc-helper-status' => 'helper-refreshed',
), ),
isset( $_GET['redirect-to-wc-admin'] ) isset( $_GET['redirect-to-wc-admin'] ),
isset( $_GET['install'] ) ? sanitize_text_field( wp_unslash( $_GET['install'] ) ) : ''
); );
wp_safe_redirect( $redirect_uri ); wp_safe_redirect( $redirect_uri );