diff --git a/projects/js-packages/connection/changelog/update-ai-assistant-intertitial-checkout-url b/projects/js-packages/connection/changelog/update-ai-assistant-intertitial-checkout-url new file mode 100644 index 0000000000000..6639598e28b96 --- /dev/null +++ b/projects/js-packages/connection/changelog/update-ai-assistant-intertitial-checkout-url @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Connection: Add optional quantity to product checkout workflow hook diff --git a/projects/js-packages/connection/hooks/use-product-checkout-workflow/index.jsx b/projects/js-packages/connection/hooks/use-product-checkout-workflow/index.jsx index 4605b652d0ef7..fa0f58d11a0c5 100644 --- a/projects/js-packages/connection/hooks/use-product-checkout-workflow/index.jsx +++ b/projects/js-packages/connection/hooks/use-product-checkout-workflow/index.jsx @@ -21,15 +21,16 @@ const defaultAdminUrl = * Custom hook that performs the needed steps * to concrete the checkout workflow. * - * @param {object} props - The props passed to the hook. - * @param {string} props.productSlug - The WordPress product slug. - * @param {string} props.redirectUrl - The URI to redirect to after checkout. - * @param {string} [props.siteSuffix] - The site suffix. - * @param {string} [props.adminUrl] - The site wp-admin url. - * @param {boolean} props.connectAfterCheckout - Whether or not to conect after checkout if not connected (default false - connect before). + * @param {object} props - The props passed to the hook. + * @param {string} props.productSlug - The WordPress product slug. + * @param {string} props.redirectUrl - The URI to redirect to after checkout. + * @param {string} [props.siteSuffix] - The site suffix. + * @param {string} [props.adminUrl] - The site wp-admin url. + * @param {boolean} props.connectAfterCheckout - Whether or not to conect after checkout if not connected (default false - connect before). * @param {Function} props.siteProductAvailabilityHandler - The function used to check whether the site already has the requested product. This will be checked after registration and the checkout page will be skipped if the promise returned resloves true. - * @param {Function} props.from - The plugin slug initiated the flow. - * @returns {Function} - The useEffect hook. + * @param {Function} props.from - The plugin slug initiated the flow. + * @param {number} [props.quantity] - The quantity of the product to purchase. + * @returns {Function} The useEffect hook. */ export default function useProductCheckoutWorkflow( { productSlug, @@ -38,6 +39,7 @@ export default function useProductCheckoutWorkflow( { adminUrl = defaultAdminUrl, connectAfterCheckout = false, siteProductAvailabilityHandler = null, + quantity = null, from, } = {} ) { debug( 'productSlug is %s', productSlug ); @@ -61,7 +63,11 @@ export default function useProductCheckoutWorkflow( { ? 'checkout/jetpack/' : `checkout/${ siteSuffix }/`; - const productCheckoutUrl = new URL( `${ origin }${ checkoutPath }${ productSlug }` ); + const quantitySuffix = quantity != null ? `:-q-${ quantity }` : ''; + + const productCheckoutUrl = new URL( + `${ origin }${ checkoutPath }${ productSlug }${ quantitySuffix }` + ); if ( shouldConnectAfterCheckout ) { productCheckoutUrl.searchParams.set( 'connect_after_checkout', true ); @@ -87,14 +93,15 @@ export default function useProductCheckoutWorkflow( { return productCheckoutUrl; }, [ - connectAfterCheckout, isRegistered, + isUserConnected, + connectAfterCheckout, siteSuffix, + quantity, productSlug, - adminUrl, from, redirectUrl, - isUserConnected, + adminUrl, ] ); debug( 'isRegistered is %s', isRegistered ); diff --git a/projects/packages/my-jetpack/_inc/components/product-detail-card/index.jsx b/projects/packages/my-jetpack/_inc/components/product-detail-card/index.jsx index a814dc79a4d40..b923de622dbde 100644 --- a/projects/packages/my-jetpack/_inc/components/product-detail-card/index.jsx +++ b/projects/packages/my-jetpack/_inc/components/product-detail-card/index.jsx @@ -67,6 +67,7 @@ function Price( { value, currency, isOld } ) { * @param {React.ReactNode} props.supportingInfo - Complementary links or support/legal text * @param {string} [props.ctaButtonLabel] - The label for the Call To Action button * @param {boolean} [props.hideTOS] - Whether to hide the Terms of Service text + * @param {number} [props.quantity] - The quantity of the product to purchase * @returns {object} ProductDetailCard react component. */ const ProductDetailCard = ( { @@ -78,6 +79,7 @@ const ProductDetailCard = ( { supportingInfo, ctaButtonLabel = null, hideTOS = false, + quantity = null, } ) => { const { fileSystemWriteAccess, siteSuffix, adminUrl, myJetpackUrl } = window?.myJetpackInitialState ?? {}; @@ -131,6 +133,7 @@ const ProductDetailCard = ( { adminUrl, connectAfterCheckout: true, from: 'my-jetpack', + quantity, } ); const { run: trialCheckoutRedirect, hasCheckoutStarted: hasTrialCheckoutStarted } = @@ -139,6 +142,7 @@ const ProductDetailCard = ( { redirectUrl: myJetpackUrl, siteSuffix, from: 'my-jetpack', + quantity, } ); // Suppported products icons. diff --git a/projects/packages/my-jetpack/_inc/components/product-interstitial/index.jsx b/projects/packages/my-jetpack/_inc/components/product-interstitial/index.jsx index e16ee3a7b4685..3795236af4846 100644 --- a/projects/packages/my-jetpack/_inc/components/product-interstitial/index.jsx +++ b/projects/packages/my-jetpack/_inc/components/product-interstitial/index.jsx @@ -39,6 +39,8 @@ import videoPressImage from './videopress.png'; * @param {string} props.imageContainerClassName - Append a class to the image container * @param {string} [props.ctaButtonLabel] - The label for the Call To Action button * @param {boolean} [props.hideTOS] - Whether to hide the Terms of Service text + * @param {number} [props.quantity] - The quantity of the product to purchase + * @param {number} [props.directCheckout] - Whether to go straight to the checkout page, e.g. for products with usage tiers * @returns {object} ProductInterstitial react component. */ export default function ProductInterstitial( { @@ -52,6 +54,8 @@ export default function ProductInterstitial( { imageContainerClassName = '', ctaButtonLabel = null, hideTOS = false, + quantity = null, + directCheckout = false, } ) { const { activate, detail } = useProduct( slug ); const { isUpgradableByBundle, tiers } = detail; @@ -80,15 +84,13 @@ export default function ProductInterstitial( { const clickHandler = useCallback( ( checkout, product, tier ) => { - const activateOrCheckout = () => ( product?.isBundle ? Promise.resolve() : activate() ); - - activateOrCheckout().finally( () => { - if ( product?.isBundle ) { - // Get straight to the checkout page. - checkout?.(); - return; - } + if ( product?.isBundle || directCheckout ) { + // Get straight to the checkout page. + checkout?.(); + return; + } + activate().finally( () => { const postActivationUrl = product?.postActivationUrl; const hasRequiredPlan = tier ? product?.hasRequiredTier?.[ tier ] @@ -113,7 +115,7 @@ export default function ProductInterstitial( { checkout?.(); } ); }, - [ navigateToMyJetpackOverviewPage, activate ] + [ directCheckout, activate, navigateToMyJetpackOverviewPage ] ); return ( @@ -166,6 +168,7 @@ export default function ProductInterstitial( { preferProductName={ preferProductName } ctaButtonLabel={ ctaButtonLabel } hideTOS={ hideTOS } + quantity={ quantity } />