From 2964800f27ad5cc1da24a27514fe0ce34970b1d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?berislav=20grgi=C4=8Dak?= Date: Thu, 14 Dec 2023 12:45:40 +0100 Subject: [PATCH] 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 --- .../marketplace/components/constants.ts | 6 + .../header-account/header-account.scss | 7 +- .../my-subscriptions/my-subscriptions.scss | 58 ++++- .../my-subscriptions/my-subscriptions.tsx | 3 + .../table/actions/connect-account-button.tsx | 29 +++ .../table/actions/install-modal.tsx | 228 ++++++++++++++++++ .../table/actions/install.tsx | 14 +- .../components/product-card/product-card.scss | 22 ++ .../components/product-card/product-card.tsx | 2 + .../marketplace/stylesheets/_variables.scss | 5 + .../client/marketplace/utils/functions.tsx | 18 ++ .../42009-add-subscription-install-modal | 4 + .../includes/admin/helper/class-wc-helper.php | 34 ++- 13 files changed, 417 insertions(+), 13 deletions(-) create mode 100644 plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/connect-account-button.tsx create mode 100644 plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/install-modal.tsx create mode 100644 plugins/woocommerce/changelog/42009-add-subscription-install-modal diff --git a/plugins/woocommerce-admin/client/marketplace/components/constants.ts b/plugins/woocommerce-admin/client/marketplace/components/constants.ts index eae1a0471ac..97dfffbfcf3 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/constants.ts +++ b/plugins/woocommerce-admin/client/marketplace/components/constants.ts @@ -1,3 +1,8 @@ +/** + * Internal dependencies + */ +import { ADMIN_URL } from '../../utils/admin-settings'; + export const DEFAULT_TAB_KEY = 'discover'; export const MARKETPLACE_HOST = 'https://woo.com'; export const MARKETPLACE_PATH = '/extensions'; @@ -11,3 +16,4 @@ 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'; +export const WP_ADMIN_PLUGIN_LIST_URL = ADMIN_URL + '/plugins.php'; diff --git a/plugins/woocommerce-admin/client/marketplace/components/header-account/header-account.scss b/plugins/woocommerce-admin/client/marketplace/components/header-account/header-account.scss index 0da04a93478..19ba2e9c4d4 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/header-account/header-account.scss +++ b/plugins/woocommerce-admin/client/marketplace/components/header-account/header-account.scss @@ -56,7 +56,12 @@ @media screen and (min-width: $break-small) { .woocommerce-marketplace { &__header-account-modal { - max-width: 350px; + max-width: $modal-min-width; + + &.has-size-medium { + max-width: $modal-width-medium; + width: 100%; + } } } } diff --git a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.scss b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.scss index 1b3b298c383..5119c8c6d14 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.scss +++ b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.scss @@ -233,9 +233,11 @@ } } -.woocommerce-marketplace__my-subscriptions .components-button.is-link { - text-decoration: none; - padding: 6px 12px; +.woocommerce-marketplace__my-subscriptions .components-button { + &.is-link { + text-decoration: none; + padding: 6px 12px; + } } .woocommerce-marketplace__my-subscriptions @@ -300,6 +302,7 @@ box-shadow: 0 2px 6px 0 rgba($gray-100, 0.05); border: 1px solid var(--gutenberg-gray-100, #f0f0f0); padding-right: $grid-unit-15; + position: relative; &::before { content: ''; @@ -338,3 +341,52 @@ 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; + } +} diff --git a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.tsx b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.tsx index e3e37992456..bb1e5f4f64d 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/my-subscriptions.tsx @@ -23,6 +23,7 @@ import { import { Subscription } from './types'; import { RefreshButton } from './table/actions/refresh-button'; import Notices from './notices'; +import InstallModal from './table/actions/install-modal'; export default function MySubscriptions(): JSX.Element { const { subscriptions, isLoading } = useContext( SubscriptionsContext ); @@ -58,6 +59,7 @@ export default function MySubscriptions(): JSX.Element { if ( ! wccomSettings?.isConnected ) { return (
+

{ __( 'Manage your subscriptions', 'woocommerce' ) } @@ -77,6 +79,7 @@ export default function MySubscriptions(): JSX.Element { return (
+
diff --git a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/connect-account-button.tsx b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/connect-account-button.tsx new file mode 100644 index 00000000000..d45c9e4f381 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/connect-account-button.tsx @@ -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 ( + + ); +} diff --git a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/install-modal.tsx b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/install-modal.tsx new file mode 100644 index 00000000000..98bb8e760ec --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/install-modal.tsx @@ -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 ( + + { __( + 'In order to install a product, you need to first connect your account.', + 'woocommerce' + ) } + + ); + } 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 ( + <> +

+ { installContent } +

+ + + ); + } + }; + const modalButtons = () => { + const buttons = []; + if ( isInstalled ) { + buttons.push( + + ); + buttons.push( + + ); + } else { + buttons.push( + + ); + + if ( ! isConnected ) { + buttons.push( + + ); + } else if ( subscription ) { + buttons.push( + + ); + } + } + return ( + + { buttons } + + ); + }; + + if ( ! showModal ) { + return null; + } + + return ( + + { modalContent() } + { modalButtons() } + + ); +} diff --git a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/install.tsx b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/install.tsx index 9de06cb7101..f57206d9628 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/install.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/install.tsx @@ -22,6 +22,9 @@ import { NoticeStatus } from '../../../../contexts/types'; interface InstallProps { subscription: Subscription; + variant?: Button.ButtonVariant; + onSuccess?: () => void; + onError?: () => void; } export default function Install( props: InstallProps ) { @@ -76,6 +79,10 @@ export default function Install( props: InstallProps ) { product_id: props.subscription.product_id, product_current_version: props.subscription.version, } ); + + if ( props.onSuccess ) { + props.onSuccess(); + } } ) .catch( ( error ) => { loadSubscriptions( false ).then( () => { @@ -101,19 +108,24 @@ export default function Install( props: InstallProps ) { } ); stopInstall(); + + if ( props.onError ) { + props.onError(); + } } ); recordEvent( 'marketplace_product_install_failed', { product_zip_slug: props.subscription.zip_slug, product_id: props.subscription.product_id, product_current_version: props.subscription.version, + error_message: error?.data?.message, } ); } ); }; return (