diff --git a/client/me/purchases/manage-purchase/change-payment-method/index.jsx b/client/me/purchases/manage-purchase/change-payment-method/index.jsx index d48e191dd7e164..52bee5596f571c 100644 --- a/client/me/purchases/manage-purchase/change-payment-method/index.jsx +++ b/client/me/purchases/manage-purchase/change-payment-method/index.jsx @@ -183,7 +183,7 @@ function ChangePaymentMethodList( { const currentlyAssignedPaymentMethodId = 'existingCard-' + currentPaymentMethod.stored_details_id; // TODO: make this work for paypal. const translate = useTranslate(); - const { isStripeLoading } = useStripe(); + const { isStripeLoading, stripe, stripeConfiguration } = useStripe(); const paymentMethods = useAssignablePaymentMethods(); const showErrorMessage = useCallback( ( error ) => { @@ -218,7 +218,10 @@ function ChangePaymentMethodList( { paymentProcessors={ { 'existing-card': ( data ) => assignExistingCardProcessor( purchase, data ), card: ( data ) => - assignNewCardProcessor( { purchase, translate, siteSlug, apiParams }, data ), + assignNewCardProcessor( + { purchase, translate, siteSlug, apiParams, stripe, stripeConfiguration }, + data + ), } } isLoading={ isStripeLoading } initiallySelectedPaymentMethodId={ currentlyAssignedPaymentMethodId } @@ -252,8 +255,8 @@ async function assignExistingCardProcessor( purchase, { storedDetailsId } ) { } async function assignNewCardProcessor( - { purchase, translate, siteSlug, apiParams }, - { stripe, stripeConfiguration, name, countryCode, postalCode } + { purchase, translate, siteSlug, apiParams, stripe, stripeConfiguration }, + { name, countryCode, postalCode } ) { const createStripeSetupIntentAsync = async ( paymentDetails ) => { const { country, 'postal-code': zip } = paymentDetails; diff --git a/client/my-sites/checkout/composite-checkout/composite-checkout.tsx b/client/my-sites/checkout/composite-checkout/composite-checkout.tsx index 412f5676eaf56c..948a25bc220f5f 100644 --- a/client/my-sites/checkout/composite-checkout/composite-checkout.tsx +++ b/client/my-sites/checkout/composite-checkout/composite-checkout.tsx @@ -51,11 +51,11 @@ import { freePurchaseProcessor, multiPartnerCardProcessor, fullCreditsProcessor, - existingCardProcessor, payPalProcessor, genericRedirectProcessor, weChatProcessor, } from './payment-method-processors'; +import existingCardProcessor from './lib/existing-card-processor'; import useGetThankYouUrl from './hooks/use-get-thank-you-url'; import createAnalyticsEventHandler from './record-analytics'; import { useProductVariants } from './hooks/product-variants'; @@ -84,6 +84,9 @@ import { WPCOMCartItem } from './types/checkout-cart'; import doesValueExist from './lib/does-value-exist'; import EmptyCart from './components/empty-cart'; import getContactDetailsType from './lib/get-contact-details-type'; +import getDomainDetails from './lib/get-domain-details'; +import getPostalCode from './lib/get-postal-code'; +import mergeIfObjects from './lib/merge-if-objects'; import type { ReactStandardAction } from './types/analytics'; import useCreatePaymentCompleteCallback from './hooks/use-create-payment-complete-callback'; @@ -139,7 +142,6 @@ export default function CompositeCheckout( { const isPrivate = useSelector( ( state ) => siteId && isPrivateSite( state, siteId ) ) || false; const { stripe, stripeConfiguration, isStripeLoading, stripeLoadingError } = useStripe(); const createUserAndSiteBeforeTransaction = Boolean( isLoggedOutCart || isNoSiteCart ); - const transactionOptions = { createUserAndSiteBeforeTransaction }; const reduxDispatch = useDispatch(); // eslint-disable-next-line react-hooks/exhaustive-deps const recordEvent: ( action: ReactStandardAction ) => void = useCallback( @@ -356,6 +358,7 @@ export default function CompositeCheckout( { const contactInfo: ManagedContactDetails | undefined = select( 'wpcom' )?.getContactInfo(); const countryCode: string = contactInfo?.countryCode?.value ?? ''; + const subdivisionCode: string = contactInfo?.state?.value ?? ''; const paymentMethods = arePaymentMethodsLoading ? [] @@ -416,13 +419,22 @@ export default function CompositeCheckout( { const contactDetailsType = getContactDetailsType( responseCart ); const includeDomainDetails = contactDetailsType === 'domain'; const includeGSuiteDetails = contactDetailsType === 'gsuite'; + const transactionOptions = { createUserAndSiteBeforeTransaction }; const dataForProcessor = useMemo( () => ( { includeDomainDetails, includeGSuiteDetails, recordEvent, + createUserAndSiteBeforeTransaction, + stripeConfiguration, } ), - [ includeDomainDetails, includeGSuiteDetails, recordEvent ] + [ + includeDomainDetails, + includeGSuiteDetails, + recordEvent, + createUserAndSiteBeforeTransaction, + stripeConfiguration, + ] ); const dataForRedirectProcessor = useMemo( () => ( { @@ -433,6 +445,16 @@ export default function CompositeCheckout( { [ dataForProcessor, getThankYouUrl, siteSlug ] ); + const domainDetails = useMemo( + () => + getDomainDetails( { + includeDomainDetails, + includeGSuiteDetails, + } ), + [ includeGSuiteDetails, includeDomainDetails ] + ); + const postalCode = getPostalCode(); + const paymentProcessors = useMemo( () => ( { 'apple-pay': ( transactionData: unknown ) => @@ -466,7 +488,16 @@ export default function CompositeCheckout( { 'full-credits': ( transactionData: unknown ) => fullCreditsProcessor( transactionData, dataForProcessor, transactionOptions ), 'existing-card': ( transactionData: unknown ) => - existingCardProcessor( transactionData, dataForProcessor, transactionOptions ), + existingCardProcessor( + mergeIfObjects( transactionData, { + country: countryCode, + postalCode, + subdivisionCode, + siteId: siteId ? String( siteId ) : undefined, + domainDetails, + } ), + dataForProcessor + ), paypal: ( transactionData: unknown ) => payPalProcessor( transactionData, @@ -474,7 +505,18 @@ export default function CompositeCheckout( { transactionOptions ), } ), - [ couponItem, dataForProcessor, dataForRedirectProcessor, getThankYouUrl, transactionOptions ] + [ + siteId, + couponItem, + dataForProcessor, + dataForRedirectProcessor, + getThankYouUrl, + transactionOptions, + countryCode, + subdivisionCode, + postalCode, + domainDetails, + ] ); const jetpackColors = isJetpackNotAtomic diff --git a/client/my-sites/checkout/composite-checkout/lib/existing-card-processor.ts b/client/my-sites/checkout/composite-checkout/lib/existing-card-processor.ts new file mode 100644 index 00000000000000..66d1c110385c1b --- /dev/null +++ b/client/my-sites/checkout/composite-checkout/lib/existing-card-processor.ts @@ -0,0 +1,101 @@ +/** + * External dependencies + */ +import debugFactory from 'debug'; +import { makeSuccessResponse, makeRedirectResponse } from '@automattic/composite-checkout'; +import { confirmStripePaymentIntent } from '@automattic/calypso-stripe'; +import type { PaymentProcessorResponse } from '@automattic/composite-checkout'; + +/** + * Internal dependencies + */ +import { createTransactionEndpointRequestPayloadFromLineItems } from './translate-cart'; +import { wpcomTransaction } from '../payment-method-helpers'; +import type { PaymentProcessorOptions } from '../types/payment-processors'; +import type { ExistingCardTransactionRequestWithLineItems } from '../types/transaction-endpoint'; + +const debug = debugFactory( 'calypso:composite-checkout:payment-method-helpers' ); + +export default async function existingCardProcessor( + transactionData: unknown, + dataForProcessor: PaymentProcessorOptions +): Promise< PaymentProcessorResponse > { + if ( ! isValidTransactionData( transactionData ) ) { + throw new Error( 'Required purchase data is missing' ); + } + const { stripeConfiguration, recordEvent } = dataForProcessor; + if ( ! stripeConfiguration ) { + throw new Error( 'Stripe configuration is required' ); + } + return submitExistingCardPayment( transactionData, dataForProcessor ) + .then( ( stripeResponse ) => { + if ( stripeResponse?.message?.payment_intent_client_secret ) { + // 3DS authentication required + recordEvent( { type: 'SHOW_MODAL_AUTHORIZATION' } ); + return confirmStripePaymentIntent( + stripeConfiguration, + stripeResponse?.message?.payment_intent_client_secret + ); + } + return stripeResponse; + } ) + .then( ( stripeResponse ) => { + if ( stripeResponse?.redirect_url ) { + return makeRedirectResponse( stripeResponse.redirect_url ); + } + return makeSuccessResponse( stripeResponse ); + } ); +} + +async function submitExistingCardPayment( + transactionData: ExistingCardTransactionRequest, + transactionOptions: PaymentProcessorOptions +) { + debug( 'formatting existing card transaction', transactionData ); + const formattedTransactionData = createTransactionEndpointRequestPayloadFromLineItems( { + ...transactionData, + paymentMethodType: 'WPCOM_Billing_MoneyPress_Stored', + } ); + debug( 'submitting existing card transaction', formattedTransactionData ); + + return wpcomTransaction( formattedTransactionData, transactionOptions ); +} + +type ExistingCardTransactionRequest = Omit< + ExistingCardTransactionRequestWithLineItems, + 'paymentMethodType' +>; + +function isValidTransactionData( + submitData: unknown +): submitData is ExistingCardTransactionRequest { + const data = submitData as ExistingCardTransactionRequest; + if ( ! ( data?.items?.length > 0 ) ) { + throw new Error( 'Transaction requires items and none were provided' ); + } + // Validate data required for this payment method type. Some other data may + // be required by the server but not required here since the server will give + // a better localized error message than we can provide. + if ( ! data.siteId ) { + throw new Error( 'Transaction requires siteId and none was provided' ); + } + if ( ! data.country ) { + throw new Error( 'Transaction requires country code and none was provided' ); + } + if ( ! data.postalCode ) { + throw new Error( 'Transaction requires postal code and none was provided' ); + } + if ( ! data.storedDetailsId ) { + throw new Error( 'Transaction requires saved card information and none was provided' ); + } + if ( ! data.name ) { + throw new Error( 'Transaction requires cardholder name and none was provided' ); + } + if ( ! data.paymentMethodToken ) { + throw new Error( 'Transaction requires a Stripe token and none was provided' ); + } + if ( ! data.paymentPartnerProcessorId ) { + throw new Error( 'Transaction requires a processor id and none was provided' ); + } + return true; +} diff --git a/client/my-sites/checkout/composite-checkout/lib/get-domain-details.ts b/client/my-sites/checkout/composite-checkout/lib/get-domain-details.ts new file mode 100644 index 00000000000000..8f04ae1de4a31a --- /dev/null +++ b/client/my-sites/checkout/composite-checkout/lib/get-domain-details.ts @@ -0,0 +1,30 @@ +/** + * External dependencies + */ +import { defaultRegistry } from '@automattic/composite-checkout'; +import type { DomainContactDetails } from '@automattic/shopping-cart'; + +/** + * Internal dependencies + */ +import { prepareDomainContactDetailsForTransaction } from 'calypso/my-sites/checkout/composite-checkout/types/wpcom-store-state'; +import type { ManagedContactDetails } from '../types/wpcom-store-state'; + +const { select } = defaultRegistry; + +export default function getDomainDetails( { + includeDomainDetails, + includeGSuiteDetails, +}: { + includeDomainDetails: boolean; + includeGSuiteDetails: boolean; +} ): DomainContactDetails | undefined { + const managedContactDetails: ManagedContactDetails | undefined = select( + 'wpcom' + )?.getContactInfo(); + if ( ! managedContactDetails ) { + return undefined; + } + const domainDetails = prepareDomainContactDetailsForTransaction( managedContactDetails ); + return includeDomainDetails || includeGSuiteDetails ? domainDetails : undefined; +} diff --git a/client/my-sites/checkout/composite-checkout/lib/get-postal-code.ts b/client/my-sites/checkout/composite-checkout/lib/get-postal-code.ts new file mode 100644 index 00000000000000..3cb0789a9987a2 --- /dev/null +++ b/client/my-sites/checkout/composite-checkout/lib/get-postal-code.ts @@ -0,0 +1,24 @@ +/** + * External dependencies + */ +import { defaultRegistry } from '@automattic/composite-checkout'; + +/** + * Internal dependencies + */ +import { tryToGuessPostalCodeFormat } from 'calypso/lib/postal-code'; +import type { ManagedContactDetails } from '../types/wpcom-store-state'; + +const { select } = defaultRegistry; + +export default function getPostalCode(): string { + const managedContactDetails: ManagedContactDetails | undefined = select( + 'wpcom' + )?.getContactInfo(); + if ( ! managedContactDetails ) { + return ''; + } + const countryCode = managedContactDetails.countryCode?.value ?? ''; + const postalCode = managedContactDetails.postalCode?.value ?? ''; + return tryToGuessPostalCodeFormat( postalCode.toUpperCase(), countryCode ); +} diff --git a/client/my-sites/checkout/composite-checkout/lib/merge-if-objects.ts b/client/my-sites/checkout/composite-checkout/lib/merge-if-objects.ts new file mode 100644 index 00000000000000..d41a1f67644e3d --- /dev/null +++ b/client/my-sites/checkout/composite-checkout/lib/merge-if-objects.ts @@ -0,0 +1,10 @@ +export default function mergeIfObjects( ...objects: unknown[] ): MergedObject { + return objects.reduce( ( merged: MergedObject, obj: unknown ): MergedObject => { + if ( typeof obj !== 'object' ) { + return merged; + } + return { ...merged, ...obj }; + }, {} ); +} + +type MergedObject = Record< string, unknown >; diff --git a/client/my-sites/checkout/composite-checkout/lib/translate-cart.ts b/client/my-sites/checkout/composite-checkout/lib/translate-cart.ts index a0ae794a99eff3..39e143ea1fde74 100644 --- a/client/my-sites/checkout/composite-checkout/lib/translate-cart.ts +++ b/client/my-sites/checkout/composite-checkout/lib/translate-cart.ts @@ -3,6 +3,7 @@ */ import { translate } from 'i18n-calypso'; import { ResponseCart, ResponseCartProduct } from '@automattic/shopping-cart'; +import debugFactory from 'debug'; /** * Internal dependencies @@ -22,6 +23,16 @@ import { isPlan, isDomainTransferProduct, isDomainProduct } from 'calypso/lib/pr import { isRenewal } from 'calypso/lib/cart-values/cart-items'; import doesValueExist from './does-value-exist'; import doesPurchaseHaveFullCredits from './does-purchase-have-full-credits'; +import type { DomainContactDetails } from '../types/backend/domain-contact-details-components'; +import type { + WPCOMTransactionEndpointCart, + WPCOMTransactionEndpointCartItem, + WPCOMTransactionEndpointRequestPayload, + TransactionRequestWithLineItems, +} from '../types/transaction-endpoint'; +import { isGSuiteProductSlug } from 'calypso/lib/gsuite'; + +const debug = debugFactory( 'calypso:composite-checkout:translate-cart' ); /** * Translate a cart object as returned by the WPCOM cart endpoint to @@ -266,3 +277,154 @@ function translateReponseCartProductToWPCOMCartItem( export function getNonProductWPCOMCartItemTypes(): string[] { return [ 'tax', 'coupon', 'total', 'subtotal', 'credits', 'savings' ]; } + +// Create cart object as required by the WPCOM transactions endpoint +// '/me/transactions/': WPCOM_JSON_API_Transactions_Endpoint +export function createTransactionEndpointCartFromLineItems( { + siteId, + couponId, + country, + postalCode, + subdivisionCode, + items, + contactDetails, +}: { + siteId: string | undefined; + couponId?: string; + country: string; + postalCode: string; + subdivisionCode?: string; + items: WPCOMCartItem[]; + contactDetails: DomainContactDetails | null; +} ): WPCOMTransactionEndpointCart { + debug( 'creating cart from items', items ); + + const currency: string = items.reduce( + ( firstValue: string, item ) => firstValue || item.amount.currency, + '' + ); + + return { + blog_id: siteId || '0', + cart_key: siteId || 'no-site', + create_new_blog: siteId ? false : true, + coupon: couponId || '', + currency: currency || '', + temporary: false, + extra: [], + products: items + .filter( ( product ) => ! getNonProductWPCOMCartItemTypes().includes( product.type ) ) + .map( ( item ) => addRegistrationDataToGSuiteItem( item, contactDetails ) ) + .map( createTransactionEndpointCartItemFromLineItem ), + tax: { + location: { + country_code: country, + postal_code: postalCode, + subdivision_code: subdivisionCode, + }, + }, + }; +} + +function createTransactionEndpointCartItemFromLineItem( + item: WPCOMCartItem +): WPCOMTransactionEndpointCartItem { + return { + product_id: item.wpcom_meta?.product_id, + meta: item.wpcom_meta?.meta, + currency: item.amount.currency, + volume: item.wpcom_meta?.volume ?? 1, + quantity: item.wpcom_meta?.quantity ?? null, + extra: item.wpcom_meta?.extra, + } as WPCOMTransactionEndpointCartItem; +} + +function addRegistrationDataToGSuiteItem( + item: WPCOMCartItem, + contactDetails: DomainContactDetails | null +): WPCOMCartItem { + if ( ! isGSuiteProductSlug( item.wpcom_meta?.product_slug ) ) { + return item; + } + return { + ...item, + wpcom_meta: { + ...item.wpcom_meta, + extra: { ...item.wpcom_meta.extra, google_apps_registration_data: contactDetails }, + }, + } as WPCOMCartItem; +} + +export function createTransactionEndpointRequestPayloadFromLineItems( { + siteId, + couponId, + country, + state, + postalCode, + subdivisionCode, + city, + address, + streetNumber, + phoneNumber, + document, + deviceId, + domainDetails, + items, + paymentMethodType, + paymentMethodToken, + paymentPartnerProcessorId, + storedDetailsId, + name, + email, + cancelUrl, + successUrl, + idealBank, + tefBank, + pan, + gstin, + nik, +}: TransactionRequestWithLineItems ): WPCOMTransactionEndpointRequestPayload { + return { + cart: createTransactionEndpointCartFromLineItems( { + siteId, + couponId: couponId || getCouponIdFromProducts( items ), + country, + postalCode, + subdivisionCode, + items: items.filter( ( item ) => item.type !== 'tax' ), + contactDetails: domainDetails || {}, + } ), + domainDetails, + payment: { + paymentMethod: paymentMethodType, + paymentKey: paymentMethodToken, + paymentPartner: paymentPartnerProcessorId, + storedDetailsId, + name, + email, + country, + countryCode: country, + state, + postalCode, + zip: postalCode, // TODO: do we need this in addition to postalCode? + city, + address, + streetNumber, + phoneNumber, + document, + deviceId, + successUrl, + cancelUrl, + idealBank, + tefBank, + pan, + gstin, + nik, + }, + }; +} + +function getCouponIdFromProducts( items: WPCOMCartItem[] ): string | undefined { + const couponItem = items.find( ( item ) => item.type === 'coupon' ); + return couponItem?.wpcom_meta?.couponCode; +} diff --git a/client/my-sites/checkout/composite-checkout/payment-method-helpers.js b/client/my-sites/checkout/composite-checkout/payment-method-helpers.js index 604cf4238eea34..a4bccf04146e0a 100644 --- a/client/my-sites/checkout/composite-checkout/payment-method-helpers.js +++ b/client/my-sites/checkout/composite-checkout/payment-method-helpers.js @@ -13,8 +13,6 @@ import wp from 'calypso/lib/wp'; import { createTransactionEndpointRequestPayloadFromLineItems } from './types/transaction-endpoint'; import { createPayPalExpressEndpointRequestPayloadFromLineItems } from './types/paypal-express'; import { translateCheckoutPaymentMethodToWpcomPaymentMethod } from './lib/translate-payment-method-names'; -import { prepareDomainContactDetailsForTransaction } from 'calypso/my-sites/checkout/composite-checkout/types/wpcom-store-state'; -import { tryToGuessPostalCodeFormat } from 'calypso/lib/postal-code'; import { getSavedVariations } from 'calypso/lib/abtest'; import { stringifyBody } from 'calypso/state/login/utils'; import { recordGoogleRecaptchaAction } from 'calypso/lib/analytics/recaptcha'; @@ -22,16 +20,6 @@ import { recordGoogleRecaptchaAction } from 'calypso/lib/analytics/recaptcha'; const debug = debugFactory( 'calypso:composite-checkout:payment-method-helpers' ); const { select } = defaultRegistry; -export async function submitExistingCardPayment( transactionData, submit, transactionOptions ) { - debug( 'formatting existing card transaction', transactionData ); - const formattedTransactionData = createTransactionEndpointRequestPayloadFromLineItems( { - ...transactionData, - paymentMethodType: 'WPCOM_Billing_MoneyPress_Stored', - } ); - debug( 'submitting existing card transaction', formattedTransactionData ); - return submit( formattedTransactionData, transactionOptions ); -} - export async function submitApplePayPayment( transactionData, submit, transactionOptions ) { debug( 'formatting apple-pay transaction', transactionData ); const formattedTransactionData = createTransactionEndpointRequestPayloadFromLineItems( { @@ -51,12 +39,6 @@ export async function submitPayPalExpressRequest( transactionData, submit, trans return submit( formattedTransactionData, transactionOptions ); } -export function getDomainDetails( { includeDomainDetails, includeGSuiteDetails } ) { - const managedContactDetails = select( 'wpcom' )?.getContactInfo?.() ?? {}; - const domainDetails = prepareDomainContactDetailsForTransaction( managedContactDetails ); - return includeDomainDetails || includeGSuiteDetails ? domainDetails : null; -} - export async function fetchStripeConfiguration( requestArgs, wpcom ) { return wpcom.stripeConfiguration( requestArgs ); } @@ -273,9 +255,3 @@ export function createStripePaymentMethodToken( { stripe, name, country, postalC }, } ); } - -export function getPostalCode() { - const countryCode = select( 'wpcom' )?.getContactInfo?.()?.countryCode?.value ?? ''; - const postalCode = select( 'wpcom' )?.getContactInfo?.()?.postalCode?.value ?? ''; - return tryToGuessPostalCodeFormat( postalCode.toUpperCase(), countryCode ); -} diff --git a/client/my-sites/checkout/composite-checkout/payment-method-processors.js b/client/my-sites/checkout/composite-checkout/payment-method-processors.js index 725e9711be7e0a..eb83d262914ade 100644 --- a/client/my-sites/checkout/composite-checkout/payment-method-processors.js +++ b/client/my-sites/checkout/composite-checkout/payment-method-processors.js @@ -14,8 +14,6 @@ import { confirmStripePaymentIntent } from '@automattic/calypso-stripe'; * Internal dependencies */ import { - getPostalCode, - getDomainDetails, createStripePaymentMethodToken, wpcomTransaction, wpcomPayPalExpress, @@ -25,9 +23,10 @@ import { submitRedirectTransaction, submitFreePurchaseTransaction, submitCreditsTransaction, - submitExistingCardPayment, submitPayPalExpressRequest, } from './payment-method-helpers'; +import getPostalCode from './lib/get-postal-code'; +import getDomainDetails from './lib/get-domain-details'; import { createEbanxToken } from 'calypso/lib/store-transactions'; import userAgent from 'calypso/lib/user-agent'; @@ -240,42 +239,6 @@ export async function multiPartnerCardProcessor( throw new RangeError( 'Unrecognized card payment partner: "' + paymentPartner + '"' ); } -export async function existingCardProcessor( - submitData, - { includeDomainDetails, includeGSuiteDetails, recordEvent }, - transactionOptions -) { - return submitExistingCardPayment( - { - ...submitData, - country: select( 'wpcom' )?.getContactInfo?.()?.countryCode?.value, - postalCode: getPostalCode(), - subdivisionCode: select( 'wpcom' )?.getContactInfo?.()?.state?.value, - siteId: select( 'wpcom' )?.getSiteId?.(), - domainDetails: getDomainDetails( { includeDomainDetails, includeGSuiteDetails } ), - }, - wpcomTransaction, - transactionOptions - ) - .then( ( stripeResponse ) => { - if ( stripeResponse?.message?.payment_intent_client_secret ) { - // 3DS authentication required - recordEvent( { type: 'SHOW_MODAL_AUTHORIZATION' } ); - return confirmStripePaymentIntent( - submitData.stripeConfiguration, - stripeResponse?.message?.payment_intent_client_secret - ); - } - return stripeResponse; - } ) - .then( ( stripeResponse ) => { - if ( stripeResponse?.redirect_url ) { - return makeRedirectResponse( stripeResponse.redirect_url ); - } - return makeSuccessResponse( stripeResponse ); - } ); -} - export async function freePurchaseProcessor( submitData, { includeDomainDetails, includeGSuiteDetails } diff --git a/client/my-sites/checkout/composite-checkout/types/payment-processors.ts b/client/my-sites/checkout/composite-checkout/types/payment-processors.ts new file mode 100644 index 00000000000000..f9216afa484553 --- /dev/null +++ b/client/my-sites/checkout/composite-checkout/types/payment-processors.ts @@ -0,0 +1,17 @@ +/** + * External dependencies + */ +import type { StripeConfiguration } from '@automattic/calypso-stripe'; + +/** + * Internal dependencies + */ +import type { ReactStandardAction } from '../types/analytics'; + +export interface PaymentProcessorOptions { + includeDomainDetails: boolean; + includeGSuiteDetails: boolean; + createUserAndSiteBeforeTransaction: boolean; + stripeConfiguration: StripeConfiguration | null; + recordEvent: ( action: ReactStandardAction ) => void; +} diff --git a/client/my-sites/checkout/composite-checkout/types/transaction-endpoint.ts b/client/my-sites/checkout/composite-checkout/types/transaction-endpoint.ts index 7fb5f66748b85e..a55a03147e800f 100644 --- a/client/my-sites/checkout/composite-checkout/types/transaction-endpoint.ts +++ b/client/my-sites/checkout/composite-checkout/types/transaction-endpoint.ts @@ -1,19 +1,60 @@ /** * External dependencies */ -import debugFactory from 'debug'; -import { ResponseCartProductExtra } from '@automattic/shopping-cart'; +import type { ResponseCartProductExtra } from '@automattic/shopping-cart'; /** * Internal dependencies */ -import { getNonProductWPCOMCartItemTypes } from 'calypso/my-sites/checkout/composite-checkout/lib/translate-cart'; import type { WPCOMCartItem } from './checkout-cart'; import type { Purchase } from './wpcom-store-state'; import type { DomainContactDetails } from './backend/domain-contact-details-components'; -import { isGSuiteProductSlug } from 'calypso/lib/gsuite'; -const debug = debugFactory( 'calypso:composite-checkout:transaction-endpoint' ); +// The data required by createTransactionEndpointRequestPayloadFromLineItems +export interface TransactionRequestWithLineItems { + country: string; + postalCode: string; + items: WPCOMCartItem[]; + paymentMethodType: string; + name: string; + siteId?: string | undefined; + couponId?: string | undefined; + state?: string | undefined; + subdivisionCode?: string | undefined; + city?: string | undefined; + address?: string | undefined; + streetNumber?: string | undefined; + phoneNumber?: string | undefined; + document?: string | undefined; + deviceId?: string | undefined; + domainDetails?: DomainContactDetails | undefined; + paymentMethodToken?: string | undefined; + paymentPartnerProcessorId?: string | undefined; + storedDetailsId?: string | undefined; + email?: string | undefined; + successUrl?: string | undefined; + cancelUrl?: string | undefined; + idealBank?: string | undefined; + tefBank?: string | undefined; + pan?: string | undefined; + gstin?: string | undefined; + nik?: string | undefined; +} + +export type ExistingCardTransactionRequestWithLineItems = Partial< TransactionRequestWithLineItems > & + Required< + Pick< + TransactionRequestWithLineItems, + | 'country' + | 'postalCode' + | 'items' + | 'name' + | 'storedDetailsId' + | 'siteId' + | 'paymentMethodToken' + | 'paymentPartnerProcessorId' + > + >; export type WPCOMTransactionEndpoint = ( _: WPCOMTransactionEndpointRequestPayload @@ -72,7 +113,7 @@ export type WPCOMTransactionEndpointCart = { }; }; -type WPCOMTransactionEndpointCartItem = { +export type WPCOMTransactionEndpointCartItem = { product_id: number; meta?: string; currency: string; @@ -80,185 +121,6 @@ type WPCOMTransactionEndpointCartItem = { extra?: ResponseCartProductExtra; }; -// Create cart object as required by the WPCOM transactions endpoint -// '/me/transactions/': WPCOM_JSON_API_Transactions_Endpoint -export function createTransactionEndpointCartFromLineItems( { - siteId, - couponId, - country, - postalCode, - subdivisionCode, - items, - contactDetails, -}: { - siteId: string; - couponId?: string; - country: string; - postalCode: string; - subdivisionCode?: string; - items: WPCOMCartItem[]; - contactDetails: DomainContactDetails | null; -} ): WPCOMTransactionEndpointCart { - debug( 'creating cart from items', items ); - - const currency: string = items.reduce( - ( firstValue: string, item ) => firstValue || item.amount.currency, - '' - ); - - return { - blog_id: siteId || '0', - cart_key: siteId || 'no-site', - create_new_blog: siteId ? false : true, - coupon: couponId || '', - currency: currency || '', - temporary: false, - extra: [], - products: items - .filter( ( product ) => ! getNonProductWPCOMCartItemTypes().includes( product.type ) ) - .map( ( item ) => addRegistrationDataToGSuiteItem( item, contactDetails ) ) - .map( createTransactionEndpointCartItemFromLineItem ), - tax: { - location: { - country_code: country, - postal_code: postalCode, - subdivision_code: subdivisionCode, - }, - }, - }; -} - -function createTransactionEndpointCartItemFromLineItem( - item: WPCOMCartItem -): WPCOMTransactionEndpointCartItem { - return { - product_id: item.wpcom_meta?.product_id, - meta: item.wpcom_meta?.meta, - currency: item.amount.currency, - volume: item.wpcom_meta?.volume ?? 1, - quantity: item.wpcom_meta?.quantity ?? null, - extra: item.wpcom_meta?.extra, - } as WPCOMTransactionEndpointCartItem; -} - -function addRegistrationDataToGSuiteItem( - item: WPCOMCartItem, - contactDetails: DomainContactDetails | null -): WPCOMCartItem { - if ( ! isGSuiteProductSlug( item.wpcom_meta?.product_slug ) ) { - return item; - } - return { - ...item, - wpcom_meta: { - ...item.wpcom_meta, - extra: { ...item.wpcom_meta.extra, google_apps_registration_data: contactDetails }, - }, - } as WPCOMCartItem; -} - -export function createTransactionEndpointRequestPayloadFromLineItems( { - siteId, - couponId, - country, - state, - postalCode, - subdivisionCode, - city, - address, - streetNumber, - phoneNumber, - document, - deviceId, - domainDetails, - items, - paymentMethodType, - paymentMethodToken, - paymentPartnerProcessorId, - storedDetailsId, - name, - email, - cancelUrl, - successUrl, - idealBank, - tefBank, - pan, - gstin, - nik, -}: { - siteId: string; - couponId?: string; - country: string; - state?: string; - postalCode: string; - subdivisionCode?: string; - city?: string; - address?: string; - streetNumber?: string; - phoneNumber?: string; - document?: string; - deviceId?: string; - domainDetails?: DomainContactDetails; - items: WPCOMCartItem[]; - paymentMethodType: string; - paymentMethodToken?: string; - paymentPartnerProcessorId?: string; - storedDetailsId?: string; - name: string; - email?: string; - successUrl?: string; - cancelUrl?: string; - idealBank?: string; - tefBank?: string; - pan?: string; - gstin?: string; - nik?: string; -} ): WPCOMTransactionEndpointRequestPayload { - return { - cart: createTransactionEndpointCartFromLineItems( { - siteId, - couponId: couponId || getCouponIdFromProducts( items ), - country, - postalCode, - subdivisionCode, - items: items.filter( ( item ) => item.type !== 'tax' ), - contactDetails: domainDetails || {}, - } ), - domainDetails, - payment: { - paymentMethod: paymentMethodType, - paymentKey: paymentMethodToken, - paymentPartner: paymentPartnerProcessorId, - storedDetailsId, - name, - email, - country, - countryCode: country, - state, - postalCode, - zip: postalCode, // TODO: do we need this in addition to postalCode? - city, - address, - streetNumber, - phoneNumber, - document, - deviceId, - successUrl, - cancelUrl, - idealBank, - tefBank, - pan, - gstin, - nik, - }, - }; -} - -function getCouponIdFromProducts( items: WPCOMCartItem[] ): string | undefined { - const couponItem = items.find( ( item ) => item.type === 'coupon' ); - return couponItem?.wpcom_meta?.couponCode; -} - export type WPCOMTransactionEndpointResponse = { success: boolean; error_code: string; diff --git a/client/my-sites/checkout/composite-checkout/use-create-payment-methods.js b/client/my-sites/checkout/composite-checkout/use-create-payment-methods.js index 3909b32b2e1969..899cff7cced493 100644 --- a/client/my-sites/checkout/composite-checkout/use-create-payment-methods.js +++ b/client/my-sites/checkout/composite-checkout/use-create-payment-methods.js @@ -290,7 +290,6 @@ function useCreateApplePay( { export function useCreateExistingCards( { storedCards, - stripeConfiguration, activePayButtonText = undefined, } ) { const existingCardMethods = useMemo( () => { @@ -304,11 +303,10 @@ export function useCreateExistingCards( { storedDetailsId: storedDetails.stored_details_id, paymentMethodToken: storedDetails.mp_ref, paymentPartnerProcessorId: storedDetails.payment_partner, - stripeConfiguration, activePayButtonText, } ) ); - }, [ stripeConfiguration, storedCards, activePayButtonText ] ); + }, [ storedCards, activePayButtonText ] ); return existingCardMethods; } diff --git a/packages/composite-checkout/README.md b/packages/composite-checkout/README.md index 3a165b196f8970..ed511bb70032b2 100644 --- a/packages/composite-checkout/README.md +++ b/packages/composite-checkout/README.md @@ -376,21 +376,6 @@ Creates a [Payment Method](#payment-methods) object. Requires passing an object Creates a [data store](#data-stores) registry to be passed (optionally) to [CheckoutProvider](#checkoutprovider). See the `@wordpress/data` [docs for this function](https://developer.wordpress.org/block-editor/packages/packages-data/#createRegistry). -### createExistingCardMethod - -Creates a [Payment Method](#payment-methods) object for an existing credit card. Requires passing an object with the following properties: - -- `registerStore: object => object`. The `registerStore` function from the return value of [createRegistry](#createRegistry). -- `submitTransaction: async ?object => object`. An async function that sends the request to the endpoint process the payment. -- `getCountry: () => string`. A function that returns the country to use for the transaction. -- `getPostalCode: () => string`. A function that returns the postal code for the transaction. -- `getSubdivisionCode: () => string`. A function that returns the subdivision code for the transaction. -- `id: string`. A unique id for this payment method (since there are likely to be several existing cards). -- `cardholderName: string`. The cardholder's name. Used for display only. -- `cardExpiry: string`. The card's expiry date. Used for display only. -- `brand: string`. The card's brand (eg: `visa`). Used for display only. -- `last4: string`. The card's last four digits. Used for display only. - ### createPayPalMethod Creates a [Payment Method](#payment-methods) object. Requires passing an object with the following properties: diff --git a/packages/composite-checkout/src/lib/payment-methods/existing-credit-card.js b/packages/composite-checkout/src/lib/payment-methods/existing-credit-card.js index af89f757aad934..b5f2628c792bb0 100644 --- a/packages/composite-checkout/src/lib/payment-methods/existing-credit-card.js +++ b/packages/composite-checkout/src/lib/payment-methods/existing-credit-card.js @@ -27,7 +27,6 @@ export function createExistingCardMethod( { storedDetailsId, paymentMethodToken, paymentPartnerProcessorId, - stripeConfiguration, activePayButtonText = undefined, } ) { debug( 'creating a new existing credit card payment method', { @@ -50,7 +49,6 @@ export function createExistingCardMethod( { ), submitButton: (