Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Checkout: Convert PurchaseModal to use composite checkout #48152

Merged
merged 24 commits into from
Dec 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e1e47cf
Fill in missing PropTypes for UpsellNudge
sirbrillig Dec 8, 2020
b9754c0
Replace CheckoutContainer with ShoppingCartProvider around UpsellNudge
sirbrillig Dec 8, 2020
bdf098d
Wrap UpsellNudge with withShoppingCart
sirbrillig Dec 8, 2020
dcdc4ee
Explicitly add isFetchingStoredCards, cards to UpsellNudge selectors
sirbrillig Dec 8, 2020
7d04f4a
Refactor CheckoutProvider to be a named function
sirbrillig Dec 10, 2020
8ee0eae
Make total and items optional props to CheckoutProvider
sirbrillig Dec 10, 2020
236339f
Convert useSubmitTransaction to use useProcessPayment
sirbrillig Dec 11, 2020
b719922
Update UpsellNudge to use shoppingCartManager
sirbrillig Dec 8, 2020
4379a94
Remove handleCheckoutCompleteRedirect from UpsellNudge
sirbrillig Dec 11, 2020
8e9d912
Add CheckoutProvider to PurchaseModal
sirbrillig Dec 11, 2020
0b05df7
Remove onComplete from useSubmitTransaction
sirbrillig Dec 11, 2020
e074350
Use withPaymentCompleteCallback on UpsellNudge
sirbrillig Dec 11, 2020
7cc8ff5
Use getThankYouPageUrl directly in UpsellNudge
sirbrillig Dec 11, 2020
b78db8e
Add QueryStoredCards to UpsellNudge
sirbrillig Dec 11, 2020
3fa3923
Move CheckoutProvider outside PurchaseModal by adding wrapper
sirbrillig Dec 11, 2020
1b2f4b4
Allow mounting UpsellNudge if products are still fetching
sirbrillig Dec 11, 2020
fbc473b
Pass siteId to PurchaseModal
sirbrillig Dec 11, 2020
81fd1db
Pass correct args to existingCardProcessor
sirbrillig Dec 11, 2020
2c0a5cc
Pass country/postalCode to existingCardProcessor
sirbrillig Dec 11, 2020
9a25436
Set tax country code in UpsellNudge
sirbrillig Dec 11, 2020
bf4c2d0
Add StripeHookProvider to UpsellNudge
sirbrillig Dec 11, 2020
91ac293
Set form to processing before submit in useSubmitTransaction
sirbrillig Dec 11, 2020
66a9d4a
Remove declined support session when closing modal
sirbrillig Dec 11, 2020
e43d60a
Include receiptId in UpsellNudge getThankYouPageUrlArguments
sirbrillig Dec 14, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions client/my-sites/checkout/controller.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { getSiteBySlug } from 'calypso/state/sites/selectors';
import { getSelectedSite } from 'calypso/state/ui/selectors';
import GSuiteNudge from './gsuite-nudge';
import CheckoutContainer from './checkout/checkout-container';
import CalypsoShoppingCartProvider from './calypso-shopping-cart-provider';
import CheckoutSystemDecider from './checkout-system-decider';
import CheckoutPendingComponent from './checkout-thank-you/pending';
import CheckoutThankYouComponent from './checkout-thank-you';
Expand Down Expand Up @@ -219,14 +220,14 @@ export function upsellNudge( context, next ) {
setSectionMiddleware( { name: upsellType } )( context );

context.primary = (
<CheckoutContainer purchaseId={ Number( receiptId ) }>
<CalypsoShoppingCartProvider>
<UpsellNudge
siteSlugParam={ site }
receiptId={ Number( receiptId ) }
upsellType={ upsellType }
upgradeItem={ upgradeItem }
/>
</CheckoutContainer>
</CalypsoShoppingCartProvider>
);

next();
Expand Down
116 changes: 86 additions & 30 deletions client/my-sites/checkout/upsell-nudge/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import page from 'page';
import { omit } from 'lodash';
import { pick } from 'lodash';
import { withShoppingCart, createRequestCartProduct } from '@automattic/shopping-cart';
import { StripeHookProvider } from '@automattic/calypso-stripe';

/**
* Internal dependencies
*/
import Main from 'calypso/components/main';
import QuerySites from 'calypso/components/data/query-sites';
import QueryStoredCards from 'calypso/components/data/query-stored-cards';
import QueryProductsList from 'calypso/components/data/query-products-list';
import QuerySitePlans from 'calypso/components/data/query-site-plans';
import { CompactCard } from '@automattic/components';
Expand All @@ -38,11 +41,14 @@ import { ConciergeSupportSession } from './concierge-support-session';
import { BusinessPlanUpgradeUpsell } from './business-plan-upgrade-upsell';
import { PremiumPlanUpgradeUpsell } from './premium-plan-upgrade-upsell';
import getUpgradePlanSlugFromPath from 'calypso/state/selectors/get-upgrade-plan-slug-from-path';
import { PurchaseModal } from './purchase-modal';
import { replaceCartWithItems } from 'calypso/lib/cart/actions';
import PurchaseModal from './purchase-modal';
import Gridicon from 'calypso/components/gridicon';
import { isMonthly } from 'calypso/lib/plans/constants';
import { getPlanByPathSlug } from 'calypso/lib/plans';
import { isFetchingStoredCards, getStoredCards } from 'calypso/state/stored-cards/selectors';
import getThankYouPageUrl from 'calypso/my-sites/checkout/composite-checkout/hooks/use-get-thank-you-url/get-thank-you-page-url';
import { extractStoredCardMetaValue } from './purchase-modal/util';
import { getStripeConfiguration } from 'calypso/lib/store-transactions';

/**
* Style dependencies
Expand All @@ -60,7 +66,29 @@ export const BUSINESS_PLAN_UPGRADE_UPSELL = 'business-plan-upgrade-upsell';
export class UpsellNudge extends React.Component {
static propTypes = {
receiptId: PropTypes.number,
handleCheckoutCompleteRedirect: PropTypes.func.isRequired,
upsellType: PropTypes.string,
upgradeItem: PropTypes.string,
siteSlugParam: PropTypes.string,

// Below are provided by HOCs
currencyCode: PropTypes.string,
isLoading: PropTypes.bool,
hasProductsList: PropTypes.bool,
hasSitePlans: PropTypes.bool,
product: PropTypes.object,
productCost: PropTypes.number,
productDisplayCost: PropTypes.string,
planRawPrice: PropTypes.string,
planDiscountedRawPrice: PropTypes.string,
isLoggedIn: PropTypes.bool,
siteSlug: PropTypes.string,
selectedSiteId: PropTypes.oneOfType( [ PropTypes.string, PropTypes.number ] ).isRequired,
hasSevenDayRefundPeriod: PropTypes.bool,
trackUpsellButtonClick: PropTypes.func.isRequired,
translate: PropTypes.func.isRequired,
cards: PropTypes.arrayOf( PropTypes.object ),
cart: PropTypes.object,
isFetchingStoredCards: PropTypes.bool,
};

state = {
Expand All @@ -73,6 +101,7 @@ export class UpsellNudge extends React.Component {
return (
<Main className={ upsellType }>
<QuerySites siteId={ selectedSiteId } />
<QueryStoredCards />
{ ! hasProductsList && <QueryProductsList /> }
{ ! hasSitePlans && <QuerySitePlans siteId={ selectedSiteId } /> }
{ isLoading ? this.renderPlaceholders() : this.renderContent() }
Expand Down Expand Up @@ -204,10 +233,17 @@ export class UpsellNudge extends React.Component {
}

handleClickDecline = ( shouldHideUpsellNudges = true ) => {
const { trackUpsellButtonClick, upsellType, handleCheckoutCompleteRedirect } = this.props;
const { trackUpsellButtonClick, upsellType } = this.props;

trackUpsellButtonClick( `calypso_${ upsellType.replace( /-/g, '_' ) }_decline_button_click` );
handleCheckoutCompleteRedirect( shouldHideUpsellNudges );
const getThankYouPageUrlArguments = {
siteSlug: this.props.siteSlug,
receiptId: this.props.receiptId,
cart: this.props.cart,
hideNudge: shouldHideUpsellNudges,
};
const url = getThankYouPageUrl( getThankYouPageUrlArguments );
page.redirect( url );
};

handleClickAccept = ( buttonAction ) => {
Expand All @@ -220,9 +256,16 @@ export class UpsellNudge extends React.Component {
if ( this.isEligibleForOneClickUpsell( buttonAction ) ) {
this.setState( {
showPurchaseModal: true,
cartLastServerResponseDate: this.getCartUpdatedTime(),
} );
replaceCartWithItems( [ this.props.product ] );
const storedCard = this.props.cards[ 0 ];
const countryCode = extractStoredCardMetaValue( storedCard, 'country_code' );
const postalCode = extractStoredCardMetaValue( storedCard, 'card_zip' );
this.props.shoppingCartManager.updateLocation( {
countryCode,
postalCode,
subdivisionCode: null,
} );
this.props.shoppingCartManager.replaceProductsInCart( [ this.props.product ] );
return;
}

Expand All @@ -232,7 +275,11 @@ export class UpsellNudge extends React.Component {
};

isEligibleForOneClickUpsell = ( buttonAction ) => {
const { cards, siteSlug, upsellType } = this.props;
const { product, cards, siteSlug, upsellType } = this.props;

if ( ! product ) {
return false;
}

const supportedUpsellTypes = [
CONCIERGE_QUICKSTART_SESSION,
Expand All @@ -255,22 +302,25 @@ export class UpsellNudge extends React.Component {
return true;
};

handleOneClickUpsellComplete = ( currentRecieptId ) => {
this.props.handleCheckoutCompleteRedirect( true, currentRecieptId );
};

renderPurchaseModal = () => {
const isCartUpdating = this.state.cartLastServerResponseDate === this.getCartUpdatedTime();
const isCartUpdating = this.props.shoppingCartManager.isPendingUpdate;

const onCloseModal = () => {
this.props.shoppingCartManager.replaceProductsInCart( [] );
this.setState( { showPurchaseModal: false } );
};

return (
<PurchaseModal
cart={ this.props.cart }
cards={ this.props.cards }
onComplete={ this.handleOneClickUpsellComplete }
onClose={ () => this.setState( { showPurchaseModal: false } ) }
siteSlug={ this.props.siteSlug }
isCartUpdating={ isCartUpdating }
/>
<StripeHookProvider fetchStripeConfiguration={ getStripeConfiguration }>
<PurchaseModal
cart={ this.props.cart }
cards={ this.props.cards }
onClose={ onCloseModal }
siteId={ this.props.selectedSiteId }
siteSlug={ this.props.siteSlug }
isCartUpdating={ isCartUpdating }
/>
</StripeHookProvider>
);
};

Expand All @@ -281,10 +331,6 @@ export class UpsellNudge extends React.Component {
</div>
);
};

getCartUpdatedTime = () => {
return this.props.cart?.client_metadata?.last_server_response_date;
};
}

const trackUpsellButtonClick = ( eventName ) => {
Expand Down Expand Up @@ -318,18 +364,28 @@ export default connect(
const annualPrice = getSitePlanRawPrice( state, selectedSiteId, planSlug, {
isMonthly: false,
} );

const isFetchingCards = isFetchingStoredCards( state );
const productSlug = resolveProductSlug( upsellType, upgradeItem );
const productProperties = pick( getProductBySlug( state, productSlug ), [
'product_slug',
'product_id',
] );
const product =
productProperties.product_slug && productProperties.product_id
? createRequestCartProduct( productProperties )
: null;

return {
isFetchingStoredCards: isFetchingCards,
cards: getStoredCards( state ),
currencyCode: getCurrentUserCurrencyCode( state ),
isLoading:
props.isFetchingStoredCards ||
isFetchingCards ||
isProductsListFetching( state ) ||
isRequestingSitePlans( state, selectedSiteId ),
hasProductsList: Object.keys( productsList ).length > 0,
hasSitePlans: sitePlans && sitePlans.length > 0,
product: omit( getProductBySlug( state, productSlug ), 'prices' ),
product,
productCost: getProductCost( state, productSlug ),
productDisplayCost: getProductDisplayCost( state, productSlug ),
planRawPrice: annualPrice,
Expand All @@ -343,4 +399,4 @@ export default connect(
{
trackUpsellButtonClick,
}
)( localize( UpsellNudge ) );
)( withShoppingCart( localize( UpsellNudge ) ) );
55 changes: 51 additions & 4 deletions client/my-sites/checkout/upsell-nudge/purchase-modal/index.jsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,39 @@
/**
* External dependencies
*/
import React, { useState } from 'react';
import React, { useState, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { Dialog } from '@automattic/components';
import { useTranslate } from 'i18n-calypso';
import { CheckoutProvider } from '@automattic/composite-checkout';
import { useStripe } from '@automattic/calypso-stripe';

/**
* Internal dependencies
*/
import { Dialog } from '@automattic/components';
import { useSubmitTransaction } from './util';
import { BEFORE_SUBMIT } from './constants';
import Content from './content';
import Placeholder from './placeholder';
import useCreatePaymentCompleteCallback from 'calypso/my-sites/checkout/composite-checkout/hooks/use-create-payment-complete-callback';
import existingCardProcessor from 'calypso/my-sites/checkout/composite-checkout/lib/existing-card-processor';
import getContactDetailsType from 'calypso/my-sites/checkout/composite-checkout/lib/get-contact-details-type';

/**
* Style dependencies
*/
import './style.scss';

export function PurchaseModal( { cart, cards, isCartUpdating, onComplete, onClose, siteSlug } ) {
const noop = () => null;

export function PurchaseModal( { cart, cards, isCartUpdating, onClose, siteSlug, siteId } ) {
const translate = useTranslate();
const [ step, setStep ] = useState( BEFORE_SUBMIT );
const submitTransaction = useSubmitTransaction( {
cart,
siteId,
setStep,
storedCard: cards?.[ 0 ],
onComplete,
onClose,
successMessage: translate( 'Your purchase has been completed!' ),
} );
Expand All @@ -44,3 +52,42 @@ export function PurchaseModal( { cart, cards, isCartUpdating, onComplete, onClos
</Dialog>
);
}

export default function PurchaseModalWrapper( props ) {
const onComplete = useCreatePaymentCompleteCallback( {
isComingFromUpsell: true,
} );
const { stripeConfiguration } = useStripe();
const reduxDispatch = useDispatch();

const contactDetailsType = getContactDetailsType( props.cart );
const includeDomainDetails = contactDetailsType === 'domain';
const includeGSuiteDetails = contactDetailsType === 'gsuite';
const dataForProcessor = useMemo(
() => ( {
includeDomainDetails,
includeGSuiteDetails,
recordEvent: noop,
stripeConfiguration,
createUserAndSiteBeforeTransaction: false,
reduxDispatch,
} ),
[ includeDomainDetails, includeGSuiteDetails, stripeConfiguration, reduxDispatch ]
);

return (
<CheckoutProvider
paymentMethods={ [] }
onPaymentComplete={ onComplete }
showErrorMessage={ noop }
showInfoMessage={ noop }
showSuccessMessage={ noop }
paymentProcessors={ {
'existing-card': ( transactionData ) =>
existingCardProcessor( transactionData, dataForProcessor ),
} }
>
<PurchaseModal { ...props } />;
</CheckoutProvider>
);
}
Loading